@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/conneg-DbPVX8oc.d.cts +27 -0
- package/dist/conneg-DbPVX8oc.d.ts +27 -0
- package/dist/express.cjs +173 -60
- package/dist/express.cjs.map +1 -1
- package/dist/express.d.cts +2 -1
- package/dist/express.d.ts +2 -1
- package/dist/express.js +173 -60
- package/dist/express.js.map +1 -1
- package/dist/fastify.cjs +176 -61
- package/dist/fastify.cjs.map +1 -1
- package/dist/fastify.d.cts +2 -1
- package/dist/fastify.d.ts +2 -1
- package/dist/fastify.js +176 -61
- package/dist/fastify.js.map +1 -1
- package/dist/{handler-DGnThUpM.d.cts → handler-CP5HUtCf.d.ts} +2 -1
- package/dist/{handler-DGnThUpM.d.ts → handler-DMqP6HcG.d.cts} +2 -1
- package/dist/hono.cjs +163 -44
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +2 -0
- package/dist/hono.d.ts +2 -0
- package/dist/hono.js +163 -44
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +173 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -27
- package/dist/index.d.ts +5 -27
- package/dist/index.js +173 -60
- package/dist/index.js.map +1 -1
- package/package.json +53 -16
package/dist/hono.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { isCvFile, extract } from '@cvfile/sdk';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
2
3
|
|
|
3
4
|
// src/hono.ts
|
|
4
5
|
|
|
5
6
|
// src/conneg.ts
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"text/html": "html",
|
|
10
|
-
"application/xhtml+xml": "html",
|
|
11
|
-
"application/pdf": "pdf",
|
|
12
|
-
"application/vnd.cv+pdf": "pdf"
|
|
13
|
-
};
|
|
7
|
+
var MARKDOWN_MIMES = /* @__PURE__ */ new Set(["text/markdown", "text/x-markdown", "application/vnd.cv+markdown"]);
|
|
8
|
+
var PDF_MIMES = /* @__PURE__ */ new Set(["application/pdf", "application/vnd.cv+pdf"]);
|
|
9
|
+
var HTML_MIMES = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml"]);
|
|
14
10
|
var FORMAT_BY_QUERY = {
|
|
15
11
|
md: "markdown",
|
|
16
12
|
markdown: "markdown",
|
|
@@ -22,13 +18,21 @@ function parseAccept(header) {
|
|
|
22
18
|
if (!header) return [];
|
|
23
19
|
return header.split(",").map((part) => {
|
|
24
20
|
const [type, ...params] = part.trim().split(";").map((s) => s.trim());
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const m = p.match(/^q\s*=\s*(\d*\.?\d+)/i);
|
|
28
|
-
if (m) q = Number(m[1]);
|
|
29
|
-
}
|
|
21
|
+
const q = parseQ(params);
|
|
22
|
+
if (q === null) return null;
|
|
30
23
|
return { type: (type ?? "").toLowerCase(), q };
|
|
31
|
-
}).filter((p) => p.type).sort((a, b) => b.q - a.q);
|
|
24
|
+
}).filter((p) => p !== null && p.type !== "" && p.q > 0).sort((a, b) => b.q - a.q);
|
|
25
|
+
}
|
|
26
|
+
function parseQ(params) {
|
|
27
|
+
for (const p of params) {
|
|
28
|
+
if (!/^q\s*=/i.test(p)) continue;
|
|
29
|
+
const m = p.match(/^q\s*=\s*(\d*\.?\d+)\s*$/i);
|
|
30
|
+
if (!m) return null;
|
|
31
|
+
const value = Number(m[1]);
|
|
32
|
+
if (Number.isNaN(value)) return null;
|
|
33
|
+
return Math.min(1, Math.max(0, value));
|
|
34
|
+
}
|
|
35
|
+
return 1;
|
|
32
36
|
}
|
|
33
37
|
function parseAcceptLanguage(header) {
|
|
34
38
|
if (!header) return [];
|
|
@@ -50,20 +54,32 @@ function negotiate(input) {
|
|
|
50
54
|
return { format: fromQuery, language };
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const fromAccept = negotiateFromAccept(input.accept);
|
|
58
|
+
const format = fromAccept ?? input.defaultFormat ?? "pdf";
|
|
59
|
+
return { format, language };
|
|
60
|
+
}
|
|
61
|
+
function negotiateFromAccept(header) {
|
|
62
|
+
const accepts = parseAccept(header);
|
|
63
|
+
if (accepts.length === 0) return void 0;
|
|
64
|
+
const topQ = accepts[0].q;
|
|
65
|
+
const top = accepts.filter((a) => a.q === topQ);
|
|
66
|
+
const hasWildcard = accepts.some((a) => a.type === "*/*" || a.type === "application/*");
|
|
67
|
+
if (top.some((a) => MARKDOWN_MIMES.has(a.type))) {
|
|
68
|
+
return "markdown";
|
|
69
|
+
}
|
|
70
|
+
if (top.some((a) => PDF_MIMES.has(a.type))) {
|
|
71
|
+
return "pdf";
|
|
72
|
+
}
|
|
73
|
+
if (top.some((a) => HTML_MIMES.has(a.type)) && !hasWildcard) {
|
|
74
|
+
return "html";
|
|
75
|
+
}
|
|
76
|
+
if (hasWildcard || top.some((a) => HTML_MIMES.has(a.type))) {
|
|
77
|
+
return "pdf";
|
|
78
|
+
}
|
|
79
|
+
if (top.some((a) => a.type === "text/*")) {
|
|
80
|
+
return "html";
|
|
65
81
|
}
|
|
66
|
-
return
|
|
82
|
+
return void 0;
|
|
67
83
|
}
|
|
68
84
|
function buildLinkHeader({ selfUrl, cvMime = "application/vnd.cv+pdf" }) {
|
|
69
85
|
const sep = selfUrl.includes("?") ? "&" : "?";
|
|
@@ -79,7 +95,8 @@ async function serveCv(req) {
|
|
|
79
95
|
const decision = negotiate({
|
|
80
96
|
accept: req.accept,
|
|
81
97
|
acceptLanguage: req.acceptLanguage,
|
|
82
|
-
formatQuery: req.formatQuery
|
|
98
|
+
formatQuery: req.formatQuery,
|
|
99
|
+
defaultFormat: req.defaultFormat
|
|
83
100
|
});
|
|
84
101
|
if (decision.format === "pdf") {
|
|
85
102
|
return {
|
|
@@ -96,7 +113,7 @@ async function serveCv(req) {
|
|
|
96
113
|
if (md) {
|
|
97
114
|
return {
|
|
98
115
|
format: "markdown",
|
|
99
|
-
contentType:
|
|
116
|
+
contentType: "text/markdown; charset=utf-8",
|
|
100
117
|
...md.language !== void 0 ? { language: md.language } : {},
|
|
101
118
|
body: md.bytes
|
|
102
119
|
};
|
|
@@ -108,7 +125,7 @@ async function serveCv(req) {
|
|
|
108
125
|
if (html) {
|
|
109
126
|
return {
|
|
110
127
|
format: "html",
|
|
111
|
-
contentType:
|
|
128
|
+
contentType: "text/html; charset=utf-8",
|
|
112
129
|
...html.language !== void 0 ? { language: html.language } : {},
|
|
113
130
|
body: html.bytes
|
|
114
131
|
};
|
|
@@ -149,9 +166,111 @@ function renderMarkdownAsHtml(md, file) {
|
|
|
149
166
|
</html>`;
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
// src/response.ts
|
|
170
|
+
async function buildCvResponse(input) {
|
|
171
|
+
const result = await serveCv({
|
|
172
|
+
bytes: input.bytes,
|
|
173
|
+
accept: input.accept,
|
|
174
|
+
acceptLanguage: input.acceptLanguage,
|
|
175
|
+
formatQuery: input.formatQuery,
|
|
176
|
+
defaultFormat: input.defaultFormat
|
|
177
|
+
});
|
|
178
|
+
const etag = computeETag(result.body, result.format);
|
|
179
|
+
const lastModified = input.lastModified?.toUTCString();
|
|
180
|
+
const headers = {
|
|
181
|
+
"Content-Type": result.contentType,
|
|
182
|
+
Vary: "Accept, Accept-Language",
|
|
183
|
+
Link: buildLinkHeader({ selfUrl: input.selfUrl, cvMime: PDF_PRIMARY_MIME }),
|
|
184
|
+
"Cache-Control": input.cacheControl,
|
|
185
|
+
ETag: etag,
|
|
186
|
+
"Content-Disposition": contentDisposition(input.selfUrl, result.format)
|
|
187
|
+
};
|
|
188
|
+
if (result.language) {
|
|
189
|
+
headers["Content-Language"] = result.language;
|
|
190
|
+
}
|
|
191
|
+
if (lastModified) {
|
|
192
|
+
headers["Last-Modified"] = lastModified;
|
|
193
|
+
}
|
|
194
|
+
const notModified = isNotModified({
|
|
195
|
+
etag,
|
|
196
|
+
lastModified: input.lastModified,
|
|
197
|
+
ifNoneMatch: input.ifNoneMatch,
|
|
198
|
+
ifModifiedSince: input.ifModifiedSince
|
|
199
|
+
});
|
|
200
|
+
if (notModified) {
|
|
201
|
+
return { status: 304, headers, format: result.format, body: new Uint8Array(0) };
|
|
202
|
+
}
|
|
203
|
+
headers["Content-Length"] = String(result.body.length);
|
|
204
|
+
return { status: 200, headers, format: result.format, body: result.body };
|
|
205
|
+
}
|
|
206
|
+
function computeETag(body, format) {
|
|
207
|
+
const hash = createHash("sha1").update(body).digest("base64url");
|
|
208
|
+
return `W/"${format}-${body.length.toString(16)}-${hash}"`;
|
|
209
|
+
}
|
|
210
|
+
function isNotModified({ etag, lastModified, ifNoneMatch, ifModifiedSince }) {
|
|
211
|
+
if (ifNoneMatch) {
|
|
212
|
+
return etagMatches(ifNoneMatch, etag);
|
|
213
|
+
}
|
|
214
|
+
if (ifModifiedSince && lastModified) {
|
|
215
|
+
const since = Date.parse(ifModifiedSince);
|
|
216
|
+
if (!Number.isNaN(since)) {
|
|
217
|
+
return Math.floor(lastModified.getTime() / 1e3) <= Math.floor(since / 1e3);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
function etagMatches(ifNoneMatch, etag) {
|
|
223
|
+
if (ifNoneMatch.trim() === "*") return true;
|
|
224
|
+
const normalize = (tag) => tag.trim().replace(/^W\//, "");
|
|
225
|
+
const target = normalize(etag);
|
|
226
|
+
return ifNoneMatch.split(",").some((candidate) => normalize(candidate) === target);
|
|
227
|
+
}
|
|
228
|
+
function contentDisposition(selfUrl, format) {
|
|
229
|
+
const filename = filenameForFormat(selfUrl, format);
|
|
230
|
+
const asciiSafe = sanitizeAsciiFilename(filename);
|
|
231
|
+
const base = `inline; filename="${asciiSafe}"`;
|
|
232
|
+
if (!hasNonAscii(filename)) return base;
|
|
233
|
+
return `${base}; filename*=UTF-8''${encodeRFC5987(filename)}`;
|
|
234
|
+
}
|
|
235
|
+
function sanitizeAsciiFilename(filename) {
|
|
236
|
+
let out = "";
|
|
237
|
+
for (const ch of filename) {
|
|
238
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
239
|
+
if (code < 32 || code === 127) continue;
|
|
240
|
+
if (ch === '"' || ch === "\\") continue;
|
|
241
|
+
out += code > 126 ? "_" : ch;
|
|
242
|
+
}
|
|
243
|
+
return out || "document";
|
|
244
|
+
}
|
|
245
|
+
function hasNonAscii(value) {
|
|
246
|
+
for (const ch of value) {
|
|
247
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
248
|
+
if (code < 32 || code > 126) return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
function filenameForFormat(selfUrl, format) {
|
|
253
|
+
const pathname = selfUrl.split("?")[0] ?? selfUrl;
|
|
254
|
+
const base = decodeOrRaw(pathname.split("/").pop() ?? "document");
|
|
255
|
+
const stem = base.replace(/\.cv$/i, "").replace(/\.(pdf|md|html)$/i, "") || "document";
|
|
256
|
+
if (format === "markdown") return `${stem}.md`;
|
|
257
|
+
if (format === "html") return `${stem}.html`;
|
|
258
|
+
return `${stem}.cv`;
|
|
259
|
+
}
|
|
260
|
+
function decodeOrRaw(value) {
|
|
261
|
+
try {
|
|
262
|
+
return decodeURIComponent(value);
|
|
263
|
+
} catch {
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function encodeRFC5987(value) {
|
|
268
|
+
return encodeURIComponent(value).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
152
271
|
// src/hono.ts
|
|
153
272
|
function cvHono(options) {
|
|
154
|
-
const { loader, cacheControl = "public, max-age=300" } = options;
|
|
273
|
+
const { loader, cacheControl = "public, max-age=300", defaultFormat } = options;
|
|
155
274
|
return async (c) => {
|
|
156
275
|
const url = new URL(c.req.url);
|
|
157
276
|
const logical = decodeURIComponent(url.pathname);
|
|
@@ -163,23 +282,23 @@ function cvHono(options) {
|
|
|
163
282
|
if (!await isCvFile(bytes)) {
|
|
164
283
|
return c.text("Not a .cv file", 415);
|
|
165
284
|
}
|
|
166
|
-
const
|
|
285
|
+
const built = await buildCvResponse({
|
|
167
286
|
bytes,
|
|
287
|
+
selfUrl: url.pathname,
|
|
168
288
|
accept: c.req.header("accept"),
|
|
169
289
|
acceptLanguage: c.req.header("accept-language"),
|
|
170
|
-
formatQuery: c.req.query("format") ?? void 0
|
|
290
|
+
formatQuery: c.req.query("format") ?? void 0,
|
|
291
|
+
defaultFormat,
|
|
292
|
+
cacheControl,
|
|
293
|
+
ifNoneMatch: c.req.header("if-none-match"),
|
|
294
|
+
ifModifiedSince: c.req.header("if-modified-since")
|
|
171
295
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
296
|
+
if (built.status === 304) {
|
|
297
|
+
return c.body(null, 304, built.headers);
|
|
298
|
+
}
|
|
299
|
+
const view = new Uint8Array(built.body.byteLength);
|
|
300
|
+
view.set(built.body);
|
|
301
|
+
return c.newResponse(view.buffer, 200, built.headers);
|
|
183
302
|
};
|
|
184
303
|
}
|
|
185
304
|
|
package/dist/hono.js.map
CHANGED
|
@@ -1 +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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>');\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/conneg.ts","../src/serve.ts","../src/response.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 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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>');\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 { Context, MiddlewareHandler } from 'hono';\nimport { isCvFile } from '@cvfile/sdk';\nimport type { ServeFormat } from './conneg.js';\nimport { buildCvResponse } from './response.js';\n\nexport interface CvHonoOptions {\n loader: (logicalPath: string) => Promise<Uint8Array | null>;\n cacheControl?: string;\n defaultFormat?: ServeFormat;\n}\n\nexport function cvHono(options: CvHonoOptions): MiddlewareHandler {\n const { loader, cacheControl = 'public, max-age=300', defaultFormat } = 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\n const built = await buildCvResponse({\n bytes,\n selfUrl: url.pathname,\n accept: c.req.header('accept'),\n acceptLanguage: c.req.header('accept-language'),\n formatQuery: c.req.query('format') ?? undefined,\n defaultFormat,\n cacheControl,\n ifNoneMatch: c.req.header('if-none-match'),\n ifModifiedSince: c.req.header('if-modified-since'),\n });\n\n if (built.status === 304) {\n return c.body(null, 304, built.headers);\n }\n const view = new Uint8Array(built.body.byteLength);\n view.set(built.body);\n return c.newResponse(view.buffer, 200, built.headers);\n };\n}\n"]}
|