@xnetjs/data 2.5.0 → 3.0.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 (41) hide show
  1. package/README.md +5 -0
  2. package/dist/auth/index.d.ts +4 -4
  3. package/dist/chunk-GJAOMEQO.js +114 -0
  4. package/dist/chunk-KY62NUOI.js +67 -0
  5. package/dist/chunk-N37U6ZPI.js +789 -0
  6. package/dist/chunk-NFPEFD2I.js +42 -0
  7. package/dist/{chunk-ODOPVGUX.js → chunk-QKVIKVAL.js} +65 -26
  8. package/dist/{chunk-CY6EJUNV.js → chunk-S7SEN4LH.js} +5 -1
  9. package/dist/{chunk-4HJZJMPI.js → chunk-TN7LGV66.js} +1 -1
  10. package/dist/{chunk-YRFCXKJC.js → chunk-UQ2VFHU3.js} +2 -2
  11. package/dist/chunk-VZSEZNYA.js +83 -0
  12. package/dist/{chunk-JF4VCZER.js → chunk-WSINZHSR.js} +27 -1
  13. package/dist/{chunk-MAB662PR.js → chunk-YCBJDT23.js} +62 -48
  14. package/dist/{clone-VRVODkBn.d.ts → clone-ywzFVkID.d.ts} +4 -4
  15. package/dist/course-C5DXOIVO.js +24 -0
  16. package/dist/database/index.d.ts +5 -5
  17. package/dist/database/index.js +8 -8
  18. package/dist/event-VW3FBN4F.js +20 -0
  19. package/dist/{form-types-Dkqm9UO2.d.ts → form-types-BrHYdpi0.d.ts} +1 -1
  20. package/dist/{game-NEQP7OFX.js → game-7AXW7XKS.js} +1 -1
  21. package/dist/{grant-expiration-cleaner-CTsW0hvU.d.ts → grant-expiration-cleaner-CRS9OyhV.d.ts} +3 -3
  22. package/dist/index.d.ts +10 -320
  23. package/dist/index.js +292 -867
  24. package/dist/{moderation-HRMMBFVH.js → moderation-5Z4LKOKN.js} +5 -3
  25. package/dist/{page-MADZ5OHM.js → page-W4G6433H.js} +1 -1
  26. package/dist/portability/index.d.ts +330 -0
  27. package/dist/portability/index.js +34 -0
  28. package/dist/post-7IOO64MD.js +14 -0
  29. package/dist/{profile-5H6XWCK4.js → profile-KLHCAQY2.js} +1 -1
  30. package/dist/publication-3MN3WWKP.js +10 -0
  31. package/dist/{query-ast-h31svZnG.d.ts → query-ast-DQJO7yRV.d.ts} +1 -1
  32. package/dist/{registry-Dh4Kr3fo.d.ts → registry-C9IRLLeR.d.ts} +1 -1
  33. package/dist/schema/index.d.ts +366 -26
  34. package/dist/schema/index.js +195 -149
  35. package/dist/space-GBTEHZOK.js +1 -1
  36. package/dist/store/index.d.ts +4 -4
  37. package/dist/store/index.js +2 -2
  38. package/dist/{store-jukJYsvr.d.ts → store-D1oiFQuF.d.ts} +19 -2
  39. package/dist/{types-Z9dUzbDF.d.ts → types-gws1tSf-.d.ts} +1 -1
  40. package/package.json +27 -25
  41. package/dist/{chunk-TW5XJHNF.js → chunk-ANMAR3KY.js} +14 -14
@@ -0,0 +1,789 @@
1
+ // src/portability/types.ts
2
+ var XNETPACK_FORMAT_VERSION = "xnetpack/1";
3
+ var BUNDLE_ENTRY = {
4
+ manifest: "manifest.json",
5
+ changes: "changes.ndjson",
6
+ /** Batch commits covering the owner's own changes (exploration 0357). */
7
+ commits: "commits.ndjson",
8
+ blobIndex: "blobs.ndjson",
9
+ yjsDocs: "yjs/docs.ndjson"
10
+ };
11
+ var FRONTIER_HEADS_CAP = 32;
12
+
13
+ // src/portability/serialize.ts
14
+ import { bytesToBase64, base64ToBytes, hash, hashHex } from "@xnetjs/crypto";
15
+ var textEncoder = new TextEncoder();
16
+ var textDecoder = new TextDecoder();
17
+ function toPortableChangeRecord(change) {
18
+ const record = {
19
+ id: change.id,
20
+ type: change.type,
21
+ hash: change.hash,
22
+ nodeId: change.payload.nodeId,
23
+ schemaId: change.payload.schemaId,
24
+ lamportTime: change.lamport,
25
+ authorDid: change.authorDID,
26
+ wallTime: change.wallTime,
27
+ parentHash: change.parentHash,
28
+ payload: change.payload,
29
+ signatureB64: bytesToBase64(change.signature),
30
+ // protocolVersion is part of the hashed fields — dropping it makes the
31
+ // record fail verifyChangeHash on the importing side.
32
+ protocolVersion: change.protocolVersion,
33
+ batchId: change.batchId,
34
+ batchIndex: change.batchIndex,
35
+ batchSize: change.batchSize
36
+ };
37
+ return record;
38
+ }
39
+ function fromPortableChangeRecord(record) {
40
+ const payload = record.payload && !record.payload.schemaId && record.schemaId ? { ...record.payload, schemaId: record.schemaId } : record.payload;
41
+ return {
42
+ id: record.id,
43
+ type: record.type,
44
+ hash: record.hash,
45
+ parentHash: record.parentHash,
46
+ authorDID: record.authorDid,
47
+ signature: base64ToBytes(record.signatureB64),
48
+ wallTime: record.wallTime,
49
+ lamport: record.lamportTime,
50
+ payload,
51
+ protocolVersion: record.protocolVersion,
52
+ batchId: record.batchId,
53
+ batchIndex: record.batchIndex,
54
+ batchSize: record.batchSize
55
+ };
56
+ }
57
+ function sortObjectKeys(obj) {
58
+ if (obj === null || typeof obj !== "object") return obj;
59
+ if (Array.isArray(obj)) return obj.map(sortObjectKeys);
60
+ const sorted = {};
61
+ for (const key of Object.keys(obj).sort()) {
62
+ sorted[key] = sortObjectKeys(obj[key]);
63
+ }
64
+ return sorted;
65
+ }
66
+ function canonicalManifestBytes(manifest) {
67
+ const unsigned = { ...manifest };
68
+ delete unsigned.signatureB64;
69
+ return textEncoder.encode(JSON.stringify(sortObjectKeys(unsigned)));
70
+ }
71
+ function createNdjsonDigest() {
72
+ const lineDigests = [];
73
+ return {
74
+ addLine(line) {
75
+ lineDigests.push(hash(textEncoder.encode(line)));
76
+ },
77
+ finish() {
78
+ const concat = new Uint8Array(lineDigests.length * 32);
79
+ lineDigests.forEach((d, i) => concat.set(d, i * 32));
80
+ return hashHex(concat);
81
+ },
82
+ lineCount() {
83
+ return lineDigests.length;
84
+ }
85
+ };
86
+ }
87
+ function digestEntryBytes(bytes) {
88
+ return hashHex(bytes);
89
+ }
90
+ function combineEntryDigests(entries) {
91
+ const lines = [...entries.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([path, digest]) => `${path}
92
+ ${digest}
93
+ `);
94
+ return hashHex(textEncoder.encode(lines.join("")));
95
+ }
96
+ function encodeNdjsonLine(value) {
97
+ return JSON.stringify(value);
98
+ }
99
+ function decodeUtf8(bytes) {
100
+ return textDecoder.decode(bytes);
101
+ }
102
+ function encodeUtf8(text) {
103
+ return textEncoder.encode(text);
104
+ }
105
+
106
+ // src/portability/write.ts
107
+ import { compareChangeApplicationOrder } from "@xnetjs/core";
108
+ import { bytesToBase64 as bytesToBase642 } from "@xnetjs/crypto";
109
+ import {
110
+ CURRENT_PROTOCOL_VERSION,
111
+ chunkForCommits,
112
+ computeBatchCommitHash,
113
+ createUnsignedBatchCommit
114
+ } from "@xnetjs/sync";
115
+ function blobEntryPath(cid) {
116
+ const match = /^cid:([a-z0-9-]+):([0-9a-f]+)$/i.exec(cid);
117
+ if (match) return `blobs/${match[1]}/${match[2]}`;
118
+ return `blobs/raw/${encodeURIComponent(cid)}`;
119
+ }
120
+ function frontierOf(changes) {
121
+ let lamport = 0;
122
+ for (const change of changes) if (change.lamport > lamport) lamport = change.lamport;
123
+ const heads = changes.filter((c) => c.lamport === lamport).slice(0, FRONTIER_HEADS_CAP).map((c) => c.hash);
124
+ return { lamport, heads, changeCount: changes.length };
125
+ }
126
+ function spaceOf(properties) {
127
+ return properties["space"];
128
+ }
129
+ async function collectScopedNodeIds(store, scope) {
130
+ if (scope.kind === "nodes") return [...scope.nodeIds];
131
+ if (scope.kind === "schemas") {
132
+ const ids2 = [];
133
+ for (const schemaId of scope.schemaIds) {
134
+ const nodes = await store.list({ schemaId, includeDeleted: true });
135
+ for (const node of nodes) ids2.push(node.id);
136
+ }
137
+ return ids2;
138
+ }
139
+ if (scope.kind !== "space") return [];
140
+ const all = await store.list({ includeDeleted: true });
141
+ const ids = [];
142
+ for (const node of all) {
143
+ if (node.id === scope.spaceId) {
144
+ ids.push(node.id);
145
+ continue;
146
+ }
147
+ const space = spaceOf(node.properties);
148
+ if (space === scope.spaceId) ids.push(node.id);
149
+ else if (Array.isArray(space) && space.includes(scope.spaceId)) ids.push(node.id);
150
+ }
151
+ return ids;
152
+ }
153
+ async function collectChanges(store, scope, since) {
154
+ let changes;
155
+ if (scope.kind === "full") {
156
+ changes = since ? await store.getChangesSince(since.lamport) : await store.getAllChanges();
157
+ } else {
158
+ const nodeIds = await collectScopedNodeIds(store, scope);
159
+ const byHash = /* @__PURE__ */ new Map();
160
+ for (const nodeId of nodeIds) {
161
+ for (const change of await store.getChanges(nodeId)) {
162
+ if (!since || change.lamport > since.lamport) byHash.set(change.hash, change);
163
+ }
164
+ }
165
+ changes = [...byHash.values()];
166
+ }
167
+ return changes.sort(
168
+ (a, b) => compareChangeApplicationOrder(
169
+ { lamport: a.lamport, author: a.authorDID },
170
+ { lamport: b.lamport, author: b.authorDID }
171
+ )
172
+ );
173
+ }
174
+ async function writeBundle(store, scope, sink, options) {
175
+ const entryDigests = /* @__PURE__ */ new Map();
176
+ const changes = await collectChanges(store, scope, options.since);
177
+ const changesDigest = createNdjsonDigest();
178
+ const changeLines = [];
179
+ for (const change of changes) {
180
+ const line = encodeNdjsonLine(toPortableChangeRecord(change));
181
+ changesDigest.addLine(line);
182
+ changeLines.push(line);
183
+ }
184
+ await sink.writeEntry(
185
+ BUNDLE_ENTRY.changes,
186
+ encodeUtf8(changeLines.join("\n") + (changeLines.length ? "\n" : ""))
187
+ );
188
+ entryDigests.set(BUNDLE_ENTRY.changes, changesDigest.finish());
189
+ const commitLines = [];
190
+ const commitsDigest = createNdjsonDigest();
191
+ let commitCount = 0;
192
+ if (options.commitSigner) {
193
+ const ownChanges = changes.filter((change) => change.authorDID === options.ownerDid);
194
+ let commitIndex = 0;
195
+ for (const group of chunkForCommits(ownChanges)) {
196
+ const unsigned = createUnsignedBatchCommit({
197
+ id: `${options.ownerDid}:commit:${commitIndex++}`,
198
+ authorDID: options.ownerDid,
199
+ changeHashes: group.map((change) => change.hash),
200
+ // Commits order after every change they cover, so a consumer replaying
201
+ // in lamport order sees the members first.
202
+ lamport: group[group.length - 1].lamport,
203
+ wallTime: group[group.length - 1].wallTime
204
+ });
205
+ const hash2 = computeBatchCommitHash(unsigned);
206
+ const signature = await options.commitSigner(encodeUtf8(hash2));
207
+ const record = {
208
+ id: unsigned.id,
209
+ type: "batch-commit",
210
+ protocolVersion: unsigned.protocolVersion,
211
+ authorDid: unsigned.authorDID,
212
+ changeHashes: unsigned.changeHashes,
213
+ root: unsigned.root,
214
+ lamportTime: unsigned.lamport,
215
+ wallTime: unsigned.wallTime,
216
+ hash: hash2,
217
+ signatureB64: bytesToBase642(signature)
218
+ };
219
+ const line = encodeNdjsonLine(record);
220
+ commitsDigest.addLine(line);
221
+ commitLines.push(line);
222
+ commitCount++;
223
+ }
224
+ }
225
+ await sink.writeEntry(
226
+ BUNDLE_ENTRY.commits,
227
+ encodeUtf8(commitLines.join("\n") + (commitLines.length ? "\n" : ""))
228
+ );
229
+ entryDigests.set(BUNDLE_ENTRY.commits, commitsDigest.finish());
230
+ let blobCount = 0;
231
+ const blobIndexDigest = createNdjsonDigest();
232
+ const blobIndexLines = [];
233
+ if (options.blobPort) {
234
+ for await (const blob of options.blobPort.list()) {
235
+ const path = blobEntryPath(blob.cid);
236
+ await sink.writeEntry(path, blob.bytes);
237
+ entryDigests.set(path, digestEntryBytes(blob.bytes));
238
+ const record = {
239
+ cid: blob.cid,
240
+ path,
241
+ size: blob.bytes.byteLength,
242
+ mimeType: blob.mimeType
243
+ };
244
+ const line = encodeNdjsonLine(record);
245
+ blobIndexDigest.addLine(line);
246
+ blobIndexLines.push(line);
247
+ blobCount++;
248
+ }
249
+ }
250
+ await sink.writeEntry(
251
+ BUNDLE_ENTRY.blobIndex,
252
+ encodeUtf8(blobIndexLines.join("\n") + (blobIndexLines.length ? "\n" : ""))
253
+ );
254
+ entryDigests.set(BUNDLE_ENTRY.blobIndex, blobIndexDigest.finish());
255
+ let yjsCount = 0;
256
+ const yjsDigest = createNdjsonDigest();
257
+ const yjsLines = [];
258
+ if (options.yjsPort) {
259
+ for await (const doc of options.yjsPort.list()) {
260
+ const line = encodeNdjsonLine({ nodeId: doc.nodeId, updateB64: bytesToBase642(doc.update) });
261
+ yjsDigest.addLine(line);
262
+ yjsLines.push(line);
263
+ yjsCount++;
264
+ }
265
+ }
266
+ await sink.writeEntry(
267
+ BUNDLE_ENTRY.yjsDocs,
268
+ encodeUtf8(yjsLines.join("\n") + (yjsLines.length ? "\n" : ""))
269
+ );
270
+ entryDigests.set(BUNDLE_ENTRY.yjsDocs, yjsDigest.finish());
271
+ const manifest = {
272
+ formatVersion: XNETPACK_FORMAT_VERSION,
273
+ protocolVersion: { change: CURRENT_PROTOCOL_VERSION },
274
+ ownerDid: options.ownerDid,
275
+ scope,
276
+ createdAt: Date.now(),
277
+ frontier: frontierOf(changes),
278
+ prerequisites: options.since,
279
+ counts: {
280
+ changes: changes.length,
281
+ blobs: blobCount,
282
+ yjsDocs: yjsCount,
283
+ commits: commitCount
284
+ },
285
+ contentDigest: combineEntryDigests(entryDigests)
286
+ };
287
+ if (options.manifestSigner) {
288
+ const signature = await options.manifestSigner(canonicalManifestBytes(manifest));
289
+ manifest.signatureB64 = bytesToBase642(signature);
290
+ }
291
+ await sink.writeEntry(BUNDLE_ENTRY.manifest, encodeUtf8(JSON.stringify(manifest, null, 2)));
292
+ return manifest;
293
+ }
294
+
295
+ // src/portability/verify.ts
296
+ import { base64ToBytes as base64ToBytes2, verify as verifySignature } from "@xnetjs/crypto";
297
+ import { parseDID } from "@xnetjs/identity";
298
+ import { verifyChangeFast, verifyChangeHash, CURRENT_PROTOCOL_VERSION as CURRENT_PROTOCOL_VERSION2 } from "@xnetjs/sync";
299
+ function parseCidHex(path) {
300
+ const match = /^blobs\/([a-z0-9-]+)\/([0-9a-f]+)$/i.exec(path);
301
+ if (!match || match[1] === "raw") return null;
302
+ return { algo: match[1], hex: match[2] };
303
+ }
304
+ async function verifyBundle(source, options = {}) {
305
+ const verifyChangeSignatures = options.verifyChangeSignatures ?? true;
306
+ const issues = [];
307
+ const error = (code, detail, subject) => issues.push({ severity: "error", code, detail, subject });
308
+ const warning = (code, detail, subject) => issues.push({ severity: "warning", code, detail, subject });
309
+ const manifestBytes = await source.readEntry(BUNDLE_ENTRY.manifest);
310
+ if (!manifestBytes) {
311
+ error("unknown-format", "bundle has no manifest.json");
312
+ return { ok: false, manifest: null, issues, danglingParents: 0 };
313
+ }
314
+ let manifest;
315
+ try {
316
+ manifest = JSON.parse(decodeUtf8(manifestBytes));
317
+ } catch (err) {
318
+ error("unknown-format", `manifest.json is not valid JSON: ${err.message}`);
319
+ return { ok: false, manifest: null, issues, danglingParents: 0 };
320
+ }
321
+ if (manifest.formatVersion !== XNETPACK_FORMAT_VERSION) {
322
+ error("unknown-format", `unsupported formatVersion "${manifest.formatVersion}"`);
323
+ return { ok: false, manifest, issues, danglingParents: 0 };
324
+ }
325
+ if ((manifest.protocolVersion?.change ?? 0) > CURRENT_PROTOCOL_VERSION2) {
326
+ error(
327
+ "future-protocol",
328
+ `bundle uses change protocol v${manifest.protocolVersion.change}, this build supports v${CURRENT_PROTOCOL_VERSION2}`
329
+ );
330
+ }
331
+ if (!manifest.signatureB64) {
332
+ warning("manifest-unsigned", "manifest carries no signature");
333
+ } else {
334
+ try {
335
+ const publicKey = parseDID(manifest.ownerDid);
336
+ const signature = base64ToBytes2(manifest.signatureB64);
337
+ if (!verifySignature(canonicalManifestBytes(manifest), signature, publicKey)) {
338
+ error(
339
+ "manifest-signature-invalid",
340
+ `manifest signature does not verify against ${manifest.ownerDid}`
341
+ );
342
+ }
343
+ } catch (err) {
344
+ error(
345
+ "manifest-signature-invalid",
346
+ `cannot verify manifest signature: ${err.message}`
347
+ );
348
+ }
349
+ }
350
+ const entryDigests = /* @__PURE__ */ new Map();
351
+ const changesDigest = createNdjsonDigest();
352
+ const presentHashes = /* @__PURE__ */ new Set();
353
+ const parentHashes = [];
354
+ let lineNumber = 0;
355
+ for await (const line of source.readLines(BUNDLE_ENTRY.changes)) {
356
+ lineNumber++;
357
+ changesDigest.addLine(line);
358
+ let record;
359
+ try {
360
+ record = JSON.parse(line);
361
+ } catch {
362
+ error(
363
+ "change-unparseable",
364
+ `changes.ndjson line ${lineNumber} is not valid JSON`,
365
+ `line:${lineNumber}`
366
+ );
367
+ continue;
368
+ }
369
+ let change;
370
+ try {
371
+ change = fromPortableChangeRecord(record);
372
+ } catch (err) {
373
+ error("change-unparseable", `line ${lineNumber}: ${err.message}`, record.hash);
374
+ continue;
375
+ }
376
+ if (verifyChangeSignatures) {
377
+ if (!verifyChangeHash(change)) {
378
+ error(
379
+ "change-hash-invalid",
380
+ `change ${change.id} fails hash re-computation (tampered?)`,
381
+ record.hash
382
+ );
383
+ continue;
384
+ }
385
+ try {
386
+ const publicKey = parseDID(change.authorDID);
387
+ if (!await verifyChangeFast(change, publicKey)) {
388
+ error(
389
+ "change-signature-invalid",
390
+ `change ${change.id} signature does not match ${change.authorDID}`,
391
+ record.hash
392
+ );
393
+ continue;
394
+ }
395
+ } catch (err) {
396
+ error(
397
+ "change-signature-invalid",
398
+ `change ${change.id}: ${err.message}`,
399
+ record.hash
400
+ );
401
+ continue;
402
+ }
403
+ }
404
+ presentHashes.add(change.hash);
405
+ parentHashes.push(change.parentHash);
406
+ }
407
+ entryDigests.set(BUNDLE_ENTRY.changes, changesDigest.finish());
408
+ if (changesDigest.lineCount() !== manifest.counts.changes) {
409
+ error(
410
+ "count-mismatch",
411
+ `manifest declares ${manifest.counts.changes} changes, bundle has ${changesDigest.lineCount()}`
412
+ );
413
+ }
414
+ const prerequisiteHeads = new Set(manifest.prerequisites?.heads ?? []);
415
+ let danglingParents = 0;
416
+ for (const parent of parentHashes) {
417
+ if (parent !== null && !presentHashes.has(parent) && !prerequisiteHeads.has(parent)) {
418
+ danglingParents++;
419
+ }
420
+ }
421
+ if (danglingParents > 0) {
422
+ warning(
423
+ "dangling-parent",
424
+ `${danglingParents} change(s) reference parents outside the bundle (expected for scoped/incremental exports)`
425
+ );
426
+ }
427
+ const commitsDigest = createNdjsonDigest();
428
+ try {
429
+ for await (const line of source.readLines(BUNDLE_ENTRY.commits)) {
430
+ commitsDigest.addLine(line);
431
+ }
432
+ entryDigests.set(BUNDLE_ENTRY.commits, commitsDigest.finish());
433
+ const declaredCommits = manifest.counts.commits ?? 0;
434
+ if (commitsDigest.lineCount() !== declaredCommits) {
435
+ error(
436
+ "count-mismatch",
437
+ `manifest declares ${declaredCommits} batch commits, bundle has ${commitsDigest.lineCount()}`
438
+ );
439
+ }
440
+ } catch {
441
+ }
442
+ const blobIndexDigest = createNdjsonDigest();
443
+ const indexedPaths = /* @__PURE__ */ new Set();
444
+ for await (const line of source.readLines(BUNDLE_ENTRY.blobIndex)) {
445
+ blobIndexDigest.addLine(line);
446
+ try {
447
+ const record = JSON.parse(line);
448
+ indexedPaths.add(record.path);
449
+ } catch {
450
+ error("change-unparseable", "blobs.ndjson contains an unparseable line");
451
+ }
452
+ }
453
+ entryDigests.set(BUNDLE_ENTRY.blobIndex, blobIndexDigest.finish());
454
+ if (blobIndexDigest.lineCount() !== manifest.counts.blobs) {
455
+ error(
456
+ "count-mismatch",
457
+ `manifest declares ${manifest.counts.blobs} blobs, index has ${blobIndexDigest.lineCount()}`
458
+ );
459
+ }
460
+ for (const path of await source.listEntries("blobs/")) {
461
+ if (path === BUNDLE_ENTRY.blobIndex) continue;
462
+ const bytes = await source.readEntry(path);
463
+ if (!bytes) continue;
464
+ entryDigests.set(path, digestEntryBytes(bytes));
465
+ const cid = parseCidHex(path);
466
+ if (cid && cid.algo === "blake3" && digestEntryBytes(bytes) !== cid.hex.toLowerCase()) {
467
+ error("blob-digest-mismatch", `blob ${path} bytes do not hash to their filename`, path);
468
+ }
469
+ if (!indexedPaths.has(path)) {
470
+ warning("count-mismatch", `blob entry ${path} is not listed in blobs.ndjson`, path);
471
+ }
472
+ }
473
+ const yjsDigest = createNdjsonDigest();
474
+ for await (const line of source.readLines(BUNDLE_ENTRY.yjsDocs)) {
475
+ yjsDigest.addLine(line);
476
+ }
477
+ entryDigests.set(BUNDLE_ENTRY.yjsDocs, yjsDigest.finish());
478
+ if (yjsDigest.lineCount() !== manifest.counts.yjsDocs) {
479
+ error(
480
+ "count-mismatch",
481
+ `manifest declares ${manifest.counts.yjsDocs} yjs docs, bundle has ${yjsDigest.lineCount()}`
482
+ );
483
+ }
484
+ if (combineEntryDigests(entryDigests) !== manifest.contentDigest) {
485
+ error("content-digest-mismatch", "entry digests do not match manifest.contentDigest");
486
+ }
487
+ return {
488
+ ok: !issues.some((i) => i.severity === "error"),
489
+ manifest,
490
+ issues,
491
+ danglingParents
492
+ };
493
+ }
494
+
495
+ // src/portability/apply.ts
496
+ import { compareChangeApplicationOrder as compareChangeApplicationOrder2 } from "@xnetjs/core";
497
+ import { base64ToBytes as base64ToBytes3 } from "@xnetjs/crypto";
498
+ import { parseDID as parseDID2 } from "@xnetjs/identity";
499
+ import { recomputeChangeHash, verifyBatchCommitFast } from "@xnetjs/sync";
500
+ var BundleImportError = class extends Error {
501
+ constructor(message, code) {
502
+ super(message);
503
+ this.code = code;
504
+ this.name = "BundleImportError";
505
+ }
506
+ };
507
+ var APPLY_BATCH_SIZE = 500;
508
+ async function applyBundle(store, source, options) {
509
+ const verifyReport = await verifyBundle(source, { verifyChangeSignatures: false });
510
+ if (!verifyReport.ok || !verifyReport.manifest) {
511
+ const details = verifyReport.issues.filter((i) => i.severity === "error").map((i) => i.detail).join("; ");
512
+ throw new BundleImportError(`bundle failed verification: ${details}`, "verify-failed");
513
+ }
514
+ const manifest = verifyReport.manifest;
515
+ if (!manifest.signatureB64 && !options.allowUnsigned) {
516
+ throw new BundleImportError(
517
+ "bundle manifest is unsigned \u2014 pass allowUnsigned to import anyway",
518
+ "unsigned-manifest"
519
+ );
520
+ }
521
+ if (manifest.ownerDid !== options.importerDid && !options.allowForeignOwner) {
522
+ throw new BundleImportError(
523
+ `bundle is owned by ${manifest.ownerDid} but the importer is ${options.importerDid} \u2014 pass allowForeignOwner to import a bundle from another identity`,
524
+ "foreign-owner"
525
+ );
526
+ }
527
+ const missingPrerequisites = [];
528
+ if (manifest.prerequisites) {
529
+ for (const head of manifest.prerequisites.heads) {
530
+ if (!await store.hasChange(head)) missingPrerequisites.push(head);
531
+ }
532
+ if (missingPrerequisites.length > 0 && !options.ignoreMissingPrerequisites) {
533
+ throw new BundleImportError(
534
+ `store is missing ${missingPrerequisites.length} prerequisite head(s) \u2014 this incremental bundle needs its base bundle imported first (or pass ignoreMissingPrerequisites)`,
535
+ "missing-prerequisites"
536
+ );
537
+ }
538
+ }
539
+ const quarantined = [];
540
+ const quarantine = (record) => {
541
+ quarantined.push(record);
542
+ options.onQuarantine?.(record);
543
+ };
544
+ const changes = [];
545
+ for await (const line of source.readLines(BUNDLE_ENTRY.changes)) {
546
+ try {
547
+ changes.push(fromPortableChangeRecord(JSON.parse(line)));
548
+ } catch (err) {
549
+ quarantine({ kind: "change", subject: line.slice(0, 80), reason: err.message });
550
+ }
551
+ }
552
+ changes.sort(
553
+ (a, b) => compareChangeApplicationOrder2(
554
+ { lamport: a.lamport, author: a.authorDID },
555
+ { lamport: b.lamport, author: b.authorDID }
556
+ )
557
+ );
558
+ const commitCoveredHashes = /* @__PURE__ */ new Set();
559
+ try {
560
+ for await (const line of source.readLines(BUNDLE_ENTRY.commits)) {
561
+ if (!line.trim()) continue;
562
+ let record;
563
+ try {
564
+ record = JSON.parse(line);
565
+ } catch (err) {
566
+ quarantine({ kind: "change", subject: line.slice(0, 80), reason: err.message });
567
+ continue;
568
+ }
569
+ const commit = {
570
+ id: record.id,
571
+ type: "batch-commit",
572
+ protocolVersion: record.protocolVersion,
573
+ authorDID: record.authorDid,
574
+ changeHashes: record.changeHashes,
575
+ root: record.root,
576
+ lamport: record.lamportTime,
577
+ wallTime: record.wallTime,
578
+ hash: record.hash,
579
+ signature: base64ToBytes3(record.signatureB64)
580
+ };
581
+ try {
582
+ if (!await verifyBatchCommitFast(commit, parseDID2(commit.authorDID))) {
583
+ quarantine({
584
+ kind: "change",
585
+ subject: record.hash,
586
+ reason: "batch commit failed verification"
587
+ });
588
+ continue;
589
+ }
590
+ } catch (err) {
591
+ quarantine({ kind: "change", subject: record.hash, reason: err.message });
592
+ continue;
593
+ }
594
+ for (const [index, hash2] of commit.changeHashes.entries()) {
595
+ const change = changes.find((candidate) => candidate.hash === hash2);
596
+ void index;
597
+ if (change && change.authorDID === commit.authorDID) commitCoveredHashes.add(hash2);
598
+ }
599
+ }
600
+ } catch {
601
+ }
602
+ let applied = 0;
603
+ let duplicates = 0;
604
+ for (let i = 0; i < changes.length; i += APPLY_BATCH_SIZE) {
605
+ const slice = changes.slice(i, i + APPLY_BATCH_SIZE);
606
+ const needsSignatureCheck = slice.filter((change) => !commitCoveredHashes.has(change.hash));
607
+ const checked = await store.verifyRemoteChanges(needsSignatureCheck);
608
+ const verdictByHash = /* @__PURE__ */ new Map();
609
+ for (const [index, change] of needsSignatureCheck.entries()) {
610
+ verdictByHash.set(change.hash, checked[index]);
611
+ }
612
+ for (const change of slice) {
613
+ if (!verdictByHash.has(change.hash)) {
614
+ verdictByHash.set(change.hash, recomputeChangeHash(change) === change.hash);
615
+ }
616
+ }
617
+ const verdicts = slice.map((change) => verdictByHash.get(change.hash) ?? false);
618
+ for (const [index, change] of slice.entries()) {
619
+ try {
620
+ if (await store.hasChange(change.hash)) {
621
+ duplicates++;
622
+ continue;
623
+ }
624
+ if (!verdicts[index]) {
625
+ quarantine({
626
+ kind: "change",
627
+ subject: change.hash,
628
+ reason: "hash or signature verification failed"
629
+ });
630
+ continue;
631
+ }
632
+ await store.applyRemoteChange(change, { preVerified: true });
633
+ applied++;
634
+ } catch (err) {
635
+ quarantine({
636
+ kind: "change",
637
+ subject: change.hash,
638
+ reason: err instanceof Error ? err.message : String(err)
639
+ });
640
+ }
641
+ }
642
+ }
643
+ let blobsInstalled = 0;
644
+ if (options.blobPort) {
645
+ for await (const line of source.readLines(BUNDLE_ENTRY.blobIndex)) {
646
+ let record;
647
+ try {
648
+ record = JSON.parse(line);
649
+ } catch (err) {
650
+ quarantine({ kind: "blob", subject: line.slice(0, 80), reason: err.message });
651
+ continue;
652
+ }
653
+ try {
654
+ if (await options.blobPort.has(record.cid)) continue;
655
+ const bytes = await source.readEntry(record.path);
656
+ if (!bytes) {
657
+ quarantine({
658
+ kind: "blob",
659
+ subject: record.cid,
660
+ reason: `bundle entry ${record.path} missing`
661
+ });
662
+ continue;
663
+ }
664
+ await options.blobPort.put(bytes, { cid: record.cid, mimeType: record.mimeType });
665
+ blobsInstalled++;
666
+ } catch (err) {
667
+ quarantine({ kind: "blob", subject: record.cid, reason: err.message });
668
+ }
669
+ }
670
+ }
671
+ let yjsDocsApplied = 0;
672
+ if (options.yjsPort) {
673
+ for await (const line of source.readLines(BUNDLE_ENTRY.yjsDocs)) {
674
+ let record;
675
+ try {
676
+ record = JSON.parse(line);
677
+ } catch (err) {
678
+ quarantine({ kind: "yjs-doc", subject: line.slice(0, 80), reason: err.message });
679
+ continue;
680
+ }
681
+ try {
682
+ await options.yjsPort.apply(record.nodeId, base64ToBytes3(record.updateB64));
683
+ yjsDocsApplied++;
684
+ } catch (err) {
685
+ quarantine({ kind: "yjs-doc", subject: record.nodeId, reason: err.message });
686
+ }
687
+ }
688
+ }
689
+ return { applied, duplicates, quarantined, blobsInstalled, yjsDocsApplied, missingPrerequisites };
690
+ }
691
+ async function readBundleManifest(source) {
692
+ const bytes = await source.readEntry(BUNDLE_ENTRY.manifest);
693
+ if (!bytes) return null;
694
+ try {
695
+ return JSON.parse(decodeUtf8(bytes));
696
+ } catch {
697
+ return null;
698
+ }
699
+ }
700
+
701
+ // src/portability/memory-bundle.ts
702
+ var MemoryBundleSink = class {
703
+ entries = /* @__PURE__ */ new Map();
704
+ writeEntry(path, data) {
705
+ this.entries.set(path, data);
706
+ }
707
+ toSource() {
708
+ return new MemoryBundleSource(this.entries);
709
+ }
710
+ };
711
+ var MemoryBundleSource = class {
712
+ constructor(entries) {
713
+ this.entries = entries;
714
+ }
715
+ async readEntry(path) {
716
+ return this.entries.get(path) ?? null;
717
+ }
718
+ async *readLines(path) {
719
+ const bytes = this.entries.get(path);
720
+ if (!bytes) return;
721
+ let start = 0;
722
+ const text = decodeUtf8(bytes);
723
+ while (start < text.length) {
724
+ let end = text.indexOf("\n", start);
725
+ if (end === -1) end = text.length;
726
+ const line = text.slice(start, end);
727
+ if (line.length > 0) yield line;
728
+ start = end + 1;
729
+ }
730
+ }
731
+ async listEntries(prefix) {
732
+ return [...this.entries.keys()].filter((p) => p.startsWith(prefix)).sort();
733
+ }
734
+ };
735
+
736
+ // src/portability/store-yjs-port.ts
737
+ import * as Y from "yjs";
738
+ function createStoreYjsPort(store) {
739
+ return {
740
+ async *list() {
741
+ const nodes = await store.list({ includeDeleted: true });
742
+ for (const node of nodes) {
743
+ const content = await store.getDocumentContent(node.id);
744
+ if (content && content.byteLength > 0) {
745
+ yield { nodeId: node.id, update: content };
746
+ }
747
+ }
748
+ },
749
+ async apply(nodeId, update) {
750
+ const existing = await store.getDocumentContent(nodeId);
751
+ if (!existing || existing.byteLength === 0) {
752
+ await store.setDocumentContent(nodeId, update);
753
+ return;
754
+ }
755
+ let merged;
756
+ const doc = new Y.Doc();
757
+ try {
758
+ Y.applyUpdate(doc, existing);
759
+ Y.applyUpdate(doc, update);
760
+ merged = Y.encodeStateAsUpdate(doc);
761
+ } catch (err) {
762
+ throw new Error(
763
+ `document content for ${nodeId} is not a mergeable Yjs update (${err.message}); left the existing content untouched`
764
+ );
765
+ } finally {
766
+ doc.destroy();
767
+ }
768
+ await store.setDocumentContent(nodeId, merged);
769
+ }
770
+ };
771
+ }
772
+
773
+ export {
774
+ XNETPACK_FORMAT_VERSION,
775
+ BUNDLE_ENTRY,
776
+ FRONTIER_HEADS_CAP,
777
+ toPortableChangeRecord,
778
+ fromPortableChangeRecord,
779
+ canonicalManifestBytes,
780
+ blobEntryPath,
781
+ writeBundle,
782
+ verifyBundle,
783
+ BundleImportError,
784
+ applyBundle,
785
+ readBundleManifest,
786
+ MemoryBundleSink,
787
+ MemoryBundleSource,
788
+ createStoreYjsPort
789
+ };