@git-stunts/git-warp 12.2.1 → 12.4.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/README.md +5 -5
- package/bin/cli/commands/info.js +1 -5
- package/bin/cli/infrastructure.js +6 -9
- package/bin/cli/shared.js +8 -0
- package/bin/presenters/text.js +10 -3
- package/bin/warp-graph.js +6 -6
- package/package.json +1 -1
- package/src/domain/WarpGraph.js +5 -35
- package/src/domain/crdt/ORSet.js +3 -0
- package/src/domain/crdt/VersionVector.js +1 -1
- package/src/domain/entities/GraphNode.js +1 -6
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/PatchError.js +1 -1
- package/src/domain/errors/PersistenceError.js +45 -0
- package/src/domain/errors/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +1 -1
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AdjacencyNeighborProvider.js +1 -4
- package/src/domain/services/AnchorMessageCodec.js +1 -3
- package/src/domain/services/AuditMessageCodec.js +1 -5
- package/src/domain/services/AuditReceiptService.js +4 -18
- package/src/domain/services/AuditVerifierService.js +3 -7
- package/src/domain/services/BitmapIndexBuilder.js +6 -12
- package/src/domain/services/BitmapIndexReader.js +7 -20
- package/src/domain/services/BitmapNeighborProvider.js +1 -3
- package/src/domain/services/BoundaryTransitionRecord.js +7 -23
- package/src/domain/services/CheckpointMessageCodec.js +6 -6
- package/src/domain/services/CheckpointSerializerV5.js +8 -12
- package/src/domain/services/CheckpointService.js +28 -40
- package/src/domain/services/CommitDagTraversalService.js +1 -3
- package/src/domain/services/DagPathFinding.js +9 -59
- package/src/domain/services/DagTopology.js +4 -16
- package/src/domain/services/DagTraversal.js +7 -31
- package/src/domain/services/Frontier.js +4 -6
- package/src/domain/services/GitLogParser.js +1 -2
- package/src/domain/services/GraphTraversal.js +14 -114
- package/src/domain/services/HealthCheckService.js +3 -9
- package/src/domain/services/HookInstaller.js +2 -8
- package/src/domain/services/HttpSyncServer.js +24 -25
- package/src/domain/services/IncrementalIndexUpdater.js +4 -6
- package/src/domain/services/IndexRebuildService.js +6 -52
- package/src/domain/services/IndexStalenessChecker.js +2 -3
- package/src/domain/services/JoinReducer.js +200 -100
- package/src/domain/services/KeyCodec.js +48 -0
- package/src/domain/services/LogicalBitmapIndexBuilder.js +1 -2
- package/src/domain/services/LogicalIndexBuildService.js +2 -6
- package/src/domain/services/LogicalIndexReader.js +1 -2
- package/src/domain/services/LogicalTraversal.js +13 -64
- package/src/domain/services/MaterializedViewService.js +5 -19
- package/src/domain/services/MessageSchemaDetector.js +35 -5
- package/src/domain/services/MigrationService.js +1 -4
- package/src/domain/services/ObserverView.js +1 -7
- package/src/domain/services/OpNormalizer.js +79 -0
- package/src/domain/services/PatchBuilderV2.js +67 -38
- package/src/domain/services/PatchMessageCodec.js +1 -6
- package/src/domain/services/PropertyIndexBuilder.js +1 -2
- package/src/domain/services/PropertyIndexReader.js +1 -4
- package/src/domain/services/ProvenanceIndex.js +5 -7
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -16
- package/src/domain/services/StateDiff.js +3 -9
- package/src/domain/services/StateSerializerV5.js +10 -10
- package/src/domain/services/StreamingBitmapIndexBuilder.js +13 -41
- package/src/domain/services/SyncAuthService.js +8 -32
- package/src/domain/services/SyncController.js +5 -25
- package/src/domain/services/SyncProtocol.js +10 -13
- package/src/domain/services/SyncTrustGate.js +4 -9
- package/src/domain/services/TemporalQuery.js +9 -27
- package/src/domain/services/TranslationCost.js +2 -8
- package/src/domain/services/WarpMessageCodec.js +2 -0
- package/src/domain/services/WarpStateIndexBuilder.js +2 -4
- package/src/domain/services/WormholeService.js +9 -25
- package/src/domain/trust/TrustCrypto.js +9 -10
- package/src/domain/trust/TrustEvaluator.js +1 -8
- package/src/domain/trust/TrustRecordService.js +5 -10
- package/src/domain/types/TickReceipt.js +9 -11
- package/src/domain/types/WarpTypes.js +1 -5
- package/src/domain/types/WarpTypesV2.js +78 -13
- package/src/domain/utils/CachedValue.js +1 -4
- package/src/domain/utils/MinHeap.js +3 -3
- package/src/domain/utils/RefLayout.js +26 -0
- package/src/domain/utils/WriterId.js +2 -7
- package/src/domain/utils/canonicalCbor.js +1 -1
- package/src/domain/utils/defaultClock.js +1 -0
- package/src/domain/utils/defaultCodec.js +1 -1
- package/src/domain/utils/parseCursorBlob.js +4 -4
- package/src/domain/warp/PatchSession.js +3 -8
- package/src/domain/warp/Writer.js +9 -12
- package/src/domain/warp/_wire.js +2 -2
- package/src/domain/warp/_wiredMethods.d.ts +5 -7
- package/src/domain/warp/checkpoint.methods.js +1 -1
- package/src/domain/warp/fork.methods.js +2 -6
- package/src/domain/warp/materializeAdvanced.methods.js +3 -3
- package/src/domain/warp/patch.methods.js +8 -8
- package/src/domain/warp/provenance.methods.js +5 -5
- package/src/domain/warp/query.methods.js +9 -18
- package/src/domain/warp/subscribe.methods.js +2 -8
- package/src/globals.d.ts +7 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -18
- package/src/infrastructure/adapters/ConsoleLogger.js +2 -9
- package/src/infrastructure/adapters/DenoHttpAdapter.js +15 -15
- package/src/infrastructure/adapters/GitGraphAdapter.js +234 -58
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +9 -2
- package/src/infrastructure/adapters/NodeHttpAdapter.js +14 -14
- package/src/infrastructure/adapters/WebCryptoAdapter.js +1 -2
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/HttpServerPort.js +24 -2
- package/src/ports/RefPort.js +2 -1
- package/src/visualization/renderers/ascii/box.js +1 -1
- package/src/visualization/renderers/ascii/check.js +1 -5
- package/src/visualization/renderers/ascii/history.js +1 -6
- package/src/visualization/renderers/ascii/path.js +4 -22
- package/src/visualization/renderers/ascii/progress.js +1 -4
- package/src/visualization/renderers/ascii/seek.js +1 -5
- package/src/visualization/renderers/ascii/table.js +1 -3
|
@@ -86,12 +86,7 @@ function extractNodeSnapshot(state, nodeId) {
|
|
|
86
86
|
/**
|
|
87
87
|
* Evaluates checkpoint boundary semantics for `always()`.
|
|
88
88
|
*
|
|
89
|
-
* @param {
|
|
90
|
-
* @param {import('./JoinReducer.js').WarpStateV5} params.state
|
|
91
|
-
* @param {string} params.nodeId
|
|
92
|
-
* @param {Function} params.predicate
|
|
93
|
-
* @param {number|null} params.checkpointMaxLamport
|
|
94
|
-
* @param {number} params.since
|
|
89
|
+
* @param {{ state: import('./JoinReducer.js').WarpStateV5, nodeId: string, predicate: (snapshot: {id: string, exists: boolean, props: Record<string, unknown>}) => boolean, checkpointMaxLamport: number|null, since: number }} params
|
|
95
90
|
* @returns {{ nodeEverExisted: boolean, shouldReturn: boolean, returnValue: boolean }}
|
|
96
91
|
* @private
|
|
97
92
|
*/
|
|
@@ -118,12 +113,7 @@ function evaluateAlwaysCheckpointBoundary({
|
|
|
118
113
|
/**
|
|
119
114
|
* Evaluates checkpoint boundary semantics for `eventually()`.
|
|
120
115
|
*
|
|
121
|
-
* @param {
|
|
122
|
-
* @param {import('./JoinReducer.js').WarpStateV5} params.state
|
|
123
|
-
* @param {string} params.nodeId
|
|
124
|
-
* @param {Function} params.predicate
|
|
125
|
-
* @param {number|null} params.checkpointMaxLamport
|
|
126
|
-
* @param {number} params.since
|
|
116
|
+
* @param {{ state: import('./JoinReducer.js').WarpStateV5, nodeId: string, predicate: (snapshot: {id: string, exists: boolean, props: Record<string, unknown>}) => boolean, checkpointMaxLamport: number|null, since: number }} params
|
|
127
117
|
* @returns {boolean}
|
|
128
118
|
* @private
|
|
129
119
|
*/
|
|
@@ -149,16 +139,12 @@ function evaluateEventuallyCheckpointBoundary({
|
|
|
149
139
|
*/
|
|
150
140
|
export class TemporalQuery {
|
|
151
141
|
/**
|
|
152
|
-
* @param {
|
|
153
|
-
* @param {Function} options.loadAllPatches - Async function that returns
|
|
154
|
-
* all patches as Array<{ patch, sha }> in causal order.
|
|
155
|
-
* @param {Function} [options.loadCheckpoint] - Async function returning
|
|
156
|
-
* { state: WarpStateV5, maxLamport: number } or null.
|
|
142
|
+
* @param {{ loadAllPatches: () => Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string}>>, loadCheckpoint?: () => Promise<{state: import('./JoinReducer.js').WarpStateV5, maxLamport: number}|null> }} options
|
|
157
143
|
*/
|
|
158
144
|
constructor({ loadAllPatches, loadCheckpoint }) {
|
|
159
|
-
/** @type {
|
|
145
|
+
/** @type {() => Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string}>>} */
|
|
160
146
|
this._loadAllPatches = loadAllPatches;
|
|
161
|
-
/** @type {
|
|
147
|
+
/** @type {(() => Promise<{state: import('./JoinReducer.js').WarpStateV5, maxLamport: number}|null>)|null} */
|
|
162
148
|
this._loadCheckpoint = loadCheckpoint || null;
|
|
163
149
|
}
|
|
164
150
|
|
|
@@ -172,11 +158,9 @@ export class TemporalQuery {
|
|
|
172
158
|
* Returns false if the node never existed in the range.
|
|
173
159
|
*
|
|
174
160
|
* @param {string} nodeId - The node ID to evaluate
|
|
175
|
-
* @param {
|
|
161
|
+
* @param {(snapshot: {id: string, exists: boolean, props: Record<string, unknown>}) => boolean} predicate - Predicate receiving node snapshot
|
|
176
162
|
* `{ id, exists, props }`. Should return boolean.
|
|
177
|
-
* @param {
|
|
178
|
-
* @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
|
|
179
|
-
* Only patches with lamport >= since are considered.
|
|
163
|
+
* @param {{ since?: number }} [options={}] - Options
|
|
180
164
|
* @returns {Promise<boolean>} True if predicate held at every tick
|
|
181
165
|
*
|
|
182
166
|
* @example
|
|
@@ -232,11 +216,9 @@ export class TemporalQuery {
|
|
|
232
216
|
* soon as the predicate returns true at any tick.
|
|
233
217
|
*
|
|
234
218
|
* @param {string} nodeId - The node ID to evaluate
|
|
235
|
-
* @param {
|
|
219
|
+
* @param {(snapshot: {id: string, exists: boolean, props: Record<string, unknown>}) => boolean} predicate - Predicate receiving node snapshot
|
|
236
220
|
* `{ id, exists, props }`. Should return boolean.
|
|
237
|
-
* @param {
|
|
238
|
-
* @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
|
|
239
|
-
* Only patches with lamport >= since are considered.
|
|
221
|
+
* @param {{ since?: number }} [options={}] - Options
|
|
240
222
|
* @returns {Promise<boolean>} True if predicate held at any tick
|
|
241
223
|
*
|
|
242
224
|
* @example
|
|
@@ -169,14 +169,8 @@ function computePropLoss(state, { nodesA, nodesBSet, configA, configB }) {
|
|
|
169
169
|
* The cost measures how much information is lost when translating from
|
|
170
170
|
* A's view to B's view. It is asymmetric: cost(A->B) != cost(B->A) in general.
|
|
171
171
|
*
|
|
172
|
-
* @param {
|
|
173
|
-
* @param {string|string[]}
|
|
174
|
-
* @param {string[]} [configA.expose] - Property keys to include
|
|
175
|
-
* @param {string[]} [configA.redact] - Property keys to exclude
|
|
176
|
-
* @param {Object} configB - Observer configuration for B
|
|
177
|
-
* @param {string|string[]} configB.match - Glob pattern(s) for visible nodes
|
|
178
|
-
* @param {string[]} [configB.expose] - Property keys to include
|
|
179
|
-
* @param {string[]} [configB.redact] - Property keys to exclude
|
|
172
|
+
* @param {{ match: string|string[], expose?: string[], redact?: string[] }} configA - Observer configuration for A
|
|
173
|
+
* @param {{ match: string|string[], expose?: string[], redact?: string[] }} configB - Observer configuration for B
|
|
180
174
|
* @param {WarpStateV5} state - WarpStateV5 materialized state
|
|
181
175
|
* @returns {{ cost: number, breakdown: { nodeLoss: number, edgeLoss: number, propLoss: number } }}
|
|
182
176
|
*/
|
|
@@ -30,8 +30,7 @@ import { decodeEdgeKey } from './KeyCodec.js';
|
|
|
30
30
|
export default class WarpStateIndexBuilder {
|
|
31
31
|
/**
|
|
32
32
|
* Creates a new WarpStateIndexBuilder.
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort for shard checksums
|
|
33
|
+
* @param {{ crypto?: import('../../ports/CryptoPort.js').default }} [options] - Configuration
|
|
35
34
|
*/
|
|
36
35
|
constructor({ crypto } = {}) {
|
|
37
36
|
/** @type {BitmapIndexBuilder} */
|
|
@@ -109,8 +108,7 @@ export default class WarpStateIndexBuilder {
|
|
|
109
108
|
* Convenience function to build and serialize a WARP state index.
|
|
110
109
|
*
|
|
111
110
|
* @param {import('./JoinReducer.js').WarpStateV5} state - The materialized state
|
|
112
|
-
* @param {
|
|
113
|
-
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort for shard checksums
|
|
111
|
+
* @param {{ crypto?: import('../../ports/CryptoPort.js').default }} [options] - Configuration
|
|
114
112
|
* @returns {Promise<{tree: Record<string, Buffer>, stats: {nodes: number, edges: number}}>} Serialized index and stats
|
|
115
113
|
*
|
|
116
114
|
* @example
|
|
@@ -61,13 +61,8 @@ async function verifyShaExists(persistence, sha, paramName) {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Processes a single commit in the wormhole chain.
|
|
64
|
-
* @param {
|
|
65
|
-
* @
|
|
66
|
-
* @param {string} opts.sha - The commit SHA
|
|
67
|
-
* @param {string} opts.graphName - Expected graph name
|
|
68
|
-
* @param {string|null} opts.expectedWriter - Expected writer ID (null for first commit)
|
|
69
|
-
* @param {import('../../ports/CodecPort.js').default} [opts.codec] - Codec for deserialization
|
|
70
|
-
* @returns {Promise<{patch: Object, sha: string, writerId: string, parentSha: string|null}>}
|
|
64
|
+
* @param {{ persistence: import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default, sha: string, graphName: string, expectedWriter: string|null, codec?: import('../../ports/CodecPort.js').default }} opts - Options
|
|
65
|
+
* @returns {Promise<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string, writerId: string, parentSha: string|null}>}
|
|
71
66
|
* @throws {WormholeError} On validation errors
|
|
72
67
|
* @private
|
|
73
68
|
*/
|
|
@@ -101,7 +96,7 @@ async function processCommit({ persistence, sha, graphName, expectedWriter, code
|
|
|
101
96
|
}
|
|
102
97
|
|
|
103
98
|
const patchBuffer = await persistence.readBlob(patchMeta.patchOid);
|
|
104
|
-
const patch = /** @type {
|
|
99
|
+
const patch = /** @type {import('../types/WarpTypesV2.js').PatchV2} */ (codec.decode(patchBuffer));
|
|
105
100
|
|
|
106
101
|
return {
|
|
107
102
|
patch,
|
|
@@ -135,12 +130,7 @@ async function processCommit({ persistence, sha, graphName, expectedWriter, code
|
|
|
135
130
|
* must be an ancestor of `toSha` in the writer's patch chain. Both endpoints
|
|
136
131
|
* are inclusive in the wormhole.
|
|
137
132
|
*
|
|
138
|
-
* @param {
|
|
139
|
-
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} options.persistence - Git persistence adapter
|
|
140
|
-
* @param {string} options.graphName - Name of the graph
|
|
141
|
-
* @param {string} options.fromSha - SHA of the first (oldest) patch commit
|
|
142
|
-
* @param {string} options.toSha - SHA of the last (newest) patch commit
|
|
143
|
-
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
133
|
+
* @param {{ persistence: import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default, graphName: string, fromSha: string, toSha: string, codec?: import('../../ports/CodecPort.js').default }} options - Wormhole creation options
|
|
144
134
|
* @returns {Promise<WormholeEdge>} The created wormhole
|
|
145
135
|
* @throws {WormholeError} If fromSha or toSha doesn't exist (E_WORMHOLE_SHA_NOT_FOUND)
|
|
146
136
|
* @throws {WormholeError} If fromSha is not an ancestor of toSha (E_WORMHOLE_INVALID_RANGE)
|
|
@@ -171,13 +161,8 @@ export async function createWormhole({ persistence, graphName, fromSha, toSha, c
|
|
|
171
161
|
* Walks the parent chain from toSha towards fromSha, collecting and
|
|
172
162
|
* validating each commit along the way.
|
|
173
163
|
*
|
|
174
|
-
* @param {
|
|
175
|
-
* @
|
|
176
|
-
* @param {string} options.graphName - Expected graph name
|
|
177
|
-
* @param {string} options.fromSha - SHA of the first (oldest) patch commit
|
|
178
|
-
* @param {string} options.toSha - SHA of the last (newest) patch commit
|
|
179
|
-
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
180
|
-
* @returns {Promise<Array<{patch: Object, sha: string, writerId: string}>>} Patches in newest-first order
|
|
164
|
+
* @param {{ persistence: import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default, graphName: string, fromSha: string, toSha: string, codec?: import('../../ports/CodecPort.js').default }} options
|
|
165
|
+
* @returns {Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string, writerId: string}>>} Patches in newest-first order
|
|
181
166
|
* @throws {WormholeError} If fromSha is not an ancestor of toSha or range is empty
|
|
182
167
|
* @private
|
|
183
168
|
*/
|
|
@@ -232,8 +217,7 @@ async function collectPatchRange({ persistence, graphName, fromSha, toSha, codec
|
|
|
232
217
|
*
|
|
233
218
|
* @param {WormholeEdge} first - The earlier (older) wormhole
|
|
234
219
|
* @param {WormholeEdge} second - The later (newer) wormhole
|
|
235
|
-
* @param {
|
|
236
|
-
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default} [options.persistence] - Git persistence adapter (for validation)
|
|
220
|
+
* @param {{ persistence?: import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default }} [options] - Composition options
|
|
237
221
|
* @returns {Promise<WormholeEdge>} The composed wormhole
|
|
238
222
|
* @throws {WormholeError} If wormholes are from different writers (E_WORMHOLE_MULTI_WRITER)
|
|
239
223
|
* @throws {WormholeError} If wormholes are not consecutive (E_WORMHOLE_INVALID_RANGE)
|
|
@@ -294,7 +278,7 @@ export function replayWormhole(wormhole, initialState) {
|
|
|
294
278
|
* Serializes a wormhole to a JSON-serializable object.
|
|
295
279
|
*
|
|
296
280
|
* @param {WormholeEdge} wormhole - The wormhole to serialize
|
|
297
|
-
* @returns {
|
|
281
|
+
* @returns {Record<string, unknown>} JSON-serializable representation
|
|
298
282
|
*/
|
|
299
283
|
export function serializeWormhole(wormhole) {
|
|
300
284
|
return {
|
|
@@ -309,7 +293,7 @@ export function serializeWormhole(wormhole) {
|
|
|
309
293
|
/**
|
|
310
294
|
* Deserializes a wormhole from a JSON object.
|
|
311
295
|
*
|
|
312
|
-
* @param {
|
|
296
|
+
* @param {Record<string, unknown>} json - The JSON object to deserialize
|
|
313
297
|
* @returns {WormholeEdge} The deserialized wormhole
|
|
314
298
|
* @throws {WormholeError} If the JSON structure is invalid
|
|
315
299
|
*/
|
|
@@ -16,6 +16,13 @@ export const SUPPORTED_ALGORITHMS = new Set(['ed25519']);
|
|
|
16
16
|
|
|
17
17
|
const ED25519_PUBLIC_KEY_LENGTH = 32;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* DER-encoded SPKI prefix for Ed25519 public keys (RFC 8410, Section 4).
|
|
21
|
+
* Prepend to a 32-byte raw key to form a valid SPKI structure for `createPublicKey()`.
|
|
22
|
+
* @see https://www.rfc-editor.org/rfc/rfc8410#section-4
|
|
23
|
+
*/
|
|
24
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
25
|
+
|
|
19
26
|
/**
|
|
20
27
|
* Decodes a base64-encoded Ed25519 public key and validates its length.
|
|
21
28
|
*
|
|
@@ -56,11 +63,7 @@ function decodePublicKey(base64) {
|
|
|
56
63
|
/**
|
|
57
64
|
* Verifies an Ed25519 signature against a payload.
|
|
58
65
|
*
|
|
59
|
-
* @param {
|
|
60
|
-
* @param {string} params.algorithm - Must be 'ed25519'
|
|
61
|
-
* @param {string} params.publicKeyBase64 - Base64-encoded 32-byte public key
|
|
62
|
-
* @param {string} params.signatureBase64 - Base64-encoded signature
|
|
63
|
-
* @param {Buffer} params.payload - Bytes to verify
|
|
66
|
+
* @param {{ algorithm: string, publicKeyBase64: string, signatureBase64: string, payload: Buffer }} params
|
|
64
67
|
* @returns {boolean} true if signature is valid
|
|
65
68
|
* @throws {TrustError} E_TRUST_UNSUPPORTED_ALGORITHM for non-ed25519
|
|
66
69
|
* @throws {TrustError} E_TRUST_INVALID_KEY for malformed public key
|
|
@@ -81,11 +84,7 @@ export function verifySignature({
|
|
|
81
84
|
const raw = decodePublicKey(publicKeyBase64);
|
|
82
85
|
|
|
83
86
|
const keyObject = createPublicKey({
|
|
84
|
-
key: Buffer.concat([
|
|
85
|
-
// DER prefix for Ed25519 public key (RFC 8410)
|
|
86
|
-
Buffer.from('302a300506032b6570032100', 'hex'),
|
|
87
|
-
raw,
|
|
88
|
-
]),
|
|
87
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
|
|
89
88
|
format: 'der',
|
|
90
89
|
type: 'spki',
|
|
91
90
|
});
|
|
@@ -21,14 +21,7 @@ import { deriveTrustVerdict } from './verdict.js';
|
|
|
21
21
|
* @property {number} trustSchemaVersion
|
|
22
22
|
* @property {string} mode
|
|
23
23
|
* @property {string} trustVerdict
|
|
24
|
-
* @property {
|
|
25
|
-
* @property {'configured'|'pinned'|'error'|'not_configured'} trust.status
|
|
26
|
-
* @property {string} trust.source
|
|
27
|
-
* @property {string|null} trust.sourceDetail
|
|
28
|
-
* @property {string[]} trust.evaluatedWriters
|
|
29
|
-
* @property {string[]} trust.untrustedWriters
|
|
30
|
-
* @property {ReadonlyArray<{writerId: string, trusted: boolean, reasonCode: string, reason: string}>} trust.explanations
|
|
31
|
-
* @property {Record<string, number> & {recordsScanned: number, activeKeys: number, revokedKeys: number, activeBindings: number, revokedBindings: number}} trust.evidenceSummary
|
|
24
|
+
* @property {{ status: 'configured'|'pinned'|'error'|'not_configured', source: string, sourceDetail: string|null, evaluatedWriters: string[], untrustedWriters: string[], explanations: ReadonlyArray<{writerId: string, trusted: boolean, reasonCode: string, reason: string}>, evidenceSummary: Record<string, number> & {recordsScanned: number, activeKeys: number, revokedKeys: number, activeBindings: number, revokedBindings: number} }} trust
|
|
32
25
|
*/
|
|
33
26
|
|
|
34
27
|
/**
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { buildTrustRecordRef } from '../utils/RefLayout.js';
|
|
13
13
|
import { TrustRecordSchema } from './schemas.js';
|
|
14
14
|
import { verifyRecordId } from './TrustCanonical.js';
|
|
15
|
+
import PersistenceError from '../errors/PersistenceError.js';
|
|
15
16
|
import TrustError from '../errors/TrustError.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -32,9 +33,7 @@ const MAX_CAS_ATTEMPTS = 3;
|
|
|
32
33
|
|
|
33
34
|
export class TrustRecordService {
|
|
34
35
|
/**
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default & import('../../ports/RefPort.js').default} options.persistence - GraphPersistencePort adapter
|
|
37
|
-
* @param {import('../../ports/CodecPort.js').default} options.codec - CodecPort adapter (CBOR)
|
|
36
|
+
* @param {{ persistence: import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default & import('../../ports/RefPort.js').default, codec: import('../../ports/CodecPort.js').default }} options
|
|
38
37
|
*/
|
|
39
38
|
constructor({ persistence, codec }) {
|
|
40
39
|
this._persistence = persistence;
|
|
@@ -104,8 +103,7 @@ export class TrustRecordService {
|
|
|
104
103
|
* Reads all trust records from the chain, oldest first.
|
|
105
104
|
*
|
|
106
105
|
* @param {string} graphName
|
|
107
|
-
* @param {
|
|
108
|
-
* @param {string} [options.tip] - Override tip commit (for pinned reads)
|
|
106
|
+
* @param {{ tip?: string }} [options]
|
|
109
107
|
* @returns {Promise<ReadRecordsResult>}
|
|
110
108
|
*/
|
|
111
109
|
async readRecords(graphName, options = {}) {
|
|
@@ -118,7 +116,7 @@ export class TrustRecordService {
|
|
|
118
116
|
tip = await this._persistence.readRef(ref);
|
|
119
117
|
} catch (err) {
|
|
120
118
|
// Distinguish "ref not found" from operational error (J15)
|
|
121
|
-
if (err instanceof
|
|
119
|
+
if (err instanceof PersistenceError && err.code === PersistenceError.E_REF_NOT_FOUND) {
|
|
122
120
|
return { ok: true, records: [] };
|
|
123
121
|
}
|
|
124
122
|
return {
|
|
@@ -234,10 +232,7 @@ export class TrustRecordService {
|
|
|
234
232
|
*
|
|
235
233
|
* @param {string} graphName
|
|
236
234
|
* @param {Record<string, unknown>} record - Complete signed trust record
|
|
237
|
-
* @param {
|
|
238
|
-
* @param {number} [options.maxRetries=3] - Maximum rebuild-and-retry attempts
|
|
239
|
-
* @param {((record: Record<string, unknown>) => Promise<Record<string, unknown>>)|null} [options.resign] - Function to re-sign a rebuilt record (null for unsigned)
|
|
240
|
-
* @param {boolean} [options.skipSignatureVerify=false] - Skip signature verification
|
|
235
|
+
* @param {{ maxRetries?: number, resign?: ((record: Record<string, unknown>) => Promise<Record<string, unknown>>)|null, skipSignatureVerify?: boolean }} [options]
|
|
241
236
|
* @returns {Promise<{commitSha: string, ref: string, attempts: number}>}
|
|
242
237
|
* @throws {TrustError} E_TRUST_CAS_EXHAUSTED if all retries fail
|
|
243
238
|
*/
|
|
@@ -25,6 +25,8 @@ export const OP_TYPES = Object.freeze([
|
|
|
25
25
|
'EdgeAdd',
|
|
26
26
|
'EdgeTombstone',
|
|
27
27
|
'PropSet',
|
|
28
|
+
'NodePropSet',
|
|
29
|
+
'EdgePropSet',
|
|
28
30
|
'BlobValue',
|
|
29
31
|
]);
|
|
30
32
|
|
|
@@ -80,9 +82,9 @@ function validateOp(op, index) {
|
|
|
80
82
|
/**
|
|
81
83
|
* Validates that an operation type is one of the allowed OP_TYPES.
|
|
82
84
|
*
|
|
83
|
-
* Valid operation types correspond to the
|
|
84
|
-
*
|
|
85
|
-
*
|
|
85
|
+
* Valid operation types correspond to the eight receipt operation types:
|
|
86
|
+
* NodeAdd, NodeTombstone, EdgeAdd, EdgeTombstone, PropSet, NodePropSet,
|
|
87
|
+
* EdgePropSet, and BlobValue.
|
|
86
88
|
*
|
|
87
89
|
* @param {unknown} value - The operation type to validate
|
|
88
90
|
* @param {number} i - Index of the operation in the ops array (for error messages)
|
|
@@ -156,7 +158,7 @@ function validateOpResult(value, i) {
|
|
|
156
158
|
|
|
157
159
|
/**
|
|
158
160
|
* @typedef {Object} OpOutcome
|
|
159
|
-
* @property {string} op - Operation type ('NodeAdd' | 'NodeTombstone' | 'EdgeAdd' | 'EdgeTombstone' | 'PropSet' | 'BlobValue')
|
|
161
|
+
* @property {string} op - Operation type ('NodeAdd' | 'NodeTombstone' | 'EdgeAdd' | 'EdgeTombstone' | 'PropSet' | 'NodePropSet' | 'EdgePropSet' | 'BlobValue')
|
|
160
162
|
* @property {string} target - Node ID or edge key
|
|
161
163
|
* @property {'applied' | 'superseded' | 'redundant'} result - Outcome of the operation
|
|
162
164
|
* @property {string} [reason] - Human-readable explanation (e.g., "LWW: writer bob at lamport 43 wins")
|
|
@@ -173,11 +175,7 @@ function validateOpResult(value, i) {
|
|
|
173
175
|
/**
|
|
174
176
|
* Creates an immutable TickReceipt.
|
|
175
177
|
*
|
|
176
|
-
* @param {
|
|
177
|
-
* @param {string} params.patchSha - SHA of the patch commit
|
|
178
|
-
* @param {string} params.writer - Writer ID
|
|
179
|
-
* @param {number} params.lamport - Lamport timestamp (non-negative integer)
|
|
180
|
-
* @param {OpOutcome[]} params.ops - Per-operation outcome records
|
|
178
|
+
* @param {{ patchSha: string, writer: string, lamport: number, ops: OpOutcome[] }} params
|
|
181
179
|
* @returns {Readonly<TickReceipt>} Frozen tick receipt
|
|
182
180
|
* @throws {Error} If any parameter is invalid
|
|
183
181
|
*/
|
|
@@ -277,9 +275,9 @@ export function canonicalJson(receipt) {
|
|
|
277
275
|
*/
|
|
278
276
|
function sortedReplacer(_key, value) {
|
|
279
277
|
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
280
|
-
/** @type {{ [x: string]:
|
|
278
|
+
/** @type {{ [x: string]: unknown }} */
|
|
281
279
|
const sorted = {};
|
|
282
|
-
const obj = /** @type {{ [x: string]:
|
|
280
|
+
const obj = /** @type {{ [x: string]: unknown }} */ (value);
|
|
283
281
|
for (const k of Object.keys(obj).sort()) {
|
|
284
282
|
sorted[k] = obj[k];
|
|
285
283
|
}
|
|
@@ -197,11 +197,7 @@ export function createPropSet(node, key, value) {
|
|
|
197
197
|
|
|
198
198
|
/**
|
|
199
199
|
* Creates an EventId
|
|
200
|
-
* @param {
|
|
201
|
-
* @param {number} options.lamport - Lamport timestamp
|
|
202
|
-
* @param {string} options.writerId - Writer ID
|
|
203
|
-
* @param {string} options.patchSha - Patch SHA
|
|
204
|
-
* @param {number} options.opIndex - Operation index within patch
|
|
200
|
+
* @param {{ lamport: number, writerId: string, patchSha: string, opIndex: number }} options - EventId options
|
|
205
201
|
* @returns {EventId} EventId object
|
|
206
202
|
*/
|
|
207
203
|
export function createEventId({ lamport, writerId, patchSha, opIndex }) {
|
|
@@ -74,18 +74,64 @@
|
|
|
74
74
|
*/
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
* Property set operation - sets a property value on a node
|
|
78
|
-
* Uses EventId for identification (derived from patch context)
|
|
77
|
+
* Property set operation - sets a property value on a node (raw/persisted form).
|
|
78
|
+
* Uses EventId for identification (derived from patch context).
|
|
79
|
+
*
|
|
80
|
+
* In raw patches, edge properties are also encoded as PropSet with the node
|
|
81
|
+
* field carrying a \x01-prefixed edge identity. See {@link OpV2NodePropSet}
|
|
82
|
+
* and {@link OpV2EdgePropSet} for the canonical (internal) representations.
|
|
83
|
+
*
|
|
79
84
|
* @typedef {Object} OpV2PropSet
|
|
80
85
|
* @property {'PropSet'} type - Operation type discriminator
|
|
86
|
+
* @property {NodeId} node - Node ID to set property on (may contain \x01 prefix for edge props)
|
|
87
|
+
* @property {string} key - Property key
|
|
88
|
+
* @property {unknown} value - Property value (any JSON-serializable type)
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Canonical node property set operation (internal only — never persisted).
|
|
93
|
+
* @typedef {Object} OpV2NodePropSet
|
|
94
|
+
* @property {'NodePropSet'} type - Operation type discriminator
|
|
81
95
|
* @property {NodeId} node - Node ID to set property on
|
|
82
96
|
* @property {string} key - Property key
|
|
83
97
|
* @property {unknown} value - Property value (any JSON-serializable type)
|
|
84
98
|
*/
|
|
85
99
|
|
|
86
100
|
/**
|
|
87
|
-
*
|
|
88
|
-
* @typedef {
|
|
101
|
+
* Canonical edge property set operation (internal only — never persisted).
|
|
102
|
+
* @typedef {Object} OpV2EdgePropSet
|
|
103
|
+
* @property {'EdgePropSet'} type - Operation type discriminator
|
|
104
|
+
* @property {NodeId} from - Source node ID
|
|
105
|
+
* @property {NodeId} to - Target node ID
|
|
106
|
+
* @property {string} label - Edge label
|
|
107
|
+
* @property {string} key - Property key
|
|
108
|
+
* @property {unknown} value - Property value (any JSON-serializable type)
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Blob value reference operation.
|
|
113
|
+
* @typedef {Object} OpV2BlobValue
|
|
114
|
+
* @property {'BlobValue'} type - Operation type discriminator
|
|
115
|
+
* @property {string} node - Node ID the blob is attached to
|
|
116
|
+
* @property {string} oid - Blob object ID in the Git object store
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Union of all raw (persisted) v2 operation types.
|
|
121
|
+
* @typedef {OpV2NodeAdd | OpV2NodeRemove | OpV2EdgeAdd | OpV2EdgeRemove | OpV2PropSet | OpV2BlobValue} RawOpV2
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Union of all canonical (internal) v2 operation types.
|
|
126
|
+
* Reducers, provenance, receipts, and queries operate on canonical ops only.
|
|
127
|
+
* @typedef {OpV2NodeAdd | OpV2NodeRemove | OpV2EdgeAdd | OpV2EdgeRemove | OpV2NodePropSet | OpV2EdgePropSet | OpV2BlobValue} CanonicalOpV2
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Union of all v2 operation types (raw + canonical).
|
|
132
|
+
* Used in patch containers that may hold either raw ops (from disk)
|
|
133
|
+
* or canonical ops (after normalization).
|
|
134
|
+
* @typedef {RawOpV2 | CanonicalOpV2} OpV2
|
|
89
135
|
*/
|
|
90
136
|
|
|
91
137
|
// ============================================================================
|
|
@@ -153,7 +199,9 @@ export function createEdgeRemoveV2(from, to, label, observedDots) {
|
|
|
153
199
|
}
|
|
154
200
|
|
|
155
201
|
/**
|
|
156
|
-
* Creates a PropSet operation (no dot - uses EventId)
|
|
202
|
+
* Creates a raw PropSet operation (no dot - uses EventId).
|
|
203
|
+
* This is the persisted form. For internal use, prefer
|
|
204
|
+
* {@link createNodePropSetV2} or {@link createEdgePropSetV2}.
|
|
157
205
|
* @param {NodeId} node - Node ID to set property on
|
|
158
206
|
* @param {string} key - Property key
|
|
159
207
|
* @param {unknown} value - Property value (any JSON-serializable type)
|
|
@@ -163,20 +211,37 @@ export function createPropSetV2(node, key, value) {
|
|
|
163
211
|
return { type: 'PropSet', node, key, value };
|
|
164
212
|
}
|
|
165
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Creates a canonical NodePropSet operation (internal only).
|
|
216
|
+
* @param {NodeId} node - Node ID to set property on
|
|
217
|
+
* @param {string} key - Property key
|
|
218
|
+
* @param {unknown} value - Property value (any JSON-serializable type)
|
|
219
|
+
* @returns {OpV2NodePropSet} NodePropSet operation
|
|
220
|
+
*/
|
|
221
|
+
export function createNodePropSetV2(node, key, value) {
|
|
222
|
+
return { type: 'NodePropSet', node, key, value };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Creates a canonical EdgePropSet operation (internal only).
|
|
227
|
+
* @param {NodeId} from - Source node ID
|
|
228
|
+
* @param {NodeId} to - Target node ID
|
|
229
|
+
* @param {string} label - Edge label
|
|
230
|
+
* @param {string} key - Property key
|
|
231
|
+
* @param {unknown} value - Property value (any JSON-serializable type)
|
|
232
|
+
* @returns {OpV2EdgePropSet} EdgePropSet operation
|
|
233
|
+
*/
|
|
234
|
+
export function createEdgePropSetV2(from, to, label, key, value) {
|
|
235
|
+
return { type: 'EdgePropSet', from, to, label, key, value };
|
|
236
|
+
}
|
|
237
|
+
|
|
166
238
|
// ============================================================================
|
|
167
239
|
// Factory Functions - Patch
|
|
168
240
|
// ============================================================================
|
|
169
241
|
|
|
170
242
|
/**
|
|
171
243
|
* Creates a PatchV2
|
|
172
|
-
* @param {
|
|
173
|
-
* @param {2|3} [options.schema=2] - Schema version (2 for node-only, 3 for edge properties)
|
|
174
|
-
* @param {string} options.writer - Writer ID
|
|
175
|
-
* @param {number} options.lamport - Lamport timestamp
|
|
176
|
-
* @param {VersionVector} options.context - Writer's observed frontier
|
|
177
|
-
* @param {OpV2[]} options.ops - Array of operations
|
|
178
|
-
* @param {string[]} [options.reads] - Node/edge IDs read by this patch (for provenance tracking)
|
|
179
|
-
* @param {string[]} [options.writes] - Node/edge IDs written by this patch (for provenance tracking)
|
|
244
|
+
* @param {{ schema?: 2|3, writer: string, lamport: number, context: VersionVector, ops: OpV2[], reads?: string[], writes?: string[] }} options - Patch options
|
|
180
245
|
* @returns {PatchV2} PatchV2 object
|
|
181
246
|
*/
|
|
182
247
|
export function createPatchV2({ schema = 2, writer, lamport, context, ops, reads, writes }) {
|
|
@@ -28,10 +28,7 @@ class CachedValue {
|
|
|
28
28
|
/**
|
|
29
29
|
* Creates a CachedValue instance.
|
|
30
30
|
*
|
|
31
|
-
* @param {
|
|
32
|
-
* @param {import('../../ports/ClockPort.js').default} options.clock - Clock port for timing
|
|
33
|
-
* @param {number} options.ttlMs - Time-to-live in milliseconds
|
|
34
|
-
* @param {() => T | Promise<T>} options.compute - Function to compute the value when cache is stale
|
|
31
|
+
* @param {{ clock: import('../../ports/ClockPort.js').default, ttlMs: number, compute: () => T | Promise<T> }} options
|
|
35
32
|
* @throws {Error} If ttlMs is not a positive number
|
|
36
33
|
*/
|
|
37
34
|
constructor({ clock, ttlMs, compute }) {
|
|
@@ -9,9 +9,9 @@ class MinHeap {
|
|
|
9
9
|
/**
|
|
10
10
|
* Creates an empty MinHeap.
|
|
11
11
|
*
|
|
12
|
-
* @param {
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* @param {{ tieBreaker?: (a: T, b: T) => number }} [options] - Configuration options.
|
|
13
|
+
* `tieBreaker`: comparator invoked when two entries have equal priority.
|
|
14
|
+
* Negative return = a wins (comes out first).
|
|
15
15
|
* When omitted, equal-priority extraction order is unspecified (heap-natural).
|
|
16
16
|
*/
|
|
17
17
|
constructor(options) {
|
|
@@ -45,6 +45,23 @@ const WRITER_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
|
45
45
|
*/
|
|
46
46
|
const PATH_TRAVERSAL_PATTERN = /\.\./;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Ref-layout keywords that must not appear as any `/`-delimited segment
|
|
50
|
+
* of a graph name. Using one of these would create an ambiguous ref path
|
|
51
|
+
* (e.g. `refs/warp/writers/writers/alice`).
|
|
52
|
+
*
|
|
53
|
+
* @type {Set<string>}
|
|
54
|
+
*/
|
|
55
|
+
export const RESERVED_GRAPH_NAME_SEGMENTS = new Set([
|
|
56
|
+
'writers',
|
|
57
|
+
'checkpoints',
|
|
58
|
+
'coverage',
|
|
59
|
+
'cursor',
|
|
60
|
+
'audit',
|
|
61
|
+
'trust',
|
|
62
|
+
'seek-cache',
|
|
63
|
+
]);
|
|
64
|
+
|
|
48
65
|
// -----------------------------------------------------------------------------
|
|
49
66
|
// Validators
|
|
50
67
|
// -----------------------------------------------------------------------------
|
|
@@ -94,6 +111,15 @@ export function validateGraphName(name) {
|
|
|
94
111
|
if (name.includes('\0')) {
|
|
95
112
|
throw new Error(`Invalid graph name: contains null byte: ${name}`);
|
|
96
113
|
}
|
|
114
|
+
|
|
115
|
+
const segments = name.split('/');
|
|
116
|
+
for (const seg of segments) {
|
|
117
|
+
if (RESERVED_GRAPH_NAME_SEGMENTS.has(seg)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Invalid graph name: segment '${seg}' is a reserved ref-layout keyword: ${name}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
97
123
|
}
|
|
98
124
|
|
|
99
125
|
/**
|
|
@@ -116,8 +116,7 @@ function crockfordBase32(bytes) {
|
|
|
116
116
|
* Uses 128 bits of entropy (16 bytes) encoded as Crockford Base32.
|
|
117
117
|
* The result is prefixed with `w_` for a total length of 28 characters.
|
|
118
118
|
*
|
|
119
|
-
* @param {
|
|
120
|
-
* @param {(n: number) => Uint8Array} [options.randomBytes] - Custom RNG for testing
|
|
119
|
+
* @param {{ randomBytes?: (n: number) => Uint8Array }} [options] - Options with optional custom RNG for testing
|
|
121
120
|
* @returns {string} A canonical writer ID (e.g., 'w_0123456789abcdefghjkmnpqrs')
|
|
122
121
|
* @throws {WriterIdError} If RNG is unavailable or returns wrong shape
|
|
123
122
|
*
|
|
@@ -148,11 +147,7 @@ export function generateWriterId({ randomBytes } = {}) {
|
|
|
148
147
|
* 2. Load from git config key `warp.writerId.<graphName>`
|
|
149
148
|
* 3. If missing or invalid, generate new canonical ID, persist, and return
|
|
150
149
|
*
|
|
151
|
-
* @param {
|
|
152
|
-
* @param {string} args.graphName - The graph name
|
|
153
|
-
* @param {string|undefined} args.explicitWriterId - Optional explicit writer ID
|
|
154
|
-
* @param {(key: string) => Promise<string|null>} args.configGet - Function to read git config
|
|
155
|
-
* @param {(key: string, value: string) => Promise<void>} args.configSet - Function to write git config
|
|
150
|
+
* @param {{ graphName: string, explicitWriterId: string|null|undefined, configGet: (key: string) => Promise<string|null>, configSet: (key: string, value: string) => Promise<void> }} args
|
|
156
151
|
* @returns {Promise<string>} The resolved writer ID
|
|
157
152
|
* @throws {WriterIdError} If config operations fail
|
|
158
153
|
*
|
|
@@ -28,7 +28,7 @@ export function encodeCanonicalCbor(value) {
|
|
|
28
28
|
/**
|
|
29
29
|
* Decodes CBOR bytes to a value.
|
|
30
30
|
*
|
|
31
|
-
* @param {
|
|
31
|
+
* @param {Uint8Array} buffer - CBOR bytes
|
|
32
32
|
* @returns {unknown} Decoded value
|
|
33
33
|
*/
|
|
34
34
|
export function decodeCanonicalCbor(buffer) {
|
|
@@ -34,7 +34,7 @@ function sortKeys(value) {
|
|
|
34
34
|
}
|
|
35
35
|
return sorted;
|
|
36
36
|
}
|
|
37
|
-
if (typeof value === 'object' && (/** @type {
|
|
37
|
+
if (typeof value === 'object' && (/** @type {Record<string, unknown>} */ (value).constructor === Object || /** @type {Record<string, unknown>} */ (value).constructor === undefined)) {
|
|
38
38
|
/** @type {Record<string, unknown>} */
|
|
39
39
|
const sorted = {};
|
|
40
40
|
for (const key of Object.keys(value).sort()) {
|