feat: add semantic CSS stylesheets (#3274)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Amruth Pillai
2026-07-30 12:39:15 +02:00
committed by GitHub
co-authored by Cursor Agent
parent 4ac19f81b3
commit d2ffbf9618
320 changed files with 78393 additions and 2915 deletions
+3
View File
@@ -1,3 +1,4 @@
import type { StylesheetPreflightRunner } from "@reactive-resume/pdf/server";
import type { Locale } from "@reactive-resume/utils/locale";
import type { User } from "better-auth";
import { ORPCError, os } from "@orpc/server";
@@ -10,6 +11,8 @@ interface ORPCContext {
locale: Locale;
reqHeaders: Headers;
resHeaders?: Headers;
trustedClient?: string;
stylesheetPreflightRunner?: StylesheetPreflightRunner;
}
async function getUserFromBearerToken(headers: Headers): Promise<User | null> {
+165
View File
@@ -4,6 +4,110 @@ import { redactResumeForViewer } from "../features/resume/access-policy";
import { resumeDto } from "./resume";
describe("resume DTO output validation", () => {
it("normalizes ordinary PUT data without losing compatible custom-section overlap", () => {
const parsed = resumeDto.update.input.parse({
id: "resume-id",
data: {
...structuredClone(defaultResumeData),
customSections: [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [
{
id: "experience-item",
hidden: false,
company: "Analytical Engines",
position: "Programmer",
location: "London",
period: "18421843",
description: "<p>Wrote the first algorithm.</p>",
content: "<p>Compatible overlap</p>",
},
],
},
],
},
});
expect(parsed.data?.customSections[0]?.items[0]).toMatchObject({
content: "<p>Compatible overlap</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
});
it("rejects renderer-unsafe custom sections before update or import persistence", () => {
const data = {
...defaultResumeData,
customSections: [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "summary-item", hidden: false, content: "<p>Not an experience item</p>" }],
},
],
};
expect(resumeDto.update.input.safeParse({ id: "resume-id", data }).success).toBe(false);
expect(resumeDto.import.input.safeParse({ data }).success).toBe(false);
});
it("defers imported stylesheet validation to the stable unavailable-feature error", () => {
expect(
resumeDto.import.input.safeParse({
data: {
...defaultResumeData,
metadata: {
...defaultResumeData.metadata,
stylesheet: { invalid: true },
},
},
}).success,
).toBe(true);
});
it("does not let otherwise-invalid imports bypass validation without a stylesheet field", () => {
expect(resumeDto.import.input.safeParse({ data: { metadata: {} } }).success).toBe(false);
});
it.each([
[resumeDto.getById.output, {}],
[resumeDto.getBySlug.output, { stylesheetMode: "legacy" }],
[resumeDto.update.output, {}],
[resumeDto.patch.output, {}],
] as const)("keeps server concurrency columns out of ordinary resume outputs", (output, extra) => {
const parsed = output.parse({
id: "019e128d-0598-75d2-ae6a-771e2eb84614",
name: "Resume",
slug: "resume",
tags: [],
data: defaultResumeData,
isPublic: false,
isLocked: false,
updatedAt: new Date("2026-01-01T00:00:00Z"),
hasPassword: false,
stylesheetRevision: 7,
renderDataVersion: 9,
...extra,
});
expect(parsed).not.toHaveProperty("stylesheetRevision");
expect(parsed).not.toHaveProperty("renderDataVersion");
});
it("accepts public resume responses after owner-only fields are redacted", () => {
const dbResume = {
id: "019e128d-0598-75d2-ae6a-771e2eb84614",
@@ -26,10 +130,71 @@ describe("resume DTO output validation", () => {
const publicResume = {
...redactResumeForViewer(dbResume, false),
hasPassword: dbResume.hasPassword,
stylesheetMode: "legacy",
};
expect(publicResume.name).toBe("Resume");
expect(publicResume.data.metadata.notes).toBe("");
expect(resumeDto.getBySlug.output.safeParse(publicResume).success).toBe(true);
});
it("exposes only the safe public stylesheet mode discriminator", () => {
const source = { languageVersion: 1, text: "@version 1;\nresume { color: red; }\n" };
const parsed = resumeDto.getBySlug.output.parse({
id: "019e128d-0598-75d2-ae6a-771e2eb84614",
name: "Resume",
slug: "resume",
tags: [],
data: redactResumeForViewer(
{
name: "Owner title",
data: {
...defaultResumeData,
metadata: {
...defaultResumeData.metadata,
stylesheet: { mode: "semantic", source, applied: source },
},
},
},
false,
).data,
isPublic: true,
isLocked: false,
hasPassword: false,
stylesheetMode: "semantic",
});
expect(parsed.stylesheetMode).toBe("semantic");
expect(JSON.stringify(parsed)).not.toContain("@version");
expect(JSON.stringify(parsed)).not.toContain("stylesheetRevision");
});
it("returns current canonical stylesheet state only on version restore", () => {
const resume = {
id: "019e128d-0598-75d2-ae6a-771e2eb84614",
name: "Resume",
slug: "resume",
tags: [],
data: defaultResumeData,
isPublic: false,
isLocked: false,
updatedAt: new Date("2026-01-01T00:00:00Z"),
hasPassword: false,
};
const stylesheet = {
mode: "semantic" as const,
source: { languageVersion: 1, text: "@version 1;\n" },
applied: { languageVersion: 1, text: "@version 1;\n" },
};
expect(
resumeDto.restoreVersion.output.parse({
resume,
stylesheetState: { stylesheet, revision: 8, renderDataVersion: 13 },
}),
).toEqual({
resume,
stylesheetState: { stylesheet, revision: 8, renderDataVersion: 13 },
});
});
});
+124 -3
View File
@@ -1,8 +1,18 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { createSelectSchema } from "drizzle-zod";
import z from "zod";
import * as schema from "@reactive-resume/db/schema";
import { jsonPatchOperationSchema } from "@reactive-resume/resume/patch";
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
import { semanticStylesheetSchema, stylesheetSourceSchema } from "@reactive-resume/schema/resume/stylesheet";
const importedResumeDataSchema = z.custom<ResumeData>((value) => {
if (typeof value !== "object" || value === null) return false;
const data = value as Record<string, unknown>;
if (typeof data.metadata !== "object" || data.metadata === null) return false;
const { stylesheet: _stylesheet, ...metadata } = data.metadata as Record<string, unknown>;
return resumeDataSchema.safeParse({ ...data, metadata }).success;
});
const resumeSchema = createSelectSchema(schema.resume, {
id: z.string().describe("The ID of the resume."),
@@ -16,6 +26,60 @@ const resumeSchema = createSelectSchema(schema.resume, {
userId: z.string().describe("The ID of the user who owns the resume."),
createdAt: z.date().describe("The date and time the resume was created."),
updatedAt: z.date().describe("The date and time the resume was last updated."),
}).omit({ stylesheetRevision: true, renderDataVersion: true });
const stylesheetMutationCommon = {
id: z.string().describe("The ID of the resume."),
expectedRevision: z.number().int().nonnegative(),
expectedRenderDataVersion: z.number().int().nonnegative(),
editGeneration: z.number().int().nonnegative(),
};
const stylesheetDiagnosticSchema = z.strictObject({
code: z.string(),
severity: z.enum(["error", "warning"]),
message: z.string(),
range: z.strictObject({
start: z.strictObject({
line: z.number().int().positive(),
column: z.number().int().positive(),
offset: z.number().int().nonnegative(),
}),
end: z.strictObject({
line: z.number().int().positive(),
column: z.number().int().positive(),
offset: z.number().int().nonnegative(),
}),
}),
});
const stylesheetStateSchema = z.strictObject({
stylesheet: semanticStylesheetSchema,
revision: z.number().int().nonnegative(),
renderDataVersion: z.number().int().nonnegative(),
});
const publicPdfPageSizeSchema = z.union([
z.enum(["A4", "LETTER"]),
z.strictObject({ width: z.number().finite(), height: z.number().finite().optional() }),
]);
const publicPdfNodePresentationSchema = z.strictObject({
style: z.record(z.string(), z.union([z.string(), z.number().finite(), z.null()])).optional(),
size: publicPdfPageSizeSchema.optional(),
break: z.boolean().optional(),
wrap: z.boolean().optional(),
fixed: z.boolean().optional(),
minPresenceAhead: z.number().finite().optional(),
orphans: z.number().finite().optional(),
widows: z.number().finite().optional(),
hidden: z.boolean().optional(),
order: z.number().int().nonnegative().optional(),
});
const publicStyleProjectionSchema = z.strictObject({
formatVersion: z.literal(1),
languageVersion: z.number().int().positive(),
semanticTreeVersion: z.literal(1),
registryFingerprint: z.string().regex(/^[a-f0-9]{64}$/),
adapterFingerprint: z.string().regex(/^[a-f0-9]{64}$/),
renderDataHash: z.string().regex(/^[a-f0-9]{64}$/),
nodes: z.record(z.string(), publicPdfNodePresentationSchema),
});
export const resumeDto = {
@@ -43,7 +107,12 @@ export const resumeDto = {
// the redacted public response passes output validation.
output: resumeSchema
.omit({ name: true, password: true, userId: true, createdAt: true, updatedAt: true })
.extend({ name: z.string() }),
.extend({ name: z.string(), stylesheetMode: z.enum(["legacy", "semantic"]) }),
},
getStyleProjection: {
input: z.strictObject({ username: z.string(), slug: z.string() }),
output: publicStyleProjectionSchema,
},
create: {
@@ -54,7 +123,7 @@ export const resumeDto = {
},
import: {
input: resumeSchema.pick({ data: true }),
input: z.object({ data: importedResumeDataSchema }),
output: z.string().describe("The ID of the imported resume."),
},
@@ -122,6 +191,58 @@ export const resumeDto = {
resumeId: z.string().describe("The ID of the resume to restore."),
versionId: z.string().describe("The ID of the version snapshot to restore."),
}),
output: resumeSchema.omit({ password: true, userId: true, createdAt: true }).extend({ hasPassword: z.boolean() }),
output: z.strictObject({
resume: resumeSchema.omit({ password: true, userId: true, createdAt: true }).extend({ hasPassword: z.boolean() }),
stylesheetState: stylesheetStateSchema,
}),
},
stylesheet: {
errors: {
validation: z.strictObject({
diagnostics: z.array(stylesheetDiagnosticSchema),
}),
parity: z.strictObject({
mismatches: z.array(z.string()),
}),
revisionConflict: z.strictObject({
state: stylesheetStateSchema,
}),
},
getState: {
input: z.strictObject({ id: z.string().describe("The ID of the resume.") }),
output: stylesheetStateSchema,
},
mutate: {
input: z.discriminatedUnion("transition", [
z.strictObject({
...stylesheetMutationCommon,
transition: z.literal("edit_source"),
source: stylesheetSourceSchema,
}),
z.strictObject({
...stylesheetMutationCommon,
transition: z.literal("activate"),
source: stylesheetSourceSchema,
}),
z.strictObject({
...stylesheetMutationCommon,
transition: z.literal("deactivate"),
}),
z.strictObject({
...stylesheetMutationCommon,
transition: z.literal("restore_history"),
restore: z.strictObject({
mode: z.enum(["legacy", "semantic"]),
source: stylesheetSourceSchema,
applied: stylesheetSourceSchema,
}),
}),
]),
output: stylesheetStateSchema.extend({
editGeneration: z.number().int().nonnegative(),
diagnostics: z.array(stylesheetDiagnosticSchema),
}),
},
},
};
@@ -7,6 +7,8 @@ export type FeatureFlags = {
disableEmailAuth: boolean;
showSponsors: boolean;
smtpEnabled: boolean;
semanticCssAuthoring: boolean;
semanticCssDefault: boolean;
};
// Mirrors isSmtpEnabled() in packages/email/src/transport.ts (kept local to avoid an api -> email dependency).
@@ -30,6 +32,8 @@ export const flagsRouter = {
disableEmailAuth: z.boolean().describe("Whether email-based authentication is disabled on this instance."),
showSponsors: z.boolean().describe("Whether sponsor placements are shown on this instance."),
smtpEnabled: z.boolean().describe("Whether outbound email (SMTP) is configured on this instance."),
semanticCssAuthoring: z.boolean().describe("Whether Semantic CSS authoring is enabled on this instance."),
semanticCssDefault: z.boolean().describe("Whether new resumes start in Semantic CSS mode."),
}),
)
.handler(
@@ -38,6 +42,8 @@ export const flagsRouter = {
disableEmailAuth: env.FLAG_DISABLE_EMAIL_AUTH,
showSponsors: env.FLAG_SHOW_SPONSORS,
smtpEnabled: isSmtpEnabled(),
semanticCssAuthoring: env.FLAG_SEMANTIC_CSS_AUTHORING,
semanticCssDefault: env.FLAG_SEMANTIC_CSS_DEFAULT,
}),
),
};
@@ -71,6 +71,25 @@ describe("redactResumeForViewer", () => {
expect(result.data.metadata.notes).toBe("");
});
it("strips editable and applied stylesheet source for non-owner", () => {
const source = { languageVersion: 1, text: "@version 1;\nresume { color: red; }\n" };
const resume = {
name: "Title",
data: {
...defaultResumeData,
metadata: {
...defaultResumeData.metadata,
stylesheet: { mode: "semantic" as const, source, applied: source },
},
},
};
const result = redactResumeForViewer(resume, false);
expect(result.data.metadata.stylesheet).toBeUndefined();
expect(JSON.stringify(result)).not.toContain("@version");
});
it("preserves resume.data.basics.name (the person's name) for non-owner", () => {
const resume = {
name: "Dashboard title",
@@ -53,6 +53,7 @@ export function redactResumeForViewer<T extends { name: string; data: ResumeData
viewerIsOwner: boolean,
): T {
if (viewerIsOwner) return resume;
const { stylesheet: _stylesheet, ...metadata } = resume.data.metadata;
return {
...resume,
@@ -60,7 +61,7 @@ export function redactResumeForViewer<T extends { name: string; data: ResumeData
data: {
...resume.data,
metadata: {
...resume.data.metadata,
...metadata,
notes: "",
},
},
@@ -0,0 +1,77 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createRouterClient } from "@orpc/server";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
const mocks = vi.hoisted(() => ({
create: vi.fn(),
getById: vi.fn(),
}));
vi.mock("@reactive-resume/env/server", () => ({ env: { FLAG_SEMANTIC_CSS_DEFAULT: false } }));
vi.mock("../../context", async () => {
const { os } = await vi.importActual<typeof import("@orpc/server")>("@orpc/server");
return {
protectedProcedure: os.$context<{
locale: "en-US";
reqHeaders: Headers;
user: { id: string };
}>(),
};
});
vi.mock("./service", () => ({
resumeService: {
create: mocks.create,
getById: mocks.getById,
},
}));
const { crudRouter } = await import("./crud");
const rendererUnsafeData = (): ResumeData =>
({
...structuredClone(defaultResumeData),
customSections: [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "summary-item", hidden: false, content: "<p>Missing company</p>" }],
},
],
}) as unknown as ResumeData;
describe("resume duplicate route", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.create.mockResolvedValue("copy-id");
});
it("rejects invalid stored source data before calling the shared create service", async () => {
mocks.getById.mockResolvedValue({
id: "resume-id",
name: "Resume",
slug: "resume",
tags: [],
data: rendererUnsafeData(),
});
const client = createRouterClient(crudRouter, {
context: { locale: "en-US", reqHeaders: new Headers(), user: { id: "user-id" } } as never,
});
const error = await client
.duplicate({ id: "resume-id", name: "Copy", slug: "copy", tags: [] })
.catch((caught: unknown) => caught);
expect(error).toMatchObject({ code: "INTERNAL_SERVER_ERROR", status: 500 });
expect(error).toHaveProperty("cause.issues.0.path", ["customSections", 0, "items", 0, "company"]);
expect(mocks.create).not.toHaveBeenCalled();
});
});
+33 -7
View File
@@ -1,9 +1,13 @@
import { createSampleResumeData } from "@reactive-resume/schema/resume/sample";
import { generateRandomName, slugify } from "@reactive-resume/utils/string";
import { env } from "@reactive-resume/env/server";
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
import { generateId, generateRandomName, slugify } from "@reactive-resume/utils/string";
import { protectedProcedure } from "../../context";
import { resumeDto } from "../../dto/resume";
import { resumeMutationRateLimit } from "../../middleware/rate-limit";
import { parseStoredResumeData } from "./resume-data-validation";
import { resumeService } from "./service";
import { prepareImportedResumeData } from "./stylesheet-preflight";
import { createResumeData } from "./stylesheet-preservation";
export const crudRouter = {
list: protectedProcedure
@@ -69,7 +73,12 @@ export const crudRouter = {
tags: input.tags,
locale: context.locale,
userId: context.user.id,
...(input.withSampleData ? { data: createSampleResumeData(input.name) } : {}),
data: createResumeData({
semanticCssDefault: env.FLAG_SEMANTIC_CSS_DEFAULT,
withSampleData: input.withSampleData,
name: input.name,
locale: context.locale,
}),
}),
),
@@ -92,16 +101,32 @@ export const crudRouter = {
message: "A resume with this slug already exists.",
status: 400,
},
SEMANTIC_STYLESHEET_UNAVAILABLE: {
message: "Semantic stylesheet PDF preflight is unavailable.",
status: 503,
},
STYLESHEET_VALIDATION_FAILED: {
message: "The imported stylesheet failed validation.",
status: 400,
},
})
.handler(async ({ context, input }) => {
const id = generateId();
const data = await prepareImportedResumeData({
data: resumeDataSchema.parse(input.data),
resumeId: id,
revision: 0,
...(context.stylesheetPreflightRunner ? { runner: context.stylesheetPreflightRunner } : {}),
});
const name = generateRandomName();
const slug = slugify(name);
const id = await resumeService.create({
await resumeService.create({
id,
name,
slug,
tags: [],
data: input.data,
data,
locale: context.locale,
userId: context.user.id,
});
@@ -110,7 +135,7 @@ export const crudRouter = {
await resumeService.versions.snapshot({
resumeId: id,
userId: context.user.id,
data: input.data,
data,
label: "Imported",
});
@@ -220,6 +245,7 @@ export const crudRouter = {
.output(resumeDto.duplicate.output)
.handler(async ({ context, input }) => {
const original = await resumeService.getById({ id: input.id, userId: context.user.id });
const data = parseStoredResumeData(original.data);
return resumeService.create({
userId: context.user.id,
@@ -227,7 +253,7 @@ export const crudRouter = {
slug: input.slug ?? original.slug,
tags: input.tags ?? original.tags,
locale: context.locale,
data: original.data,
data,
});
}),
@@ -144,6 +144,31 @@ describe("subscribeResumeUpdated", () => {
await iterator.next();
});
it("accepts stylesheet invalidations and ignores unknown mutation names", async () => {
const client = makeFakeClient();
pool.connect.mockResolvedValueOnce(client);
const controller = new AbortController();
const iterator = subscribeResumeUpdated({
resumeId: "r1",
userId: "u1",
signal: controller.signal,
});
const resultP = iterator.next();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
client.__notify("resume_updated", JSON.stringify({ ...exampleEvent, mutation: "forged" }));
client.__notify("resume_updated", JSON.stringify({ ...exampleEvent, mutation: "stylesheet" }));
const result = await resultP;
expect(result.value?.mutation).toBe("stylesheet");
controller.abort();
await iterator.next();
});
it("terminates immediately if signal is already aborted", async () => {
const client = makeFakeClient();
pool.connect.mockResolvedValueOnce(client);
+13 -2
View File
@@ -1,6 +1,16 @@
import { getPool } from "@reactive-resume/db/client";
const RESUME_UPDATED_CHANNEL = "resume_updated";
const resumeMutationNames = new Set([
"sync",
"create",
"update",
"patch",
"lock",
"password",
"delete",
"stylesheet",
] as const);
type PgNotification = {
channel?: string | undefined;
@@ -12,7 +22,7 @@ export type ResumeUpdatedEvent = {
resumeId: string;
userId: string;
updatedAt: string;
mutation: "sync" | "create" | "update" | "patch" | "lock" | "password" | "delete";
mutation: "sync" | "create" | "update" | "patch" | "lock" | "password" | "delete" | "stylesheet";
};
type SubscribeResumeUpdatedInput = {
@@ -30,7 +40,8 @@ function isResumeUpdatedEvent(value: unknown): value is ResumeUpdatedEvent {
typeof event.resumeId === "string" &&
typeof event.userId === "string" &&
typeof event.updatedAt === "string" &&
typeof event.mutation === "string"
typeof event.mutation === "string" &&
resumeMutationNames.has(event.mutation as ResumeUpdatedEvent["mutation"])
);
}
@@ -0,0 +1,105 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
const mocks = vi.hoisted(() => ({
getById: vi.fn(),
renderPdf: vi.fn(),
}));
vi.mock("./service", () => ({
resumeService: {
getById: mocks.getById,
},
}));
vi.mock("@reactive-resume/pdf/server", () => ({
createResumePdfFile: mocks.renderPdf,
}));
const { createResumePdfDownload } = await import("./export");
const createRendererUnsafeResumeData = (): ResumeData => {
const data = structuredClone(defaultResumeData);
data.customSections = [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "summary-shaped-item", hidden: false, content: "<p>Missing company</p>" }],
} as never,
];
return data;
};
const createLegacyRendererSafeResumeData = (): ResumeData =>
({
...structuredClone(defaultResumeData),
customSections: [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [
{
id: "experience-item",
hidden: false,
company: "Analytical Engines",
position: "Programmer",
location: "London",
period: "18421843",
description: "<p>Wrote the first algorithm.</p>",
content: "<p>Compatible overlap</p>",
},
],
},
],
}) as unknown as ResumeData;
describe("createResumePdfDownload", () => {
beforeEach(() => {
mocks.getById.mockReset();
mocks.renderPdf.mockReset();
});
it("rejects renderer-unsafe stored data before export projection or PDF dispatch", async () => {
mocks.getById.mockResolvedValue({
id: "resume-1",
name: "Resume",
data: createRendererUnsafeResumeData(),
});
await expect(createResumePdfDownload({ id: "resume-1", userId: "user-1" })).rejects.toMatchObject({
code: "INTERNAL_SERVER_ERROR",
});
expect(mocks.renderPdf).not.toHaveBeenCalled();
});
it("still renders valid stored data", async () => {
const body = new File(["%PDF"], "resume.pdf", { type: "application/pdf" });
mocks.getById.mockResolvedValue({
id: "resume-1",
name: "Resume",
data: createLegacyRendererSafeResumeData(),
});
mocks.renderPdf.mockResolvedValue(body);
await expect(createResumePdfDownload({ id: "resume-1", userId: "user-1" })).resolves.toMatchObject({ body });
expect(mocks.renderPdf.mock.calls[0]?.[0].data.customSections[0]?.items[0]).toMatchObject({
content: "<p>Compatible overlap</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
});
});
+4 -2
View File
@@ -5,6 +5,7 @@ import { getResumeExportData, resumeHasCoverLetter } from "@reactive-resume/resu
import { generateFilename } from "@reactive-resume/utils/file";
import { protectedProcedure } from "../../context";
import { pdfExportRateLimit } from "../../middleware/rate-limit";
import { parseStoredResumeData } from "./resume-data-validation";
import { resumeService } from "./service";
export {
@@ -21,8 +22,9 @@ type CreateResumePdfDownloadInput = {
export async function createResumePdfDownload(input: CreateResumePdfDownloadInput) {
const resume = await resumeService.getById({ id: input.id, userId: input.userId });
const data = parseStoredResumeData(resume.data);
const target = input.target ?? "resume";
if (target === "cover-letter" && !resumeHasCoverLetter(resume.data)) {
if (target === "cover-letter" && !resumeHasCoverLetter(data)) {
throw new ORPCError("NOT_FOUND", { message: "No cover letter found for this resume" });
}
@@ -34,7 +36,7 @@ export async function createResumePdfDownload(input: CreateResumePdfDownloadInpu
// exported, instead of at server boot. Slashes cold-start file I/O on
// constrained/slow-disk hosts. See fork perf/lazy-load-pdf.
const { createResumePdfFile } = await import("@reactive-resume/pdf/server");
const body = await createResumePdfFile({ data: getResumeExportData(resume.data, target), filename });
const body = await createResumePdfFile({ data: getResumeExportData(data, target), filename });
return {
headers: {
@@ -0,0 +1,259 @@
import { describe, expect, it, vi } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { createPublicResumePdf } from "./public-pdf";
import { createPublicRenderRateLimiter } from "./public-render-rate-limit";
import { getStyleProjection } from "./public-style-projection";
const requestHeaders = new Headers({ "x-forwarded-for": "203.0.113.7" });
const input = {
username: "jane",
slug: "resume",
requestHeaders,
trustedClient: "203.0.113.9",
mismatchReason: "render-data-hash" as const,
};
const buildResume = (overrides: Partial<{ isPublic: boolean; passwordHash: string | null }> = {}) => ({
id: "resume-1",
userId: "owner-1",
name: "Private dashboard title",
slug: "resume",
data: structuredClone(defaultResumeData),
isPublic: overrides.isPublic ?? true,
passwordHash: overrides.passwordHash ?? null,
});
const buildRendererUnsafeResume = () => {
const resume = buildResume();
resume.data.customSections = [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "summary-shaped-item", hidden: false, content: "<p>Missing company</p>" }],
} as never,
];
return resume;
};
describe("createPublicResumePdf", () => {
it("rejects unbounded mismatch metadata before access or rendering", async () => {
const findResume = vi.fn();
await expect(
createPublicResumePdf(
{
...input,
mismatchReason: "private source" as typeof input.mismatchReason,
clientRegistryFingerprint: "not-a-fingerprint",
},
{
findResume,
hasPasswordAccess: vi.fn(),
resolveCurrentUserId: vi.fn(),
rateLimiter: { consume: vi.fn() },
renderPdf: vi.fn(),
getFingerprints: vi.fn(),
now: () => 0,
observe: vi.fn(),
},
),
).rejects.toMatchObject({ code: "BAD_REQUEST", status: 400 });
expect(findResume).not.toHaveBeenCalled();
});
it("authorizes private and password-protected resumes before budget or render", async () => {
const consume = vi.fn();
const renderPdf = vi.fn();
await expect(
createPublicResumePdf(input, {
findResume: vi.fn().mockResolvedValue(buildResume({ isPublic: false })),
hasPasswordAccess: vi.fn(),
resolveCurrentUserId: vi.fn().mockResolvedValue(undefined),
rateLimiter: { consume },
renderPdf,
getFingerprints: vi.fn(),
now: () => 0,
observe: vi.fn(),
}),
).rejects.toMatchObject({ code: "NOT_FOUND" });
expect(consume).not.toHaveBeenCalled();
expect(renderPdf).not.toHaveBeenCalled();
await expect(
createPublicResumePdf(input, {
findResume: vi.fn().mockResolvedValue(buildResume({ passwordHash: "hash" })),
hasPasswordAccess: vi.fn().mockReturnValue(false),
resolveCurrentUserId: vi.fn().mockResolvedValue(undefined),
rateLimiter: { consume },
renderPdf,
getFingerprints: vi.fn(),
now: () => 0,
observe: vi.fn(),
}),
).rejects.toMatchObject({ code: "NEED_PASSWORD" });
expect(consume).not.toHaveBeenCalled();
expect(renderPdf).not.toHaveBeenCalled();
});
it("rejects renderer-unsafe stored data before budget, projection metadata, or rendering", async () => {
const consume = vi.fn();
const renderPdf = vi.fn();
const getFingerprints = vi.fn();
await expect(
createPublicResumePdf(input, {
findResume: vi.fn().mockResolvedValue(buildRendererUnsafeResume()),
hasPasswordAccess: vi.fn(),
resolveCurrentUserId: vi.fn().mockResolvedValue(undefined),
rateLimiter: { consume },
renderPdf,
getFingerprints,
now: () => 0,
observe: vi.fn(),
}),
).rejects.toMatchObject({ code: "INTERNAL_SERVER_ERROR" });
expect(consume).not.toHaveBeenCalled();
expect(getFingerprints).not.toHaveBeenCalled();
expect(renderPdf).not.toHaveBeenCalled();
});
it("shares the exact limiter with style projection", async () => {
const limiter = createPublicRenderRateLimiter({ capacity: 1, refillWindowMs: 60_000, now: () => 0 });
const findResume = vi.fn().mockResolvedValue(buildResume());
const hasPasswordAccess = vi.fn();
const projectionInput = {
...input,
requestHeaders: new Headers({ "x-forwarded-for": "198.51.100.1" }),
};
const pdfInput = {
...input,
requestHeaders: new Headers({ "x-forwarded-for": "198.51.100.2" }),
};
await getStyleProjection(projectionInput, {
findResume,
hasPasswordAccess,
rateLimiter: limiter,
createProjection: vi.fn().mockResolvedValue({
formatVersion: 1,
languageVersion: 1,
semanticTreeVersion: 1,
registryFingerprint: "0".repeat(64),
adapterFingerprint: "1".repeat(64),
renderDataHash: "2".repeat(64),
nodes: {},
}),
cache: new Map(),
});
await expect(
createPublicResumePdf(pdfInput, {
findResume,
hasPasswordAccess,
resolveCurrentUserId: vi.fn().mockResolvedValue(undefined),
rateLimiter: limiter,
renderPdf: vi.fn(),
getFingerprints: vi.fn(),
now: () => 0,
observe: vi.fn(),
}),
).rejects.toMatchObject({ code: "RATE_LIMIT_EXCEEDED" });
});
it("does not accept a caller-supplied owner identity for a private resume", async () => {
const forgedInput = { ...input, currentUserId: "owner-1" };
await expect(
createPublicResumePdf(forgedInput, {
findResume: vi.fn().mockResolvedValue(buildResume({ isPublic: false })),
hasPasswordAccess: vi.fn(),
resolveCurrentUserId: vi.fn().mockResolvedValue(undefined),
rateLimiter: { consume: vi.fn() },
renderPdf: vi.fn().mockResolvedValue(new File(["%PDF"], "resume.pdf")),
getFingerprints: vi.fn().mockResolvedValue({
registryFingerprint: "0".repeat(64),
adapterFingerprint: "1".repeat(64),
}),
now: () => 0,
observe: vi.fn(),
}),
).rejects.toMatchObject({ code: "NOT_FOUND" });
});
it("propagates semantic diagnostics instead of returning an unstyled fallback PDF", async () => {
const observe = vi.fn();
const semanticError = new Error("The semantic stylesheet could not be rendered.", {
cause: [{ code: "RESOURCE_LIMIT", severity: "error" }],
});
const renderPdf = vi.fn().mockRejectedValue(semanticError);
await expect(
createPublicResumePdf(input, {
findResume: vi.fn().mockResolvedValue(buildResume()),
hasPasswordAccess: vi.fn(),
resolveCurrentUserId: vi.fn().mockResolvedValue(undefined),
rateLimiter: { consume: vi.fn() },
renderPdf,
getFingerprints: vi.fn().mockResolvedValue({
registryFingerprint: "0".repeat(64),
adapterFingerprint: "1".repeat(64),
}),
now: () => 10,
observe,
}),
).rejects.toBe(semanticError);
expect(renderPdf).toHaveBeenCalledTimes(1);
expect(observe).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
expect(observe).not.toHaveBeenCalledWith(expect.objectContaining({ success: true }));
});
it("emits source-free, hashed fallback metadata", async () => {
const sensitive = "Ada <ada@example.test> /* source */";
const observe = vi.fn();
let now = 10;
const resume = buildResume();
resume.id = sensitive;
resume.data.basics.name = sensitive;
resume.data.metadata.stylesheet = {
mode: "semantic",
source: { languageVersion: 1, text: sensitive },
applied: { languageVersion: 1, text: sensitive },
};
await createPublicResumePdf(input, {
findResume: vi.fn().mockResolvedValue(resume),
hasPasswordAccess: vi.fn(),
resolveCurrentUserId: vi.fn().mockResolvedValue(undefined),
rateLimiter: { consume: vi.fn() },
renderPdf: vi.fn().mockResolvedValue(new File(["%PDF"], "resume.pdf", { type: "application/pdf" })),
getFingerprints: vi.fn().mockResolvedValue({
registryFingerprint: "0".repeat(64),
adapterFingerprint: "1".repeat(64),
}),
now: () => (now += 5),
observe,
});
const serialized = JSON.stringify(observe.mock.calls);
expect(observe).toHaveBeenCalledWith({
name: "semantic_css.render_fallback",
resumeIdHash: expect.stringMatching(/^[a-f0-9]{64}$/),
mismatchReason: "render-data-hash",
registryFingerprint: "0".repeat(64),
adapterFingerprint: "1".repeat(64),
durationMs: 5,
success: true,
});
expect(serialized).not.toContain(sensitive);
expect(serialized).not.toMatch(/source|comment|diagnostic|email/i);
});
});
@@ -0,0 +1,112 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { PublicRenderAccessDependencies } from "./public-style-projection";
import { ORPCError } from "@orpc/server";
import { generateFilename } from "@reactive-resume/utils/file";
import { publicRenderRateLimiter } from "./public-render-rate-limit";
import { defaultPublicRenderAccessDependencies, loadAuthorizedPublicRenderResume } from "./public-style-projection";
import { hashSemanticCssResumeId } from "./stylesheet-observability";
export type PublicResumePdfMismatchReason =
| "missing-projection"
| "format-version"
| "language-version"
| "semantic-tree-version"
| "registry-fingerprint"
| "adapter-fingerprint"
| "render-data-hash"
| "invalid-projection";
export const PUBLIC_RESUME_PDF_MISMATCH_REASONS = [
"missing-projection",
"format-version",
"language-version",
"semantic-tree-version",
"registry-fingerprint",
"adapter-fingerprint",
"render-data-hash",
"invalid-projection",
] as const satisfies readonly PublicResumePdfMismatchReason[];
export type CreatePublicResumePdfInput = {
username: string;
slug: string;
requestHeaders: Headers;
trustedClient: string;
mismatchReason: PublicResumePdfMismatchReason;
clientRegistryFingerprint?: string;
clientAdapterFingerprint?: string;
};
export type PublicResumePdfDependencies = PublicRenderAccessDependencies & {
resolveCurrentUserId(requestHeaders: Headers): Promise<string | undefined>;
rateLimiter: { consume(input: { trustedClient: string; resumeId: string }): void };
renderPdf(input: { data: ResumeData; filename: string }): Promise<File>;
getFingerprints(): Promise<{ registryFingerprint: string; adapterFingerprint: string }>;
now(): number;
observe(event: Readonly<Record<string, unknown>>): void;
};
const defaultDependencies: PublicResumePdfDependencies = {
...defaultPublicRenderAccessDependencies,
resolveCurrentUserId: async (requestHeaders) =>
(await import("../../context")).resolveUserFromRequestHeaders(requestHeaders).then((user) => user?.id),
rateLimiter: publicRenderRateLimiter,
renderPdf: async (input) => (await import("@reactive-resume/pdf/server")).createResumePdfFile(input),
getFingerprints: async () =>
(await import("@reactive-resume/pdf/public-projection")).getPublicStyleProjectionFingerprints(),
now: Date.now,
observe: console.info,
};
export async function createPublicResumePdf(
input: CreatePublicResumePdfInput,
dependencies: PublicResumePdfDependencies = defaultDependencies,
): Promise<{ body: File; filename: string }> {
const fingerprintPattern = /^[a-f0-9]{64}$/;
if (
!PUBLIC_RESUME_PDF_MISMATCH_REASONS.includes(input.mismatchReason) ||
(input.clientRegistryFingerprint !== undefined && !fingerprintPattern.test(input.clientRegistryFingerprint)) ||
(input.clientAdapterFingerprint !== undefined && !fingerprintPattern.test(input.clientAdapterFingerprint))
) {
throw new ORPCError("BAD_REQUEST", { status: 400, message: "Invalid public PDF fallback metadata." });
}
const currentUserId = await dependencies.resolveCurrentUserId(input.requestHeaders);
const resume = await loadAuthorizedPublicRenderResume(
{
username: input.username,
slug: input.slug,
requestHeaders: input.requestHeaders,
trustedClient: input.trustedClient,
...(currentUserId ? { currentUserId } : {}),
},
dependencies,
);
dependencies.rateLimiter.consume({ trustedClient: input.trustedClient, resumeId: resume.id });
const startedAt = dependencies.now();
const fingerprints = await dependencies.getFingerprints();
const event = (success: boolean) => {
dependencies.observe({
name: "semantic_css.render_fallback",
resumeIdHash: hashSemanticCssResumeId(resume.id),
mismatchReason: input.mismatchReason,
...(input.clientRegistryFingerprint ? { clientRegistryFingerprint: input.clientRegistryFingerprint } : {}),
...(input.clientAdapterFingerprint ? { clientAdapterFingerprint: input.clientAdapterFingerprint } : {}),
...fingerprints,
durationMs: Math.max(0, dependencies.now() - startedAt),
success,
});
};
try {
const filename = generateFilename(resume.data.basics.name || "Resume", "pdf");
const body = await dependencies.renderPdf({ data: resume.data, filename });
event(true);
return {
body,
filename,
};
} catch (error) {
event(false);
throw error;
}
}
@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { createPublicRenderRateLimiter } from "./public-render-rate-limit";
describe("public render rate limit", () => {
it("cannot reset a transport client's budget by rotating forwarding headers", () => {
const limiter = createPublicRenderRateLimiter({ capacity: 1, refillWindowMs: 60_000, now: () => 0 });
const first = {
trustedClient: "203.0.113.9",
requestHeaders: new Headers({
"cf-connecting-ip": "198.51.100.1",
"x-forwarded-for": "198.51.100.2",
}),
resumeId: "resume-1",
};
const rotated = {
...first,
requestHeaders: new Headers({
"cf-connecting-ip": "198.51.100.3",
"x-forwarded-for": "198.51.100.4",
}),
};
limiter.consume(first);
expect(() => limiter.consume(rotated)).toThrowError(
expect.objectContaining({ code: "RATE_LIMIT_EXCEEDED", status: 429 }),
);
});
it("shares one IP-and-resume token bucket across projection and PDF consumers", () => {
const limiter = createPublicRenderRateLimiter({ capacity: 2, refillWindowMs: 60_000, now: () => 0 });
const input = { trustedClient: "203.0.113.7", resumeId: "resume-1" };
limiter.consume(input);
limiter.consume(input);
expect(() => limiter.consume(input)).toThrowError(
expect.objectContaining({ code: "RATE_LIMIT_EXCEEDED", status: 429 }),
);
});
it("keeps budgets separate by client IP and resume", () => {
const limiter = createPublicRenderRateLimiter({ capacity: 1, refillWindowMs: 60_000, now: () => 0 });
limiter.consume({ trustedClient: "203.0.113.7", resumeId: "resume-1" });
expect(() => limiter.consume({ trustedClient: "203.0.113.8", resumeId: "resume-1" })).not.toThrow();
expect(() => limiter.consume({ trustedClient: "203.0.113.7", resumeId: "resume-2" })).not.toThrow();
});
it("refills the bounded bucket over time", () => {
let now = 0;
const limiter = createPublicRenderRateLimiter({ capacity: 1, refillWindowMs: 1_000, now: () => now });
const input = { trustedClient: "203.0.113.7", resumeId: "resume-1" };
limiter.consume(input);
expect(() => limiter.consume(input)).toThrow();
now = 1_000;
expect(() => limiter.consume(input)).not.toThrow();
});
});
@@ -0,0 +1,59 @@
import { ORPCError } from "@orpc/server";
type PublicRenderRateLimitInput = {
/** Sanitized transport identity supplied by the server adapter, never by request headers. */
trustedClient: string;
resumeId: string;
};
export type PublicRenderRateLimiter = {
consume(input: PublicRenderRateLimitInput): void;
};
type Bucket = {
tokens: number;
updatedAt: number;
};
const MAX_BUCKETS = 50_000;
export function createPublicRenderRateLimiter(
options: { capacity?: number; refillWindowMs?: number; now?: () => number } = {},
): PublicRenderRateLimiter {
const capacity = options.capacity ?? 6;
const refillWindowMs = options.refillWindowMs ?? 60_000;
const now = options.now ?? Date.now;
if (!Number.isInteger(capacity) || capacity <= 0 || !Number.isFinite(refillWindowMs) || refillWindowMs <= 0) {
throw new Error("Public render token bucket requires positive finite limits");
}
const buckets = new Map<string, Bucket>();
return {
consume(input) {
const currentTime = now();
const trustedClient = input.trustedClient.trim() || "unknown";
const key = `${trustedClient}:${input.resumeId}`;
const previous = buckets.get(key) ?? { tokens: capacity, updatedAt: currentTime };
const elapsed = Math.max(0, currentTime - previous.updatedAt);
const tokens = Math.min(capacity, previous.tokens + (elapsed * capacity) / refillWindowMs);
if (tokens < 1) {
throw new ORPCError("RATE_LIMIT_EXCEEDED", {
status: 429,
message: "Public resume rendering rate limit exceeded.",
});
}
buckets.delete(key);
buckets.set(key, { tokens: tokens - 1, updatedAt: currentTime });
if (buckets.size <= MAX_BUCKETS) return;
for (const [candidate, bucket] of buckets) {
if (currentTime - bucket.updatedAt >= refillWindowMs) buckets.delete(candidate);
}
if (buckets.size > MAX_BUCKETS) buckets.delete(buckets.keys().next().value as string);
},
};
}
export const publicRenderRateLimiter = createPublicRenderRateLimiter();
@@ -0,0 +1,72 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createRouterClient } from "@orpc/server";
const mocks = vi.hoisted(() => ({
getStyleProjection: vi.fn(),
}));
vi.mock("../../context", async () => {
const { os } = await vi.importActual<typeof import("@orpc/server")>("@orpc/server");
const base = os.$context<{
locale: "en-US";
reqHeaders: Headers;
resHeaders?: Headers;
trustedClient?: string;
}>();
return { publicProcedure: base, protectedProcedure: base };
});
vi.mock("./public-style-projection", () => ({
getStyleProjection: mocks.getStyleProjection,
}));
vi.mock("./service", () => ({ resumeService: {} }));
const { sharingRouter } = await import("./sharing");
describe("public style projection route", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.getStyleProjection.mockResolvedValue({
formatVersion: 1,
languageVersion: 1,
semanticTreeVersion: 1,
registryFingerprint: "0".repeat(64),
adapterFingerprint: "1".repeat(64),
renderDataHash: "2".repeat(64),
nodes: {},
});
});
it("mounts a concrete public GET route beside the ordinary JSON read", () => {
expect(sharingRouter.getStyleProjection["~orpc"].route).toMatchObject({
method: "GET",
path: "/resumes/{username}/{slug}/style-projection",
operationId: "getResumeStyleProjection",
});
expect(sharingRouter.getBySlug["~orpc"].route.path).toBe("/resumes/{username}/{slug}");
});
it("marks the concrete projection response private and uses the server-derived client identity", async () => {
const reqHeaders = new Headers({ "x-forwarded-for": "198.51.100.1" });
const resHeaders = new Headers();
const client = createRouterClient(sharingRouter, {
context: {
locale: "en-US",
reqHeaders,
resHeaders,
trustedClient: "203.0.113.9",
},
});
await client.getStyleProjection({ username: "jane", slug: "resume" });
expect(resHeaders.get("Cache-Control")).toBe("private, no-store");
expect(mocks.getStyleProjection).toHaveBeenCalledWith({
username: "jane",
slug: "resume",
requestHeaders: reqHeaders,
trustedClient: "203.0.113.9",
});
});
});
@@ -0,0 +1,190 @@
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
import { describe, expect, it, vi } from "vitest";
import { createPublicStyleProjection } from "@reactive-resume/pdf/public-projection";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { resumeDto } from "../../dto/resume";
import { getStyleProjection } from "./public-style-projection";
const source = {
languageVersion: 1,
text: "@version 1;\nname { color: #123456; }\n",
};
const buildResume = () => {
const data = structuredClone(defaultResumeData);
data.metadata.stylesheet = { mode: "semantic", source, applied: source };
return {
id: "resume-1",
userId: "owner-1",
name: "Private dashboard title",
slug: "resume",
data,
isPublic: true,
passwordHash: null,
};
};
const input = {
username: "jane",
slug: "resume",
requestHeaders: new Headers({ "x-forwarded-for": "203.0.113.7" }),
trustedClient: "203.0.113.9",
};
describe("getStyleProjection", () => {
it("authorizes before consuming budget or looking up a cached projection", async () => {
const consume = vi.fn();
const createProjection = vi.fn();
const cache = new Map<string, PublicStyleProjection>([
["cached", { renderDataHash: "cached" } as PublicStyleProjection],
]);
await expect(
getStyleProjection(input, {
findResume: vi.fn().mockResolvedValue({ ...buildResume(), isPublic: false }),
hasPasswordAccess: vi.fn(),
rateLimiter: { consume },
createProjection,
cache,
}),
).rejects.toMatchObject({ code: "NOT_FOUND" });
expect(consume).not.toHaveBeenCalled();
expect(createProjection).not.toHaveBeenCalled();
});
it("preserves password authorization before consuming the shared render budget", async () => {
const consume = vi.fn();
await expect(
getStyleProjection(input, {
findResume: vi.fn().mockResolvedValue({ ...buildResume(), passwordHash: "hash" }),
hasPasswordAccess: vi.fn().mockReturnValue(false),
rateLimiter: { consume },
createProjection: vi.fn(),
cache: new Map(),
}),
).rejects.toMatchObject({ code: "NEED_PASSWORD", status: 401 });
expect(consume).not.toHaveBeenCalled();
});
it("returns a source-free resolved projection and consumes one render token", async () => {
const consume = vi.fn();
const result = await getStyleProjection(input, {
findResume: vi.fn().mockResolvedValue(buildResume()),
hasPasswordAccess: vi.fn(),
rateLimiter: { consume },
createProjection: createPublicStyleProjection,
cache: new Map(),
});
expect(result).toEqual(expect.objectContaining({ formatVersion: 1, nodes: expect.any(Object) }));
expect(JSON.stringify(result)).not.toContain("@version");
expect(consume).toHaveBeenCalledOnce();
expect(consume).toHaveBeenCalledWith({ trustedClient: "203.0.113.9", resumeId: "resume-1" });
});
it("normalizes stored data before public projection", async () => {
const resume = buildResume();
resume.data.customSections = [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [
{
id: "experience-item",
hidden: false,
company: "Analytical Engines",
position: "Programmer",
location: "London",
period: "18421843",
description: "<p>Wrote the first algorithm.</p>",
content: "<p>Compatible overlap</p>",
},
],
} as never,
];
const projection = {
formatVersion: 1,
languageVersion: 1,
semanticTreeVersion: 1,
registryFingerprint: "registry",
adapterFingerprint: "adapter",
renderDataHash: "render-hash",
nodes: {},
} satisfies PublicStyleProjection;
const createProjection = vi.fn().mockResolvedValue(projection);
await getStyleProjection(input, {
findResume: vi.fn().mockResolvedValue(resume),
hasPasswordAccess: vi.fn(),
rateLimiter: { consume: vi.fn() },
createProjection,
cache: new Map(),
});
expect(createProjection.mock.calls[0]?.[0].data.customSections[0]?.items[0]).toMatchObject({
content: "<p>Compatible overlap</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
});
it("caches only authorized projections by renderDataHash", async () => {
const projection = {
formatVersion: 1,
languageVersion: 1,
semanticTreeVersion: 1,
registryFingerprint: "registry",
adapterFingerprint: "adapter",
renderDataHash: "render-hash",
nodes: {},
} satisfies PublicStyleProjection;
const createProjection = vi
.fn()
.mockResolvedValueOnce(projection)
.mockResolvedValueOnce({ ...projection });
const cache = new Map<string, PublicStyleProjection>();
const dependencies = {
findResume: vi.fn().mockResolvedValue(buildResume()),
hasPasswordAccess: vi.fn(),
rateLimiter: { consume: vi.fn() },
createProjection,
cache,
};
const first = await getStyleProjection(input, dependencies);
const second = await getStyleProjection(input, dependencies);
expect(cache.get("render-hash")).toBe(first);
expect(second).toBe(first);
});
it("uses a strict source-free public projection DTO", async () => {
const projection = await createPublicStyleProjection({ data: buildResume().data });
expect(resumeDto.getStyleProjection.output.safeParse(projection).success).toBe(true);
expect(
resumeDto.getStyleProjection.output.safeParse({
...projection,
source,
}).success,
).toBe(false);
expect(
resumeDto.getStyleProjection.output.safeParse({
...projection,
nodes: {
...projection.nodes,
private: { diagnostics: [{ message: "source location" }] },
},
}).success,
).toBe(false);
});
});
@@ -0,0 +1,130 @@
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { ORPCError } from "@orpc/server";
import { createPublicStyleProjection } from "@reactive-resume/pdf/public-projection";
import { assertCanView, isOwner } from "./access-policy";
import { publicRenderRateLimiter } from "./public-render-rate-limit";
import { parseStoredResumeData } from "./resume-data-validation";
type PublicRenderResume = {
id: string;
userId: string;
name: string;
slug: string;
data: ResumeData;
isPublic: boolean;
passwordHash: string | null;
};
export type GetStyleProjectionInput = {
username: string;
slug: string;
requestHeaders: Headers;
trustedClient: string;
currentUserId?: string;
};
export type PublicStyleProjectionDependencies = {
findResume(input: Pick<GetStyleProjectionInput, "username" | "slug">): Promise<PublicRenderResume | null>;
hasPasswordAccess(requestHeaders: Headers, resumeId: string, passwordHash: string | null): boolean | Promise<boolean>;
rateLimiter: { consume(input: { trustedClient: string; resumeId: string }): void };
createProjection(input: { data: ResumeData }): Promise<PublicStyleProjection>;
cache: Map<string, PublicStyleProjection>;
};
export type AuthorizedPublicRenderResume = PublicRenderResume & {
viewerIsOwner: boolean;
hasPassword: boolean;
};
export type PublicRenderAccessDependencies = Pick<
PublicStyleProjectionDependencies,
"findResume" | "hasPasswordAccess"
>;
const findResume = async ({
username,
slug,
}: Pick<GetStyleProjectionInput, "username" | "slug">): Promise<PublicRenderResume | null> => {
const [{ db }, schema, { and, eq }] = await Promise.all([
import("@reactive-resume/db/client"),
import("@reactive-resume/db/schema"),
import("drizzle-orm"),
]);
const [resume] = await db
.select({
id: schema.resume.id,
userId: schema.resume.userId,
name: schema.resume.name,
slug: schema.resume.slug,
data: schema.resume.data,
isPublic: schema.resume.isPublic,
passwordHash: schema.resume.password,
})
.from(schema.resume)
.innerJoin(schema.user, eq(schema.resume.userId, schema.user.id))
.where(and(eq(schema.resume.slug, slug), eq(schema.user.username, username)));
return resume ?? null;
};
export const defaultPublicRenderAccessDependencies: PublicRenderAccessDependencies = {
findResume,
hasPasswordAccess: async (requestHeaders, resumeId, passwordHash) =>
(await import("./access")).hasResumeAccess(requestHeaders, resumeId, passwordHash),
};
const projectionCache = new Map<string, PublicStyleProjection>();
const MAX_PROJECTION_CACHE_ENTRIES = 128;
const defaultDependencies: PublicStyleProjectionDependencies = {
...defaultPublicRenderAccessDependencies,
rateLimiter: publicRenderRateLimiter,
createProjection: createPublicStyleProjection,
cache: projectionCache,
};
export async function loadAuthorizedPublicRenderResume(
input: GetStyleProjectionInput,
dependencies: PublicRenderAccessDependencies = defaultPublicRenderAccessDependencies,
): Promise<AuthorizedPublicRenderResume> {
const resume = await dependencies.findResume(input);
if (!resume) throw new ORPCError("NOT_FOUND");
const viewer = input.currentUserId ? { id: input.currentUserId } : null;
assertCanView(resume, viewer);
if (
resume.passwordHash &&
!(await dependencies.hasPasswordAccess(input.requestHeaders, resume.id, resume.passwordHash))
) {
throw new ORPCError("NEED_PASSWORD", {
status: 401,
data: { username: input.username, slug: input.slug },
});
}
const data = parseStoredResumeData(resume.data);
return {
...resume,
data,
viewerIsOwner: isOwner(resume, viewer),
hasPassword: resume.passwordHash !== null,
};
}
export async function getStyleProjection(
input: GetStyleProjectionInput,
dependencies: PublicStyleProjectionDependencies = defaultDependencies,
): Promise<PublicStyleProjection> {
const resume = await loadAuthorizedPublicRenderResume(input, dependencies);
dependencies.rateLimiter.consume({ trustedClient: input.trustedClient, resumeId: resume.id });
const candidate = await dependencies.createProjection({ data: resume.data });
const cached = dependencies.cache.get(candidate.renderDataHash);
if (cached) return cached;
dependencies.cache.set(candidate.renderDataHash, candidate);
if (dependencies.cache.size > MAX_PROJECTION_CACHE_ENTRIES) {
dependencies.cache.delete(dependencies.cache.keys().next().value as string);
}
return candidate;
}
@@ -0,0 +1,21 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { ORPCError } from "@orpc/client";
import { parseResumeData } from "@reactive-resume/schema/resume/data";
function parseApiResumeData(data: unknown, code: "BAD_REQUEST" | "INTERNAL_SERVER_ERROR", message: string): ResumeData {
try {
return parseResumeData(data);
} catch (cause) {
throw new ORPCError(code, {
status: code === "BAD_REQUEST" ? 400 : 500,
message,
cause,
});
}
}
export const parseWritableResumeData = (data: unknown) =>
parseApiResumeData(data, "BAD_REQUEST", "Resume data does not match the canonical schema.");
export const parseStoredResumeData = (data: unknown) =>
parseApiResumeData(data, "INTERNAL_SERVER_ERROR", "Stored resume data does not match the canonical schema.");
@@ -3,6 +3,7 @@ import { crudRouter } from "./crud";
import { updatesRouter } from "./event-router";
import { sharingRouter } from "./sharing";
import { resumeStatisticsRouter } from "./statistics";
import { stylesheetRouter } from "./stylesheet";
import { tagsRouter } from "./tags";
import { versionsRouter } from "./versions";
@@ -11,10 +12,12 @@ export const resumeRouter = {
statistics: resumeStatisticsRouter,
analysis: analysisRouter,
updates: updatesRouter,
stylesheet: stylesheetRouter,
list: crudRouter.list,
getById: crudRouter.getById,
getBySlug: sharingRouter.getBySlug,
getStyleProjection: sharingRouter.getStyleProjection,
create: crudRouter.create,
import: crudRouter.import,
update: crudRouter.update,
+813 -10
View File
@@ -1,4 +1,6 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
// Characterization tests for the resume service. The goal is to pin down CURRENT behavior
// (CRUD / lock / password / statistics branching) so later changes are deliberate. The DB
@@ -30,6 +32,8 @@ vi.mock("@reactive-resume/db/schema", () => ({
isPublic: "is_public",
isLocked: "is_locked",
password: "password",
stylesheetRevision: "stylesheet_revision",
renderDataVersion: "render_data_version",
updatedAt: "updated_at",
createdAt: "created_at",
},
@@ -86,7 +90,7 @@ const { resumeService } = await import("./service");
const createUpdateChain = (rows: unknown[]) => {
const returning = vi.fn(() => Promise.resolve(rows));
const where = vi.fn(() => ({ returning }));
const set = vi.fn(() => ({ where }));
const set = vi.fn((_input: unknown) => ({ where }));
return { chain: { set }, set, where, returning };
};
@@ -95,6 +99,134 @@ const createSelectChain = (rows: unknown[]) => ({
from: () => ({ where: () => Promise.resolve(rows) }),
});
const createLockedSelectChain = (rows: unknown[]) => {
const forUpdate = vi.fn(() => Promise.resolve(rows));
return {
chain: { from: () => ({ where: () => ({ for: forUpdate }) }) },
forUpdate,
};
};
const createSemanticResumeData = (): ResumeData => {
const data: ResumeData = structuredClone(defaultResumeData);
data.metadata.stylesheet = {
mode: "semantic",
source: { languageVersion: 1, text: "@version 1;\n" },
applied: { languageVersion: 1, text: "@version 1;\n" },
};
return data;
};
const createStylesheetResumeData = (mode: "legacy" | "semantic"): ResumeData => {
const data = createSemanticResumeData();
if (data.metadata.stylesheet) data.metadata.stylesheet.mode = mode;
return data;
};
const createRendererUnsafeResumeData = (): ResumeData =>
({
...structuredClone(defaultResumeData),
customSections: [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "summary-shaped-item", hidden: false, content: "<p>Missing company</p>" }],
},
],
}) as unknown as ResumeData;
const createOverlappingRendererSafeResumeData = (): ResumeData =>
({
...structuredClone(defaultResumeData),
customSections: [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [
{
id: "experience-item",
hidden: false,
company: "Analytical Engines",
position: "Programmer",
location: "London",
period: "18421843",
description: "<p>Wrote the first algorithm.</p>",
content: "<p>Renderer-irrelevant overlap must survive.</p>",
},
],
},
],
}) as unknown as ResumeData;
const createResumeRow = (data: ResumeData, updatedAt = new Date()) => ({
id: "r1",
name: "Resume",
slug: "resume",
tags: [],
data,
isPublic: false,
isLocked: false,
updatedAt,
hasPassword: false,
});
const createRestoreHarness = (currentData: ResumeData, restoredData: ResumeData) => {
const currentRow = { ...createResumeRow(currentData), stylesheetRevision: 3 };
const versionLookup = {
from: () => ({
innerJoin: () => ({ where: () => Promise.resolve([{ data: restoredData }]) }),
}),
};
const versionRetention = {
from: () => ({ where: () => ({ orderBy: () => ({ limit: () => [] }) }) }),
};
dbMock.select
.mockReturnValueOnce(createSelectChain([currentRow]))
.mockReturnValueOnce(versionLookup)
.mockReturnValueOnce(versionRetention)
.mockReturnValueOnce(versionRetention);
const snapshotValues = vi.fn(() => Promise.resolve());
dbMock.insert.mockReturnValue({ values: snapshotValues });
dbMock.delete.mockReturnValue({ where: () => Promise.resolve() });
const lockedSelect = createLockedSelectChain([
{
data: currentData,
isLocked: false,
stylesheetRevision: 3,
renderDataVersion: 7,
updatedAt: currentRow.updatedAt,
},
]);
let persistedData: ResumeData | undefined;
const returning = vi.fn(() =>
Promise.resolve([createResumeRow(persistedData ?? restoredData, new Date("2026-01-02T00:00:00Z"))]),
);
const where = vi.fn(() => ({ returning }));
const set = vi.fn((values: { data: ResumeData }) => {
persistedData = values.data;
return { where };
});
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => lockedSelect.chain, update: () => ({ set }) }),
);
return { set, snapshotValues };
};
beforeEach(() => {
dbMock.select.mockReset();
dbMock.insert.mockReset();
@@ -116,17 +248,354 @@ it("imports", () => {
expect(resumeService).toBeDefined();
});
describe("create", () => {
it("copies stylesheet content while leaving both concurrency versions at database defaults", async () => {
const data = createSemanticResumeData();
const values = vi.fn((_input: unknown) => Promise.resolve());
dbMock.insert.mockReturnValueOnce({ values });
await resumeService.create({
userId: "u1",
name: "Copy",
slug: "copy",
tags: [],
locale: "en-US",
data,
});
expect(values).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
metadata: expect.objectContaining({ stylesheet: data.metadata.stylesheet }),
}),
}),
);
expect(values.mock.calls[0]?.[0]).not.toHaveProperty("stylesheetRevision");
expect(values.mock.calls[0]?.[0]).not.toHaveProperty("renderDataVersion");
});
it("rejects renderer-unsafe data from direct and duplicate callers before insertion", async () => {
const values = vi.fn(() => Promise.resolve());
dbMock.insert.mockReturnValueOnce({ values });
const error = await resumeService
.create({
userId: "u1",
name: "Unsafe copy",
slug: "unsafe-copy",
tags: [],
locale: "en-US",
data: createRendererUnsafeResumeData(),
})
.catch((caught: unknown) => caught);
expect(error).toMatchObject({ code: "BAD_REQUEST", status: 400 });
expect(error).toHaveProperty("cause.issues.0.path", ["customSections", 0, "items", 0, "company"]);
expect(values).not.toHaveBeenCalled();
});
it("persists normalized renderer-safe overlapping data", async () => {
const values = vi.fn((_input: unknown) => Promise.resolve());
dbMock.insert.mockReturnValueOnce({ values });
await resumeService.create({
userId: "u1",
name: "Compatible",
slug: "compatible",
tags: [],
locale: "en-US",
data: createOverlappingRendererSafeResumeData(),
});
expect(values.mock.calls[0]?.[0]).toHaveProperty(
"data.customSections.0.items.0.content",
"<p>Renderer-irrelevant overlap must survive.</p>",
);
expect(values.mock.calls[0]?.[0]).toHaveProperty("data.customSections.0.items.0.roles", []);
expect(values.mock.calls[0]?.[0]).toHaveProperty("data.customSections.0.items.0.website", {
url: "",
label: "",
inlineLink: false,
});
});
});
describe("versions.snapshot", () => {
it("persists normalized data in version snapshots", async () => {
const values = vi.fn((_input: unknown) => Promise.resolve());
dbMock.insert.mockReturnValueOnce({ values });
dbMock.select.mockReturnValueOnce({
from: () => ({ where: () => ({ orderBy: () => ({ limit: () => [] }) }) }),
});
dbMock.delete.mockReturnValueOnce({ where: () => Promise.resolve() });
await resumeService.versions.snapshot({
resumeId: "r1",
userId: "u1",
data: createOverlappingRendererSafeResumeData(),
label: "Manual",
});
expect(values).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
customSections: [
expect.objectContaining({
items: [
expect.objectContaining({
content: "<p>Renderer-irrelevant overlap must survive.</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
}),
],
}),
],
}),
}),
);
});
});
describe("versions.restore", () => {
it.each([
{ name: "changed render data", changeRenderData: true, expectedRenderDataVersion: 8 },
{ name: "notes-only data", changeRenderData: false, expectedRenderDataVersion: undefined },
])(
"preserves canonical stylesheet state and snapshots the returned data for $name",
async ({ changeRenderData, expectedRenderDataVersion }) => {
const currentData = createSemanticResumeData();
const restoredData: ResumeData = structuredClone(defaultResumeData);
if (changeRenderData) {
restoredData.basics.name = "Restored Name";
restoredData.metadata.stylesheet = {
mode: "semantic",
source: { languageVersion: 1, text: "@version 1;\nsection { color: #123456; }\n" },
applied: { languageVersion: 1, text: "@version 1;\nsection { color: #123456; }\n" },
};
} else {
restoredData.metadata.notes = "Restored private note";
restoredData.metadata.stylesheet = structuredClone(currentData.metadata.stylesheet);
}
const { set, snapshotValues } = createRestoreHarness(currentData, restoredData);
const prepareData = vi.fn(async ({ data }: { data: ResumeData }) => data);
const result = await resumeService.versions.restore({
resumeId: "r1",
versionId: "v1",
userId: "u1",
prepareData,
});
expect(set).toHaveBeenCalledTimes(1);
expect(prepareData).toHaveBeenCalledWith({ data: restoredData, stylesheetRevision: 3 });
expect(result.data.metadata.stylesheet).toEqual(restoredData.metadata.stylesheet);
const updateValues = set.mock.calls[0]?.[0];
expect(updateValues).toHaveProperty("stylesheetRevision", 4);
if (expectedRenderDataVersion === undefined) {
expect(updateValues).not.toHaveProperty("renderDataVersion");
} else {
expect(updateValues).toHaveProperty("renderDataVersion", expectedRenderDataVersion);
}
expect(snapshotValues).toHaveBeenNthCalledWith(1, {
resumeId: "r1",
userId: "u1",
data: currentData,
label: "Before restore",
});
expect(snapshotValues).toHaveBeenNthCalledWith(2, {
resumeId: "r1",
userId: "u1",
data: result.data,
label: "Restored version",
});
},
);
it.each([
{ currentMode: "legacy" as const, historicalMode: "semantic" as const },
{ currentMode: "semantic" as const, historicalMode: "legacy" as const },
])(
"does not increment render-data version for a stylesheet-only $currentMode to $historicalMode restore",
async ({ currentMode, historicalMode }) => {
const currentData = createStylesheetResumeData(currentMode);
const restoredData = structuredClone(currentData);
if (restoredData.metadata.stylesheet) restoredData.metadata.stylesheet.mode = historicalMode;
const { set } = createRestoreHarness(currentData, restoredData);
await resumeService.versions.restore({
resumeId: "r1",
versionId: "v1",
userId: "u1",
prepareData: ({ data }) => Promise.resolve(data),
});
expect(set.mock.calls[0]?.[0]).toEqual(
expect.objectContaining({
data: expect.objectContaining({
metadata: expect.objectContaining({
stylesheet: expect.objectContaining({ mode: historicalMode }),
}),
}),
stylesheetRevision: 4,
}),
);
expect(set.mock.calls[0]?.[0]).not.toHaveProperty("renderDataVersion");
},
);
it("increments render-data version for a real restored content change", async () => {
const currentData = createStylesheetResumeData("semantic");
const restoredData = structuredClone(currentData);
restoredData.basics.name = "Historical Name";
const { set } = createRestoreHarness(currentData, restoredData);
await resumeService.versions.restore({
resumeId: "r1",
versionId: "v1",
userId: "u1",
prepareData: ({ data }) => Promise.resolve(data),
});
expect(set.mock.calls[0]?.[0]).toEqual(expect.objectContaining({ stylesheetRevision: 4, renderDataVersion: 8 }));
});
it("increments render-data version when active restored legacy rules change", async () => {
const currentData = createStylesheetResumeData("legacy");
const restoredData = structuredClone(currentData);
restoredData.metadata.styleRules = [
{
id: "restored-rule",
label: "Restored",
enabled: true,
target: { scope: "global" },
slots: { heading: { color: "#123456" } },
},
];
const { set } = createRestoreHarness(currentData, restoredData);
await resumeService.versions.restore({
resumeId: "r1",
versionId: "v1",
userId: "u1",
prepareData: ({ data }) => Promise.resolve(data),
});
expect(set.mock.calls[0]?.[0]).toEqual(expect.objectContaining({ stylesheetRevision: 4, renderDataVersion: 8 }));
});
it("rejects a currently locked resume before version lookup, preparation, or snapshots", async () => {
const callOrder: string[] = [];
const currentRow = {
...createResumeRow(createStylesheetResumeData("semantic")),
isLocked: true,
stylesheetRevision: 3,
};
const currentLookup = {
from: () => ({
where: () => {
callOrder.push("getById");
return Promise.resolve([currentRow]);
},
}),
};
const versionLookup = {
from: () => ({
innerJoin: () => ({
where: () => {
callOrder.push("versionLookup");
return Promise.resolve([{ data: createStylesheetResumeData("semantic") }]);
},
}),
}),
};
dbMock.select.mockImplementation((selection: Record<string, unknown>) =>
Object.hasOwn(selection, "isLocked") ? currentLookup : versionLookup,
);
const prepareData = vi.fn(() => {
callOrder.push("prepareData");
return Promise.reject(
Object.assign(new Error("runner unavailable"), { code: "SEMANTIC_STYLESHEET_UNAVAILABLE" }),
);
});
await expect(
resumeService.versions.restore({
resumeId: "r1",
versionId: "v1",
userId: "u1",
prepareData,
}),
).rejects.toMatchObject({ code: "RESUME_LOCKED" });
expect(callOrder).toEqual(["getById"]);
expect(prepareData).not.toHaveBeenCalled();
expect(dbMock.insert).not.toHaveBeenCalled();
expect(dbMock.transaction).not.toHaveBeenCalled();
});
it("rejects a renderer-unsafe historical snapshot before preparation, snapshots, or update", async () => {
const currentData = createSemanticResumeData();
const { set, snapshotValues } = createRestoreHarness(currentData, createRendererUnsafeResumeData());
const prepareData = vi.fn(async ({ data }: { data: ResumeData }) => data);
await expect(
resumeService.versions.restore({
resumeId: "r1",
versionId: "v1",
userId: "u1",
prepareData,
}),
).rejects.toMatchObject({ code: "INTERNAL_SERVER_ERROR", status: 500 });
expect(prepareData).not.toHaveBeenCalled();
expect(snapshotValues).not.toHaveBeenCalled();
expect(set).not.toHaveBeenCalled();
expect(dbMock.transaction).not.toHaveBeenCalled();
});
it("normalizes a valid historical snapshot before preparation and persistence", async () => {
const currentData = createSemanticResumeData();
const restoredData = createOverlappingRendererSafeResumeData();
const { set } = createRestoreHarness(currentData, restoredData);
const prepareData = vi.fn(async ({ data }: { data: ResumeData }) => data);
await resumeService.versions.restore({
resumeId: "r1",
versionId: "v1",
userId: "u1",
prepareData,
});
expect(prepareData.mock.calls[0]?.[0].data.customSections[0]?.items[0]).toMatchObject({
content: "<p>Renderer-irrelevant overlap must survive.</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
expect(set.mock.calls[0]?.[0].data.customSections[0]?.items[0]).toMatchObject({
content: "<p>Renderer-irrelevant overlap must survive.</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
});
});
describe("update", () => {
it("throws RESUME_LOCKED when the pre-read reports the resume is locked", async () => {
dbMock.select.mockReturnValueOnce(createSelectChain([{ isLocked: true }]));
const select = createLockedSelectChain([
{ data: defaultResumeData, isLocked: true, renderDataVersion: 0, updatedAt: new Date() },
]);
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => select.chain }),
);
await expect(resumeService.update({ id: "r1", userId: "u1", name: "New" })).rejects.toMatchObject({
code: "RESUME_LOCKED",
});
expect(select.forUpdate).toHaveBeenCalledWith("update");
});
it("returns the updated row on success", async () => {
dbMock.select.mockReturnValueOnce(createSelectChain([{ isLocked: false }]));
it("updates and returns the ordinary row inside one locked transaction", async () => {
const row = {
id: "r1",
name: "New",
@@ -138,17 +607,29 @@ describe("update", () => {
updatedAt: new Date("2026-01-01T00:00:00Z"),
hasPassword: false,
};
dbMock.update.mockReturnValueOnce(createUpdateChain([row]).chain);
const select = createLockedSelectChain([
{ data: defaultResumeData, isLocked: false, renderDataVersion: 0, updatedAt: row.updatedAt },
]);
const update = createUpdateChain([row]);
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => select.chain, update: () => update.chain }),
);
const result = await resumeService.update({ id: "r1", userId: "u1", name: "New" });
expect(result).toEqual(row);
expect(dbMock.transaction).toHaveBeenCalledTimes(1);
expect(select.forUpdate).toHaveBeenCalledWith("update");
expect(publishResumeUpdatedMock).toHaveBeenCalledTimes(1);
});
it("throws NOT_FOUND when the UPDATE ... RETURNING matches no row", async () => {
dbMock.select.mockReturnValueOnce(createSelectChain([{ isLocked: false }]));
dbMock.update.mockReturnValueOnce(createUpdateChain([]).chain);
const select = createLockedSelectChain([
{ data: defaultResumeData, isLocked: false, renderDataVersion: 0, updatedAt: new Date() },
]);
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => select.chain, update: () => createUpdateChain([]).chain }),
);
await expect(resumeService.update({ id: "r1", userId: "u1", name: "New" })).rejects.toMatchObject({
code: "NOT_FOUND",
@@ -156,8 +637,10 @@ describe("update", () => {
});
it("maps a resume_slug_user_id_unique violation to RESUME_SLUG_ALREADY_EXISTS", async () => {
dbMock.select.mockReturnValueOnce(createSelectChain([{ isLocked: false }]));
dbMock.update.mockReturnValueOnce({
const select = createLockedSelectChain([
{ data: defaultResumeData, isLocked: false, renderDataVersion: 0, updatedAt: new Date() },
]);
const update = {
set: () => ({
where: () => ({
returning: () => {
@@ -167,12 +650,332 @@ describe("update", () => {
},
}),
}),
});
};
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => select.chain, update: () => update }),
);
await expect(resumeService.update({ id: "r1", userId: "u1", slug: "taken" })).rejects.toMatchObject({
code: "RESUME_SLUG_ALREADY_EXISTS",
});
});
it("preserves the server stylesheet and increments render-data version once for visual changes", async () => {
const serverData = createSemanticResumeData();
const clientData: ResumeData = structuredClone(defaultResumeData);
clientData.basics.name = "Changed";
const row = createResumeRow(clientData);
const select = createLockedSelectChain([
{ data: serverData, isLocked: false, renderDataVersion: 3, updatedAt: row.updatedAt },
]);
const update = createUpdateChain([row]);
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => select.chain, update: () => update.chain }),
);
await resumeService.update({ id: "r1", userId: "u1", data: clientData, skipAutoSnapshot: true });
expect(update.set).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
metadata: expect.objectContaining({ stylesheet: serverData.metadata.stylesheet }),
}),
renderDataVersion: 4,
}),
);
});
it("does not increment render-data version for notes-only changes", async () => {
const serverData: ResumeData = structuredClone(defaultResumeData);
const clientData: ResumeData = structuredClone(defaultResumeData);
clientData.metadata.notes = "private note";
const row = createResumeRow(clientData);
const select = createLockedSelectChain([
{ data: serverData, isLocked: false, renderDataVersion: 3, updatedAt: row.updatedAt },
]);
const update = createUpdateChain([row]);
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => select.chain, update: () => update.chain }),
);
await resumeService.update({ id: "r1", userId: "u1", data: clientData, skipAutoSnapshot: true });
expect(update.set).toHaveBeenCalledWith(expect.not.objectContaining({ renderDataVersion: expect.anything() }));
});
it("rejects renderer-unsafe data before updating the JSONB column", async () => {
const select = createLockedSelectChain([
{ data: defaultResumeData, isLocked: false, renderDataVersion: 3, updatedAt: new Date() },
]);
const update = createUpdateChain([createResumeRow(defaultResumeData)]);
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => select.chain, update: () => update.chain }),
);
await expect(
resumeService.update({
id: "r1",
userId: "u1",
data: createRendererUnsafeResumeData(),
skipAutoSnapshot: true,
}),
).rejects.toMatchObject({ code: "BAD_REQUEST", status: 400 });
expect(update.set).not.toHaveBeenCalled();
expect(publishResumeUpdatedMock).not.toHaveBeenCalled();
});
it("persists normalized renderer-safe overlapping data", async () => {
const clientData = createOverlappingRendererSafeResumeData();
const select = createLockedSelectChain([
{ data: defaultResumeData, isLocked: false, renderDataVersion: 3, updatedAt: new Date() },
]);
const update = createUpdateChain([createResumeRow(clientData)]);
dbMock.transaction.mockImplementationOnce(async (callback: (tx: unknown) => Promise<unknown>) =>
callback({ select: () => select.chain, update: () => update.chain }),
);
await resumeService.update({ id: "r1", userId: "u1", data: clientData, skipAutoSnapshot: true });
expect(update.set).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
customSections: [
expect.objectContaining({
items: [
expect.objectContaining({
content: "<p>Renderer-irrelevant overlap must survive.</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
}),
],
}),
],
}),
}),
);
});
});
describe("patch", () => {
const styleRule = {
id: "rule",
label: "Rule",
enabled: true,
target: { scope: "global" as const },
slots: { heading: { color: "#000000" } },
};
const createPatchTx = (existing: {
data: ResumeData;
isLocked: boolean;
renderDataVersion: number;
updatedAt: Date;
}) => {
const lockedSelect = createLockedSelectChain([existing]);
const row = createResumeRow(existing.data, existing.updatedAt);
const update = createUpdateChain([row]);
const versionSelect = {
from: () => ({ where: () => ({ orderBy: () => ({ limit: () => [] }) }) }),
};
const tx = {
select: vi.fn().mockReturnValueOnce(lockedSelect.chain).mockReturnValueOnce(versionSelect),
update: vi.fn(() => update.chain),
insert: vi.fn(() => ({ values: vi.fn(() => Promise.resolve()) })),
delete: vi.fn(() => ({ where: vi.fn(() => Promise.resolve()) })),
};
return { tx, update };
};
it.each([
{
name: "stylesheet descendant target",
op: "replace" as const,
path: "/metadata/stylesheet/source/text",
value: "@version 1;\nresume { color: red; }\n",
},
{
name: "stylesheet descendant copy source",
op: "copy" as const,
from: "/metadata/stylesheet/applied~1text",
path: "/metadata/notes",
},
{
name: "exact stylesheet move source",
op: "move" as const,
from: "/metadata/stylesheet",
path: "/metadata/notes",
},
{
name: "root target",
op: "replace" as const,
path: "",
value: defaultResumeData,
},
{
name: "metadata ancestor target",
op: "replace" as const,
path: "/metadata",
value: defaultResumeData.metadata,
},
{
name: "root copy source",
op: "copy" as const,
from: "",
path: "/metadata/notes",
},
{
name: "metadata ancestor move source",
op: "move" as const,
from: "/metadata",
path: "/metadata/notes",
},
])("rejects $name before applying it", async ({ name: _name, ...operation }) => {
const data = createSemanticResumeData();
const { tx, update } = createPatchTx({
data,
isLocked: false,
renderDataVersion: 2,
updatedAt: new Date(),
});
await expect(
resumeService.patchInTransaction(tx as never, {
id: "r1",
userId: "u1",
operations: [operation],
}),
).rejects.toMatchObject({
code: "INVALID_PATCH_OPERATIONS",
message: expect.stringContaining("server-owned stylesheet"),
});
expect(update.set).not.toHaveBeenCalled();
});
it.each([
{
name: "path",
operation: { op: "add" as const, path: "/metadata/~2stylesheet", value: "invalid escape" },
},
{
name: "from",
operation: {
op: "copy" as const,
from: "/metadata/notes~",
path: "/metadata/notes",
},
},
])("rejects malformed JSON Pointer escapes in $name before applying", async ({ operation }) => {
const data = createSemanticResumeData();
const { tx, update } = createPatchTx({
data,
isLocked: false,
renderDataVersion: 2,
updatedAt: new Date(),
});
const error = await resumeService
.patchInTransaction(tx as never, {
id: "r1",
userId: "u1",
operations: [operation],
})
.catch((caught: unknown) => caught);
expect(error).toMatchObject({
code: "INVALID_PATCH_OPERATIONS",
message: expect.stringContaining("valid JSON Pointer"),
});
expect((error as { data: unknown }).data).toEqual({ index: 0, operation });
expect(update.set).not.toHaveBeenCalled();
});
it("rejects legacy style-rule changes while semantic mode is active", async () => {
const data = createSemanticResumeData();
const { tx, update } = createPatchTx({
data,
isLocked: false,
renderDataVersion: 2,
updatedAt: new Date(),
});
await expect(
resumeService.patchInTransaction(tx as never, {
id: "r1",
userId: "u1",
operations: [{ op: "add", path: "/metadata/styleRules/-", value: styleRule }],
}),
).rejects.toMatchObject({
code: "INVALID_PATCH_OPERATIONS",
message: expect.stringContaining("Legacy style rules"),
});
expect(update.set).not.toHaveBeenCalled();
});
it.each(["", "/metadata"])("rejects semantic style-rule changes through ancestor path %j", async (path) => {
const data = createSemanticResumeData();
const changedMetadata = { ...data.metadata, styleRules: [styleRule] };
const value = path === "" ? { ...data, metadata: changedMetadata } : changedMetadata;
const { tx, update } = createPatchTx({
data,
isLocked: false,
renderDataVersion: 2,
updatedAt: new Date(),
});
await expect(
resumeService.patchInTransaction(tx as never, {
id: "r1",
userId: "u1",
operations: [{ op: "replace", path, value }],
}),
).rejects.toMatchObject({ code: "INVALID_PATCH_OPERATIONS" });
expect(update.set).not.toHaveBeenCalled();
});
it("allows legacy style-rule changes and increments render-data version once", async () => {
const data: ResumeData = structuredClone(defaultResumeData);
const { tx, update } = createPatchTx({
data,
isLocked: false,
renderDataVersion: 5,
updatedAt: new Date(),
});
await resumeService.patchInTransaction(tx as never, {
id: "r1",
userId: "u1",
operations: [{ op: "add", path: "/metadata/styleRules/-", value: styleRule }],
});
expect(update.set).toHaveBeenCalledWith(expect.objectContaining({ renderDataVersion: 6 }));
});
it("allows a harmless sibling operation below metadata while preserving the server stylesheet", async () => {
const data = createSemanticResumeData();
const { tx, update } = createPatchTx({
data,
isLocked: false,
renderDataVersion: 5,
updatedAt: new Date(),
});
await resumeService.patchInTransaction(tx as never, {
id: "r1",
userId: "u1",
operations: [{ op: "replace", path: "/metadata/notes", value: "private note" }],
});
expect(update.set).toHaveBeenCalledWith({
data: expect.objectContaining({
metadata: expect.objectContaining({
notes: "private note",
stylesheet: data.metadata.stylesheet,
}),
}),
});
});
});
describe("setLocked", () => {
+199 -71
View File
@@ -17,6 +17,8 @@ import { getStorageService } from "../storage/service";
import { grantResumeAccess, hasResumeAccess } from "./access";
import { assertCanView, isOwner, redactResumeForViewer, shouldCountForStatistics } from "./access-policy";
import { publishResumeUpdated } from "./events";
import { parseStoredResumeData, parseWritableResumeData } from "./resume-data-validation";
import { hasRenderDataChanged, preserveServerStylesheet } from "./stylesheet-preservation";
import { clientKeyFromHeaders, shouldCountView } from "./view-dedup";
type DbOrTx = typeof db | Parameters<Parameters<typeof db.transaction>[0]>[0];
@@ -29,6 +31,59 @@ function resumeVersionConflict(updatedAt: Date) {
});
}
function invalidPatchOperation(message: string, index?: number, operation?: JsonPatchOperation) {
if (index !== undefined && operation !== undefined) {
return new ORPCError("INVALID_PATCH_OPERATIONS", { status: 400, message, data: { index, operation } });
}
return new ORPCError("INVALID_PATCH_OPERATIONS", { status: 400, message });
}
type JsonPointerClass = "root" | "metadata" | "stylesheet" | "other";
function classifyJsonPointer(pointer: string): JsonPointerClass | undefined {
if (pointer === "") return "root";
if (!pointer.startsWith("/")) return undefined;
const segments = pointer
.slice(1)
.split("/")
.map((segment) => {
if (/~(?:[^01]|$)/.test(segment)) return undefined;
return segment.replace(/~[01]/g, (encoded) => (encoded === "~1" ? "/" : "~"));
});
if (segments.some((segment) => segment === undefined)) return undefined;
if (segments[0] !== "metadata") return "other";
if (segments.length === 1) return "metadata";
return segments[1] === "stylesheet" ? "stylesheet" : "other";
}
function assertSafePatchPointers(operation: JsonPatchOperation, index: number) {
const pathClass = classifyJsonPointer(operation.path);
if (!pathClass) {
throw invalidPatchOperation("Operation `path` property is not a valid JSON Pointer string.", index, operation);
}
let fromClass: JsonPointerClass | undefined;
if ("from" in operation) {
fromClass = classifyJsonPointer(operation.from);
if (!fromClass) {
throw invalidPatchOperation("Operation `from` property is not a valid JSON Pointer string.", index, operation);
}
}
const protectedPath =
pathClass === "stylesheet" || (operation.op !== "test" && (pathClass === "root" || pathClass === "metadata"));
const protectedSource = fromClass === "stylesheet" || fromClass === "root" || fromClass === "metadata";
if (protectedPath || protectedSource) {
throw invalidPatchOperation(
"The server-owned stylesheet cannot be changed through generic resume patches.",
index,
operation,
);
}
}
// Version history: keep a bounded, rolling window of snapshots per resume.
const MAX_VERSIONS_PER_RESUME = 30;
// Manual-save milestones are debounced server-side: an autosave only checkpoints if the newest
@@ -39,10 +94,12 @@ async function writeResumeVersion(
client: DbOrTx,
input: { resumeId: string; userId: string; data: ResumeData; label: string },
) {
const data = parseWritableResumeData(input.data);
await client.insert(schema.resumeVersion).values({
resumeId: input.resumeId,
userId: input.userId,
data: input.data,
data,
label: input.label,
});
@@ -88,7 +145,12 @@ async function applyResumePatchTx(
},
) {
const [existing] = await client
.select({ data: schema.resume.data, isLocked: schema.resume.isLocked, updatedAt: schema.resume.updatedAt })
.select({
data: schema.resume.data,
isLocked: schema.resume.isLocked,
renderDataVersion: schema.resume.renderDataVersion,
updatedAt: schema.resume.updatedAt,
})
.from(schema.resume)
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)))
.for("update");
@@ -99,6 +161,8 @@ async function applyResumePatchTx(
throw resumeVersionConflict(existing.updatedAt);
}
input.operations.forEach(assertSafePatchPointers);
let patchedData: ResumeData;
try {
@@ -118,9 +182,21 @@ async function applyResumePatchTx(
});
}
patchedData = parseWritableResumeData(preserveServerStylesheet(existing.data, patchedData));
if (
existing.data.metadata.stylesheet?.mode === "semantic" &&
JSON.stringify(existing.data.metadata.styleRules) !== JSON.stringify(patchedData.metadata.styleRules)
) {
throw invalidPatchOperation("Legacy style rules cannot be changed while Semantic CSS mode is active.");
}
const renderDataChanged = hasRenderDataChanged(existing.data, patchedData);
const [resume] = await client
.update(schema.resume)
.set({ data: patchedData })
.set({
data: patchedData,
...(renderDataChanged ? { renderDataVersion: existing.renderDataVersion + 1 } : {}),
})
.where(
and(
eq(schema.resume.id, input.id),
@@ -319,6 +395,7 @@ function toSharedResumeResponse(
isLocked: boolean;
},
hasPassword: boolean,
stylesheetMode: "legacy" | "semantic",
) {
return {
id: resume.id,
@@ -329,6 +406,7 @@ function toSharedResumeResponse(
isPublic: resume.isPublic,
isLocked: resume.isLocked,
hasPassword,
stylesheetMode,
};
}
@@ -377,7 +455,16 @@ export const resumeService = {
// Non-destructive restore: writes the snapshot's data back through the normal update path, so
// prior versions remain and the restore is itself just another (snapshot-able, undoable) change.
restore: async (input: { resumeId: string; versionId: string; userId: string }) => {
restore: async (input: {
resumeId: string;
versionId: string;
userId: string;
prepareData(input: { data: ResumeData; stylesheetRevision: number }): Promise<ResumeData>;
}) => {
// Check lock state before loading or validating historical data so locked resumes fail without expensive work.
const current = await resumeService.getById({ id: input.resumeId, userId: input.userId });
if (current.isLocked) throw new ORPCError("RESUME_LOCKED");
const [version] = await db
.select({ data: schema.resumeVersion.data })
.from(schema.resumeVersion)
@@ -391,9 +478,13 @@ export const resumeService = {
);
if (!version) throw new ORPCError("NOT_FOUND");
const versionData = parseStoredResumeData(version.data);
// Capture the pre-restore state first so the restore itself is undoable.
const current = await resumeService.getById({ id: input.resumeId, userId: input.userId });
const restoredData = await input.prepareData({
data: versionData,
stylesheetRevision: current.stylesheetRevision,
});
await resumeService.versions.snapshot({
resumeId: input.resumeId,
userId: input.userId,
@@ -404,14 +495,15 @@ export const resumeService = {
const updated = await resumeService.update({
id: input.resumeId,
userId: input.userId,
data: version.data,
data: restoredData,
restoreStylesheet: true,
skipAutoSnapshot: true,
});
await resumeService.versions.snapshot({
resumeId: input.resumeId,
userId: input.userId,
data: version.data,
data: updated.data,
label: "Restored version",
});
@@ -458,6 +550,7 @@ export const resumeService = {
data: schema.resume.data,
isPublic: schema.resume.isPublic,
isLocked: schema.resume.isLocked,
stylesheetRevision: schema.resume.stylesheetRevision,
updatedAt: schema.resume.updatedAt,
hasPassword: sql<boolean>`${schema.resume.password} IS NOT NULL`,
})
@@ -506,10 +599,16 @@ export const resumeService = {
}
}
return toSharedResumeResponse(redactResumeForViewer(resume, isOwner(resume, viewer)), resume.hasPassword);
const stylesheetMode = resume.data.metadata.stylesheet?.mode ?? "legacy";
return toSharedResumeResponse(
redactResumeForViewer(resume, isOwner(resume, viewer)),
resume.hasPassword,
stylesheetMode,
);
},
create: async (input: {
id?: string;
userId: string;
name: string;
slug: string;
@@ -517,8 +616,8 @@ export const resumeService = {
locale: Locale;
data?: ResumeData;
}) => {
const id = generateId();
const data = input.data ?? defaultResumeData;
const id = input.id ?? generateId();
const data = parseWritableResumeData(structuredClone(input.data ?? defaultResumeData));
data.metadata.page.locale = input.locale;
try {
@@ -560,78 +659,107 @@ export const resumeService = {
tags?: string[];
data?: ResumeData;
isPublic?: boolean;
restoreStylesheet?: boolean;
skipAutoSnapshot?: boolean;
}) => {
const [resume] = await db
.select({ isLocked: schema.resume.isLocked })
.from(schema.resume)
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
const resume = await db
.transaction(async (tx) => {
const [existing] = await tx
.select({
data: schema.resume.data,
isLocked: schema.resume.isLocked,
stylesheetRevision: schema.resume.stylesheetRevision,
renderDataVersion: schema.resume.renderDataVersion,
})
.from(schema.resume)
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)))
.for("update");
if (resume?.isLocked) throw new ORPCError("RESUME_LOCKED");
if (!existing) throw new ORPCError("NOT_FOUND");
if (existing.isLocked) throw new ORPCError("RESUME_LOCKED");
const inputData = input.data ? parseWritableResumeData(input.data) : undefined;
const updateData: Partial<typeof schema.resume.$inferSelect> = {
...(input.name !== undefined ? { name: input.name } : {}),
...(input.slug !== undefined ? { slug: input.slug } : {}),
...(input.tags !== undefined ? { tags: input.tags } : {}),
...(input.data !== undefined ? { data: input.data } : {}),
...(input.isPublic !== undefined ? { isPublic: input.isPublic } : {}),
};
const data = inputData
? input.restoreStylesheet
? inputData
: preserveServerStylesheet(existing.data, inputData)
: undefined;
const normalizedData = data ? parseWritableResumeData(data) : undefined;
const dataForRenderComparison =
normalizedData && input.restoreStylesheet
? preserveServerStylesheet(existing.data, normalizedData)
: normalizedData;
const renderDataChanged = dataForRenderComparison
? hasRenderDataChanged(existing.data, dataForRenderComparison)
: false;
const updateData: Partial<typeof schema.resume.$inferSelect> = {
...(input.name !== undefined ? { name: input.name } : {}),
...(input.slug !== undefined ? { slug: input.slug } : {}),
...(input.tags !== undefined ? { tags: input.tags } : {}),
...(normalizedData ? { data: normalizedData } : {}),
...(input.isPublic !== undefined ? { isPublic: input.isPublic } : {}),
...(input.restoreStylesheet ? { stylesheetRevision: existing.stylesheetRevision + 1 } : {}),
...(renderDataChanged ? { renderDataVersion: existing.renderDataVersion + 1 } : {}),
};
try {
const [resume] = await db
.update(schema.resume)
.set(updateData)
.where(
and(
eq(schema.resume.id, input.id),
eq(schema.resume.isLocked, false),
eq(schema.resume.userId, input.userId),
),
)
.returning({
id: schema.resume.id,
name: schema.resume.name,
slug: schema.resume.slug,
tags: schema.resume.tags,
data: schema.resume.data,
isPublic: schema.resume.isPublic,
isLocked: schema.resume.isLocked,
updatedAt: schema.resume.updatedAt,
hasPassword: sql<boolean>`${schema.resume.password} IS NOT NULL`,
});
const [updated] = await tx
.update(schema.resume)
.set(updateData)
.where(
and(
eq(schema.resume.id, input.id),
eq(schema.resume.isLocked, false),
eq(schema.resume.userId, input.userId),
),
)
.returning({
id: schema.resume.id,
name: schema.resume.name,
slug: schema.resume.slug,
tags: schema.resume.tags,
data: schema.resume.data,
isPublic: schema.resume.isPublic,
isLocked: schema.resume.isLocked,
stylesheetRevision: schema.resume.stylesheetRevision,
renderDataVersion: schema.resume.renderDataVersion,
updatedAt: schema.resume.updatedAt,
hasPassword: sql<boolean>`${schema.resume.password} IS NOT NULL`,
});
if (!resume) throw new ORPCError("NOT_FOUND");
if (!updated) throw new ORPCError("NOT_FOUND");
return updated;
})
.catch((error: unknown) => {
if (error instanceof ORPCError) throw error;
// Debounced manual-save milestone: only snapshots data edits, and only when the previous
// snapshot is old enough (see SNAPSHOT_THROTTLE_MS). Covers template switches and typing.
if (input.data !== undefined && !input.skipAutoSnapshot) {
await maybeSnapshotOnSave({
resumeId: resume.id,
userId: input.userId,
data: resume.data,
label: "Manual save",
});
}
if (get(error, "cause.constraint") === "resume_slug_user_id_unique") {
throw new ORPCError("RESUME_SLUG_ALREADY_EXISTS", { status: 400 });
}
await notifyResumeUpdated({
type: "resume.updated",
resumeId: resume.id,
userId: input.userId,
updatedAt: resume.updatedAt.toISOString(),
mutation: "update",
console.error("Failed to update resume:", error);
throw new ORPCError("INTERNAL_SERVER_ERROR", { message: "Failed to update resume" });
});
return resume;
} catch (error) {
if (error instanceof ORPCError) throw error;
if (get(error, "cause.constraint") === "resume_slug_user_id_unique") {
throw new ORPCError("RESUME_SLUG_ALREADY_EXISTS", { status: 400 });
}
console.error("Failed to update resume:", error);
throw new ORPCError("INTERNAL_SERVER_ERROR", { message: "Failed to update resume" });
// Debounced manual-save milestone: only snapshots data edits, and only when the previous
// snapshot is old enough (see SNAPSHOT_THROTTLE_MS). Covers template switches and typing.
if (input.data !== undefined && !input.skipAutoSnapshot) {
await maybeSnapshotOnSave({
resumeId: resume.id,
userId: input.userId,
data: resume.data,
label: "Manual save",
});
}
await notifyResumeUpdated({
type: "resume.updated",
resumeId: resume.id,
userId: input.userId,
updatedAt: resume.updatedAt.toISOString(),
mutation: "update",
});
return resume;
},
patch: async (input: { id: string; userId: string; operations: JsonPatchOperation[]; expectedUpdatedAt?: Date }) => {
@@ -2,6 +2,7 @@ import z from "zod";
import { protectedProcedure, publicProcedure } from "../../context";
import { resumeDto } from "../../dto/resume";
import { resumeMutationRateLimit, resumePasswordRateLimit } from "../../middleware/rate-limit";
import { getStyleProjection } from "./public-style-projection";
import { resumeService } from "./service";
export const sharingRouter = {
@@ -26,6 +27,29 @@ export const sharingRouter = {
}),
),
getStyleProjection: publicProcedure
.route({
method: "GET",
path: "/resumes/{username}/{slug}/style-projection",
tags: ["Resume Sharing"],
operationId: "getResumeStyleProjection",
summary: "Get public resume style projection",
description:
"Returns the source-free resolved semantic PDF style projection after applying public, private-owner, and password access rules.",
successDescription: "The validated public style projection.",
})
.input(resumeDto.getStyleProjection.input)
.output(resumeDto.getStyleProjection.output)
.handler(({ input, context }) => {
context.resHeaders?.set("Cache-Control", "private, no-store");
return getStyleProjection({
...input,
requestHeaders: context.reqHeaders,
trustedClient: context.trustedClient ?? "unknown",
...(context.user?.id ? { currentUserId: context.user.id } : {}),
});
}),
setPassword: protectedProcedure
.route({
method: "PUT",
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { resumeDto } from "../../dto/resume";
const source = { languageVersion: 1, text: "@version 1;\n" };
const common = {
id: "resume-1",
expectedRevision: 3,
expectedRenderDataVersion: 8,
editGeneration: 13,
};
describe("resume stylesheet mutation DTO", () => {
it.each([
{ ...common, transition: "edit_source", source },
{ ...common, transition: "activate", source },
{ ...common, transition: "deactivate" },
{
...common,
transition: "restore_history",
restore: { mode: "semantic", source, applied: source },
},
])("accepts the exact $transition payload", (input) => {
expect(resumeDto.stylesheet.mutate.input.safeParse(input).success).toBe(true);
});
it.each([
{ name: "forged applied edit", input: { ...common, transition: "edit_source", source, applied: source } },
{ name: "source on deactivate", input: { ...common, transition: "deactivate", source } },
{ name: "missing activation source", input: { ...common, transition: "activate" } },
{
name: "extra restore field",
input: {
...common,
transition: "restore_history",
restore: { mode: "semantic", source, applied: source, revision: 99 },
},
},
{
name: "missing edit generation",
input: {
id: common.id,
expectedRevision: common.expectedRevision,
expectedRenderDataVersion: common.expectedRenderDataVersion,
transition: "deactivate",
},
},
])("rejects $name", ({ input }) => {
expect(resumeDto.stylesheet.mutate.input.safeParse(input).success).toBe(false);
});
});
@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from "vitest";
import { hashSemanticCssResumeId, recordSemanticCssEvent } from "./stylesheet-observability";
const sensitive = "Ada Lovelace <ada@example.test> /* private comment */";
describe("semantic CSS observability", () => {
it("emits the bounded structured fields with a stable domain-separated resume hash", () => {
const logger = vi.fn();
recordSemanticCssEvent(
{
name: "semantic_css.preflight",
resumeId: "resume-123",
durationMs: 17,
languageVersion: 1,
sourceBytes: 42,
template: "onyx",
diagnosticCodes: ["MISSING_VERSION_DIRECTIVE"],
pageCount: 2,
revision: 9,
success: true,
},
logger,
);
expect(logger).toHaveBeenCalledWith({
name: "semantic_css.preflight",
resumeIdHash: hashSemanticCssResumeId("resume-123"),
durationMs: 17,
languageVersion: 1,
sourceBytes: 42,
template: "onyx",
diagnosticCodes: ["MISSING_VERSION_DIRECTIVE"],
pageCount: 2,
revision: 9,
success: true,
});
expect(hashSemanticCssResumeId("resume-123")).toMatch(/^[a-f0-9]{64}$/);
expect(hashSemanticCssResumeId("resume-123")).not.toBe(hashSemanticCssResumeId("123"));
});
it("never logs source, comments, resume content, personal fields, raw IDs, or unsafe diagnostic text", () => {
const logger = vi.fn();
const input = {
name: "semantic_css.compile",
resumeId: sensitive,
durationMs: 4,
languageVersion: 1,
sourceBytes: 101,
template: "onyx",
diagnosticCodes: ["PARSE_ERROR", sensitive],
pageCount: null,
revision: 3,
success: false,
source: sensitive,
comments: sensitive,
resume: { basics: { name: sensitive } },
email: sensitive,
} as const;
recordSemanticCssEvent(input, logger);
const serialized = JSON.stringify(logger.mock.calls);
const event = logger.mock.calls[0]?.[0];
expect(serialized).not.toContain(sensitive);
expect(event).not.toHaveProperty("source");
expect(event).not.toHaveProperty("comments");
expect(event).not.toHaveProperty("resume");
expect(event).not.toHaveProperty("email");
expect(logger).toHaveBeenCalledWith(
expect.objectContaining({ diagnosticCodes: ["PARSE_ERROR", "UNKNOWN_DIAGNOSTIC"] }),
);
});
});
@@ -0,0 +1,46 @@
import type { Template } from "@reactive-resume/schema/templates";
import { createHash } from "node:crypto";
type SemanticCssEventName =
| "semantic_css.compile"
| "semantic_css.preflight"
| "semantic_css.convert_legacy"
| "semantic_css.parity_check"
| "semantic_css.activate";
export type SemanticCssEventInput = {
name: SemanticCssEventName;
resumeId: string;
durationMs: number;
languageVersion: number;
sourceBytes: number;
template: Template;
diagnosticCodes: readonly string[];
pageCount: number | null;
revision: number;
success: boolean;
};
export const hashSemanticCssResumeId = (resumeId: string): string =>
createHash("sha256").update(`reactive-resume:semantic-css:resume:${resumeId}`).digest("hex");
const safeDiagnosticCode = (code: string): string =>
/^[A-Z][A-Z0-9_]{0,63}$/.test(code) ? code : "UNKNOWN_DIAGNOSTIC";
export function recordSemanticCssEvent(
event: SemanticCssEventInput,
logger: (event: Readonly<Record<string, unknown>>) => void = console.info,
): void {
logger({
name: event.name,
resumeIdHash: hashSemanticCssResumeId(event.resumeId),
durationMs: event.durationMs,
languageVersion: event.languageVersion,
sourceBytes: event.sourceBytes,
template: event.template,
diagnosticCodes: event.diagnosticCodes.map(safeDiagnosticCode),
pageCount: event.pageCount,
revision: event.revision,
success: event.success,
});
}
@@ -0,0 +1,202 @@
import type { StylesheetPreflightRunner } from "@reactive-resume/pdf/server";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
import {
checkLegacyStylesheetParity,
prepareImportedResumeData,
validateHistoricalStylesheet,
} from "./stylesheet-preflight";
const semanticPdfMocks = vi.hoisted(() => ({
compareLegacySemanticPresentation: vi.fn(),
}));
vi.mock("@reactive-resume/pdf/semantic", () => semanticPdfMocks);
const validSource = {
languageVersion: 1,
text: "@version 1;\nsection { color: #123456; }\n",
};
const validApplied = {
languageVersion: 1,
text: "@version 1;\nsection-heading { color: #654321; }\n",
};
const invalidSource = {
languageVersion: 99,
text: "@version 99;\n/* preserved future source */\n",
};
const resumeData = (source = validSource, applied = validApplied): ResumeData => {
const data: ResumeData = structuredClone(defaultResumeData);
data.metadata.stylesheet = { mode: "semantic", source, applied };
return data;
};
const createRunner = () => {
const run = vi.fn<StylesheetPreflightRunner["run"]>(async ({ stylesheet }) => ({
ok: true,
pageCount: stylesheet.text === EMPTY_SEMANTIC_CSS_SOURCE ? 1 : 2,
byteCount: 100,
diagnostics: [],
}));
return { run } satisfies StylesheetPreflightRunner;
};
describe("stylesheet persistence preparation", () => {
beforeEach(() => {
vi.restoreAllMocks();
semanticPdfMocks.compareLegacySemanticPresentation.mockReset();
});
it("uses a valid imported source as applied only after PDF preflight", async () => {
const runner = createRunner();
const result = await prepareImportedResumeData({
data: resumeData(validSource, validApplied),
resumeId: "import-1",
revision: 0,
runner,
});
expect(result.metadata.stylesheet).toEqual({
mode: "semantic",
source: validSource,
applied: validSource,
});
expect(runner.run).toHaveBeenCalledTimes(1);
expect(runner.run).toHaveBeenCalledWith({
data: expect.any(Object),
template: defaultResumeData.metadata.template,
stylesheet: validSource,
});
});
it("returns a legacy stylesheet unchanged without PDF preflight", async () => {
const data = resumeData();
data.metadata.stylesheet = { mode: "legacy", source: validSource, applied: validApplied };
const runner = createRunner();
await expect(prepareImportedResumeData({ data, resumeId: "import-legacy", revision: 0, runner })).resolves.toBe(
data,
);
expect(runner.run).not.toHaveBeenCalled();
});
it("preserves invalid imported source and independently validates the imported applied source", async () => {
const runner = createRunner();
const result = await prepareImportedResumeData({
data: resumeData(invalidSource, validApplied),
resumeId: "import-2",
revision: 0,
runner,
});
expect(result.metadata.stylesheet).toEqual({
mode: "semantic",
source: invalidSource,
applied: validApplied,
});
expect(runner.run).toHaveBeenCalledTimes(1);
expect(runner.run.mock.calls[0]?.[0].stylesheet).toEqual(validApplied);
});
it("uses the supported empty applied source when neither imported source is valid", async () => {
const runner = createRunner();
const result = await prepareImportedResumeData({
data: resumeData(invalidSource, invalidSource),
resumeId: "import-3",
revision: 0,
runner,
});
expect(result.metadata.stylesheet).toEqual({
mode: "semantic",
source: invalidSource,
applied: { languageVersion: 1, text: EMPTY_SEMANTIC_CSS_SOURCE },
});
expect(runner.run).toHaveBeenCalledTimes(1);
expect(runner.run.mock.calls[0]?.[0].stylesheet.text).toBe(EMPTY_SEMANTIC_CSS_SOURCE);
});
it("reports a controlled unavailable error when imported stylesheet preflight has no runner", async () => {
await expect(
prepareImportedResumeData({
data: resumeData(),
resumeId: "import-4",
revision: 0,
}),
).rejects.toMatchObject({ code: "SEMANTIC_STYLESHEET_UNAVAILABLE", status: 503 });
});
it("validates and preflights historical applied independently while retaining invalid historical source", async () => {
const runner = createRunner();
const restored = {
mode: "semantic" as const,
source: invalidSource,
applied: validApplied,
};
await expect(
validateHistoricalStylesheet({
data: resumeData(),
resumeId: "restore-1",
revision: 7,
stylesheet: restored,
runner,
}),
).resolves.toEqual(restored);
expect(runner.run).toHaveBeenCalledTimes(1);
expect(runner.run.mock.calls[0]?.[0].stylesheet).toEqual(validApplied);
});
it("emits compile and preflight metrics without embedding imported source text", async () => {
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
await prepareImportedResumeData({
data: resumeData(),
resumeId: "import-observed",
revision: 0,
runner: createRunner(),
});
const serialized = JSON.stringify(log.mock.calls);
expect(log.mock.calls.map(([event]) => (event as { name: string }).name)).toEqual([
"semantic_css.compile",
"semantic_css.preflight",
]);
expect(serialized).not.toContain(validSource.text);
expect(serialized).not.toContain("import-observed");
});
it("records a sanitized parity failure metric when the comparator throws", async () => {
const privateText = "private parity failure john.doe@example.com";
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
semanticPdfMocks.compareLegacySemanticPresentation.mockRejectedValueOnce(new Error(privateText));
await expect(
checkLegacyStylesheetParity({
data: resumeData(),
stylesheet: validSource,
resumeId: "parity-observed",
revision: 5,
}),
).rejects.toThrow(privateText);
expect(log.mock.calls.map(([event]) => event)).toContainEqual(
expect.objectContaining({
name: "semantic_css.parity_check",
durationMs: expect.any(Number),
diagnosticCodes: [],
success: false,
}),
);
const serialized = JSON.stringify(log.mock.calls);
expect(serialized).not.toContain(privateText);
expect(serialized).not.toContain(validSource.text);
expect(serialized).not.toContain("parity-observed");
});
});
@@ -0,0 +1,237 @@
import type { StylesheetPreflightRunner } from "@reactive-resume/pdf/server";
import type { CompileStylesheetResult } from "@reactive-resume/resume/stylesheet";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { SemanticStylesheet, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { StylesheetSnapshot } from "./stylesheet-service";
import { ORPCError } from "@orpc/client";
import { compileStylesheet } from "@reactive-resume/resume/stylesheet";
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
import { templateSchema } from "@reactive-resume/schema/templates";
import { recordSemanticCssEvent } from "./stylesheet-observability";
type PrepareImportedResumeDataInput = {
data: ResumeData;
resumeId: string;
revision: number;
runner?: StylesheetPreflightRunner | undefined;
};
type ValidateHistoricalStylesheetInput = {
data: ResumeData;
resumeId: string;
revision: number;
stylesheet: SemanticStylesheet;
runner?: StylesheetPreflightRunner | undefined;
};
const byteCount = (source: StylesheetSource): number => new TextEncoder().encode(source.text).byteLength;
const observeCompile = (
input: Pick<PrepareImportedResumeDataInput, "data" | "resumeId" | "revision">,
source: StylesheetSource,
): CompileStylesheetResult => {
const startedAt = performance.now();
try {
const result = compileStylesheet(source);
recordSemanticCssEvent({
name: "semantic_css.compile",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: source.languageVersion,
sourceBytes: byteCount(source),
template: input.data.metadata.template,
diagnosticCodes: result.diagnostics.map(({ code }) => code),
pageCount: null,
revision: input.revision,
success: result.program !== null,
});
return result;
} catch (error) {
recordSemanticCssEvent({
name: "semantic_css.compile",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: source.languageVersion,
sourceBytes: byteCount(source),
template: input.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: input.revision,
success: false,
});
throw error;
}
};
const unavailableError = () =>
new ORPCError("SEMANTIC_STYLESHEET_UNAVAILABLE", {
status: 503,
message: "Semantic stylesheet PDF preflight is unavailable.",
});
const validationError = (message: string) =>
new ORPCError("STYLESHEET_VALIDATION_FAILED", {
status: 400,
message,
});
const runPreflight = async (
input: Pick<PrepareImportedResumeDataInput, "data" | "resumeId" | "revision" | "runner">,
stylesheet: StylesheetSource,
) => {
const startedAt = performance.now();
try {
if (!input.runner) throw unavailableError();
const result = await input.runner.run({
data: input.data,
template: input.data.metadata.template,
stylesheet,
});
recordSemanticCssEvent({
name: "semantic_css.preflight",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: stylesheet.languageVersion,
sourceBytes: byteCount(stylesheet),
template: input.data.metadata.template,
diagnosticCodes: result.diagnostics.map(({ code }) => code),
pageCount: result.ok ? result.pageCount : null,
revision: input.revision,
success: result.ok,
});
return result;
} catch (error) {
recordSemanticCssEvent({
name: "semantic_css.preflight",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: stylesheet.languageVersion,
sourceBytes: byteCount(stylesheet),
template: input.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: input.revision,
success: false,
});
throw error;
}
};
const validApplied = async (input: PrepareImportedResumeDataInput, stylesheet: StylesheetSource): Promise<boolean> => {
const compiled = observeCompile(input, stylesheet);
return compiled.program !== null && (await runPreflight(input, stylesheet)).ok;
};
export async function prepareImportedResumeData(input: PrepareImportedResumeDataInput): Promise<ResumeData> {
const stylesheet = input.data.metadata.stylesheet;
if (stylesheet?.mode !== "semantic") return input.data;
let applied = stylesheet.source;
if (!(await validApplied(input, stylesheet.source))) {
applied = stylesheet.applied;
if (!(await validApplied(input, stylesheet.applied))) {
applied = { languageVersion: 1, text: EMPTY_SEMANTIC_CSS_SOURCE };
if (!(await validApplied(input, applied))) {
throw validationError("The empty stylesheet failed PDF preflight.");
}
}
}
return {
...input.data,
metadata: {
...input.data.metadata,
stylesheet: { ...stylesheet, applied },
},
};
}
export async function validateHistoricalStylesheet(
input: ValidateHistoricalStylesheetInput,
): Promise<SemanticStylesheet> {
const compiled = observeCompile(input, input.stylesheet.applied);
if (!compiled.program) throw validationError("The historical applied stylesheet is invalid.");
if (!(await runPreflight(input, input.stylesheet.applied)).ok) {
throw validationError("The historical applied stylesheet failed PDF preflight.");
}
return input.stylesheet;
}
export async function convertLegacyStylesheet(snapshot: StylesheetSnapshot): Promise<StylesheetSource> {
const startedAt = performance.now();
try {
const { convertLegacyStyleRules } = await import("@reactive-resume/pdf/semantic");
const source = convertLegacyStyleRules(snapshot.data).source;
recordSemanticCssEvent({
name: "semantic_css.convert_legacy",
resumeId: snapshot.id,
durationMs: performance.now() - startedAt,
languageVersion: source.languageVersion,
sourceBytes: byteCount(source),
template: snapshot.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: snapshot.stylesheetRevision,
success: true,
});
return source;
} catch (error) {
recordSemanticCssEvent({
name: "semantic_css.convert_legacy",
resumeId: snapshot.id,
durationMs: performance.now() - startedAt,
languageVersion: 1,
sourceBytes: 0,
template: snapshot.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: snapshot.stylesheetRevision,
success: false,
});
throw error;
}
}
export async function checkLegacyStylesheetParity(input: {
data: ResumeData;
stylesheet: StylesheetSource;
resumeId: string;
revision: number;
}): Promise<{ mismatches: readonly string[] }> {
const startedAt = performance.now();
try {
const { compareLegacySemanticPresentation } = await import("@reactive-resume/pdf/semantic");
const result = await compareLegacySemanticPresentation({
data: input.data,
convertedSource: input.stylesheet,
templates: templateSchema.options,
});
recordSemanticCssEvent({
name: "semantic_css.parity_check",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: input.stylesheet.languageVersion,
sourceBytes: byteCount(input.stylesheet),
template: input.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: input.revision,
success: result.mismatches.length === 0,
});
return result;
} catch (error) {
recordSemanticCssEvent({
name: "semantic_css.parity_check",
resumeId: input.resumeId,
durationMs: performance.now() - startedAt,
languageVersion: input.stylesheet.languageVersion,
sourceBytes: byteCount(input.stylesheet),
template: input.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: input.revision,
success: false,
});
throw error;
}
}
@@ -0,0 +1,123 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { createResumeData, hasRenderDataChanged, preserveServerStylesheet } from "./stylesheet-preservation";
const cloneData = (): ResumeData => structuredClone(defaultResumeData);
const semanticStylesheet = {
mode: "semantic",
source: { languageVersion: 1, text: "@version 1;\nresume { color: red; }\n" },
applied: { languageVersion: 1, text: "@version 1;\nresume { color: red; }\n" },
} as const;
describe("preserveServerStylesheet", () => {
it("preserves the server-owned stylesheet when an old client omits it", () => {
const serverData = cloneData();
serverData.metadata.stylesheet = semanticStylesheet;
const clientData = cloneData();
const merged = preserveServerStylesheet(serverData, clientData);
expect(merged.metadata.stylesheet).toEqual(semanticStylesheet);
});
it("preserves the server-owned stylesheet when a client tries to replace it", () => {
const serverData = cloneData();
serverData.metadata.stylesheet = semanticStylesheet;
const clientData = cloneData();
clientData.metadata.stylesheet = {
mode: "legacy",
source: { languageVersion: 1, text: "@version 1;\n" },
applied: { languageVersion: 1, text: "@version 1;\n" },
};
expect(preserveServerStylesheet(serverData, clientData).metadata.stylesheet).toEqual(semanticStylesheet);
});
it("does not let a generic update introduce a stylesheet", () => {
const serverData = cloneData();
const clientData = cloneData();
clientData.metadata.stylesheet = semanticStylesheet;
expect(preserveServerStylesheet(serverData, clientData).metadata.stylesheet).toBeUndefined();
});
});
describe("hasRenderDataChanged", () => {
it("detects content and base-design changes", () => {
const before = cloneData();
const changedContent = cloneData();
changedContent.basics.name = "Changed";
const changedDesign = cloneData();
changedDesign.metadata.design.colors.primary = "#000000";
expect(hasRenderDataChanged(before, changedContent)).toBe(true);
expect(hasRenderDataChanged(before, changedDesign)).toBe(true);
});
it("ignores notes and stylesheet source or applied changes", () => {
const before = cloneData();
before.metadata.stylesheet = semanticStylesheet;
const changedNotes = structuredClone(before);
changedNotes.metadata.notes = "private note";
const changedStylesheet = structuredClone(before);
changedStylesheet.metadata.stylesheet = {
...semanticStylesheet,
source: { languageVersion: 1, text: "@version 1;\nresume { color: blue; }\n" },
applied: { languageVersion: 1, text: "@version 1;\nresume { color: blue; }\n" },
};
expect(hasRenderDataChanged(before, changedNotes)).toBe(false);
expect(hasRenderDataChanged(before, changedStylesheet)).toBe(false);
});
it("detects active legacy style-rule changes but ignores dormant rules in semantic mode", () => {
const legacyBefore = cloneData();
const legacyAfter = cloneData();
legacyAfter.metadata.styleRules = [
{
id: "rule",
label: "Rule",
enabled: true,
target: { scope: "global" },
slots: { heading: { color: "#000000" } },
},
];
const semanticBefore = cloneData();
semanticBefore.metadata.stylesheet = semanticStylesheet;
const semanticAfter = structuredClone(semanticBefore);
semanticAfter.metadata.styleRules = legacyAfter.metadata.styleRules;
expect(hasRenderDataChanged(legacyBefore, legacyAfter)).toBe(true);
expect(hasRenderDataChanged(semanticBefore, semanticAfter)).toBe(false);
});
});
describe("createResumeData", () => {
it("seeds empty semantic source only for the default-enabled cohort", () => {
expect(createResumeData({ semanticCssDefault: true }).metadata.stylesheet).toEqual({
mode: "semantic",
source: { languageVersion: 1, text: "@version 1;\n" },
applied: { languageVersion: 1, text: "@version 1;\n" },
});
expect(createResumeData({ semanticCssDefault: false }).metadata.stylesheet).toBeUndefined();
});
it("clones normal and sample defaults instead of mutating shared data", () => {
const normal = createResumeData({ semanticCssDefault: false, locale: "de-DE" });
const sample = createResumeData({
semanticCssDefault: true,
withSampleData: true,
name: "Sample Person",
locale: "de-DE",
});
normal.basics.name = "Mutated";
sample.metadata.page.locale = "en-US";
expect(defaultResumeData.basics.name).toBe("");
expect(defaultResumeData.metadata.page.locale).not.toBe("de-DE");
expect(sample.basics.name).toBe("Sample Person");
});
});
@@ -0,0 +1,42 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { Locale } from "@reactive-resume/utils/locale";
import { projectRenderData } from "@reactive-resume/resume/stylesheet/render-data";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { createSampleResumeData } from "@reactive-resume/schema/resume/sample";
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
type CreateResumeDataOptions = {
semanticCssDefault: boolean;
withSampleData?: boolean;
name?: string;
locale?: Locale;
};
export function preserveServerStylesheet(serverData: ResumeData, clientData: ResumeData): ResumeData {
const { stylesheet: _clientStylesheet, ...metadata } = clientData.metadata;
const stylesheet = serverData.metadata.stylesheet;
return {
...clientData,
metadata: stylesheet ? { ...metadata, stylesheet: structuredClone(stylesheet) } : metadata,
};
}
export function hasRenderDataChanged(before: ResumeData, after: ResumeData): boolean {
return JSON.stringify(projectRenderData(before)) !== JSON.stringify(projectRenderData(after));
}
export function createResumeData(options: CreateResumeDataOptions): ResumeData {
const data = structuredClone(options.withSampleData ? createSampleResumeData(options.name) : defaultResumeData);
if (options.locale) data.metadata.page.locale = options.locale;
if (options.semanticCssDefault) {
data.metadata.stylesheet = {
mode: "semantic",
source: { languageVersion: 1, text: EMPTY_SEMANTIC_CSS_SOURCE },
applied: { languageVersion: 1, text: EMPTY_SEMANTIC_CSS_SOURCE },
};
}
return data;
}
@@ -0,0 +1,81 @@
import type { InferSchemaOutput, Schema } from "@orpc/server";
import type { SemanticCssDiagnostic } from "@reactive-resume/resume/stylesheet";
import type z from "zod";
import type { resumeDto } from "../../dto/resume";
import { describe, expect, expectTypeOf, it, vi } from "vitest";
vi.mock("../../context", async () => {
const { os } = await vi.importActual<typeof import("@orpc/server")>("@orpc/server");
return { protectedProcedure: os.$context() };
});
vi.mock("./stylesheet-service", () => ({ createDatabaseStylesheetService: vi.fn() }));
const { stylesheetRouter } = await import("./stylesheet");
type MutateErrorMap = (typeof stylesheetRouter.mutate)["~orpc"]["errorMap"];
type ErrorData<TKey extends keyof MutateErrorMap> = MutateErrorMap[TKey] extends {
data: infer TSchema extends Schema<unknown, unknown>;
}
? InferSchemaOutput<TSchema>
: never;
type StylesheetState = z.infer<typeof resumeDto.stylesheet.getState.output>;
const source = { languageVersion: 1, text: "@version 1;\n" };
const state = {
stylesheet: { mode: "semantic" as const, source, applied: source },
revision: 4,
renderDataVersion: 8,
};
const diagnostic = {
code: "PARSE_ERROR",
severity: "error" as const,
message: "Invalid stylesheet.",
range: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 },
},
};
const dataSchema = (code: keyof MutateErrorMap) => {
const error = stylesheetRouter.mutate["~orpc"].errorMap[code];
const schema = error && "data" in error ? error.data : undefined;
expect(schema, `${String(code)} must declare an oRPC data schema`).toBeDefined();
return schema as z.ZodType;
};
describe("resume stylesheet route error contract", () => {
it("exposes strict validation diagnostic data at runtime and in the inferred client error", () => {
const schema = dataSchema("STYLESHEET_VALIDATION_FAILED");
expect(schema.safeParse({ diagnostics: [diagnostic] }).success).toBe(true);
expect(schema.safeParse({ diagnostics: [diagnostic], source }).success).toBe(false);
expect(schema.safeParse({ diagnostics: [{ ...diagnostic, source }] }).success).toBe(false);
expectTypeOf<ErrorData<"STYLESHEET_VALIDATION_FAILED">>().toEqualTypeOf<{
diagnostics: SemanticCssDiagnostic[];
}>();
});
it("exposes strict parity mismatch data at runtime and in the inferred client error", () => {
const schema = dataSchema("STYLESHEET_PARITY_FAILED");
expect(schema.safeParse({ mismatches: ["onyx: page 1"] }).success).toBe(true);
expect(schema.safeParse({ mismatches: ["onyx: page 1"], source }).success).toBe(false);
expectTypeOf<ErrorData<"STYLESHEET_PARITY_FAILED">>().toEqualTypeOf<{ mismatches: string[] }>();
});
it("exposes strict canonical conflict state for type-safe client rebasing", () => {
const schema = dataSchema("STYLESHEET_REVISION_CONFLICT");
expect(schema.safeParse({ state }).success).toBe(true);
expect(schema.safeParse({ state, expectedRevision: 3 }).success).toBe(false);
expect(schema.safeParse({ state: { ...state, source } }).success).toBe(false);
expectTypeOf<ErrorData<"STYLESHEET_REVISION_CONFLICT">>().toEqualTypeOf<{
state: StylesheetState;
}>();
});
it("does not invent data for the message-only unavailable error", () => {
expect("data" in stylesheetRouter.mutate["~orpc"].errorMap.SEMANTIC_STYLESHEET_UNAVAILABLE).toBe(false);
});
});
@@ -0,0 +1,135 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { randomUUID } from "node:crypto";
import { describe, expect, it } from "vitest";
import { drizzle } from "drizzle-orm/node-postgres";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
const databaseTest = process.env.DATABASE_URL ? describe : describe.skip;
databaseTest("stylesheet PostgreSQL compare-and-swap", () => {
it("rejects a preflighted candidate when render data changes on a second connection", async () => {
const [{ getPool }, { createDatabaseStylesheetService }] = await Promise.all([
import("@reactive-resume/db/client"),
import("./stylesheet-service"),
]);
const firstConnection = await getPool().connect();
const secondConnection = await getPool().connect();
const userId = `stylesheet-user-${randomUUID()}`;
const resumeId = `stylesheet-resume-${randomUUID()}`;
const initialSource = { languageVersion: 1, text: "@version 1;\n" };
const candidateSource = {
languageVersion: 1,
text: "@version 1;\nsection { color: #123456; }\n",
};
const data: ResumeData = structuredClone(defaultResumeData);
data.metadata.stylesheet = {
mode: "semantic",
source: initialSource,
applied: initialSource,
};
let releasePreflight: () => void = () => {};
let signalPaused: () => void = () => {};
const paused = new Promise<void>((resolve) => {
signalPaused = resolve;
});
const released = new Promise<void>((resolve) => {
releasePreflight = resolve;
});
try {
await secondConnection.query(
`insert into "user" (id, name, email, username, display_username)
values ($1, $2, $3, $4, $4)`,
[userId, "Stylesheet Test", `${userId}@example.test`, userId],
);
await secondConnection.query(
`insert into resume
(id, name, slug, tags, data, user_id, stylesheet_revision, render_data_version)
values ($1, 'Resume', 'resume', '{}', $2::jsonb, $3, 0, 0)`,
[resumeId, JSON.stringify(data), userId],
);
const service = createDatabaseStylesheetService({
database: drizzle({ client: firstConnection }),
runner: {
run: async () => ({
ok: true,
pageCount: 1,
byteCount: 128,
diagnostics: [],
}),
},
afterPreflight: async () => {
signalPaused();
await released;
},
publish: async () => undefined,
});
const mutation = service.mutate({
id: resumeId,
userId,
expectedRevision: 0,
expectedRenderDataVersion: 0,
editGeneration: 1,
transition: "edit_source",
source: candidateSource,
});
await paused;
await secondConnection.query(
`update resume
set data = jsonb_set(data, '{basics,name}', to_jsonb($1::text), true),
render_data_version = render_data_version + 1
where id = $2 and user_id = $3`,
["Concurrent content", resumeId, userId],
);
releasePreflight();
const conflict = await mutation.catch((error: unknown) => error);
expect(conflict).toMatchObject({
code: "STYLESHEET_REVISION_CONFLICT",
status: 409,
data: {
state: {
revision: 0,
renderDataVersion: 1,
stylesheet: {
source: initialSource,
applied: initialSource,
},
},
},
});
const persisted = await secondConnection.query<{
data: ResumeData;
stylesheet_revision: number;
render_data_version: number;
}>(
`select data, stylesheet_revision, render_data_version
from resume where id = $1 and user_id = $2`,
[resumeId, userId],
);
expect(persisted.rows[0]).toMatchObject({
data: {
basics: { name: "Concurrent content" },
metadata: {
stylesheet: {
source: initialSource,
applied: initialSource,
},
},
},
stylesheet_revision: 0,
render_data_version: 1,
});
} finally {
releasePreflight();
await secondConnection.query(`delete from "user" where id = $1`, [userId]).catch(() => undefined);
firstConnection.release();
secondConnection.release();
}
});
});
@@ -0,0 +1,535 @@
import type { PdfPreflightResult } from "@reactive-resume/pdf/server";
import type { CompileStylesheetResult } from "@reactive-resume/resume/stylesheet";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { SemanticStylesheet, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { SemanticCssEventInput } from "./stylesheet-observability";
import type { StylesheetSnapshot } from "./stylesheet-service";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { createStylesheetService } from "./stylesheet-service";
vi.mock("@reactive-resume/db/client", () => ({ db: {}, getPool: vi.fn() }));
const validSource = {
languageVersion: 1,
text: "@version 1;\nsection { color: #112233; }\n",
} satisfies StylesheetSource;
const invalidSource = {
languageVersion: 1,
text: "@version 1;\nsection {",
} satisfies StylesheetSource;
const previousSource = {
languageVersion: 1,
text: "@version 1;\nsection { color: #445566; }\n",
} satisfies StylesheetSource;
const previousApplied = {
languageVersion: 1,
text: "@version 1;\nsection { color: #778899; }\n",
} satisfies StylesheetSource;
const previousStylesheet = {
mode: "semantic",
source: previousSource,
applied: previousApplied,
} satisfies SemanticStylesheet;
const diagnostic = {
code: "PARSE_ERROR",
severity: "error" as const,
message: "Invalid stylesheet.",
range: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 },
},
};
const successfulCompile = {
program: { languageVersion: 1, rules: [] },
diagnostics: [],
} satisfies CompileStylesheetResult;
const failedCompile = { program: null, diagnostics: [diagnostic] } satisfies CompileStylesheetResult;
const successfulPreflight = {
ok: true,
pageCount: 1,
byteCount: 256,
diagnostics: [],
} satisfies PdfPreflightResult;
const commonMutationInput = {
id: "resume-1",
userId: "user-1",
expectedRevision: 3,
expectedRenderDataVersion: 8,
editGeneration: 11,
} as const;
const snapshot = (stylesheet: SemanticStylesheet = previousStylesheet): StylesheetSnapshot => {
const data: ResumeData = structuredClone(defaultResumeData);
data.metadata.stylesheet = stylesheet;
data.metadata.styleRules = [
{
id: "legacy-rule",
label: "Legacy",
enabled: true,
target: { scope: "global" },
slots: { heading: { color: "#123456" } },
},
];
return {
id: "resume-1",
userId: "user-1",
data,
isLocked: false,
stylesheetRevision: 3,
renderDataVersion: 8,
updatedAt: new Date("2026-07-28T12:00:00.000Z"),
};
};
type HarnessOptions = {
initial?: StylesheetSnapshot;
compile?: (source: StylesheetSource) => CompileStylesheetResult;
preflight?: ((input: { data: ResumeData; stylesheet: StylesheetSource }) => Promise<PdfPreflightResult>) | undefined;
parityMismatches?: readonly string[];
parity?: () => Promise<{ mismatches: readonly string[] }>;
locked?: StylesheetSnapshot;
useDefaultObserver?: boolean;
};
const createHarness = (options: HarnessOptions = {}) => {
const callOrder: string[] = [];
const events: SemanticCssEventInput[] = [];
let persisted = structuredClone(options.initial ?? snapshot());
const compile = vi.fn((source: StylesheetSource) => {
callOrder.push("compile");
return options.compile?.(source) ?? successfulCompile;
});
const preflight = options.preflight
? vi.fn(options.preflight)
: vi.fn(() => {
callOrder.push("preflight");
return Promise.resolve(successfulPreflight);
});
const parity = vi.fn(() => {
callOrder.push("parity");
return options.parity?.() ?? Promise.resolve({ mismatches: options.parityMismatches ?? [] });
});
const publish = vi.fn(() => {
callOrder.push("publish");
return Promise.resolve();
});
const service = createStylesheetService({
readSnapshot: () => {
callOrder.push("readSnapshot");
return Promise.resolve(structuredClone(persisted));
},
convertLegacy: () => {
callOrder.push("convertLegacy");
return { languageVersion: 1, text: "@version 1;\n" };
},
compile,
preflight:
options.preflight === undefined
? preflight
: (input) => {
callOrder.push("preflight");
return preflight(input);
},
parity,
transaction: async (run) => {
callOrder.push("begin");
try {
const result = await run({
lock: () => {
callOrder.push("lock");
return Promise.resolve(structuredClone(options.locked ?? persisted));
},
update: ({ data }) => {
callOrder.push("update");
persisted = {
...persisted,
data,
stylesheetRevision: persisted.stylesheetRevision + 1,
};
return Promise.resolve(structuredClone(persisted));
},
});
callOrder.push("commit");
return result;
} catch (error) {
callOrder.push("rollback");
throw error;
}
},
compare: (locked, input) => {
callOrder.push("compare");
return (
locked.stylesheetRevision === input.expectedRevision &&
locked.renderDataVersion === input.expectedRenderDataVersion
);
},
publish,
...(options.useDefaultObserver ? {} : { observe: (event: SemanticCssEventInput) => events.push(event) }),
});
return { callOrder, compile, events, parity, persisted: () => persisted, preflight, publish, service };
};
describe("stylesheet service", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("stores invalid editable source while preserving the last-valid applied source and render version", async () => {
const harness = createHarness({ compile: () => failedCompile });
const result = await harness.service.mutate({
...commonMutationInput,
transition: "edit_source",
source: invalidSource,
});
expect(result.stylesheet.source).toEqual(invalidSource);
expect(result.stylesheet.applied).toEqual(previousApplied);
expect(result.revision).toBe(4);
expect(result.renderDataVersion).toBe(8);
expect(result.editGeneration).toBe(11);
expect(result.diagnostics).toEqual([diagnostic]);
expect(harness.preflight).not.toHaveBeenCalled();
});
it("promotes a valid edit only in semantic mode after preflight outside the short transaction", async () => {
const harness = createHarness();
const result = await harness.service.mutate({
...commonMutationInput,
transition: "edit_source",
source: validSource,
});
expect(result.stylesheet.source).toEqual(validSource);
expect(result.stylesheet.applied).toEqual(validSource);
expect(harness.callOrder).toEqual([
"readSnapshot",
"compile",
"preflight",
"begin",
"lock",
"compare",
"update",
"commit",
"publish",
]);
});
it("keeps a valid legacy-mode edit dormant without preflight", async () => {
const initial = snapshot({ ...previousStylesheet, mode: "legacy" });
const harness = createHarness({ initial });
const result = await harness.service.mutate({
...commonMutationInput,
transition: "edit_source",
source: validSource,
});
expect(result.stylesheet).toEqual({
mode: "legacy",
source: validSource,
applied: previousApplied,
});
expect(harness.preflight).not.toHaveBeenCalled();
});
it("preserves editable source but not applied source when semantic render preflight fails", async () => {
const harness = createHarness({
preflight: async () => ({
ok: false,
code: "STYLESHEET_PREFLIGHT_RENDER_FAILED",
message: "PDF rendering failed.",
diagnostics: [],
}),
});
const result = await harness.service.mutate({
...commonMutationInput,
transition: "edit_source",
source: validSource,
});
expect(result.stylesheet.source).toEqual(validSource);
expect(result.stylesheet.applied).toEqual(previousApplied);
expect(result.diagnostics).toEqual([
expect.objectContaining({ code: "STYLESHEET_PREFLIGHT_RENDER_FAILED", severity: "error" }),
]);
});
it("requires parity and preflight before explicit activation", async () => {
const initial = snapshot({ ...previousStylesheet, mode: "legacy" });
const harness = createHarness({ initial });
const result = await harness.service.mutate({
...commonMutationInput,
transition: "activate",
source: validSource,
});
expect(result.stylesheet).toEqual({ mode: "semantic", source: validSource, applied: validSource });
expect(harness.parity).toHaveBeenCalledOnce();
expect(harness.preflight).toHaveBeenCalledOnce();
expect(result.revision).toBe(4);
expect(result.renderDataVersion).toBe(8);
expect(harness.events).toContainEqual(
expect.objectContaining({
name: "semantic_css.activate",
durationMs: expect.any(Number),
pageCount: 1,
revision: 4,
success: true,
}),
);
});
it("records a sanitized compile failure metric when the compiler throws", async () => {
const privateText = "private compiler failure john.doe@example.com";
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
const harness = createHarness({
compile: () => {
throw new Error(privateText);
},
useDefaultObserver: true,
});
await expect(
harness.service.mutate({ ...commonMutationInput, transition: "edit_source", source: validSource }),
).rejects.toThrow(privateText);
expect(log.mock.calls.map(([event]) => event)).toContainEqual(
expect.objectContaining({
name: "semantic_css.compile",
durationMs: expect.any(Number),
diagnosticCodes: [],
success: false,
}),
);
const serialized = JSON.stringify(log.mock.calls);
expect(serialized).not.toContain(privateText);
expect(serialized).not.toContain(validSource.text);
expect(serialized).not.toContain("resume-1");
});
it("records a sanitized preflight failure metric when the runner throws", async () => {
const privateText = "private preflight failure john.doe@example.com";
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
const harness = createHarness({
preflight: () => Promise.reject(new Error(privateText)),
useDefaultObserver: true,
});
await expect(
harness.service.mutate({ ...commonMutationInput, transition: "edit_source", source: validSource }),
).rejects.toThrow(privateText);
expect(log.mock.calls.map(([event]) => event)).toContainEqual(
expect.objectContaining({
name: "semantic_css.preflight",
durationMs: expect.any(Number),
diagnosticCodes: [],
success: false,
}),
);
const serialized = JSON.stringify(log.mock.calls);
expect(serialized).not.toContain(privateText);
expect(serialized).not.toContain(validSource.text);
expect(serialized).not.toContain("resume-1");
});
it("records a sanitized activation failure metric when parity throws", async () => {
const privateText = "private parity failure john.doe@example.com";
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
const harness = createHarness({
initial: snapshot({ ...previousStylesheet, mode: "legacy" }),
parity: () => Promise.reject(new Error(privateText)),
useDefaultObserver: true,
});
await expect(
harness.service.mutate({ ...commonMutationInput, transition: "activate", source: validSource }),
).rejects.toThrow(privateText);
expect(log.mock.calls.map(([event]) => event)).toContainEqual(
expect.objectContaining({
name: "semantic_css.activate",
durationMs: expect.any(Number),
diagnosticCodes: [],
pageCount: null,
revision: 3,
success: false,
}),
);
const serialized = JSON.stringify(log.mock.calls);
expect(serialized).not.toContain(privateText);
expect(serialized).not.toContain(validSource.text);
expect(serialized).not.toContain("resume-1");
});
it("deactivates without compiling or deleting either source", async () => {
const harness = createHarness();
const result = await harness.service.mutate({ ...commonMutationInput, transition: "deactivate" });
expect(result.stylesheet).toEqual({ ...previousStylesheet, mode: "legacy" });
expect(harness.compile).not.toHaveBeenCalled();
expect(harness.parity).not.toHaveBeenCalled();
expect(harness.preflight).not.toHaveBeenCalled();
});
it("does not rewrite invalid stored resume data during deactivation", async () => {
const initial = snapshot();
initial.data.customSections = [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "summary-item", hidden: false, content: "<p>Missing company</p>" }],
} as never,
];
const harness = createHarness({ initial });
const error = await harness.service
.mutate({ ...commonMutationInput, transition: "deactivate" })
.catch((caught: unknown) => caught);
expect(error).toMatchObject({ code: "INTERNAL_SERVER_ERROR", status: 500 });
expect(error).toHaveProperty("cause.issues.0.path", ["customSections", 0, "items", 0, "company"]);
expect(harness.callOrder).not.toContain("update");
expect(harness.publish).not.toHaveBeenCalled();
});
it("normalizes valid stored resume data during deactivation", async () => {
const initial = snapshot();
initial.data.customSections = [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [
{
id: "experience-item",
hidden: false,
company: "Analytical Engines",
position: "Programmer",
location: "London",
period: "18421843",
description: "<p>Wrote the first algorithm.</p>",
content: "<p>Compatible overlap</p>",
},
],
} as never,
];
const harness = createHarness({ initial });
await harness.service.mutate({ ...commonMutationInput, transition: "deactivate" });
expect(harness.persisted().data.customSections[0]?.items[0]).toMatchObject({
content: "<p>Compatible overlap</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
});
it("validates and preflights restored applied source independently from invalid editable source", async () => {
const restoredApplied = { ...validSource, text: "@version 1;\nresume { color: #abcdef; }\n" };
const compile = vi.fn((source: StylesheetSource) =>
source === restoredApplied ? successfulCompile : failedCompile,
);
const harness = createHarness({ compile });
const result = await harness.service.mutate({
...commonMutationInput,
transition: "restore_history",
restore: {
mode: "semantic",
source: invalidSource,
applied: restoredApplied,
},
});
expect(compile).toHaveBeenCalledTimes(1);
expect(compile).toHaveBeenCalledWith(restoredApplied);
expect(harness.preflight).toHaveBeenCalledWith({
data: expect.any(Object),
stylesheet: restoredApplied,
});
expect(result.stylesheet).toEqual({
mode: "semantic",
source: invalidSource,
applied: restoredApplied,
});
});
it("rejects locked resumes before compiler or preflight work", async () => {
const initial = { ...snapshot(), isLocked: true };
const harness = createHarness({ initial });
await expect(
harness.service.mutate({ ...commonMutationInput, transition: "edit_source", source: validSource }),
).rejects.toMatchObject({ code: "RESUME_LOCKED" });
expect(harness.compile).not.toHaveBeenCalled();
expect(harness.preflight).not.toHaveBeenCalled();
});
it("returns the locked canonical snapshot on a two-version conflict without writing", async () => {
const locked = {
...snapshot(),
stylesheetRevision: 4,
renderDataVersion: 9,
};
delete locked.data.metadata.stylesheet;
const harness = createHarness({ locked });
const error = await harness.service
.mutate({ ...commonMutationInput, transition: "edit_source", source: validSource })
.catch((caught: unknown) => caught);
expect(error).toMatchObject({
code: "STYLESHEET_REVISION_CONFLICT",
data: {
state: expect.objectContaining({
revision: 4,
renderDataVersion: 9,
stylesheet: expect.objectContaining({
mode: "legacy",
source: { languageVersion: 1, text: "@version 1;\n" },
}),
}),
},
});
expect(harness.callOrder).not.toContain("update");
expect(harness.publish).not.toHaveBeenCalled();
expect(harness.callOrder.indexOf("rollback")).toBeLessThan(harness.callOrder.indexOf("convertLegacy"));
});
it("reloads the exact invalid source, last-valid applied source, and untouched legacy rules", async () => {
const harness = createHarness({ compile: () => failedCompile });
await harness.service.mutate({
...commonMutationInput,
transition: "edit_source",
source: invalidSource,
});
const reloaded = await harness.service.getState({ id: "resume-1", userId: "user-1" });
expect(reloaded.stylesheet.source).toEqual(invalidSource);
expect(reloaded.stylesheet.applied).toEqual(previousApplied);
expect(harness.persisted().data.metadata.styleRules).toEqual(snapshot().data.metadata.styleRules);
});
});
@@ -0,0 +1,447 @@
import type { PdfPreflightResult, StylesheetPreflightRunner } from "@reactive-resume/pdf/server";
import type { CompileStylesheetResult, SemanticCssDiagnostic } from "@reactive-resume/resume/stylesheet";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { SemanticStylesheet, StylesheetMode, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { SemanticCssEventInput } from "./stylesheet-observability";
import { ORPCError } from "@orpc/client";
import { and, eq } from "drizzle-orm";
import { db } from "@reactive-resume/db/client";
import * as schema from "@reactive-resume/db/schema";
import { compileStylesheet } from "@reactive-resume/resume/stylesheet";
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
import { publishResumeUpdated } from "./events";
import { parseStoredResumeData } from "./resume-data-validation";
import { recordSemanticCssEvent } from "./stylesheet-observability";
import { checkLegacyStylesheetParity, convertLegacyStylesheet } from "./stylesheet-preflight";
export type StylesheetSnapshot = {
id: string;
userId: string;
data: ResumeData;
isLocked: boolean;
stylesheetRevision: number;
renderDataVersion: number;
updatedAt: Date;
};
type StylesheetMutationCommon = {
id: string;
userId: string;
expectedRevision: number;
expectedRenderDataVersion: number;
editGeneration: number;
};
export type MutateResumeStylesheetInput = StylesheetMutationCommon &
(
| { transition: "edit_source"; source: StylesheetSource }
| { transition: "activate"; source: StylesheetSource }
| { transition: "deactivate" }
| {
transition: "restore_history";
restore: {
mode: StylesheetMode;
source: StylesheetSource;
applied: StylesheetSource;
};
}
);
export type StylesheetState = {
stylesheet: SemanticStylesheet;
revision: number;
renderDataVersion: number;
};
export type StylesheetMutationResult = StylesheetState & {
editGeneration: number;
diagnostics: SemanticCssDiagnostic[];
};
type StylesheetTransaction = {
lock(input: { id: string; userId: string }): Promise<StylesheetSnapshot>;
update(input: { snapshot: StylesheetSnapshot; data: ResumeData }): Promise<StylesheetSnapshot>;
};
type StylesheetServiceDependencies = {
readSnapshot(input: { id: string; userId: string }): Promise<StylesheetSnapshot>;
convertLegacy(snapshot: StylesheetSnapshot): StylesheetSource | Promise<StylesheetSource>;
compile(source: StylesheetSource): CompileStylesheetResult;
preflight?(input: { data: ResumeData; stylesheet: StylesheetSource }): Promise<PdfPreflightResult>;
parity(input: {
data: ResumeData;
stylesheet: StylesheetSource;
resumeId: string;
revision: number;
}): Promise<{ mismatches: readonly string[] }>;
transaction<T>(run: (transaction: StylesheetTransaction) => Promise<T>): Promise<T>;
compare(snapshot: StylesheetSnapshot, input: StylesheetMutationCommon): boolean | Promise<boolean>;
publish(snapshot: StylesheetSnapshot): Promise<void>;
afterPreflight?(): Promise<void>;
observe?(event: SemanticCssEventInput): void;
};
class StylesheetRevisionConflict extends Error {
constructor(readonly snapshot: StylesheetSnapshot) {
super("The resume or stylesheet changed while the candidate was being validated.");
}
}
const emptySource = (): StylesheetSource => ({ languageVersion: 1, text: EMPTY_SEMANTIC_CSS_SOURCE });
export async function stylesheetFromSnapshot(
snapshot: StylesheetSnapshot,
convertLegacy: (snapshot: StylesheetSnapshot) => StylesheetSource | Promise<StylesheetSource>,
): Promise<SemanticStylesheet> {
const stylesheet = snapshot.data.metadata.stylesheet;
if (stylesheet) return stylesheet;
return {
mode: "legacy",
source: await convertLegacy(snapshot),
applied: emptySource(),
};
}
const stateFromSnapshot = async (
snapshot: StylesheetSnapshot,
convertLegacy: (snapshot: StylesheetSnapshot) => StylesheetSource | Promise<StylesheetSource>,
): Promise<StylesheetState> => ({
stylesheet: await stylesheetFromSnapshot(snapshot, convertLegacy),
revision: snapshot.stylesheetRevision,
renderDataVersion: snapshot.renderDataVersion,
});
const preflightDiagnostic = (result: Extract<PdfPreflightResult, { ok: false }>): SemanticCssDiagnostic => ({
code: result.code,
severity: "error",
message: result.message,
range: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 },
},
});
const validationError = (message: string, diagnostics: readonly SemanticCssDiagnostic[]) =>
new ORPCError("STYLESHEET_VALIDATION_FAILED", {
status: 400,
message,
data: { diagnostics },
});
const unavailableError = () =>
new ORPCError("SEMANTIC_STYLESHEET_UNAVAILABLE", {
status: 503,
message: "Semantic stylesheet PDF preflight is unavailable.",
});
export function createStylesheetService(dependencies: StylesheetServiceDependencies): {
getState(input: { id: string; userId: string }): Promise<StylesheetState>;
mutate(input: MutateResumeStylesheetInput): Promise<StylesheetMutationResult>;
} {
const observe = dependencies.observe ?? recordSemanticCssEvent;
const sourceBytes = (source: StylesheetSource) => new TextEncoder().encode(source.text).byteLength;
const compile = (snapshot: StylesheetSnapshot, source: StylesheetSource) => {
const startedAt = performance.now();
try {
const result = dependencies.compile(source);
observe({
name: "semantic_css.compile",
resumeId: snapshot.id,
durationMs: performance.now() - startedAt,
languageVersion: source.languageVersion,
sourceBytes: sourceBytes(source),
template: snapshot.data.metadata.template,
diagnosticCodes: result.diagnostics.map(({ code }) => code),
pageCount: null,
revision: snapshot.stylesheetRevision,
success: result.program !== null,
});
return result;
} catch (error) {
observe({
name: "semantic_css.compile",
resumeId: snapshot.id,
durationMs: performance.now() - startedAt,
languageVersion: source.languageVersion,
sourceBytes: sourceBytes(source),
template: snapshot.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: snapshot.stylesheetRevision,
success: false,
});
throw error;
}
};
const runPreflight = async (snapshot: StylesheetSnapshot, stylesheet: StylesheetSource) => {
const startedAt = performance.now();
try {
if (!dependencies.preflight) throw unavailableError();
const result = await dependencies.preflight({ data: snapshot.data, stylesheet });
observe({
name: "semantic_css.preflight",
resumeId: snapshot.id,
durationMs: performance.now() - startedAt,
languageVersion: stylesheet.languageVersion,
sourceBytes: sourceBytes(stylesheet),
template: snapshot.data.metadata.template,
diagnosticCodes: result.diagnostics.map(({ code }) => code),
pageCount: result.ok ? result.pageCount : null,
revision: snapshot.stylesheetRevision,
success: result.ok,
});
return result;
} catch (error) {
observe({
name: "semantic_css.preflight",
resumeId: snapshot.id,
durationMs: performance.now() - startedAt,
languageVersion: stylesheet.languageVersion,
sourceBytes: sourceBytes(stylesheet),
template: snapshot.data.metadata.template,
diagnosticCodes: [],
pageCount: null,
revision: snapshot.stylesheetRevision,
success: false,
});
throw error;
}
};
return {
getState: async (input) => stateFromSnapshot(await dependencies.readSnapshot(input), dependencies.convertLegacy),
mutate: async (input) => {
const snapshot = await dependencies.readSnapshot(input);
let diagnostics: SemanticCssDiagnostic[] = [];
let activationPageCount: number | null = null;
const activationStartedAt = performance.now();
const observeActivation = (success: boolean, revision: number) => {
if (input.transition !== "activate") return;
observe({
name: "semantic_css.activate",
resumeId: snapshot.id,
durationMs: performance.now() - activationStartedAt,
languageVersion: input.source.languageVersion,
sourceBytes: sourceBytes(input.source),
template: snapshot.data.metadata.template,
diagnosticCodes: diagnostics.map(({ code }) => code),
pageCount: activationPageCount,
revision,
success,
});
};
try {
if (snapshot.isLocked) throw new ORPCError("RESUME_LOCKED");
const current = await stylesheetFromSnapshot(snapshot, dependencies.convertLegacy);
let next = current;
let didPreflight = false;
if (input.transition === "edit_source") {
const compiled = compile(snapshot, input.source);
diagnostics = [...compiled.diagnostics];
next = { ...current, source: input.source };
if (compiled.program && current.mode === "semantic") {
const preflight = await runPreflight(snapshot, input.source);
didPreflight = true;
diagnostics = preflight.ok
? [...compiled.diagnostics, ...preflight.diagnostics]
: [...compiled.diagnostics, ...preflight.diagnostics, preflightDiagnostic(preflight)];
if (preflight.ok) next = { ...next, applied: input.source };
}
}
if (input.transition === "activate") {
const compiled = compile(snapshot, input.source);
diagnostics = [...compiled.diagnostics];
if (!compiled.program) {
throw validationError("The stylesheet cannot be activated because it is invalid.", compiled.diagnostics);
}
const parity = await dependencies.parity({
data: snapshot.data,
stylesheet: input.source,
resumeId: snapshot.id,
revision: snapshot.stylesheetRevision,
});
if (parity.mismatches.length > 0) {
throw new ORPCError("STYLESHEET_PARITY_FAILED", {
status: 400,
message: "The converted stylesheet does not preserve the legacy PDF presentation.",
data: { mismatches: parity.mismatches },
});
}
const preflight = await runPreflight(snapshot, input.source);
didPreflight = true;
activationPageCount = preflight.ok ? preflight.pageCount : null;
if (!preflight.ok) {
diagnostics = [...compiled.diagnostics, ...preflight.diagnostics, preflightDiagnostic(preflight)];
throw validationError("The stylesheet failed PDF preflight.", diagnostics);
}
diagnostics = [...compiled.diagnostics, ...preflight.diagnostics];
next = { mode: "semantic", source: input.source, applied: input.source };
}
if (input.transition === "deactivate") {
next = { ...current, mode: "legacy" };
}
if (input.transition === "restore_history") {
const compiled = compile(snapshot, input.restore.applied);
if (!compiled.program) {
throw validationError("The historical applied stylesheet is invalid.", compiled.diagnostics);
}
const preflight = await runPreflight(snapshot, input.restore.applied);
didPreflight = true;
if (!preflight.ok) {
throw validationError("The historical applied stylesheet failed PDF preflight.", [
...compiled.diagnostics,
...preflight.diagnostics,
preflightDiagnostic(preflight),
]);
}
diagnostics = [...compiled.diagnostics, ...preflight.diagnostics];
next = input.restore;
}
if (didPreflight) await dependencies.afterPreflight?.();
const updated = await dependencies.transaction(async (transaction) => {
const locked = await transaction.lock(input);
if (locked.isLocked) throw new ORPCError("RESUME_LOCKED");
if (!(await dependencies.compare(locked, input))) throw new StylesheetRevisionConflict(locked);
const data = parseStoredResumeData({
...locked.data,
metadata: { ...locked.data.metadata, stylesheet: next },
});
return transaction.update({ snapshot: locked, data });
});
await dependencies.publish(updated);
observeActivation(true, updated.stylesheetRevision);
return {
...(await stateFromSnapshot(updated, dependencies.convertLegacy)),
editGeneration: input.editGeneration,
diagnostics,
};
} catch (error) {
if (error instanceof StylesheetRevisionConflict) {
observeActivation(false, error.snapshot.stylesheetRevision);
throw new ORPCError("STYLESHEET_REVISION_CONFLICT", {
status: 409,
message: error.message,
data: {
state: await stateFromSnapshot(error.snapshot, dependencies.convertLegacy),
},
});
}
observeActivation(false, snapshot.stylesheetRevision);
throw error;
}
},
};
}
type StylesheetDatabase = Pick<typeof db, "select" | "transaction">;
type DatabaseStylesheetServiceOptions = {
database?: StylesheetDatabase;
runner?: StylesheetPreflightRunner;
afterPreflight?: () => Promise<void>;
publish?: (snapshot: StylesheetSnapshot) => Promise<void>;
};
const snapshotSelection = {
id: schema.resume.id,
userId: schema.resume.userId,
data: schema.resume.data,
isLocked: schema.resume.isLocked,
stylesheetRevision: schema.resume.stylesheetRevision,
renderDataVersion: schema.resume.renderDataVersion,
updatedAt: schema.resume.updatedAt,
};
export function createDatabaseStylesheetService(options: DatabaseStylesheetServiceOptions = {}) {
const database = options.database ?? db;
const runner = options.runner;
const readSnapshot = async (input: { id: string; userId: string }) => {
const [snapshot] = await database
.select(snapshotSelection)
.from(schema.resume)
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
if (!snapshot) throw new ORPCError("NOT_FOUND");
return snapshot;
};
return createStylesheetService({
readSnapshot,
convertLegacy: convertLegacyStylesheet,
compile: compileStylesheet,
...(runner
? {
preflight: ({ data, stylesheet }: { data: ResumeData; stylesheet: StylesheetSource }) =>
runner.run({
data,
template: data.metadata.template,
stylesheet,
}),
}
: {}),
parity: checkLegacyStylesheetParity,
transaction: (run) =>
database.transaction((transaction) =>
run({
lock: async (input) => {
const [snapshot] = await transaction
.select(snapshotSelection)
.from(schema.resume)
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)))
.for("update");
if (!snapshot) throw new ORPCError("NOT_FOUND");
return snapshot;
},
update: async ({ snapshot, data }) => {
const [updated] = await transaction
.update(schema.resume)
.set({
data,
stylesheetRevision: snapshot.stylesheetRevision + 1,
})
.where(and(eq(schema.resume.id, snapshot.id), eq(schema.resume.userId, snapshot.userId)))
.returning(snapshotSelection);
if (!updated) throw new ORPCError("NOT_FOUND");
return updated;
},
}),
),
compare: (snapshot, input) =>
snapshot.stylesheetRevision === input.expectedRevision &&
snapshot.renderDataVersion === input.expectedRenderDataVersion,
publish:
options.publish ??
(async (snapshot) => {
try {
await publishResumeUpdated({
type: "resume.updated",
resumeId: snapshot.id,
userId: snapshot.userId,
updatedAt: snapshot.updatedAt.toISOString(),
mutation: "stylesheet",
});
} catch (error) {
console.warn("Failed to publish resume.updated event:", error);
}
}),
...(options.afterPreflight ? { afterPreflight: options.afterPreflight } : {}),
});
}
@@ -0,0 +1,69 @@
import { protectedProcedure } from "../../context";
import { resumeDto } from "../../dto/resume";
import { resumeMutationRateLimit } from "../../middleware/rate-limit";
import { createDatabaseStylesheetService } from "./stylesheet-service";
const errors = {
SEMANTIC_STYLESHEET_UNAVAILABLE: {
message: "Semantic stylesheet PDF preflight is unavailable.",
status: 503,
},
STYLESHEET_VALIDATION_FAILED: {
message: "The stylesheet failed validation.",
status: 400,
data: resumeDto.stylesheet.errors.validation,
},
STYLESHEET_PARITY_FAILED: {
message: "The converted stylesheet does not preserve legacy PDF presentation.",
status: 400,
data: resumeDto.stylesheet.errors.parity,
},
STYLESHEET_REVISION_CONFLICT: {
message: "The resume or stylesheet changed while the candidate was being validated.",
status: 409,
data: resumeDto.stylesheet.errors.revisionConflict,
},
} as const;
export const stylesheetRouter = {
getState: protectedProcedure
.route({
method: "GET",
path: "/resumes/{id}/stylesheet",
tags: ["Resumes"],
operationId: "getResumeStylesheetState",
summary: "Get resume stylesheet state",
description: "Returns the owner-only canonical semantic stylesheet state and concurrency versions.",
successDescription: "The canonical stylesheet state.",
})
.input(resumeDto.stylesheet.getState.input)
.output(resumeDto.stylesheet.getState.output)
.handler(({ context, input }) =>
createDatabaseStylesheetService({
...(context.stylesheetPreflightRunner ? { runner: context.stylesheetPreflightRunner } : {}),
}).getState({ id: input.id, userId: context.user.id }),
),
mutate: protectedProcedure
.route({
method: "POST",
path: "/resumes/{id}/stylesheet",
tags: ["Resumes"],
operationId: "mutateResumeStylesheet",
summary: "Mutate resume stylesheet state",
description: "Applies one revisioned semantic stylesheet transition after validation and PDF preflight.",
successDescription: "The committed canonical stylesheet state.",
})
.input(resumeDto.stylesheet.mutate.input)
.use(resumeMutationRateLimit)
.output(resumeDto.stylesheet.mutate.output)
.errors(errors)
.handler(({ context, input }) =>
createDatabaseStylesheetService({
...(context.stylesheetPreflightRunner ? { runner: context.stylesheetPreflightRunner } : {}),
}).mutate({
...input,
userId: context.user.id,
}),
),
};
+41 -4
View File
@@ -2,6 +2,8 @@ import { protectedProcedure } from "../../context";
import { resumeDto } from "../../dto/resume";
import { resumeMutationRateLimit } from "../../middleware/rate-limit";
import { resumeService } from "./service";
import { convertLegacyStylesheet, validateHistoricalStylesheet } from "./stylesheet-preflight";
import { stylesheetFromSnapshot } from "./stylesheet-service";
export const versionsRouter = {
listVersions: protectedProcedure
@@ -35,11 +37,46 @@ export const versionsRouter = {
.input(resumeDto.restoreVersion.input)
.use(resumeMutationRateLimit)
.output(resumeDto.restoreVersion.output)
.handler(({ context, input }) =>
resumeService.versions.restore({
.errors({
SEMANTIC_STYLESHEET_UNAVAILABLE: {
message: "Semantic stylesheet PDF preflight is unavailable.",
status: 503,
},
STYLESHEET_VALIDATION_FAILED: {
message: "The historical stylesheet failed validation.",
status: 400,
},
})
.handler(async ({ context, input }) => {
const resume = await resumeService.versions.restore({
resumeId: input.resumeId,
versionId: input.versionId,
userId: context.user.id,
}),
),
prepareData: async ({ data, stylesheetRevision }) => {
const stylesheet = data.metadata.stylesheet;
if (!stylesheet) return data;
const validated = await validateHistoricalStylesheet({
data,
resumeId: input.resumeId,
revision: stylesheetRevision,
stylesheet,
...(context.stylesheetPreflightRunner ? { runner: context.stylesheetPreflightRunner } : {}),
});
return {
...data,
metadata: { ...data.metadata, stylesheet: validated },
};
},
});
const stylesheet = await stylesheetFromSnapshot({ ...resume, userId: context.user.id }, convertLegacyStylesheet);
return {
resume,
stylesheetState: {
stylesheet,
revision: resume.stylesheetRevision,
renderDataVersion: resume.renderDataVersion,
},
};
}),
};