@coderook/cli 0.22.0 → 0.22.1

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.
@@ -2,7 +2,7 @@
2
2
  "name": "coderook",
3
3
  "displayName": "CodeRook",
4
4
  "description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, from Claude Code.",
5
- "version": "0.22.0",
5
+ "version": "0.22.1",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -27,6 +27,7 @@ const profile_js_1 = require("./profile.js");
27
27
  const solid_js_1 = require("./solid.js");
28
28
  const retry_js_1 = require("./retry.js");
29
29
  const staging_js_1 = require("./staging.js");
30
+ const delta_js_1 = require("../shared/delta.js");
30
31
  /** Above this the API insists on a multipart session. */
31
32
  const DIRECT_LIMIT = 95 * 1024 * 1024;
32
33
  /*
@@ -105,6 +106,10 @@ class Uploader {
105
106
  chunkingSupported = null;
106
107
  /** Null until the service has been asked; see packingAllowed. */
107
108
  packingSupported = null;
109
+ /** Null until the service has advertised its best changed-file transport. */
110
+ deltaProtocol = null;
111
+ /** Batch delta is separate so V2 single-file support remains compatible. */
112
+ deltaBatchSupported = null;
108
113
  constructor(credentials) {
109
114
  this.credentials = credentials;
110
115
  }
@@ -345,8 +350,15 @@ class Uploader {
345
350
  });
346
351
  totalBytes += size;
347
352
  }
348
- if (!declarations.length)
353
+ /*
354
+ A deletion-only save has no file to hash or upload, but it is still a
355
+ real new Version: the retained base minus the explicitly selected path.
356
+ Refusing it here made the object phase look healthy while preventing the
357
+ manifest-only operation that deletion is supposed to be.
358
+ */
359
+ if (!declarations.length && !deletions.size) {
349
360
  throw new Error("Nothing is selected to upload");
361
+ }
350
362
  // 2. The project needs somewhere on the account to live.
351
363
  this.check();
352
364
  let repositoryId = request.repositoryId;
@@ -394,6 +406,24 @@ class Uploader {
394
406
  */
395
407
  const allDeclarations = declarations;
396
408
  const declarationByPath = new Map(allDeclarations.map((one) => [one.path, one]));
409
+ /*
410
+ The narrow first use of the delta transport.
411
+
412
+ One changed small file used to miss solid packing (which correctly
413
+ requires a group) and upload its complete compressed bytes. When more
414
+ than one small file changes, the existing pack remains the better
415
+ answer and is deliberately untouched. New files have no receiver base
416
+ and also stay on the existing path.
417
+ */
418
+ const logicallyChanged = allDeclarations.filter((one) => prior.get(one.path)?.sha256 !== one.sha256);
419
+ const deltaDeclaration = logicallyChanged.length === 1 &&
420
+ request.baseVersionId &&
421
+ prior.has(logicallyChanged[0].path) &&
422
+ logicallyChanged[0].size <= BATCH_FILE_LIMIT &&
423
+ logicallyChanged[0].size <= delta_js_1.DELTA_MAX_FILE_BYTES &&
424
+ prior.get(logicallyChanged[0].path).sourceSize <= delta_js_1.DELTA_MAX_FILE_BYTES
425
+ ? logicallyChanged[0]
426
+ : null;
397
427
  const sections = (0, staging_js_1.planSections)(allDeclarations.map((one) => ({ path: one.path, size: one.size })));
398
428
  (0, profile_js_1.counted)("sections planned", sections.length);
399
429
  const uploaded = [];
@@ -544,7 +574,22 @@ class Uploader {
544
574
  an object of their own.
545
575
  */
546
576
  const packedInto = new Map();
547
- if (smallOnes.length > 1) {
577
+ if (deltaDeclaration &&
578
+ smallOnes.some((one) => one.path === deltaDeclaration.path)) {
579
+ const held = prior.get(deltaDeclaration.path);
580
+ const placed = await (0, profile_js_1.timed)("send changed-file delta", () => this.putDelta(repositoryId, request.baseVersionId, held, deltaDeclaration));
581
+ if (placed) {
582
+ batched.set(deltaDeclaration.sha256, {
583
+ objectId: placed.objectId,
584
+ size: placed.storedSize,
585
+ });
586
+ sentBytes += deltaDeclaration.size;
587
+ transferred += placed.sentBytes;
588
+ (0, profile_js_1.counted)("files sent as deltas", 1);
589
+ }
590
+ }
591
+ const smallForPacking = smallOnes.filter((one) => !batched.has(one.sha256));
592
+ if (smallForPacking.length > 1) {
548
593
  let pack = [];
549
594
  let packBytes = 0;
550
595
  /*
@@ -569,6 +614,7 @@ class Uploader {
569
614
  pack = [];
570
615
  packBytes = 0;
571
616
  const task = (async () => {
617
+ let exactWireBytes = null;
572
618
  try {
573
619
  /*
574
620
  Compressed together first. This is what the .cbx format is for and
@@ -577,18 +623,36 @@ class Uploader {
577
623
  and 30.2% compressed together, because a two-kilobyte file gives a
578
624
  compressor no dictionary worth having.
579
625
  */
580
- const placed = (await this.packingAllowed())
581
- ? await (0, profile_js_1.timed)("send packs", () => this.putPack(repositoryId, taking.map((item) => ({
582
- declaration: item.declaration,
626
+ const packItems = taking.map((item) => ({
627
+ declaration: item.declaration,
628
+ body: item.body,
629
+ }));
630
+ const built = (await this.packingAllowed())
631
+ ? await (0, solid_js_1.buildSolidPack)(packItems.map((item) => ({
632
+ sha256: item.declaration.sha256,
583
633
  body: item.body,
584
- }))))
634
+ })))
635
+ : null;
636
+ const packedBytes = built ? this.frameSolidPack(built).byteLength : 0;
637
+ const delta = built
638
+ ? await (0, profile_js_1.timed)("price changed-file batch", () => this.putDeltaBatch(repositoryId, request.baseVersionId, taking, prior, packedBytes))
639
+ : null;
640
+ if (delta) {
641
+ (0, profile_js_1.counted)("files sent as delta batch", taking.length);
642
+ exactWireBytes = delta.sentBytes;
643
+ for (const [digest, one] of delta.objects)
644
+ batched.set(digest, one);
645
+ }
646
+ const placed = !delta && built
647
+ ? await (0, profile_js_1.timed)("send packs", () => this.putPack(repositoryId, packItems, built))
585
648
  : null;
586
649
  if (placed) {
587
650
  (0, profile_js_1.counted)("files sent in packs", taking.length);
651
+ exactWireBytes = packedBytes;
588
652
  for (const [digest, one] of placed)
589
653
  packedInto.set(digest, one);
590
654
  }
591
- else {
655
+ else if (!delta) {
592
656
  /*
593
657
  Not worth packing — already-compressed content, or too few files.
594
658
  They still travel together, just as separate objects.
@@ -617,8 +681,11 @@ class Uploader {
617
681
  if (!batched.has(digest) && !packedInto.has(digest))
618
682
  continue;
619
683
  sentBytes += item.declaration.size;
620
- transferred += item.encoded.body.byteLength;
684
+ if (exactWireBytes === null)
685
+ transferred += item.encoded.body.byteLength;
621
686
  }
687
+ if (exactWireBytes !== null)
688
+ transferred += exactWireBytes;
622
689
  report({
623
690
  stage: "upload",
624
691
  files: 0,
@@ -634,7 +701,7 @@ class Uploader {
634
701
  if (flying.size >= BATCH_LANES)
635
702
  await Promise.race(flying);
636
703
  };
637
- for (const declaration of smallOnes) {
704
+ for (const declaration of smallForPacking) {
638
705
  this.check();
639
706
  let body;
640
707
  try {
@@ -1128,24 +1195,14 @@ class Uploader {
1128
1195
  * service would not take it — in which case the caller falls back to sending
1129
1196
  * them separately, which is slower and larger but always works.
1130
1197
  */
1131
- async putPack(repositoryId, items) {
1132
- const built = await (0, solid_js_1.buildSolidPack)(items.map((item) => ({
1198
+ async putPack(repositoryId, items, alreadyBuilt) {
1199
+ const built = alreadyBuilt ?? await (0, solid_js_1.buildSolidPack)(items.map((item) => ({
1133
1200
  sha256: item.declaration.sha256,
1134
1201
  body: item.body,
1135
1202
  })));
1136
1203
  if (!built)
1137
1204
  return null;
1138
- const manifest = JSON.stringify({
1139
- sha256: built.sha256,
1140
- storedSha256: built.storedSha256,
1141
- size: built.size,
1142
- members: built.members,
1143
- });
1144
- const manifestBytes = new TextEncoder().encode(manifest);
1145
- const packed = new Uint8Array(4 + manifestBytes.byteLength + built.body.byteLength);
1146
- new DataView(packed.buffer).setUint32(0, manifestBytes.byteLength, false);
1147
- packed.set(manifestBytes, 4);
1148
- packed.set(built.body, 4 + manifestBytes.byteLength);
1205
+ const packed = this.frameSolidPack(built);
1149
1206
  const answer = await this.call(`/v1/repositories/${repositoryId}/objects/pack`, {
1150
1207
  method: "POST",
1151
1208
  contentType: "application/octet-stream",
@@ -1174,6 +1231,216 @@ class Uploader {
1174
1231
  }
1175
1232
  return placed;
1176
1233
  }
1234
+ /** The exact HTTP body used for the solid-pack alternative. */
1235
+ frameSolidPack(built) {
1236
+ const manifestBytes = new TextEncoder().encode(JSON.stringify({
1237
+ sha256: built.sha256,
1238
+ storedSha256: built.storedSha256,
1239
+ size: built.size,
1240
+ members: built.members,
1241
+ }));
1242
+ const packed = new Uint8Array(4 + manifestBytes.byteLength + built.body.byteLength);
1243
+ new DataView(packed.buffer).setUint32(0, manifestBytes.byteLength, false);
1244
+ packed.set(manifestBytes, 4);
1245
+ packed.set(built.body, 4 + manifestBytes.byteLength);
1246
+ return packed;
1247
+ }
1248
+ /**
1249
+ * Price several independent file deltas against the already-built solid
1250
+ * pack. The batch is optional and may only replace the pack when every
1251
+ * member has an immutable base and the complete two-way wire cost wins by
1252
+ * the same twenty-percent margin used by the single-file transport.
1253
+ */
1254
+ async putDeltaBatch(repositoryId, baseVersionId, items, prior, solidPackBytes) {
1255
+ if (!baseVersionId ||
1256
+ solidPackBytes <= 0 ||
1257
+ items.length < 2 ||
1258
+ items.length > delta_js_1.DELTA_BATCH_MAX_FILES ||
1259
+ !(await this.deltaBatchAllowed()))
1260
+ return null;
1261
+ let sourceBytes = 0;
1262
+ let signatureEstimate = 6 + items.length * 4;
1263
+ const bases = [];
1264
+ for (const item of items) {
1265
+ const base = prior.get(item.declaration.path);
1266
+ if (!base ||
1267
+ !base.sha256 ||
1268
+ item.declaration.size > delta_js_1.DELTA_MAX_FILE_BYTES ||
1269
+ base.sourceSize > delta_js_1.DELTA_MAX_FILE_BYTES)
1270
+ return null;
1271
+ if ((0, node_crypto_1.createHash)("sha256").update(item.body).digest("hex") !==
1272
+ item.declaration.sha256)
1273
+ return null;
1274
+ sourceBytes += item.declaration.size;
1275
+ if (sourceBytes > delta_js_1.DELTA_BATCH_MAX_SOURCE_BYTES)
1276
+ return null;
1277
+ signatureEstimate += (0, delta_js_1.estimateDeltaSignatureBytes)(base.sourceSize, 2);
1278
+ bases.push(base);
1279
+ }
1280
+ if (signatureEstimate >= solidPackBytes * 0.8)
1281
+ return null;
1282
+ try {
1283
+ const token = await this.credentials.token();
1284
+ if (!token)
1285
+ return null;
1286
+ const response = await fetch(`${this.credentials.origin()}/v1/repositories/${repositoryId}/objects/delta/signatures`, {
1287
+ method: "POST",
1288
+ headers: {
1289
+ accept: "application/vnd.coderook.delta-signature-batch; version=2",
1290
+ authorization: `Bearer ${token}`,
1291
+ "content-type": "application/json",
1292
+ "user-agent": "CodeRook/0.1",
1293
+ ...(0, identify_js_1.clientHeaders)(),
1294
+ },
1295
+ body: JSON.stringify({
1296
+ baseVersionId,
1297
+ files: items.map((item, index) => ({
1298
+ path: item.declaration.path,
1299
+ baseSha256: bases[index].sha256,
1300
+ })),
1301
+ }),
1302
+ signal: this.controller.signal,
1303
+ });
1304
+ if (!response.ok)
1305
+ return null;
1306
+ const signatureBytes = new Uint8Array(await response.arrayBuffer());
1307
+ const signatures = (0, delta_js_1.decodeDeltaSignatureBatch)(signatureBytes);
1308
+ if (signatures.length !== items.length)
1309
+ return null;
1310
+ const framed = (0, delta_js_1.encodeDeltaBatchEnvelope)({
1311
+ baseVersionId,
1312
+ entries: items.map((item, index) => ({
1313
+ path: item.declaration.path,
1314
+ sha256: item.declaration.sha256,
1315
+ mediaType: item.declaration.mediaType,
1316
+ patch: (0, delta_js_1.createDeltaPatch)(item.body, signatures[index]),
1317
+ })),
1318
+ });
1319
+ const wireBytes = signatureBytes.byteLength + framed.byteLength;
1320
+ if (wireBytes >= solidPackBytes * 0.8)
1321
+ return null;
1322
+ const answer = await this.call(`/v1/repositories/${repositoryId}/objects/delta/batch`, {
1323
+ method: "POST",
1324
+ contentType: "application/octet-stream",
1325
+ body: framed,
1326
+ });
1327
+ if (answer.objects.length !== items.length)
1328
+ return null;
1329
+ const objects = new Map();
1330
+ for (const object of answer.objects) {
1331
+ if (!items.some((item) => item.declaration.sha256 === object.sha256)) {
1332
+ return null;
1333
+ }
1334
+ objects.set(object.sha256, {
1335
+ objectId: object.objectId,
1336
+ size: object.storedSize,
1337
+ });
1338
+ }
1339
+ const expectedDigests = new Set(items.map((item) => item.declaration.sha256)).size;
1340
+ return objects.size === expectedDigests
1341
+ ? { objects, sentBytes: wireBytes }
1342
+ : null;
1343
+ }
1344
+ catch {
1345
+ return null;
1346
+ }
1347
+ }
1348
+ /**
1349
+ * Try the negotiated receiver-signature transport for one changed file.
1350
+ *
1351
+ * Any refusal, old deployment, stale base, or poor result returns null and
1352
+ * the caller sends the complete object exactly as it did before. The patch
1353
+ * is never a repository object: the service reconstructs and verifies the
1354
+ * complete target before returning the ordinary object id used below.
1355
+ */
1356
+ async putDelta(repositoryId, baseVersionId, prior, declaration) {
1357
+ const deltaVersion = await this.deltaVersion();
1358
+ if (deltaVersion === 0)
1359
+ return null;
1360
+ let target;
1361
+ try {
1362
+ target = new Uint8Array(await (0, promises_1.readFile)(declaration.full));
1363
+ }
1364
+ catch {
1365
+ return null;
1366
+ }
1367
+ const digest = (0, node_crypto_1.createHash)("sha256").update(target).digest("hex");
1368
+ if (digest !== declaration.sha256)
1369
+ return null;
1370
+ const ordinary = await (0, compress_js_1.encodeForUpload)(target, declaration.path, await this.gzipAllowed());
1371
+ const signatureEstimate = (0, delta_js_1.estimateDeltaSignatureBytes)(prior.sourceSize, deltaVersion);
1372
+ if (signatureEstimate >= ordinary.body.byteLength * 0.8)
1373
+ return null;
1374
+ try {
1375
+ const signatureBytes = await this.deltaSignature(repositoryId, baseVersionId, declaration.path, prior.sha256, deltaVersion);
1376
+ const signature = (0, delta_js_1.parseDeltaSignature)(signatureBytes);
1377
+ const patch = (0, delta_js_1.createDeltaPatch)(target, signature);
1378
+ let framed;
1379
+ if (signature.version === 2) {
1380
+ framed = (0, delta_js_1.encodeDeltaEnvelope)({
1381
+ baseVersionId,
1382
+ path: declaration.path,
1383
+ sha256: declaration.sha256,
1384
+ mediaType: declaration.mediaType,
1385
+ patch,
1386
+ });
1387
+ }
1388
+ else {
1389
+ const manifest = new TextEncoder().encode(JSON.stringify({
1390
+ baseVersionId,
1391
+ path: declaration.path,
1392
+ baseSha256: prior.sha256,
1393
+ sha256: declaration.sha256,
1394
+ size: target.byteLength,
1395
+ mediaType: declaration.mediaType,
1396
+ }));
1397
+ framed = new Uint8Array(4 + manifest.byteLength + patch.byteLength);
1398
+ new DataView(framed.buffer).setUint32(0, manifest.byteLength, false);
1399
+ framed.set(manifest, 4);
1400
+ framed.set(patch, 4 + manifest.byteLength);
1401
+ }
1402
+ // Count both directions. A patch that only looks small because its
1403
+ // receiver signature was ignored is not an improvement.
1404
+ const wireBytes = signatureBytes.byteLength + framed.byteLength;
1405
+ if (wireBytes >= ordinary.body.byteLength * 0.8)
1406
+ return null;
1407
+ const answer = await this.call(`/v1/repositories/${repositoryId}/objects/delta`, {
1408
+ method: "POST",
1409
+ contentType: "application/octet-stream",
1410
+ body: framed,
1411
+ });
1412
+ return {
1413
+ objectId: answer.objectId,
1414
+ storedSize: answer.storedSize,
1415
+ sentBytes: wireBytes,
1416
+ };
1417
+ }
1418
+ catch {
1419
+ // This is an optional transport. The complete-object path is authority.
1420
+ return null;
1421
+ }
1422
+ }
1423
+ async deltaSignature(repositoryId, baseVersionId, filePath, baseSha256, version) {
1424
+ const token = await this.credentials.token();
1425
+ if (!token)
1426
+ throw new Error("Sign in again before uploading");
1427
+ const response = await fetch(`${this.credentials.origin()}/v1/repositories/${repositoryId}/objects/delta/signature`, {
1428
+ method: "POST",
1429
+ headers: {
1430
+ accept: `application/vnd.coderook.delta-signature; version=${version}`,
1431
+ authorization: `Bearer ${token}`,
1432
+ "content-type": "application/json",
1433
+ "user-agent": "CodeRook/0.1",
1434
+ ...(0, identify_js_1.clientHeaders)(),
1435
+ },
1436
+ body: JSON.stringify({ baseVersionId, path: filePath, baseSha256 }),
1437
+ signal: this.controller.signal,
1438
+ });
1439
+ if (!response.ok) {
1440
+ throw new Error(`Delta signature failed with HTTP ${response.status}`);
1441
+ }
1442
+ return new Uint8Array(await response.arrayBuffer());
1443
+ }
1177
1444
  /**
1178
1445
  * Send many small objects in one request.
1179
1446
  *
@@ -1501,6 +1768,23 @@ class Uploader {
1501
1768
  this.packingSupported = (await this.serviceFeatures()).includes("solid-packs");
1502
1769
  return this.packingSupported;
1503
1770
  }
1771
+ async deltaVersion() {
1772
+ if (this.deltaProtocol !== null)
1773
+ return this.deltaProtocol;
1774
+ const features = await this.serviceFeatures();
1775
+ this.deltaProtocol = features.includes("delta-transport-v2")
1776
+ ? 2
1777
+ : features.includes("delta-transport")
1778
+ ? 1
1779
+ : 0;
1780
+ return this.deltaProtocol;
1781
+ }
1782
+ async deltaBatchAllowed() {
1783
+ if (this.deltaBatchSupported !== null)
1784
+ return this.deltaBatchSupported;
1785
+ this.deltaBatchSupported = (await this.serviceFeatures()).includes("delta-batch-v2");
1786
+ return this.deltaBatchSupported;
1787
+ }
1504
1788
  async chunkingAllowed() {
1505
1789
  if (this.chunkingSupported !== null)
1506
1790
  return this.chunkingSupported;
@@ -0,0 +1,772 @@
1
+ "use strict";
2
+ /**
3
+ * CodeRook's changed-file transport delta.
4
+ *
5
+ * This is deliberately not a fourth permanent storage shape. The receiver
6
+ * describes blocks from the complete file it already has, the sender emits a
7
+ * short stream of COPY and INSERT operations, and the receiver reconstructs a
8
+ * complete object before it is admitted to the ordinary object catalogue.
9
+ *
10
+ * The shape follows the useful part of rsync and Git pack deltas without
11
+ * inheriting delta chains: a rolling weak checksum finds candidate blocks, a
12
+ * second checksum rejects ordinary collisions, and the final file SHA-256 is
13
+ * still the authority at the API boundary.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.DELTA_BATCH_MAX_SOURCE_BYTES = exports.DELTA_BATCH_MAX_FILES = exports.DELTA_MAX_FILE_BYTES = void 0;
17
+ exports.deltaBlockSize = deltaBlockSize;
18
+ exports.estimateDeltaSignatureBytes = estimateDeltaSignatureBytes;
19
+ exports.createDeltaSignature = createDeltaSignature;
20
+ exports.parseDeltaSignature = parseDeltaSignature;
21
+ exports.createDeltaPatch = createDeltaPatch;
22
+ exports.deltaPatchTargetSize = deltaPatchTargetSize;
23
+ exports.applyDeltaPatch = applyDeltaPatch;
24
+ exports.encodeDeltaEnvelope = encodeDeltaEnvelope;
25
+ exports.isDeltaEnvelope = isDeltaEnvelope;
26
+ exports.decodeDeltaEnvelope = decodeDeltaEnvelope;
27
+ exports.encodeDeltaSignatureBatch = encodeDeltaSignatureBatch;
28
+ exports.decodeDeltaSignatureBatch = decodeDeltaSignatureBatch;
29
+ exports.encodeDeltaBatchEnvelope = encodeDeltaBatchEnvelope;
30
+ exports.decodeDeltaBatchEnvelope = decodeDeltaBatchEnvelope;
31
+ const SIGNATURE_MAGIC_V1 = [0x43, 0x52, 0x53, 0x31]; // CRS1
32
+ const SIGNATURE_MAGIC_V2 = [0x43, 0x52, 0x53, 0x32]; // CRS2
33
+ const PATCH_MAGIC_V1 = [0x43, 0x52, 0x50, 0x31]; // CRP1
34
+ const PATCH_MAGIC_V2 = [0x43, 0x52, 0x50, 0x32]; // CRP2
35
+ const ENVELOPE_MAGIC_V2 = [0x43, 0x44, 0x45, 0x32]; // CDE2
36
+ const SIGNATURE_BATCH_MAGIC_V2 = [0x43, 0x44, 0x53, 0x32]; // CDS2
37
+ const DELTA_BATCH_MAGIC_V2 = [0x43, 0x44, 0x42, 0x32]; // CDB2
38
+ const SIGNATURE_HEADER_V1_BYTES = 16;
39
+ const SIGNATURE_HEADER_V2_BYTES = 12;
40
+ const SIGNATURE_RECORD_V1_BYTES = 8;
41
+ const SIGNATURE_RECORD_V2_BYTES = 6;
42
+ const PATCH_HEADER_V1_BYTES = 12;
43
+ const COPY_BYTES = 9;
44
+ const LITERAL_HEADER_BYTES = 5;
45
+ const ENVELOPE_V2_FIXED_BYTES = 55;
46
+ const MOD_ADLER = 65_521;
47
+ /** Bound Worker memory and make malformed requests cheap to reject. */
48
+ exports.DELTA_MAX_FILE_BYTES = 8 * 1024 * 1024;
49
+ exports.DELTA_BATCH_MAX_FILES = 64;
50
+ exports.DELTA_BATCH_MAX_SOURCE_BYTES = 24 * 1024 * 1024;
51
+ function sameMagic(bytes, expected) {
52
+ return expected.every((value, index) => bytes[index] === value);
53
+ }
54
+ function nextPowerOfTwo(value) {
55
+ let power = 1;
56
+ while (power < value && power < 0x4000_0000)
57
+ power *= 2;
58
+ return power;
59
+ }
60
+ /**
61
+ * Keep signature replies small while retaining fine matches.
62
+ *
63
+ * sqrt(file size), rounded to a power of two, is the traditional useful shape
64
+ * for receiver signatures: an 8 KiB file uses 128-byte blocks and sends only
65
+ * 512 bytes of checksum records; an 8 MiB file uses 4 KiB blocks and sends
66
+ * 16 KiB of records.
67
+ */
68
+ function deltaBlockSize(size, version = 1) {
69
+ /*
70
+ V2 minimizes the bytes on both sides, rather than only the number of
71
+ literal bytes in the patch. Six checksum bytes are paid for every block,
72
+ while a narrow edit normally invalidates one complete block. The minimum
73
+ of those two costs is close to sqrt(fileSize * recordSize).
74
+ */
75
+ const weight = version === 2 ? SIGNATURE_RECORD_V2_BYTES : 1;
76
+ const wanted = Math.ceil(Math.sqrt(Math.max(1, size) * weight));
77
+ return Math.min(16 * 1024, Math.max(64, nextPowerOfTwo(wanted)));
78
+ }
79
+ function weakChecksum(bytes, offset, length) {
80
+ let a = 0;
81
+ let b = 0;
82
+ for (let index = 0; index < length; index += 1) {
83
+ const value = bytes[offset + index] ?? 0;
84
+ a += value;
85
+ b += (length - index) * value;
86
+ }
87
+ a %= MOD_ADLER;
88
+ b %= MOD_ADLER;
89
+ return (((b << 16) | a) >>> 0);
90
+ }
91
+ function rollWeak(checksum, outgoing, incoming, length) {
92
+ let a = (checksum & 0xffff) - outgoing + incoming;
93
+ a %= MOD_ADLER;
94
+ if (a < 0)
95
+ a += MOD_ADLER;
96
+ let b = (checksum >>> 16) - length * outgoing + a;
97
+ b %= MOD_ADLER;
98
+ if (b < 0)
99
+ b += MOD_ADLER;
100
+ return (((b << 16) | a) >>> 0);
101
+ }
102
+ /** A fast second opinion. Final reconstruction is still verified by SHA-256. */
103
+ function strongChecksum(bytes, offset, length) {
104
+ let hash = 0x811c9dc5;
105
+ for (let index = 0; index < length; index += 1) {
106
+ hash ^= bytes[offset + index] ?? 0;
107
+ hash = Math.imul(hash, 0x01000193);
108
+ }
109
+ return hash >>> 0;
110
+ }
111
+ function estimateDeltaSignatureBytes(baseSize, version = 1) {
112
+ const blockSize = deltaBlockSize(baseSize, version);
113
+ return version === 2
114
+ ? SIGNATURE_HEADER_V2_BYTES +
115
+ Math.floor(baseSize / blockSize) * SIGNATURE_RECORD_V2_BYTES
116
+ : SIGNATURE_HEADER_V1_BYTES +
117
+ Math.floor(baseSize / blockSize) * SIGNATURE_RECORD_V1_BYTES;
118
+ }
119
+ function createDeltaSignature(base, version = 1) {
120
+ if (base.byteLength > exports.DELTA_MAX_FILE_BYTES) {
121
+ throw new Error("Delta base exceeds the bounded transport limit");
122
+ }
123
+ const blockSize = deltaBlockSize(base.byteLength, version);
124
+ const count = Math.floor(base.byteLength / blockSize);
125
+ const headerBytes = version === 2 ? SIGNATURE_HEADER_V2_BYTES : SIGNATURE_HEADER_V1_BYTES;
126
+ const recordBytes = version === 2 ? SIGNATURE_RECORD_V2_BYTES : SIGNATURE_RECORD_V1_BYTES;
127
+ const out = new Uint8Array(headerBytes + count * recordBytes);
128
+ out.set(version === 2 ? SIGNATURE_MAGIC_V2 : SIGNATURE_MAGIC_V1, 0);
129
+ const view = new DataView(out.buffer);
130
+ if (version === 2) {
131
+ view.setUint16(4, blockSize, false);
132
+ out[6] = 16;
133
+ out[7] = 0;
134
+ view.setUint32(8, base.byteLength, false);
135
+ }
136
+ else {
137
+ view.setUint32(4, blockSize, false);
138
+ view.setUint32(8, base.byteLength, false);
139
+ view.setUint32(12, count, false);
140
+ }
141
+ for (let index = 0; index < count; index += 1) {
142
+ const offset = index * blockSize;
143
+ const at = headerBytes + index * recordBytes;
144
+ view.setUint32(at, weakChecksum(base, offset, blockSize), false);
145
+ if (version === 2) {
146
+ view.setUint16(at + 4, strongChecksum(base, offset, blockSize) & 0xffff, false);
147
+ }
148
+ else {
149
+ view.setUint32(at + 4, strongChecksum(base, offset, blockSize), false);
150
+ }
151
+ }
152
+ return out;
153
+ }
154
+ function parseDeltaSignature(bytes) {
155
+ const version = sameMagic(bytes, SIGNATURE_MAGIC_V2)
156
+ ? 2
157
+ : sameMagic(bytes, SIGNATURE_MAGIC_V1)
158
+ ? 1
159
+ : 0;
160
+ const headerBytes = version === 2 ? SIGNATURE_HEADER_V2_BYTES : SIGNATURE_HEADER_V1_BYTES;
161
+ const recordBytes = version === 2 ? SIGNATURE_RECORD_V2_BYTES : SIGNATURE_RECORD_V1_BYTES;
162
+ if (!version || bytes.byteLength < headerBytes) {
163
+ throw new Error("Delta signature is malformed");
164
+ }
165
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
166
+ const blockSize = version === 2 ? view.getUint16(4, false) : view.getUint32(4, false);
167
+ const baseSize = view.getUint32(8, false);
168
+ const count = version === 2 ? Math.floor(baseSize / blockSize) : view.getUint32(12, false);
169
+ if (blockSize < 64 ||
170
+ blockSize > 16 * 1024 ||
171
+ baseSize > exports.DELTA_MAX_FILE_BYTES ||
172
+ (version === 2 && (bytes[6] !== 16 || bytes[7] !== 0)) ||
173
+ count !== Math.floor(baseSize / blockSize) ||
174
+ bytes.byteLength !== headerBytes + count * recordBytes) {
175
+ throw new Error("Delta signature dimensions are invalid");
176
+ }
177
+ const blocks = [];
178
+ for (let index = 0; index < count; index += 1) {
179
+ const at = headerBytes + index * recordBytes;
180
+ blocks.push({
181
+ weak: view.getUint32(at, false),
182
+ strong: version === 2
183
+ ? view.getUint16(at + 4, false)
184
+ : view.getUint32(at + 4, false),
185
+ });
186
+ }
187
+ return {
188
+ version,
189
+ blockSize,
190
+ baseSize,
191
+ strongBits: version === 2 ? 16 : 32,
192
+ blocks,
193
+ byteLength: bytes.byteLength,
194
+ };
195
+ }
196
+ function appendOperation(operations, operation) {
197
+ if (operation.kind === "literal" && operation.bytes.byteLength === 0)
198
+ return;
199
+ if (operation.kind === "copy" && operation.length === 0)
200
+ return;
201
+ const previous = operations[operations.length - 1];
202
+ if (previous?.kind === "copy" &&
203
+ operation.kind === "copy" &&
204
+ previous.offset + previous.length === operation.offset) {
205
+ previous.length += operation.length;
206
+ return;
207
+ }
208
+ if (previous?.kind === "literal" && operation.kind === "literal") {
209
+ const joined = new Uint8Array(previous.bytes.byteLength + operation.bytes.byteLength);
210
+ joined.set(previous.bytes);
211
+ joined.set(operation.bytes, previous.bytes.byteLength);
212
+ previous.bytes = joined;
213
+ return;
214
+ }
215
+ operations.push(operation);
216
+ }
217
+ function varUintBytes(value) {
218
+ let remaining = value >>> 0;
219
+ let count = 1;
220
+ while (remaining >= 0x80) {
221
+ remaining >>>= 7;
222
+ count += 1;
223
+ }
224
+ return count;
225
+ }
226
+ function writeVarUint(out, offset, value) {
227
+ let remaining = value >>> 0;
228
+ let at = offset;
229
+ while (remaining >= 0x80) {
230
+ out[at] = (remaining & 0x7f) | 0x80;
231
+ remaining >>>= 7;
232
+ at += 1;
233
+ }
234
+ out[at] = remaining;
235
+ return at + 1;
236
+ }
237
+ function readVarUint(bytes, offset) {
238
+ let value = 0;
239
+ let shift = 0;
240
+ let at = offset;
241
+ for (let count = 0; count < 5; count += 1) {
242
+ if (at >= bytes.byteLength)
243
+ throw new Error("Delta varint is truncated");
244
+ const byte = bytes[at] ?? 0;
245
+ if (count === 4 && (byte & 0xf0) !== 0) {
246
+ throw new Error("Delta varint exceeds 32 bits");
247
+ }
248
+ value = (value | ((byte & 0x7f) << shift)) >>> 0;
249
+ at += 1;
250
+ if ((byte & 0x80) === 0) {
251
+ if (count > 0 && byte === 0) {
252
+ throw new Error("Delta varint is not canonical");
253
+ }
254
+ return { value, next: at };
255
+ }
256
+ shift += 7;
257
+ }
258
+ throw new Error("Delta varint is malformed");
259
+ }
260
+ function encodePatchV1(targetSize, operations) {
261
+ const total = PATCH_HEADER_V1_BYTES +
262
+ operations.reduce((sum, operation) => sum +
263
+ (operation.kind === "copy"
264
+ ? COPY_BYTES
265
+ : LITERAL_HEADER_BYTES + operation.bytes.byteLength), 0);
266
+ const out = new Uint8Array(total);
267
+ out.set(PATCH_MAGIC_V1, 0);
268
+ const view = new DataView(out.buffer);
269
+ view.setUint32(4, targetSize, false);
270
+ view.setUint32(8, operations.length, false);
271
+ let at = PATCH_HEADER_V1_BYTES;
272
+ for (const operation of operations) {
273
+ if (operation.kind === "copy") {
274
+ out[at] = 1;
275
+ view.setUint32(at + 1, operation.offset, false);
276
+ view.setUint32(at + 5, operation.length, false);
277
+ at += COPY_BYTES;
278
+ }
279
+ else {
280
+ out[at] = 0;
281
+ view.setUint32(at + 1, operation.bytes.byteLength, false);
282
+ out.set(operation.bytes, at + LITERAL_HEADER_BYTES);
283
+ at += LITERAL_HEADER_BYTES + operation.bytes.byteLength;
284
+ }
285
+ }
286
+ return out;
287
+ }
288
+ function encodePatchV2(targetSize, operations) {
289
+ const total = 4 +
290
+ varUintBytes(targetSize) +
291
+ varUintBytes(operations.length) +
292
+ operations.reduce((sum, operation) => sum +
293
+ 1 +
294
+ (operation.kind === "copy"
295
+ ? varUintBytes(operation.offset) + varUintBytes(operation.length)
296
+ : varUintBytes(operation.bytes.byteLength) + operation.bytes.byteLength), 0);
297
+ const out = new Uint8Array(total);
298
+ out.set(PATCH_MAGIC_V2, 0);
299
+ let at = writeVarUint(out, 4, targetSize);
300
+ at = writeVarUint(out, at, operations.length);
301
+ for (const operation of operations) {
302
+ if (operation.kind === "copy") {
303
+ out[at] = 1;
304
+ at = writeVarUint(out, at + 1, operation.offset);
305
+ at = writeVarUint(out, at, operation.length);
306
+ }
307
+ else {
308
+ out[at] = 0;
309
+ at = writeVarUint(out, at + 1, operation.bytes.byteLength);
310
+ out.set(operation.bytes, at);
311
+ at += operation.bytes.byteLength;
312
+ }
313
+ }
314
+ return out;
315
+ }
316
+ /** Build COPY/INSERT operations from receiver signatures, without base bytes. */
317
+ function createDeltaPatch(target, signature) {
318
+ if (target.byteLength > exports.DELTA_MAX_FILE_BYTES) {
319
+ throw new Error("Delta target exceeds the bounded transport limit");
320
+ }
321
+ if (signature.blocks.length === 0 || target.byteLength < signature.blockSize) {
322
+ const literal = [{ kind: "literal", bytes: target.slice() }];
323
+ return signature.version === 2
324
+ ? encodePatchV2(target.byteLength, literal)
325
+ : encodePatchV1(target.byteLength, literal);
326
+ }
327
+ const candidates = new Map();
328
+ signature.blocks.forEach((block, index) => {
329
+ const list = candidates.get(block.weak) ?? [];
330
+ // Repeated zero-filled blocks can otherwise make adversarial work quadratic.
331
+ if (list.length < 64)
332
+ list.push(index);
333
+ candidates.set(block.weak, list);
334
+ });
335
+ const operations = [];
336
+ const width = signature.blockSize;
337
+ let cursor = 0;
338
+ let literalStart = 0;
339
+ let weak = weakChecksum(target, 0, width);
340
+ while (cursor + width <= target.byteLength) {
341
+ const possible = candidates.get(weak);
342
+ let matched = -1;
343
+ if (possible?.length) {
344
+ const strong = strongChecksum(target, cursor, width);
345
+ matched = possible.find((index) => signature.blocks[index]?.strong ===
346
+ (signature.strongBits === 16 ? strong & 0xffff : strong)) ?? -1;
347
+ }
348
+ if (matched >= 0) {
349
+ appendOperation(operations, {
350
+ kind: "literal",
351
+ bytes: target.slice(literalStart, cursor),
352
+ });
353
+ appendOperation(operations, {
354
+ kind: "copy",
355
+ offset: matched * width,
356
+ length: width,
357
+ });
358
+ cursor += width;
359
+ literalStart = cursor;
360
+ if (cursor + width <= target.byteLength) {
361
+ weak = weakChecksum(target, cursor, width);
362
+ }
363
+ continue;
364
+ }
365
+ if (cursor + width >= target.byteLength)
366
+ break;
367
+ weak = rollWeak(weak, target[cursor] ?? 0, target[cursor + width] ?? 0, width);
368
+ cursor += 1;
369
+ }
370
+ appendOperation(operations, {
371
+ kind: "literal",
372
+ bytes: target.slice(literalStart),
373
+ });
374
+ return signature.version === 2
375
+ ? encodePatchV2(target.byteLength, operations)
376
+ : encodePatchV1(target.byteLength, operations);
377
+ }
378
+ /** Apply an untrusted patch with strict bounds; the route verifies SHA-256 next. */
379
+ function deltaPatchTargetSize(patch) {
380
+ if (sameMagic(patch, PATCH_MAGIC_V2)) {
381
+ const field = readVarUint(patch, 4);
382
+ if (field.value > exports.DELTA_MAX_FILE_BYTES) {
383
+ throw new Error("Delta patch target exceeds the transport limit");
384
+ }
385
+ return field.value;
386
+ }
387
+ if (patch.byteLength < PATCH_HEADER_V1_BYTES ||
388
+ !sameMagic(patch, PATCH_MAGIC_V1)) {
389
+ throw new Error("Delta patch is malformed");
390
+ }
391
+ const size = new DataView(patch.buffer, patch.byteOffset, patch.byteLength).getUint32(4, false);
392
+ if (size > exports.DELTA_MAX_FILE_BYTES) {
393
+ throw new Error("Delta patch target exceeds the transport limit");
394
+ }
395
+ return size;
396
+ }
397
+ function applyDeltaPatch(base, patch) {
398
+ if (sameMagic(patch, PATCH_MAGIC_V2)) {
399
+ return applyDeltaPatchV2(base, patch);
400
+ }
401
+ if (patch.byteLength < PATCH_HEADER_V1_BYTES ||
402
+ !sameMagic(patch, PATCH_MAGIC_V1)) {
403
+ throw new Error("Delta patch is malformed");
404
+ }
405
+ const view = new DataView(patch.buffer, patch.byteOffset, patch.byteLength);
406
+ const targetSize = view.getUint32(4, false);
407
+ const operationCount = view.getUint32(8, false);
408
+ if (targetSize > exports.DELTA_MAX_FILE_BYTES || operationCount > 1_000_000) {
409
+ throw new Error("Delta patch dimensions are invalid");
410
+ }
411
+ const out = new Uint8Array(targetSize);
412
+ let readAt = PATCH_HEADER_V1_BYTES;
413
+ let writeAt = 0;
414
+ for (let index = 0; index < operationCount; index += 1) {
415
+ if (readAt >= patch.byteLength)
416
+ throw new Error("Delta patch is truncated");
417
+ const opcode = patch[readAt];
418
+ if (opcode === 1) {
419
+ if (readAt + COPY_BYTES > patch.byteLength) {
420
+ throw new Error("Delta copy operation is truncated");
421
+ }
422
+ const offset = view.getUint32(readAt + 1, false);
423
+ const length = view.getUint32(readAt + 5, false);
424
+ if (length === 0 ||
425
+ offset + length > base.byteLength ||
426
+ writeAt + length > out.byteLength) {
427
+ throw new Error("Delta copy operation is out of bounds");
428
+ }
429
+ out.set(base.subarray(offset, offset + length), writeAt);
430
+ writeAt += length;
431
+ readAt += COPY_BYTES;
432
+ continue;
433
+ }
434
+ if (opcode === 0) {
435
+ if (readAt + LITERAL_HEADER_BYTES > patch.byteLength) {
436
+ throw new Error("Delta literal operation is truncated");
437
+ }
438
+ const length = view.getUint32(readAt + 1, false);
439
+ const from = readAt + LITERAL_HEADER_BYTES;
440
+ if (length === 0 ||
441
+ from + length > patch.byteLength ||
442
+ writeAt + length > out.byteLength) {
443
+ throw new Error("Delta literal operation is out of bounds");
444
+ }
445
+ out.set(patch.subarray(from, from + length), writeAt);
446
+ writeAt += length;
447
+ readAt = from + length;
448
+ continue;
449
+ }
450
+ throw new Error("Delta patch contains an unknown operation");
451
+ }
452
+ if (readAt !== patch.byteLength || writeAt !== out.byteLength) {
453
+ throw new Error("Delta patch does not reconstruct the declared target");
454
+ }
455
+ return out;
456
+ }
457
+ function applyDeltaPatchV2(base, patch) {
458
+ const targetField = readVarUint(patch, 4);
459
+ const targetSize = targetField.value;
460
+ const countField = readVarUint(patch, targetField.next);
461
+ const operationCount = countField.value;
462
+ if (targetSize > exports.DELTA_MAX_FILE_BYTES || operationCount > 1_000_000) {
463
+ throw new Error("Delta patch dimensions are invalid");
464
+ }
465
+ const out = new Uint8Array(targetSize);
466
+ let readAt = countField.next;
467
+ let writeAt = 0;
468
+ for (let index = 0; index < operationCount; index += 1) {
469
+ if (readAt >= patch.byteLength)
470
+ throw new Error("Delta patch is truncated");
471
+ const opcode = patch[readAt] ?? 0xff;
472
+ readAt += 1;
473
+ if (opcode === 1) {
474
+ const offsetField = readVarUint(patch, readAt);
475
+ const lengthField = readVarUint(patch, offsetField.next);
476
+ const offset = offsetField.value;
477
+ const length = lengthField.value;
478
+ if (length === 0 ||
479
+ offset + length > base.byteLength ||
480
+ writeAt + length > out.byteLength) {
481
+ throw new Error("Delta copy operation is out of bounds");
482
+ }
483
+ out.set(base.subarray(offset, offset + length), writeAt);
484
+ writeAt += length;
485
+ readAt = lengthField.next;
486
+ continue;
487
+ }
488
+ if (opcode === 0) {
489
+ const lengthField = readVarUint(patch, readAt);
490
+ const length = lengthField.value;
491
+ const from = lengthField.next;
492
+ if (length === 0 ||
493
+ from + length > patch.byteLength ||
494
+ writeAt + length > out.byteLength) {
495
+ throw new Error("Delta literal operation is out of bounds");
496
+ }
497
+ out.set(patch.subarray(from, from + length), writeAt);
498
+ writeAt += length;
499
+ readAt = from + length;
500
+ continue;
501
+ }
502
+ throw new Error("Delta patch contains an unknown operation");
503
+ }
504
+ if (readAt !== patch.byteLength || writeAt !== out.byteLength) {
505
+ throw new Error("Delta patch does not reconstruct the declared target");
506
+ }
507
+ return out;
508
+ }
509
+ function uuidBytes(value) {
510
+ const compact = value.replaceAll("-", "").toLowerCase();
511
+ if (!/^[0-9a-f]{32}$/.test(compact)) {
512
+ throw new Error("Delta base Version id is invalid");
513
+ }
514
+ const out = new Uint8Array(16);
515
+ for (let index = 0; index < out.byteLength; index += 1) {
516
+ out[index] = Number.parseInt(compact.slice(index * 2, index * 2 + 2), 16);
517
+ }
518
+ return out;
519
+ }
520
+ function digestBytes(value) {
521
+ if (!/^[0-9a-f]{64}$/.test(value)) {
522
+ throw new Error("Delta target digest is invalid");
523
+ }
524
+ const out = new Uint8Array(32);
525
+ for (let index = 0; index < out.byteLength; index += 1) {
526
+ out[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
527
+ }
528
+ return out;
529
+ }
530
+ function bytesHex(bytes) {
531
+ let value = "";
532
+ for (const byte of bytes)
533
+ value += byte.toString(16).padStart(2, "0");
534
+ return value;
535
+ }
536
+ function bytesUuid(bytes) {
537
+ const value = bytesHex(bytes);
538
+ return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
539
+ }
540
+ /**
541
+ * Frame V2 transport metadata without JSON field names or duplicate sizes.
542
+ *
543
+ * The selected immutable Version and path uniquely identify the base. Its
544
+ * digest is still checked when the signature is requested and again against
545
+ * the stored Version while reconstructing, so repeating 32 bytes in this
546
+ * second request adds no authority.
547
+ */
548
+ function encodeDeltaEnvelope(input) {
549
+ const path = new TextEncoder().encode(input.path);
550
+ const mediaType = new TextEncoder().encode(input.mediaType);
551
+ if (path.byteLength === 0 || path.byteLength > 0xffff) {
552
+ throw new Error("Delta path length is invalid");
553
+ }
554
+ if (mediaType.byteLength === 0 || mediaType.byteLength > 0xff) {
555
+ throw new Error("Delta media type length is invalid");
556
+ }
557
+ if (input.patch.byteLength === 0)
558
+ throw new Error("Delta patch is empty");
559
+ const out = new Uint8Array(ENVELOPE_V2_FIXED_BYTES + path.byteLength + mediaType.byteLength + input.patch.byteLength);
560
+ out.set(ENVELOPE_MAGIC_V2, 0);
561
+ const view = new DataView(out.buffer);
562
+ view.setUint16(4, path.byteLength, false);
563
+ out[6] = mediaType.byteLength;
564
+ out.set(uuidBytes(input.baseVersionId), 7);
565
+ out.set(digestBytes(input.sha256), 23);
566
+ let at = ENVELOPE_V2_FIXED_BYTES;
567
+ out.set(path, at);
568
+ at += path.byteLength;
569
+ out.set(mediaType, at);
570
+ at += mediaType.byteLength;
571
+ out.set(input.patch, at);
572
+ return out;
573
+ }
574
+ function isDeltaEnvelope(bytes) {
575
+ return sameMagic(bytes, ENVELOPE_MAGIC_V2);
576
+ }
577
+ function decodeDeltaEnvelope(bytes) {
578
+ if (bytes.byteLength < ENVELOPE_V2_FIXED_BYTES + 1 || !isDeltaEnvelope(bytes)) {
579
+ throw new Error("Delta envelope is malformed");
580
+ }
581
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
582
+ const pathLength = view.getUint16(4, false);
583
+ const mediaLength = bytes[6] ?? 0;
584
+ const patchAt = ENVELOPE_V2_FIXED_BYTES + pathLength + mediaLength;
585
+ if (pathLength === 0 || mediaLength === 0 || patchAt >= bytes.byteLength) {
586
+ throw new Error("Delta envelope dimensions are invalid");
587
+ }
588
+ let path;
589
+ let mediaType;
590
+ try {
591
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
592
+ path = decoder.decode(bytes.subarray(ENVELOPE_V2_FIXED_BYTES, ENVELOPE_V2_FIXED_BYTES + pathLength));
593
+ mediaType = decoder.decode(bytes.subarray(ENVELOPE_V2_FIXED_BYTES + pathLength, patchAt));
594
+ }
595
+ catch {
596
+ throw new Error("Delta envelope text is invalid UTF-8");
597
+ }
598
+ return {
599
+ baseVersionId: bytesUuid(bytes.subarray(7, 23)),
600
+ sha256: bytesHex(bytes.subarray(23, 55)),
601
+ path,
602
+ mediaType,
603
+ patch: bytes.subarray(patchAt),
604
+ };
605
+ }
606
+ /** Frame positional V2 signatures once, without repeating paths in the reply. */
607
+ function encodeDeltaSignatureBatch(signatures) {
608
+ if (signatures.length === 0 ||
609
+ signatures.length > exports.DELTA_BATCH_MAX_FILES) {
610
+ throw new Error("Delta signature batch count is invalid");
611
+ }
612
+ let total = 6;
613
+ for (const signature of signatures) {
614
+ if (parseDeltaSignature(signature).version !== 2) {
615
+ throw new Error("A batch may contain only V2 delta signatures");
616
+ }
617
+ total += 4 + signature.byteLength;
618
+ }
619
+ const out = new Uint8Array(total);
620
+ out.set(SIGNATURE_BATCH_MAGIC_V2, 0);
621
+ const view = new DataView(out.buffer);
622
+ view.setUint16(4, signatures.length, false);
623
+ let at = 6;
624
+ for (const signature of signatures) {
625
+ view.setUint32(at, signature.byteLength, false);
626
+ at += 4;
627
+ out.set(signature, at);
628
+ at += signature.byteLength;
629
+ }
630
+ return out;
631
+ }
632
+ function decodeDeltaSignatureBatch(bytes) {
633
+ if (bytes.byteLength < 6 || !sameMagic(bytes, SIGNATURE_BATCH_MAGIC_V2)) {
634
+ throw new Error("Delta signature batch is malformed");
635
+ }
636
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
637
+ const count = view.getUint16(4, false);
638
+ if (count === 0 || count > exports.DELTA_BATCH_MAX_FILES) {
639
+ throw new Error("Delta signature batch count is invalid");
640
+ }
641
+ const signatures = [];
642
+ let at = 6;
643
+ for (let index = 0; index < count; index += 1) {
644
+ if (at + 4 > bytes.byteLength) {
645
+ throw new Error("Delta signature batch is truncated");
646
+ }
647
+ const length = view.getUint32(at, false);
648
+ at += 4;
649
+ if (length === 0 || at + length > bytes.byteLength) {
650
+ throw new Error("Delta signature batch member is truncated");
651
+ }
652
+ const signature = parseDeltaSignature(bytes.subarray(at, at + length));
653
+ if (signature.version !== 2) {
654
+ throw new Error("Delta signature batch contains an old signature");
655
+ }
656
+ signatures.push(signature);
657
+ at += length;
658
+ }
659
+ if (at !== bytes.byteLength) {
660
+ throw new Error("Delta signature batch contains trailing bytes");
661
+ }
662
+ return signatures;
663
+ }
664
+ /**
665
+ * Frame several independently verifiable patches under one immutable base
666
+ * Version. Each member keeps its own path, target digest, media type and patch;
667
+ * only the Version id and HTTP round trip are shared.
668
+ */
669
+ function encodeDeltaBatchEnvelope(input) {
670
+ if (input.entries.length === 0 || input.entries.length > exports.DELTA_BATCH_MAX_FILES) {
671
+ throw new Error("Delta batch count is invalid");
672
+ }
673
+ const encoded = input.entries.map((entry) => {
674
+ const path = new TextEncoder().encode(entry.path);
675
+ const mediaType = new TextEncoder().encode(entry.mediaType);
676
+ if (path.byteLength === 0 || path.byteLength > 0xffff) {
677
+ throw new Error("Delta batch path length is invalid");
678
+ }
679
+ if (mediaType.byteLength === 0 || mediaType.byteLength > 0xff) {
680
+ throw new Error("Delta batch media type length is invalid");
681
+ }
682
+ if (entry.patch.byteLength === 0)
683
+ throw new Error("Delta batch patch is empty");
684
+ return { entry, path, mediaType };
685
+ });
686
+ const total = 22 +
687
+ encoded.reduce((sum, item) => sum +
688
+ 39 +
689
+ item.path.byteLength +
690
+ item.mediaType.byteLength +
691
+ item.entry.patch.byteLength, 0);
692
+ const out = new Uint8Array(total);
693
+ out.set(DELTA_BATCH_MAGIC_V2, 0);
694
+ out.set(uuidBytes(input.baseVersionId), 4);
695
+ const view = new DataView(out.buffer);
696
+ view.setUint16(20, encoded.length, false);
697
+ let at = 22;
698
+ for (const item of encoded) {
699
+ view.setUint16(at, item.path.byteLength, false);
700
+ out[at + 2] = item.mediaType.byteLength;
701
+ out.set(digestBytes(item.entry.sha256), at + 3);
702
+ view.setUint32(at + 35, item.entry.patch.byteLength, false);
703
+ at += 39;
704
+ out.set(item.path, at);
705
+ at += item.path.byteLength;
706
+ out.set(item.mediaType, at);
707
+ at += item.mediaType.byteLength;
708
+ out.set(item.entry.patch, at);
709
+ at += item.entry.patch.byteLength;
710
+ }
711
+ return out;
712
+ }
713
+ function decodeDeltaBatchEnvelope(bytes) {
714
+ if (bytes.byteLength < 23 || !sameMagic(bytes, DELTA_BATCH_MAGIC_V2)) {
715
+ throw new Error("Delta batch envelope is malformed");
716
+ }
717
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
718
+ const count = view.getUint16(20, false);
719
+ if (count === 0 || count > exports.DELTA_BATCH_MAX_FILES) {
720
+ throw new Error("Delta batch count is invalid");
721
+ }
722
+ const entries = [];
723
+ const paths = new Set();
724
+ let at = 22;
725
+ for (let index = 0; index < count; index += 1) {
726
+ if (at + 39 > bytes.byteLength) {
727
+ throw new Error("Delta batch member is truncated");
728
+ }
729
+ const pathLength = view.getUint16(at, false);
730
+ const mediaLength = bytes[at + 2] ?? 0;
731
+ const sha256 = bytesHex(bytes.subarray(at + 3, at + 35));
732
+ const patchLength = view.getUint32(at + 35, false);
733
+ at += 39;
734
+ const pathAt = at;
735
+ const mediaAt = pathAt + pathLength;
736
+ const patchAt = mediaAt + mediaLength;
737
+ const next = patchAt + patchLength;
738
+ if (pathLength === 0 ||
739
+ mediaLength === 0 ||
740
+ patchLength === 0 ||
741
+ next > bytes.byteLength) {
742
+ throw new Error("Delta batch member dimensions are invalid");
743
+ }
744
+ let path;
745
+ let mediaType;
746
+ try {
747
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
748
+ path = decoder.decode(bytes.subarray(pathAt, mediaAt));
749
+ mediaType = decoder.decode(bytes.subarray(mediaAt, patchAt));
750
+ }
751
+ catch {
752
+ throw new Error("Delta batch text is invalid UTF-8");
753
+ }
754
+ if (paths.has(path))
755
+ throw new Error("Delta batch contains a duplicate path");
756
+ paths.add(path);
757
+ entries.push({
758
+ path,
759
+ mediaType,
760
+ sha256,
761
+ patch: bytes.subarray(patchAt, next),
762
+ });
763
+ at = next;
764
+ }
765
+ if (at !== bytes.byteLength) {
766
+ throw new Error("Delta batch contains trailing bytes");
767
+ }
768
+ return {
769
+ baseVersionId: bytesUuid(bytes.subarray(4, 20)),
770
+ entries,
771
+ };
772
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",