@cvfile/server 0.1.0 → 0.3.1

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.cjs CHANGED
@@ -5,18 +5,14 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var promises = require('fs/promises');
6
6
  var path = require('path');
7
7
  var sdk = require('@cvfile/sdk');
8
+ var crypto = require('crypto');
8
9
 
9
10
  // src/handler.ts
10
11
 
11
12
  // src/conneg.ts
12
- var FORMAT_BY_MIME = {
13
- "text/markdown": "markdown",
14
- "text/x-markdown": "markdown",
15
- "text/html": "html",
16
- "application/xhtml+xml": "html",
17
- "application/pdf": "pdf",
18
- "application/vnd.cv+pdf": "pdf"
19
- };
13
+ var MARKDOWN_MIMES = /* @__PURE__ */ new Set(["text/markdown", "text/x-markdown", "application/vnd.cv+markdown"]);
14
+ var PDF_MIMES = /* @__PURE__ */ new Set(["application/pdf", "application/vnd.cv+pdf"]);
15
+ var HTML_MIMES = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
20
16
  var FORMAT_BY_QUERY = {
21
17
  md: "markdown",
22
18
  markdown: "markdown",
@@ -28,13 +24,21 @@ function parseAccept(header) {
28
24
  if (!header) return [];
29
25
  return header.split(",").map((part) => {
30
26
  const [type, ...params] = part.trim().split(";").map((s) => s.trim());
31
- let q = 1;
32
- for (const p of params) {
33
- const m = p.match(/^q\s*=\s*(\d*\.?\d+)/i);
34
- if (m) q = Number(m[1]);
35
- }
27
+ const q = parseQ(params);
28
+ if (q === null) return null;
36
29
  return { type: (type ?? "").toLowerCase(), q };
37
- }).filter((p) => p.type).sort((a, b) => b.q - a.q);
30
+ }).filter((p) => p !== null && p.type !== "" && p.q > 0).sort((a, b) => b.q - a.q);
31
+ }
32
+ function parseQ(params) {
33
+ for (const p of params) {
34
+ if (!/^q\s*=/i.test(p)) continue;
35
+ const m = p.match(/^q\s*=\s*(\d*\.?\d+)\s*$/i);
36
+ if (!m) return null;
37
+ const value = Number(m[1]);
38
+ if (Number.isNaN(value)) return null;
39
+ return Math.min(1, Math.max(0, value));
40
+ }
41
+ return 1;
38
42
  }
39
43
  function parseAcceptLanguage(header) {
40
44
  if (!header) return [];
@@ -56,20 +60,32 @@ function negotiate(input) {
56
60
  return { format: fromQuery, language };
57
61
  }
58
62
  }
59
- const accepts = parseAccept(input.accept);
60
- for (const a of accepts) {
61
- const direct = FORMAT_BY_MIME[a.type];
62
- if (direct) {
63
- return { format: direct, language };
64
- }
65
- if (a.type === "*/*" || a.type === "application/*") {
66
- return { format: "pdf", language };
67
- }
68
- if (a.type === "text/*") {
69
- return { format: "html", language };
70
- }
63
+ const fromAccept = negotiateFromAccept(input.accept);
64
+ const format = fromAccept ?? input.defaultFormat ?? "pdf";
65
+ return { format, language };
66
+ }
67
+ function negotiateFromAccept(header) {
68
+ const accepts = parseAccept(header);
69
+ if (accepts.length === 0) return void 0;
70
+ const topQ = accepts[0].q;
71
+ const top = accepts.filter((a) => a.q === topQ);
72
+ const hasWildcard = accepts.some((a) => a.type === "*/*" || a.type === "application/*");
73
+ if (top.some((a) => MARKDOWN_MIMES.has(a.type))) {
74
+ return "markdown";
75
+ }
76
+ if (top.some((a) => PDF_MIMES.has(a.type))) {
77
+ return "pdf";
78
+ }
79
+ if (top.some((a) => HTML_MIMES.has(a.type)) && !hasWildcard) {
80
+ return "html";
71
81
  }
72
- return { format: "pdf", language };
82
+ if (hasWildcard || top.some((a) => HTML_MIMES.has(a.type))) {
83
+ return "pdf";
84
+ }
85
+ if (top.some((a) => a.type === "text/*")) {
86
+ return "html";
87
+ }
88
+ return void 0;
73
89
  }
74
90
  function buildLinkHeader({ selfUrl, cvMime = "application/vnd.cv+pdf" }) {
75
91
  const sep2 = selfUrl.includes("?") ? "&" : "?";
@@ -85,7 +101,8 @@ async function serveCv(req) {
85
101
  const decision = negotiate({
86
102
  accept: req.accept,
87
103
  acceptLanguage: req.acceptLanguage,
88
- formatQuery: req.formatQuery
104
+ formatQuery: req.formatQuery,
105
+ defaultFormat: req.defaultFormat
89
106
  });
90
107
  if (decision.format === "pdf") {
91
108
  return {
@@ -102,7 +119,7 @@ async function serveCv(req) {
102
119
  if (md) {
103
120
  return {
104
121
  format: "markdown",
105
- contentType: `text/markdown; charset=utf-8; cv-language=${md.language ?? preferLang}`,
122
+ contentType: "text/markdown; charset=utf-8",
106
123
  ...md.language !== void 0 ? { language: md.language } : {},
107
124
  body: md.bytes
108
125
  };
@@ -114,7 +131,7 @@ async function serveCv(req) {
114
131
  if (html) {
115
132
  return {
116
133
  format: "html",
117
- contentType: `text/html; charset=utf-8; cv-language=${html.language ?? preferLang}`,
134
+ contentType: "text/html; charset=utf-8",
118
135
  ...html.language !== void 0 ? { language: html.language } : {},
119
136
  body: html.bytes
120
137
  };
@@ -155,6 +172,108 @@ function renderMarkdownAsHtml(md, file) {
155
172
  </html>`;
156
173
  }
157
174
 
175
+ // src/response.ts
176
+ async function buildCvResponse(input) {
177
+ const result = await serveCv({
178
+ bytes: input.bytes,
179
+ accept: input.accept,
180
+ acceptLanguage: input.acceptLanguage,
181
+ formatQuery: input.formatQuery,
182
+ defaultFormat: input.defaultFormat
183
+ });
184
+ const etag = computeETag(result.body, result.format);
185
+ const lastModified = input.lastModified?.toUTCString();
186
+ const headers = {
187
+ "Content-Type": result.contentType,
188
+ Vary: "Accept, Accept-Language",
189
+ Link: buildLinkHeader({ selfUrl: input.selfUrl, cvMime: PDF_PRIMARY_MIME }),
190
+ "Cache-Control": input.cacheControl,
191
+ ETag: etag,
192
+ "Content-Disposition": contentDisposition(input.selfUrl, result.format)
193
+ };
194
+ if (result.language) {
195
+ headers["Content-Language"] = result.language;
196
+ }
197
+ if (lastModified) {
198
+ headers["Last-Modified"] = lastModified;
199
+ }
200
+ const notModified = isNotModified({
201
+ etag,
202
+ lastModified: input.lastModified,
203
+ ifNoneMatch: input.ifNoneMatch,
204
+ ifModifiedSince: input.ifModifiedSince
205
+ });
206
+ if (notModified) {
207
+ return { status: 304, headers, format: result.format, body: new Uint8Array(0) };
208
+ }
209
+ headers["Content-Length"] = String(result.body.length);
210
+ return { status: 200, headers, format: result.format, body: result.body };
211
+ }
212
+ function computeETag(body, format) {
213
+ const hash = crypto.createHash("sha1").update(body).digest("base64url");
214
+ return `W/"${format}-${body.length.toString(16)}-${hash}"`;
215
+ }
216
+ function isNotModified({ etag, lastModified, ifNoneMatch, ifModifiedSince }) {
217
+ if (ifNoneMatch) {
218
+ return etagMatches(ifNoneMatch, etag);
219
+ }
220
+ if (ifModifiedSince && lastModified) {
221
+ const since = Date.parse(ifModifiedSince);
222
+ if (!Number.isNaN(since)) {
223
+ return Math.floor(lastModified.getTime() / 1e3) <= Math.floor(since / 1e3);
224
+ }
225
+ }
226
+ return false;
227
+ }
228
+ function etagMatches(ifNoneMatch, etag) {
229
+ if (ifNoneMatch.trim() === "*") return true;
230
+ const normalize2 = (tag) => tag.trim().replace(/^W\//, "");
231
+ const target = normalize2(etag);
232
+ return ifNoneMatch.split(",").some((candidate) => normalize2(candidate) === target);
233
+ }
234
+ function contentDisposition(selfUrl, format) {
235
+ const filename = filenameForFormat(selfUrl, format);
236
+ const asciiSafe = sanitizeAsciiFilename(filename);
237
+ const base = `inline; filename="${asciiSafe}"`;
238
+ if (!hasNonAscii(filename)) return base;
239
+ return `${base}; filename*=UTF-8''${encodeRFC5987(filename)}`;
240
+ }
241
+ function sanitizeAsciiFilename(filename) {
242
+ let out = "";
243
+ for (const ch of filename) {
244
+ const code = ch.codePointAt(0) ?? 0;
245
+ if (code < 32 || code === 127) continue;
246
+ if (ch === '"' || ch === "\\") continue;
247
+ out += code > 126 ? "_" : ch;
248
+ }
249
+ return out || "document";
250
+ }
251
+ function hasNonAscii(value) {
252
+ for (const ch of value) {
253
+ const code = ch.codePointAt(0) ?? 0;
254
+ if (code < 32 || code > 126) return true;
255
+ }
256
+ return false;
257
+ }
258
+ function filenameForFormat(selfUrl, format) {
259
+ const pathname = selfUrl.split("?")[0] ?? selfUrl;
260
+ const base = decodeOrRaw(pathname.split("/").pop() ?? "document");
261
+ const stem = base.replace(/\.cv$/i, "").replace(/\.(pdf|md|html)$/i, "") || "document";
262
+ if (format === "markdown") return `${stem}.md`;
263
+ if (format === "html") return `${stem}.html`;
264
+ return `${stem}.cv`;
265
+ }
266
+ function decodeOrRaw(value) {
267
+ try {
268
+ return decodeURIComponent(value);
269
+ } catch {
270
+ return value;
271
+ }
272
+ }
273
+ function encodeRFC5987(value) {
274
+ return encodeURIComponent(value).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
275
+ }
276
+
158
277
  // src/handler.ts
159
278
  function cvHandler(options = {}) {
160
279
  const { root, loader, cacheControl = "public, max-age=300", defaultFormat } = options;
@@ -166,47 +285,48 @@ function cvHandler(options = {}) {
166
285
  try {
167
286
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
168
287
  const logical = decodeURIComponent(url.pathname);
169
- const formatQuery = url.searchParams.get("format") ?? defaultFormat;
170
- const bytes = await load(logical, { baseRoot, loader });
171
- if (!bytes) {
288
+ const loaded = await load(logical, { baseRoot, loader });
289
+ if (!loaded) {
172
290
  res.statusCode = 404;
173
291
  res.end("Not found");
174
292
  return;
175
293
  }
176
- if (!await sdk.isCvFile(bytes)) {
294
+ if (!await sdk.isCvFile(loaded.bytes)) {
177
295
  res.statusCode = 415;
178
296
  res.end("Not a .cv file");
179
297
  return;
180
298
  }
181
- const result = await serveCv({
182
- bytes,
299
+ const built = await buildCvResponse({
300
+ bytes: loaded.bytes,
301
+ selfUrl: url.pathname,
183
302
  accept: req.headers["accept"],
184
303
  acceptLanguage: req.headers["accept-language"],
185
- formatQuery: formatQuery ?? void 0
304
+ formatQuery: url.searchParams.get("format") ?? void 0,
305
+ defaultFormat,
306
+ cacheControl,
307
+ lastModified: loaded.lastModified,
308
+ ifNoneMatch: req.headers["if-none-match"],
309
+ ifModifiedSince: req.headers["if-modified-since"]
186
310
  });
187
- const link = buildLinkHeader({ selfUrl: url.pathname, cvMime: PDF_PRIMARY_MIME });
188
- res.setHeader("Content-Type", result.contentType);
189
- res.setHeader("Content-Length", String(result.body.length));
190
- res.setHeader("Vary", "Accept, Accept-Language");
191
- res.setHeader("Link", link);
192
- res.setHeader("Cache-Control", cacheControl);
193
- if (result.language) {
194
- res.setHeader("Content-Language", result.language);
311
+ for (const [name, value] of Object.entries(built.headers)) {
312
+ res.setHeader(name, value);
313
+ }
314
+ res.statusCode = built.status;
315
+ res.end(built.status === 304 ? void 0 : Buffer.from(built.body));
316
+ } catch {
317
+ if (res.headersSent) {
318
+ res.end();
319
+ return;
195
320
  }
196
- const filename = filenameForFormat(logical, result.format);
197
- res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
198
- res.statusCode = 200;
199
- res.end(Buffer.from(result.body));
200
- } catch (err) {
201
321
  res.statusCode = 500;
202
- res.end(`cvHandler error: ${err.message}`);
322
+ res.end("Internal Server Error");
203
323
  }
204
324
  };
205
325
  }
206
326
  async function load(logicalPath, { baseRoot, loader }) {
207
327
  if (loader) {
208
328
  const bytes = await loader(logicalPath);
209
- return bytes ?? null;
329
+ return bytes ? { bytes } : null;
210
330
  }
211
331
  if (!baseRoot) return null;
212
332
  const safe = path.normalize(logicalPath).replace(/^[/\\]+/, "");
@@ -217,7 +337,7 @@ async function load(logicalPath, { baseRoot, loader }) {
217
337
  try {
218
338
  const s = await promises.stat(full);
219
339
  if (!s.isFile()) return null;
220
- return new Uint8Array(await promises.readFile(full));
340
+ return { bytes: new Uint8Array(await promises.readFile(full)), lastModified: s.mtime };
221
341
  } catch {
222
342
  return null;
223
343
  }
@@ -227,23 +347,18 @@ function isWithin(parent, child) {
227
347
  const base = path.resolve(parent);
228
348
  return rel === base || rel.startsWith(base + path.sep);
229
349
  }
230
- function filenameForFormat(logical, format) {
231
- const base = logical.split("/").pop() ?? "document";
232
- const stem = base.replace(/\.cv$/i, "").replace(/\.(pdf|md|html)$/i, "") || "document";
233
- if (format === "markdown") return `${stem}.md`;
234
- if (format === "html") return `${stem}.html`;
235
- return `${stem}.cv`;
236
- }
237
350
 
238
351
  // src/fastify.ts
239
352
  var cvFastifyPlugin = async (fastify, opts) => {
240
353
  const handler = cvHandler(opts);
241
354
  const route = `${opts.prefix ?? ""}/*`;
242
355
  fastify.get(route, async (request, reply) => {
243
- if (!request.url.toLowerCase().includes(".cv")) {
356
+ const { pathname } = new URL(request.url, `http://${request.headers.host ?? "localhost"}`);
357
+ if (!decodeURIComponent(pathname).toLowerCase().endsWith(".cv")) {
244
358
  reply.code(404).send("Not found");
245
359
  return;
246
360
  }
361
+ reply.hijack();
247
362
  await handler(request.raw, reply.raw);
248
363
  });
249
364
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/conneg.ts","../src/serve.ts","../src/handler.ts","../src/fastify.ts"],"names":["sep","extract","resolve","isCvFile","normalize","stat","readFile"],"mappings":"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","file":"fastify.cjs","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","extract","createHash","normalize","resolve","isCvFile","stat","readFile"],"mappings":"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","file":"fastify.cjs","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,6 +1,7 @@
1
1
  import { FastifyPluginAsync } from 'fastify';
2
- import { a as CvHandlerOptions } from './handler-DGnThUpM.cjs';
2
+ import { a as CvHandlerOptions } from './handler-DMqP6HcG.cjs';
3
3
  import 'node:http';
4
+ import './conneg-DbPVX8oc.cjs';
4
5
 
5
6
  interface CvFastifyOptions extends CvHandlerOptions {
6
7
  prefix?: string;
package/dist/fastify.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FastifyPluginAsync } from 'fastify';
2
- import { a as CvHandlerOptions } from './handler-DGnThUpM.js';
2
+ import { a as CvHandlerOptions } from './handler-CP5HUtCf.js';
3
3
  import 'node:http';
4
+ import './conneg-DbPVX8oc.js';
4
5
 
5
6
  interface CvFastifyOptions extends CvHandlerOptions {
6
7
  prefix?: string;