@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.
- package/LICENSE +201 -0
- package/NOTICE +16 -0
- package/README.md +480 -0
- package/SECURITY.md +30 -0
- package/bin/git-warp +24 -0
- package/bin/warp-graph.js +1574 -0
- package/index.d.ts +2366 -0
- package/index.js +180 -0
- package/package.json +129 -0
- package/scripts/install-git-warp.sh +258 -0
- package/scripts/uninstall-git-warp.sh +139 -0
- package/src/domain/WarpGraph.js +3157 -0
- package/src/domain/crdt/Dot.js +160 -0
- package/src/domain/crdt/LWW.js +154 -0
- package/src/domain/crdt/ORSet.js +371 -0
- package/src/domain/crdt/VersionVector.js +222 -0
- package/src/domain/entities/GraphNode.js +60 -0
- package/src/domain/errors/EmptyMessageError.js +47 -0
- package/src/domain/errors/ForkError.js +30 -0
- package/src/domain/errors/IndexError.js +23 -0
- package/src/domain/errors/OperationAbortedError.js +22 -0
- package/src/domain/errors/QueryError.js +39 -0
- package/src/domain/errors/SchemaUnsupportedError.js +17 -0
- package/src/domain/errors/ShardCorruptionError.js +56 -0
- package/src/domain/errors/ShardLoadError.js +57 -0
- package/src/domain/errors/ShardValidationError.js +61 -0
- package/src/domain/errors/StorageError.js +57 -0
- package/src/domain/errors/SyncError.js +30 -0
- package/src/domain/errors/TraversalError.js +23 -0
- package/src/domain/errors/WarpError.js +31 -0
- package/src/domain/errors/WormholeError.js +28 -0
- package/src/domain/errors/WriterError.js +39 -0
- package/src/domain/errors/index.js +21 -0
- package/src/domain/services/AnchorMessageCodec.js +99 -0
- package/src/domain/services/BitmapIndexBuilder.js +225 -0
- package/src/domain/services/BitmapIndexReader.js +435 -0
- package/src/domain/services/BoundaryTransitionRecord.js +463 -0
- package/src/domain/services/CheckpointMessageCodec.js +147 -0
- package/src/domain/services/CheckpointSerializerV5.js +281 -0
- package/src/domain/services/CheckpointService.js +384 -0
- package/src/domain/services/CommitDagTraversalService.js +156 -0
- package/src/domain/services/DagPathFinding.js +712 -0
- package/src/domain/services/DagTopology.js +239 -0
- package/src/domain/services/DagTraversal.js +245 -0
- package/src/domain/services/Frontier.js +108 -0
- package/src/domain/services/GCMetrics.js +101 -0
- package/src/domain/services/GCPolicy.js +122 -0
- package/src/domain/services/GitLogParser.js +205 -0
- package/src/domain/services/HealthCheckService.js +246 -0
- package/src/domain/services/HookInstaller.js +326 -0
- package/src/domain/services/HttpSyncServer.js +262 -0
- package/src/domain/services/IndexRebuildService.js +426 -0
- package/src/domain/services/IndexStalenessChecker.js +103 -0
- package/src/domain/services/JoinReducer.js +582 -0
- package/src/domain/services/KeyCodec.js +113 -0
- package/src/domain/services/LegacyAnchorDetector.js +67 -0
- package/src/domain/services/LogicalTraversal.js +351 -0
- package/src/domain/services/MessageCodecInternal.js +132 -0
- package/src/domain/services/MessageSchemaDetector.js +145 -0
- package/src/domain/services/MigrationService.js +55 -0
- package/src/domain/services/ObserverView.js +265 -0
- package/src/domain/services/PatchBuilderV2.js +669 -0
- package/src/domain/services/PatchMessageCodec.js +140 -0
- package/src/domain/services/ProvenanceIndex.js +337 -0
- package/src/domain/services/ProvenancePayload.js +242 -0
- package/src/domain/services/QueryBuilder.js +835 -0
- package/src/domain/services/StateDiff.js +300 -0
- package/src/domain/services/StateSerializerV5.js +156 -0
- package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
- package/src/domain/services/SyncProtocol.js +593 -0
- package/src/domain/services/TemporalQuery.js +201 -0
- package/src/domain/services/TranslationCost.js +221 -0
- package/src/domain/services/TraversalService.js +8 -0
- package/src/domain/services/WarpMessageCodec.js +29 -0
- package/src/domain/services/WarpStateIndexBuilder.js +127 -0
- package/src/domain/services/WormholeService.js +353 -0
- package/src/domain/types/TickReceipt.js +285 -0
- package/src/domain/types/WarpTypes.js +209 -0
- package/src/domain/types/WarpTypesV2.js +200 -0
- package/src/domain/utils/CachedValue.js +140 -0
- package/src/domain/utils/EventId.js +89 -0
- package/src/domain/utils/LRUCache.js +112 -0
- package/src/domain/utils/MinHeap.js +114 -0
- package/src/domain/utils/RefLayout.js +280 -0
- package/src/domain/utils/WriterId.js +205 -0
- package/src/domain/utils/cancellation.js +33 -0
- package/src/domain/utils/canonicalStringify.js +42 -0
- package/src/domain/utils/defaultClock.js +20 -0
- package/src/domain/utils/defaultCodec.js +51 -0
- package/src/domain/utils/nullLogger.js +21 -0
- package/src/domain/utils/roaring.js +181 -0
- package/src/domain/utils/shardVersion.js +9 -0
- package/src/domain/warp/PatchSession.js +217 -0
- package/src/domain/warp/Writer.js +181 -0
- package/src/hooks/post-merge.sh +60 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
- package/src/infrastructure/adapters/ClockAdapter.js +57 -0
- package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
- package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
- package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
- package/src/infrastructure/adapters/NoOpLogger.js +62 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
- package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
- package/src/infrastructure/codecs/CborCodec.js +384 -0
- package/src/ports/BlobPort.js +30 -0
- package/src/ports/ClockPort.js +25 -0
- package/src/ports/CodecPort.js +25 -0
- package/src/ports/CommitPort.js +114 -0
- package/src/ports/ConfigPort.js +31 -0
- package/src/ports/CryptoPort.js +38 -0
- package/src/ports/GraphPersistencePort.js +57 -0
- package/src/ports/HttpServerPort.js +25 -0
- package/src/ports/IndexStoragePort.js +39 -0
- package/src/ports/LoggerPort.js +68 -0
- package/src/ports/RefPort.js +51 -0
- package/src/ports/TreePort.js +51 -0
- package/src/visualization/index.js +26 -0
- package/src/visualization/layouts/converters.js +75 -0
- package/src/visualization/layouts/elkAdapter.js +86 -0
- package/src/visualization/layouts/elkLayout.js +95 -0
- package/src/visualization/layouts/index.js +29 -0
- package/src/visualization/renderers/ascii/box.js +16 -0
- package/src/visualization/renderers/ascii/check.js +271 -0
- package/src/visualization/renderers/ascii/colors.js +13 -0
- package/src/visualization/renderers/ascii/formatters.js +73 -0
- package/src/visualization/renderers/ascii/graph.js +344 -0
- package/src/visualization/renderers/ascii/history.js +335 -0
- package/src/visualization/renderers/ascii/index.js +14 -0
- package/src/visualization/renderers/ascii/info.js +245 -0
- package/src/visualization/renderers/ascii/materialize.js +255 -0
- package/src/visualization/renderers/ascii/path.js +240 -0
- package/src/visualization/renderers/ascii/progress.js +32 -0
- package/src/visualization/renderers/ascii/symbols.js +33 -0
- package/src/visualization/renderers/ascii/table.js +19 -0
- package/src/visualization/renderers/browser/index.js +1 -0
- package/src/visualization/renderers/svg/index.js +159 -0
- package/src/visualization/utils/ansi.js +14 -0
- package/src/visualization/utils/time.js +40 -0
- package/src/visualization/utils/truncate.js +40 -0
- 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
|
+
}
|