@gmickel/gno 1.39.2 → 1.40.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/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.39.2.zip → gno-browser-clipper-v1.40.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.40.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/src/core/network-boundary-inventory.ts +10 -0
- package/src/serve/CLAUDE.md +22 -2
- package/src/serve/clipper-security.ts +3 -17
- package/src/serve/public/components/pdf/PdfViewer.tsx +97 -19
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/globals.css +4 -0
- package/src/serve/public/hooks/use-pdf-document.ts +101 -37
- package/src/serve/public/hooks/use-pdf-pages.ts +324 -92
- package/src/serve/public/lib/pdf-transport.ts +13 -0
- package/src/serve/public/lib/pdf.ts +77 -10
- package/src/serve/public/lib/server-capabilities.ts +19 -0
- package/src/serve/public/pages/Ask.tsx +8 -9
- package/src/serve/public/pages/DocView.tsx +80 -18
- package/src/serve/public/pages/Search.tsx +8 -9
- package/src/serve/request-locality.ts +84 -0
- package/src/serve/routes/api.ts +88 -3
- package/src/serve/server.ts +163 -29
- package/browser-extension/artifacts/gno-browser-clipper-v1.39.2.zip.sha256 +0 -1
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1d6a17618bddf4425527e94961459fa2a414d06f5123d43ffebadfeca8a3a89b gno-browser-clipper-v1.40.0.zip
|
package/package.json
CHANGED
|
@@ -192,6 +192,16 @@ export const NETWORK_BOUNDARY_INVENTORY = [
|
|
|
192
192
|
enforcement: "client_transport",
|
|
193
193
|
serverBoundary: "src/serve/server.ts",
|
|
194
194
|
},
|
|
195
|
+
{
|
|
196
|
+
// Same-origin HEAD against /api/doc-asset to pick the PDF transport tier.
|
|
197
|
+
id: "browser-pdf-transport-probe",
|
|
198
|
+
key: "src/serve/public/hooks/use-pdf-document.ts::fetch#1",
|
|
199
|
+
path: "src/serve/public/hooks/use-pdf-document.ts",
|
|
200
|
+
primitive: "fetch",
|
|
201
|
+
action: "serve",
|
|
202
|
+
enforcement: "client_transport",
|
|
203
|
+
serverBoundary: "src/serve/routes/api.ts",
|
|
204
|
+
},
|
|
195
205
|
{
|
|
196
206
|
id: "clipper-pair-csrf",
|
|
197
207
|
key: "src/serve/public/lib/clipper-approval.ts::fetch#1",
|
package/src/serve/CLAUDE.md
CHANGED
|
@@ -71,12 +71,13 @@ Answer generation uses shared module to stay in sync with CLI:
|
|
|
71
71
|
| --------------------- | ------ | ------------------------------------------ |
|
|
72
72
|
| `/api/health` | GET | Health check |
|
|
73
73
|
| `/api/status` | GET | Index stats, onboarding, health, bootstrap |
|
|
74
|
-
| `/api/capabilities` | GET | Available features
|
|
74
|
+
| `/api/capabilities` | GET | Available features + `localClient` |
|
|
75
75
|
| `/api/collections` | GET | List collections |
|
|
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 |
|
|
79
|
+
| `/api/doc-asset` | GET | Source file bytes (Range, HEAD, ETag/304) |
|
|
80
|
+
| `/api/docs/:id/reveal`| POST | Reveal source on host (local client only) |
|
|
80
81
|
| `/vendor/pdfjs/*` | GET | Same-origin pdfjs worker/cmaps/fonts |
|
|
81
82
|
| `/api/search` | POST | BM25 search |
|
|
82
83
|
| `/api/query` | POST | Hybrid search |
|
|
@@ -94,6 +95,17 @@ Answer generation uses shared module to stay in sync with CLI:
|
|
|
94
95
|
- **AI Elements**: Conversation, Message, Sources, CodeBlock, Loader
|
|
95
96
|
- **Tag Components**: TagInput, TagFacets (filter sidebar)
|
|
96
97
|
- **Routing**: Simple hash-free SPA routing in App.tsx
|
|
98
|
+
- **Server capabilities**: `public/lib/server-capabilities.ts` is the one
|
|
99
|
+
shared `ServerCapabilities` type (`localClient`) plus
|
|
100
|
+
`fetchServerCapabilities()`; a failed read counts as remote
|
|
101
|
+
- **PDF viewer transport**: `public/lib/pdf-transport.ts` holds the tier
|
|
102
|
+
constants (`PDF_WHOLE_FILE_MAX_BYTES` = 8 MB, `PDF_RANGE_CHUNK_BYTES` =
|
|
103
|
+
1 MB) with no pdfjs import so Bun-side tooling can read them;
|
|
104
|
+
`hooks/use-pdf-document.ts` sends one same-origin `HEAD` per load to pick
|
|
105
|
+
the tier and falls back to ranged on any probe failure;
|
|
106
|
+
`hooks/use-pdf-pages.ts` publishes page slots from page 1 geometry and
|
|
107
|
+
corrects sizes and fit scales in one commit when the full pass lands
|
|
108
|
+
(fatal error = page 1 or document; later pages carry `slot.error`)
|
|
97
109
|
|
|
98
110
|
## Development
|
|
99
111
|
|
|
@@ -110,6 +122,14 @@ gno serve --port 3000
|
|
|
110
122
|
- Binds to `127.0.0.1` only (no LAN exposure)
|
|
111
123
|
- CSP headers on all responses
|
|
112
124
|
- CORS protection on POST endpoints
|
|
125
|
+
- Request locality (`request-locality.ts`): `localClient` is true only for a
|
|
126
|
+
loopback peer, a loopback `Host`, and no forwarding headers; the reveal
|
|
127
|
+
route refuses non-local clients with 403
|
|
128
|
+
- HTTP caching: `/api/doc-asset` sends a strong `ETag` (size + mtime) with
|
|
129
|
+
`private, max-age=0, must-revalidate` and answers `If-None-Match` with 304
|
|
130
|
+
before Range handling; hashed SPA chunks (`/chunk-*.js|css`) get a one-year
|
|
131
|
+
`immutable` policy and a gzip body via `createPublicFetchFallback` in
|
|
132
|
+
`server.ts` (production only; the entry HTML and dev/HMR are untouched)
|
|
113
133
|
- No external font/script loading — the PDF.js worker, cMaps, and standard fonts
|
|
114
134
|
are served same-origin from the installed `pdfjs-dist` package via
|
|
115
135
|
`/vendor/pdfjs/*`, so the CSP keeps `worker-src 'self'` and `font-src 'self'`
|
|
@@ -4,6 +4,7 @@ import type { HttpMcpPeerServer } from "../mcp/http-security";
|
|
|
4
4
|
|
|
5
5
|
import { readClipperBoundedJson } from "./clipper-body";
|
|
6
6
|
import { clipperSecurityErrorResponse } from "./clipper-security-errors";
|
|
7
|
+
import { isLoopbackAddress } from "./request-locality";
|
|
7
8
|
import { ReaderGate } from "./resident-admission";
|
|
8
9
|
|
|
9
10
|
export { readClipperBoundedJson } from "./clipper-body";
|
|
@@ -126,7 +127,7 @@ function validateSameOriginEntry(origin: string): boolean {
|
|
|
126
127
|
|
|
127
128
|
function isLoopbackHostname(hostname: string): boolean {
|
|
128
129
|
if (hostname === "localhost") return true;
|
|
129
|
-
return
|
|
130
|
+
return isLoopbackAddress(hostname.replace(/^\[|\]$/g, ""));
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
function normalizedHeaderNames(headers: readonly string[]): string[] {
|
|
@@ -144,21 +145,6 @@ function normalizedHeaderNames(headers: readonly string[]): string[] {
|
|
|
144
145
|
return [...new Set(normalized)];
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
export function isClipperLoopbackAddress(address: string): boolean {
|
|
148
|
-
const normalized = address.toLowerCase();
|
|
149
|
-
if (normalized === "::1") return true;
|
|
150
|
-
const mapped = normalized.startsWith("::ffff:")
|
|
151
|
-
? normalized.slice("::ffff:".length)
|
|
152
|
-
: normalized;
|
|
153
|
-
const octets = mapped.split(".");
|
|
154
|
-
return (
|
|
155
|
-
octets.length === 4 &&
|
|
156
|
-
octets.every((octet) => /^\d{1,3}$/.test(octet)) &&
|
|
157
|
-
Number(octets[0]) === 127 &&
|
|
158
|
-
octets.every((octet) => Number(octet) <= 255)
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
148
|
export function parseClipperExtensionOrigin(
|
|
163
149
|
origin: string
|
|
164
150
|
): { extensionId: string; origin: string } | null {
|
|
@@ -370,7 +356,7 @@ export class ClipperSecurityBoundary {
|
|
|
370
356
|
}
|
|
371
357
|
| ClipperSecurityFailure {
|
|
372
358
|
const peer = server.requestIP(request);
|
|
373
|
-
if (!peer || !
|
|
359
|
+
if (!peer || !isLoopbackAddress(peer.address)) {
|
|
374
360
|
return {
|
|
375
361
|
ok: false,
|
|
376
362
|
response: clipperSecurityErrorResponse("CLIPPER_FORBIDDEN"),
|
|
@@ -99,6 +99,69 @@ function StatePanel({
|
|
|
99
99
|
);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const ERROR_EYEBROW: Record<PdfFallbackReason, string> = {
|
|
103
|
+
corrupt: "CANNOT RENDER",
|
|
104
|
+
password: "PASSWORD PROTECTED",
|
|
105
|
+
network: "COULD NOT LOAD",
|
|
106
|
+
bootstrap: "VIEWER UNAVAILABLE",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const PAGE_ERROR_BODY: Record<PdfFallbackReason, string> = {
|
|
110
|
+
corrupt: "This page could not be rendered.",
|
|
111
|
+
password: "This page is password protected.",
|
|
112
|
+
network: "This page could not be loaded from this session.",
|
|
113
|
+
bootstrap: "The PDF viewer could not render this page.",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type PdfPageErrorSlotProps = {
|
|
117
|
+
pageNumber: number;
|
|
118
|
+
width: number;
|
|
119
|
+
height: number;
|
|
120
|
+
error: PdfFallbackReason;
|
|
121
|
+
onMount: (pageNumber: number, el: HTMLElement | null) => void;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A later page whose geometry failed (R2): the page error state rendered in
|
|
126
|
+
* place of the page, at the slot's placeholder size so the scroll model and
|
|
127
|
+
* the visibility window keep their shape. Never fires the text fallback.
|
|
128
|
+
*/
|
|
129
|
+
function PdfPageErrorSlot({
|
|
130
|
+
pageNumber,
|
|
131
|
+
width,
|
|
132
|
+
height,
|
|
133
|
+
error,
|
|
134
|
+
onMount,
|
|
135
|
+
}: PdfPageErrorSlotProps) {
|
|
136
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
onMount(pageNumber, rootRef.current);
|
|
140
|
+
return () => {
|
|
141
|
+
onMount(pageNumber, null);
|
|
142
|
+
};
|
|
143
|
+
}, [pageNumber, onMount]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
className="gno-pdf-page flex flex-col items-center justify-center gap-2 px-4 text-center"
|
|
148
|
+
data-page-error={error}
|
|
149
|
+
data-page-number={pageNumber}
|
|
150
|
+
data-testid={`pdf-page-${pageNumber}`}
|
|
151
|
+
ref={rootRef}
|
|
152
|
+
role="status"
|
|
153
|
+
style={{ width: width || undefined, height: height || undefined }}
|
|
154
|
+
>
|
|
155
|
+
<p className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
|
|
156
|
+
{ERROR_EYEBROW[error]}
|
|
157
|
+
</p>
|
|
158
|
+
<p className="text-[13px] text-foreground/90 leading-relaxed">
|
|
159
|
+
Page {pageNumber}: {PAGE_ERROR_BODY[error]}
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
102
165
|
function prefersReducedMotion(): boolean {
|
|
103
166
|
if (
|
|
104
167
|
typeof window === "undefined" ||
|
|
@@ -184,7 +247,11 @@ export function PdfViewer({
|
|
|
184
247
|
containerWidth,
|
|
185
248
|
containerHeight,
|
|
186
249
|
genId,
|
|
250
|
+
scrollContainerRef: columnRef,
|
|
187
251
|
});
|
|
252
|
+
// Fatal only: a document load failure or the hook's fatal error (page 1
|
|
253
|
+
// geometry, render-path acquisition). A later page's geometry failure rides
|
|
254
|
+
// on its slot and never reaches this path.
|
|
188
255
|
const viewerError = status === "error" ? error : pages.error;
|
|
189
256
|
|
|
190
257
|
// Fallback: exactly once per failed load/page attempt when extracted text exists.
|
|
@@ -400,7 +467,7 @@ export function PdfViewer({
|
|
|
400
467
|
<StatePanel
|
|
401
468
|
body="This PDF could not be rendered. Download the original to read it."
|
|
402
469
|
downloadUrl={downloadUrl}
|
|
403
|
-
eyebrow=
|
|
470
|
+
eyebrow={ERROR_EYEBROW.corrupt}
|
|
404
471
|
onRetry={handleRetry}
|
|
405
472
|
role="alert"
|
|
406
473
|
testId="pdf-state-corrupt"
|
|
@@ -411,7 +478,7 @@ export function PdfViewer({
|
|
|
411
478
|
<StatePanel
|
|
412
479
|
body="This PDF is password protected. Download the original to open it in a PDF reader."
|
|
413
480
|
downloadUrl={downloadUrl}
|
|
414
|
-
eyebrow=
|
|
481
|
+
eyebrow={ERROR_EYEBROW.password}
|
|
415
482
|
role="alert"
|
|
416
483
|
testId="pdf-state-password"
|
|
417
484
|
/>
|
|
@@ -421,7 +488,7 @@ export function PdfViewer({
|
|
|
421
488
|
<StatePanel
|
|
422
489
|
body="The document could not be loaded from this session. Try again, or download the original."
|
|
423
490
|
downloadUrl={downloadUrl}
|
|
424
|
-
eyebrow=
|
|
491
|
+
eyebrow={ERROR_EYEBROW.network}
|
|
425
492
|
onRetry={handleRetry}
|
|
426
493
|
role="alert"
|
|
427
494
|
testId="pdf-state-network"
|
|
@@ -432,7 +499,7 @@ export function PdfViewer({
|
|
|
432
499
|
<StatePanel
|
|
433
500
|
body="The PDF viewer could not start in this window. Download the original to read it."
|
|
434
501
|
downloadUrl={downloadUrl}
|
|
435
|
-
eyebrow=
|
|
502
|
+
eyebrow={ERROR_EYEBROW.bootstrap}
|
|
436
503
|
onRetry={handleRetry}
|
|
437
504
|
role="alert"
|
|
438
505
|
testId="pdf-state-bootstrap"
|
|
@@ -512,21 +579,32 @@ export function PdfViewer({
|
|
|
512
579
|
|
|
513
580
|
{showProgressive ? (
|
|
514
581
|
<div className="mx-auto flex w-fit flex-col items-center gap-6">
|
|
515
|
-
{pages.slots.map((slot) =>
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
582
|
+
{pages.slots.map((slot) =>
|
|
583
|
+
slot.error ? (
|
|
584
|
+
<PdfPageErrorSlot
|
|
585
|
+
key={slot.pageNumber}
|
|
586
|
+
error={slot.error}
|
|
587
|
+
height={slot.height}
|
|
588
|
+
onMount={pages.observePage}
|
|
589
|
+
pageNumber={slot.pageNumber}
|
|
590
|
+
width={slot.width}
|
|
591
|
+
/>
|
|
592
|
+
) : (
|
|
593
|
+
<PdfPageView
|
|
594
|
+
key={slot.pageNumber}
|
|
595
|
+
active={slot.active}
|
|
596
|
+
doc={doc}
|
|
597
|
+
height={slot.height}
|
|
598
|
+
onMount={pages.observePage}
|
|
599
|
+
onInternalNavigate={goToPage}
|
|
600
|
+
onRender={pages.ensureRendered}
|
|
601
|
+
pageNumber={slot.pageNumber}
|
|
602
|
+
rendered={slot.rendered}
|
|
603
|
+
scale={pages.scale}
|
|
604
|
+
width={slot.width}
|
|
605
|
+
/>
|
|
606
|
+
)
|
|
607
|
+
)}
|
|
530
608
|
</div>
|
|
531
609
|
) : null}
|
|
532
610
|
</div>
|