@git-stunts/git-warp 10.8.0 → 11.3.3
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 +53 -32
- package/SECURITY.md +64 -0
- package/bin/cli/commands/check.js +168 -0
- package/bin/cli/commands/doctor/checks.js +422 -0
- package/bin/cli/commands/doctor/codes.js +46 -0
- package/bin/cli/commands/doctor/index.js +239 -0
- package/bin/cli/commands/doctor/types.js +89 -0
- package/bin/cli/commands/history.js +80 -0
- package/bin/cli/commands/info.js +139 -0
- package/bin/cli/commands/install-hooks.js +128 -0
- package/bin/cli/commands/materialize.js +99 -0
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +235 -0
- package/bin/cli/commands/registry.js +32 -0
- package/bin/cli/commands/seek.js +598 -0
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +114 -0
- package/bin/cli/commands/view.js +46 -0
- package/bin/cli/infrastructure.js +350 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +96 -0
- package/bin/presenters/index.js +41 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +286 -28
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +111 -21
- package/index.js +2 -0
- package/package.json +10 -8
- package/src/domain/WarpGraph.js +109 -3252
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- 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/QueryError.js +3 -3
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +29 -0
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditMessageCodec.js +137 -0
- package/src/domain/services/AuditReceiptService.js +471 -0
- package/src/domain/services/AuditVerifierService.js +707 -0
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +2 -2
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +120 -55
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +4 -1
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +71 -4
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WarpMessageCodec.js +4 -1
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +195 -0
- package/src/domain/trust/TrustRecordService.js +281 -0
- package/src/domain/trust/TrustStateBuilder.js +222 -0
- package/src/domain/trust/canonical.js +68 -0
- package/src/domain/trust/reasonCodes.js +64 -0
- package/src/domain/trust/schemas.js +160 -0
- package/src/domain/trust/verdict.js +42 -0
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/types/git-cas.d.ts +20 -0
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +26 -17
- package/src/domain/warp/Writer.js +18 -3
- package/src/domain/warp/_internal.js +26 -0
- package/src/domain/warp/_wire.js +58 -0
- package/src/domain/warp/_wiredMethods.d.ts +254 -0
- package/src/domain/warp/checkpoint.methods.js +401 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +238 -0
- package/src/domain/warp/materializeAdvanced.methods.js +350 -0
- package/src/domain/warp/patch.methods.js +554 -0
- package/src/domain/warp/provenance.methods.js +286 -0
- package/src/domain/warp/query.methods.js +280 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +554 -0
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
- package/src/hooks/post-merge.sh +0 -60
|
@@ -42,6 +42,16 @@ import { join, cloneStateV5 } from './JoinReducer.js';
|
|
|
42
42
|
import { cloneFrontier, updateFrontier } from './Frontier.js';
|
|
43
43
|
import { vvDeserialize } from '../crdt/VersionVector.js';
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A decoded patch object after CBOR deserialization.
|
|
47
|
+
* @typedef {Object} DecodedPatch
|
|
48
|
+
* @property {Object | Map<string, number>} [context] - VersionVector (Map after normalization, plain object before)
|
|
49
|
+
* @property {import('../types/WarpTypesV2.js').OpV2[]} ops - Ordered array of operations
|
|
50
|
+
* @property {string} [writer] - Writer ID
|
|
51
|
+
* @property {number} [lamport] - Lamport timestamp
|
|
52
|
+
* @property {number} [schema] - Schema version
|
|
53
|
+
*/
|
|
54
|
+
|
|
45
55
|
// -----------------------------------------------------------------------------
|
|
46
56
|
// Patch Loading
|
|
47
57
|
// -----------------------------------------------------------------------------
|
|
@@ -56,9 +66,9 @@ import { vvDeserialize } from '../crdt/VersionVector.js';
|
|
|
56
66
|
* **Mutation**: This function mutates the input patch object for efficiency.
|
|
57
67
|
* The original object reference is returned.
|
|
58
68
|
*
|
|
59
|
-
* @param {
|
|
69
|
+
* @param {DecodedPatch} patch - The raw decoded patch from CBOR.
|
|
60
70
|
* If context is present as a plain object, it will be converted to a Map.
|
|
61
|
-
* @returns {
|
|
71
|
+
* @returns {DecodedPatch} The same patch object with context converted to Map
|
|
62
72
|
* @private
|
|
63
73
|
*/
|
|
64
74
|
function normalizePatch(patch) {
|
|
@@ -88,7 +98,7 @@ function normalizePatch(patch) {
|
|
|
88
98
|
* @param {string} sha - The 40-character commit SHA to load the patch from
|
|
89
99
|
* @param {Object} [options]
|
|
90
100
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
91
|
-
* @returns {Promise<
|
|
101
|
+
* @returns {Promise<DecodedPatch>} The decoded and normalized patch object containing:
|
|
92
102
|
* - `ops`: Array of patch operations
|
|
93
103
|
* - `context`: VersionVector (Map) of causal dependencies
|
|
94
104
|
* - `writerId`: The writer who created this patch
|
|
@@ -99,7 +109,7 @@ function normalizePatch(patch) {
|
|
|
99
109
|
* @throws {Error} If the patch blob cannot be CBOR-decoded (corrupted data)
|
|
100
110
|
* @private
|
|
101
111
|
*/
|
|
102
|
-
async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = /** @type {
|
|
112
|
+
async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = /** @type {{ codec?: import('../../ports/CodecPort.js').default }} */ ({})) {
|
|
103
113
|
const codec = codecOpt || defaultCodec;
|
|
104
114
|
// Read commit message to extract patch OID
|
|
105
115
|
const message = await persistence.showNode(sha);
|
|
@@ -107,7 +117,7 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = /** @
|
|
|
107
117
|
|
|
108
118
|
// Read and decode the patch blob
|
|
109
119
|
const patchBuffer = await persistence.readBlob(decoded.patchOid);
|
|
110
|
-
const patch = /** @type {
|
|
120
|
+
const patch = /** @type {DecodedPatch} */ (codec.decode(patchBuffer));
|
|
111
121
|
|
|
112
122
|
// Normalize the patch (convert context from object to Map)
|
|
113
123
|
return normalizePatch(patch);
|
|
@@ -134,7 +144,9 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = /** @
|
|
|
134
144
|
* @param {string|null} fromSha - Start SHA (exclusive). Pass null to load ALL patches
|
|
135
145
|
* for this writer from the beginning of their chain.
|
|
136
146
|
* @param {string} toSha - End SHA (inclusive). This is typically the writer's current tip.
|
|
137
|
-
* @
|
|
147
|
+
* @param {Object} [options]
|
|
148
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
149
|
+
* @returns {Promise<Array<{patch: DecodedPatch, sha: string}>>} Array of patch objects in
|
|
138
150
|
* chronological order (oldest first). Each entry contains:
|
|
139
151
|
* - `patch`: The decoded patch object
|
|
140
152
|
* - `sha`: The commit SHA this patch came from
|
|
@@ -152,7 +164,7 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = /** @
|
|
|
152
164
|
* // Load ALL patches for a new writer
|
|
153
165
|
* const patches = await loadPatchRange(persistence, 'events', 'new-writer', null, tipSha);
|
|
154
166
|
*/
|
|
155
|
-
export async function loadPatchRange(persistence, graphName, writerId, fromSha, toSha, { codec } = /** @type {
|
|
167
|
+
export async function loadPatchRange(persistence, graphName, writerId, fromSha, toSha, { codec } = /** @type {{ codec?: import('../../ports/CodecPort.js').default }} */ ({})) {
|
|
156
168
|
const patches = [];
|
|
157
169
|
let cur = toSha;
|
|
158
170
|
|
|
@@ -298,7 +310,7 @@ export function computeSyncDelta(localFrontier, remoteFrontier) {
|
|
|
298
310
|
* @property {'sync-response'} type - Message type discriminator for protocol parsing
|
|
299
311
|
* @property {Object.<string, string>} frontier - Responder's frontier as a plain object.
|
|
300
312
|
* Keys are writer IDs, values are SHAs.
|
|
301
|
-
* @property {Array<{writerId: string, sha: string, patch:
|
|
313
|
+
* @property {Array<{writerId: string, sha: string, patch: DecodedPatch}>} patches - Patches
|
|
302
314
|
* the requester needs, in chronological order per writer. Contains:
|
|
303
315
|
* - `writerId`: The writer who created this patch
|
|
304
316
|
* - `sha`: The commit SHA this patch came from (for frontier updates)
|
|
@@ -361,6 +373,8 @@ export function createSyncRequest(frontier) {
|
|
|
361
373
|
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} persistence - Git persistence
|
|
362
374
|
* layer for loading patches (uses CommitPort + BlobPort methods)
|
|
363
375
|
* @param {string} graphName - Graph name for error messages and logging
|
|
376
|
+
* @param {Object} [options]
|
|
377
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
364
378
|
* @returns {Promise<SyncResponse>} Response containing local frontier and patches.
|
|
365
379
|
* Patches are ordered chronologically within each writer.
|
|
366
380
|
* @throws {Error} If patch loading fails for reasons other than divergence
|
|
@@ -374,7 +388,7 @@ export function createSyncRequest(frontier) {
|
|
|
374
388
|
* res.json(response);
|
|
375
389
|
* });
|
|
376
390
|
*/
|
|
377
|
-
export async function processSyncRequest(request, localFrontier, persistence, graphName, { codec } = /** @type {
|
|
391
|
+
export async function processSyncRequest(request, localFrontier, persistence, graphName, { codec } = /** @type {{ codec?: import('../../ports/CodecPort.js').default }} */ ({})) {
|
|
378
392
|
// Convert incoming frontier from object to Map
|
|
379
393
|
const remoteFrontier = new Map(Object.entries(request.frontier));
|
|
380
394
|
|
|
@@ -401,7 +415,7 @@ export async function processSyncRequest(request, localFrontier, persistence, gr
|
|
|
401
415
|
} catch (err) {
|
|
402
416
|
// If we detect divergence, skip this writer
|
|
403
417
|
// The requester may need to handle this separately
|
|
404
|
-
if (/** @type {
|
|
418
|
+
if ((err instanceof Error && 'code' in err && /** @type {{ code: string }} */ (err).code === 'E_SYNC_DIVERGENCE') || (err instanceof Error && err.message?.includes('Divergence detected'))) {
|
|
405
419
|
continue;
|
|
406
420
|
}
|
|
407
421
|
throw err;
|
|
@@ -491,7 +505,7 @@ export function applySyncResponse(response, state, frontier) {
|
|
|
491
505
|
// will prevent silent data loss until the reader is upgraded.
|
|
492
506
|
assertOpsCompatible(normalizedPatch.ops, SCHEMA_V3);
|
|
493
507
|
// Apply patch to state
|
|
494
|
-
join(newState, /** @type {
|
|
508
|
+
join(newState, /** @type {Parameters<typeof join>[1]} */ (normalizedPatch), sha);
|
|
495
509
|
applied++;
|
|
496
510
|
}
|
|
497
511
|
|
|
@@ -36,13 +36,16 @@ import { orsetContains } from '../crdt/ORSet.js';
|
|
|
36
36
|
* InlineValue objects `{ type: 'inline', value: ... }` are unwrapped
|
|
37
37
|
* to their inner value. All other values pass through unchanged.
|
|
38
38
|
*
|
|
39
|
-
* @param {
|
|
40
|
-
* @returns {
|
|
39
|
+
* @param {unknown} value - Property value (potentially InlineValue-wrapped)
|
|
40
|
+
* @returns {unknown} The unwrapped value
|
|
41
41
|
* @private
|
|
42
42
|
*/
|
|
43
43
|
function unwrapValue(value) {
|
|
44
|
-
if (value && typeof value === 'object' &&
|
|
45
|
-
|
|
44
|
+
if (value && typeof value === 'object' && 'type' in value) {
|
|
45
|
+
const rec = /** @type {Record<string, unknown>} */ (value);
|
|
46
|
+
if (rec.type === 'inline') {
|
|
47
|
+
return rec.value;
|
|
48
|
+
}
|
|
46
49
|
}
|
|
47
50
|
return value;
|
|
48
51
|
}
|
|
@@ -60,11 +63,11 @@ function unwrapValue(value) {
|
|
|
60
63
|
*
|
|
61
64
|
* @param {import('./JoinReducer.js').WarpStateV5} state - Current state
|
|
62
65
|
* @param {string} nodeId - Node ID to extract
|
|
63
|
-
* @returns {{ id: string, exists: boolean, props: Record<string,
|
|
66
|
+
* @returns {{ id: string, exists: boolean, props: Record<string, unknown> }}
|
|
64
67
|
*/
|
|
65
68
|
function extractNodeSnapshot(state, nodeId) {
|
|
66
69
|
const exists = orsetContains(state.nodeAlive, nodeId);
|
|
67
|
-
/** @type {Record<string,
|
|
70
|
+
/** @type {Record<string, unknown>} */
|
|
68
71
|
const props = {};
|
|
69
72
|
|
|
70
73
|
if (exists) {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import { orsetElements, orsetContains } from '../crdt/ORSet.js';
|
|
17
17
|
import { decodeEdgeKey, decodePropKey, isEdgePropKey } from './KeyCodec.js';
|
|
18
18
|
|
|
19
|
+
/** @typedef {import('./JoinReducer.js').WarpStateV5} WarpStateV5 */
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
22
|
* Tests whether a string matches a glob-style pattern.
|
|
21
23
|
*
|
|
@@ -38,7 +40,7 @@ function matchGlob(pattern, str) {
|
|
|
38
40
|
/**
|
|
39
41
|
* Computes the set of property keys visible under an observer config.
|
|
40
42
|
*
|
|
41
|
-
* @param {Map<string,
|
|
43
|
+
* @param {Map<string, unknown>} allNodeProps - Map of propKey -> placeholder
|
|
42
44
|
* @param {string[]|undefined} expose - Whitelist of property keys
|
|
43
45
|
* @param {string[]|undefined} redact - Blacklist of property keys
|
|
44
46
|
* @returns {Set<string>} Visible property keys
|
|
@@ -63,7 +65,7 @@ function visiblePropKeys(allNodeProps, expose, redact) {
|
|
|
63
65
|
/**
|
|
64
66
|
* Collects node property keys from state for a given node.
|
|
65
67
|
*
|
|
66
|
-
* @param {
|
|
68
|
+
* @param {WarpStateV5} state - WarpStateV5 materialized state
|
|
67
69
|
* @param {string} nodeId - The node ID
|
|
68
70
|
* @returns {Map<string, boolean>} Map of propKey -> true
|
|
69
71
|
*/
|
|
@@ -111,7 +113,7 @@ function countMissing(source, targetSet) {
|
|
|
111
113
|
/**
|
|
112
114
|
* Computes edge loss between two observer node sets.
|
|
113
115
|
*
|
|
114
|
-
* @param {
|
|
116
|
+
* @param {WarpStateV5} state
|
|
115
117
|
* @param {Set<string>} nodesASet - Nodes visible to A
|
|
116
118
|
* @param {Set<string>} nodesBSet - Nodes visible to B
|
|
117
119
|
* @returns {number} edgeLoss fraction
|
|
@@ -156,7 +158,7 @@ function countNodePropLoss(nodeProps, { configA, configB, nodeInB }) {
|
|
|
156
158
|
/**
|
|
157
159
|
* Computes property loss across all A-visible nodes.
|
|
158
160
|
*
|
|
159
|
-
* @param {
|
|
161
|
+
* @param {WarpStateV5} state - WarpStateV5
|
|
160
162
|
* @param {{ nodesA: string[], nodesBSet: Set<string>, configA: {expose?: string[], redact?: string[]}, configB: {expose?: string[], redact?: string[]} }} opts
|
|
161
163
|
* @returns {number} propLoss fraction
|
|
162
164
|
*/
|
|
@@ -193,7 +195,7 @@ function computePropLoss(state, { nodesA, nodesBSet, configA, configB }) {
|
|
|
193
195
|
* @param {string} configB.match - Glob pattern for visible nodes
|
|
194
196
|
* @param {string[]} [configB.expose] - Property keys to include
|
|
195
197
|
* @param {string[]} [configB.redact] - Property keys to exclude
|
|
196
|
-
* @param {
|
|
198
|
+
* @param {WarpStateV5} state - WarpStateV5 materialized state
|
|
197
199
|
* @returns {{ cost: number, breakdown: { nodeLoss: number, edgeLoss: number, propLoss: number } }}
|
|
198
200
|
*/
|
|
199
201
|
export function computeTranslationCost(configA, configB, state) {
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* WARP Message Codec — facade re-exporting all message encoding, decoding,
|
|
3
3
|
* and schema utilities.
|
|
4
4
|
*
|
|
5
|
-
* This module provides backward-compatible access to the
|
|
5
|
+
* This module provides backward-compatible access to the four types of
|
|
6
6
|
* WARP (Write-Ahead Reference Protocol) commit messages:
|
|
7
7
|
* - Patch: Contains graph mutations from a single writer
|
|
8
8
|
* - Checkpoint: Contains a snapshot of materialized graph state
|
|
9
9
|
* - Anchor: Marks a merge point in the WARP DAG
|
|
10
|
+
* - Audit: Records tamper-evident audit receipts for data commits
|
|
10
11
|
*
|
|
11
12
|
* Implementation is split across focused sub-modules:
|
|
12
13
|
* - {@link module:domain/services/PatchMessageCodec}
|
|
13
14
|
* - {@link module:domain/services/CheckpointMessageCodec}
|
|
14
15
|
* - {@link module:domain/services/AnchorMessageCodec}
|
|
16
|
+
* - {@link module:domain/services/AuditMessageCodec}
|
|
15
17
|
* - {@link module:domain/services/MessageSchemaDetector}
|
|
16
18
|
*
|
|
17
19
|
* @module domain/services/WarpMessageCodec
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
export { encodePatchMessage, decodePatchMessage } from './PatchMessageCodec.js';
|
|
21
23
|
export { encodeCheckpointMessage, decodeCheckpointMessage } from './CheckpointMessageCodec.js';
|
|
22
24
|
export { encodeAnchorMessage, decodeAnchorMessage } from './AnchorMessageCodec.js';
|
|
25
|
+
export { encodeAuditMessage, decodeAuditMessage } from './AuditMessageCodec.js';
|
|
23
26
|
export {
|
|
24
27
|
detectSchemaVersion,
|
|
25
28
|
detectMessageKind,
|
|
@@ -27,7 +27,7 @@ import { detectMessageKind, decodePatchMessage } from './WarpMessageCodec.js';
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Validates that a SHA parameter is a non-empty string.
|
|
30
|
-
* @param {
|
|
30
|
+
* @param {unknown} sha - The SHA to validate
|
|
31
31
|
* @param {string} paramName - Parameter name for error messages
|
|
32
32
|
* @throws {WormholeError} If SHA is invalid
|
|
33
33
|
* @private
|
|
@@ -321,7 +321,7 @@ export function deserializeWormhole(json) {
|
|
|
321
321
|
});
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
const /** @type {Record<string,
|
|
324
|
+
const /** @type {Record<string, unknown>} */ typedJson = /** @type {Record<string, unknown>} */ (json);
|
|
325
325
|
const requiredFields = ['fromSha', 'toSha', 'writerId', 'patchCount', 'payload'];
|
|
326
326
|
for (const field of requiredFields) {
|
|
327
327
|
if (typedJson[field] === undefined) {
|
|
@@ -332,6 +332,15 @@ export function deserializeWormhole(json) {
|
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
for (const field of ['fromSha', 'toSha', 'writerId']) {
|
|
336
|
+
if (typeof typedJson[field] !== 'string') {
|
|
337
|
+
throw new WormholeError(`Invalid wormhole JSON: '${field}' must be a string`, {
|
|
338
|
+
code: 'E_INVALID_WORMHOLE_JSON',
|
|
339
|
+
context: { [field]: typedJson[field] },
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
335
344
|
if (typeof typedJson.patchCount !== 'number' || typedJson.patchCount < 0) {
|
|
336
345
|
throw new WormholeError('Invalid wormhole JSON: patchCount must be a non-negative number', {
|
|
337
346
|
code: 'E_INVALID_WORMHOLE_JSON',
|
|
@@ -340,11 +349,11 @@ export function deserializeWormhole(json) {
|
|
|
340
349
|
}
|
|
341
350
|
|
|
342
351
|
return {
|
|
343
|
-
fromSha: typedJson.fromSha,
|
|
344
|
-
toSha: typedJson.toSha,
|
|
345
|
-
writerId: typedJson.writerId,
|
|
346
|
-
patchCount: typedJson.patchCount,
|
|
347
|
-
payload: ProvenancePayload.fromJSON(typedJson.payload),
|
|
352
|
+
fromSha: /** @type {string} */ (typedJson.fromSha),
|
|
353
|
+
toSha: /** @type {string} */ (typedJson.toSha),
|
|
354
|
+
writerId: /** @type {string} */ (typedJson.writerId),
|
|
355
|
+
patchCount: /** @type {number} */ (typedJson.patchCount),
|
|
356
|
+
payload: ProvenancePayload.fromJSON(/** @type {import('./ProvenancePayload.js').PatchEntry[]} */ (typedJson.payload)),
|
|
348
357
|
};
|
|
349
358
|
}
|
|
350
359
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 hashing layer on top of canonical.js string payloads.
|
|
3
|
+
*
|
|
4
|
+
* Computes record IDs (content-addressed hex digests) and
|
|
5
|
+
* signature payloads (raw UTF-8 bytes) for trust records.
|
|
6
|
+
*
|
|
7
|
+
* @module domain/trust/TrustCanonical
|
|
8
|
+
* @see docs/specs/TRUST_V1_CRYPTO.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { recordIdPayload, signaturePayload } from './canonical.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Computes the record ID (SHA-256 hex digest) for a trust record.
|
|
16
|
+
*
|
|
17
|
+
* @param {Record<string, unknown>} record - Full trust record
|
|
18
|
+
* @returns {string} 64-character lowercase hex string
|
|
19
|
+
*/
|
|
20
|
+
export function computeRecordId(record) {
|
|
21
|
+
return createHash('sha256').update(recordIdPayload(record)).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Computes the signature payload as a Buffer (UTF-8 bytes).
|
|
26
|
+
*
|
|
27
|
+
* @param {Record<string, unknown>} record - Full trust record (signature will be stripped)
|
|
28
|
+
* @returns {Buffer} UTF-8 encoded bytes of the domain-separated canonical string
|
|
29
|
+
*/
|
|
30
|
+
export function computeSignaturePayload(record) {
|
|
31
|
+
return Buffer.from(signaturePayload(record), 'utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Verifies that a record's recordId matches its content.
|
|
36
|
+
*
|
|
37
|
+
* @param {Record<string, unknown>} record - Trust record with `recordId` field
|
|
38
|
+
* @returns {boolean} true if recordId matches computed value
|
|
39
|
+
*/
|
|
40
|
+
export function verifyRecordId(record) {
|
|
41
|
+
return record.recordId === computeRecordId(record);
|
|
42
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 cryptographic operations for trust records.
|
|
3
|
+
*
|
|
4
|
+
* Uses `node:crypto` directly — Ed25519 is trust-specific and does not
|
|
5
|
+
* belong on the general CryptoPort hash/hmac interface.
|
|
6
|
+
*
|
|
7
|
+
* @module domain/trust/TrustCrypto
|
|
8
|
+
* @see docs/specs/TRUST_V1_CRYPTO.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash, createPublicKey, verify } from 'node:crypto';
|
|
12
|
+
import TrustError from '../errors/TrustError.js';
|
|
13
|
+
|
|
14
|
+
/** Algorithms supported by this module. */
|
|
15
|
+
export const SUPPORTED_ALGORITHMS = new Set(['ed25519']);
|
|
16
|
+
|
|
17
|
+
const ED25519_PUBLIC_KEY_LENGTH = 32;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decodes a base64-encoded Ed25519 public key and validates its length.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} base64 - Base64-encoded raw public key bytes
|
|
23
|
+
* @returns {Buffer} 32-byte raw key
|
|
24
|
+
* @throws {TrustError} E_TRUST_INVALID_KEY if base64 is malformed or wrong length
|
|
25
|
+
*/
|
|
26
|
+
function decodePublicKey(base64) {
|
|
27
|
+
/** @type {Buffer} */
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = Buffer.from(base64, 'base64');
|
|
31
|
+
} catch {
|
|
32
|
+
throw new TrustError('Malformed base64 in public key', {
|
|
33
|
+
code: 'E_TRUST_INVALID_KEY',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Buffer.from with 'base64' never throws on bad input — it silently
|
|
38
|
+
// produces an empty or truncated buffer. Validate that the round-trip
|
|
39
|
+
// matches to detect garbage input.
|
|
40
|
+
if (raw.toString('base64') !== base64) {
|
|
41
|
+
throw new TrustError('Malformed base64 in public key', {
|
|
42
|
+
code: 'E_TRUST_INVALID_KEY',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (raw.length !== ED25519_PUBLIC_KEY_LENGTH) {
|
|
47
|
+
throw new TrustError(
|
|
48
|
+
`Ed25519 public key must be ${ED25519_PUBLIC_KEY_LENGTH} bytes, got ${raw.length}`,
|
|
49
|
+
{ code: 'E_TRUST_INVALID_KEY' },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Verifies an Ed25519 signature against a payload.
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} params
|
|
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
|
|
64
|
+
* @returns {boolean} true if signature is valid
|
|
65
|
+
* @throws {TrustError} E_TRUST_UNSUPPORTED_ALGORITHM for non-ed25519
|
|
66
|
+
* @throws {TrustError} E_TRUST_INVALID_KEY for malformed public key
|
|
67
|
+
*/
|
|
68
|
+
export function verifySignature({
|
|
69
|
+
algorithm,
|
|
70
|
+
publicKeyBase64,
|
|
71
|
+
signatureBase64,
|
|
72
|
+
payload,
|
|
73
|
+
}) {
|
|
74
|
+
if (!SUPPORTED_ALGORITHMS.has(algorithm)) {
|
|
75
|
+
throw new TrustError(`Unsupported algorithm: ${algorithm}`, {
|
|
76
|
+
code: 'E_TRUST_UNSUPPORTED_ALGORITHM',
|
|
77
|
+
context: { algorithm },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const raw = decodePublicKey(publicKeyBase64);
|
|
82
|
+
|
|
83
|
+
const keyObject = createPublicKey({
|
|
84
|
+
key: Buffer.concat([
|
|
85
|
+
// DER prefix for Ed25519 public key (RFC 8410)
|
|
86
|
+
Buffer.from('302a300506032b6570032100', 'hex'),
|
|
87
|
+
raw,
|
|
88
|
+
]),
|
|
89
|
+
format: 'der',
|
|
90
|
+
type: 'spki',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const sig = Buffer.from(signatureBase64, 'base64');
|
|
94
|
+
|
|
95
|
+
return verify(null, payload, keyObject, sig);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Computes the key fingerprint for an Ed25519 public key.
|
|
100
|
+
*
|
|
101
|
+
* Format: `"ed25519:" + sha256_hex(rawBytes)`
|
|
102
|
+
*
|
|
103
|
+
* @param {string} publicKeyBase64 - Base64-encoded 32-byte public key
|
|
104
|
+
* @returns {string} Fingerprint string, e.g. "ed25519:abcd1234..."
|
|
105
|
+
* @throws {TrustError} E_TRUST_INVALID_KEY for malformed key
|
|
106
|
+
*/
|
|
107
|
+
export function computeKeyFingerprint(publicKeyBase64) {
|
|
108
|
+
const raw = decodePublicKey(publicKeyBase64);
|
|
109
|
+
const hash = createHash('sha256').update(raw).digest('hex');
|
|
110
|
+
return `ed25519:${hash}`;
|
|
111
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust V1 evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Pure function that evaluates writer trust status against a built
|
|
5
|
+
* trust state and policy configuration. No I/O, no side effects.
|
|
6
|
+
*
|
|
7
|
+
* @module domain/trust/TrustEvaluator
|
|
8
|
+
* @see docs/specs/TRUST_V1_CRYPTO.md Section 12
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { TrustPolicySchema } from './schemas.js';
|
|
12
|
+
import { TRUST_REASON_CODES } from './reasonCodes.js';
|
|
13
|
+
import { deriveTrustVerdict } from './verdict.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {import('./TrustStateBuilder.js').TrustState} TrustState
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} TrustAssessment
|
|
21
|
+
* @property {number} trustSchemaVersion
|
|
22
|
+
* @property {string} mode
|
|
23
|
+
* @property {string} trustVerdict
|
|
24
|
+
* @property {Object} trust
|
|
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
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Evaluates trust status for a set of writers against the current trust state.
|
|
36
|
+
*
|
|
37
|
+
* For each writer (sorted deterministically), checks:
|
|
38
|
+
* 1. Whether any active binding exists for that writer
|
|
39
|
+
* 2. Whether the bound key is still active (not revoked)
|
|
40
|
+
*
|
|
41
|
+
* @param {string[]} writerIds - Writer IDs to evaluate
|
|
42
|
+
* @param {TrustState} trustState - Built trust state from TrustStateBuilder
|
|
43
|
+
* @param {Record<string, unknown>} policy - Trust policy configuration
|
|
44
|
+
* @returns {TrustAssessment} Frozen TrustAssessment object
|
|
45
|
+
*/
|
|
46
|
+
export function evaluateWriters(writerIds, trustState, policy) {
|
|
47
|
+
const policyResult = TrustPolicySchema.safeParse(policy);
|
|
48
|
+
if (!policyResult.success) {
|
|
49
|
+
return buildErrorAssessment(writerIds, TRUST_REASON_CODES.TRUST_POLICY_INVALID);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sortedWriters = [...writerIds].sort();
|
|
53
|
+
const explanations = sortedWriters.map((writerId) =>
|
|
54
|
+
evaluateSingleWriter(writerId, trustState),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const untrustedWriters = explanations
|
|
58
|
+
.filter((e) => !e.trusted)
|
|
59
|
+
.map((e) => e.writerId);
|
|
60
|
+
|
|
61
|
+
const trust = {
|
|
62
|
+
status: /** @type {'configured'|'pinned'|'error'|'not_configured'} */ ('configured'),
|
|
63
|
+
source: 'ref',
|
|
64
|
+
sourceDetail: null,
|
|
65
|
+
evaluatedWriters: sortedWriters,
|
|
66
|
+
untrustedWriters,
|
|
67
|
+
explanations: explanations.map((e) => Object.freeze(e)),
|
|
68
|
+
evidenceSummary: buildEvidenceSummary(trustState),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const trustVerdict = deriveTrustVerdict(trust);
|
|
72
|
+
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
trustSchemaVersion: 1,
|
|
75
|
+
mode: 'signed_evidence_v1',
|
|
76
|
+
trustVerdict,
|
|
77
|
+
trust: Object.freeze(trust),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Evaluates trust for a single writer.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} writerId
|
|
85
|
+
* @param {TrustState} trustState
|
|
86
|
+
* @returns {{writerId: string, trusted: boolean, reasonCode: string, reason: string}}
|
|
87
|
+
*/
|
|
88
|
+
function evaluateSingleWriter(writerId, trustState) {
|
|
89
|
+
// Check all bindings for this writer
|
|
90
|
+
const activeBindingKeys = [];
|
|
91
|
+
for (const [bindingKey, binding] of trustState.writerBindings) {
|
|
92
|
+
if (bindingKey.startsWith(`${writerId}\0`)) {
|
|
93
|
+
activeBindingKeys.push({ bindingKey, keyId: binding.keyId });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check revoked bindings too (for reason code accuracy)
|
|
98
|
+
let hasRevokedBinding = false;
|
|
99
|
+
for (const bindingKey of trustState.revokedBindings.keys()) {
|
|
100
|
+
if (bindingKey.startsWith(`${writerId}\0`)) {
|
|
101
|
+
hasRevokedBinding = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (activeBindingKeys.length === 0) {
|
|
106
|
+
if (hasRevokedBinding) {
|
|
107
|
+
return {
|
|
108
|
+
writerId,
|
|
109
|
+
trusted: false,
|
|
110
|
+
reasonCode: TRUST_REASON_CODES.BINDING_REVOKED,
|
|
111
|
+
reason: `Writer '${writerId}' has no active bindings (all revoked)`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
writerId,
|
|
116
|
+
trusted: false,
|
|
117
|
+
reasonCode: TRUST_REASON_CODES.WRITER_HAS_NO_ACTIVE_BINDING,
|
|
118
|
+
reason: `Writer '${writerId}' has no active bindings`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if any active binding points to an active key
|
|
123
|
+
for (const { keyId } of activeBindingKeys) {
|
|
124
|
+
if (trustState.activeKeys.has(keyId)) {
|
|
125
|
+
return {
|
|
126
|
+
writerId,
|
|
127
|
+
trusted: true,
|
|
128
|
+
reasonCode: TRUST_REASON_CODES.WRITER_BOUND_TO_ACTIVE_KEY,
|
|
129
|
+
reason: `Writer '${writerId}' is bound to active key ${keyId}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// All bindings point to revoked keys
|
|
135
|
+
return {
|
|
136
|
+
writerId,
|
|
137
|
+
trusted: false,
|
|
138
|
+
reasonCode: TRUST_REASON_CODES.WRITER_BOUND_KEY_REVOKED,
|
|
139
|
+
reason: `Writer '${writerId}' is bound only to revoked keys`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Builds an error assessment when policy validation fails.
|
|
145
|
+
*
|
|
146
|
+
* @param {string[]} writerIds
|
|
147
|
+
* @param {string} reasonCode
|
|
148
|
+
* @returns {TrustAssessment}
|
|
149
|
+
*/
|
|
150
|
+
function buildErrorAssessment(writerIds, reasonCode) {
|
|
151
|
+
const sortedWriters = [...writerIds].sort();
|
|
152
|
+
const trust = {
|
|
153
|
+
status: /** @type {'configured'|'pinned'|'error'|'not_configured'} */ ('error'),
|
|
154
|
+
source: 'none',
|
|
155
|
+
sourceDetail: null,
|
|
156
|
+
evaluatedWriters: sortedWriters,
|
|
157
|
+
untrustedWriters: sortedWriters,
|
|
158
|
+
explanations: sortedWriters.map((writerId) => Object.freeze({
|
|
159
|
+
writerId,
|
|
160
|
+
trusted: false,
|
|
161
|
+
reasonCode,
|
|
162
|
+
reason: `Policy validation failed: ${reasonCode}`,
|
|
163
|
+
})),
|
|
164
|
+
evidenceSummary: {
|
|
165
|
+
recordsScanned: 0,
|
|
166
|
+
activeKeys: 0,
|
|
167
|
+
revokedKeys: 0,
|
|
168
|
+
activeBindings: 0,
|
|
169
|
+
revokedBindings: 0,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
trustSchemaVersion: 1,
|
|
175
|
+
mode: 'signed_evidence_v1',
|
|
176
|
+
trustVerdict: deriveTrustVerdict(trust),
|
|
177
|
+
trust: Object.freeze(trust),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Builds the evidence summary from trust state.
|
|
183
|
+
*
|
|
184
|
+
* @param {TrustState} trustState
|
|
185
|
+
* @returns {{recordsScanned: number, activeKeys: number, revokedKeys: number, activeBindings: number, revokedBindings: number}}
|
|
186
|
+
*/
|
|
187
|
+
function buildEvidenceSummary(trustState) {
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
recordsScanned: trustState.recordsProcessed,
|
|
190
|
+
activeKeys: trustState.activeKeys.size,
|
|
191
|
+
revokedKeys: trustState.revokedKeys.size,
|
|
192
|
+
activeBindings: trustState.writerBindings.size,
|
|
193
|
+
revokedBindings: trustState.revokedBindings.size,
|
|
194
|
+
});
|
|
195
|
+
}
|