@gmickel/gno 1.24.0 → 1.25.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 (51) hide show
  1. package/README.md +9 -1
  2. package/assets/skill/SKILL.md +11 -0
  3. package/assets/skill/recipes/capture-and-file.md +20 -5
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +1 -0
  6. package/browser-extension/dist/PRIVACY.md +55 -0
  7. package/browser-extension/dist/chunk-vn5f663b.js +50 -0
  8. package/browser-extension/dist/chunk-ydfx5d7p.css +1 -0
  9. package/browser-extension/dist/content.js +1 -0
  10. package/browser-extension/dist/manifest.json +25 -0
  11. package/browser-extension/dist/preview.html +13 -0
  12. package/browser-extension/dist/service-worker.js +40 -0
  13. package/package.json +13 -3
  14. package/spec/cli.md +50 -0
  15. package/spec/db/schema.sql +101 -0
  16. package/spec/mcp.md +10 -0
  17. package/spec/output-schemas/browser-clip-preview.schema.json +83 -0
  18. package/spec/output-schemas/browser-clip.schema.json +586 -0
  19. package/spec/output-schemas/capture-receipt.schema.json +22 -1
  20. package/spec/output-schemas/clipper-csrf.schema.json +12 -0
  21. package/spec/output-schemas/clipper-error.schema.json +46 -0
  22. package/spec/output-schemas/clipper-pair-approval.schema.json +17 -0
  23. package/spec/output-schemas/clipper-pair-start.schema.json +26 -0
  24. package/spec/output-schemas/clipper-pair-status.schema.json +46 -0
  25. package/spec/output-schemas/clipper-revoke.schema.json +28 -0
  26. package/spec/output-schemas/mcp-capture-result.schema.json +12 -1
  27. package/src/core/browser-clip-provenance.ts +139 -0
  28. package/src/core/browser-clip.ts +473 -0
  29. package/src/core/capture-write.ts +5 -0
  30. package/src/core/capture.ts +75 -18
  31. package/src/core/file-lock.ts +20 -6
  32. package/src/serve/capture-service.ts +420 -0
  33. package/src/serve/clipper-body.ts +62 -0
  34. package/src/serve/clipper-capture.ts +248 -0
  35. package/src/serve/clipper-contract.ts +57 -0
  36. package/src/serve/clipper-idempotency.ts +35 -0
  37. package/src/serve/clipper-pairing.ts +297 -0
  38. package/src/serve/clipper-security-errors.ts +23 -0
  39. package/src/serve/clipper-security.ts +449 -0
  40. package/src/serve/public/app.tsx +8 -1
  41. package/src/serve/public/globals.built.css +1 -1
  42. package/src/serve/public/index.html +1 -0
  43. package/src/serve/public/lib/clipper-approval.ts +206 -0
  44. package/src/serve/public/pages/ClipperPairing.tsx +210 -0
  45. package/src/serve/routes/api.ts +19 -115
  46. package/src/serve/routes/clipper.ts +394 -0
  47. package/src/serve/server.ts +22 -0
  48. package/src/store/migrations/020-browser-clipper-security.ts +128 -0
  49. package/src/store/migrations/index.ts +2 -0
  50. package/src/store/sqlite/clipper-store-types.ts +104 -0
  51. package/src/store/sqlite/clipper-store.ts +496 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.24.0",
3
+ "version": "1.25.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",
@@ -27,6 +27,8 @@
27
27
  },
28
28
  "files": [
29
29
  "assets",
30
+ "browser-extension/artifacts",
31
+ "browser-extension/dist",
30
32
  "bunfig.toml",
31
33
  "spec",
32
34
  "src",
@@ -53,6 +55,9 @@
53
55
  "scripts": {
54
56
  "dev": "bun run --hot src/index.ts",
55
57
  "build": "bun build src/index.ts",
58
+ "build:clipper": "bun browser-extension/build-cli.ts",
59
+ "package:clipper": "bun browser-extension/package-cli.ts",
60
+ "verify:clipper-package": "bun browser-extension/verify-package.ts",
56
61
  "start": "bun run src/index.ts",
57
62
  "lint": "oxlint --fix --type-aware --type-check && oxfmt .",
58
63
  "lint:check": "oxlint --type-aware --type-check && oxfmt --check .",
@@ -60,8 +65,10 @@
60
65
  "test": "bun test",
61
66
  "test:web": "bun test test/serve/public --timeout 30000",
62
67
  "test:e2e": "bun scripts/web-ui-smoke.ts",
68
+ "test:e2e:clipper": "bun test test/clipper/e2e.test.ts --timeout 240000",
63
69
  "test:e2e:install": "bunx playwright install chromium",
64
70
  "test:package": "bun scripts/package-smoke.ts",
71
+ "test:package:clipper": "bun scripts/package-smoke-clipper-cli.ts",
65
72
  "test:watch": "bun test --watch",
66
73
  "test:coverage": "bun test --coverage",
67
74
  "test:coverage:html": "bun test --coverage --html",
@@ -137,10 +144,11 @@
137
144
  "version:patch": "npm version patch --no-git-tag-version",
138
145
  "version:minor": "npm version minor --no-git-tag-version",
139
146
  "version:major": "npm version major --no-git-tag-version",
140
- "prerelease": "bun run lint:check && bun test && bun run docs:verify && bun run test:package",
147
+ "prerelease": "bun run lint:check && bun test && bun run docs:verify && bun run verify:clipper-package && bun run test:package",
141
148
  "release:dry-run": "gh workflow run publish.yml -f publish=false",
142
149
  "release:trigger": "gh workflow run publish.yml -f publish=true",
143
- "prepare": "lefthook install"
150
+ "prepare": "lefthook install",
151
+ "prepack": "bun run package:clipper"
144
152
  },
145
153
  "dependencies": {
146
154
  "@codemirror/lang-markdown": "6.5.0",
@@ -199,6 +207,7 @@
199
207
  "@vscode/tree-sitter-wasm": "0.3.1",
200
208
  "ajv": "8.17.1",
201
209
  "ajv-formats": "3.0.1",
210
+ "bun": "1.3.14",
202
211
  "docx": "9.5.1",
203
212
  "evalite": "1.0.0-beta.16",
204
213
  "exceljs": "4.4.0",
@@ -221,6 +230,7 @@
221
230
  "bun": ">=1.3.0"
222
231
  },
223
232
  "trustedDependencies": [
233
+ "bun",
224
234
  "node-llama-cpp"
225
235
  ]
226
236
  }
package/spec/cli.md CHANGED
@@ -1167,6 +1167,24 @@ Capture writes structured `source:` frontmatter and returns the shared
1167
1167
  [`capture-receipt`](./output-schemas/capture-receipt.schema.json). `--source-date`
1168
1168
  maps to `source.observedAt`; `--source-id` maps to `source.externalId`.
1169
1169
 
1170
+ Browser-clip receipts MAY extend `source` with `canonicalUrl`, `site`,
1171
+ `publishedAt`, and the closed
1172
+ [`browser-clip`](./output-schemas/browser-clip.schema.json) provenance object.
1173
+ The provenance preserves exact selection text separately from canonical final
1174
+ Markdown and carries extraction/final hashes, deterministic identity and
1175
+ preview digests, normalized dates/URLs, browser metadata, server capture time,
1176
+ and bounded warnings. Existing capture inputs and receipts remain compatible.
1177
+ Browser-clip URLs use a closed, credential-free HTTP(S) subset: ASCII DNS or
1178
+ punycode hosts (or IPv4), optional ports from 1 through 65535, and ASCII
1179
+ RFC 3986 path/query/fragment characters with well-formed percent escapes. Raw
1180
+ Unicode hosts and IPv6 literals are rejected; browser clients should submit
1181
+ their serialized IDNA/punycode URL. Free-form clip strings reject C0/C1 control
1182
+ characters except tab, LF, and CR.
1183
+ For browser provenance, `open_existing` MUST open only when the stored
1184
+ `clipIdentity` matches; missing or different provenance MUST return
1185
+ `collisionPolicyResult: "conflict"` without writing. `create_with_suffix`
1186
+ creates a distinct note.
1187
+
1170
1188
  **Path and Collision Rules:**
1171
1189
 
1172
1190
  - Explicit `--path` wins.
@@ -2952,6 +2970,38 @@ is blocked.
2952
2970
  - Mounts stateful Streamable HTTP MCP at `/mcp` only after the fail-closed
2953
2971
  actual-peer, Host, Origin, bearer, body, rate, request, queue, and session
2954
2972
  boundary initializes
2973
+ - On the loopback `gno serve` listener only, mounts the browser-clipper pairing,
2974
+ preview, and capture routes. These require exact extension/same-origin
2975
+ policies and dedicated origin-bound capture grants; MCP bearer tokens,
2976
+ `GNO_API_TOKEN`, and `gateway.enableWrite` do not authorize them. Clipper
2977
+ routes are structurally absent from non-loopback listeners. Capture
2978
+ idempotency persists exact content-free plan hashes and reconciles an
2979
+ interrupted atomic write only at its original path; changed plans fail
2980
+ closed without a duplicate write.
2981
+ - Serves the standalone `/clipper/pair` approval page. Its exact pair-ID
2982
+ fragment is synchronously validated and scrubbed before normal workspace
2983
+ state initializes; the user enters the eight-digit code manually. The page
2984
+ obtains same-origin CSRF state, never receives the extension grant, and does
2985
+ not persist pairing material.
2986
+ - Browser-clipper HTTP write receipts add `schemaVersion: "1.0"` to the closed
2987
+ shared capture receipt. HTTP 409 can therefore be either a valid provenance
2988
+ conflict receipt or a closed clipper error; clients parse the body contract
2989
+ before classifying the outcome.
2990
+ - Browser clip response/status closure is exact: HTTP 200 only for
2991
+ `opened_existing`; HTTP 202 only for `created`, `created_with_suffix`, or
2992
+ `overwritten`; HTTP 409 for either a valid `conflict` receipt or the matching
2993
+ closed `clipper-error@1.0`. Unknown versions, fields, codes, non-JSON bodies,
2994
+ and status/body mismatches fail as invalid responses. `CLIPPER_OFFLINE`,
2995
+ `CLIPPER_INVALID_RESPONSE`, and `CLIPPER_CLIENT` are client-only
2996
+ classifications.
2997
+ - Browser clip provenance contains exactly `extractionHash`, `finalBodyHash`,
2998
+ `clipIdentity`, and `previewDigest`; it does not contain `sourceHash`.
2999
+ - The Chromium service worker stores the loopback origin, plaintext bounded
3000
+ grant, and at most one `{payload, previewDigest, idempotencyKey}` in protected
3001
+ local extension storage. Unfinished pairing state uses extension session
3002
+ storage. Page extraction and the approval page receive neither grant nor
3003
+ pending-write state. Recovery reuses the same logical write or fails closed
3004
+ without changing destination.
2955
3005
  - On `--detach`: forks a detached child with stdio redirected to `--log-file`, writes pid-file JSON (`{pid, port, cmd:"serve", version, started_at}`), prints `{pid, url}` on stdout, exits 0
2956
3006
  - On `--status`: output matches the [process-status schema](./output-schemas/process-status.schema.json). Liveness via `process.kill(pid, 0)`; stale pid-files (ESRCH) are reported as `running:false`. Live status best-effort reads the same redacted `resident-status@1.0` snapshot from the recorded listener.
2957
3007
  - On `--stop`: sends SIGTERM, polls every 100ms for up to 10s, falls back to SIGKILL, polls 2s more, unlinks pid-file if the process cleaned up after itself
@@ -610,3 +610,104 @@ CREATE TABLE IF NOT EXISTS saved_capsule_reverification_state (
610
610
  registration_epoch INTEGER NOT NULL DEFAULT 0
611
611
  CHECK (registration_epoch >= 0)
612
612
  );
613
+
614
+ -- ─────────────────────────────────────────────────────────────────────────────
615
+ -- Browser clipper security state
616
+ -- ─────────────────────────────────────────────────────────────────────────────
617
+
618
+ CREATE TABLE IF NOT EXISTS clipper_grants (
619
+ id TEXT PRIMARY KEY,
620
+ token_hash TEXT NOT NULL UNIQUE,
621
+ origin TEXT NOT NULL,
622
+ scope TEXT NOT NULL DEFAULT 'capture'
623
+ CHECK (scope = 'capture'),
624
+ created_at_ms INTEGER NOT NULL CHECK (created_at_ms >= 0),
625
+ expires_at_ms INTEGER NOT NULL CHECK (expires_at_ms > created_at_ms),
626
+ revoked_at_ms INTEGER
627
+ CHECK (revoked_at_ms IS NULL OR revoked_at_ms >= created_at_ms),
628
+ CHECK (length(id) BETWEEN 1 AND 128),
629
+ CHECK (
630
+ length(token_hash) = 64
631
+ AND token_hash NOT GLOB '*[^0-9a-f]*'
632
+ ),
633
+ CHECK (
634
+ length(origin) = 51
635
+ AND substr(origin, 1, 19) = 'chrome-extension://'
636
+ AND substr(origin, 20) NOT GLOB '*[^a-p]*'
637
+ )
638
+ );
639
+
640
+ CREATE INDEX IF NOT EXISTS idx_clipper_grants_expiry
641
+ ON clipper_grants(expires_at_ms, id);
642
+
643
+ CREATE TABLE IF NOT EXISTS clipper_capture_idempotency (
644
+ grant_id TEXT NOT NULL,
645
+ key_hash TEXT NOT NULL,
646
+ request_digest TEXT NOT NULL,
647
+ collection TEXT NOT NULL,
648
+ rel_path TEXT NOT NULL,
649
+ collision_policy_result TEXT NOT NULL
650
+ CHECK (
651
+ collision_policy_result IN (
652
+ 'created',
653
+ 'opened_existing',
654
+ 'created_with_suffix',
655
+ 'overwritten',
656
+ 'conflict'
657
+ )
658
+ ),
659
+ content_hash TEXT NOT NULL,
660
+ clip_identity TEXT NOT NULL,
661
+ state TEXT NOT NULL CHECK (state IN ('pending', 'completed')),
662
+ status_code INTEGER,
663
+ response_json TEXT,
664
+ created_at_ms INTEGER NOT NULL CHECK (created_at_ms >= 0),
665
+ completed_at_ms INTEGER
666
+ CHECK (completed_at_ms IS NULL OR completed_at_ms >= created_at_ms),
667
+ PRIMARY KEY (grant_id, key_hash),
668
+ FOREIGN KEY (grant_id)
669
+ REFERENCES clipper_grants(id)
670
+ ON DELETE CASCADE,
671
+ CHECK (
672
+ length(key_hash) = 64
673
+ AND key_hash NOT GLOB '*[^0-9a-f]*'
674
+ ),
675
+ UNIQUE (grant_id, request_digest),
676
+ CHECK (
677
+ length(request_digest) = 64
678
+ AND request_digest NOT GLOB '*[^0-9a-f]*'
679
+ ),
680
+ CHECK (length(CAST(collection AS BLOB)) BETWEEN 1 AND 256),
681
+ CHECK (length(CAST(rel_path AS BLOB)) BETWEEN 1 AND 8192),
682
+ CHECK (
683
+ length(content_hash) = 64
684
+ AND content_hash NOT GLOB '*[^0-9a-f]*'
685
+ ),
686
+ CHECK (
687
+ length(clip_identity) = 64
688
+ AND clip_identity NOT GLOB '*[^0-9a-f]*'
689
+ ),
690
+ CHECK (
691
+ response_json IS NULL
692
+ OR length(CAST(response_json AS BLOB)) <= 2097152
693
+ ),
694
+ CHECK (
695
+ (
696
+ state = 'pending'
697
+ AND status_code IS NULL
698
+ AND response_json IS NULL
699
+ AND completed_at_ms IS NULL
700
+ )
701
+ OR
702
+ (
703
+ state = 'completed'
704
+ AND status_code BETWEEN 200 AND 599
705
+ AND response_json IS NOT NULL
706
+ AND json_valid(response_json)
707
+ AND completed_at_ms IS NOT NULL
708
+ )
709
+ )
710
+ );
711
+
712
+ CREATE INDEX IF NOT EXISTS idx_clipper_idempotency_created
713
+ ON clipper_capture_idempotency(created_at_ms, grant_id, key_hash);
package/spec/mcp.md CHANGED
@@ -1101,6 +1101,16 @@ Create a new document in a collection (write-enabled).
1101
1101
  - Capture writes structured `source:` frontmatter with `kind`, `capturedAt`,
1102
1102
  and optional `url`, `uri`, `docid`, `mime`, `ext`, `author`, `observedAt`,
1103
1103
  `externalId`, and `title`
1104
+ - Browser-clip receipts MAY additionally carry normalized `canonicalUrl`,
1105
+ `site`, `publishedAt`, and closed `browserClip` provenance as defined by
1106
+ `browser-clip.schema.json`; existing `gno_capture` inputs remain compatible
1107
+ - Browser provenance preserves exact selection separately from canonical final
1108
+ Markdown and includes extraction/final hashes, deterministic clip/preview
1109
+ digests, normalized dates/URLs, browser metadata, server capture time, and
1110
+ bounded warnings
1111
+ - Browser `open_existing` MUST require a matching stored `clipIdentity`;
1112
+ missing or different provenance returns `collisionPolicyResult: "conflict"`
1113
+ without writing, while `create_with_suffix` creates a distinct note
1104
1114
  - Tags are validated and normalized to lowercase
1105
1115
  - For Markdown files, tags are added to frontmatter
1106
1116
  - For non-Markdown files, tags are stored as user-source in the database
@@ -0,0 +1,83 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "gno://schemas/browser-clip-preview@1.0",
4
+ "title": "Browser Clip Preview",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "preview", "provenance", "plan"],
8
+ "properties": {
9
+ "schemaVersion": { "const": "1.0" },
10
+ "preview": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["body", "digest", "source", "destination", "tags"],
14
+ "properties": {
15
+ "body": { "type": "string" },
16
+ "digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
17
+ "source": { "$ref": "#/definitions/source" },
18
+ "destination": {
19
+ "$ref": "gno://schemas/browser-clip@1.0#/properties/destination"
20
+ },
21
+ "tags": {
22
+ "type": "array",
23
+ "items": { "type": "string" }
24
+ }
25
+ }
26
+ },
27
+ "provenance": {
28
+ "$ref": "gno://schemas/browser-clip@1.0#/definitions/provenance"
29
+ },
30
+ "plan": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "required": ["collection", "relPath", "outcome", "provenanceConflict"],
34
+ "properties": {
35
+ "collection": { "type": "string", "minLength": 1 },
36
+ "relPath": { "type": "string", "minLength": 1 },
37
+ "outcome": {
38
+ "enum": [
39
+ "created",
40
+ "opened_existing",
41
+ "created_with_suffix",
42
+ "overwritten",
43
+ "conflict"
44
+ ]
45
+ },
46
+ "provenanceConflict": { "type": "boolean" }
47
+ }
48
+ }
49
+ },
50
+ "definitions": {
51
+ "source": {
52
+ "type": "object",
53
+ "additionalProperties": false,
54
+ "required": [
55
+ "kind",
56
+ "title",
57
+ "url",
58
+ "observedAt",
59
+ "capturedAt",
60
+ "browserClip"
61
+ ],
62
+ "properties": {
63
+ "kind": { "const": "web" },
64
+ "title": { "type": "string" },
65
+ "url": { "type": "string", "format": "uri" },
66
+ "author": { "type": "string" },
67
+ "observedAt": { "type": "string", "format": "date-time" },
68
+ "capturedAt": { "type": "string", "format": "date-time" },
69
+ "canonicalUrl": { "type": "string", "format": "uri" },
70
+ "site": { "type": "string" },
71
+ "publishedAt": {
72
+ "oneOf": [
73
+ { "type": "string", "format": "date" },
74
+ { "type": "string", "format": "date-time" }
75
+ ]
76
+ },
77
+ "browserClip": {
78
+ "$ref": "gno://schemas/browser-clip@1.0#/definitions/provenance"
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }