@gmickel/gno 1.29.5 → 1.30.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.
Files changed (30) hide show
  1. package/README.md +15 -5
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.29.5.zip → gno-browser-clipper-v1.30.1.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.30.1.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +4 -1
  6. package/src/core/network-boundary-inventory.ts +51 -0
  7. package/src/ingestion/index.ts +1 -1
  8. package/src/ingestion/walker.ts +45 -23
  9. package/src/serve/AGENTS.md +5 -1
  10. package/src/serve/CLAUDE.md +5 -1
  11. package/src/serve/fn112-routes.ts +232 -0
  12. package/src/serve/pdfjs-assets.ts +391 -0
  13. package/src/serve/public/components/pdf/PdfPageView.tsx +427 -0
  14. package/src/serve/public/components/pdf/PdfToolbar.tsx +384 -0
  15. package/src/serve/public/components/pdf/PdfViewer.tsx +539 -0
  16. package/src/serve/public/components/pdf/pdf-viewer-deps.tsx +94 -0
  17. package/src/serve/public/globals.built.css +1 -1
  18. package/src/serve/public/globals.css +113 -0
  19. package/src/serve/public/hooks/use-pdf-document.ts +227 -0
  20. package/src/serve/public/hooks/use-pdf-pages.ts +1197 -0
  21. package/src/serve/public/lib/doc-asset-url.ts +57 -0
  22. package/src/serve/public/lib/math-sum-precise.ts +34 -0
  23. package/src/serve/public/lib/pdf.ts +772 -0
  24. package/src/serve/public/pages/DocView.tsx +295 -39
  25. package/src/serve/public/pages/doc-pdf-viewer.tsx +7 -0
  26. package/src/serve/routes/api.ts +154 -14
  27. package/src/serve/server.ts +190 -37
  28. package/src/serve/spa-bundle-source.ts +99 -0
  29. package/src/serve/watch-service.ts +219 -23
  30. package/browser-extension/artifacts/gno-browser-clipper-v1.29.5.zip.sha256 +0 -1
package/README.md CHANGED
@@ -20,13 +20,23 @@ gno mcp install --target cursor # or claude-code, claude-desktop, zed, ...
20
20
 
21
21
  ## What you get
22
22
 
23
- BM25 and vector retrieval fused and reranked over Markdown, code, PDFs, Office files, and exported mail, calendar and transcripts. A web workspace with cross-collection browse, a knowledge graph, a markdown editor and provenance-carrying capture. A fast CLI, a TypeScript SDK, a REST API, a headless daemon, and one-command install into ten agent clients. Optional hosted publishing when a slice needs a URL.
23
+ **One local index across everything you have.** Markdown, PDFs, Office documents, plain text, source code, and portable mail, calendar and transcript exports. Point it at a folder that mixes all of them and it handles the mix.
24
24
 
25
- That is the table stakes. Here is the part that is harder to find elsewhere.
25
+ **Three ways to search it.** Keyword (BM25), semantic (vector), and hybrid — fused, reranked, and explainable, with structured intent controls and metadata filters. Ask for "how we handle retries" and find the paragraph about exponential backoff that never uses the word.
26
+
27
+ **A workspace, not a search box.** Cross-collection folder tree, per-tab browse context, a markdown editor, provenance-carrying quick capture, and a navigable knowledge graph.
28
+
29
+ **Answers with citations.** Ask a question in natural language and get an answer built from your own documents, with citations that resolve to the source passage.
30
+
31
+ **Six interfaces on one index.** A fast CLI, a web UI, a REST API, a TypeScript SDK, an MCP server, and a headless daemon — plus one-command install into ten agent clients. Nothing drifts between them.
32
+
33
+ **Optional hosted publishing** at [gno.sh](https://gno.sh) when a slice needs a URL.
34
+
35
+ No GPU required, no account required, no telemetry. Free and MIT licensed.
26
36
 
27
37
  ## Why not just another local RAG tool
28
38
 
29
- Most retrieval tools return a ranked list and leave the rest to optimism. Four things here are different, and each one is measurable rather than adjectival:
39
+ The above is most of what people use day to day. Beyond it, four things here are different, and each one is measurable rather than adjectival:
30
40
 
31
41
  | | What it does | Why it matters |
32
42
  | :----------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -37,7 +47,7 @@ Most retrieval tools return a ranked list and leave the rest to optimism. Four t
37
47
 
38
48
  Everything runs on your machine. Zero telemetry. The three network boundaries are explicit: downloading a model, configuring an HTTP inference endpoint, and uploading an artifact you exported for publishing.
39
49
 
40
- **And when it fails, that ships too.** The CJK lexical benchmark missed its own promotion gates, so the analyzer was not shipped and [the failing numbers were published](#general-multilingual-embedding-benchmark) instead.
50
+ **And the receipts ship too.** Every benchmark behind these numbers is committed to the repository against a pinned corpus, with its limitations stated alongside the result, so you can replay it rather than take it on trust.
41
51
 
42
52
  ## Use it when
43
53
 
@@ -107,7 +117,7 @@ gno daemon --detach # headless indexing + resident MCP gateway
107
117
 
108
118
  <!-- public-truth:current-version -->
109
119
 
110
- > Current release: **v1.29.5** — see [CHANGELOG.md](./CHANGELOG.md)
120
+ > Current release: **v1.30.1** — see [CHANGELOG.md](./CHANGELOG.md)
111
121
 
112
122
  <!-- /public-truth -->
113
123
 
@@ -0,0 +1 @@
1
+ 7f28a8daa50eec43dcd95d5af91002a5e25cd87682ff7c99905af8c9f3457b24 gno-browser-clipper-v1.30.1.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "1.29.5"
24
+ "version": "1.30.1"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.29.5",
3
+ "version": "1.30.1",
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",
@@ -180,6 +182,7 @@
180
182
  "nanoid": "5.1.6",
181
183
  "node-llama-cpp": "3.18.1",
182
184
  "officeparser": "6.0.4",
185
+ "pdfjs-dist": "5.7.284",
183
186
  "picocolors": "1.1.1",
184
187
  "react": "19.2.4",
185
188
  "react-dom": "19.2.4",
@@ -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",
@@ -31,4 +31,4 @@ export type {
31
31
  } from "./types";
32
32
  export { collectionToWalkConfig, DEFAULT_CHUNK_PARAMS } from "./types";
33
33
  // Walker
34
- export { defaultWalker, FileWalker } from "./walker";
34
+ export { defaultWalker, FileWalker, matchesWalkPath } from "./walker";
@@ -174,6 +174,50 @@ function matchesInclude(
174
174
  });
175
175
  }
176
176
 
177
+ type PathEligibilityConfig = Pick<
178
+ WalkConfig,
179
+ "additionalDefaultExtensions" | "exclude" | "include" | "pattern"
180
+ >;
181
+
182
+ /**
183
+ * Check collection-relative path eligibility without touching the filesystem.
184
+ * This intentionally remains usable for deleted paths so incremental sync can
185
+ * mark previously indexed documents inactive.
186
+ */
187
+ export function matchesWalkPath(
188
+ relPath: string,
189
+ config: PathEligibilityConfig
190
+ ): boolean {
191
+ const normalizedPath = relPath.replaceAll("\\", "/");
192
+ if (
193
+ isAbsolute(normalizedPath) ||
194
+ DANGEROUS_PATTERN_REGEX.test(normalizedPath) ||
195
+ isRecordVirtualPath(normalizedPath)
196
+ ) {
197
+ return false;
198
+ }
199
+
200
+ let matchesPattern = false;
201
+ try {
202
+ matchesPattern = new Bun.Glob(config.pattern).match(normalizedPath);
203
+ } catch {
204
+ return false;
205
+ }
206
+ if (!matchesPattern) {
207
+ return false;
208
+ }
209
+
210
+ if (matchesCollectionExclusion(normalizedPath, config.exclude)) {
211
+ return false;
212
+ }
213
+
214
+ return matchesInclude(
215
+ normalizedPath,
216
+ config.include,
217
+ config.additionalDefaultExtensions ?? []
218
+ );
219
+ }
220
+
177
221
  /**
178
222
  * File walker implementation using Bun.Glob.
179
223
  *
@@ -228,29 +272,7 @@ export class FileWalker implements WalkerPort {
228
272
  }
229
273
  const { absPath, relPath } = safePath;
230
274
 
231
- if (isRecordVirtualPath(relPath)) {
232
- skipped.push({ absPath, relPath, reason: "EXCLUDED" });
233
- continue;
234
- }
235
-
236
- // Check exclude patterns
237
- if (matchesCollectionExclusion(relPath, config.exclude)) {
238
- skipped.push({
239
- absPath,
240
- relPath,
241
- reason: "EXCLUDED",
242
- });
243
- continue;
244
- }
245
-
246
- // Check include extensions
247
- if (
248
- !matchesInclude(
249
- relPath,
250
- config.include,
251
- config.additionalDefaultExtensions ?? []
252
- )
253
- ) {
275
+ if (!matchesWalkPath(relPath, config)) {
254
276
  skipped.push({
255
277
  absPath,
256
278
  relPath,
@@ -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
 
@@ -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
+ }