@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.
- package/README.md +15 -5
- package/assets/skill/SKILL.md +34 -0
- package/assets/skill/cli-reference.md +10 -2
- package/browser-extension/artifacts/{gno-browser-clipper-v1.31.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 +35 -1
- package/spec/db/schema.sql +55 -0
- package/spec/mcp.md +114 -11
- package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
- package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
- package/spec/output-schemas/publish-artifact.schema.json +76 -1
- package/src/cli/commands/publish.ts +43 -7
- package/src/core/document-capabilities.ts +13 -0
- package/src/core/file-ops.ts +129 -1
- package/src/core/file-refactor-adapter.ts +329 -0
- package/src/core/file-refactor-apply-edits.ts +61 -0
- package/src/core/file-refactor-apply-fs.ts +512 -0
- package/src/core/file-refactor-apply-safety.ts +340 -0
- package/src/core/file-refactor-apply-validate.ts +401 -0
- package/src/core/file-refactor-contract.ts +486 -0
- package/src/core/file-refactor-destination.ts +123 -0
- package/src/core/file-refactor-from-snapshot.ts +148 -0
- package/src/core/file-refactor-journal-port.ts +150 -0
- package/src/core/file-refactor-journal.ts +347 -0
- package/src/core/file-refactor-paths.ts +60 -0
- package/src/core/file-refactor-plan-classify.ts +208 -0
- package/src/core/file-refactor-plan-validate.ts +169 -0
- package/src/core/file-refactor-planner-types.ts +62 -0
- package/src/core/file-refactor-planner.ts +423 -0
- package/src/core/file-refactor-resolve.ts +280 -0
- package/src/core/file-refactor-service.ts +468 -0
- package/src/core/file-refactors.ts +84 -56
- package/src/core/link-destination-parse.ts +275 -0
- package/src/core/link-inventory-markdown.ts +454 -0
- package/src/core/link-inventory-opaque.ts +244 -0
- package/src/core/link-inventory-types.ts +47 -0
- package/src/core/link-inventory.ts +182 -0
- package/src/core/link-relevance.ts +150 -0
- package/src/ingestion/strip.ts +152 -26
- package/src/mcp/tools/index.ts +18 -17
- package/src/mcp/tools/workspace-write.ts +215 -97
- 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/sdk/client.ts +167 -115
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +32 -2
- package/src/serve/file-refactor-http.ts +239 -0
- package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/DocView.tsx +176 -41
- package/src/serve/routes/api.ts +193 -105
- package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +452 -0
- package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
- package/src/store/types.ts +84 -0
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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;
|