@gmickel/gno 2.1.1 → 2.2.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 +1 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v2.1.1.zip → gno-browser-clipper-v2.2.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.2.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +14 -1
- package/spec/output-schemas/publish-artifact.schema.json +12 -0
- package/src/cli/commands/publish.ts +1 -0
- package/src/publish/artifact-validation.ts +27 -0
- package/src/publish/artifact.ts +6 -0
- package/src/publish/encrypted-export.ts +1 -1
- package/src/publish/export-attachments.ts +3 -0
- package/src/publish/export-service.ts +20 -0
- package/src/publish/identities.ts +153 -0
- package/src/serve/routes/api.ts +3 -1
- package/src/serve/server.ts +2 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.1.1.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 source version: **v2.
|
|
120
|
+
> Current source version: **v2.2.0**. See [CHANGELOG.md](./CHANGELOG.md).
|
|
121
121
|
|
|
122
122
|
<!-- /public-truth -->
|
|
123
123
|
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c2367d97e5599b41454becfa7d7e09ba9c1d5962096ce9e4f89b8794f9929f39 gno-browser-clipper-v2.2.0.zip
|
package/package.json
CHANGED
package/spec/cli.md
CHANGED
|
@@ -2313,6 +2313,18 @@ MUST fail without an artifact. Encrypted CLI export requires `--passphrase`;
|
|
|
2313
2313
|
the local API requires `encryptionPassphrase`. Neither sends that input to
|
|
2314
2314
|
gno.sh. Export is a local operation, not hosted activation or deletion.
|
|
2315
2315
|
|
|
2316
|
+
New exports MUST assign each note a random lowercase UUIDv4 persisted in
|
|
2317
|
+
`publish-identities.json` beside the resolved config file (`--config` and
|
|
2318
|
+
`GNO_CONFIG_DIR` apply). The private registry keys canonical collection roots
|
|
2319
|
+
and source-relative paths, outside the disposable index. The collection root
|
|
2320
|
+
MUST be accessible for canonical-path resolution. V1 notes emit `id`;
|
|
2321
|
+
V2 spaces emit `noteIds` matching decrypted reader cards’ `noteId` values.
|
|
2322
|
+
Legacy artifacts without IDs remain valid. Registry corruption or write failure
|
|
2323
|
+
MUST fail export rather than reset IDs. Content/title edits, published route/slug
|
|
2324
|
+
changes, and index rebuilds preserve IDs for unchanged source paths. Moving a
|
|
2325
|
+
source file or collection root starts a new identity; no move inference or
|
|
2326
|
+
source-Markdown mutation is performed. Registry paths MUST NOT enter artifacts.
|
|
2327
|
+
|
|
2316
2328
|
Public V1 spaces MUST carry a `manifest` conforming to
|
|
2317
2329
|
[`publish-artifact.schema.json`](./output-schemas/publish-artifact.schema.json).
|
|
2318
2330
|
The manifest contains schema version `1.0`, a deterministic projection
|
|
@@ -2342,7 +2354,8 @@ insufficient). Asset-free exports omit `assets` /
|
|
|
2342
2354
|
|
|
2343
2355
|
Secret-link and invite-only V1 spaces MUST NOT contain a manifest or agent
|
|
2344
2356
|
capability field. Encrypted V2 spaces MUST contain only ciphertext parameters,
|
|
2345
|
-
the opaque secret token, route slug, source type,
|
|
2357
|
+
the opaque secret token, route slug, source type, encrypted visibility, and an
|
|
2358
|
+
optional unique `noteIds` UUIDv4 roster (1–5000 IDs); no
|
|
2346
2359
|
plaintext manifest or evidence may appear outside the ciphertext. V2 builders
|
|
2347
2360
|
MUST emit a closed projection, validate payload strings as non-empty bounded
|
|
2348
2361
|
base64, require a positive safe-integer KDF iteration count, and bound the
|
|
@@ -173,11 +173,16 @@
|
|
|
173
173
|
]
|
|
174
174
|
}
|
|
175
175
|
},
|
|
176
|
+
"noteId": {
|
|
177
|
+
"type": "string",
|
|
178
|
+
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
179
|
+
},
|
|
176
180
|
"note": {
|
|
177
181
|
"type": "object",
|
|
178
182
|
"additionalProperties": false,
|
|
179
183
|
"required": ["markdown", "slug", "summary", "title"],
|
|
180
184
|
"properties": {
|
|
185
|
+
"id": { "$ref": "#/definitions/noteId" },
|
|
181
186
|
"markdown": { "type": "string" },
|
|
182
187
|
"metadata": { "$ref": "#/definitions/metadata" },
|
|
183
188
|
"slug": { "$ref": "#/definitions/slug" },
|
|
@@ -347,6 +352,13 @@
|
|
|
347
352
|
"visibility"
|
|
348
353
|
],
|
|
349
354
|
"properties": {
|
|
355
|
+
"noteIds": {
|
|
356
|
+
"type": "array",
|
|
357
|
+
"minItems": 1,
|
|
358
|
+
"maxItems": 5000,
|
|
359
|
+
"uniqueItems": true,
|
|
360
|
+
"items": { "$ref": "#/definitions/noteId" }
|
|
361
|
+
},
|
|
350
362
|
"encryptedPayload": {
|
|
351
363
|
"type": "object",
|
|
352
364
|
"additionalProperties": false,
|
|
@@ -98,6 +98,7 @@ export async function publishExport(
|
|
|
98
98
|
const { artifact, assetSummary, warnings } = await exportPublishArtifact({
|
|
99
99
|
collections,
|
|
100
100
|
options: {
|
|
101
|
+
configPath: initResult.actualConfigPath,
|
|
101
102
|
routeSlug: options.slug,
|
|
102
103
|
encryptionPassphrase: options.encryptionPassphrase,
|
|
103
104
|
summary: options.summary,
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { MAX_PUBLISH_UPLOAD_BYTES } from "./artifact-asset-contract";
|
|
8
8
|
|
|
9
|
+
export const PUBLISH_NOTE_ID_PATTERN =
|
|
10
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
11
|
+
|
|
9
12
|
export const MAX_PUBLISH_SLUG_LENGTH = 80;
|
|
10
13
|
/**
|
|
11
14
|
* Ciphertext base64 character ceiling aligned to the 100 MiB final-envelope
|
|
@@ -33,6 +36,7 @@ const SOURCE_TYPES = new Set(["collection", "note"]);
|
|
|
33
36
|
const READER_VISIBILITIES = new Set(["invite-only", "public", "secret-link"]);
|
|
34
37
|
|
|
35
38
|
export interface ValidatedPublishNote {
|
|
39
|
+
id?: string;
|
|
36
40
|
markdown: string;
|
|
37
41
|
metadata?: Record<string, string | string[]>;
|
|
38
42
|
slug: string;
|
|
@@ -51,6 +55,7 @@ export interface ValidatedPublishSpaceInput {
|
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
export interface ValidatedEncryptedPublishInput {
|
|
58
|
+
noteIds?: string[];
|
|
54
59
|
encryptedPayload: {
|
|
55
60
|
ciphertext: string;
|
|
56
61
|
iterations: number;
|
|
@@ -158,6 +163,21 @@ const projectMetadata = (
|
|
|
158
163
|
return result;
|
|
159
164
|
};
|
|
160
165
|
|
|
166
|
+
function requireNoteId(value: unknown): string {
|
|
167
|
+
if (typeof value !== "string" || !PUBLISH_NOTE_ID_PATTERN.test(value))
|
|
168
|
+
throw new Error("Publish note ID must be a lowercase UUIDv4");
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function projectNoteIds(value: unknown): string[] {
|
|
173
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 5000)
|
|
174
|
+
throw new Error("noteIds must contain 1 to 5000 UUIDv4 IDs");
|
|
175
|
+
const ids = value.map(requireNoteId);
|
|
176
|
+
if (new Set(ids).size !== ids.length)
|
|
177
|
+
throw new Error("Duplicate publish note ID");
|
|
178
|
+
return ids;
|
|
179
|
+
}
|
|
180
|
+
|
|
161
181
|
const projectNote = (value: unknown, index: number): ValidatedPublishNote => {
|
|
162
182
|
const field = `notes[${index}]`;
|
|
163
183
|
const input = requireRecord(value, field);
|
|
@@ -168,6 +188,7 @@ const projectNote = (value: unknown, index: number): ValidatedPublishNote => {
|
|
|
168
188
|
summary: requireString(input.summary, `${field}.summary`),
|
|
169
189
|
title: requireNonblankString(input.title, `${field}.title`),
|
|
170
190
|
};
|
|
191
|
+
if (input.id !== undefined) note.id = requireNoteId(input.id);
|
|
171
192
|
if (metadata !== undefined) note.metadata = metadata;
|
|
172
193
|
return note;
|
|
173
194
|
};
|
|
@@ -181,6 +202,9 @@ export const validateAndProjectPublishSpaceInput = (
|
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
const notes = input.notes.map(projectNote);
|
|
205
|
+
const ids = notes.flatMap((note) => (note.id ? [note.id] : []));
|
|
206
|
+
if (new Set(ids).size !== ids.length)
|
|
207
|
+
throw new Error("Duplicate publish note ID");
|
|
184
208
|
const noteSlugs = new Set<string>();
|
|
185
209
|
for (const note of notes) {
|
|
186
210
|
if (noteSlugs.has(note.slug)) {
|
|
@@ -246,6 +270,9 @@ export const validateAndProjectEncryptedPublishInput = (
|
|
|
246
270
|
}
|
|
247
271
|
|
|
248
272
|
return {
|
|
273
|
+
...(input.noteIds === undefined
|
|
274
|
+
? {}
|
|
275
|
+
: { noteIds: projectNoteIds(input.noteIds) }),
|
|
249
276
|
encryptedPayload: {
|
|
250
277
|
ciphertext: requireEncryptedCiphertext(payload.ciphertext),
|
|
251
278
|
iterations,
|
package/src/publish/artifact.ts
CHANGED
|
@@ -68,6 +68,7 @@ export type PublishVisibility =
|
|
|
68
68
|
| "secret-link";
|
|
69
69
|
|
|
70
70
|
export interface PublishArtifactNote {
|
|
71
|
+
id?: string;
|
|
71
72
|
markdown: string;
|
|
72
73
|
metadata?: Record<string, string | string[]>;
|
|
73
74
|
slug: string;
|
|
@@ -148,6 +149,7 @@ export interface EncryptedArtifactPayload {
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
export interface EncryptedPublishArtifactSpace {
|
|
152
|
+
noteIds?: string[];
|
|
151
153
|
encryptedPayload: EncryptedArtifactPayload;
|
|
152
154
|
routeSlug: string;
|
|
153
155
|
secretToken: string;
|
|
@@ -441,6 +443,7 @@ export const buildPublishArtifact = (input: {
|
|
|
441
443
|
};
|
|
442
444
|
|
|
443
445
|
export const buildEncryptedPublishArtifact = (input: {
|
|
446
|
+
noteIds?: string[];
|
|
444
447
|
egressLineage?: EgressLineage;
|
|
445
448
|
encryptedPayload: EncryptedArtifactPayload;
|
|
446
449
|
requiredCapabilities?: KnownPublishRequiredCapability[];
|
|
@@ -464,6 +467,9 @@ export const buildEncryptedPublishArtifact = (input: {
|
|
|
464
467
|
source: validated.routeSlug,
|
|
465
468
|
spaces: [
|
|
466
469
|
{
|
|
470
|
+
...(validated.noteIds === undefined
|
|
471
|
+
? {}
|
|
472
|
+
: { noteIds: validated.noteIds }),
|
|
467
473
|
encryptedPayload: validated.encryptedPayload,
|
|
468
474
|
routeSlug: validated.routeSlug,
|
|
469
475
|
secretToken: validated.secretToken,
|
|
@@ -183,7 +183,7 @@ const deriveReaderPayload = (input: {
|
|
|
183
183
|
const noteCards: ReaderNoteCard[] = input.notes.map((note) => {
|
|
184
184
|
const blocks = parseMarkdownBlocks(note.markdown);
|
|
185
185
|
return {
|
|
186
|
-
noteId: `${input.routeSlug}:${note.slug}`,
|
|
186
|
+
noteId: note.id ?? `${input.routeSlug}:${note.slug}`,
|
|
187
187
|
slug: note.slug,
|
|
188
188
|
title: note.title,
|
|
189
189
|
excerpt: deriveExcerpt(note.summary, blocks),
|
|
@@ -197,6 +197,9 @@ export async function finalizeEncryptedArtifact(input: {
|
|
|
197
197
|
const artifact = buildEncryptedPublishArtifact({
|
|
198
198
|
egressLineage: input.egressLineage,
|
|
199
199
|
encryptedPayload: encrypted.encryptedPayload,
|
|
200
|
+
...(input.notes.every((note) => note.id !== undefined)
|
|
201
|
+
? { noteIds: input.notes.map((note) => note.id!) }
|
|
202
|
+
: {}),
|
|
200
203
|
requiredCapabilities:
|
|
201
204
|
assets.length > 0 ? [BUNDLED_RASTER_ASSETS_CAPABILITY] : undefined,
|
|
202
205
|
routeSlug: input.routeSlug,
|
|
@@ -34,12 +34,15 @@ import {
|
|
|
34
34
|
sanitizeNoteMarkdown,
|
|
35
35
|
type NoteBuildAccumulator,
|
|
36
36
|
} from "./export-attachments";
|
|
37
|
+
import { resolvePublishNoteIds } from "./identities";
|
|
37
38
|
import {
|
|
38
39
|
isPublishDisabledByFrontmatter,
|
|
39
40
|
type SanitizeWarning,
|
|
40
41
|
} from "./obsidian-sanitize";
|
|
41
42
|
|
|
42
43
|
export interface PublishExportCoreOptions {
|
|
44
|
+
/** Actual loaded config file; defaults to the platform config directory. */
|
|
45
|
+
configPath?: string;
|
|
43
46
|
encryptionPassphrase?: string;
|
|
44
47
|
routeSlug?: string;
|
|
45
48
|
summary?: string;
|
|
@@ -199,6 +202,7 @@ async function exportCollectionArtifact(
|
|
|
199
202
|
preDedupRawBytes: 0,
|
|
200
203
|
};
|
|
201
204
|
const notes: PublishArtifactNote[] = [];
|
|
205
|
+
const sourceRelPaths: string[] = [];
|
|
202
206
|
|
|
203
207
|
for (const doc of activeDocs) {
|
|
204
208
|
if (!doc.mirrorHash) {
|
|
@@ -230,6 +234,7 @@ async function exportCollectionArtifact(
|
|
|
230
234
|
acc.externalCount += sanitized.externalCount;
|
|
231
235
|
acc.preDedupRawBytes += sanitized.preDedupRawBytes;
|
|
232
236
|
acc.encodedAssetBytes += mergePayloads(acc.payloads, sanitized.payloads);
|
|
237
|
+
sourceRelPaths.push(doc.relPath);
|
|
233
238
|
notes.push({
|
|
234
239
|
markdown: sanitized.markdown,
|
|
235
240
|
metadata: buildExportedMetadata(
|
|
@@ -268,6 +273,13 @@ async function exportCollectionArtifact(
|
|
|
268
273
|
store,
|
|
269
274
|
});
|
|
270
275
|
|
|
276
|
+
const noteIds = await resolvePublishNoteIds({
|
|
277
|
+
collectionRoot: collection.path,
|
|
278
|
+
sourceRelPaths,
|
|
279
|
+
configPath: options.configPath,
|
|
280
|
+
});
|
|
281
|
+
for (const [index, note] of notes.entries()) note.id = noteIds[index];
|
|
282
|
+
|
|
271
283
|
if (visibility === "encrypted") {
|
|
272
284
|
if (!options.encryptionPassphrase) {
|
|
273
285
|
throw new Error(
|
|
@@ -321,6 +333,8 @@ async function exportDocumentArtifact(
|
|
|
321
333
|
|
|
322
334
|
const collection =
|
|
323
335
|
collections.find((entry) => entry.name === doc.collection) ?? null;
|
|
336
|
+
if (!collection)
|
|
337
|
+
throw new Error(`Collection not configured: ${doc.collection}`);
|
|
324
338
|
const rawMarkdown = await loadDocumentMarkdown(store, doc);
|
|
325
339
|
if (isPublishDisabledByFrontmatter(rawMarkdown)) {
|
|
326
340
|
throw new Error(
|
|
@@ -362,7 +376,13 @@ async function exportDocumentArtifact(
|
|
|
362
376
|
store,
|
|
363
377
|
});
|
|
364
378
|
|
|
379
|
+
const [id] = await resolvePublishNoteIds({
|
|
380
|
+
collectionRoot: collection.path,
|
|
381
|
+
sourceRelPaths: [doc.relPath],
|
|
382
|
+
configPath: options.configPath,
|
|
383
|
+
});
|
|
365
384
|
const note: PublishArtifactNote = {
|
|
385
|
+
id,
|
|
366
386
|
markdown,
|
|
367
387
|
metadata: buildExportedMetadata(doc, frontmatter, tags),
|
|
368
388
|
slug,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Private durable publish identities, independent of the disposable index. */
|
|
2
|
+
// node:fs/promises — canonical paths, permissions and atomic rename have no Bun equivalents.
|
|
3
|
+
import {
|
|
4
|
+
chmod,
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
realpath,
|
|
8
|
+
rename,
|
|
9
|
+
unlink,
|
|
10
|
+
} from "node:fs/promises";
|
|
11
|
+
// node:path — Bun has no path utilities.
|
|
12
|
+
import { dirname, isAbsolute, join, normalize } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { getConfigPaths, toAbsolutePath } from "../config/paths";
|
|
15
|
+
import { acquireWriteLock, type WriteLockHandle } from "../core/file-lock";
|
|
16
|
+
import { PUBLISH_NOTE_ID_PATTERN } from "./artifact-validation";
|
|
17
|
+
|
|
18
|
+
const IDENTITY_LOCK_TIMEOUT_MS = 5000;
|
|
19
|
+
const IDENTITY_LOCK_RETRY_MS = 25;
|
|
20
|
+
|
|
21
|
+
type Registry = { version: 1; identities: Record<string, string> };
|
|
22
|
+
|
|
23
|
+
export function publishIdentityRegistryPath(configPath?: string): string {
|
|
24
|
+
return join(
|
|
25
|
+
configPath
|
|
26
|
+
? dirname(toAbsolutePath(configPath))
|
|
27
|
+
: getConfigPaths().configDir,
|
|
28
|
+
"publish-identities.json"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateRegistry(value: unknown): Registry {
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
34
|
+
throw new Error("Invalid registry object");
|
|
35
|
+
const record = value as Record<string, unknown>;
|
|
36
|
+
if (
|
|
37
|
+
record.version !== 1 ||
|
|
38
|
+
Object.keys(record).length !== 2 ||
|
|
39
|
+
!record.identities ||
|
|
40
|
+
typeof record.identities !== "object" ||
|
|
41
|
+
Array.isArray(record.identities)
|
|
42
|
+
)
|
|
43
|
+
throw new Error("Invalid registry version or identities");
|
|
44
|
+
const identities = record.identities as Record<string, unknown>;
|
|
45
|
+
const ids = new Set<string>();
|
|
46
|
+
for (const [key, id] of Object.entries(identities)) {
|
|
47
|
+
const source: unknown = JSON.parse(key);
|
|
48
|
+
if (
|
|
49
|
+
!Array.isArray(source) ||
|
|
50
|
+
source.length !== 2 ||
|
|
51
|
+
!source.every((part) => typeof part === "string" && part.length > 0) ||
|
|
52
|
+
typeof id !== "string" ||
|
|
53
|
+
!PUBLISH_NOTE_ID_PATTERN.test(id) ||
|
|
54
|
+
ids.has(id)
|
|
55
|
+
)
|
|
56
|
+
throw new Error("Invalid or duplicate registry identity");
|
|
57
|
+
ids.add(id);
|
|
58
|
+
}
|
|
59
|
+
return value as Registry;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readRegistry(path: string): Promise<Registry> {
|
|
63
|
+
try {
|
|
64
|
+
const info = await lstat(path);
|
|
65
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
66
|
+
throw new Error("Registry must be a regular private file");
|
|
67
|
+
await chmod(path, 0o600);
|
|
68
|
+
return validateRegistry(await Bun.file(path).json());
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
71
|
+
return { version: 1, identities: {} };
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Cannot read publish identity registry; restore a valid backup before exporting",
|
|
74
|
+
{ cause: error }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function acquireIdentityLock(path: string): Promise<WriteLockHandle> {
|
|
80
|
+
const deadline = performance.now() + IDENTITY_LOCK_TIMEOUT_MS;
|
|
81
|
+
// Zero-wait attempts avoid blocking the JS holder on SQLite's busy timeout.
|
|
82
|
+
// flock -w 0 and lockf -t 0 also mean immediate acquisition or failure.
|
|
83
|
+
let lock = await acquireWriteLock(path, 0);
|
|
84
|
+
while (!lock) {
|
|
85
|
+
const remaining = deadline - performance.now();
|
|
86
|
+
if (remaining <= 0)
|
|
87
|
+
throw new Error("Publish identity registry is busy; retry the export");
|
|
88
|
+
await Bun.sleep(Math.min(IDENTITY_LOCK_RETRY_MS, remaining));
|
|
89
|
+
lock = await acquireWriteLock(path, 0);
|
|
90
|
+
}
|
|
91
|
+
return lock;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Allocate a batch under one OS lock; publish IDs only after the private file commits. */
|
|
95
|
+
export async function resolvePublishNoteIds(input: {
|
|
96
|
+
collectionRoot: string;
|
|
97
|
+
sourceRelPaths: string[];
|
|
98
|
+
configPath?: string;
|
|
99
|
+
}): Promise<string[]> {
|
|
100
|
+
let root: string;
|
|
101
|
+
try {
|
|
102
|
+
root = await realpath(toAbsolutePath(input.collectionRoot));
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"Collection root must exist and be accessible to resolve publish identities",
|
|
106
|
+
{ cause }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const keys = input.sourceRelPaths.map((source) => {
|
|
110
|
+
const rel = normalize(source).replaceAll("\\", "/");
|
|
111
|
+
if (
|
|
112
|
+
!rel ||
|
|
113
|
+
rel === "." ||
|
|
114
|
+
rel === ".." ||
|
|
115
|
+
rel.startsWith("../") ||
|
|
116
|
+
isAbsolute(rel) ||
|
|
117
|
+
rel.includes("\0")
|
|
118
|
+
)
|
|
119
|
+
throw new Error(
|
|
120
|
+
"Publish source path must be relative to its collection root"
|
|
121
|
+
);
|
|
122
|
+
return JSON.stringify([root, rel]);
|
|
123
|
+
});
|
|
124
|
+
const path = publishIdentityRegistryPath(input.configPath);
|
|
125
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
126
|
+
const lock = await acquireIdentityLock(`${path}.lock`);
|
|
127
|
+
const temporaryPath = `${path}.tmp.${crypto.randomUUID()}`;
|
|
128
|
+
try {
|
|
129
|
+
const registry = await readRegistry(path);
|
|
130
|
+
let changed = false;
|
|
131
|
+
const ids = keys.map((key) => {
|
|
132
|
+
const existing = registry.identities[key];
|
|
133
|
+
if (existing) return existing;
|
|
134
|
+
const id = crypto.randomUUID();
|
|
135
|
+
registry.identities[key] = id;
|
|
136
|
+
changed = true;
|
|
137
|
+
return id;
|
|
138
|
+
});
|
|
139
|
+
if (changed) {
|
|
140
|
+
await Bun.write(temporaryPath, JSON.stringify(registry), { mode: 0o600 });
|
|
141
|
+
await rename(temporaryPath, path);
|
|
142
|
+
}
|
|
143
|
+
return ids;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"Publish identity registry update failed; export aborted to preserve note identity",
|
|
147
|
+
{ cause: error }
|
|
148
|
+
);
|
|
149
|
+
} finally {
|
|
150
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
151
|
+
await lock.release();
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -1266,7 +1266,8 @@ export async function handleEgressAuditPurge(
|
|
|
1266
1266
|
export async function handlePublishExport(
|
|
1267
1267
|
config: Config,
|
|
1268
1268
|
store: SqliteAdapter,
|
|
1269
|
-
req: Request
|
|
1269
|
+
req: Request,
|
|
1270
|
+
configPath?: string
|
|
1270
1271
|
): Promise<Response> {
|
|
1271
1272
|
let body: PublishExportRequestBody;
|
|
1272
1273
|
try {
|
|
@@ -1304,6 +1305,7 @@ export async function handlePublishExport(
|
|
|
1304
1305
|
const { artifact, assetSummary, warnings } = await exportPublishArtifact({
|
|
1305
1306
|
collections: config.collections,
|
|
1306
1307
|
options: {
|
|
1308
|
+
configPath,
|
|
1307
1309
|
encryptionPassphrase: body.encryptionPassphrase,
|
|
1308
1310
|
routeSlug: body.slug,
|
|
1309
1311
|
summary: body.summary,
|
package/src/serve/server.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
bf5fc1ea3dd699217e270114051b19e88b239265b3a33e395f5b5ba753bc9e47 gno-browser-clipper-v2.1.1.zip
|