diff --git a/orchestrator/package-lock.json b/orchestrator/package-lock.json index 3fcd2ce..9e01d12 100644 --- a/orchestrator/package-lock.json +++ b/orchestrator/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@hookform/resolvers": "^5.2.2", + "@paralleldrive/cuid2": "^3.0.6", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.2", @@ -1485,6 +1486,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1523,6 +1535,19 @@ "node": ">= 8" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-3.0.6.tgz", + "integrity": "sha512-ujtxTTvr4fwPrzuQT7o6VLKs5BzdWetR9+/zRQ0SyK9hVIwZQllEccxgcHYXN6I3Z429y1yg3F6+uiVxMDPrLQ==", + "dependencies": { + "@noble/hashes": "^2.0.1", + "bignumber.js": "^9.3.1", + "error-causes": "^3.0.2" + }, + "bin": { + "cuid2": "bin/cuid2.js" + } + }, "node_modules/@petamoriken/float16": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", @@ -3780,6 +3805,14 @@ "prebuild-install": "^7.1.1" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4708,6 +4741,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/error-causes": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/error-causes/-/error-causes-3.0.2.tgz", + "integrity": "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", diff --git a/orchestrator/package.json b/orchestrator/package.json index 3999cd9..56c5b4e 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@hookform/resolvers": "^5.2.2", + "@paralleldrive/cuid2": "^3.0.6", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.2", diff --git a/orchestrator/src/server/services/pdf.ts b/orchestrator/src/server/services/pdf.ts index 4c32e84..ad50866 100644 --- a/orchestrator/src/server/services/pdf.ts +++ b/orchestrator/src/server/services/pdf.ts @@ -8,7 +8,7 @@ import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { readFile, writeFile, mkdir, access, unlink } from 'fs/promises'; import { existsSync } from 'fs'; -import crypto from 'node:crypto'; +import { createId } from '@paralleldrive/cuid2'; import { getSetting } from '../repositories/settings.js'; import { pickProjectIdsForJob } from './projectSelection.js'; @@ -20,26 +20,6 @@ import { resumeDataSchema } from '../../shared/rxresume-schema.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); -/** - * Generate a CUID2-compatible ID for RXResume. - * CUID2 format: starts with a letter, lowercase alphanumeric, ~24 chars - */ -function generateCuid2(): string { - const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; - const letters = 'abcdefghijklmnopqrstuvwxyz'; - const bytes = crypto.randomBytes(24); - - // First char must be a letter - let result = letters[bytes[0] % letters.length]; - - // Rest can be alphanumeric - for (let i = 1; i < 24; i++) { - result += alphabet[bytes[i] % alphabet.length]; - } - - return result; -} - // Paths - can be overridden via env for Docker const RESUME_GEN_DIR = process.env.RESUME_GEN_DIR || join(__dirname, '../../../../resume-generator'); const OUTPUT_DIR = join(getDataDir(), 'pdfs'); @@ -92,7 +72,7 @@ export async function generatePdf( if (baseResume.sections?.skills?.items && Array.isArray(baseResume.sections.skills.items)) { baseResume.sections.skills.items = baseResume.sections.skills.items.map((skill: any) => ({ ...skill, - id: skill.id || generateCuid2(), + id: skill.id || createId(), visible: skill.visible ?? true, // Zod schema requires string, default to empty string if missing description: skill.description ?? '', @@ -135,7 +115,7 @@ export async function generatePdf( const existing = existingSkills.find((s: any) => s.name === newSkill.name); return { - id: newSkill.id || existing?.id || generateCuid2(), + id: newSkill.id || existing?.id || createId(), visible: newSkill.visible !== undefined ? newSkill.visible : (existing?.visible ?? true), name: newSkill.name || existing?.name || '', description: newSkill.description !== undefined ? newSkill.description : (existing?.description || ''),