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
This commit is contained in:
Chandler Copeland
2026-04-03 15:43:00 -06:00
parent cdd4b8b38c
commit 70c48cc377

View File

@@ -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<string> {
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 };
}