@gmickel/gno 1.32.0 → 1.34.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 (54) hide show
  1. package/README.md +17 -3
  2. package/assets/skill/SKILL.md +30 -0
  3. package/assets/skill/cli-reference.md +10 -2
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.32.0.zip → gno-browser-clipper-v1.34.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.34.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 +60 -1
  9. package/spec/mcp.md +21 -0
  10. package/spec/output-schemas/audit-report.schema.json +284 -0
  11. package/spec/output-schemas/publish-artifact.schema.json +76 -1
  12. package/src/cli/commands/audit.ts +231 -0
  13. package/src/cli/commands/publish.ts +43 -7
  14. package/src/cli/errors.ts +9 -2
  15. package/src/cli/program.ts +112 -0
  16. package/src/core/audit-contract.ts +296 -0
  17. package/src/core/audit-freshness.ts +233 -0
  18. package/src/core/audit-links.ts +222 -0
  19. package/src/core/audit-provenance.ts +154 -0
  20. package/src/core/audit-report.ts +318 -0
  21. package/src/core/audit-workspace.ts +678 -0
  22. package/src/core/audit.ts +569 -0
  23. package/src/core/capture.ts +196 -3
  24. package/src/core/document-capabilities.ts +9 -8
  25. package/src/core/record-metadata.ts +33 -0
  26. package/src/ingestion/strip.ts +152 -26
  27. package/src/mcp/http-egress.ts +8 -0
  28. package/src/mcp/tools/audit.ts +97 -0
  29. package/src/mcp/tools/index.ts +13 -0
  30. package/src/publish/artifact-asset-codec.ts +75 -0
  31. package/src/publish/artifact-asset-contract.ts +152 -0
  32. package/src/publish/artifact-asset-parse.ts +401 -0
  33. package/src/publish/artifact-asset-sniff.ts +108 -0
  34. package/src/publish/artifact-asset-validate.ts +209 -0
  35. package/src/publish/artifact-assets.ts +58 -0
  36. package/src/publish/artifact-validation.ts +32 -6
  37. package/src/publish/artifact.ts +50 -3
  38. package/src/publish/attachment-bundle.ts +145 -0
  39. package/src/publish/attachment-discover.ts +203 -0
  40. package/src/publish/attachment-load.ts +133 -0
  41. package/src/publish/attachment-obsidian.ts +45 -0
  42. package/src/publish/attachment-path.ts +334 -0
  43. package/src/publish/attachment-raster.ts +852 -0
  44. package/src/publish/attachment-resolver.ts +280 -0
  45. package/src/publish/attachment-types.ts +54 -0
  46. package/src/publish/encrypted-export.ts +121 -44
  47. package/src/publish/export-attachments.ts +224 -0
  48. package/src/publish/export-service.ts +142 -80
  49. package/src/publish/obsidian-sanitize.ts +121 -13
  50. package/src/serve/routes/api.ts +2 -1
  51. package/src/store/sqlite/adapter.ts +82 -0
  52. package/src/store/sqlite/graph-link-bulk-resolver.ts +191 -0
  53. package/src/store/sqlite/graph-link-resolver.ts +241 -2
  54. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +0 -1
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Parser-aware local attachment discovery, rewrite, and v1 bundling entrypoints.
3
+ *
4
+ * @module src/publish/attachment-resolver
5
+ */
6
+
7
+ import type {
8
+ AttachmentDiagnostic,
9
+ AttachmentResolveContext,
10
+ PendingAssetPayload,
11
+ } from "./attachment-types";
12
+
13
+ import {
14
+ GNO_ASSET_SENTINEL_PREFIX,
15
+ MAX_PUBLISH_UPLOAD_BYTES,
16
+ } from "./artifact-asset-contract";
17
+ import { formatGnoAssetSentinel } from "./artifact-asset-sniff";
18
+ import {
19
+ attachAssetsToV1Artifact,
20
+ buildDeterministicAssets,
21
+ emptyAssetEgressSummary,
22
+ summarizeAssetEgress,
23
+ } from "./attachment-bundle";
24
+ import {
25
+ discoverImageOccurrences,
26
+ type DiscoveredImageRef,
27
+ } from "./attachment-discover";
28
+ import { readAndValidateAsset } from "./attachment-load";
29
+ import {
30
+ assertContainedFile,
31
+ buildAttachmentBasenameIndex,
32
+ diagnostic,
33
+ resolveCandidateRelPath,
34
+ } from "./attachment-path";
35
+
36
+ export type {
37
+ AttachmentDiagnostic,
38
+ AttachmentDiagnosticCode,
39
+ AttachmentResolveContext,
40
+ PendingAssetPayload,
41
+ PublishAssetEgressSummary,
42
+ } from "./attachment-types";
43
+ export {
44
+ attachAssetsToV1Artifact,
45
+ buildDeterministicAssets,
46
+ emptyAssetEgressSummary,
47
+ summarizeAssetEgress,
48
+ } from "./attachment-bundle";
49
+ export { buildAttachmentBasenameIndex } from "./attachment-path";
50
+
51
+ export interface AttachmentRewriteResult {
52
+ diagnostics: AttachmentDiagnostic[];
53
+ externalCount: number;
54
+ markdown: string;
55
+ payloads: Map<string, PendingAssetPayload>;
56
+ /** Raw bytes summed per successful load before cross-ref dedup. */
57
+ preDedupRawBytes: number;
58
+ }
59
+
60
+ type ImageOccurrence = DiscoveredImageRef;
61
+
62
+ const isExternalDestination = (value: string): boolean => {
63
+ const trimmed = value.trim();
64
+ return trimmed.startsWith("//") || /^[a-z][a-z0-9+.-]*:/iu.test(trimmed);
65
+ };
66
+
67
+ const isDataUrl = (value: string): boolean =>
68
+ value.trim().toLowerCase().startsWith("data:");
69
+
70
+ const escapeMarkdownImageAlt = (value: string): string =>
71
+ value.replace(/[\\[\]]/gu, "\\$&");
72
+
73
+ const formatMarkdownImageTitle = (value: string | null | undefined): string =>
74
+ value === null || value === undefined
75
+ ? ""
76
+ : ` "${value.replace(/[\\"]/gu, "\\$&")}"`;
77
+
78
+ const rewriteOccurrence = (
79
+ occurrence: ImageOccurrence,
80
+ assetId: string
81
+ ): string => {
82
+ const sentinel = formatGnoAssetSentinel(assetId);
83
+ if (occurrence.kind === "markdown") {
84
+ return `![${escapeMarkdownImageAlt(occurrence.alt)}](${sentinel}${formatMarkdownImageTitle(occurrence.title)})`;
85
+ }
86
+ const alias = occurrence.alt.trim();
87
+ const display = alias.length > 0 && !/^\d+$/u.test(alias) ? alias : "";
88
+ return `![${display}](${sentinel})`;
89
+ };
90
+
91
+ const mergePayload = (
92
+ payloads: Map<string, PendingAssetPayload>,
93
+ loaded: PendingAssetPayload,
94
+ noteSlug: string,
95
+ relPath: string
96
+ ): void => {
97
+ const existing = payloads.get(loaded.sha256);
98
+ if (existing) {
99
+ existing.references.push({ noteSlug, sourceRef: relPath });
100
+ return;
101
+ }
102
+ payloads.set(loaded.sha256, loaded);
103
+ };
104
+
105
+ /**
106
+ * Discover local/external image refs in one parser-aware pass and rewrite
107
+ * successfully bundled destinations to gno-asset:<sha256>.
108
+ */
109
+ export async function rewriteAttachmentsInMarkdown(
110
+ markdown: string,
111
+ ctx: AttachmentResolveContext
112
+ ): Promise<AttachmentRewriteResult> {
113
+ const occurrences = discoverImageOccurrences(markdown);
114
+ const diagnostics: AttachmentDiagnostic[] = [];
115
+ const payloads = new Map<string, PendingAssetPayload>();
116
+ let externalCount = 0;
117
+ let newEncodedAssetBytes = 0;
118
+ let preDedupRawBytes = 0;
119
+ const replacements: Array<{ end: number; start: number; text: string }> = [];
120
+
121
+ for (const occurrence of occurrences) {
122
+ const ref = occurrence.sourceRef.trim();
123
+ if (!ref) {
124
+ diagnostics.push(
125
+ diagnostic(
126
+ "ASSET_CORRUPT",
127
+ "Empty image destination",
128
+ ctx.noteSlug,
129
+ ref
130
+ )
131
+ );
132
+ replacements.push({
133
+ start: occurrence.start,
134
+ end: occurrence.end,
135
+ text: "",
136
+ });
137
+ continue;
138
+ }
139
+ if (isDataUrl(ref)) {
140
+ diagnostics.push(
141
+ diagnostic(
142
+ "ASSET_UNSUPPORTED_FORMAT",
143
+ "data: image URLs are unsupported",
144
+ ctx.noteSlug,
145
+ ref
146
+ )
147
+ );
148
+ replacements.push({
149
+ start: occurrence.start,
150
+ end: occurrence.end,
151
+ text: "",
152
+ });
153
+ continue;
154
+ }
155
+ if (ref.startsWith(GNO_ASSET_SENTINEL_PREFIX)) {
156
+ diagnostics.push(
157
+ diagnostic(
158
+ "ASSET_CORRUPT",
159
+ "Authored gno-asset references are reserved for publish export",
160
+ ctx.noteSlug,
161
+ ref
162
+ )
163
+ );
164
+ replacements.push({
165
+ start: occurrence.start,
166
+ end: occurrence.end,
167
+ text: "",
168
+ });
169
+ continue;
170
+ }
171
+ if (isExternalDestination(ref)) {
172
+ if (/^https?:/iu.test(ref) || ref.startsWith("//")) {
173
+ externalCount += 1;
174
+ continue;
175
+ }
176
+ diagnostics.push(
177
+ diagnostic(
178
+ "ASSET_UNSUPPORTED_FORMAT",
179
+ `Unsupported image protocol in "${ref}"`,
180
+ ctx.noteSlug,
181
+ ref
182
+ )
183
+ );
184
+ replacements.push({
185
+ start: occurrence.start,
186
+ end: occurrence.end,
187
+ text: "",
188
+ });
189
+ continue;
190
+ }
191
+ const resolved = resolveCandidateRelPath(ref, ctx, occurrence.kind);
192
+ if (!resolved.ok) {
193
+ if (resolved.diagnostic.code === "ASSET_TRAVERSAL") {
194
+ throw new Error(
195
+ `ASSET_TRAVERSAL: ${resolved.diagnostic.message} (${ref})`
196
+ );
197
+ }
198
+ diagnostics.push(resolved.diagnostic);
199
+ replacements.push({
200
+ start: occurrence.start,
201
+ end: occurrence.end,
202
+ text: "",
203
+ });
204
+ continue;
205
+ }
206
+
207
+ const contained = await assertContainedFile(
208
+ ctx.collectionRoot,
209
+ resolved.relPath,
210
+ ctx.noteSlug,
211
+ ref,
212
+ ctx.collectionExcludes
213
+ );
214
+ if ("code" in contained) {
215
+ diagnostics.push(contained);
216
+ replacements.push({
217
+ start: occurrence.start,
218
+ end: occurrence.end,
219
+ text: "",
220
+ });
221
+ continue;
222
+ }
223
+
224
+ const loaded = await readAndValidateAsset(
225
+ contained.absPath,
226
+ ctx.noteSlug,
227
+ ref,
228
+ resolved.relPath
229
+ );
230
+ if ("code" in loaded) {
231
+ diagnostics.push(loaded);
232
+ replacements.push({
233
+ start: occurrence.start,
234
+ end: occurrence.end,
235
+ text: "",
236
+ });
237
+ continue;
238
+ }
239
+
240
+ preDedupRawBytes += loaded.byteLength;
241
+ const isNewAsset =
242
+ !payloads.has(loaded.sha256) && !ctx.existingAssetIds?.has(loaded.sha256);
243
+ if (isNewAsset) {
244
+ const projectedEncodedBytes =
245
+ (ctx.existingEncodedAssetBytes ?? 0) +
246
+ newEncodedAssetBytes +
247
+ loaded.data.length;
248
+ if (projectedEncodedBytes > MAX_PUBLISH_UPLOAD_BYTES) {
249
+ throw new Error(
250
+ `ENVELOPE_OVERSIZE: encoded asset data exceeds ${MAX_PUBLISH_UPLOAD_BYTES} bytes`
251
+ );
252
+ }
253
+ newEncodedAssetBytes += loaded.data.length;
254
+ }
255
+ mergePayload(payloads, loaded, ctx.noteSlug, resolved.relPath);
256
+ replacements.push({
257
+ start: occurrence.start,
258
+ end: occurrence.end,
259
+ text: rewriteOccurrence(occurrence, loaded.sha256),
260
+ });
261
+ }
262
+
263
+ let output = markdown;
264
+ for (const replacement of [...replacements].sort(
265
+ (a, b) => b.start - a.start
266
+ )) {
267
+ output =
268
+ output.slice(0, replacement.start) +
269
+ replacement.text +
270
+ output.slice(replacement.end);
271
+ }
272
+
273
+ return {
274
+ diagnostics,
275
+ externalCount,
276
+ markdown: output,
277
+ payloads,
278
+ preDedupRawBytes,
279
+ };
280
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared attachment diagnostic and payload types for publish export.
3
+ *
4
+ * @module src/publish/attachment-types
5
+ */
6
+
7
+ import type {
8
+ PublishArtifactAsset,
9
+ PublishArtifactAssetReference,
10
+ PublishAssetDiagnosticCode,
11
+ } from "./artifact-asset-contract";
12
+
13
+ export type AttachmentDiagnosticCode =
14
+ | PublishAssetDiagnosticCode
15
+ | "ASSET_AMBIGUOUS"
16
+ | "ASSET_EXTERNAL";
17
+
18
+ export interface AttachmentDiagnostic {
19
+ code: AttachmentDiagnosticCode;
20
+ message: string;
21
+ noteSlug: string;
22
+ sourceRef: string;
23
+ }
24
+
25
+ export interface PublishAssetEgressSummary {
26
+ assetCount: number;
27
+ dedupSavedBytes: number;
28
+ diagnostics: AttachmentDiagnostic[];
29
+ encodedBytes: number;
30
+ externalCount: number;
31
+ finalUploadBytes: number;
32
+ rawBytes: number;
33
+ referenceCount: number;
34
+ }
35
+
36
+ export interface PendingAssetPayload {
37
+ byteLength: number;
38
+ data: string;
39
+ height: number;
40
+ mediaType: PublishArtifactAsset["mediaType"];
41
+ references: PublishArtifactAssetReference[];
42
+ sha256: string;
43
+ width: number;
44
+ }
45
+
46
+ export interface AttachmentResolveContext {
47
+ basenameIndex: Map<string, string[]>;
48
+ collectionExcludes?: readonly string[];
49
+ collectionRoot: string;
50
+ existingAssetIds?: ReadonlySet<string>;
51
+ existingEncodedAssetBytes?: number;
52
+ noteSlug: string;
53
+ sourceRelPath: string;
54
+ }
@@ -1,14 +1,25 @@
1
- import { randomBytes, webcrypto } from "node:crypto";
1
+ /**
2
+ * Client-encrypted publish payload builder (v2).
3
+ * Raster assets are embedded only inside AES-GCM plaintext ReaderSpaceData.
4
+ *
5
+ * @module src/publish/encrypted-export
6
+ */
2
7
 
3
- import type { EncryptedArtifactPayload, PublishArtifactNote } from "./artifact";
8
+ import type {
9
+ EncryptedArtifactPayload,
10
+ PublishArtifactAsset,
11
+ PublishArtifactNote,
12
+ } from "./artifact";
4
13
 
5
- const { subtle } = webcrypto;
14
+ import { encodeBytesToBase64 } from "./artifact-asset-codec";
15
+ import { BUNDLED_RASTER_ASSETS_CAPABILITY } from "./artifact-asset-contract";
6
16
 
7
17
  const PBKDF2_ITERATIONS = 210_000;
8
18
  const IV_BYTES = 12;
9
19
  const SALT_BYTES = 16;
10
20
 
11
21
  const encoder = new TextEncoder();
22
+ const decoder = new TextDecoder();
12
23
 
13
24
  type MetadataEntry = {
14
25
  label: string;
@@ -47,14 +58,19 @@ type ReaderNoteCard = {
47
58
  title: string;
48
59
  };
49
60
 
50
- type ReaderSpaceData = {
61
+ /** AES-GCM plaintext shape for encrypted shares (assets stay client-only). */
62
+ export type EncryptedReaderSpaceData = {
63
+ /** Always empty — encrypted shares never project server asset manifests. */
51
64
  assetManifest: [];
65
+ /** Validated descriptors + base64 bytes; omitted when asset-free. */
66
+ assets?: PublishArtifactAsset[];
52
67
  currentNote: ReaderNoteCard;
53
68
  homeNoteSlug?: string;
54
69
  metadataPreview: MetadataEntry[];
55
70
  nextNoteSlug?: string;
56
71
  noteCards: ReaderNoteCard[];
57
72
  previousNoteSlug?: string;
73
+ requiredCapabilities?: Array<typeof BUNDLED_RASTER_ASSETS_CAPABILITY>;
58
74
  searchIndex: Array<{
59
75
  excerpt: string;
60
76
  haystack: string;
@@ -77,7 +93,28 @@ type ReaderSpaceData = {
77
93
  visibility: "encrypted";
78
94
  };
79
95
 
80
- const toBase64 = (value: Uint8Array) => Buffer.from(value).toString("base64");
96
+ const toBase64 = (value: Uint8Array): string => encodeBytesToBase64(value);
97
+
98
+ const fromBase64 = (value: string): Uint8Array => {
99
+ const binary = atob(value);
100
+ const bytes = new Uint8Array(binary.length);
101
+ for (let i = 0; i < binary.length; i += 1) {
102
+ bytes[i] = binary.charCodeAt(i);
103
+ }
104
+ return bytes;
105
+ };
106
+
107
+ const randomBytes = (size: number): Uint8Array => {
108
+ const bytes = new Uint8Array(size);
109
+ crypto.getRandomValues(bytes);
110
+ return bytes;
111
+ };
112
+
113
+ const toArrayBuffer = (value: Uint8Array): ArrayBuffer =>
114
+ value.buffer.slice(
115
+ value.byteOffset,
116
+ value.byteOffset + value.byteLength
117
+ ) as ArrayBuffer;
81
118
 
82
119
  const stripFrontmatter = (markdown: string) => {
83
120
  if (!markdown.startsWith("---\n")) {
@@ -134,6 +171,7 @@ const deriveExcerpt = (summary: string, blocks: NoteBlock[]) => {
134
171
  };
135
172
 
136
173
  const deriveReaderPayload = (input: {
174
+ assets: PublishArtifactAsset[];
137
175
  exportedAt: string;
138
176
  homeNoteSlug?: string;
139
177
  notes: PublishArtifactNote[];
@@ -141,7 +179,7 @@ const deriveReaderPayload = (input: {
141
179
  sourceType: "note" | "collection";
142
180
  summary: string;
143
181
  title: string;
144
- }) => {
182
+ }): { payload: EncryptedReaderSpaceData; secretToken: string } => {
145
183
  const noteCards: ReaderNoteCard[] = input.notes.map((note) => {
146
184
  const blocks = parseMarkdownBlocks(note.markdown);
147
185
  return {
@@ -173,43 +211,55 @@ const deriveReaderPayload = (input: {
173
211
  (note) => note.noteId === currentNote.noteId
174
212
  );
175
213
  const sharePath = `/locked/${makeToken(input.routeSlug)}`;
214
+ const hasAssets = input.assets.length > 0;
215
+
216
+ const payload: EncryptedReaderSpaceData = {
217
+ sharePath,
218
+ shareLabel: "Encrypted share",
219
+ visibility: "encrypted",
220
+ sourceType: input.sourceType,
221
+ title: input.title,
222
+ summary: input.summary,
223
+ snapshot: {
224
+ id: `snapshot-${input.routeSlug}-encrypted-v1`,
225
+ version: 1,
226
+ createdAt: input.exportedAt,
227
+ lastIndexedAt: input.exportedAt,
228
+ searchEnabled: noteCards.length > 1,
229
+ },
230
+ metadataPreview: [],
231
+ assetManifest: [],
232
+ searchIndex: noteCards.map((note) => ({
233
+ noteId: note.noteId,
234
+ slug: note.slug,
235
+ title: note.title,
236
+ excerpt: note.excerpt,
237
+ haystack: `${note.title} ${note.summary}`.toLowerCase(),
238
+ })),
239
+ noteCards,
240
+ currentNote,
241
+ previousNoteSlug: noteCards[currentIndex - 1]?.slug,
242
+ nextNoteSlug: noteCards[currentIndex + 1]?.slug,
243
+ homeNoteSlug: input.homeNoteSlug ?? noteCards[0]?.slug,
244
+ };
245
+
246
+ if (hasAssets) {
247
+ payload.assets = input.assets;
248
+ payload.requiredCapabilities = [BUNDLED_RASTER_ASSETS_CAPABILITY];
249
+ }
176
250
 
177
251
  return {
178
- payload: {
179
- sharePath,
180
- shareLabel: "Encrypted share",
181
- visibility: "encrypted" as const,
182
- sourceType: input.sourceType,
183
- title: input.title,
184
- summary: input.summary,
185
- snapshot: {
186
- id: `snapshot-${input.routeSlug}-encrypted-v1`,
187
- version: 1,
188
- createdAt: input.exportedAt,
189
- lastIndexedAt: input.exportedAt,
190
- searchEnabled: noteCards.length > 1,
191
- },
192
- metadataPreview: [],
193
- assetManifest: [],
194
- searchIndex: noteCards.map((note) => ({
195
- noteId: note.noteId,
196
- slug: note.slug,
197
- title: note.title,
198
- excerpt: note.excerpt,
199
- haystack: `${note.title} ${note.summary}`.toLowerCase(),
200
- })),
201
- noteCards,
202
- currentNote,
203
- previousNoteSlug: noteCards[currentIndex - 1]?.slug,
204
- nextNoteSlug: noteCards[currentIndex + 1]?.slug,
205
- homeNoteSlug: input.homeNoteSlug ?? noteCards[0]?.slug,
206
- } satisfies ReaderSpaceData,
252
+ payload,
207
253
  secretToken: sharePath.replace("/locked/", ""),
208
254
  };
209
255
  };
210
256
 
211
- const deriveKey = async (passphrase: string, salt: Uint8Array) => {
212
- const material = await subtle.importKey(
257
+ const deriveKey = async (
258
+ passphrase: string,
259
+ salt: Uint8Array,
260
+ usages: KeyUsage[]
261
+ ) => {
262
+ const material = await crypto.subtle.importKey(
213
263
  "raw",
214
264
  encoder.encode(passphrase),
215
265
  "PBKDF2",
@@ -217,11 +267,11 @@ const deriveKey = async (passphrase: string, salt: Uint8Array) => {
217
267
  ["deriveKey"]
218
268
  );
219
269
 
220
- return subtle.deriveKey(
270
+ return crypto.subtle.deriveKey(
221
271
  {
222
272
  name: "PBKDF2",
223
273
  hash: "SHA-256",
224
- salt: Uint8Array.from(salt),
274
+ salt: toArrayBuffer(salt),
225
275
  iterations: PBKDF2_ITERATIONS,
226
276
  },
227
277
  material,
@@ -230,7 +280,7 @@ const deriveKey = async (passphrase: string, salt: Uint8Array) => {
230
280
  length: 256,
231
281
  },
232
282
  false,
233
- ["encrypt"]
283
+ usages
234
284
  );
235
285
  };
236
286
 
@@ -240,10 +290,10 @@ const encryptJson = async (
240
290
  ): Promise<EncryptedArtifactPayload> => {
241
291
  const salt = randomBytes(SALT_BYTES);
242
292
  const iv = randomBytes(IV_BYTES);
243
- const key = await deriveKey(passphrase, salt);
293
+ const key = await deriveKey(passphrase, salt, ["encrypt"]);
244
294
  const plaintext = encoder.encode(JSON.stringify(payload));
245
- const ciphertext = await subtle.encrypt(
246
- { name: "AES-GCM", iv },
295
+ const ciphertext = await crypto.subtle.encrypt(
296
+ { name: "AES-GCM", iv: toArrayBuffer(iv) },
247
297
  key,
248
298
  plaintext
249
299
  );
@@ -256,7 +306,25 @@ const encryptJson = async (
256
306
  };
257
307
  };
258
308
 
309
+ export const decryptEncryptedArtifactPayload = async <
310
+ T = EncryptedReaderSpaceData,
311
+ >(
312
+ passphrase: string,
313
+ payload: EncryptedArtifactPayload
314
+ ): Promise<T> => {
315
+ const key = await deriveKey(passphrase, fromBase64(payload.salt), [
316
+ "decrypt",
317
+ ]);
318
+ const plaintext = await crypto.subtle.decrypt(
319
+ { name: "AES-GCM", iv: toArrayBuffer(fromBase64(payload.iv)) },
320
+ key,
321
+ toArrayBuffer(fromBase64(payload.ciphertext))
322
+ );
323
+ return JSON.parse(decoder.decode(plaintext)) as T;
324
+ };
325
+
259
326
  export const buildEncryptedArtifactPayload = async (input: {
327
+ assets?: PublishArtifactAsset[];
260
328
  exportedAt: string;
261
329
  homeNoteSlug?: string;
262
330
  notes: PublishArtifactNote[];
@@ -266,7 +334,16 @@ export const buildEncryptedArtifactPayload = async (input: {
266
334
  summary: string;
267
335
  title: string;
268
336
  }) => {
269
- const { payload, secretToken } = deriveReaderPayload(input);
337
+ const { payload, secretToken } = deriveReaderPayload({
338
+ assets: input.assets ?? [],
339
+ exportedAt: input.exportedAt,
340
+ homeNoteSlug: input.homeNoteSlug,
341
+ notes: input.notes,
342
+ routeSlug: input.routeSlug,
343
+ sourceType: input.sourceType,
344
+ summary: input.summary,
345
+ title: input.title,
346
+ });
270
347
 
271
348
  return {
272
349
  encryptedPayload: await encryptJson(input.passphrase, payload),