@git-stunts/git-warp 10.1.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.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +16 -0
  3. package/README.md +480 -0
  4. package/SECURITY.md +30 -0
  5. package/bin/git-warp +24 -0
  6. package/bin/warp-graph.js +1574 -0
  7. package/index.d.ts +2366 -0
  8. package/index.js +180 -0
  9. package/package.json +129 -0
  10. package/scripts/install-git-warp.sh +258 -0
  11. package/scripts/uninstall-git-warp.sh +139 -0
  12. package/src/domain/WarpGraph.js +3157 -0
  13. package/src/domain/crdt/Dot.js +160 -0
  14. package/src/domain/crdt/LWW.js +154 -0
  15. package/src/domain/crdt/ORSet.js +371 -0
  16. package/src/domain/crdt/VersionVector.js +222 -0
  17. package/src/domain/entities/GraphNode.js +60 -0
  18. package/src/domain/errors/EmptyMessageError.js +47 -0
  19. package/src/domain/errors/ForkError.js +30 -0
  20. package/src/domain/errors/IndexError.js +23 -0
  21. package/src/domain/errors/OperationAbortedError.js +22 -0
  22. package/src/domain/errors/QueryError.js +39 -0
  23. package/src/domain/errors/SchemaUnsupportedError.js +17 -0
  24. package/src/domain/errors/ShardCorruptionError.js +56 -0
  25. package/src/domain/errors/ShardLoadError.js +57 -0
  26. package/src/domain/errors/ShardValidationError.js +61 -0
  27. package/src/domain/errors/StorageError.js +57 -0
  28. package/src/domain/errors/SyncError.js +30 -0
  29. package/src/domain/errors/TraversalError.js +23 -0
  30. package/src/domain/errors/WarpError.js +31 -0
  31. package/src/domain/errors/WormholeError.js +28 -0
  32. package/src/domain/errors/WriterError.js +39 -0
  33. package/src/domain/errors/index.js +21 -0
  34. package/src/domain/services/AnchorMessageCodec.js +99 -0
  35. package/src/domain/services/BitmapIndexBuilder.js +225 -0
  36. package/src/domain/services/BitmapIndexReader.js +435 -0
  37. package/src/domain/services/BoundaryTransitionRecord.js +463 -0
  38. package/src/domain/services/CheckpointMessageCodec.js +147 -0
  39. package/src/domain/services/CheckpointSerializerV5.js +281 -0
  40. package/src/domain/services/CheckpointService.js +384 -0
  41. package/src/domain/services/CommitDagTraversalService.js +156 -0
  42. package/src/domain/services/DagPathFinding.js +712 -0
  43. package/src/domain/services/DagTopology.js +239 -0
  44. package/src/domain/services/DagTraversal.js +245 -0
  45. package/src/domain/services/Frontier.js +108 -0
  46. package/src/domain/services/GCMetrics.js +101 -0
  47. package/src/domain/services/GCPolicy.js +122 -0
  48. package/src/domain/services/GitLogParser.js +205 -0
  49. package/src/domain/services/HealthCheckService.js +246 -0
  50. package/src/domain/services/HookInstaller.js +326 -0
  51. package/src/domain/services/HttpSyncServer.js +262 -0
  52. package/src/domain/services/IndexRebuildService.js +426 -0
  53. package/src/domain/services/IndexStalenessChecker.js +103 -0
  54. package/src/domain/services/JoinReducer.js +582 -0
  55. package/src/domain/services/KeyCodec.js +113 -0
  56. package/src/domain/services/LegacyAnchorDetector.js +67 -0
  57. package/src/domain/services/LogicalTraversal.js +351 -0
  58. package/src/domain/services/MessageCodecInternal.js +132 -0
  59. package/src/domain/services/MessageSchemaDetector.js +145 -0
  60. package/src/domain/services/MigrationService.js +55 -0
  61. package/src/domain/services/ObserverView.js +265 -0
  62. package/src/domain/services/PatchBuilderV2.js +669 -0
  63. package/src/domain/services/PatchMessageCodec.js +140 -0
  64. package/src/domain/services/ProvenanceIndex.js +337 -0
  65. package/src/domain/services/ProvenancePayload.js +242 -0
  66. package/src/domain/services/QueryBuilder.js +835 -0
  67. package/src/domain/services/StateDiff.js +300 -0
  68. package/src/domain/services/StateSerializerV5.js +156 -0
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
  70. package/src/domain/services/SyncProtocol.js +593 -0
  71. package/src/domain/services/TemporalQuery.js +201 -0
  72. package/src/domain/services/TranslationCost.js +221 -0
  73. package/src/domain/services/TraversalService.js +8 -0
  74. package/src/domain/services/WarpMessageCodec.js +29 -0
  75. package/src/domain/services/WarpStateIndexBuilder.js +127 -0
  76. package/src/domain/services/WormholeService.js +353 -0
  77. package/src/domain/types/TickReceipt.js +285 -0
  78. package/src/domain/types/WarpTypes.js +209 -0
  79. package/src/domain/types/WarpTypesV2.js +200 -0
  80. package/src/domain/utils/CachedValue.js +140 -0
  81. package/src/domain/utils/EventId.js +89 -0
  82. package/src/domain/utils/LRUCache.js +112 -0
  83. package/src/domain/utils/MinHeap.js +114 -0
  84. package/src/domain/utils/RefLayout.js +280 -0
  85. package/src/domain/utils/WriterId.js +205 -0
  86. package/src/domain/utils/cancellation.js +33 -0
  87. package/src/domain/utils/canonicalStringify.js +42 -0
  88. package/src/domain/utils/defaultClock.js +20 -0
  89. package/src/domain/utils/defaultCodec.js +51 -0
  90. package/src/domain/utils/nullLogger.js +21 -0
  91. package/src/domain/utils/roaring.js +181 -0
  92. package/src/domain/utils/shardVersion.js +9 -0
  93. package/src/domain/warp/PatchSession.js +217 -0
  94. package/src/domain/warp/Writer.js +181 -0
  95. package/src/hooks/post-merge.sh +60 -0
  96. package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
  97. package/src/infrastructure/adapters/ClockAdapter.js +57 -0
  98. package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
  99. package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
  100. package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
  101. package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
  102. package/src/infrastructure/adapters/NoOpLogger.js +62 -0
  103. package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
  105. package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
  106. package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
  107. package/src/infrastructure/codecs/CborCodec.js +384 -0
  108. package/src/ports/BlobPort.js +30 -0
  109. package/src/ports/ClockPort.js +25 -0
  110. package/src/ports/CodecPort.js +25 -0
  111. package/src/ports/CommitPort.js +114 -0
  112. package/src/ports/ConfigPort.js +31 -0
  113. package/src/ports/CryptoPort.js +38 -0
  114. package/src/ports/GraphPersistencePort.js +57 -0
  115. package/src/ports/HttpServerPort.js +25 -0
  116. package/src/ports/IndexStoragePort.js +39 -0
  117. package/src/ports/LoggerPort.js +68 -0
  118. package/src/ports/RefPort.js +51 -0
  119. package/src/ports/TreePort.js +51 -0
  120. package/src/visualization/index.js +26 -0
  121. package/src/visualization/layouts/converters.js +75 -0
  122. package/src/visualization/layouts/elkAdapter.js +86 -0
  123. package/src/visualization/layouts/elkLayout.js +95 -0
  124. package/src/visualization/layouts/index.js +29 -0
  125. package/src/visualization/renderers/ascii/box.js +16 -0
  126. package/src/visualization/renderers/ascii/check.js +271 -0
  127. package/src/visualization/renderers/ascii/colors.js +13 -0
  128. package/src/visualization/renderers/ascii/formatters.js +73 -0
  129. package/src/visualization/renderers/ascii/graph.js +344 -0
  130. package/src/visualization/renderers/ascii/history.js +335 -0
  131. package/src/visualization/renderers/ascii/index.js +14 -0
  132. package/src/visualization/renderers/ascii/info.js +245 -0
  133. package/src/visualization/renderers/ascii/materialize.js +255 -0
  134. package/src/visualization/renderers/ascii/path.js +240 -0
  135. package/src/visualization/renderers/ascii/progress.js +32 -0
  136. package/src/visualization/renderers/ascii/symbols.js +33 -0
  137. package/src/visualization/renderers/ascii/table.js +19 -0
  138. package/src/visualization/renderers/browser/index.js +1 -0
  139. package/src/visualization/renderers/svg/index.js +159 -0
  140. package/src/visualization/utils/ansi.js +14 -0
  141. package/src/visualization/utils/time.js +40 -0
  142. package/src/visualization/utils/truncate.js +40 -0
  143. package/src/visualization/utils/unicode.js +52 -0
@@ -0,0 +1,463 @@
1
+ /**
2
+ * BoundaryTransitionRecord (BTR) - Tamper-Evident Provenance Packaging
3
+ *
4
+ * Implements Boundary Transition Records from Paper III (Computational Holography).
5
+ *
6
+ * A BTR binds (h_in, h_out, U_0, P, t, kappa):
7
+ * - h_in: hash of input state
8
+ * - h_out: hash of output state (after replay)
9
+ * - U_0: initial state snapshot (serialized)
10
+ * - P: provenance payload
11
+ * - t: timestamp
12
+ * - kappa: authentication tag (HMAC)
13
+ *
14
+ * BTRs enable tamper-evident exchange of graph segments between parties
15
+ * who don't share full history. The HMAC ensures integrity; replay
16
+ * verification ensures correctness.
17
+ *
18
+ * @module domain/services/BoundaryTransitionRecord
19
+ * @see Paper III, Section 4 -- Boundary Transition Records
20
+ */
21
+
22
+ import defaultCodec from '../utils/defaultCodec.js';
23
+ import { ProvenancePayload } from './ProvenancePayload.js';
24
+ import { serializeFullStateV5, deserializeFullStateV5, computeStateHashV5 } from './StateSerializerV5.js';
25
+
26
+ /**
27
+ * Converts a Uint8Array to a hex string.
28
+ * @param {Uint8Array} bytes
29
+ * @returns {string}
30
+ */
31
+ function uint8ArrayToHex(bytes) {
32
+ let hex = '';
33
+ for (let i = 0; i < bytes.length; i++) {
34
+ hex += bytes[i].toString(16).padStart(2, '0');
35
+ }
36
+ return hex;
37
+ }
38
+
39
+ /**
40
+ * Converts a hex string to a Uint8Array.
41
+ * @param {string} hex
42
+ * @returns {Uint8Array}
43
+ */
44
+ function hexToUint8Array(hex) {
45
+ if (typeof hex !== 'string' || hex.length % 2 !== 0) {
46
+ throw new RangeError(`Invalid hex string (length ${hex?.length})`);
47
+ }
48
+ const bytes = new Uint8Array(hex.length / 2);
49
+ for (let i = 0; i < hex.length; i += 2) {
50
+ const byte = parseInt(hex.substring(i, i + 2), 16);
51
+ if (Number.isNaN(byte)) {
52
+ throw new RangeError(`Invalid hex byte at offset ${i}: ${hex.substring(i, i + 2)}`);
53
+ }
54
+ bytes[i / 2] = byte;
55
+ }
56
+ return bytes;
57
+ }
58
+
59
+ /**
60
+ * HMAC algorithm used for authentication tags.
61
+ * SHA-256 provides 256-bit security with wide hardware support.
62
+ * @const {string}
63
+ */
64
+ const HMAC_ALGORITHM = 'sha256';
65
+
66
+ /**
67
+ * BTR format version for future compatibility.
68
+ * @const {number}
69
+ */
70
+ const BTR_VERSION = 1;
71
+
72
+ /**
73
+ * Computes HMAC authentication tag over BTR fields.
74
+ *
75
+ * The tag is computed over the canonical CBOR encoding of:
76
+ * (version, h_in, h_out, U_0, P, t)
77
+ *
78
+ * This ensures all fields are covered and the encoding is deterministic.
79
+ *
80
+ * @param {Object} fields - BTR fields to authenticate
81
+ * @param {number} fields.version - BTR format version
82
+ * @param {string} fields.h_in - Hash of input state
83
+ * @param {string} fields.h_out - Hash of output state
84
+ * @param {Uint8Array} fields.U_0 - Serialized initial state
85
+ * @param {Array} fields.P - Serialized provenance payload
86
+ * @param {string} fields.t - ISO timestamp
87
+ * @param {string|Uint8Array} key - HMAC key
88
+ * @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
89
+ * @returns {Promise<string>} Hex-encoded HMAC tag
90
+ * @private
91
+ */
92
+ async function computeHmac(fields, key, { crypto, codec }) {
93
+ const c = codec || defaultCodec;
94
+ const message = c.encode({
95
+ version: fields.version,
96
+ h_in: fields.h_in,
97
+ h_out: fields.h_out,
98
+ U_0: fields.U_0,
99
+ P: fields.P,
100
+ t: fields.t,
101
+ });
102
+
103
+ const rawHmac = await crypto.hmac(HMAC_ALGORITHM, key, message);
104
+ const bytes = rawHmac instanceof Uint8Array ? rawHmac : new Uint8Array(rawHmac);
105
+ return uint8ArrayToHex(bytes);
106
+ }
107
+
108
+ /**
109
+ * @typedef {Object} BTR
110
+ * @property {number} version - BTR format version
111
+ * @property {string} h_in - Hash of input state (hex SHA-256)
112
+ * @property {string} h_out - Hash of output state (hex SHA-256)
113
+ * @property {Uint8Array} U_0 - Serialized initial state (CBOR)
114
+ * @property {Array} P - Serialized provenance payload
115
+ * @property {string} t - ISO 8601 timestamp
116
+ * @property {string} kappa - Authentication tag (hex HMAC-SHA256)
117
+ */
118
+
119
+ /**
120
+ * @typedef {Object} VerificationResult
121
+ * @property {boolean} valid - Whether the BTR is valid
122
+ * @property {string} [reason] - Reason for failure (if invalid)
123
+ */
124
+
125
+ /**
126
+ * Creates a Boundary Transition Record from an initial state and payload.
127
+ *
128
+ * The BTR captures:
129
+ * 1. The hash of the initial state (h_in)
130
+ * 2. The provenance payload for replay
131
+ * 3. The hash of the final state after replay (h_out)
132
+ * 4. A timestamp
133
+ * 5. An HMAC authentication tag covering all fields
134
+ *
135
+ * ## Security Properties
136
+ *
137
+ * - **Integrity**: The HMAC tag detects any modification to any field
138
+ * - **Authenticity**: Only holders of the key can create valid BTRs
139
+ * - **Non-repudiation**: The BTR binds h_in → h_out via the payload
140
+ *
141
+ * ## Example
142
+ *
143
+ * ```javascript
144
+ * const initialState = createEmptyStateV5();
145
+ * const payload = new ProvenancePayload([...patches]);
146
+ * const key = 'secret-key';
147
+ *
148
+ * const btr = createBTR(initialState, payload, { key });
149
+ * // btr.h_in, btr.h_out, btr.kappa are all set
150
+ * ```
151
+ *
152
+ * @param {import('./JoinReducer.js').WarpStateV5} initialState - The input state U_0
153
+ * @param {ProvenancePayload} payload - The provenance payload P
154
+ * @param {Object} options - BTR creation options
155
+ * @param {string|Uint8Array} options.key - HMAC key for authentication
156
+ * @param {string} [options.timestamp] - ISO timestamp (defaults to now)
157
+ * @param {import('../../ports/CryptoPort.js').default} options.crypto - CryptoPort instance
158
+ * @returns {Promise<BTR>} The created BTR
159
+ * @throws {TypeError} If payload is not a ProvenancePayload
160
+ */
161
+ export async function createBTR(initialState, payload, options) {
162
+ if (!(payload instanceof ProvenancePayload)) {
163
+ throw new TypeError('payload must be a ProvenancePayload');
164
+ }
165
+
166
+ const { key, timestamp = new Date().toISOString(), crypto, codec } = options;
167
+
168
+ // Validate HMAC key is not empty/falsy
169
+ if (!key || (typeof key === 'string' && key.length === 0) ||
170
+ (ArrayBuffer.isView(key) && key.byteLength === 0)) {
171
+ throw new Error('Invalid HMAC key: key must not be empty');
172
+ }
173
+
174
+ const h_in = await computeStateHashV5(initialState, { crypto, codec });
175
+ const U_0 = serializeFullStateV5(initialState, { codec });
176
+ const finalState = payload.replay(initialState);
177
+ const h_out = await computeStateHashV5(finalState, { crypto, codec });
178
+ const P = payload.toJSON();
179
+
180
+ const fields = { version: BTR_VERSION, h_in, h_out, U_0, P, t: timestamp };
181
+ const kappa = await computeHmac(fields, key, { crypto, codec });
182
+
183
+ return { ...fields, kappa };
184
+ }
185
+
186
+ const REQUIRED_FIELDS = ['version', 'h_in', 'h_out', 'U_0', 'P', 't', 'kappa'];
187
+
188
+ /**
189
+ * Validates BTR structure and returns failure reason if invalid.
190
+ *
191
+ * @param {*} btr - The BTR object to validate
192
+ * @returns {string|null} Error message if invalid, null if valid
193
+ * @private
194
+ */
195
+ function validateBTRStructure(btr) {
196
+ if (!btr || typeof btr !== 'object') {
197
+ return 'BTR must be an object';
198
+ }
199
+ for (const field of REQUIRED_FIELDS) {
200
+ if (!(field in btr)) {
201
+ return `Missing required field: ${field}`;
202
+ }
203
+ }
204
+ if (btr.version !== BTR_VERSION) {
205
+ return `Unsupported BTR version: ${btr.version} (expected ${BTR_VERSION})`;
206
+ }
207
+ return null;
208
+ }
209
+
210
+ /**
211
+ * Verifies HMAC authentication tag using timing-safe comparison.
212
+ *
213
+ * @param {BTR} btr - The BTR to verify
214
+ * @param {string|Uint8Array} key - HMAC key
215
+ * @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
216
+ * @returns {Promise<boolean>} True if the HMAC tag matches
217
+ * @private
218
+ */
219
+ async function verifyHmac(btr, key, { crypto, codec }) {
220
+ const fields = {
221
+ version: btr.version,
222
+ h_in: btr.h_in,
223
+ h_out: btr.h_out,
224
+ U_0: btr.U_0,
225
+ P: btr.P,
226
+ t: btr.t,
227
+ };
228
+ const expectedKappa = await computeHmac(fields, key, { crypto, codec });
229
+
230
+ // Convert hex strings to byte arrays for timing-safe comparison
231
+ const actualBuf = hexToUint8Array(btr.kappa);
232
+ const expectedBuf = hexToUint8Array(expectedKappa);
233
+
234
+ // Check lengths first to avoid timingSafeEqual throwing on length mismatch
235
+ if (actualBuf.length !== expectedBuf.length) {
236
+ return false;
237
+ }
238
+
239
+ return crypto.timingSafeEqual(actualBuf, expectedBuf);
240
+ }
241
+
242
+ /**
243
+ * Verifies replay produces expected h_out.
244
+ *
245
+ * @param {BTR} btr - The BTR to verify
246
+ * @returns {Promise<string|null>} Error message if replay mismatch, null if valid
247
+ * @private
248
+ */
249
+ async function verifyReplayHash(btr, { crypto, codec } = {}) {
250
+ try {
251
+ const result = await replayBTR(btr, { crypto, codec });
252
+ if (result.h_out !== btr.h_out) {
253
+ return `Replay produced different h_out: expected ${btr.h_out}, got ${result.h_out}`;
254
+ }
255
+ return null;
256
+ } catch (err) {
257
+ return `Replay failed: ${err.message}`;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Verifies a Boundary Transition Record.
263
+ *
264
+ * Verification checks:
265
+ * 1. **HMAC verification**: The authentication tag matches
266
+ * 2. **Replay verification** (optional): Replaying P from U_0 produces h_out
267
+ *
268
+ * The HMAC check is fast (O(1) relative to BTR size). Replay verification
269
+ * is O(|P|) and optional for performance-sensitive scenarios.
270
+ *
271
+ * @param {BTR} btr - The BTR to verify
272
+ * @param {string|Uint8Array} key - HMAC key
273
+ * @param {Object} [options] - Verification options
274
+ * @param {boolean} [options.verifyReplay=false] - Also verify replay produces h_out
275
+ * @param {import('../../ports/CryptoPort.js').default} options.crypto - CryptoPort instance
276
+ * @returns {Promise<VerificationResult>} Verification result with valid flag and optional reason
277
+ */
278
+ export async function verifyBTR(btr, key, options = {}) {
279
+ const { crypto, codec } = options;
280
+
281
+ const structureError = validateBTRStructure(btr);
282
+ if (structureError) {
283
+ return { valid: false, reason: structureError };
284
+ }
285
+
286
+ let hmacValid;
287
+ try {
288
+ hmacValid = await verifyHmac(btr, key, { crypto, codec });
289
+ } catch (err) {
290
+ if (err instanceof RangeError) {
291
+ return { valid: false, reason: `Invalid hex in authentication tag: ${err.message}` };
292
+ }
293
+ throw err;
294
+ }
295
+ if (!hmacValid) {
296
+ return { valid: false, reason: 'Authentication tag mismatch' };
297
+ }
298
+
299
+ if (options.verifyReplay) {
300
+ const replayError = await verifyReplayHash(btr, { crypto, codec });
301
+ if (replayError) {
302
+ return { valid: false, reason: replayError };
303
+ }
304
+ }
305
+
306
+ return { valid: true };
307
+ }
308
+
309
+ /**
310
+ * Replays a BTR to produce the final state.
311
+ *
312
+ * This implements the computational holography theorem: given the boundary
313
+ * encoding (U_0, P), replay uniquely determines the interior worldline.
314
+ *
315
+ * @param {BTR} btr - The BTR to replay
316
+ * @returns {Promise<{ state: import('./JoinReducer.js').WarpStateV5, h_out: string }>}
317
+ * The final state and its hash
318
+ * @throws {Error} If replay fails
319
+ */
320
+ export async function replayBTR(btr, { crypto, codec } = {}) {
321
+ // Deserialize initial state from U_0
322
+ // Note: U_0 is the full serialized state (via serializeFullStateV5)
323
+ const initialState = deserializeInitialState(btr.U_0, { codec });
324
+
325
+ // Reconstruct payload
326
+ const payload = ProvenancePayload.fromJSON(btr.P);
327
+
328
+ // Replay
329
+ const finalState = payload.replay(initialState);
330
+
331
+ // Compute h_out
332
+ const h_out = await computeStateHashV5(finalState, { crypto, codec });
333
+
334
+ return { state: finalState, h_out };
335
+ }
336
+
337
+ /**
338
+ * Deserializes the initial state from the U_0 field.
339
+ *
340
+ * The U_0 field contains the complete WarpStateV5 serialized via
341
+ * serializeFullStateV5, including full CRDT internals (ORSet entries,
342
+ * tombstones, LWW registers, version vectors).
343
+ *
344
+ * This ensures replay starts from the exact initial state, producing
345
+ * the correct h_out hash.
346
+ *
347
+ * @param {Uint8Array} U_0 - Serialized full state
348
+ * @returns {import('./JoinReducer.js').WarpStateV5} The deserialized state
349
+ * @private
350
+ */
351
+ function deserializeInitialState(U_0, { codec } = {}) {
352
+ return deserializeFullStateV5(U_0, { codec });
353
+ }
354
+
355
+ /**
356
+ * Serializes a BTR to CBOR bytes for transport.
357
+ *
358
+ * The serialized form is deterministic (canonical CBOR encoding),
359
+ * enabling byte-for-byte comparison of BTRs.
360
+ *
361
+ * @param {BTR} btr - The BTR to serialize
362
+ * @param {Object} [options]
363
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
364
+ * @returns {Uint8Array} CBOR-encoded BTR
365
+ */
366
+ export function serializeBTR(btr, { codec } = {}) {
367
+ const c = codec || defaultCodec;
368
+ return c.encode({
369
+ version: btr.version,
370
+ h_in: btr.h_in,
371
+ h_out: btr.h_out,
372
+ U_0: btr.U_0,
373
+ P: btr.P,
374
+ t: btr.t,
375
+ kappa: btr.kappa,
376
+ });
377
+ }
378
+
379
+ /**
380
+ * Deserializes a BTR from CBOR bytes.
381
+ *
382
+ * @param {Uint8Array} bytes - CBOR-encoded BTR
383
+ * @param {Object} [options]
384
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
385
+ * @returns {BTR} The deserialized BTR
386
+ * @throws {Error} If the bytes are not valid CBOR or missing required fields
387
+ */
388
+ export function deserializeBTR(bytes, { codec } = {}) {
389
+ const c = codec || defaultCodec;
390
+ const obj = c.decode(bytes);
391
+
392
+ // Validate structure (reuse module-level constant for consistency with validateBTRStructure)
393
+ for (const field of REQUIRED_FIELDS) {
394
+ if (!(field in obj)) {
395
+ throw new Error(`Invalid BTR: missing field ${field}`);
396
+ }
397
+ }
398
+
399
+ return {
400
+ version: obj.version,
401
+ h_in: obj.h_in,
402
+ h_out: obj.h_out,
403
+ U_0: obj.U_0,
404
+ P: obj.P,
405
+ t: obj.t,
406
+ kappa: obj.kappa,
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Gets the initial state hash from a BTR without full deserialization.
412
+ *
413
+ * @param {BTR} btr - The BTR
414
+ * @returns {string} The h_in hash
415
+ */
416
+ export function getBTRInputHash(btr) {
417
+ return btr.h_in;
418
+ }
419
+
420
+ /**
421
+ * Gets the output state hash from a BTR without replay.
422
+ *
423
+ * @param {BTR} btr - The BTR
424
+ * @returns {string} The h_out hash
425
+ */
426
+ export function getBTROutputHash(btr) {
427
+ return btr.h_out;
428
+ }
429
+
430
+ /**
431
+ * Gets the timestamp from a BTR.
432
+ *
433
+ * @param {BTR} btr - The BTR
434
+ * @returns {string} ISO 8601 timestamp
435
+ */
436
+ export function getBTRTimestamp(btr) {
437
+ return btr.t;
438
+ }
439
+
440
+ /**
441
+ * Gets the payload length (number of patches) from a BTR.
442
+ *
443
+ * @param {BTR} btr - The BTR
444
+ * @returns {number} Number of patches in the payload
445
+ */
446
+ export function getBTRPayloadLength(btr) {
447
+ return Array.isArray(btr.P) ? btr.P.length : 0;
448
+ }
449
+
450
+ /**
451
+ * Default export with all BTR functions.
452
+ */
453
+ export default {
454
+ createBTR,
455
+ verifyBTR,
456
+ replayBTR,
457
+ serializeBTR,
458
+ deserializeBTR,
459
+ getBTRInputHash,
460
+ getBTROutputHash,
461
+ getBTRTimestamp,
462
+ getBTRPayloadLength,
463
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Checkpoint message encoding and decoding for WARP commit messages.
3
+ *
4
+ * Handles the 'checkpoint' message type which contains a snapshot of
5
+ * materialized graph state. See {@link module:domain/services/WarpMessageCodec}
6
+ * for the facade that re-exports all codec functions.
7
+ *
8
+ * @module domain/services/CheckpointMessageCodec
9
+ */
10
+
11
+ import { validateGraphName } from '../utils/RefLayout.js';
12
+ import {
13
+ getCodec,
14
+ MESSAGE_TITLES,
15
+ TRAILER_KEYS,
16
+ validateOid,
17
+ validateSha256,
18
+ validateSchema,
19
+ } from './MessageCodecInternal.js';
20
+
21
+ // -----------------------------------------------------------------------------
22
+ // Encoder
23
+ // -----------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Encodes a checkpoint commit message.
27
+ *
28
+ * @param {Object} options - The checkpoint message options
29
+ * @param {string} options.graph - The graph name
30
+ * @param {string} options.stateHash - The SHA-256 hash of the materialized state
31
+ * @param {string} options.frontierOid - The OID of the frontier blob
32
+ * @param {string} options.indexOid - The OID of the index tree
33
+ * @param {number} [options.schema=2] - The schema version (defaults to 2 for new messages)
34
+ * @returns {string} The encoded commit message
35
+ * @throws {Error} If any validation fails
36
+ *
37
+ * @example
38
+ * const message = encodeCheckpointMessage({
39
+ * graph: 'events',
40
+ * stateHash: 'abc123...' // 64-char hex
41
+ * frontierOid: 'def456...' // 40-char hex
42
+ * indexOid: 'ghi789...' // 40-char hex
43
+ * });
44
+ */
45
+ export function encodeCheckpointMessage({ graph, stateHash, frontierOid, indexOid, schema = 2 }) {
46
+ // Validate inputs
47
+ validateGraphName(graph);
48
+ validateSha256(stateHash, 'stateHash');
49
+ validateOid(frontierOid, 'frontierOid');
50
+ validateOid(indexOid, 'indexOid');
51
+ validateSchema(schema);
52
+
53
+ const codec = getCodec();
54
+ const trailers = {
55
+ [TRAILER_KEYS.kind]: 'checkpoint',
56
+ [TRAILER_KEYS.graph]: graph,
57
+ [TRAILER_KEYS.stateHash]: stateHash,
58
+ [TRAILER_KEYS.frontierOid]: frontierOid,
59
+ [TRAILER_KEYS.indexOid]: indexOid,
60
+ [TRAILER_KEYS.schema]: String(schema),
61
+ };
62
+
63
+ // Add checkpoint version marker for V5 format (schema:2 and schema:3)
64
+ if (schema === 2 || schema === 3) {
65
+ trailers[TRAILER_KEYS.checkpointVersion] = 'v5';
66
+ }
67
+
68
+ return codec.encode({
69
+ title: MESSAGE_TITLES.checkpoint,
70
+ trailers,
71
+ });
72
+ }
73
+
74
+ // -----------------------------------------------------------------------------
75
+ // Decoder
76
+ // -----------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Decodes a checkpoint commit message.
80
+ *
81
+ * @param {string} message - The raw commit message
82
+ * @returns {Object} The decoded checkpoint message
83
+ * @returns {string} return.kind - Always 'checkpoint'
84
+ * @returns {string} return.graph - The graph name
85
+ * @returns {string} return.stateHash - The SHA-256 state hash
86
+ * @returns {string} return.frontierOid - The frontier blob OID
87
+ * @returns {string} return.indexOid - The index tree OID
88
+ * @returns {number} return.schema - The schema version
89
+ * @throws {Error} If the message is not a valid checkpoint message
90
+ *
91
+ * @example
92
+ * const { kind, graph, stateHash, frontierOid, indexOid, schema } = decodeCheckpointMessage(message);
93
+ */
94
+ export function decodeCheckpointMessage(message) {
95
+ const codec = getCodec();
96
+ const decoded = codec.decode(message);
97
+ const { trailers } = decoded;
98
+
99
+ // Validate kind discriminator
100
+ const kind = trailers[TRAILER_KEYS.kind];
101
+ if (kind !== 'checkpoint') {
102
+ throw new Error(`Invalid checkpoint message: eg-kind must be 'checkpoint', got '${kind}'`);
103
+ }
104
+
105
+ // Extract and validate required fields
106
+ const graph = trailers[TRAILER_KEYS.graph];
107
+ if (!graph) {
108
+ throw new Error('Invalid checkpoint message: missing required trailer eg-graph');
109
+ }
110
+
111
+ const stateHash = trailers[TRAILER_KEYS.stateHash];
112
+ if (!stateHash) {
113
+ throw new Error('Invalid checkpoint message: missing required trailer eg-state-hash');
114
+ }
115
+
116
+ const frontierOid = trailers[TRAILER_KEYS.frontierOid];
117
+ if (!frontierOid) {
118
+ throw new Error('Invalid checkpoint message: missing required trailer eg-frontier-oid');
119
+ }
120
+
121
+ const indexOid = trailers[TRAILER_KEYS.indexOid];
122
+ if (!indexOid) {
123
+ throw new Error('Invalid checkpoint message: missing required trailer eg-index-oid');
124
+ }
125
+
126
+ const schemaStr = trailers[TRAILER_KEYS.schema];
127
+ if (!schemaStr) {
128
+ throw new Error('Invalid checkpoint message: missing required trailer eg-schema');
129
+ }
130
+ const schema = parseInt(schemaStr, 10);
131
+ if (!Number.isInteger(schema) || schema < 1) {
132
+ throw new Error(`Invalid checkpoint message: eg-schema must be a positive integer, got '${schemaStr}'`);
133
+ }
134
+
135
+ // Extract optional checkpoint version (v5 for schema:2)
136
+ const checkpointVersion = trailers[TRAILER_KEYS.checkpointVersion] || null;
137
+
138
+ return {
139
+ kind: 'checkpoint',
140
+ graph,
141
+ stateHash,
142
+ frontierOid,
143
+ indexOid,
144
+ schema,
145
+ checkpointVersion,
146
+ };
147
+ }