From 70c48cc3774cb6512ecd63d1f8abfa86cb87455c Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Fri, 3 Apr 2026 15:43:00 -0600 Subject: [PATCH] feat(15-01): extend createSigningToken with signerEmail, add signer-download token pair - createSigningToken now accepts optional signerEmail param and persists to DB - Added createSignerDownloadToken (72h TTL, purpose: signer-download) - Added verifySignerDownloadToken with purpose claim validation --- .../src/lib/signing/token.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/teressa-copeland-homes/src/lib/signing/token.ts b/teressa-copeland-homes/src/lib/signing/token.ts index fa719cf..4606099 100644 --- a/teressa-copeland-homes/src/lib/signing/token.ts +++ b/teressa-copeland-homes/src/lib/signing/token.ts @@ -4,7 +4,7 @@ import { signingTokens } from '@/lib/db/schema'; const getSecret = () => new TextEncoder().encode(process.env.SIGNING_JWT_SECRET!); -export async function createSigningToken(documentId: string): Promise<{ token: string; jti: string; expiresAt: Date }> { +export async function createSigningToken(documentId: string, signerEmail?: string): Promise<{ token: string; jti: string; expiresAt: Date }> { const jti = crypto.randomUUID(); const expiresAt = new Date(Date.now() + 72 * 60 * 60 * 1000); // 72 hours @@ -19,6 +19,7 @@ export async function createSigningToken(documentId: string): Promise<{ token: s await db.insert(signingTokens).values({ jti, documentId, + signerEmail: signerEmail ?? null, expiresAt, }); @@ -62,3 +63,19 @@ export async function verifyAgentDownloadToken(token: string): Promise<{ documen if (payload['purpose'] !== 'agent-download') throw new Error('Not an agent download token'); return { documentId: payload['documentId'] as string }; } + +// Signer download token — purpose: 'signer-download', 72h TTL, no DB record +// Sent to signers on completion so they can download the fully signed PDF. +export async function createSignerDownloadToken(documentId: string): Promise { + return await new SignJWT({ documentId, purpose: 'signer-download' }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('72h') + .sign(getSecret()); +} + +export async function verifySignerDownloadToken(token: string): Promise<{ documentId: string }> { + const { payload } = await jwtVerify(token, getSecret()); + if (payload['purpose'] !== 'signer-download') throw new Error('Not a signer download token'); + return { documentId: payload['documentId'] as string }; +}