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