@gmickel/gno 1.31.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.
Files changed (77) hide show
  1. package/README.md +15 -5
  2. package/assets/skill/SKILL.md +34 -0
  3. package/assets/skill/cli-reference.md +10 -2
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.31.0.zip → gno-browser-clipper-v1.33.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.33.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +5 -1
  8. package/spec/cli.md +35 -1
  9. package/spec/db/schema.sql +55 -0
  10. package/spec/mcp.md +114 -11
  11. package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
  12. package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
  13. package/spec/output-schemas/publish-artifact.schema.json +76 -1
  14. package/src/cli/commands/publish.ts +43 -7
  15. package/src/core/document-capabilities.ts +13 -0
  16. package/src/core/file-ops.ts +129 -1
  17. package/src/core/file-refactor-adapter.ts +329 -0
  18. package/src/core/file-refactor-apply-edits.ts +61 -0
  19. package/src/core/file-refactor-apply-fs.ts +512 -0
  20. package/src/core/file-refactor-apply-safety.ts +340 -0
  21. package/src/core/file-refactor-apply-validate.ts +401 -0
  22. package/src/core/file-refactor-contract.ts +486 -0
  23. package/src/core/file-refactor-destination.ts +123 -0
  24. package/src/core/file-refactor-from-snapshot.ts +148 -0
  25. package/src/core/file-refactor-journal-port.ts +150 -0
  26. package/src/core/file-refactor-journal.ts +347 -0
  27. package/src/core/file-refactor-paths.ts +60 -0
  28. package/src/core/file-refactor-plan-classify.ts +208 -0
  29. package/src/core/file-refactor-plan-validate.ts +169 -0
  30. package/src/core/file-refactor-planner-types.ts +62 -0
  31. package/src/core/file-refactor-planner.ts +423 -0
  32. package/src/core/file-refactor-resolve.ts +280 -0
  33. package/src/core/file-refactor-service.ts +468 -0
  34. package/src/core/file-refactors.ts +84 -56
  35. package/src/core/link-destination-parse.ts +275 -0
  36. package/src/core/link-inventory-markdown.ts +454 -0
  37. package/src/core/link-inventory-opaque.ts +244 -0
  38. package/src/core/link-inventory-types.ts +47 -0
  39. package/src/core/link-inventory.ts +182 -0
  40. package/src/core/link-relevance.ts +150 -0
  41. package/src/ingestion/strip.ts +152 -26
  42. package/src/mcp/tools/index.ts +18 -17
  43. package/src/mcp/tools/workspace-write.ts +215 -97
  44. package/src/publish/artifact-asset-codec.ts +75 -0
  45. package/src/publish/artifact-asset-contract.ts +152 -0
  46. package/src/publish/artifact-asset-parse.ts +401 -0
  47. package/src/publish/artifact-asset-sniff.ts +108 -0
  48. package/src/publish/artifact-asset-validate.ts +209 -0
  49. package/src/publish/artifact-assets.ts +58 -0
  50. package/src/publish/artifact-validation.ts +32 -6
  51. package/src/publish/artifact.ts +50 -3
  52. package/src/publish/attachment-bundle.ts +145 -0
  53. package/src/publish/attachment-discover.ts +203 -0
  54. package/src/publish/attachment-load.ts +133 -0
  55. package/src/publish/attachment-obsidian.ts +45 -0
  56. package/src/publish/attachment-path.ts +334 -0
  57. package/src/publish/attachment-raster.ts +852 -0
  58. package/src/publish/attachment-resolver.ts +280 -0
  59. package/src/publish/attachment-types.ts +54 -0
  60. package/src/publish/encrypted-export.ts +121 -44
  61. package/src/publish/export-attachments.ts +224 -0
  62. package/src/publish/export-service.ts +142 -80
  63. package/src/publish/obsidian-sanitize.ts +121 -13
  64. package/src/sdk/client.ts +167 -115
  65. package/src/sdk/index.ts +7 -0
  66. package/src/sdk/types.ts +32 -2
  67. package/src/serve/file-refactor-http.ts +239 -0
  68. package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
  69. package/src/serve/public/globals.built.css +1 -1
  70. package/src/serve/public/pages/DocView.tsx +176 -41
  71. package/src/serve/routes/api.ts +193 -105
  72. package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
  73. package/src/store/migrations/index.ts +2 -0
  74. package/src/store/sqlite/adapter.ts +452 -0
  75. package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
  76. package/src/store/types.ts +84 -0
  77. package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +0 -1
@@ -8,6 +8,7 @@
8
8
  import { mkdir } from "node:fs/promises";
9
9
  // node:path for dirname/join (no Bun path utils)
10
10
  import { dirname, join } from "node:path";
11
+ import { z } from "zod";
11
12
 
12
13
  import type { Collection } from "../../config/types";
13
14
  import type { ToolContext } from "../server";
@@ -15,17 +16,22 @@ import type { ToolContext } from "../server";
15
16
  import { getDocumentCapabilities } from "../../core/document-capabilities";
16
17
  import { MCP_ERRORS } from "../../core/errors";
17
18
  import { withWriteLock } from "../../core/file-lock";
19
+ import { copyFilePath, createFolderPath } from "../../core/file-ops";
18
20
  import {
19
- copyFilePath,
20
- createFolderPath,
21
- renameFilePath,
22
- } from "../../core/file-ops";
23
- import {
21
+ applyCanonicalFileRefactor,
22
+ assertFileRefactorSyncConverged,
23
+ buildCanonicalRefactorPlan,
24
+ buildDurableFileRefactorApplyDeps,
24
25
  buildRefactorWarnings,
26
+ FILE_REFACTOR_APPLY_CONFIRMATION,
27
+ FILE_REFACTOR_SCHEMA_VERSION,
28
+ parseRefactorApplyConfirmation,
25
29
  planCreateFolder,
26
30
  planDuplicateRefactor,
27
- planMoveRefactor,
28
- planRenameRefactor,
31
+ resolveMoveTarget,
32
+ resolveRenameTarget,
33
+ type FileRefactorApplyResult,
34
+ type FileRefactorPreviewPlan,
29
35
  } from "../../core/file-refactors";
30
36
  import { recordContentMutation } from "../../core/mutation-generations";
31
37
  import { defaultSyncService, withContentTypeRules } from "../../ingestion";
@@ -37,23 +43,72 @@ interface CreateFolderInput {
37
43
  parentPath?: string;
38
44
  }
39
45
 
40
- interface RenameNoteInput {
41
- ref: string;
42
- name: string;
43
- }
44
-
45
- interface MoveNoteInput {
46
- ref: string;
47
- folderPath: string;
48
- name?: string;
49
- }
50
-
51
46
  interface DuplicateNoteInput {
52
47
  ref: string;
53
48
  folderPath?: string;
54
49
  name?: string;
55
50
  }
56
51
 
52
+ const refactorPreviewActionSchema = z.literal("preview");
53
+ const refactorApplyActionSchema = z.literal("apply");
54
+
55
+ export const renameNoteInputSchema = z.discriminatedUnion("action", [
56
+ z.object({
57
+ action: refactorPreviewActionSchema,
58
+ ref: z.string().min(1, "ref cannot be empty"),
59
+ name: z.string().min(1, "name cannot be empty"),
60
+ }),
61
+ z.object({
62
+ action: refactorApplyActionSchema,
63
+ ref: z.string().min(1, "ref cannot be empty"),
64
+ name: z.string().min(1, "name cannot be empty"),
65
+ schemaVersion: z.literal(FILE_REFACTOR_SCHEMA_VERSION),
66
+ planDigest: z
67
+ .string()
68
+ .regex(/^[a-f0-9]{64}$/, "planDigest must be a lowercase SHA-256"),
69
+ confirmation: z.literal(FILE_REFACTOR_APPLY_CONFIRMATION),
70
+ confirm: z.literal(true),
71
+ }),
72
+ ]);
73
+
74
+ export const moveNoteInputSchema = z.discriminatedUnion("action", [
75
+ z.object({
76
+ action: refactorPreviewActionSchema,
77
+ ref: z.string().min(1, "ref cannot be empty"),
78
+ folderPath: z.string().min(1, "folderPath cannot be empty"),
79
+ name: z.string().optional(),
80
+ }),
81
+ z.object({
82
+ action: refactorApplyActionSchema,
83
+ ref: z.string().min(1, "ref cannot be empty"),
84
+ folderPath: z.string().min(1, "folderPath cannot be empty"),
85
+ name: z.string().optional(),
86
+ schemaVersion: z.literal(FILE_REFACTOR_SCHEMA_VERSION),
87
+ planDigest: z
88
+ .string()
89
+ .regex(/^[a-f0-9]{64}$/, "planDigest must be a lowercase SHA-256"),
90
+ confirmation: z.literal(FILE_REFACTOR_APPLY_CONFIRMATION),
91
+ confirm: z.literal(true),
92
+ }),
93
+ ]);
94
+
95
+ export type RenameNoteInput = z.infer<typeof renameNoteInputSchema>;
96
+ export type MoveNoteInput = z.infer<typeof moveNoteInputSchema>;
97
+
98
+ export const RENAME_NOTE_MCP_ANNOTATIONS = {
99
+ readOnlyHint: false,
100
+ destructiveHint: true,
101
+ idempotentHint: false,
102
+ openWorldHint: false,
103
+ } as const;
104
+
105
+ export const MOVE_NOTE_MCP_ANNOTATIONS = {
106
+ readOnlyHint: false,
107
+ destructiveHint: true,
108
+ idempotentHint: false,
109
+ openWorldHint: false,
110
+ } as const;
111
+
57
112
  function resolveCollection(ctx: ToolContext, name: string): Collection {
58
113
  const normalized = name.trim().toLowerCase();
59
114
  const collection = ctx.collections.find((entry) => entry.name === normalized);
@@ -156,6 +211,136 @@ function ensureEditable(doc: {
156
211
  }`
157
212
  );
158
213
  }
214
+ return capabilities;
215
+ }
216
+
217
+ function requireWriteEnabled(ctx: ToolContext): void {
218
+ if (!ctx.enableWrite) {
219
+ throw new Error("Write tools disabled. Start MCP with --enable-write.");
220
+ }
221
+ }
222
+
223
+ function requireApplyConfirmation(args: {
224
+ schemaVersion: unknown;
225
+ planDigest: unknown;
226
+ confirmation: unknown;
227
+ confirm: unknown;
228
+ }) {
229
+ if (args.confirm !== true) {
230
+ throw new Error(
231
+ `${MCP_ERRORS.INVALID_INPUT.code}: confirm must be true to apply`
232
+ );
233
+ }
234
+ const parsed = parseRefactorApplyConfirmation(args);
235
+ if ("error" in parsed) {
236
+ throw new Error(`${parsed.error}: ${parsed.message}`);
237
+ }
238
+ return parsed;
239
+ }
240
+
241
+ function formatPreviewSummary(plan: FileRefactorPreviewPlan): string {
242
+ return `Preview ${plan.operation}: ${plan.source.relPath} → ${plan.target.relPath} (digest ${plan.planDigest.slice(0, 12)}…, canApply=${plan.canApply})`;
243
+ }
244
+
245
+ function formatApplySummary(result: FileRefactorApplyResult): string {
246
+ return `${result.operation} ${result.status}: ${result.source.relPath} → ${result.target.relPath}`;
247
+ }
248
+
249
+ async function previewOrApplyRename(
250
+ args: RenameNoteInput,
251
+ ctx: ToolContext
252
+ ): Promise<FileRefactorPreviewPlan | FileRefactorApplyResult> {
253
+ requireWriteEnabled(ctx);
254
+ const doc = await resolveDocByRef(ctx, args.ref);
255
+ const capabilities = ensureEditable(doc);
256
+ const collection = resolveCollection(ctx, doc.collection);
257
+ const target = resolveRenameTarget({
258
+ collection: collection.name,
259
+ currentRelPath: doc.relPath,
260
+ nextName: args.name,
261
+ });
262
+ const plan = await buildCanonicalRefactorPlan({
263
+ operation: "rename",
264
+ doc,
265
+ collection,
266
+ sourceFullPath: join(collection.path, doc.relPath),
267
+ target,
268
+ store: ctx.store,
269
+ sourceEditable: capabilities.editable,
270
+ });
271
+
272
+ if (args.action === "preview") {
273
+ return plan;
274
+ }
275
+
276
+ const confirmation = requireApplyConfirmation(args);
277
+ // Collection-scoped lock lives inside applyFileRefactor — do not wrap with
278
+ // ctx.writeLockPath (avoids double-locking incompatible with REST/SDK).
279
+ return applyCanonicalFileRefactor({
280
+ plan,
281
+ confirmation,
282
+ deps: buildDurableFileRefactorApplyDeps({
283
+ collection,
284
+ store: ctx.store,
285
+ syncAfterCommit: async () => {
286
+ const syncResult = await defaultSyncService.syncCollection(
287
+ collection,
288
+ ctx.store,
289
+ withContentTypeRules({ runUpdateCmd: false }, ctx.config)
290
+ );
291
+ assertFileRefactorSyncConverged(syncResult);
292
+ recordContentMutation(syncResult, ctx.markContentMutation);
293
+ },
294
+ }),
295
+ });
296
+ }
297
+
298
+ async function previewOrApplyMove(
299
+ args: MoveNoteInput,
300
+ ctx: ToolContext
301
+ ): Promise<FileRefactorPreviewPlan | FileRefactorApplyResult> {
302
+ requireWriteEnabled(ctx);
303
+ const doc = await resolveDocByRef(ctx, args.ref);
304
+ const capabilities = ensureEditable(doc);
305
+ const collection = resolveCollection(ctx, doc.collection);
306
+ const target = resolveMoveTarget({
307
+ collection: collection.name,
308
+ currentRelPath: doc.relPath,
309
+ folderPath: args.folderPath,
310
+ nextName: args.name,
311
+ });
312
+ const plan = await buildCanonicalRefactorPlan({
313
+ operation: "move",
314
+ doc,
315
+ collection,
316
+ sourceFullPath: join(collection.path, doc.relPath),
317
+ target,
318
+ store: ctx.store,
319
+ sourceEditable: capabilities.editable,
320
+ });
321
+
322
+ if (args.action === "preview") {
323
+ return plan;
324
+ }
325
+
326
+ const confirmation = requireApplyConfirmation(args);
327
+ return applyCanonicalFileRefactor({
328
+ plan,
329
+ confirmation,
330
+ deps: buildDurableFileRefactorApplyDeps({
331
+ collection,
332
+ store: ctx.store,
333
+ syncAfterCommit: async () => {
334
+ const syncResult = await defaultSyncService.syncCollection(
335
+ collection,
336
+ ctx.store,
337
+ withContentTypeRules({ runUpdateCmd: false }, ctx.config)
338
+ );
339
+ assertFileRefactorSyncConverged(syncResult);
340
+ recordContentMutation(syncResult, ctx.markContentMutation);
341
+ },
342
+ }),
343
+ });
159
344
  }
160
345
 
161
346
  export function handleCreateFolder(
@@ -166,9 +351,7 @@ export function handleCreateFolder(
166
351
  ctx,
167
352
  "gno_create_folder",
168
353
  async () => {
169
- if (!ctx.enableWrite) {
170
- throw new Error("Write tools disabled. Start MCP with --enable-write.");
171
- }
354
+ requireWriteEnabled(ctx);
172
355
 
173
356
  return withWriteLock(ctx.writeLockPath, async () => {
174
357
  const collection = resolveCollection(ctx, args.collection);
@@ -196,40 +379,11 @@ export function handleRenameNote(
196
379
  return runTool(
197
380
  ctx,
198
381
  "gno_rename_note",
199
- async () => {
200
- if (!ctx.enableWrite) {
201
- throw new Error("Write tools disabled. Start MCP with --enable-write.");
202
- }
203
-
204
- return withWriteLock(ctx.writeLockPath, async () => {
205
- const doc = await resolveDocByRef(ctx, args.ref);
206
- ensureEditable(doc);
207
- const collection = resolveCollection(ctx, doc.collection);
208
- const plan = planRenameRefactor({
209
- collection: collection.name,
210
- currentRelPath: doc.relPath,
211
- nextName: args.name,
212
- });
213
- const currentPath = join(collection.path, doc.relPath);
214
- const nextPath = join(collection.path, plan.nextRelPath);
215
- await renameFilePath(currentPath, nextPath);
216
- const syncResult = await defaultSyncService.syncCollection(
217
- collection,
218
- ctx.store,
219
- withContentTypeRules({ runUpdateCmd: false }, ctx.config)
220
- );
221
- recordContentMutation(syncResult, ctx.markContentMutation);
222
- return {
223
- uri: plan.nextUri,
224
- relPath: plan.nextRelPath,
225
- warnings: buildRefactorWarnings(
226
- await getRefactorSnapshot(ctx, doc.id),
227
- { filenameChanged: true }
228
- ).warnings,
229
- };
230
- });
231
- },
232
- (data) => `Renamed note to ${data.relPath}`
382
+ async () => previewOrApplyRename(args, ctx),
383
+ (data) =>
384
+ "planDigest" in data && "canApply" in data
385
+ ? formatPreviewSummary(data)
386
+ : formatApplySummary(data)
233
387
  );
234
388
  }
235
389
 
@@ -240,45 +394,11 @@ export function handleMoveNote(
240
394
  return runTool(
241
395
  ctx,
242
396
  "gno_move_note",
243
- async () => {
244
- if (!ctx.enableWrite) {
245
- throw new Error("Write tools disabled. Start MCP with --enable-write.");
246
- }
247
-
248
- return withWriteLock(ctx.writeLockPath, async () => {
249
- const doc = await resolveDocByRef(ctx, args.ref);
250
- ensureEditable(doc);
251
- const collection = resolveCollection(ctx, doc.collection);
252
- const plan = planMoveRefactor({
253
- collection: collection.name,
254
- currentRelPath: doc.relPath,
255
- folderPath: args.folderPath,
256
- nextName: args.name,
257
- });
258
- const currentPath = join(collection.path, doc.relPath);
259
- const nextPath = join(collection.path, plan.nextRelPath);
260
- await mkdir(dirname(nextPath), { recursive: true });
261
- await renameFilePath(currentPath, nextPath);
262
- const syncResult = await defaultSyncService.syncCollection(
263
- collection,
264
- ctx.store,
265
- withContentTypeRules({ runUpdateCmd: false }, ctx.config)
266
- );
267
- recordContentMutation(syncResult, ctx.markContentMutation);
268
- return {
269
- uri: plan.nextUri,
270
- relPath: plan.nextRelPath,
271
- warnings: buildRefactorWarnings(
272
- await getRefactorSnapshot(ctx, doc.id),
273
- {
274
- folderChanged: true,
275
- filenameChanged: Boolean(args.name),
276
- }
277
- ).warnings,
278
- };
279
- });
280
- },
281
- (data) => `Moved note to ${data.relPath}`
397
+ async () => previewOrApplyMove(args, ctx),
398
+ (data) =>
399
+ "planDigest" in data && "canApply" in data
400
+ ? formatPreviewSummary(data)
401
+ : formatApplySummary(data)
282
402
  );
283
403
  }
284
404
 
@@ -290,9 +410,7 @@ export function handleDuplicateNote(
290
410
  ctx,
291
411
  "gno_duplicate_note",
292
412
  async () => {
293
- if (!ctx.enableWrite) {
294
- throw new Error("Write tools disabled. Start MCP with --enable-write.");
295
- }
413
+ requireWriteEnabled(ctx);
296
414
 
297
415
  return withWriteLock(ctx.writeLockPath, async () => {
298
416
  const doc = await resolveDocByRef(ctx, args.ref);
@@ -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));
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Shared publish-asset contract vocabulary (producer/consumer).
3
+ *
4
+ * @module src/publish/artifact-asset-contract
5
+ */
6
+
7
+ export const MAX_PUBLISH_UPLOAD_BYTES = 100 * 1024 * 1024;
8
+
9
+ /** Schema bounds for asset reference sourceRef (publish-artifact.schema.json). */
10
+ export const MIN_SOURCE_REF_LENGTH = 1;
11
+ export const MAX_SOURCE_REF_LENGTH = 1024;
12
+
13
+ export const BUNDLED_RASTER_ASSETS_CAPABILITY =
14
+ "bundled-raster-assets@1" as const;
15
+
16
+ export const KNOWN_PUBLISH_REQUIRED_CAPABILITIES = [
17
+ BUNDLED_RASTER_ASSETS_CAPABILITY,
18
+ ] as const;
19
+
20
+ /** Schema bounds for requiredCapabilities entries (publish-artifact.schema.json). */
21
+ export const MIN_REQUIRED_CAPABILITY_LENGTH = 1;
22
+ export const MAX_REQUIRED_CAPABILITY_LENGTH = 128;
23
+
24
+ export type KnownPublishRequiredCapability =
25
+ (typeof KNOWN_PUBLISH_REQUIRED_CAPABILITIES)[number];
26
+
27
+ export const SUPPORTED_RASTER_MEDIA_TYPES = [
28
+ "image/png",
29
+ "image/jpeg",
30
+ "image/gif",
31
+ "image/webp",
32
+ "image/avif",
33
+ ] as const;
34
+
35
+ export type SupportedRasterMediaType =
36
+ (typeof SUPPORTED_RASTER_MEDIA_TYPES)[number];
37
+
38
+ export const MAX_RASTER_DIMENSION_PX = 16_384;
39
+ export const MIN_RASTER_DIMENSION_PX = 1;
40
+
41
+ /** Strict sentinel: scheme + lowercase SHA-256 hex asset id. */
42
+ export const GNO_ASSET_SENTINEL_PATTERN = /^gno-asset:([a-f0-9]{64})$/u;
43
+ export const GNO_ASSET_SENTINEL_PREFIX = "gno-asset:";
44
+
45
+ export const PUBLISH_ASSET_NOTE_SLUG_PATTERN =
46
+ /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/u;
47
+
48
+ export const PUBLISH_ASSET_VISIBILITY = {
49
+ public: {
50
+ class: "public",
51
+ delivery: "immutable-public-url",
52
+ storage: "private-until-public-commit",
53
+ notes:
54
+ "Validated assets may use immutable public URLs only after the share is classified public.",
55
+ },
56
+ "secret-link": {
57
+ class: "secret",
58
+ delivery: "capability-authorized",
59
+ storage: "private",
60
+ forbids: ["public-object-url", "presigned-url-as-sole-authorization"],
61
+ notes:
62
+ "Secret is an authorization boundary; private storage delivery requires the secret-share capability.",
63
+ },
64
+ encrypted: {
65
+ class: "encrypted",
66
+ delivery: "client-blob-url",
67
+ storage: "none-plaintext",
68
+ assetPlacement: "encrypted-client-payload",
69
+ notes:
70
+ "Plaintext image bytes remain inside the encrypted client payload; Blob URLs are revoked on replacement/unmount.",
71
+ },
72
+ } as const;
73
+
74
+ export const PUBLISH_ASSET_LIFECYCLE_TERMINALS = [
75
+ "committed",
76
+ "rolled_back",
77
+ "deleted",
78
+ "orphan_cleaned",
79
+ "idempotent_noop",
80
+ ] as const;
81
+
82
+ export type PublishAssetLifecycleTerminal =
83
+ (typeof PUBLISH_ASSET_LIFECYCLE_TERMINALS)[number];
84
+
85
+ export const PUBLISH_ASSET_DIAGNOSTIC_CODES = [
86
+ "ASSET_MISSING",
87
+ "ASSET_CONFLICT",
88
+ "ASSET_SENTINEL_RAW",
89
+ "ASSET_SENTINEL_UNRESOLVED",
90
+ "ASSET_SENTINEL_INVALID",
91
+ "ASSET_TRAVERSAL",
92
+ "ASSET_MIME_SPOOF",
93
+ "ASSET_OVERSIZE",
94
+ "ASSET_CORRUPT",
95
+ "ASSET_UNSUPPORTED_FORMAT",
96
+ "ASSET_DIMENSION_INVALID",
97
+ "CAPABILITY_UNSUPPORTED",
98
+ "ENVELOPE_OVERSIZE",
99
+ ] as const;
100
+
101
+ export type PublishAssetDiagnosticCode =
102
+ (typeof PUBLISH_ASSET_DIAGNOSTIC_CODES)[number];
103
+
104
+ export interface PublishArtifactAssetReference {
105
+ noteSlug: string;
106
+ sourceRef: string;
107
+ }
108
+
109
+ export interface PublishArtifactAsset {
110
+ byteLength: number;
111
+ data: string;
112
+ encoding: "base64";
113
+ height: number;
114
+ id: string;
115
+ mediaType: string;
116
+ references: Array<PublishArtifactAssetReference>;
117
+ sha256: string;
118
+ width: number;
119
+ }
120
+
121
+ export interface PublishAssetDiagnostic {
122
+ code: PublishAssetDiagnosticCode;
123
+ message: string;
124
+ }
125
+
126
+ export type PublishAssetClassification =
127
+ | "asset-free"
128
+ | "bundled-raster-v1"
129
+ | "encrypted-client-payload";
130
+
131
+ export type PublishAssetContractResult =
132
+ | { ok: true; classification: PublishAssetClassification }
133
+ | { ok: false; diagnostic: PublishAssetDiagnostic };
134
+
135
+ export interface ValidatePublishAssetContractOptions {
136
+ /** Exact UTF-8 byte length of the final upload body when already serialized. */
137
+ serializedUploadBytes?: number;
138
+ }
139
+
140
+ export const ASSET_DESCRIPTOR_KEYS = [
141
+ "byteLength",
142
+ "data",
143
+ "encoding",
144
+ "height",
145
+ "id",
146
+ "mediaType",
147
+ "references",
148
+ "sha256",
149
+ "width",
150
+ ] as const;
151
+
152
+ export const ASSET_REFERENCE_KEYS = ["noteSlug", "sourceRef"] as const;