@gmickel/gno 1.29.6 → 1.30.4
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/README.md +15 -5
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.4.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.4.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-627emwpj.js +75 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +47 -22
- package/package.json +42 -39
- package/src/converters/adapters/officeparser/adapter.ts +7 -3
- package/src/core/network-boundary-inventory.ts +51 -0
- package/src/core/project-profile.ts +2 -2
- package/src/publish/encrypted-export.ts +1 -1
- package/src/serve/AGENTS.md +5 -1
- package/src/serve/CLAUDE.md +5 -1
- package/src/serve/fn112-routes.ts +232 -0
- package/src/serve/pdfjs-assets.ts +391 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +28 -10
- package/src/serve/public/components/pdf/PdfPageView.tsx +428 -0
- package/src/serve/public/components/pdf/PdfToolbar.tsx +384 -0
- package/src/serve/public/components/pdf/PdfViewer.tsx +539 -0
- package/src/serve/public/components/pdf/pdf-viewer-deps.tsx +94 -0
- package/src/serve/public/globals.built.css +2 -2
- package/src/serve/public/globals.css +113 -0
- package/src/serve/public/hooks/use-pdf-document.ts +199 -0
- package/src/serve/public/hooks/use-pdf-pages.ts +1197 -0
- package/src/serve/public/lib/doc-asset-url.ts +57 -0
- package/src/serve/public/lib/math-sum-precise.ts +34 -0
- package/src/serve/public/lib/pdf.ts +772 -0
- package/src/serve/public/pages/DocView.tsx +295 -39
- package/src/serve/public/pages/doc-pdf-viewer.tsx +7 -0
- package/src/serve/routes/api.ts +154 -14
- package/src/serve/server.ts +190 -37
- package/src/serve/spa-bundle-source.ts +99 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.6.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.6.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-b2zm0jjd.js +0 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.4",
|
|
4
4
|
"description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"embeddings",
|
|
@@ -66,6 +66,8 @@
|
|
|
66
66
|
"test:web": "bun test test/serve/public --timeout 30000",
|
|
67
67
|
"test:e2e": "bun scripts/web-ui-smoke.ts",
|
|
68
68
|
"test:e2e:clipper": "bun test test/clipper/e2e.test.ts --timeout 240000",
|
|
69
|
+
"test:e2e:pdf": "bun scripts/pdf-viewer-smoke.ts",
|
|
70
|
+
"smoke:pdf-viewer": "bun scripts/pdf-viewer-smoke.ts",
|
|
69
71
|
"test:e2e:install": "bunx playwright install chromium",
|
|
70
72
|
"test:package": "bun scripts/package-smoke.ts",
|
|
71
73
|
"test:package:clipper": "bun scripts/package-smoke-clipper-cli.ts",
|
|
@@ -151,77 +153,78 @@
|
|
|
151
153
|
"prepack": "bun run package:clipper"
|
|
152
154
|
},
|
|
153
155
|
"dependencies": {
|
|
154
|
-
"@codemirror/lang-markdown": "6.5.
|
|
156
|
+
"@codemirror/lang-markdown": "6.5.1",
|
|
155
157
|
"@codemirror/theme-one-dark": "6.1.3",
|
|
156
|
-
"@modelcontextprotocol/sdk": "1.
|
|
157
|
-
"@radix-ui/react-collapsible": "1.1.
|
|
158
|
-
"@radix-ui/react-dialog": "1.1.
|
|
159
|
-
"@radix-ui/react-dropdown-menu": "2.1.
|
|
160
|
-
"@radix-ui/react-hover-card": "1.1.
|
|
161
|
-
"@radix-ui/react-progress": "1.1.
|
|
162
|
-
"@radix-ui/react-scroll-area": "1.2.
|
|
163
|
-
"@radix-ui/react-select": "2.
|
|
164
|
-
"@radix-ui/react-separator": "1.1.
|
|
165
|
-
"@radix-ui/react-slot": "1.
|
|
166
|
-
"@radix-ui/react-tooltip": "1.2.
|
|
158
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
159
|
+
"@radix-ui/react-collapsible": "1.1.20",
|
|
160
|
+
"@radix-ui/react-dialog": "1.1.23",
|
|
161
|
+
"@radix-ui/react-dropdown-menu": "2.1.24",
|
|
162
|
+
"@radix-ui/react-hover-card": "1.1.23",
|
|
163
|
+
"@radix-ui/react-progress": "1.1.16",
|
|
164
|
+
"@radix-ui/react-scroll-area": "1.2.18",
|
|
165
|
+
"@radix-ui/react-select": "2.3.7",
|
|
166
|
+
"@radix-ui/react-separator": "1.1.15",
|
|
167
|
+
"@radix-ui/react-slot": "1.3.3",
|
|
168
|
+
"@radix-ui/react-tooltip": "1.2.16",
|
|
167
169
|
"ai": "6.0.68",
|
|
168
170
|
"bun-plugin-tailwind": "0.1.2",
|
|
169
171
|
"class-variance-authority": "0.7.1",
|
|
170
172
|
"clsx": "2.1.1",
|
|
171
173
|
"cmdk": "1.1.1",
|
|
172
174
|
"codemirror": "6.0.2",
|
|
173
|
-
"commander": "
|
|
175
|
+
"commander": "15.0.0",
|
|
174
176
|
"embla-carousel-react": "8.6.0",
|
|
175
177
|
"franc": "6.2.0",
|
|
176
178
|
"jsonc-parser": "3.3.1",
|
|
177
|
-
"lucide-react": "1.
|
|
178
|
-
"markitdown-ts": "0.0.
|
|
179
|
-
"minimatch": "10.2.
|
|
180
|
-
"nanoid": "
|
|
181
|
-
"node-llama-cpp": "3.
|
|
182
|
-
"officeparser": "
|
|
179
|
+
"lucide-react": "1.28.0",
|
|
180
|
+
"markitdown-ts": "0.0.10",
|
|
181
|
+
"minimatch": "10.2.6",
|
|
182
|
+
"nanoid": "6.0.0",
|
|
183
|
+
"node-llama-cpp": "3.19.1",
|
|
184
|
+
"officeparser": "7.5.0",
|
|
185
|
+
"pdfjs-dist": "6.2.108",
|
|
183
186
|
"picocolors": "1.1.1",
|
|
184
|
-
"react": "19.2.
|
|
185
|
-
"react-dom": "19.2.
|
|
186
|
-
"react-force-graph-2d": "1.29.
|
|
187
|
+
"react": "19.2.8",
|
|
188
|
+
"react-dom": "19.2.8",
|
|
189
|
+
"react-force-graph-2d": "1.29.1",
|
|
187
190
|
"react-markdown": "10.1.0",
|
|
188
191
|
"rehype-sanitize": "6.0.0",
|
|
189
192
|
"remark-gfm": "4.0.1",
|
|
190
|
-
"shiki": "4.
|
|
193
|
+
"shiki": "4.3.1",
|
|
191
194
|
"sqlite-vec": "0.1.9",
|
|
192
|
-
"streamdown": "2.
|
|
193
|
-
"tailwind-merge": "3.
|
|
194
|
-
"tailwindcss": "4.
|
|
195
|
-
"use-stick-to-bottom": "1.1.
|
|
196
|
-
"zod": "4.3
|
|
195
|
+
"streamdown": "2.5.0",
|
|
196
|
+
"tailwind-merge": "3.6.0",
|
|
197
|
+
"tailwindcss": "4.3.3",
|
|
198
|
+
"use-stick-to-bottom": "1.1.6",
|
|
199
|
+
"zod": "4.4.3"
|
|
197
200
|
},
|
|
198
201
|
"devDependencies": {
|
|
199
202
|
"@ai-sdk/openai": "3.0.25",
|
|
200
|
-
"@biomejs/biome": "2.
|
|
201
|
-
"@tailwindcss/cli": "4.
|
|
203
|
+
"@biomejs/biome": "2.5.6",
|
|
204
|
+
"@tailwindcss/cli": "4.3.3",
|
|
202
205
|
"@testing-library/react": "16.3.2",
|
|
203
206
|
"@testing-library/user-event": "14.6.1",
|
|
204
|
-
"@types/bun": "1.3.
|
|
205
|
-
"@types/react": "19.2.
|
|
207
|
+
"@types/bun": "1.3.14",
|
|
208
|
+
"@types/react": "19.2.17",
|
|
206
209
|
"@types/react-dom": "19.2.3",
|
|
207
210
|
"@vscode/tree-sitter-wasm": "0.3.1",
|
|
208
211
|
"ajv": "8.17.1",
|
|
209
212
|
"ajv-formats": "3.0.1",
|
|
210
213
|
"bun": "1.3.14",
|
|
211
|
-
"docx": "9.
|
|
214
|
+
"docx": "9.7.1",
|
|
212
215
|
"evalite": "1.0.0-beta.16",
|
|
213
216
|
"exceljs": "4.4.0",
|
|
214
|
-
"happy-dom": "20.
|
|
215
|
-
"lefthook": "2.1.
|
|
217
|
+
"happy-dom": "20.11.1",
|
|
218
|
+
"lefthook": "2.1.10",
|
|
216
219
|
"oxfmt": "0.28.0",
|
|
217
220
|
"oxlint": "1.43.0",
|
|
218
221
|
"oxlint-tsgolint": "0.11.5",
|
|
219
222
|
"pdf-lib": "1.17.1",
|
|
220
|
-
"playwright": "1.
|
|
223
|
+
"playwright": "1.62.0",
|
|
221
224
|
"pptxgenjs": "4.0.1",
|
|
222
225
|
"ultracite": "7.1.5",
|
|
223
|
-
"vitest": "4.1.
|
|
224
|
-
"web-tree-sitter": "0.26.
|
|
226
|
+
"vitest": "4.1.10",
|
|
227
|
+
"web-tree-sitter": "0.26.11"
|
|
225
228
|
},
|
|
226
229
|
"peerDependencies": {
|
|
227
230
|
"typescript": "^5"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* officeparser adapter for PPTX conversion.
|
|
3
|
-
* Uses parseOffice()
|
|
3
|
+
* Uses parseOffice() v7 API with Buffer for in-memory extraction.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { parseOffice } from "officeparser";
|
|
@@ -76,12 +76,16 @@ export const officeparserAdapter: Converter = {
|
|
|
76
76
|
try {
|
|
77
77
|
// Zero-copy Buffer view (input.bytes is immutable by contract)
|
|
78
78
|
const buffer = toBuffer(input.bytes);
|
|
79
|
-
//
|
|
79
|
+
// v7's async text renderer preserves notes while keeping the previous
|
|
80
|
+
// plain-text layout and omitting image placeholders.
|
|
80
81
|
const ast = await parseOffice(buffer, {
|
|
81
82
|
newlineDelimiter: "\n",
|
|
82
83
|
ignoreNotes: false, // Include speaker notes
|
|
83
84
|
});
|
|
84
|
-
const text = ast.
|
|
85
|
+
const { value: text } = await ast.to("text", {
|
|
86
|
+
includeImages: false,
|
|
87
|
+
textConfig: { preserveLayout: false, renderNotes: true },
|
|
88
|
+
});
|
|
85
89
|
|
|
86
90
|
if (!text || text.trim().length === 0) {
|
|
87
91
|
return {
|
|
@@ -218,6 +218,57 @@ export const NETWORK_BOUNDARY_INVENTORY = [
|
|
|
218
218
|
action: "serve",
|
|
219
219
|
enforcement: "loopback_only",
|
|
220
220
|
},
|
|
221
|
+
{
|
|
222
|
+
// Loopback self-request that warms the SPA shell cache from the server's
|
|
223
|
+
// own bound port (127.0.0.1). Carries no collection data and never leaves
|
|
224
|
+
// the host.
|
|
225
|
+
id: "serve-spa-shell",
|
|
226
|
+
key: "src/serve/server.ts::fetch#1",
|
|
227
|
+
path: "src/serve/server.ts",
|
|
228
|
+
primitive: "fetch",
|
|
229
|
+
action: "serve",
|
|
230
|
+
enforcement: "loopback_only",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
// Windows-only private SPA bundle listener. Bun does not support Unix
|
|
234
|
+
// sockets on Windows, so this binds an ephemeral loopback port instead.
|
|
235
|
+
id: "serve-spa-bundle-windows-listener",
|
|
236
|
+
key: "src/serve/spa-bundle-source.ts::bun_serve#1",
|
|
237
|
+
path: "src/serve/spa-bundle-source.ts",
|
|
238
|
+
primitive: "bun_serve",
|
|
239
|
+
action: "serve",
|
|
240
|
+
enforcement: "loopback_only",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
// Unix-only private SPA bundle listener. The generated assets are exposed
|
|
244
|
+
// through a process-owned Unix-domain socket, never a TCP interface.
|
|
245
|
+
id: "serve-spa-bundle-unix-listener",
|
|
246
|
+
key: "src/serve/spa-bundle-source.ts::bun_serve#2",
|
|
247
|
+
path: "src/serve/spa-bundle-source.ts",
|
|
248
|
+
primitive: "bun_serve",
|
|
249
|
+
action: "serve",
|
|
250
|
+
enforcement: "local_process_only",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
// Windows-only request from the public server to its private, ephemeral
|
|
254
|
+
// loopback SPA bundle listener.
|
|
255
|
+
id: "serve-spa-bundle-windows-fetch",
|
|
256
|
+
key: "src/serve/spa-bundle-source.ts::fetch#1",
|
|
257
|
+
path: "src/serve/spa-bundle-source.ts",
|
|
258
|
+
primitive: "fetch",
|
|
259
|
+
action: "serve",
|
|
260
|
+
enforcement: "loopback_only",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
// Unix-only request over the process-owned SPA bundle socket. The HTTP URL
|
|
264
|
+
// is a Bun fetch API requirement; the request is routed through `unix`.
|
|
265
|
+
id: "serve-spa-bundle-unix-fetch",
|
|
266
|
+
key: "src/serve/spa-bundle-source.ts::fetch#2",
|
|
267
|
+
path: "src/serve/spa-bundle-source.ts",
|
|
268
|
+
primitive: "fetch",
|
|
269
|
+
action: "serve",
|
|
270
|
+
enforcement: "local_process_only",
|
|
271
|
+
},
|
|
221
272
|
{
|
|
222
273
|
id: "http-mcp-tools",
|
|
223
274
|
key: "logical::http-mcp-tools",
|
|
@@ -146,13 +146,13 @@ const resolveContainedPath = async (
|
|
|
146
146
|
| {
|
|
147
147
|
ok: true;
|
|
148
148
|
absolutePath: string;
|
|
149
|
-
metadata: Awaited<ReturnType<typeof stat
|
|
149
|
+
metadata: NonNullable<Awaited<ReturnType<typeof stat>>>;
|
|
150
150
|
}
|
|
151
151
|
| { ok: false; diagnostic: ProjectProfileDiagnostic }
|
|
152
152
|
> => {
|
|
153
153
|
const candidate = resolve(profileRoot, normalizeLogicalPath(logicalPath));
|
|
154
154
|
let absolutePath: string;
|
|
155
|
-
let metadata: Awaited<ReturnType<typeof stat
|
|
155
|
+
let metadata: NonNullable<Awaited<ReturnType<typeof stat>>>;
|
|
156
156
|
try {
|
|
157
157
|
absolutePath = await realpath(candidate);
|
|
158
158
|
metadata = await stat(absolutePath);
|
package/src/serve/AGENTS.md
CHANGED
|
@@ -74,6 +74,8 @@ Answer generation uses shared module to stay in sync with CLI:
|
|
|
74
74
|
| `/api/publish/export` | POST | Export gno.sh publish artifact JSON |
|
|
75
75
|
| `/api/docs` | GET | List documents |
|
|
76
76
|
| `/api/doc` | GET | Get document content |
|
|
77
|
+
| `/api/doc-asset` | GET | Original source file bytes (Range, HEAD) |
|
|
78
|
+
| `/vendor/pdfjs/*` | GET | Same-origin pdfjs worker/cmaps/fonts |
|
|
77
79
|
| `/api/search` | POST | BM25 search |
|
|
78
80
|
| `/api/query` | POST | Hybrid search |
|
|
79
81
|
| `/api/ask` | POST | AI answer with citations |
|
|
@@ -106,7 +108,9 @@ gno serve --port 3000
|
|
|
106
108
|
- Binds to `127.0.0.1` only (no LAN exposure)
|
|
107
109
|
- CSP headers on all responses
|
|
108
110
|
- CORS protection on POST endpoints
|
|
109
|
-
- No external font/script loading
|
|
111
|
+
- No external font/script loading — the PDF.js worker, cMaps, and standard fonts
|
|
112
|
+
are served same-origin from the installed `pdfjs-dist` package via
|
|
113
|
+
`/vendor/pdfjs/*`, so the CSP keeps `worker-src 'self'` and `font-src 'self'`
|
|
110
114
|
|
|
111
115
|
## Bun.serve() Patterns
|
|
112
116
|
|
package/src/serve/CLAUDE.md
CHANGED
|
@@ -76,6 +76,8 @@ Answer generation uses shared module to stay in sync with CLI:
|
|
|
76
76
|
| `/api/publish/export` | POST | Export gno.sh publish artifact JSON |
|
|
77
77
|
| `/api/docs` | GET | List documents |
|
|
78
78
|
| `/api/doc` | GET | Get document content |
|
|
79
|
+
| `/api/doc-asset` | GET | Original source file bytes (Range, HEAD) |
|
|
80
|
+
| `/vendor/pdfjs/*` | GET | Same-origin pdfjs worker/cmaps/fonts |
|
|
79
81
|
| `/api/search` | POST | BM25 search |
|
|
80
82
|
| `/api/query` | POST | Hybrid search |
|
|
81
83
|
| `/api/ask` | POST | AI answer with citations |
|
|
@@ -108,7 +110,9 @@ gno serve --port 3000
|
|
|
108
110
|
- Binds to `127.0.0.1` only (no LAN exposure)
|
|
109
111
|
- CSP headers on all responses
|
|
110
112
|
- CORS protection on POST endpoints
|
|
111
|
-
- No external font/script loading
|
|
113
|
+
- No external font/script loading — the PDF.js worker, cMaps, and standard fonts
|
|
114
|
+
are served same-origin from the installed `pdfjs-dist` package via
|
|
115
|
+
`/vendor/pdfjs/*`, so the CSP keeps `worker-src 'self'` and `font-src 'self'`
|
|
112
116
|
|
|
113
117
|
## Bun.serve() Patterns
|
|
114
118
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production route factories for fn-112 doc-asset + pdfjs vendor surfaces.
|
|
3
|
+
* server.ts and tests MUST consume these — no duplicated route maps.
|
|
4
|
+
*
|
|
5
|
+
* Vendor: ONE production dispatcher (`handlePdfjsVendorRequest`) is the sole
|
|
6
|
+
* path for every /vendor/pdfjs/* request (valid or malformed, any method).
|
|
7
|
+
* server.ts routes that prefix through fetch; tests call the same function.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Config } from "../config/types";
|
|
11
|
+
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
12
|
+
import type { ResidentRuntime } from "./resident-runtime";
|
|
13
|
+
|
|
14
|
+
import { handlePdfjsAsset, PDFJS_ASSET_CACHE_CONTROL } from "./pdfjs-assets";
|
|
15
|
+
import { handleResidentRead } from "./resident-request";
|
|
16
|
+
import { handleDocAsset } from "./routes/api";
|
|
17
|
+
|
|
18
|
+
const PDFJS_WORKER_BOOTSTRAP = `
|
|
19
|
+
if (typeof Math.sumPrecise !== "function") {
|
|
20
|
+
Object.defineProperty(Math, "sumPrecise", {
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value(values) {
|
|
24
|
+
let sum = 0;
|
|
25
|
+
let compensation = 0;
|
|
26
|
+
for (const value of values) {
|
|
27
|
+
const next = sum + value;
|
|
28
|
+
compensation += Math.abs(sum) >= Math.abs(value)
|
|
29
|
+
? sum - next + value
|
|
30
|
+
: value - next + sum;
|
|
31
|
+
sum = next;
|
|
32
|
+
}
|
|
33
|
+
return sum + compensation;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
await import("/vendor/pdfjs/pdf.worker.raw.min.mjs");
|
|
38
|
+
`.trimStart();
|
|
39
|
+
|
|
40
|
+
export type SecurityHeaderWrap = (
|
|
41
|
+
response: Response,
|
|
42
|
+
isDev: boolean
|
|
43
|
+
) => Response;
|
|
44
|
+
|
|
45
|
+
export type DocAssetRouteContext = {
|
|
46
|
+
store: SqliteAdapter;
|
|
47
|
+
/** Live config getter (ctxHolder.config may change). */
|
|
48
|
+
getConfig: () => Config;
|
|
49
|
+
runtime: ResidentRuntime;
|
|
50
|
+
isDev: boolean;
|
|
51
|
+
withSecurityHeaders: SecurityHeaderWrap;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type MethodHandlers = {
|
|
55
|
+
GET: (req: Request) => Promise<Response> | Response;
|
|
56
|
+
HEAD: (req: Request) => Promise<Response> | Response;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Exact production error envelopes for vendor routes (I1-04 round 3). */
|
|
60
|
+
export const PDFJS_VENDOR_ERRORS = {
|
|
61
|
+
NOT_FOUND: {
|
|
62
|
+
code: "NOT_FOUND",
|
|
63
|
+
message: "Asset not found",
|
|
64
|
+
},
|
|
65
|
+
METHOD_NOT_ALLOWED: {
|
|
66
|
+
code: "METHOD_NOT_ALLOWED",
|
|
67
|
+
message: "Only GET and HEAD are supported",
|
|
68
|
+
},
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Production `/api/doc-asset` GET+HEAD handlers.
|
|
73
|
+
* Both methods traverse handleResidentRead (admission) then withSecurityHeaders.
|
|
74
|
+
*/
|
|
75
|
+
export function createDocAssetRouteHandlers(
|
|
76
|
+
ctx: DocAssetRouteContext
|
|
77
|
+
): MethodHandlers {
|
|
78
|
+
const dispatch = async (req: Request): Promise<Response> => {
|
|
79
|
+
const url = new URL(req.url);
|
|
80
|
+
return ctx.withSecurityHeaders(
|
|
81
|
+
await handleResidentRead(ctx.runtime, req, () =>
|
|
82
|
+
handleDocAsset(ctx.store, ctx.getConfig(), url, req)
|
|
83
|
+
),
|
|
84
|
+
ctx.isDev
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
GET: dispatch,
|
|
89
|
+
HEAD: dispatch,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type PdfjsVendorDispatchOptions = {
|
|
94
|
+
isDev: boolean;
|
|
95
|
+
withSecurityHeaders: SecurityHeaderWrap;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Production vendor dispatcher — the ONLY production path for `/vendor/pdfjs/*`.
|
|
100
|
+
*
|
|
101
|
+
* Handles every method and every pathname under the prefix (worker, cmaps,
|
|
102
|
+
* standard_fonts, multi-segment, empty, encoded traversal, invalid encoding).
|
|
103
|
+
* ALL responses — success, 404, 405 — go through withSecurityHeaders exactly once.
|
|
104
|
+
*
|
|
105
|
+
* server.ts must call this for the prefix (via fetch); tests must call this
|
|
106
|
+
* identical function (no separate test-only fallback).
|
|
107
|
+
*/
|
|
108
|
+
export async function handlePdfjsVendorRequest(
|
|
109
|
+
request: Request,
|
|
110
|
+
options: PdfjsVendorDispatchOptions
|
|
111
|
+
): Promise<Response> {
|
|
112
|
+
const wrap = options.withSecurityHeaders;
|
|
113
|
+
const isDev = options.isDev;
|
|
114
|
+
const method = request.method.toUpperCase();
|
|
115
|
+
const pathname = new URL(request.url).pathname;
|
|
116
|
+
|
|
117
|
+
// Never double-wrap: build the inner response, then wrap once.
|
|
118
|
+
const inner = await dispatchPdfjsVendorInner(method, pathname);
|
|
119
|
+
return wrap(inner, isDev);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* True when this request should be handled by the vendor dispatcher.
|
|
124
|
+
* Used by server fetch to claim the whole prefix.
|
|
125
|
+
*/
|
|
126
|
+
export function isPdfjsVendorPath(pathname: string): boolean {
|
|
127
|
+
return pathname === "/vendor/pdfjs" || pathname.startsWith("/vendor/pdfjs/");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function dispatchPdfjsVendorInner(
|
|
131
|
+
method: string,
|
|
132
|
+
pathname: string
|
|
133
|
+
): Promise<Response> {
|
|
134
|
+
// Only GET/HEAD are ever valid on any vendor path
|
|
135
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
136
|
+
return methodNotAllowedResponse();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Public worker bootstrap: install the ES2026 Math.sumPrecise compatibility
|
|
140
|
+
// required by pdfjs-dist before evaluating its worker module.
|
|
141
|
+
if (pathname === "/vendor/pdfjs/pdf.worker.min.mjs") {
|
|
142
|
+
const headers = {
|
|
143
|
+
"Cache-Control": PDFJS_ASSET_CACHE_CONTROL,
|
|
144
|
+
"Content-Type": "text/javascript",
|
|
145
|
+
"Content-Length": String(
|
|
146
|
+
new TextEncoder().encode(PDFJS_WORKER_BOOTSTRAP).byteLength
|
|
147
|
+
),
|
|
148
|
+
};
|
|
149
|
+
return new Response(method === "HEAD" ? null : PDFJS_WORKER_BOOTSTRAP, {
|
|
150
|
+
headers,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Package worker stays behind a fixed same-origin route; never referenced
|
|
155
|
+
// directly by application code, only by the bootstrap above.
|
|
156
|
+
if (pathname === "/vendor/pdfjs/pdf.worker.raw.min.mjs") {
|
|
157
|
+
return handlePdfjsAsset({ kind: "worker", method });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// cmaps/:file — single segment only
|
|
161
|
+
const cmapMatch = pathname.match(/^\/vendor\/pdfjs\/cmaps\/([^/]*)$/u);
|
|
162
|
+
if (cmapMatch) {
|
|
163
|
+
const raw = cmapMatch[1] ?? "";
|
|
164
|
+
if (raw === "") {
|
|
165
|
+
return notFoundResponse();
|
|
166
|
+
}
|
|
167
|
+
const file = decodeRouteSegment(raw);
|
|
168
|
+
if (file === null) {
|
|
169
|
+
return notFoundResponse();
|
|
170
|
+
}
|
|
171
|
+
return handlePdfjsAsset({ kind: "cmaps", file, method });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// standard_fonts/:file — single segment only
|
|
175
|
+
const fontMatch = pathname.match(
|
|
176
|
+
/^\/vendor\/pdfjs\/standard_fonts\/([^/]*)$/u
|
|
177
|
+
);
|
|
178
|
+
if (fontMatch) {
|
|
179
|
+
const raw = fontMatch[1] ?? "";
|
|
180
|
+
if (raw === "") {
|
|
181
|
+
return notFoundResponse();
|
|
182
|
+
}
|
|
183
|
+
const file = decodeRouteSegment(raw);
|
|
184
|
+
if (file === null) {
|
|
185
|
+
return notFoundResponse();
|
|
186
|
+
}
|
|
187
|
+
return handlePdfjsAsset({ kind: "standard_fonts", file, method });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Multi-segment, empty prefix, unknown subpaths, traversal that doesn't
|
|
191
|
+
// collapse to a single-segment match → 404
|
|
192
|
+
return notFoundResponse();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function decodeRouteSegment(segment: string): string | null {
|
|
196
|
+
try {
|
|
197
|
+
return decodeURIComponent(segment);
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function notFoundResponse(): Response {
|
|
204
|
+
return Response.json(
|
|
205
|
+
{ error: { ...PDFJS_VENDOR_ERRORS.NOT_FOUND } },
|
|
206
|
+
{ status: 404 }
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function methodNotAllowedResponse(): Response {
|
|
211
|
+
return Response.json(
|
|
212
|
+
{ error: { ...PDFJS_VENDOR_ERRORS.METHOD_NOT_ALLOWED } },
|
|
213
|
+
{ status: 405 }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @deprecated Use handlePdfjsVendorRequest — kept only if any import remains.
|
|
219
|
+
* Prefer the single production dispatcher.
|
|
220
|
+
*/
|
|
221
|
+
export async function dispatchPdfjsVendorRoute(
|
|
222
|
+
_routeMap: unknown,
|
|
223
|
+
request: Request,
|
|
224
|
+
options?: PdfjsVendorDispatchOptions
|
|
225
|
+
): Promise<Response> {
|
|
226
|
+
if (!options) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
"dispatchPdfjsVendorRoute requires production options; use handlePdfjsVendorRequest"
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return handlePdfjsVendorRequest(request, options);
|
|
232
|
+
}
|