@cvfile/server 0.1.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/hono.js ADDED
@@ -0,0 +1,188 @@
1
+ import { isCvFile, extract } from '@cvfile/sdk';
2
+
3
+ // src/hono.ts
4
+
5
+ // src/conneg.ts
6
+ var FORMAT_BY_MIME = {
7
+ "text/markdown": "markdown",
8
+ "text/x-markdown": "markdown",
9
+ "text/html": "html",
10
+ "application/xhtml+xml": "html",
11
+ "application/pdf": "pdf",
12
+ "application/vnd.cv+pdf": "pdf"
13
+ };
14
+ var FORMAT_BY_QUERY = {
15
+ md: "markdown",
16
+ markdown: "markdown",
17
+ html: "html",
18
+ pdf: "pdf",
19
+ cv: "pdf"
20
+ };
21
+ function parseAccept(header) {
22
+ if (!header) return [];
23
+ return header.split(",").map((part) => {
24
+ const [type, ...params] = part.trim().split(";").map((s) => s.trim());
25
+ let q = 1;
26
+ for (const p of params) {
27
+ const m = p.match(/^q\s*=\s*(\d*\.?\d+)/i);
28
+ if (m) q = Number(m[1]);
29
+ }
30
+ return { type: (type ?? "").toLowerCase(), q };
31
+ }).filter((p) => p.type).sort((a, b) => b.q - a.q);
32
+ }
33
+ function parseAcceptLanguage(header) {
34
+ if (!header) return [];
35
+ return header.split(",").map((part) => {
36
+ const [tag, ...params] = part.trim().split(";").map((s) => s.trim());
37
+ let q = 1;
38
+ for (const p of params) {
39
+ const m = p.match(/^q\s*=\s*(\d*\.?\d+)/i);
40
+ if (m) q = Number(m[1]);
41
+ }
42
+ return { tag: (tag ?? "").toLowerCase(), q };
43
+ }).filter((p) => p.tag && p.tag !== "*").sort((a, b) => b.q - a.q).map((p) => p.tag);
44
+ }
45
+ function negotiate(input) {
46
+ const language = parseAcceptLanguage(input.acceptLanguage)[0];
47
+ if (input.formatQuery) {
48
+ const fromQuery = FORMAT_BY_QUERY[input.formatQuery.toLowerCase()];
49
+ if (fromQuery) {
50
+ return { format: fromQuery, language };
51
+ }
52
+ }
53
+ const accepts = parseAccept(input.accept);
54
+ for (const a of accepts) {
55
+ const direct = FORMAT_BY_MIME[a.type];
56
+ if (direct) {
57
+ return { format: direct, language };
58
+ }
59
+ if (a.type === "*/*" || a.type === "application/*") {
60
+ return { format: "pdf", language };
61
+ }
62
+ if (a.type === "text/*") {
63
+ return { format: "html", language };
64
+ }
65
+ }
66
+ return { format: "pdf", language };
67
+ }
68
+ function buildLinkHeader({ selfUrl, cvMime = "application/vnd.cv+pdf" }) {
69
+ const sep = selfUrl.includes("?") ? "&" : "?";
70
+ return [
71
+ `<${selfUrl}>; rel="alternate"; type="${cvMime}"`,
72
+ `<${selfUrl}${sep}format=md>; rel="alternate"; type="text/markdown"`,
73
+ `<${selfUrl}${sep}format=html>; rel="alternate"; type="text/html"`
74
+ ].join(", ");
75
+ }
76
+ var PDF_PRIMARY_MIME = "application/vnd.cv+pdf";
77
+ var ENCODER = new TextEncoder();
78
+ async function serveCv(req) {
79
+ const decision = negotiate({
80
+ accept: req.accept,
81
+ acceptLanguage: req.acceptLanguage,
82
+ formatQuery: req.formatQuery
83
+ });
84
+ if (decision.format === "pdf") {
85
+ return {
86
+ format: "pdf",
87
+ contentType: "application/vnd.cv+pdf",
88
+ ...decision.language !== void 0 ? { language: decision.language } : {},
89
+ body: req.bytes
90
+ };
91
+ }
92
+ const file = await extract(req.bytes);
93
+ const preferLang = decision.language ?? file.metadata.primaryLanguage;
94
+ if (decision.format === "markdown") {
95
+ const md = pickPayload(file, "text/markdown", preferLang);
96
+ if (md) {
97
+ return {
98
+ format: "markdown",
99
+ contentType: `text/markdown; charset=utf-8; cv-language=${md.language ?? preferLang}`,
100
+ ...md.language !== void 0 ? { language: md.language } : {},
101
+ body: md.bytes
102
+ };
103
+ }
104
+ return fallbackToPdf(req.bytes);
105
+ }
106
+ if (decision.format === "html") {
107
+ const html = pickPayload(file, "text/html", preferLang);
108
+ if (html) {
109
+ return {
110
+ format: "html",
111
+ contentType: `text/html; charset=utf-8; cv-language=${html.language ?? preferLang}`,
112
+ ...html.language !== void 0 ? { language: html.language } : {},
113
+ body: html.bytes
114
+ };
115
+ }
116
+ const md = pickPayload(file, "text/markdown", preferLang);
117
+ if (md) {
118
+ const body = ENCODER.encode(renderMarkdownAsHtml(md.text(), file));
119
+ return {
120
+ format: "html",
121
+ contentType: "text/html; charset=utf-8",
122
+ ...md.language !== void 0 ? { language: md.language } : {},
123
+ body
124
+ };
125
+ }
126
+ return fallbackToPdf(req.bytes);
127
+ }
128
+ return fallbackToPdf(req.bytes);
129
+ }
130
+ function pickPayload(file, mimeType, preferLang) {
131
+ const matches = file.payloads.filter((p) => p.mimeType === mimeType);
132
+ if (matches.length === 0) return void 0;
133
+ return matches.find((p) => p.language === preferLang) ?? matches[0];
134
+ }
135
+ function fallbackToPdf(bytes) {
136
+ return { format: "pdf", contentType: "application/vnd.cv+pdf", body: bytes };
137
+ }
138
+ function renderMarkdownAsHtml(md, file) {
139
+ const safe = md.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
140
+ return `<!doctype html>
141
+ <html lang="${file.metadata.primaryLanguage}">
142
+ <head>
143
+ <meta charset="utf-8">
144
+ <title>${file.metadata.primaryPayload}</title>
145
+ </head>
146
+ <body>
147
+ <pre>${safe}</pre>
148
+ </body>
149
+ </html>`;
150
+ }
151
+
152
+ // src/hono.ts
153
+ function cvHono(options) {
154
+ const { loader, cacheControl = "public, max-age=300" } = options;
155
+ return async (c) => {
156
+ const url = new URL(c.req.url);
157
+ const logical = decodeURIComponent(url.pathname);
158
+ if (!logical.toLowerCase().endsWith(".cv")) {
159
+ return c.notFound();
160
+ }
161
+ const bytes = await loader(logical);
162
+ if (!bytes) return c.notFound();
163
+ if (!await isCvFile(bytes)) {
164
+ return c.text("Not a .cv file", 415);
165
+ }
166
+ const result = await serveCv({
167
+ bytes,
168
+ accept: c.req.header("accept"),
169
+ acceptLanguage: c.req.header("accept-language"),
170
+ formatQuery: c.req.query("format") ?? void 0
171
+ });
172
+ const link = buildLinkHeader({ selfUrl: url.pathname, cvMime: PDF_PRIMARY_MIME });
173
+ const headers = {
174
+ "Content-Type": result.contentType,
175
+ Vary: "Accept, Accept-Language",
176
+ Link: link,
177
+ "Cache-Control": cacheControl
178
+ };
179
+ if (result.language) headers["Content-Language"] = result.language;
180
+ const view = new Uint8Array(result.body.byteLength);
181
+ view.set(result.body);
182
+ return c.newResponse(view.buffer, 200, headers);
183
+ };
184
+ }
185
+
186
+ export { cvHono };
187
+ //# sourceMappingURL=hono.js.map
188
+ //# sourceMappingURL=hono.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/conneg.ts","../src/serve.ts","../src/hono.ts"],"names":[],"mappings":"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","file":"hono.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 { Context, MiddlewareHandler } from 'hono';\nimport { isCvFile } from '@cvfile/sdk';\nimport { buildLinkHeader, PDF_PRIMARY_MIME } from './conneg.js';\nimport { serveCv } from './serve.js';\n\nexport interface CvHonoOptions {\n loader: (logicalPath: string) => Promise<Uint8Array | null>;\n cacheControl?: string;\n}\n\nexport function cvHono(options: CvHonoOptions): MiddlewareHandler {\n const { loader, cacheControl = 'public, max-age=300' } = options;\n return async (c: Context) => {\n const url = new URL(c.req.url);\n const logical = decodeURIComponent(url.pathname);\n if (!logical.toLowerCase().endsWith('.cv')) {\n return c.notFound();\n }\n const bytes = await loader(logical);\n if (!bytes) return c.notFound();\n if (!(await isCvFile(bytes))) {\n return c.text('Not a .cv file', 415);\n }\n const result = await serveCv({\n bytes,\n accept: c.req.header('accept'),\n acceptLanguage: c.req.header('accept-language'),\n formatQuery: c.req.query('format') ?? undefined,\n });\n const link = buildLinkHeader({ selfUrl: url.pathname, cvMime: PDF_PRIMARY_MIME });\n const headers: Record<string, string> = {\n 'Content-Type': result.contentType,\n Vary: 'Accept, Accept-Language',\n Link: link,\n 'Cache-Control': cacheControl,\n };\n if (result.language) headers['Content-Language'] = result.language;\n const view = new Uint8Array(result.body.byteLength);\n view.set(result.body);\n return c.newResponse(view.buffer, 200, headers);\n };\n}\n"]}
package/dist/index.cjs ADDED
@@ -0,0 +1,246 @@
1
+ 'use strict';
2
+
3
+ var promises = require('fs/promises');
4
+ var path = require('path');
5
+ var sdk = require('@cvfile/sdk');
6
+
7
+ // src/handler.ts
8
+
9
+ // src/conneg.ts
10
+ var FORMAT_BY_MIME = {
11
+ "text/markdown": "markdown",
12
+ "text/x-markdown": "markdown",
13
+ "text/html": "html",
14
+ "application/xhtml+xml": "html",
15
+ "application/pdf": "pdf",
16
+ "application/vnd.cv+pdf": "pdf"
17
+ };
18
+ var FORMAT_BY_QUERY = {
19
+ md: "markdown",
20
+ markdown: "markdown",
21
+ html: "html",
22
+ pdf: "pdf",
23
+ cv: "pdf"
24
+ };
25
+ function parseAccept(header) {
26
+ if (!header) return [];
27
+ return header.split(",").map((part) => {
28
+ const [type, ...params] = part.trim().split(";").map((s) => s.trim());
29
+ let q = 1;
30
+ for (const p of params) {
31
+ const m = p.match(/^q\s*=\s*(\d*\.?\d+)/i);
32
+ if (m) q = Number(m[1]);
33
+ }
34
+ return { type: (type ?? "").toLowerCase(), q };
35
+ }).filter((p) => p.type).sort((a, b) => b.q - a.q);
36
+ }
37
+ function parseAcceptLanguage(header) {
38
+ if (!header) return [];
39
+ return header.split(",").map((part) => {
40
+ const [tag, ...params] = part.trim().split(";").map((s) => s.trim());
41
+ let q = 1;
42
+ for (const p of params) {
43
+ const m = p.match(/^q\s*=\s*(\d*\.?\d+)/i);
44
+ if (m) q = Number(m[1]);
45
+ }
46
+ return { tag: (tag ?? "").toLowerCase(), q };
47
+ }).filter((p) => p.tag && p.tag !== "*").sort((a, b) => b.q - a.q).map((p) => p.tag);
48
+ }
49
+ function negotiate(input) {
50
+ const language = parseAcceptLanguage(input.acceptLanguage)[0];
51
+ if (input.formatQuery) {
52
+ const fromQuery = FORMAT_BY_QUERY[input.formatQuery.toLowerCase()];
53
+ if (fromQuery) {
54
+ return { format: fromQuery, language };
55
+ }
56
+ }
57
+ const accepts = parseAccept(input.accept);
58
+ for (const a of accepts) {
59
+ const direct = FORMAT_BY_MIME[a.type];
60
+ if (direct) {
61
+ return { format: direct, language };
62
+ }
63
+ if (a.type === "*/*" || a.type === "application/*") {
64
+ return { format: "pdf", language };
65
+ }
66
+ if (a.type === "text/*") {
67
+ return { format: "html", language };
68
+ }
69
+ }
70
+ return { format: "pdf", language };
71
+ }
72
+ function buildLinkHeader({ selfUrl, cvMime = "application/vnd.cv+pdf" }) {
73
+ const sep2 = selfUrl.includes("?") ? "&" : "?";
74
+ return [
75
+ `<${selfUrl}>; rel="alternate"; type="${cvMime}"`,
76
+ `<${selfUrl}${sep2}format=md>; rel="alternate"; type="text/markdown"`,
77
+ `<${selfUrl}${sep2}format=html>; rel="alternate"; type="text/html"`
78
+ ].join(", ");
79
+ }
80
+ var PDF_PRIMARY_MIME = "application/vnd.cv+pdf";
81
+ var PDF_FALLBACK_MIME = "application/pdf";
82
+ var ENCODER = new TextEncoder();
83
+ async function serveCv(req) {
84
+ const decision = negotiate({
85
+ accept: req.accept,
86
+ acceptLanguage: req.acceptLanguage,
87
+ formatQuery: req.formatQuery
88
+ });
89
+ if (decision.format === "pdf") {
90
+ return {
91
+ format: "pdf",
92
+ contentType: "application/vnd.cv+pdf",
93
+ ...decision.language !== void 0 ? { language: decision.language } : {},
94
+ body: req.bytes
95
+ };
96
+ }
97
+ const file = await sdk.extract(req.bytes);
98
+ const preferLang = decision.language ?? file.metadata.primaryLanguage;
99
+ if (decision.format === "markdown") {
100
+ const md = pickPayload(file, "text/markdown", preferLang);
101
+ if (md) {
102
+ return {
103
+ format: "markdown",
104
+ contentType: `text/markdown; charset=utf-8; cv-language=${md.language ?? preferLang}`,
105
+ ...md.language !== void 0 ? { language: md.language } : {},
106
+ body: md.bytes
107
+ };
108
+ }
109
+ return fallbackToPdf(req.bytes);
110
+ }
111
+ if (decision.format === "html") {
112
+ const html = pickPayload(file, "text/html", preferLang);
113
+ if (html) {
114
+ return {
115
+ format: "html",
116
+ contentType: `text/html; charset=utf-8; cv-language=${html.language ?? preferLang}`,
117
+ ...html.language !== void 0 ? { language: html.language } : {},
118
+ body: html.bytes
119
+ };
120
+ }
121
+ const md = pickPayload(file, "text/markdown", preferLang);
122
+ if (md) {
123
+ const body = ENCODER.encode(renderMarkdownAsHtml(md.text(), file));
124
+ return {
125
+ format: "html",
126
+ contentType: "text/html; charset=utf-8",
127
+ ...md.language !== void 0 ? { language: md.language } : {},
128
+ body
129
+ };
130
+ }
131
+ return fallbackToPdf(req.bytes);
132
+ }
133
+ return fallbackToPdf(req.bytes);
134
+ }
135
+ function pickPayload(file, mimeType, preferLang) {
136
+ const matches = file.payloads.filter((p) => p.mimeType === mimeType);
137
+ if (matches.length === 0) return void 0;
138
+ return matches.find((p) => p.language === preferLang) ?? matches[0];
139
+ }
140
+ function fallbackToPdf(bytes) {
141
+ return { format: "pdf", contentType: "application/vnd.cv+pdf", body: bytes };
142
+ }
143
+ function renderMarkdownAsHtml(md, file) {
144
+ const safe = md.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
145
+ return `<!doctype html>
146
+ <html lang="${file.metadata.primaryLanguage}">
147
+ <head>
148
+ <meta charset="utf-8">
149
+ <title>${file.metadata.primaryPayload}</title>
150
+ </head>
151
+ <body>
152
+ <pre>${safe}</pre>
153
+ </body>
154
+ </html>`;
155
+ }
156
+
157
+ // src/handler.ts
158
+ function cvHandler(options = {}) {
159
+ const { root, loader, cacheControl = "public, max-age=300", defaultFormat } = options;
160
+ if (!root && !loader) {
161
+ throw new Error("cvHandler requires either { root } or { loader }");
162
+ }
163
+ const baseRoot = root ? path.resolve(root) : null;
164
+ return async (req, res) => {
165
+ try {
166
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
167
+ const logical = decodeURIComponent(url.pathname);
168
+ const formatQuery = url.searchParams.get("format") ?? defaultFormat;
169
+ const bytes = await load(logical, { baseRoot, loader });
170
+ if (!bytes) {
171
+ res.statusCode = 404;
172
+ res.end("Not found");
173
+ return;
174
+ }
175
+ if (!await sdk.isCvFile(bytes)) {
176
+ res.statusCode = 415;
177
+ res.end("Not a .cv file");
178
+ return;
179
+ }
180
+ const result = await serveCv({
181
+ bytes,
182
+ accept: req.headers["accept"],
183
+ acceptLanguage: req.headers["accept-language"],
184
+ formatQuery: formatQuery ?? void 0
185
+ });
186
+ const link = buildLinkHeader({ selfUrl: url.pathname, cvMime: PDF_PRIMARY_MIME });
187
+ res.setHeader("Content-Type", result.contentType);
188
+ res.setHeader("Content-Length", String(result.body.length));
189
+ res.setHeader("Vary", "Accept, Accept-Language");
190
+ res.setHeader("Link", link);
191
+ res.setHeader("Cache-Control", cacheControl);
192
+ if (result.language) {
193
+ res.setHeader("Content-Language", result.language);
194
+ }
195
+ const filename = filenameForFormat(logical, result.format);
196
+ res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
197
+ res.statusCode = 200;
198
+ res.end(Buffer.from(result.body));
199
+ } catch (err) {
200
+ res.statusCode = 500;
201
+ res.end(`cvHandler error: ${err.message}`);
202
+ }
203
+ };
204
+ }
205
+ async function load(logicalPath, { baseRoot, loader }) {
206
+ if (loader) {
207
+ const bytes = await loader(logicalPath);
208
+ return bytes ?? null;
209
+ }
210
+ if (!baseRoot) return null;
211
+ const safe = path.normalize(logicalPath).replace(/^[/\\]+/, "");
212
+ const full = path.resolve(baseRoot, safe);
213
+ if (!isWithin(baseRoot, full)) {
214
+ return null;
215
+ }
216
+ try {
217
+ const s = await promises.stat(full);
218
+ if (!s.isFile()) return null;
219
+ return new Uint8Array(await promises.readFile(full));
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+ function isWithin(parent, child) {
225
+ const rel = path.resolve(child);
226
+ const base = path.resolve(parent);
227
+ return rel === base || rel.startsWith(base + path.sep);
228
+ }
229
+ function filenameForFormat(logical, format) {
230
+ const base = logical.split("/").pop() ?? "document";
231
+ const stem = base.replace(/\.cv$/i, "").replace(/\.(pdf|md|html)$/i, "") || "document";
232
+ if (format === "markdown") return `${stem}.md`;
233
+ if (format === "html") return `${stem}.html`;
234
+ return `${stem}.cv`;
235
+ }
236
+
237
+ exports.PDF_FALLBACK_MIME = PDF_FALLBACK_MIME;
238
+ exports.PDF_PRIMARY_MIME = PDF_PRIMARY_MIME;
239
+ exports.buildLinkHeader = buildLinkHeader;
240
+ exports.cvHandler = cvHandler;
241
+ exports.negotiate = negotiate;
242
+ exports.parseAccept = parseAccept;
243
+ exports.parseAcceptLanguage = parseAcceptLanguage;
244
+ exports.serveCv = serveCv;
245
+ //# sourceMappingURL=index.cjs.map
246
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/conneg.ts","../src/serve.ts","../src/handler.ts"],"names":["sep","extract","resolve","isCvFile","normalize","stat","readFile"],"mappings":"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","file":"index.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"]}
@@ -0,0 +1,43 @@
1
+ export { C as CvHandler, a as CvHandlerOptions, c as cvHandler } from './handler-DGnThUpM.cjs';
2
+ import 'node:http';
3
+
4
+ type ServeFormat = 'pdf' | 'markdown' | 'html';
5
+ interface NegotiationInput {
6
+ accept?: string | undefined;
7
+ acceptLanguage?: string | undefined;
8
+ formatQuery?: string | undefined;
9
+ }
10
+ interface NegotiationResult {
11
+ format: ServeFormat;
12
+ language: string | undefined;
13
+ }
14
+ interface ParsedAccept {
15
+ type: string;
16
+ q: number;
17
+ }
18
+ declare function parseAccept(header: string | undefined | null): ParsedAccept[];
19
+ declare function parseAcceptLanguage(header: string | undefined | null): string[];
20
+ declare function negotiate(input: NegotiationInput): NegotiationResult;
21
+ interface BuildLinkHeaderInput {
22
+ selfUrl: string;
23
+ cvMime?: string;
24
+ }
25
+ declare function buildLinkHeader({ selfUrl, cvMime }: BuildLinkHeaderInput): string;
26
+ declare const PDF_PRIMARY_MIME = "application/vnd.cv+pdf";
27
+ declare const PDF_FALLBACK_MIME = "application/pdf";
28
+
29
+ interface ServeRequest {
30
+ bytes: Uint8Array;
31
+ accept?: string | undefined;
32
+ acceptLanguage?: string | undefined;
33
+ formatQuery?: string | undefined;
34
+ }
35
+ interface ServeResponse {
36
+ format: ServeFormat;
37
+ contentType: string;
38
+ language?: string | undefined;
39
+ body: Uint8Array;
40
+ }
41
+ declare function serveCv(req: ServeRequest): Promise<ServeResponse>;
42
+
43
+ export { type BuildLinkHeaderInput, type NegotiationInput, type NegotiationResult, PDF_FALLBACK_MIME, PDF_PRIMARY_MIME, type ServeFormat, type ServeRequest, type ServeResponse, buildLinkHeader, negotiate, parseAccept, parseAcceptLanguage, serveCv };
@@ -0,0 +1,43 @@
1
+ export { C as CvHandler, a as CvHandlerOptions, c as cvHandler } from './handler-DGnThUpM.js';
2
+ import 'node:http';
3
+
4
+ type ServeFormat = 'pdf' | 'markdown' | 'html';
5
+ interface NegotiationInput {
6
+ accept?: string | undefined;
7
+ acceptLanguage?: string | undefined;
8
+ formatQuery?: string | undefined;
9
+ }
10
+ interface NegotiationResult {
11
+ format: ServeFormat;
12
+ language: string | undefined;
13
+ }
14
+ interface ParsedAccept {
15
+ type: string;
16
+ q: number;
17
+ }
18
+ declare function parseAccept(header: string | undefined | null): ParsedAccept[];
19
+ declare function parseAcceptLanguage(header: string | undefined | null): string[];
20
+ declare function negotiate(input: NegotiationInput): NegotiationResult;
21
+ interface BuildLinkHeaderInput {
22
+ selfUrl: string;
23
+ cvMime?: string;
24
+ }
25
+ declare function buildLinkHeader({ selfUrl, cvMime }: BuildLinkHeaderInput): string;
26
+ declare const PDF_PRIMARY_MIME = "application/vnd.cv+pdf";
27
+ declare const PDF_FALLBACK_MIME = "application/pdf";
28
+
29
+ interface ServeRequest {
30
+ bytes: Uint8Array;
31
+ accept?: string | undefined;
32
+ acceptLanguage?: string | undefined;
33
+ formatQuery?: string | undefined;
34
+ }
35
+ interface ServeResponse {
36
+ format: ServeFormat;
37
+ contentType: string;
38
+ language?: string | undefined;
39
+ body: Uint8Array;
40
+ }
41
+ declare function serveCv(req: ServeRequest): Promise<ServeResponse>;
42
+
43
+ export { type BuildLinkHeaderInput, type NegotiationInput, type NegotiationResult, PDF_FALLBACK_MIME, PDF_PRIMARY_MIME, type ServeFormat, type ServeRequest, type ServeResponse, buildLinkHeader, negotiate, parseAccept, parseAcceptLanguage, serveCv };