@cvfile/server 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fastify.js CHANGED
@@ -1,18 +1,14 @@
1
1
  import { stat, readFile } from 'fs/promises';
2
2
  import { resolve, normalize, sep } from 'path';
3
3
  import { isCvFile, extract } from '@cvfile/sdk';
4
+ import { createHash } from 'crypto';
4
5
 
5
6
  // src/handler.ts
6
7
 
7
8
  // src/conneg.ts
8
- var FORMAT_BY_MIME = {
9
- "text/markdown": "markdown",
10
- "text/x-markdown": "markdown",
11
- "text/html": "html",
12
- "application/xhtml+xml": "html",
13
- "application/pdf": "pdf",
14
- "application/vnd.cv+pdf": "pdf"
15
- };
9
+ var MARKDOWN_MIMES = /* @__PURE__ */ new Set(["text/markdown", "text/x-markdown", "application/vnd.cv+markdown"]);
10
+ var PDF_MIMES = /* @__PURE__ */ new Set(["application/pdf", "application/vnd.cv+pdf"]);
11
+ var HTML_MIMES = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
16
12
  var FORMAT_BY_QUERY = {
17
13
  md: "markdown",
18
14
  markdown: "markdown",
@@ -24,13 +20,21 @@ function parseAccept(header) {
24
20
  if (!header) return [];
25
21
  return header.split(",").map((part) => {
26
22
  const [type, ...params] = part.trim().split(";").map((s) => s.trim());
27
- let q = 1;
28
- for (const p of params) {
29
- const m = p.match(/^q\s*=\s*(\d*\.?\d+)/i);
30
- if (m) q = Number(m[1]);
31
- }
23
+ const q = parseQ(params);
24
+ if (q === null) return null;
32
25
  return { type: (type ?? "").toLowerCase(), q };
33
- }).filter((p) => p.type).sort((a, b) => b.q - a.q);
26
+ }).filter((p) => p !== null && p.type !== "" && p.q > 0).sort((a, b) => b.q - a.q);
27
+ }
28
+ function parseQ(params) {
29
+ for (const p of params) {
30
+ if (!/^q\s*=/i.test(p)) continue;
31
+ const m = p.match(/^q\s*=\s*(\d*\.?\d+)\s*$/i);
32
+ if (!m) return null;
33
+ const value = Number(m[1]);
34
+ if (Number.isNaN(value)) return null;
35
+ return Math.min(1, Math.max(0, value));
36
+ }
37
+ return 1;
34
38
  }
35
39
  function parseAcceptLanguage(header) {
36
40
  if (!header) return [];
@@ -52,20 +56,32 @@ function negotiate(input) {
52
56
  return { format: fromQuery, language };
53
57
  }
54
58
  }
55
- const accepts = parseAccept(input.accept);
56
- for (const a of accepts) {
57
- const direct = FORMAT_BY_MIME[a.type];
58
- if (direct) {
59
- return { format: direct, language };
60
- }
61
- if (a.type === "*/*" || a.type === "application/*") {
62
- return { format: "pdf", language };
63
- }
64
- if (a.type === "text/*") {
65
- return { format: "html", language };
66
- }
59
+ const fromAccept = negotiateFromAccept(input.accept);
60
+ const format = fromAccept ?? input.defaultFormat ?? "pdf";
61
+ return { format, language };
62
+ }
63
+ function negotiateFromAccept(header) {
64
+ const accepts = parseAccept(header);
65
+ if (accepts.length === 0) return void 0;
66
+ const topQ = accepts[0].q;
67
+ const top = accepts.filter((a) => a.q === topQ);
68
+ const hasWildcard = accepts.some((a) => a.type === "*/*" || a.type === "application/*");
69
+ if (top.some((a) => MARKDOWN_MIMES.has(a.type))) {
70
+ return "markdown";
71
+ }
72
+ if (top.some((a) => PDF_MIMES.has(a.type))) {
73
+ return "pdf";
74
+ }
75
+ if (top.some((a) => HTML_MIMES.has(a.type)) && !hasWildcard) {
76
+ return "html";
67
77
  }
68
- return { format: "pdf", language };
78
+ if (hasWildcard || top.some((a) => HTML_MIMES.has(a.type))) {
79
+ return "pdf";
80
+ }
81
+ if (top.some((a) => a.type === "text/*")) {
82
+ return "html";
83
+ }
84
+ return void 0;
69
85
  }
70
86
  function buildLinkHeader({ selfUrl, cvMime = "application/vnd.cv+pdf" }) {
71
87
  const sep2 = selfUrl.includes("?") ? "&" : "?";
@@ -81,7 +97,8 @@ async function serveCv(req) {
81
97
  const decision = negotiate({
82
98
  accept: req.accept,
83
99
  acceptLanguage: req.acceptLanguage,
84
- formatQuery: req.formatQuery
100
+ formatQuery: req.formatQuery,
101
+ defaultFormat: req.defaultFormat
85
102
  });
86
103
  if (decision.format === "pdf") {
87
104
  return {
@@ -98,7 +115,7 @@ async function serveCv(req) {
98
115
  if (md) {
99
116
  return {
100
117
  format: "markdown",
101
- contentType: `text/markdown; charset=utf-8; cv-language=${md.language ?? preferLang}`,
118
+ contentType: "text/markdown; charset=utf-8",
102
119
  ...md.language !== void 0 ? { language: md.language } : {},
103
120
  body: md.bytes
104
121
  };
@@ -110,7 +127,7 @@ async function serveCv(req) {
110
127
  if (html) {
111
128
  return {
112
129
  format: "html",
113
- contentType: `text/html; charset=utf-8; cv-language=${html.language ?? preferLang}`,
130
+ contentType: "text/html; charset=utf-8",
114
131
  ...html.language !== void 0 ? { language: html.language } : {},
115
132
  body: html.bytes
116
133
  };
@@ -151,6 +168,108 @@ function renderMarkdownAsHtml(md, file) {
151
168
  </html>`;
152
169
  }
153
170
 
171
+ // src/response.ts
172
+ async function buildCvResponse(input) {
173
+ const result = await serveCv({
174
+ bytes: input.bytes,
175
+ accept: input.accept,
176
+ acceptLanguage: input.acceptLanguage,
177
+ formatQuery: input.formatQuery,
178
+ defaultFormat: input.defaultFormat
179
+ });
180
+ const etag = computeETag(result.body, result.format);
181
+ const lastModified = input.lastModified?.toUTCString();
182
+ const headers = {
183
+ "Content-Type": result.contentType,
184
+ Vary: "Accept, Accept-Language",
185
+ Link: buildLinkHeader({ selfUrl: input.selfUrl, cvMime: PDF_PRIMARY_MIME }),
186
+ "Cache-Control": input.cacheControl,
187
+ ETag: etag,
188
+ "Content-Disposition": contentDisposition(input.selfUrl, result.format)
189
+ };
190
+ if (result.language) {
191
+ headers["Content-Language"] = result.language;
192
+ }
193
+ if (lastModified) {
194
+ headers["Last-Modified"] = lastModified;
195
+ }
196
+ const notModified = isNotModified({
197
+ etag,
198
+ lastModified: input.lastModified,
199
+ ifNoneMatch: input.ifNoneMatch,
200
+ ifModifiedSince: input.ifModifiedSince
201
+ });
202
+ if (notModified) {
203
+ return { status: 304, headers, format: result.format, body: new Uint8Array(0) };
204
+ }
205
+ headers["Content-Length"] = String(result.body.length);
206
+ return { status: 200, headers, format: result.format, body: result.body };
207
+ }
208
+ function computeETag(body, format) {
209
+ const hash = createHash("sha1").update(body).digest("base64url");
210
+ return `W/"${format}-${body.length.toString(16)}-${hash}"`;
211
+ }
212
+ function isNotModified({ etag, lastModified, ifNoneMatch, ifModifiedSince }) {
213
+ if (ifNoneMatch) {
214
+ return etagMatches(ifNoneMatch, etag);
215
+ }
216
+ if (ifModifiedSince && lastModified) {
217
+ const since = Date.parse(ifModifiedSince);
218
+ if (!Number.isNaN(since)) {
219
+ return Math.floor(lastModified.getTime() / 1e3) <= Math.floor(since / 1e3);
220
+ }
221
+ }
222
+ return false;
223
+ }
224
+ function etagMatches(ifNoneMatch, etag) {
225
+ if (ifNoneMatch.trim() === "*") return true;
226
+ const normalize2 = (tag) => tag.trim().replace(/^W\//, "");
227
+ const target = normalize2(etag);
228
+ return ifNoneMatch.split(",").some((candidate) => normalize2(candidate) === target);
229
+ }
230
+ function contentDisposition(selfUrl, format) {
231
+ const filename = filenameForFormat(selfUrl, format);
232
+ const asciiSafe = sanitizeAsciiFilename(filename);
233
+ const base = `inline; filename="${asciiSafe}"`;
234
+ if (!hasNonAscii(filename)) return base;
235
+ return `${base}; filename*=UTF-8''${encodeRFC5987(filename)}`;
236
+ }
237
+ function sanitizeAsciiFilename(filename) {
238
+ let out = "";
239
+ for (const ch of filename) {
240
+ const code = ch.codePointAt(0) ?? 0;
241
+ if (code < 32 || code === 127) continue;
242
+ if (ch === '"' || ch === "\\") continue;
243
+ out += code > 126 ? "_" : ch;
244
+ }
245
+ return out || "document";
246
+ }
247
+ function hasNonAscii(value) {
248
+ for (const ch of value) {
249
+ const code = ch.codePointAt(0) ?? 0;
250
+ if (code < 32 || code > 126) return true;
251
+ }
252
+ return false;
253
+ }
254
+ function filenameForFormat(selfUrl, format) {
255
+ const pathname = selfUrl.split("?")[0] ?? selfUrl;
256
+ const base = decodeOrRaw(pathname.split("/").pop() ?? "document");
257
+ const stem = base.replace(/\.cv$/i, "").replace(/\.(pdf|md|html)$/i, "") || "document";
258
+ if (format === "markdown") return `${stem}.md`;
259
+ if (format === "html") return `${stem}.html`;
260
+ return `${stem}.cv`;
261
+ }
262
+ function decodeOrRaw(value) {
263
+ try {
264
+ return decodeURIComponent(value);
265
+ } catch {
266
+ return value;
267
+ }
268
+ }
269
+ function encodeRFC5987(value) {
270
+ return encodeURIComponent(value).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
271
+ }
272
+
154
273
  // src/handler.ts
155
274
  function cvHandler(options = {}) {
156
275
  const { root, loader, cacheControl = "public, max-age=300", defaultFormat } = options;
@@ -162,47 +281,48 @@ function cvHandler(options = {}) {
162
281
  try {
163
282
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
164
283
  const logical = decodeURIComponent(url.pathname);
165
- const formatQuery = url.searchParams.get("format") ?? defaultFormat;
166
- const bytes = await load(logical, { baseRoot, loader });
167
- if (!bytes) {
284
+ const loaded = await load(logical, { baseRoot, loader });
285
+ if (!loaded) {
168
286
  res.statusCode = 404;
169
287
  res.end("Not found");
170
288
  return;
171
289
  }
172
- if (!await isCvFile(bytes)) {
290
+ if (!await isCvFile(loaded.bytes)) {
173
291
  res.statusCode = 415;
174
292
  res.end("Not a .cv file");
175
293
  return;
176
294
  }
177
- const result = await serveCv({
178
- bytes,
295
+ const built = await buildCvResponse({
296
+ bytes: loaded.bytes,
297
+ selfUrl: url.pathname,
179
298
  accept: req.headers["accept"],
180
299
  acceptLanguage: req.headers["accept-language"],
181
- formatQuery: formatQuery ?? void 0
300
+ formatQuery: url.searchParams.get("format") ?? void 0,
301
+ defaultFormat,
302
+ cacheControl,
303
+ lastModified: loaded.lastModified,
304
+ ifNoneMatch: req.headers["if-none-match"],
305
+ ifModifiedSince: req.headers["if-modified-since"]
182
306
  });
183
- const link = buildLinkHeader({ selfUrl: url.pathname, cvMime: PDF_PRIMARY_MIME });
184
- res.setHeader("Content-Type", result.contentType);
185
- res.setHeader("Content-Length", String(result.body.length));
186
- res.setHeader("Vary", "Accept, Accept-Language");
187
- res.setHeader("Link", link);
188
- res.setHeader("Cache-Control", cacheControl);
189
- if (result.language) {
190
- res.setHeader("Content-Language", result.language);
307
+ for (const [name, value] of Object.entries(built.headers)) {
308
+ res.setHeader(name, value);
309
+ }
310
+ res.statusCode = built.status;
311
+ res.end(built.status === 304 ? void 0 : Buffer.from(built.body));
312
+ } catch {
313
+ if (res.headersSent) {
314
+ res.end();
315
+ return;
191
316
  }
192
- const filename = filenameForFormat(logical, result.format);
193
- res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
194
- res.statusCode = 200;
195
- res.end(Buffer.from(result.body));
196
- } catch (err) {
197
317
  res.statusCode = 500;
198
- res.end(`cvHandler error: ${err.message}`);
318
+ res.end("Internal Server Error");
199
319
  }
200
320
  };
201
321
  }
202
322
  async function load(logicalPath, { baseRoot, loader }) {
203
323
  if (loader) {
204
324
  const bytes = await loader(logicalPath);
205
- return bytes ?? null;
325
+ return bytes ? { bytes } : null;
206
326
  }
207
327
  if (!baseRoot) return null;
208
328
  const safe = normalize(logicalPath).replace(/^[/\\]+/, "");
@@ -213,7 +333,7 @@ async function load(logicalPath, { baseRoot, loader }) {
213
333
  try {
214
334
  const s = await stat(full);
215
335
  if (!s.isFile()) return null;
216
- return new Uint8Array(await readFile(full));
336
+ return { bytes: new Uint8Array(await readFile(full)), lastModified: s.mtime };
217
337
  } catch {
218
338
  return null;
219
339
  }
@@ -223,23 +343,18 @@ function isWithin(parent, child) {
223
343
  const base = resolve(parent);
224
344
  return rel === base || rel.startsWith(base + sep);
225
345
  }
226
- function filenameForFormat(logical, format) {
227
- const base = logical.split("/").pop() ?? "document";
228
- const stem = base.replace(/\.cv$/i, "").replace(/\.(pdf|md|html)$/i, "") || "document";
229
- if (format === "markdown") return `${stem}.md`;
230
- if (format === "html") return `${stem}.html`;
231
- return `${stem}.cv`;
232
- }
233
346
 
234
347
  // src/fastify.ts
235
348
  var cvFastifyPlugin = async (fastify, opts) => {
236
349
  const handler = cvHandler(opts);
237
350
  const route = `${opts.prefix ?? ""}/*`;
238
351
  fastify.get(route, async (request, reply) => {
239
- if (!request.url.toLowerCase().includes(".cv")) {
352
+ const { pathname } = new URL(request.url, `http://${request.headers.host ?? "localhost"}`);
353
+ if (!decodeURIComponent(pathname).toLowerCase().endsWith(".cv")) {
240
354
  reply.code(404).send("Not found");
241
355
  return;
242
356
  }
357
+ reply.hijack();
243
358
  await handler(request.raw, reply.raw);
244
359
  });
245
360
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/conneg.ts","../src/serve.ts","../src/handler.ts","../src/fastify.ts"],"names":["sep"],"mappings":"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","file":"fastify.js","sourcesContent":["export type ServeFormat = 'pdf' | 'markdown' | 'html';\n\nexport interface NegotiationInput {\n accept?: string | undefined;\n acceptLanguage?: string | undefined;\n formatQuery?: string | undefined;\n}\n\nexport interface NegotiationResult {\n format: ServeFormat;\n language: string | undefined;\n}\n\nconst FORMAT_BY_MIME: Record<string, ServeFormat> = {\n 'text/markdown': 'markdown',\n 'text/x-markdown': 'markdown',\n 'text/html': 'html',\n 'application/xhtml+xml': 'html',\n 'application/pdf': 'pdf',\n 'application/vnd.cv+pdf': 'pdf',\n};\n\nconst FORMAT_BY_QUERY: Record<string, ServeFormat> = {\n md: 'markdown',\n markdown: 'markdown',\n html: 'html',\n pdf: 'pdf',\n cv: 'pdf',\n};\n\ninterface ParsedAccept {\n type: string;\n q: number;\n}\n\nexport function parseAccept(header: string | undefined | null): ParsedAccept[] {\n if (!header) return [];\n return header\n .split(',')\n .map((part) => {\n const [type, ...params] = part.trim().split(';').map((s) => s.trim());\n let q = 1;\n for (const p of params) {\n const m = p.match(/^q\\s*=\\s*(\\d*\\.?\\d+)/i);\n if (m) q = Number(m[1]);\n }\n return { type: (type ?? '').toLowerCase(), q };\n })\n .filter((p) => p.type)\n .sort((a, b) => b.q - a.q);\n}\n\nexport function parseAcceptLanguage(header: string | undefined | null): string[] {\n if (!header) return [];\n return header\n .split(',')\n .map((part) => {\n const [tag, ...params] = part.trim().split(';').map((s) => s.trim());\n let q = 1;\n for (const p of params) {\n const m = p.match(/^q\\s*=\\s*(\\d*\\.?\\d+)/i);\n if (m) q = Number(m[1]);\n }\n return { tag: (tag ?? '').toLowerCase(), q };\n })\n .filter((p) => p.tag && p.tag !== '*')\n .sort((a, b) => b.q - a.q)\n .map((p) => p.tag);\n}\n\nexport function negotiate(input: NegotiationInput): NegotiationResult {\n const language = parseAcceptLanguage(input.acceptLanguage)[0];\n\n if (input.formatQuery) {\n const fromQuery = FORMAT_BY_QUERY[input.formatQuery.toLowerCase()];\n if (fromQuery) {\n return { format: fromQuery, language };\n }\n }\n\n const accepts = parseAccept(input.accept);\n for (const a of accepts) {\n const direct = FORMAT_BY_MIME[a.type];\n if (direct) {\n return { format: direct, language };\n }\n if (a.type === '*/*' || a.type === 'application/*') {\n return { format: 'pdf', language };\n }\n if (a.type === 'text/*') {\n return { format: 'html', language };\n }\n }\n\n return { format: 'pdf', language };\n}\n\nexport interface BuildLinkHeaderInput {\n selfUrl: string;\n cvMime?: string;\n}\n\nexport function buildLinkHeader({ selfUrl, cvMime = 'application/vnd.cv+pdf' }: BuildLinkHeaderInput): string {\n const sep = selfUrl.includes('?') ? '&' : '?';\n return [\n `<${selfUrl}>; rel=\"alternate\"; type=\"${cvMime}\"`,\n `<${selfUrl}${sep}format=md>; rel=\"alternate\"; type=\"text/markdown\"`,\n `<${selfUrl}${sep}format=html>; rel=\"alternate\"; type=\"text/html\"`,\n ].join(', ');\n}\n\nexport const PDF_PRIMARY_MIME = 'application/vnd.cv+pdf';\nexport const PDF_FALLBACK_MIME = 'application/pdf';\n","import { extract } from '@cvfile/sdk';\nimport type { CvFile, ExtractedPayload } from '@cvfile/sdk';\nimport { negotiate, type ServeFormat } from './conneg.js';\n\nexport interface ServeRequest {\n bytes: Uint8Array;\n accept?: string | undefined;\n acceptLanguage?: string | undefined;\n formatQuery?: string | undefined;\n}\n\nexport interface ServeResponse {\n format: ServeFormat;\n contentType: string;\n language?: string | undefined;\n body: Uint8Array;\n}\n\nconst ENCODER = new TextEncoder();\n\nexport async function serveCv(req: ServeRequest): Promise<ServeResponse> {\n const decision = negotiate({\n accept: req.accept,\n acceptLanguage: req.acceptLanguage,\n formatQuery: req.formatQuery,\n });\n\n if (decision.format === 'pdf') {\n return {\n format: 'pdf',\n contentType: 'application/vnd.cv+pdf',\n ...(decision.language !== undefined ? { language: decision.language } : {}),\n body: req.bytes,\n };\n }\n\n const file = await extract(req.bytes);\n const preferLang = decision.language ?? file.metadata.primaryLanguage;\n\n if (decision.format === 'markdown') {\n const md = pickPayload(file, 'text/markdown', preferLang);\n if (md) {\n return {\n format: 'markdown',\n contentType: `text/markdown; charset=utf-8; cv-language=${md.language ?? preferLang}`,\n ...(md.language !== undefined ? { language: md.language } : {}),\n body: md.bytes,\n };\n }\n return fallbackToPdf(req.bytes);\n }\n\n if (decision.format === 'html') {\n const html = pickPayload(file, 'text/html', preferLang);\n if (html) {\n return {\n format: 'html',\n contentType: `text/html; charset=utf-8; cv-language=${html.language ?? preferLang}`,\n ...(html.language !== undefined ? { language: html.language } : {}),\n body: html.bytes,\n };\n }\n const md = pickPayload(file, 'text/markdown', preferLang);\n if (md) {\n const body = ENCODER.encode(renderMarkdownAsHtml(md.text(), file));\n return {\n format: 'html',\n contentType: 'text/html; charset=utf-8',\n ...(md.language !== undefined ? { language: md.language } : {}),\n body,\n };\n }\n return fallbackToPdf(req.bytes);\n }\n\n return fallbackToPdf(req.bytes);\n}\n\nfunction pickPayload(file: CvFile, mimeType: string, preferLang: string): ExtractedPayload | undefined {\n const matches = file.payloads.filter((p) => p.mimeType === mimeType);\n if (matches.length === 0) return undefined;\n return matches.find((p) => p.language === preferLang) ?? matches[0];\n}\n\nfunction fallbackToPdf(bytes: Uint8Array): ServeResponse {\n return { format: 'pdf', contentType: 'application/vnd.cv+pdf', body: bytes };\n}\n\nfunction renderMarkdownAsHtml(md: string, file: CvFile): string {\n const safe = md\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n return `<!doctype html>\n<html lang=\"${file.metadata.primaryLanguage}\">\n<head>\n<meta charset=\"utf-8\">\n<title>${file.metadata.primaryPayload}</title>\n</head>\n<body>\n<pre>${safe}</pre>\n</body>\n</html>`;\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { normalize, resolve, sep } from 'node:path';\nimport { isCvFile } from '@cvfile/sdk';\nimport { buildLinkHeader, PDF_PRIMARY_MIME } from './conneg.js';\nimport { serveCv } from './serve.js';\n\nexport interface CvHandlerOptions {\n root?: string;\n loader?: (logicalPath: string) => Promise<Uint8Array | null>;\n cacheControl?: string;\n defaultFormat?: 'pdf' | 'markdown' | 'html';\n}\n\nexport type CvHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport function cvHandler(options: CvHandlerOptions = {}): CvHandler {\n const { root, loader, cacheControl = 'public, max-age=300', defaultFormat } = options;\n if (!root && !loader) {\n throw new Error('cvHandler requires either { root } or { loader }');\n }\n\n const baseRoot = root ? resolve(root) : null;\n\n return async (req, res) => {\n try {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const logical = decodeURIComponent(url.pathname);\n const formatQuery = url.searchParams.get('format') ?? defaultFormat;\n\n const bytes = await load(logical, { baseRoot, loader });\n if (!bytes) {\n res.statusCode = 404;\n res.end('Not found');\n return;\n }\n\n if (!(await isCvFile(bytes))) {\n res.statusCode = 415;\n res.end('Not a .cv file');\n return;\n }\n\n const result = await serveCv({\n bytes,\n accept: req.headers['accept'],\n acceptLanguage: req.headers['accept-language'],\n formatQuery: formatQuery ?? undefined,\n });\n\n const link = buildLinkHeader({ selfUrl: url.pathname, cvMime: PDF_PRIMARY_MIME });\n\n res.setHeader('Content-Type', result.contentType);\n res.setHeader('Content-Length', String(result.body.length));\n res.setHeader('Vary', 'Accept, Accept-Language');\n res.setHeader('Link', link);\n res.setHeader('Cache-Control', cacheControl);\n if (result.language) {\n res.setHeader('Content-Language', result.language);\n }\n const filename = filenameForFormat(logical, result.format);\n res.setHeader('Content-Disposition', `inline; filename=\"${filename}\"`);\n res.statusCode = 200;\n res.end(Buffer.from(result.body));\n } catch (err) {\n res.statusCode = 500;\n res.end(`cvHandler error: ${(err as Error).message}`);\n }\n };\n}\n\ninterface LoadOpts {\n baseRoot: string | null;\n loader?: ((logicalPath: string) => Promise<Uint8Array | null>) | undefined;\n}\n\nasync function load(logicalPath: string, { baseRoot, loader }: LoadOpts): Promise<Uint8Array | null> {\n if (loader) {\n const bytes = await loader(logicalPath);\n return bytes ?? null;\n }\n if (!baseRoot) return null;\n const safe = normalize(logicalPath).replace(/^[/\\\\]+/, '');\n const full = resolve(baseRoot, safe);\n if (!isWithin(baseRoot, full)) {\n return null;\n }\n try {\n const s = await stat(full);\n if (!s.isFile()) return null;\n return new Uint8Array(await readFile(full));\n } catch {\n return null;\n }\n}\n\nfunction isWithin(parent: string, child: string): boolean {\n const rel = resolve(child);\n const base = resolve(parent);\n return rel === base || rel.startsWith(base + sep);\n}\n\nfunction filenameForFormat(logical: string, format: 'pdf' | 'markdown' | 'html'): string {\n const base = logical.split('/').pop() ?? 'document';\n const stem = base.replace(/\\.cv$/i, '').replace(/\\.(pdf|md|html)$/i, '') || 'document';\n if (format === 'markdown') return `${stem}.md`;\n if (format === 'html') return `${stem}.html`;\n return `${stem}.cv`;\n}\n\n","import type { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';\nimport { cvHandler, type CvHandlerOptions } from './handler.js';\n\nexport interface CvFastifyOptions extends CvHandlerOptions {\n prefix?: string;\n}\n\nexport const cvFastifyPlugin: FastifyPluginAsync<CvFastifyOptions> = async (\n fastify: FastifyInstance,\n opts: CvFastifyOptions,\n) => {\n const handler = cvHandler(opts);\n const route = `${opts.prefix ?? ''}/*`;\n fastify.get(route, async (request: FastifyRequest, reply: FastifyReply) => {\n if (!request.url.toLowerCase().includes('.cv')) {\n reply.code(404).send('Not found');\n return;\n }\n await handler(request.raw, reply.raw);\n });\n};\n\nexport default cvFastifyPlugin;\n"]}
1
+ {"version":3,"sources":["../src/conneg.ts","../src/serve.ts","../src/response.ts","../src/handler.ts","../src/fastify.ts"],"names":["sep","normalize"],"mappings":"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","file":"fastify.js","sourcesContent":["export type ServeFormat = 'pdf' | 'markdown' | 'html';\n\nexport interface NegotiationInput {\n accept?: string | undefined;\n acceptLanguage?: string | undefined;\n formatQuery?: string | undefined;\n defaultFormat?: ServeFormat | undefined;\n}\n\nexport interface NegotiationResult {\n format: ServeFormat;\n language: string | undefined;\n}\n\nconst MARKDOWN_MIMES = new Set(['text/markdown', 'text/x-markdown', 'application/vnd.cv+markdown']);\nconst PDF_MIMES = new Set(['application/pdf', 'application/vnd.cv+pdf']);\nconst HTML_MIMES = new Set(['text/html', 'application/xhtml+xml']);\n\nconst FORMAT_BY_QUERY: Record<string, ServeFormat> = {\n md: 'markdown',\n markdown: 'markdown',\n html: 'html',\n pdf: 'pdf',\n cv: 'pdf',\n};\n\ninterface ParsedAccept {\n type: string;\n q: number;\n}\n\nexport function parseAccept(header: string | undefined | null): ParsedAccept[] {\n if (!header) return [];\n return header\n .split(',')\n .map((part): ParsedAccept | null => {\n const [type, ...params] = part.trim().split(';').map((s) => s.trim());\n const q = parseQ(params);\n // A malformed q (present but unparseable) marks the type as unusable per RFC 9110.\n if (q === null) return null;\n return { type: (type ?? '').toLowerCase(), q };\n })\n .filter((p): p is ParsedAccept => p !== null && p.type !== '' && p.q > 0)\n .sort((a, b) => b.q - a.q);\n}\n\n/**\n * Resolve the q-value of a media-range's parameters.\n * Returns 1 when absent, the clamped [0,1] value when valid, and null when a\n * q parameter is present but cannot be parsed (signalling a malformed type).\n */\nfunction parseQ(params: string[]): number | null {\n for (const p of params) {\n if (!/^q\\s*=/i.test(p)) continue;\n const m = p.match(/^q\\s*=\\s*(\\d*\\.?\\d+)\\s*$/i);\n if (!m) return null;\n const value = Number(m[1]);\n if (Number.isNaN(value)) return null;\n return Math.min(1, Math.max(0, value));\n }\n return 1;\n}\n\nexport function parseAcceptLanguage(header: string | undefined | null): string[] {\n if (!header) return [];\n return header\n .split(',')\n .map((part) => {\n const [tag, ...params] = part.trim().split(';').map((s) => s.trim());\n let q = 1;\n for (const p of params) {\n const m = p.match(/^q\\s*=\\s*(\\d*\\.?\\d+)/i);\n if (m) q = Number(m[1]);\n }\n return { tag: (tag ?? '').toLowerCase(), q };\n })\n .filter((p) => p.tag && p.tag !== '*')\n .sort((a, b) => b.q - a.q)\n .map((p) => p.tag);\n}\n\nexport function negotiate(input: NegotiationInput): NegotiationResult {\n const language = parseAcceptLanguage(input.acceptLanguage)[0];\n\n // An explicit ?format= query is the only override and wins over Accept.\n if (input.formatQuery) {\n const fromQuery = FORMAT_BY_QUERY[input.formatQuery.toLowerCase()];\n if (fromQuery) {\n return { format: fromQuery, language };\n }\n }\n\n const fromAccept = negotiateFromAccept(input.accept);\n const format = fromAccept ?? input.defaultFormat ?? 'pdf';\n return { format, language };\n}\n\n/**\n * Map an Accept header to a format following the .cv contract:\n * - markdown only when it is an explicit, top, non-wildcard preference;\n * - html only when text/html is requested without a wildcard (a deliberate fetch);\n * - pdf for the browser case (text/html alongside a wildcard, or any wildcard);\n * - undefined when the header expresses no usable preference (caller falls back).\n */\nfunction negotiateFromAccept(header: string | undefined): ServeFormat | undefined {\n const accepts = parseAccept(header);\n if (accepts.length === 0) return undefined;\n\n const topQ = accepts[0]!.q;\n const top = accepts.filter((a) => a.q === topQ);\n const hasWildcard = accepts.some((a) => a.type === '*/*' || a.type === 'application/*');\n\n // Markdown wins only as an explicit, top, non-wildcard preference.\n if (top.some((a) => MARKDOWN_MIMES.has(a.type))) {\n return 'markdown';\n }\n\n // An explicit, top preference for the PDF type also serves PDF.\n if (top.some((a) => PDF_MIMES.has(a.type))) {\n return 'pdf';\n }\n\n // A deliberate HTML fetch: text/html requested without a catch-all wildcard.\n if (top.some((a) => HTML_MIMES.has(a.type)) && !hasWildcard) {\n return 'html';\n }\n\n // Browser case (text/html + */*) or any wildcard: serve the visual PDF.\n if (hasWildcard || top.some((a) => HTML_MIMES.has(a.type))) {\n return 'pdf';\n }\n\n // text/* (without a more specific match) is a deliberate text fetch -> html.\n if (top.some((a) => a.type === 'text/*')) {\n return 'html';\n }\n\n return undefined;\n}\n\nexport interface BuildLinkHeaderInput {\n selfUrl: string;\n cvMime?: string;\n}\n\nexport function buildLinkHeader({ selfUrl, cvMime = 'application/vnd.cv+pdf' }: BuildLinkHeaderInput): string {\n const sep = selfUrl.includes('?') ? '&' : '?';\n return [\n `<${selfUrl}>; rel=\"alternate\"; type=\"${cvMime}\"`,\n `<${selfUrl}${sep}format=md>; rel=\"alternate\"; type=\"text/markdown\"`,\n `<${selfUrl}${sep}format=html>; rel=\"alternate\"; type=\"text/html\"`,\n ].join(', ');\n}\n\nexport const PDF_PRIMARY_MIME = 'application/vnd.cv+pdf';\nexport const PDF_FALLBACK_MIME = 'application/pdf';\n","import { extract } from '@cvfile/sdk';\nimport type { CvFile, ExtractedPayload } from '@cvfile/sdk';\nimport { negotiate, type ServeFormat } from './conneg.js';\n\nexport interface ServeRequest {\n bytes: Uint8Array;\n accept?: string | undefined;\n acceptLanguage?: string | undefined;\n formatQuery?: string | undefined;\n defaultFormat?: ServeFormat | undefined;\n}\n\nexport interface ServeResponse {\n format: ServeFormat;\n contentType: string;\n language?: string | undefined;\n body: Uint8Array;\n}\n\nconst ENCODER = new TextEncoder();\n\nexport async function serveCv(req: ServeRequest): Promise<ServeResponse> {\n const decision = negotiate({\n accept: req.accept,\n acceptLanguage: req.acceptLanguage,\n formatQuery: req.formatQuery,\n defaultFormat: req.defaultFormat,\n });\n\n if (decision.format === 'pdf') {\n return {\n format: 'pdf',\n contentType: 'application/vnd.cv+pdf',\n ...(decision.language !== undefined ? { language: decision.language } : {}),\n body: req.bytes,\n };\n }\n\n const file = await extract(req.bytes);\n const preferLang = decision.language ?? file.metadata.primaryLanguage;\n\n if (decision.format === 'markdown') {\n const md = pickPayload(file, 'text/markdown', preferLang);\n if (md) {\n return {\n format: 'markdown',\n contentType: 'text/markdown; charset=utf-8',\n ...(md.language !== undefined ? { language: md.language } : {}),\n body: md.bytes,\n };\n }\n return fallbackToPdf(req.bytes);\n }\n\n if (decision.format === 'html') {\n const html = pickPayload(file, 'text/html', preferLang);\n if (html) {\n return {\n format: 'html',\n contentType: 'text/html; charset=utf-8',\n ...(html.language !== undefined ? { language: html.language } : {}),\n body: html.bytes,\n };\n }\n const md = pickPayload(file, 'text/markdown', preferLang);\n if (md) {\n const body = ENCODER.encode(renderMarkdownAsHtml(md.text(), file));\n return {\n format: 'html',\n contentType: 'text/html; charset=utf-8',\n ...(md.language !== undefined ? { language: md.language } : {}),\n body,\n };\n }\n return fallbackToPdf(req.bytes);\n }\n\n return fallbackToPdf(req.bytes);\n}\n\nfunction pickPayload(file: CvFile, mimeType: string, preferLang: string): ExtractedPayload | undefined {\n const matches = file.payloads.filter((p) => p.mimeType === mimeType);\n if (matches.length === 0) return undefined;\n return matches.find((p) => p.language === preferLang) ?? matches[0];\n}\n\nfunction fallbackToPdf(bytes: Uint8Array): ServeResponse {\n return { format: 'pdf', contentType: 'application/vnd.cv+pdf', body: bytes };\n}\n\nfunction renderMarkdownAsHtml(md: string, file: CvFile): string {\n const safe = md\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n return `<!doctype html>\n<html lang=\"${file.metadata.primaryLanguage}\">\n<head>\n<meta charset=\"utf-8\">\n<title>${file.metadata.primaryPayload}</title>\n</head>\n<body>\n<pre>${safe}</pre>\n</body>\n</html>`;\n}\n","import { createHash } from 'node:crypto';\nimport { buildLinkHeader, PDF_PRIMARY_MIME, type ServeFormat } from './conneg.js';\nimport { serveCv } from './serve.js';\n\nexport interface BuildResponseInput {\n bytes: Uint8Array;\n selfUrl: string;\n accept?: string | undefined;\n acceptLanguage?: string | undefined;\n formatQuery?: string | undefined;\n defaultFormat?: ServeFormat | undefined;\n cacheControl: string;\n lastModified?: Date | undefined;\n ifNoneMatch?: string | undefined;\n ifModifiedSince?: string | undefined;\n}\n\nexport interface BuiltResponse {\n status: 200 | 304;\n headers: Record<string, string>;\n format: ServeFormat;\n body: Uint8Array;\n}\n\n/**\n * Negotiate the format and assemble the full set of response headers shared by\n * every adapter. The same URL yields a different body per negotiated format, so\n * the ETag is keyed on both the bytes and the format. Returns a 304 (empty body)\n * when the conditional request headers match.\n */\nexport async function buildCvResponse(input: BuildResponseInput): Promise<BuiltResponse> {\n const result = await serveCv({\n bytes: input.bytes,\n accept: input.accept,\n acceptLanguage: input.acceptLanguage,\n formatQuery: input.formatQuery,\n defaultFormat: input.defaultFormat,\n });\n\n const etag = computeETag(result.body, result.format);\n const lastModified = input.lastModified?.toUTCString();\n\n const headers: Record<string, string> = {\n 'Content-Type': result.contentType,\n Vary: 'Accept, Accept-Language',\n Link: buildLinkHeader({ selfUrl: input.selfUrl, cvMime: PDF_PRIMARY_MIME }),\n 'Cache-Control': input.cacheControl,\n ETag: etag,\n 'Content-Disposition': contentDisposition(input.selfUrl, result.format),\n };\n if (result.language) {\n headers['Content-Language'] = result.language;\n }\n if (lastModified) {\n headers['Last-Modified'] = lastModified;\n }\n\n const notModified = isNotModified({\n etag,\n lastModified: input.lastModified,\n ifNoneMatch: input.ifNoneMatch,\n ifModifiedSince: input.ifModifiedSince,\n });\n if (notModified) {\n return { status: 304, headers, format: result.format, body: new Uint8Array(0) };\n }\n\n headers['Content-Length'] = String(result.body.length);\n return { status: 200, headers, format: result.format, body: result.body };\n}\n\n/** Weak ETag keyed on the negotiated body so each format gets a distinct tag. */\nfunction computeETag(body: Uint8Array, format: ServeFormat): string {\n const hash = createHash('sha1').update(body).digest('base64url');\n return `W/\"${format}-${body.length.toString(16)}-${hash}\"`;\n}\n\ninterface NotModifiedInput {\n etag: string;\n lastModified?: Date | undefined;\n ifNoneMatch?: string | undefined;\n ifModifiedSince?: string | undefined;\n}\n\nfunction isNotModified({ etag, lastModified, ifNoneMatch, ifModifiedSince }: NotModifiedInput): boolean {\n if (ifNoneMatch) {\n return etagMatches(ifNoneMatch, etag);\n }\n if (ifModifiedSince && lastModified) {\n const since = Date.parse(ifModifiedSince);\n if (!Number.isNaN(since)) {\n // Compare at second resolution, matching HTTP-date granularity.\n return Math.floor(lastModified.getTime() / 1000) <= Math.floor(since / 1000);\n }\n }\n return false;\n}\n\nfunction etagMatches(ifNoneMatch: string, etag: string): boolean {\n if (ifNoneMatch.trim() === '*') return true;\n const normalize = (tag: string): string => tag.trim().replace(/^W\\//, '');\n const target = normalize(etag);\n return ifNoneMatch.split(',').some((candidate) => normalize(candidate) === target);\n}\n\n/**\n * Build a header-injection-safe Content-Disposition value. Control characters,\n * CR/LF, quotes and backslashes are stripped from the ASCII filename; non-ASCII\n * names also get an RFC 5987 filename* form.\n */\nexport function contentDisposition(selfUrl: string, format: ServeFormat): string {\n const filename = filenameForFormat(selfUrl, format);\n const asciiSafe = sanitizeAsciiFilename(filename);\n const base = `inline; filename=\"${asciiSafe}\"`;\n if (!hasNonAscii(filename)) return base;\n return `${base}; filename*=UTF-8''${encodeRFC5987(filename)}`;\n}\n\n/**\n * Produce a safe quoted-string filename: drop control chars (CR/LF/DEL),\n * double quotes and backslashes; replace any remaining non-ASCII with '_'.\n */\nfunction sanitizeAsciiFilename(filename: string): string {\n let out = '';\n for (const ch of filename) {\n const code = ch.codePointAt(0) ?? 0;\n if (code < 0x20 || code === 0x7f) continue; // control chars incl. CR/LF\n if (ch === '\"' || ch === '\\\\') continue; // quoted-string delimiters\n out += code > 0x7e ? '_' : ch; // collapse non-ASCII to underscore\n }\n return out || 'document';\n}\n\nfunction hasNonAscii(value: string): boolean {\n for (const ch of value) {\n const code = ch.codePointAt(0) ?? 0;\n if (code < 0x20 || code > 0x7e) return true;\n }\n return false;\n}\n\nfunction filenameForFormat(selfUrl: string, format: ServeFormat): string {\n const pathname = selfUrl.split('?')[0] ?? selfUrl;\n const base = decodeOrRaw(pathname.split('/').pop() ?? 'document');\n const stem = base.replace(/\\.cv$/i, '').replace(/\\.(pdf|md|html)$/i, '') || 'document';\n if (format === 'markdown') return `${stem}.md`;\n if (format === 'html') return `${stem}.html`;\n return `${stem}.cv`;\n}\n\nfunction decodeOrRaw(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction encodeRFC5987(value: string): string {\n return encodeURIComponent(value).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { normalize, resolve, sep } from 'node:path';\nimport { isCvFile } from '@cvfile/sdk';\nimport type { ServeFormat } from './conneg.js';\nimport { buildCvResponse } from './response.js';\n\nexport interface CvHandlerOptions {\n root?: string;\n loader?: (logicalPath: string) => Promise<Uint8Array | null>;\n cacheControl?: string;\n defaultFormat?: ServeFormat;\n}\n\nexport type CvHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport function cvHandler(options: CvHandlerOptions = {}): CvHandler {\n const { root, loader, cacheControl = 'public, max-age=300', defaultFormat } = options;\n if (!root && !loader) {\n throw new Error('cvHandler requires either { root } or { loader }');\n }\n\n const baseRoot = root ? resolve(root) : null;\n\n return async (req, res) => {\n try {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const logical = decodeURIComponent(url.pathname);\n\n const loaded = await load(logical, { baseRoot, loader });\n if (!loaded) {\n res.statusCode = 404;\n res.end('Not found');\n return;\n }\n\n if (!(await isCvFile(loaded.bytes))) {\n res.statusCode = 415;\n res.end('Not a .cv file');\n return;\n }\n\n const built = await buildCvResponse({\n bytes: loaded.bytes,\n selfUrl: url.pathname,\n accept: req.headers['accept'],\n acceptLanguage: req.headers['accept-language'],\n formatQuery: url.searchParams.get('format') ?? undefined,\n defaultFormat,\n cacheControl,\n lastModified: loaded.lastModified,\n ifNoneMatch: req.headers['if-none-match'],\n ifModifiedSince: req.headers['if-modified-since'],\n });\n\n for (const [name, value] of Object.entries(built.headers)) {\n res.setHeader(name, value);\n }\n res.statusCode = built.status;\n res.end(built.status === 304 ? undefined : Buffer.from(built.body));\n } catch {\n if (res.headersSent) {\n res.end();\n return;\n }\n res.statusCode = 500;\n res.end('Internal Server Error');\n }\n };\n}\n\ninterface LoadOpts {\n baseRoot: string | null;\n loader?: ((logicalPath: string) => Promise<Uint8Array | null>) | undefined;\n}\n\ninterface LoadedFile {\n bytes: Uint8Array;\n lastModified?: Date | undefined;\n}\n\nasync function load(logicalPath: string, { baseRoot, loader }: LoadOpts): Promise<LoadedFile | null> {\n if (loader) {\n const bytes = await loader(logicalPath);\n return bytes ? { bytes } : null;\n }\n if (!baseRoot) return null;\n const safe = normalize(logicalPath).replace(/^[/\\\\]+/, '');\n const full = resolve(baseRoot, safe);\n if (!isWithin(baseRoot, full)) {\n return null;\n }\n try {\n const s = await stat(full);\n if (!s.isFile()) return null;\n return { bytes: new Uint8Array(await readFile(full)), lastModified: s.mtime };\n } catch {\n return null;\n }\n}\n\nfunction isWithin(parent: string, child: string): boolean {\n const rel = resolve(child);\n const base = resolve(parent);\n return rel === base || rel.startsWith(base + sep);\n}\n","import type { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';\nimport { cvHandler, type CvHandlerOptions } from './handler.js';\n\nexport interface CvFastifyOptions extends CvHandlerOptions {\n prefix?: string;\n}\n\nexport const cvFastifyPlugin: FastifyPluginAsync<CvFastifyOptions> = async (\n fastify: FastifyInstance,\n opts: CvFastifyOptions,\n) => {\n const handler = cvHandler(opts);\n const route = `${opts.prefix ?? ''}/*`;\n fastify.get(route, async (request: FastifyRequest, reply: FastifyReply) => {\n const { pathname } = new URL(request.url, `http://${request.headers.host ?? 'localhost'}`);\n if (!decodeURIComponent(pathname).toLowerCase().endsWith('.cv')) {\n reply.code(404).send('Not found');\n return;\n }\n reply.hijack();\n await handler(request.raw, reply.raw);\n });\n};\n\nexport default cvFastifyPlugin;\n"]}
@@ -1,10 +1,11 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { S as ServeFormat } from './conneg-DbPVX8oc.js';
2
3
 
3
4
  interface CvHandlerOptions {
4
5
  root?: string;
5
6
  loader?: (logicalPath: string) => Promise<Uint8Array | null>;
6
7
  cacheControl?: string;
7
- defaultFormat?: 'pdf' | 'markdown' | 'html';
8
+ defaultFormat?: ServeFormat;
8
9
  }
9
10
  type CvHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
10
11
  declare function cvHandler(options?: CvHandlerOptions): CvHandler;
@@ -1,10 +1,11 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { S as ServeFormat } from './conneg-DbPVX8oc.cjs';
2
3
 
3
4
  interface CvHandlerOptions {
4
5
  root?: string;
5
6
  loader?: (logicalPath: string) => Promise<Uint8Array | null>;
6
7
  cacheControl?: string;
7
- defaultFormat?: 'pdf' | 'markdown' | 'html';
8
+ defaultFormat?: ServeFormat;
8
9
  }
9
10
  type CvHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
10
11
  declare function cvHandler(options?: CvHandlerOptions): CvHandler;