mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 14:52:18 +10:00
feat(mcp): add cover-letter PDF downloads (#3304)
* feat(mcp): add cover-letter PDF downloads * fix(mcp): bind signed PDF targets * test(mcp): cover unavailable cover letters * fix(api): accept legacy PDF download targets * fix(server): limit legacy PDF tokens to resumes --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
co-authored by
Amruth Pillai
parent
45303fb465
commit
6d9ebccc63
@@ -1,3 +1,4 @@
|
||||
import { createHmac } from "node:crypto";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@reactive-resume/env/server", () => ({
|
||||
@@ -43,11 +44,12 @@ describe("resume PDF signed download URLs", () => {
|
||||
ok: true,
|
||||
resumeId: "resume-1",
|
||||
userId: "user-1",
|
||||
target: "resume",
|
||||
expiresAt: "2026-06-01T10:10:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("can include the cover letter target without changing token verification", () => {
|
||||
it("binds the cover letter target to the signed token", () => {
|
||||
const result = createResumePdfDownloadUrl({
|
||||
resumeId: "resume-1",
|
||||
userId: "user-1",
|
||||
@@ -65,7 +67,30 @@ describe("resume PDF signed download URLs", () => {
|
||||
token,
|
||||
now: new Date("2026-06-01T10:01:00.000Z"),
|
||||
}),
|
||||
).toMatchObject({ ok: true });
|
||||
).toMatchObject({ ok: true, target: "cover-letter" });
|
||||
});
|
||||
|
||||
it("accepts still-valid legacy tokens without a target", () => {
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({
|
||||
v: 1,
|
||||
resumeId: "resume-1",
|
||||
userId: "user-1",
|
||||
expiresAt: new Date("2026-06-01T10:10:00.000Z").getTime(),
|
||||
issuedAt: new Date("2026-06-01T10:00:00.000Z").getTime(),
|
||||
}),
|
||||
"utf8",
|
||||
).toString("base64url");
|
||||
const token = `${payload}.${createHmac("sha256", "test-secret").update(payload).digest("base64url")}`;
|
||||
|
||||
const verification = verifyResumePdfDownloadToken({
|
||||
resumeId: "resume-1",
|
||||
token,
|
||||
now: new Date("2026-06-01T10:01:00.000Z"),
|
||||
});
|
||||
|
||||
expect(verification).toMatchObject({ ok: true });
|
||||
expect("target" in verification).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects expired, tampered, and mismatched tokens", () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ type PdfDownloadTokenPayload = {
|
||||
v: 1;
|
||||
resumeId: string;
|
||||
userId: string;
|
||||
target?: ResumeExportTarget;
|
||||
expiresAt: number;
|
||||
issuedAt: number;
|
||||
};
|
||||
@@ -31,6 +32,7 @@ type VerifyResumePdfDownloadTokenResult =
|
||||
ok: true;
|
||||
resumeId: string;
|
||||
userId: string;
|
||||
target?: ResumeExportTarget;
|
||||
expiresAt: string;
|
||||
}
|
||||
| {
|
||||
@@ -69,6 +71,7 @@ function parsePayload(value: unknown): PdfDownloadTokenPayload | null {
|
||||
if (payload.v !== 1) return null;
|
||||
if (typeof payload.resumeId !== "string" || payload.resumeId.length === 0) return null;
|
||||
if (typeof payload.userId !== "string" || payload.userId.length === 0) return null;
|
||||
if (payload.target !== undefined && payload.target !== "resume" && payload.target !== "cover-letter") return null;
|
||||
if (typeof payload.expiresAt !== "number" || !Number.isFinite(payload.expiresAt)) return null;
|
||||
if (typeof payload.issuedAt !== "number" || !Number.isFinite(payload.issuedAt)) return null;
|
||||
|
||||
@@ -88,6 +91,7 @@ export function createResumePdfDownloadUrl({
|
||||
v: 1,
|
||||
resumeId,
|
||||
userId,
|
||||
target: target ?? "resume",
|
||||
expiresAt: expiresAt.getTime(),
|
||||
issuedAt: now.getTime(),
|
||||
} satisfies PdfDownloadTokenPayload);
|
||||
@@ -122,6 +126,7 @@ export function verifyResumePdfDownloadToken({
|
||||
ok: true,
|
||||
resumeId: parsed.resumeId,
|
||||
userId: parsed.userId,
|
||||
...(parsed.target ? { target: parsed.target } : {}),
|
||||
expiresAt: new Date(parsed.expiresAt).toISOString(),
|
||||
};
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user