Skip to content

feat: quote template and render token security - #2

Open
christopher-talke wants to merge 3 commits into
devfrom
feature/quote-template-and-render-token
Open

feat: quote template and render token security#2
christopher-talke wants to merge 3 commits into
devfrom
feature/quote-template-and-render-token

Conversation

@christopher-talke

@christopher-talke christopher-talke commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a quick-quote EJS template - a compact 1-2 page PDF alternative to the full proposal template, covering product line items, totals summary, and an authorization signature block
  • Add GET /api/v1/templates endpoint on the templater that discovers available templates via metadata.json in each template folder (filtered by active: true)
  • Implement short-lived render tokens (5 min, scoped to proposalId + tenantId) so the templater can only access the specific proposal it was asked to render - no broad service token access to proposal data
  • Add GET /api/v1/render/proposal/:id on the server - a minimal endpoint only reachable with a valid render token, no user auth or service token bypass

Security model

The templater now receives a x-render-token JWT alongside the existing x-service-token. The render token is signed by the server at request time, scoped to one proposal + tenant, and expires in 5 minutes. The templater threads this token through the Puppeteer render chain and uses it to call the new /render/proposal/:id endpoint - the only server endpoint it needs. A malicious EJS template running in the templater process can at most re-request the same proposal it was already authorised to render.

Test plan

  • GET /api/v1/templates returns only templates with active: true in their metadata.json
  • PDF download works end-to-end with the render token flow (no regression on default template)
  • Quick-quote template renders correctly as a 1-2 page PDF
  • Requests to GET /api/v1/render/proposal/:id without a token return 401
  • Render token for proposal A rejected on proposal B (403 Forbidden)
  • Render token expires after 5 minutes

claude added 3 commits April 2, 2026 13:12
- Add active: boolean to metadata.json; GET /api/v1/templates filters inactive templates out
- Add RenderTokenPayload + signRenderToken/verifyRenderToken to jwt.ts (5 min expiry, scoped to proposalId + tenantId)
- Add requireRenderToken middleware
- Add GET /api/v1/render/proposal/:id - minimal endpoint only accessible with a valid render token; no user auth, no service token
- Server PDF endpoint generates a render token and passes it to the templater as x-render-token
- Templater /download/ validates and threads the render token through generatePdf() into the Puppeteer URL
- getProposalData() uses the render token against /api/v1/render/proposal/:id instead of the broad service token path

https://claude.ai/code/session_01HgpVh7XS9BHcU2YAJeJ1L8

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@christopher-talke
christopher-talke changed the base branch from main to dev April 3, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants