@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.
- package/README.md +9 -1
- package/assets/skill/SKILL.md +11 -0
- package/assets/skill/recipes/capture-and-file.md +20 -5
- package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +1 -0
- package/browser-extension/dist/PRIVACY.md +55 -0
- package/browser-extension/dist/chunk-vn5f663b.js +50 -0
- package/browser-extension/dist/chunk-ydfx5d7p.css +1 -0
- package/browser-extension/dist/content.js +1 -0
- package/browser-extension/dist/manifest.json +25 -0
- package/browser-extension/dist/preview.html +13 -0
- package/browser-extension/dist/service-worker.js +40 -0
- package/package.json +13 -3
- package/spec/cli.md +50 -0
- package/spec/db/schema.sql +101 -0
- package/spec/mcp.md +10 -0
- package/spec/output-schemas/browser-clip-preview.schema.json +83 -0
- package/spec/output-schemas/browser-clip.schema.json +586 -0
- package/spec/output-schemas/capture-receipt.schema.json +22 -1
- package/spec/output-schemas/clipper-csrf.schema.json +12 -0
- package/spec/output-schemas/clipper-error.schema.json +46 -0
- package/spec/output-schemas/clipper-pair-approval.schema.json +17 -0
- package/spec/output-schemas/clipper-pair-start.schema.json +26 -0
- package/spec/output-schemas/clipper-pair-status.schema.json +46 -0
- package/spec/output-schemas/clipper-revoke.schema.json +28 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +12 -1
- package/src/core/browser-clip-provenance.ts +139 -0
- package/src/core/browser-clip.ts +473 -0
- package/src/core/capture-write.ts +5 -0
- package/src/core/capture.ts +75 -18
- package/src/core/file-lock.ts +20 -6
- package/src/serve/capture-service.ts +420 -0
- package/src/serve/clipper-body.ts +62 -0
- package/src/serve/clipper-capture.ts +248 -0
- package/src/serve/clipper-contract.ts +57 -0
- package/src/serve/clipper-idempotency.ts +35 -0
- package/src/serve/clipper-pairing.ts +297 -0
- package/src/serve/clipper-security-errors.ts +23 -0
- package/src/serve/clipper-security.ts +449 -0
- package/src/serve/public/app.tsx +8 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/index.html +1 -0
- package/src/serve/public/lib/clipper-approval.ts +206 -0
- package/src/serve/public/pages/ClipperPairing.tsx +210 -0
- package/src/serve/routes/api.ts +19 -115
- package/src/serve/routes/clipper.ts +394 -0
- package/src/serve/server.ts +22 -0
- package/src/store/migrations/020-browser-clipper-security.ts +128 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/clipper-store-types.ts +104 -0
- 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.
|
|
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
|
package/spec/db/schema.sql
CHANGED
|
@@ -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
|
+
}
|