@gmickel/gno 1.32.0 → 1.33.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/README.md +11 -2
- package/assets/skill/SKILL.md +11 -0
- package/assets/skill/cli-reference.md +10 -2
- package/browser-extension/artifacts/{gno-browser-clipper-v1.32.0.zip → gno-browser-clipper-v1.33.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.33.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +5 -1
- package/spec/cli.md +16 -1
- package/spec/output-schemas/publish-artifact.schema.json +76 -1
- package/src/cli/commands/publish.ts +43 -7
- package/src/ingestion/strip.ts +152 -26
- package/src/publish/artifact-asset-codec.ts +75 -0
- package/src/publish/artifact-asset-contract.ts +152 -0
- package/src/publish/artifact-asset-parse.ts +401 -0
- package/src/publish/artifact-asset-sniff.ts +108 -0
- package/src/publish/artifact-asset-validate.ts +209 -0
- package/src/publish/artifact-assets.ts +58 -0
- package/src/publish/artifact-validation.ts +32 -6
- package/src/publish/artifact.ts +50 -3
- package/src/publish/attachment-bundle.ts +145 -0
- package/src/publish/attachment-discover.ts +203 -0
- package/src/publish/attachment-load.ts +133 -0
- package/src/publish/attachment-obsidian.ts +45 -0
- package/src/publish/attachment-path.ts +334 -0
- package/src/publish/attachment-raster.ts +852 -0
- package/src/publish/attachment-resolver.ts +280 -0
- package/src/publish/attachment-types.ts +54 -0
- package/src/publish/encrypted-export.ts +121 -44
- package/src/publish/export-attachments.ts +224 -0
- package/src/publish/export-service.ts +142 -80
- package/src/publish/obsidian-sanitize.ts +121 -13
- package/src/serve/routes/api.ts +2 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +0 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ gno daemon --detach # headless indexing + resident MCP gateway
|
|
|
117
117
|
|
|
118
118
|
<!-- public-truth:current-version -->
|
|
119
119
|
|
|
120
|
-
> Current release: **v1.
|
|
120
|
+
> Current release: **v1.33.0** — see [CHANGELOG.md](./CHANGELOG.md)
|
|
121
121
|
|
|
122
122
|
<!-- /public-truth -->
|
|
123
123
|
|
|
@@ -773,7 +773,7 @@ GNO is local-first, but sometimes you want a URL to send someone. [**gno.sh**](h
|
|
|
773
773
|
|
|
774
774
|

|
|
775
775
|
|
|
776
|
-
The workflow is deliberately explicit: **export locally → upload artifact → share URL**. Private and `publish: false` notes stay on your machine. Exported artifacts omit local collection paths and source URIs.
|
|
776
|
+
The workflow is deliberately explicit: **export locally → upload artifact → share URL**. Private and `publish: false` notes stay on your machine. Exported artifacts omit local collection paths and source URIs. Local PNG/JPEG/GIF/WebP/AVIF images (Markdown and Obsidian embeds) are bundled into the artifact with SHA-256 dedup, `gno-asset:` sentinels, and an exact 100 MiB serialized ceiling; external HTTPS images stay as URLs. Encrypted shares keep image bytes inside ciphertext only. Invite-only note text publishes today, but invite-only image delivery on gno.sh is not supported yet.
|
|
777
777
|
|
|
778
778
|
```bash
|
|
779
779
|
# Export a single note
|
|
@@ -817,6 +817,15 @@ agent capabilities or manifests. Encrypted exports remain ciphertext-only.
|
|
|
817
817
|
Reader metadata drops embedded local path or GNO/file URI tokens; canonical
|
|
818
818
|
and image fields accept only uncredentialed public HTTP(S) targets.
|
|
819
819
|
|
|
820
|
+
Resolved local PNG, JPEG, GIF, WebP, and AVIF references are bundled,
|
|
821
|
+
content-addressed, and deduplicated; external public HTTPS images remain
|
|
822
|
+
external. The exact serialized artifact is capped at 100 MiB. Public images
|
|
823
|
+
use immutable snapshot/generation URLs, secret-link images are authorized on
|
|
824
|
+
every request, and encrypted image bytes exist only inside ciphertext before
|
|
825
|
+
the browser creates scoped Blob URLs. Invite-only bundled-image delivery is
|
|
826
|
+
currently fail-closed; use an asset-free invite, secret link, or encrypted
|
|
827
|
+
share when local images are required.
|
|
828
|
+
|
|
820
829
|
Republishing a public, secret-link, or invite-only artifact updates the same URL. Encrypted shares should be replaced from a fresh local export so the server never needs your plaintext.
|
|
821
830
|
|
|
822
831
|
Encrypted source-backed publish on `gno.sh` is intentionally disabled. For encrypted shares, use:
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -92,6 +92,17 @@ Recipe rules:
|
|
|
92
92
|
| **Skill** | `skill install/uninstall/show/paths` | Install skill for AI agents |
|
|
93
93
|
| **Admin** | `status`, `doctor`, `cleanup`, `reset`, `vec`, `completion` | Maintenance and diagnostics |
|
|
94
94
|
|
|
95
|
+
### Publishing with local images
|
|
96
|
+
|
|
97
|
+
`gno publish export` bundles resolved local PNG, JPEG, GIF, WebP, and AVIF
|
|
98
|
+
references, deduplicates identical bytes, preserves public HTTPS images, and
|
|
99
|
+
enforces the 100 MiB exact serialized artifact limit. Review `assetSummary`
|
|
100
|
+
in `--json` output for unresolved or unsupported references. Public and
|
|
101
|
+
secret-link readers serve authorized hosted URLs; encrypted exports keep image
|
|
102
|
+
bytes inside ciphertext and create scoped Blob URLs only after browser
|
|
103
|
+
decryption. Hosted invite-only bundled-image delivery is currently
|
|
104
|
+
fail-closed, so use an asset-free invite, secret link, or encrypted share.
|
|
105
|
+
|
|
95
106
|
## Search Modes
|
|
96
107
|
|
|
97
108
|
| Command | Speed | Best For |
|
|
@@ -887,11 +887,19 @@ export pipeline runs a sanitizer over each note's markdown. It:
|
|
|
887
887
|
`_internal/` convention is treated as never-publish)
|
|
888
888
|
- converts `[[Target|Alias]]` to the alias text, and `[[Target]]` to the
|
|
889
889
|
tail segment of the target
|
|
890
|
-
-
|
|
891
|
-
|
|
890
|
+
- resolves local `![[image.png]]` / Markdown image refs for PNG/JPEG/GIF/WebP/
|
|
891
|
+
AVIF (AVIF must be AV1-decodable at producer ingress) into content-addressed
|
|
892
|
+
`gno-asset:<sha256>` sentinels (deduped; exact 100 MiB serialized ceiling;
|
|
893
|
+
ciphertext field bounds align to that envelope); preserves external HTTPS
|
|
894
|
+
images; skips fenced code (backtick and tilde); drops unsupported/missing/
|
|
895
|
+
unsafe refs with diagnostics
|
|
892
896
|
- refuses to export a note whose frontmatter contains `publish: false`
|
|
893
897
|
(single-note export errors; collection export silently skips)
|
|
894
898
|
|
|
899
|
+
Successful exports include `assetSummary` in `--json`. Encrypted visibility
|
|
900
|
+
keeps raster bytes inside ciphertext only. Hosted invite-only image delivery
|
|
901
|
+
is currently fail-closed on gno.sh.
|
|
902
|
+
|
|
895
903
|
Every sanitizer decision surfaces in the CLI output as a "Preprocessor
|
|
896
904
|
notes" section, on the `--json` response under `warnings`, and on
|
|
897
905
|
`--preview` as a structured report — so nothing is silently lost.
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c398c09680951afa0b5d436f3b92cf3dfb6e49427b9df83830ce899d3b03d7e4 gno-browser-clipper-v1.33.0.zip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
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",
|
|
@@ -178,6 +178,9 @@
|
|
|
178
178
|
"jsonc-parser": "3.3.1",
|
|
179
179
|
"lucide-react": "1.28.0",
|
|
180
180
|
"markitdown-ts": "0.0.10",
|
|
181
|
+
"mdast-util-from-markdown": "2.0.3",
|
|
182
|
+
"mdast-util-gfm": "3.1.0",
|
|
183
|
+
"micromark-extension-gfm": "3.0.0",
|
|
181
184
|
"minimatch": "10.2.6",
|
|
182
185
|
"nanoid": "6.0.0",
|
|
183
186
|
"node-llama-cpp": "3.19.1",
|
|
@@ -190,6 +193,7 @@
|
|
|
190
193
|
"react-markdown": "10.1.0",
|
|
191
194
|
"rehype-sanitize": "6.0.0",
|
|
192
195
|
"remark-gfm": "4.0.1",
|
|
196
|
+
"sharp": "0.35.3",
|
|
193
197
|
"shiki": "4.3.1",
|
|
194
198
|
"sqlite-vec": "0.1.9",
|
|
195
199
|
"streamdown": "2.5.0",
|
package/spec/cli.md
CHANGED
|
@@ -1949,6 +1949,19 @@ tokens MUST be filtered. Canonical and image metadata MUST contain
|
|
|
1949
1949
|
uncredentialed public HTTP(S) targets; local hostnames and literal loopback,
|
|
1950
1950
|
private, or link-local addresses MUST be filtered.
|
|
1951
1951
|
|
|
1952
|
+
When local raster attachments are bundled, the V1 artifact MAY include
|
|
1953
|
+
`assets[]` and `requiredCapabilities: ["bundled-raster-assets@1"]`. Export
|
|
1954
|
+
success payloads (CLI `--json` and `POST /api/publish/export`) MUST include a
|
|
1955
|
+
deterministic `assetSummary` with asset/ref counts, raw/encoded/final UTF-8
|
|
1956
|
+
upload bytes, dedup savings, external image count, and unresolved/unsupported
|
|
1957
|
+
diagnostics. Exact final serialized upload bytes MUST be enforced against the
|
|
1958
|
+
100 MiB ceiling before returning. Encrypted V2 ciphertext string bounds MUST
|
|
1959
|
+
align to that same 100 MiB final-envelope budget and MUST NOT replace exact
|
|
1960
|
+
final serialized measurement as the authoritative upload gate. Producer
|
|
1961
|
+
ingress MUST require AV1-decodable AVIF payloads (structural BMFF alone is
|
|
1962
|
+
insufficient). Asset-free exports omit `assets` /
|
|
1963
|
+
`requiredCapabilities` and report zero asset counts.
|
|
1964
|
+
|
|
1952
1965
|
Secret-link and invite-only V1 spaces MUST NOT contain a manifest or agent
|
|
1953
1966
|
capability field. Encrypted V2 spaces MUST contain only ciphertext parameters,
|
|
1954
1967
|
the opaque secret token, route slug, source type, and encrypted visibility; no
|
|
@@ -1956,7 +1969,9 @@ plaintext manifest or evidence may appear outside the ciphertext. V2 builders
|
|
|
1956
1969
|
MUST emit a closed projection, validate payload strings as non-empty bounded
|
|
1957
1970
|
base64, require a positive safe-integer KDF iteration count, and bound the
|
|
1958
1971
|
non-blank opaque token. Caller-supplied extension fields MUST NOT enter the
|
|
1959
|
-
artifact.
|
|
1972
|
+
artifact. Encrypted export MUST place bundled assets only inside the encrypted
|
|
1973
|
+
client payload. The outer V2 envelope MUST contain neither plaintext note
|
|
1974
|
+
content, asset descriptors, source references, nor raster bytes.
|
|
1960
1975
|
|
|
1961
1976
|
---
|
|
1962
1977
|
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"egressLineage": { "$ref": "#/definitions/egressLineage" },
|
|
17
17
|
"source": { "$ref": "#/definitions/slug" },
|
|
18
18
|
"exportedAt": { "type": "string", "format": "date-time" },
|
|
19
|
+
"requiredCapabilities": {
|
|
20
|
+
"$ref": "#/definitions/requiredCapabilities"
|
|
21
|
+
},
|
|
22
|
+
"assets": { "$ref": "#/definitions/assets" },
|
|
19
23
|
"spaces": {
|
|
20
24
|
"type": "array",
|
|
21
25
|
"minItems": 1,
|
|
@@ -37,6 +41,9 @@
|
|
|
37
41
|
"egressLineage": { "$ref": "#/definitions/egressLineage" },
|
|
38
42
|
"source": { "$ref": "#/definitions/slug" },
|
|
39
43
|
"exportedAt": { "type": "string", "format": "date-time" },
|
|
44
|
+
"requiredCapabilities": {
|
|
45
|
+
"$ref": "#/definitions/requiredCapabilities"
|
|
46
|
+
},
|
|
40
47
|
"spaces": {
|
|
41
48
|
"type": "array",
|
|
42
49
|
"minItems": 1,
|
|
@@ -44,6 +51,74 @@
|
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
},
|
|
54
|
+
"requiredCapabilities": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"uniqueItems": true,
|
|
57
|
+
"items": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"minLength": 1,
|
|
60
|
+
"maxLength": 128
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"assets": {
|
|
64
|
+
"type": "array",
|
|
65
|
+
"items": { "$ref": "#/definitions/artifactAsset" }
|
|
66
|
+
},
|
|
67
|
+
"artifactAsset": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"required": [
|
|
71
|
+
"id",
|
|
72
|
+
"mediaType",
|
|
73
|
+
"byteLength",
|
|
74
|
+
"sha256",
|
|
75
|
+
"width",
|
|
76
|
+
"height",
|
|
77
|
+
"encoding",
|
|
78
|
+
"data",
|
|
79
|
+
"references"
|
|
80
|
+
],
|
|
81
|
+
"properties": {
|
|
82
|
+
"id": { "$ref": "#/definitions/hash" },
|
|
83
|
+
"mediaType": {
|
|
84
|
+
"enum": [
|
|
85
|
+
"image/png",
|
|
86
|
+
"image/jpeg",
|
|
87
|
+
"image/gif",
|
|
88
|
+
"image/webp",
|
|
89
|
+
"image/avif"
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
"byteLength": { "type": "integer", "minimum": 1, "maximum": 104857600 },
|
|
93
|
+
"sha256": { "$ref": "#/definitions/hash" },
|
|
94
|
+
"width": { "type": "integer", "minimum": 1, "maximum": 16384 },
|
|
95
|
+
"height": { "type": "integer", "minimum": 1, "maximum": 16384 },
|
|
96
|
+
"encoding": { "const": "base64" },
|
|
97
|
+
"data": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"minLength": 1,
|
|
100
|
+
"maxLength": 139810134,
|
|
101
|
+
"pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
|
|
102
|
+
},
|
|
103
|
+
"references": {
|
|
104
|
+
"type": "array",
|
|
105
|
+
"minItems": 1,
|
|
106
|
+
"items": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"required": ["noteSlug", "sourceRef"],
|
|
110
|
+
"properties": {
|
|
111
|
+
"noteSlug": { "$ref": "#/definitions/slug" },
|
|
112
|
+
"sourceRef": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"minLength": 1,
|
|
115
|
+
"maxLength": 1024
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
47
122
|
"slug": {
|
|
48
123
|
"type": "string",
|
|
49
124
|
"pattern": "^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$",
|
|
@@ -280,7 +355,7 @@
|
|
|
280
355
|
"ciphertext": {
|
|
281
356
|
"type": "string",
|
|
282
357
|
"minLength": 1,
|
|
283
|
-
"maxLength":
|
|
358
|
+
"maxLength": 104857600,
|
|
284
359
|
"pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
|
|
285
360
|
},
|
|
286
361
|
"iterations": {
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @module src/cli/commands/publish
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// node:fs/promises — directory creation has no Bun-native equivalent.
|
|
8
|
+
import { mkdir } from "node:fs/promises";
|
|
8
9
|
import { dirname } from "node:path";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
|
|
@@ -12,10 +13,15 @@ import type {
|
|
|
12
13
|
PublishArtifact,
|
|
13
14
|
PublishVisibility,
|
|
14
15
|
} from "../../publish/artifact";
|
|
16
|
+
import type { PublishAssetEgressSummary } from "../../publish/attachment-resolver";
|
|
15
17
|
import type { SanitizeWarning } from "../../publish/obsidian-sanitize";
|
|
16
18
|
|
|
17
19
|
import { resolveDownloadsDir } from "../../core/user-dirs";
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
derivePublishArtifactFilename,
|
|
22
|
+
serializePublishArtifact,
|
|
23
|
+
slugify,
|
|
24
|
+
} from "../../publish/artifact";
|
|
19
25
|
import { exportPublishArtifact } from "../../publish/export-service";
|
|
20
26
|
import { formatSanitizeWarnings } from "../../publish/obsidian-sanitize";
|
|
21
27
|
import { initStore } from "./shared";
|
|
@@ -37,6 +43,7 @@ export type PublishExportResult =
|
|
|
37
43
|
success: true;
|
|
38
44
|
data: {
|
|
39
45
|
artifact: PublishArtifact;
|
|
46
|
+
assetSummary: PublishAssetEgressSummary;
|
|
40
47
|
outPath: string;
|
|
41
48
|
preview?: string;
|
|
42
49
|
uploadUrl: string;
|
|
@@ -64,6 +71,15 @@ export async function buildDefaultPublishExportPath(
|
|
|
64
71
|
);
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
/** Write exactly the canonical byte sequence used by upload-size accounting. */
|
|
75
|
+
export async function writePublishArtifactFile(
|
|
76
|
+
outPath: string,
|
|
77
|
+
artifact: PublishArtifact
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
80
|
+
await Bun.write(outPath, serializePublishArtifact(artifact));
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
export async function publishExport(
|
|
68
84
|
target: string,
|
|
69
85
|
options: PublishExportOptions
|
|
@@ -79,7 +95,7 @@ export async function publishExport(
|
|
|
79
95
|
const { collections, store } = initResult;
|
|
80
96
|
|
|
81
97
|
try {
|
|
82
|
-
const { artifact, warnings } = await exportPublishArtifact({
|
|
98
|
+
const { artifact, assetSummary, warnings } = await exportPublishArtifact({
|
|
83
99
|
collections,
|
|
84
100
|
options: {
|
|
85
101
|
routeSlug: options.slug,
|
|
@@ -104,6 +120,7 @@ export async function publishExport(
|
|
|
104
120
|
success: true,
|
|
105
121
|
data: {
|
|
106
122
|
artifact,
|
|
123
|
+
assetSummary,
|
|
107
124
|
outPath: "",
|
|
108
125
|
preview,
|
|
109
126
|
uploadUrl: "https://gno.sh/studio",
|
|
@@ -116,13 +133,13 @@ export async function publishExport(
|
|
|
116
133
|
const outPath =
|
|
117
134
|
options.out?.trim() || (await buildDefaultPublishExportPath(artifact));
|
|
118
135
|
|
|
119
|
-
await
|
|
120
|
-
await writeFile(outPath, JSON.stringify(artifact, null, 2));
|
|
136
|
+
await writePublishArtifactFile(outPath, artifact);
|
|
121
137
|
|
|
122
138
|
return {
|
|
123
139
|
success: true,
|
|
124
140
|
data: {
|
|
125
141
|
artifact,
|
|
142
|
+
assetSummary,
|
|
126
143
|
outPath,
|
|
127
144
|
uploadUrl: "https://gno.sh/studio",
|
|
128
145
|
warnings,
|
|
@@ -159,19 +176,37 @@ export function formatPublishExport(
|
|
|
159
176
|
return JSON.stringify(result.data, null, 2);
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
const {
|
|
163
|
-
|
|
179
|
+
const {
|
|
180
|
+
artifact,
|
|
181
|
+
assetSummary,
|
|
182
|
+
outPath,
|
|
183
|
+
preview,
|
|
184
|
+
uploadUrl,
|
|
185
|
+
warningsDisplay,
|
|
186
|
+
} = result.data;
|
|
164
187
|
const space = artifact.spaces[0];
|
|
165
188
|
const warningsSection =
|
|
166
189
|
warningsDisplay.length > 0
|
|
167
190
|
? ["", "Preprocessor notes:", ...warningsDisplay]
|
|
168
191
|
: [];
|
|
192
|
+
const assetSection = [
|
|
193
|
+
"",
|
|
194
|
+
"Asset summary:",
|
|
195
|
+
` assets=${assetSummary.assetCount} refs=${assetSummary.referenceCount} external=${assetSummary.externalCount}`,
|
|
196
|
+
` rawBytes=${assetSummary.rawBytes} encodedBytes=${assetSummary.encodedBytes} finalBytes=${assetSummary.finalUploadBytes}`,
|
|
197
|
+
` dedupSavedBytes=${assetSummary.dedupSavedBytes} unresolved=${assetSummary.diagnostics.length}`,
|
|
198
|
+
...assetSummary.diagnostics.map(
|
|
199
|
+
(diagnostic) =>
|
|
200
|
+
` [${diagnostic.code}] ${diagnostic.noteSlug}: ${diagnostic.sourceRef} — ${diagnostic.message}`
|
|
201
|
+
),
|
|
202
|
+
];
|
|
169
203
|
|
|
170
204
|
if (preview !== undefined) {
|
|
171
205
|
return [
|
|
172
206
|
`Preview (no file written) — ${space?.sourceType ?? "artifact"}`,
|
|
173
207
|
`Route slug: ${space?.routeSlug ?? slugify(artifact.source)}`,
|
|
174
208
|
`Visibility: ${space?.visibility ?? "public"}`,
|
|
209
|
+
...assetSection,
|
|
175
210
|
...warningsSection,
|
|
176
211
|
"",
|
|
177
212
|
"─── sanitized markdown ───",
|
|
@@ -185,6 +220,7 @@ export function formatPublishExport(
|
|
|
185
220
|
`Visibility: ${space?.visibility ?? "public"}`,
|
|
186
221
|
`Filename: ${derivePublishArtifactFilename(artifact)}`,
|
|
187
222
|
`Next: open ${uploadUrl} and drop ${outPath} into the upload zone.`,
|
|
223
|
+
...assetSection,
|
|
188
224
|
...warningsSection,
|
|
189
225
|
].join("\n");
|
|
190
226
|
}
|
package/src/ingestion/strip.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Identifies regions to exclude from link/tag extraction:
|
|
5
5
|
* - YAML frontmatter
|
|
6
|
-
* - Fenced code blocks
|
|
6
|
+
* - Fenced code blocks (CommonMark backtick and tilde fences)
|
|
7
7
|
* - Inline code
|
|
8
8
|
* - HTML comments
|
|
9
9
|
*
|
|
@@ -39,15 +39,153 @@ export interface ExcludedRange {
|
|
|
39
39
|
/** Frontmatter at start of file (YAML between --- delimiters) */
|
|
40
40
|
const FRONTMATTER_REGEX = /^---\r?\n[\s\S]*?(?:\r?\n)?---(?:\r?\n|$)/;
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
const
|
|
42
|
+
/** CommonMark fence opener: 0–3 spaces, then 3+ backticks or tildes + info. */
|
|
43
|
+
const FENCE_OPEN_REGEX = /^ {0,3}(`{3,}|~{3,})(.*)$/u;
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
const
|
|
45
|
+
/** CommonMark fence closer: matching character, length ≥ opener, trailing space/tabs only. */
|
|
46
|
+
const FENCE_CLOSE_REGEX = /^ {0,3}(`{3,}|~{3,})[\t ]*$/u;
|
|
47
47
|
|
|
48
48
|
/** HTML comments */
|
|
49
49
|
const HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
|
|
50
50
|
|
|
51
|
+
interface OpenFence {
|
|
52
|
+
marker: "`" | "~";
|
|
53
|
+
length: number;
|
|
54
|
+
start: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collect CommonMark fenced code ranges (backtick and tilde). A closer must
|
|
59
|
+
* use the same character and be at least as long as the opener; when omitted,
|
|
60
|
+
* CommonMark extends the fenced block through end of input.
|
|
61
|
+
*/
|
|
62
|
+
const collectFencedCodeRanges = (markdown: string): ExcludedRange[] => {
|
|
63
|
+
const ranges: ExcludedRange[] = [];
|
|
64
|
+
let offset = 0;
|
|
65
|
+
let open: OpenFence | null = null;
|
|
66
|
+
|
|
67
|
+
while (offset <= markdown.length) {
|
|
68
|
+
const nextNl = markdown.indexOf("\n", offset);
|
|
69
|
+
const lineEnd = nextNl === -1 ? markdown.length : nextNl;
|
|
70
|
+
const rawLine = markdown.slice(offset, lineEnd);
|
|
71
|
+
const logical = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
72
|
+
|
|
73
|
+
if (open) {
|
|
74
|
+
const closeRun = FENCE_CLOSE_REGEX.exec(logical)?.[1];
|
|
75
|
+
if (
|
|
76
|
+
closeRun &&
|
|
77
|
+
closeRun[0] === open.marker &&
|
|
78
|
+
closeRun.length >= open.length
|
|
79
|
+
) {
|
|
80
|
+
const end = nextNl === -1 ? markdown.length : nextNl + 1;
|
|
81
|
+
ranges.push({ start: open.start, end, kind: "fenced_code" });
|
|
82
|
+
open = null;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const openMatch = FENCE_OPEN_REGEX.exec(logical);
|
|
86
|
+
const run = openMatch?.[1];
|
|
87
|
+
const suffix = openMatch?.[2] ?? "";
|
|
88
|
+
// Backtick info strings cannot contain backticks (CommonMark).
|
|
89
|
+
if (run && !(run[0] === "`" && suffix.includes("`"))) {
|
|
90
|
+
open = {
|
|
91
|
+
marker: run[0] as OpenFence["marker"],
|
|
92
|
+
length: run.length,
|
|
93
|
+
start: offset,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (nextNl === -1) break;
|
|
99
|
+
offset = nextNl + 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (open) {
|
|
103
|
+
ranges.push({
|
|
104
|
+
start: open.start,
|
|
105
|
+
end: markdown.length,
|
|
106
|
+
kind: "fenced_code",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return ranges;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
interface BacktickRun {
|
|
114
|
+
end: number;
|
|
115
|
+
length: number;
|
|
116
|
+
start: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** CommonMark code spans close only on a backtick run of equal length. */
|
|
120
|
+
const collectInlineCodeRanges = (
|
|
121
|
+
markdown: string,
|
|
122
|
+
excludedRanges: ExcludedRange[]
|
|
123
|
+
): ExcludedRange[] => {
|
|
124
|
+
const runs: BacktickRun[] = [];
|
|
125
|
+
let cursor = 0;
|
|
126
|
+
let excludedIndex = 0;
|
|
127
|
+
while (cursor < markdown.length) {
|
|
128
|
+
while (
|
|
129
|
+
excludedRanges[excludedIndex] &&
|
|
130
|
+
excludedRanges[excludedIndex]!.end <= cursor
|
|
131
|
+
) {
|
|
132
|
+
excludedIndex += 1;
|
|
133
|
+
}
|
|
134
|
+
const excluded = excludedRanges[excludedIndex];
|
|
135
|
+
if (excluded && cursor >= excluded.start && cursor < excluded.end) {
|
|
136
|
+
cursor = excluded.end;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (markdown[cursor] !== "`") {
|
|
140
|
+
cursor += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const start = cursor;
|
|
144
|
+
while (markdown[cursor] === "`") cursor += 1;
|
|
145
|
+
let backslashes = 0;
|
|
146
|
+
for (let i = start - 1; i >= 0 && markdown[i] === "\\"; i -= 1) {
|
|
147
|
+
backslashes += 1;
|
|
148
|
+
}
|
|
149
|
+
if (backslashes % 2 === 0) {
|
|
150
|
+
runs.push({ start, end: cursor, length: cursor - start });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const nextMatchingRun = Array.from<number | undefined>({
|
|
155
|
+
length: runs.length,
|
|
156
|
+
});
|
|
157
|
+
const latestByLength = new Map<number, number>();
|
|
158
|
+
for (let index = runs.length - 1; index >= 0; index -= 1) {
|
|
159
|
+
const run = runs[index];
|
|
160
|
+
if (!run) continue;
|
|
161
|
+
nextMatchingRun[index] = latestByLength.get(run.length);
|
|
162
|
+
latestByLength.set(run.length, index);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const ranges: ExcludedRange[] = [];
|
|
166
|
+
let index = 0;
|
|
167
|
+
while (index < runs.length) {
|
|
168
|
+
const closeIndex = nextMatchingRun[index];
|
|
169
|
+
const opener = runs[index];
|
|
170
|
+
if (closeIndex === undefined || !opener) {
|
|
171
|
+
index += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const closer = runs[closeIndex];
|
|
175
|
+
if (!closer) {
|
|
176
|
+
index += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
ranges.push({
|
|
180
|
+
start: opener.start,
|
|
181
|
+
end: closer.end,
|
|
182
|
+
kind: "inline_code",
|
|
183
|
+
});
|
|
184
|
+
index = closeIndex + 1;
|
|
185
|
+
}
|
|
186
|
+
return ranges;
|
|
187
|
+
};
|
|
188
|
+
|
|
51
189
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
190
|
// Main Functions
|
|
53
191
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -70,29 +208,12 @@ export function getExcludedRanges(markdown: string): ExcludedRange[] {
|
|
|
70
208
|
});
|
|
71
209
|
}
|
|
72
210
|
|
|
73
|
-
// 2. Fenced code blocks
|
|
74
|
-
|
|
75
|
-
let match: RegExpExecArray | null;
|
|
76
|
-
while ((match = FENCED_CODE_REGEX.exec(markdown)) !== null) {
|
|
77
|
-
ranges.push({
|
|
78
|
-
start: match.index,
|
|
79
|
-
end: match.index + match[0].length,
|
|
80
|
-
kind: "fenced_code",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 3. Inline code
|
|
85
|
-
INLINE_CODE_REGEX.lastIndex = 0;
|
|
86
|
-
while ((match = INLINE_CODE_REGEX.exec(markdown)) !== null) {
|
|
87
|
-
ranges.push({
|
|
88
|
-
start: match.index,
|
|
89
|
-
end: match.index + match[0].length,
|
|
90
|
-
kind: "inline_code",
|
|
91
|
-
});
|
|
92
|
-
}
|
|
211
|
+
// 2. Fenced code blocks (backtick + tilde, CommonMark matching rules)
|
|
212
|
+
ranges.push(...collectFencedCodeRanges(markdown));
|
|
93
213
|
|
|
94
|
-
//
|
|
214
|
+
// 3. HTML comments
|
|
95
215
|
HTML_COMMENT_REGEX.lastIndex = 0;
|
|
216
|
+
let match: RegExpExecArray | null;
|
|
96
217
|
while ((match = HTML_COMMENT_REGEX.exec(markdown)) !== null) {
|
|
97
218
|
ranges.push({
|
|
98
219
|
start: match.index,
|
|
@@ -101,6 +222,11 @@ export function getExcludedRanges(markdown: string): ExcludedRange[] {
|
|
|
101
222
|
});
|
|
102
223
|
}
|
|
103
224
|
|
|
225
|
+
// 4. Inline code. Pair delimiters only in visible prose so unmatched
|
|
226
|
+
// backticks inside already-excluded blocks cannot consume later content.
|
|
227
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
228
|
+
ranges.push(...collectInlineCodeRanges(markdown, ranges));
|
|
229
|
+
|
|
104
230
|
// Sort by start position for efficient lookup
|
|
105
231
|
ranges.sort((a, b) => a.start - b.start);
|
|
106
232
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous SHA-256 + base64 helpers for publish assets.
|
|
3
|
+
* Bun.CryptoHasher for digests; browser-safe atob for base64 decode parity with gno.sh.
|
|
4
|
+
*
|
|
5
|
+
* @module src/publish/artifact-asset-codec
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Synchronous SHA-256 hex digest over raw bytes (Bun-native). */
|
|
9
|
+
export const sha256BytesHex = (bytes: Uint8Array): string => {
|
|
10
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
11
|
+
hasher.update(bytes);
|
|
12
|
+
return hasher.digest("hex");
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const BASE64_PATTERN =
|
|
16
|
+
/^(?:[a-zA-Z0-9+/]{4})*(?:[a-zA-Z0-9+/]{2}==|[a-zA-Z0-9+/]{3}=)?$/u;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Encode raw bytes to standard base64 without Node Buffer.
|
|
20
|
+
* Prefers Bun/Web Uint8Array.toBase64(); falls back to btoa for parity with decode.
|
|
21
|
+
*/
|
|
22
|
+
export const encodeBytesToBase64 = (bytes: Uint8Array): string => {
|
|
23
|
+
if (typeof bytes.toBase64 === "function") {
|
|
24
|
+
return bytes.toBase64();
|
|
25
|
+
}
|
|
26
|
+
const CHUNK = 0x8000;
|
|
27
|
+
let binary = "";
|
|
28
|
+
for (let offset = 0; offset < bytes.length; offset += CHUNK) {
|
|
29
|
+
const slice = bytes.subarray(offset, offset + CHUNK);
|
|
30
|
+
binary += String.fromCharCode(...slice);
|
|
31
|
+
}
|
|
32
|
+
return btoa(binary);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Decode standard base64 without Buffer (uses atob). */
|
|
36
|
+
export const decodeBase64ToBytes = (
|
|
37
|
+
data: string
|
|
38
|
+
): { ok: true; bytes: Uint8Array } | { ok: false } => {
|
|
39
|
+
if (!BASE64_PATTERN.test(data) || data.length === 0) return { ok: false };
|
|
40
|
+
try {
|
|
41
|
+
const binary = atob(data);
|
|
42
|
+
if (binary.length === 0 && data.replace(/=+$/u, "").length > 0) {
|
|
43
|
+
return { ok: false };
|
|
44
|
+
}
|
|
45
|
+
const bytes = new Uint8Array(binary.length);
|
|
46
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
47
|
+
bytes[i] = binary.charCodeAt(i);
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, bytes };
|
|
50
|
+
} catch {
|
|
51
|
+
return { ok: false };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const UTF8 = new TextEncoder();
|
|
56
|
+
|
|
57
|
+
export const measureSerializedUploadBytes = (serializedBody: string): number =>
|
|
58
|
+
UTF8.encode(serializedBody).byteLength;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Canonical on-disk/upload serialization for publish artifacts.
|
|
62
|
+
*
|
|
63
|
+
* Byte-budget enforcement and every artifact writer must use this exact
|
|
64
|
+
* representation so `finalUploadBytes` describes the bytes handed to gno.sh.
|
|
65
|
+
*/
|
|
66
|
+
export const serializePublishArtifact = (artifact: unknown): string => {
|
|
67
|
+
const serialized = JSON.stringify(artifact, null, 2);
|
|
68
|
+
if (serialized === undefined) {
|
|
69
|
+
throw new TypeError("Publish artifact is not JSON-serializable");
|
|
70
|
+
}
|
|
71
|
+
return serialized;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const measureArtifactUploadBytes = (artifact: unknown): number =>
|
|
75
|
+
measureSerializedUploadBytes(serializePublishArtifact(artifact));
|