@git-stunts/git-warp 10.7.0 → 11.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +53 -32
  2. package/SECURITY.md +64 -0
  3. package/bin/cli/commands/check.js +168 -0
  4. package/bin/cli/commands/doctor/checks.js +422 -0
  5. package/bin/cli/commands/doctor/codes.js +46 -0
  6. package/bin/cli/commands/doctor/index.js +239 -0
  7. package/bin/cli/commands/doctor/types.js +89 -0
  8. package/bin/cli/commands/history.js +73 -0
  9. package/bin/cli/commands/info.js +139 -0
  10. package/bin/cli/commands/install-hooks.js +128 -0
  11. package/bin/cli/commands/materialize.js +99 -0
  12. package/bin/cli/commands/path.js +88 -0
  13. package/bin/cli/commands/query.js +194 -0
  14. package/bin/cli/commands/registry.js +28 -0
  15. package/bin/cli/commands/seek.js +592 -0
  16. package/bin/cli/commands/trust.js +154 -0
  17. package/bin/cli/commands/verify-audit.js +113 -0
  18. package/bin/cli/commands/view.js +45 -0
  19. package/bin/cli/infrastructure.js +336 -0
  20. package/bin/cli/schemas.js +177 -0
  21. package/bin/cli/shared.js +244 -0
  22. package/bin/cli/types.js +85 -0
  23. package/bin/presenters/index.js +214 -0
  24. package/bin/presenters/json.js +66 -0
  25. package/bin/presenters/text.js +543 -0
  26. package/bin/warp-graph.js +19 -2824
  27. package/index.d.ts +32 -2
  28. package/index.js +2 -0
  29. package/package.json +9 -7
  30. package/src/domain/WarpGraph.js +106 -3252
  31. package/src/domain/errors/QueryError.js +2 -2
  32. package/src/domain/errors/TrustError.js +29 -0
  33. package/src/domain/errors/index.js +1 -0
  34. package/src/domain/services/AuditMessageCodec.js +137 -0
  35. package/src/domain/services/AuditReceiptService.js +471 -0
  36. package/src/domain/services/AuditVerifierService.js +693 -0
  37. package/src/domain/services/HttpSyncServer.js +36 -22
  38. package/src/domain/services/MessageCodecInternal.js +3 -0
  39. package/src/domain/services/MessageSchemaDetector.js +2 -2
  40. package/src/domain/services/SyncAuthService.js +69 -3
  41. package/src/domain/services/WarpMessageCodec.js +4 -1
  42. package/src/domain/trust/TrustCanonical.js +42 -0
  43. package/src/domain/trust/TrustCrypto.js +111 -0
  44. package/src/domain/trust/TrustEvaluator.js +180 -0
  45. package/src/domain/trust/TrustRecordService.js +274 -0
  46. package/src/domain/trust/TrustStateBuilder.js +209 -0
  47. package/src/domain/trust/canonical.js +68 -0
  48. package/src/domain/trust/reasonCodes.js +64 -0
  49. package/src/domain/trust/schemas.js +160 -0
  50. package/src/domain/trust/verdict.js +42 -0
  51. package/src/domain/types/git-cas.d.ts +20 -0
  52. package/src/domain/utils/RefLayout.js +59 -0
  53. package/src/domain/warp/PatchSession.js +18 -0
  54. package/src/domain/warp/Writer.js +18 -3
  55. package/src/domain/warp/_internal.js +26 -0
  56. package/src/domain/warp/_wire.js +58 -0
  57. package/src/domain/warp/_wiredMethods.d.ts +100 -0
  58. package/src/domain/warp/checkpoint.methods.js +397 -0
  59. package/src/domain/warp/fork.methods.js +323 -0
  60. package/src/domain/warp/materialize.methods.js +188 -0
  61. package/src/domain/warp/materializeAdvanced.methods.js +339 -0
  62. package/src/domain/warp/patch.methods.js +529 -0
  63. package/src/domain/warp/provenance.methods.js +284 -0
  64. package/src/domain/warp/query.methods.js +279 -0
  65. package/src/domain/warp/subscribe.methods.js +272 -0
  66. package/src/domain/warp/sync.methods.js +549 -0
  67. package/src/infrastructure/adapters/GitGraphAdapter.js +67 -1
  68. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  69. package/src/ports/CommitPort.js +10 -0
  70. package/src/ports/RefPort.js +17 -0
  71. package/src/hooks/post-merge.sh +0 -60
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Trust V1 record service.
3
+ *
4
+ * Manages the append-only chain of signed trust records stored under
5
+ * `refs/warp/<graph>/trust/records`. Each record is a Git commit
6
+ * whose message carries CBOR-encoded record data.
7
+ *
8
+ * @module domain/trust/TrustRecordService
9
+ * @see docs/specs/TRUST_V1_CRYPTO.md Section 7
10
+ */
11
+
12
+ import { buildTrustRecordRef } from '../utils/RefLayout.js';
13
+ import { TrustRecordSchema } from './schemas.js';
14
+ import { verifyRecordId } from './TrustCanonical.js';
15
+ import TrustError from '../errors/TrustError.js';
16
+
17
+ /**
18
+ * @typedef {Object} AppendOptions
19
+ * @property {boolean} [skipSignatureVerify=false] - Skip signature verification (for testing)
20
+ */
21
+
22
+ export class TrustRecordService {
23
+ /**
24
+ * @param {Object} options
25
+ * @param {*} options.persistence - GraphPersistencePort adapter
26
+ * @param {*} options.codec - CodecPort adapter (CBOR)
27
+ */
28
+ constructor({ persistence, codec }) {
29
+ this._persistence = persistence;
30
+ this._codec = codec;
31
+ }
32
+
33
+ /**
34
+ * Appends a signed trust record to the chain.
35
+ *
36
+ * Validates:
37
+ * 1. Schema conformance
38
+ * 2. RecordId integrity (content-addressed)
39
+ * 3. Signature envelope completeness (alg + sig fields present)
40
+ * 4. Prev-link consistency (must match current tip's last recordId)
41
+ *
42
+ * Note: Full cryptographic signature verification (Ed25519 verify against
43
+ * issuer public key) is NOT performed here — it requires the trust state
44
+ * to resolve the issuer's key, which is a chicken-and-egg problem for
45
+ * genesis records. Crypto verification happens during `buildState()` /
46
+ * evaluation when the full key set is available.
47
+ *
48
+ * @param {string} graphName
49
+ * @param {Record<string, *>} record - Complete signed trust record
50
+ * @param {AppendOptions} [options]
51
+ * @returns {Promise<{commitSha: string, ref: string}>}
52
+ */
53
+ async appendRecord(graphName, record, options = {}) {
54
+ // 1. Schema validation
55
+ const parsed = TrustRecordSchema.safeParse(record);
56
+ if (!parsed.success) {
57
+ throw new TrustError(
58
+ `Trust record schema validation failed: ${parsed.error.message}`,
59
+ { code: 'E_TRUST_RECORD_INVALID' },
60
+ );
61
+ }
62
+
63
+ // 2. RecordId integrity
64
+ if (!verifyRecordId(record)) {
65
+ throw new TrustError(
66
+ 'Trust record recordId does not match content',
67
+ { code: 'E_TRUST_RECORD_ID_MISMATCH' },
68
+ );
69
+ }
70
+
71
+ // 3. Signature envelope check (structural, not cryptographic)
72
+ if (!options.skipSignatureVerify) {
73
+ this._verifySignatureEnvelope(record);
74
+ }
75
+
76
+ // 4. Prev-link consistency
77
+ const ref = buildTrustRecordRef(graphName);
78
+ const { tipSha, recordId: currentTip } = await this._readTip(ref);
79
+
80
+ if (record.prev !== currentTip) {
81
+ throw new TrustError(
82
+ `Prev-link mismatch: record.prev=${record.prev}, chain tip=${currentTip}`,
83
+ { code: 'E_TRUST_PREV_MISMATCH' },
84
+ );
85
+ }
86
+
87
+ // 5. Persist as Git commit (passes tipSha to avoid re-reading ref)
88
+ const commitSha = await this._persistRecord(ref, record, tipSha);
89
+ return { commitSha, ref };
90
+ }
91
+
92
+ /**
93
+ * Reads all trust records from the chain, oldest first.
94
+ *
95
+ * @param {string} graphName
96
+ * @param {Object} [options]
97
+ * @param {string} [options.tip] - Override tip commit (for pinned reads)
98
+ * @returns {Promise<Array<Record<string, *>>>}
99
+ */
100
+ async readRecords(graphName, options = {}) {
101
+ const ref = buildTrustRecordRef(graphName);
102
+ let tip = options.tip ?? null;
103
+
104
+ if (!tip) {
105
+ try {
106
+ tip = await this._persistence.readRef(ref);
107
+ } catch {
108
+ return [];
109
+ }
110
+ if (!tip) {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ const records = [];
116
+ let current = tip;
117
+
118
+ while (current) {
119
+ const info = await this._persistence.getNodeInfo(current);
120
+ const record = this._codec.decode(
121
+ await this._persistence.readBlob(
122
+ (await this._persistence.readTreeOids(
123
+ await this._persistence.getCommitTree(current),
124
+ ))['record.cbor'],
125
+ ),
126
+ );
127
+
128
+ records.unshift(record);
129
+
130
+ if (info.parents.length === 0) {
131
+ break;
132
+ }
133
+ current = info.parents[0];
134
+ }
135
+
136
+ return records;
137
+ }
138
+
139
+ /**
140
+ * Verifies the structural integrity of a record chain.
141
+ *
142
+ * Checks:
143
+ * - Prev-links form an unbroken chain
144
+ * - No duplicate recordIds
145
+ * - Each record passes schema validation
146
+ * - First record has prev=null
147
+ *
148
+ * @param {Array<Record<string, *>>} records - Records in chain order (oldest first)
149
+ * @returns {{valid: boolean, errors: Array<{index: number, error: string}>}}
150
+ */
151
+ verifyChain(records) {
152
+ /** @type {Array<{index: number, error: string}>} */
153
+ const errors = [];
154
+ const seenIds = new Set();
155
+
156
+ for (let i = 0; i < records.length; i++) {
157
+ const record = records[i];
158
+
159
+ // Schema validation
160
+ const parsed = TrustRecordSchema.safeParse(record);
161
+ if (!parsed.success) {
162
+ errors.push({ index: i, error: `Schema: ${parsed.error.message}` });
163
+ continue;
164
+ }
165
+
166
+ // RecordId integrity
167
+ if (!verifyRecordId(record)) {
168
+ errors.push({ index: i, error: 'RecordId does not match content' });
169
+ }
170
+
171
+ // Duplicate detection
172
+ if (seenIds.has(record.recordId)) {
173
+ errors.push({ index: i, error: `Duplicate recordId: ${record.recordId}` });
174
+ }
175
+ seenIds.add(record.recordId);
176
+
177
+ // Prev-link check
178
+ if (i === 0) {
179
+ if (record.prev !== null) {
180
+ errors.push({ index: i, error: `Genesis record must have prev=null, got ${record.prev}` });
181
+ }
182
+ } else {
183
+ const expectedPrev = records[i - 1].recordId;
184
+ if (record.prev !== expectedPrev) {
185
+ errors.push({
186
+ index: i,
187
+ error: `Prev-link mismatch: expected ${expectedPrev}, got ${record.prev}`,
188
+ });
189
+ }
190
+ }
191
+ }
192
+
193
+ return { valid: errors.length === 0, errors };
194
+ }
195
+
196
+ /**
197
+ * Validates that a record's signature envelope is structurally complete.
198
+ *
199
+ * Checks for presence of `alg` and `sig` fields. Does NOT perform
200
+ * cryptographic verification — that requires the issuer's public key
201
+ * from the trust state, which is resolved during evaluation.
202
+ *
203
+ * @param {Record<string, *>} record
204
+ * @throws {TrustError} if signature envelope is missing or malformed
205
+ * @private
206
+ */
207
+ _verifySignatureEnvelope(record) {
208
+ if (!record.signature || !record.signature.sig || !record.signature.alg) {
209
+ throw new TrustError(
210
+ 'Trust record missing or malformed signature',
211
+ { code: 'E_TRUST_SIGNATURE_MISSING' },
212
+ );
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Reads the tip commit SHA and its recordId.
218
+ * @param {string} ref
219
+ * @returns {Promise<{tipSha: string|null, recordId: string|null}>}
220
+ * @private
221
+ */
222
+ async _readTip(ref) {
223
+ let tipSha;
224
+ try {
225
+ tipSha = await this._persistence.readRef(ref);
226
+ } catch {
227
+ return { tipSha: null, recordId: null };
228
+ }
229
+ if (!tipSha) {
230
+ return { tipSha: null, recordId: null };
231
+ }
232
+
233
+ const treeOid = await this._persistence.getCommitTree(tipSha);
234
+ const entries = await this._persistence.readTreeOids(treeOid);
235
+ const blobOid = entries['record.cbor'];
236
+ if (!blobOid) {
237
+ return { tipSha, recordId: null };
238
+ }
239
+
240
+ const record = this._codec.decode(await this._persistence.readBlob(blobOid));
241
+ return { tipSha, recordId: record.recordId ?? null };
242
+ }
243
+
244
+ /**
245
+ * Persists a trust record as a Git commit.
246
+ * @param {string} ref
247
+ * @param {Record<string, *>} record
248
+ * @param {string|null} parentSha - Resolved tip SHA (null for genesis)
249
+ * @returns {Promise<string>} commit SHA
250
+ * @private
251
+ */
252
+ async _persistRecord(ref, record, parentSha) {
253
+ // Encode record as CBOR blob
254
+ const encoded = this._codec.encode(record);
255
+ const blobOid = await this._persistence.writeBlob(encoded);
256
+
257
+ // Create tree with single entry
258
+ const treeOid = await this._persistence.writeTree({ 'record.cbor': blobOid });
259
+
260
+ const parents = parentSha ? [parentSha] : [];
261
+ const message = `trust: ${record.recordType} ${record.recordId.slice(0, 12)}`;
262
+
263
+ const commitSha = await this._persistence.createCommit({
264
+ tree: treeOid,
265
+ parents,
266
+ message,
267
+ });
268
+
269
+ // CAS update ref — fails atomically if a concurrent append changed the tip
270
+ await this._persistence.compareAndSwapRef(ref, commitSha, parentSha);
271
+
272
+ return commitSha;
273
+ }
274
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Trust V1 state builder.
3
+ *
4
+ * Pure function that walks an ordered sequence of trust records and
5
+ * accumulates the trust state: active/revoked keys and writer bindings.
6
+ *
7
+ * No I/O, no side effects, no infrastructure imports.
8
+ *
9
+ * @module domain/trust/TrustStateBuilder
10
+ * @see docs/specs/TRUST_V1_CRYPTO.md Section 11
11
+ */
12
+
13
+ import { TrustRecordSchema } from './schemas.js';
14
+
15
+ /**
16
+ * @typedef {Object} TrustState
17
+ * @property {Map<string, {publicKey: string, addedAt: string}>} activeKeys - keyId → key info
18
+ * @property {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
19
+ * @property {Map<string, {keyId: string, boundAt: string}>} writerBindings - "writerId\0keyId" → binding
20
+ * @property {Map<string, {keyId: string, revokedAt: string, reasonCode: string}>} revokedBindings
21
+ * @property {Array<{recordId: string, error: string}>} errors
22
+ * @property {number} recordsProcessed - Total number of records fed to the builder
23
+ */
24
+
25
+ /**
26
+ * Builds trust state from an ordered sequence of trust records.
27
+ *
28
+ * Records MUST be in chain order (oldest first). The builder enforces:
29
+ * - Monotonic revocation: once a key is revoked, it cannot be re-added
30
+ * - Binding validity: WRITER_BIND_ADD requires the referenced key to be active
31
+ * - Schema validation: each record is validated against TrustRecordSchema
32
+ *
33
+ * @param {Array<Record<string, *>>} records - Trust records in chain order
34
+ * @returns {TrustState} Frozen trust state
35
+ */
36
+ export function buildState(records) {
37
+ /** @type {Map<string, {publicKey: string, addedAt: string}>} */
38
+ const activeKeys = new Map();
39
+ /** @type {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} */
40
+ const revokedKeys = new Map();
41
+ /** @type {Map<string, {keyId: string, boundAt: string}>} */
42
+ const writerBindings = new Map();
43
+ /** @type {Map<string, {keyId: string, revokedAt: string, reasonCode: string}>} */
44
+ const revokedBindings = new Map();
45
+ /** @type {Array<{recordId: string, error: string}>} */
46
+ const errors = [];
47
+
48
+ for (const record of records) {
49
+ const parsed = TrustRecordSchema.safeParse(record);
50
+ if (!parsed.success) {
51
+ errors.push({
52
+ recordId: record.recordId ?? '(unknown)',
53
+ error: `Schema validation failed: ${parsed.error.message}`,
54
+ });
55
+ continue;
56
+ }
57
+
58
+ const rec = parsed.data;
59
+ processRecord(rec, activeKeys, revokedKeys, writerBindings, revokedBindings, errors);
60
+ }
61
+
62
+ return Object.freeze({ activeKeys, revokedKeys, writerBindings, revokedBindings, errors, recordsProcessed: records.length });
63
+ }
64
+
65
+ /**
66
+ * @param {*} rec
67
+ * @param {Map<string, {publicKey: string, addedAt: string}>} activeKeys
68
+ * @param {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
69
+ * @param {Map<string, {keyId: string, boundAt: string}>} writerBindings
70
+ * @param {Map<string, {keyId: string, revokedAt: string, reasonCode: string}>} revokedBindings
71
+ * @param {Array<{recordId: string, error: string}>} errors
72
+ */
73
+ function processRecord(rec, activeKeys, revokedKeys, writerBindings, revokedBindings, errors) {
74
+ switch (rec.recordType) {
75
+ case 'KEY_ADD':
76
+ handleKeyAdd(rec, activeKeys, revokedKeys, errors);
77
+ break;
78
+ case 'KEY_REVOKE':
79
+ handleKeyRevoke(rec, activeKeys, revokedKeys, errors);
80
+ break;
81
+ case 'WRITER_BIND_ADD':
82
+ handleBindAdd(rec, activeKeys, revokedKeys, writerBindings, errors);
83
+ break;
84
+ case 'WRITER_BIND_REVOKE':
85
+ handleBindRevoke(rec, writerBindings, revokedBindings, errors);
86
+ break;
87
+ default:
88
+ errors.push({ recordId: rec.recordId, error: `Unknown recordType: ${rec.recordType}` });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @param {*} rec
94
+ * @param {Map<string, {publicKey: string, addedAt: string}>} activeKeys
95
+ * @param {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
96
+ * @param {Array<{recordId: string, error: string}>} errors
97
+ */
98
+ function handleKeyAdd(rec, activeKeys, revokedKeys, errors) {
99
+ const { keyId, publicKey } = rec.subject;
100
+
101
+ if (revokedKeys.has(keyId)) {
102
+ errors.push({
103
+ recordId: rec.recordId,
104
+ error: `Cannot re-add revoked key: ${keyId}`,
105
+ });
106
+ return;
107
+ }
108
+
109
+ if (activeKeys.has(keyId)) {
110
+ errors.push({
111
+ recordId: rec.recordId,
112
+ error: `Duplicate KEY_ADD for already-active key: ${keyId}`,
113
+ });
114
+ return;
115
+ }
116
+
117
+ activeKeys.set(keyId, { publicKey, addedAt: rec.issuedAt });
118
+ }
119
+
120
+ /**
121
+ * @param {*} rec
122
+ * @param {Map<string, {publicKey: string, addedAt: string}>} activeKeys
123
+ * @param {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
124
+ * @param {Array<{recordId: string, error: string}>} errors
125
+ */
126
+ function handleKeyRevoke(rec, activeKeys, revokedKeys, errors) {
127
+ const { keyId, reasonCode } = rec.subject;
128
+
129
+ if (revokedKeys.has(keyId)) {
130
+ errors.push({
131
+ recordId: rec.recordId,
132
+ error: `Key already revoked: ${keyId}`,
133
+ });
134
+ return;
135
+ }
136
+
137
+ const keyInfo = activeKeys.get(keyId);
138
+ if (!keyInfo) {
139
+ errors.push({
140
+ recordId: rec.recordId,
141
+ error: `Cannot revoke unknown key: ${keyId}`,
142
+ });
143
+ return;
144
+ }
145
+
146
+ activeKeys.delete(keyId);
147
+ revokedKeys.set(keyId, {
148
+ publicKey: keyInfo.publicKey,
149
+ revokedAt: rec.issuedAt,
150
+ reasonCode,
151
+ });
152
+ }
153
+
154
+ /**
155
+ * @param {*} rec
156
+ * @param {Map<string, {publicKey: string, addedAt: string}>} activeKeys
157
+ * @param {Map<string, {publicKey: string, revokedAt: string, reasonCode: string}>} revokedKeys
158
+ * @param {Map<string, {keyId: string, boundAt: string}>} writerBindings
159
+ * @param {Array<{recordId: string, error: string}>} errors
160
+ */
161
+ function handleBindAdd(rec, activeKeys, revokedKeys, writerBindings, errors) {
162
+ const { writerId, keyId } = rec.subject;
163
+ const bindingKey = `${writerId}\0${keyId}`;
164
+
165
+ if (revokedKeys.has(keyId)) {
166
+ errors.push({
167
+ recordId: rec.recordId,
168
+ error: `Cannot bind writer to revoked key: ${keyId}`,
169
+ });
170
+ return;
171
+ }
172
+
173
+ if (!activeKeys.has(keyId)) {
174
+ errors.push({
175
+ recordId: rec.recordId,
176
+ error: `Cannot bind writer to unknown key: ${keyId}`,
177
+ });
178
+ return;
179
+ }
180
+
181
+ writerBindings.set(bindingKey, { keyId, boundAt: rec.issuedAt });
182
+ }
183
+
184
+ /**
185
+ * @param {*} rec
186
+ * @param {Map<string, {keyId: string, boundAt: string}>} writerBindings
187
+ * @param {Map<string, {keyId: string, revokedAt: string, reasonCode: string}>} revokedBindings
188
+ * @param {Array<{recordId: string, error: string}>} errors
189
+ */
190
+ function handleBindRevoke(rec, writerBindings, revokedBindings, errors) {
191
+ const { writerId, keyId, reasonCode } = rec.subject;
192
+ const bindingKey = `${writerId}\0${keyId}`;
193
+
194
+ const binding = writerBindings.get(bindingKey);
195
+ if (!binding) {
196
+ errors.push({
197
+ recordId: rec.recordId,
198
+ error: `Cannot revoke non-existent binding: writer=${writerId} key=${keyId}`,
199
+ });
200
+ return;
201
+ }
202
+
203
+ writerBindings.delete(bindingKey);
204
+ revokedBindings.set(bindingKey, {
205
+ keyId,
206
+ revokedAt: rec.issuedAt,
207
+ reasonCode,
208
+ });
209
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Trust V1 canonical serialization helpers.
3
+ *
4
+ * Domain separation constants and unsigned record extraction functions
5
+ * for recordId computation and signature payloads.
6
+ *
7
+ * @module domain/trust/canonical
8
+ * @see docs/specs/TRUST_V1_CRYPTO.md Section 6
9
+ */
10
+
11
+ import { canonicalStringify } from '../utils/canonicalStringify.js';
12
+
13
+ // ── Domain separation prefixes ──────────────────────────────────────────
14
+
15
+ /** Domain prefix for recordId computation. */
16
+ export const TRUST_RECORD_ID_DOMAIN = 'git-warp:trust-record:v1\0';
17
+
18
+ /** Domain prefix for signature payload. */
19
+ export const TRUST_SIGN_DOMAIN = 'git-warp:trust-sign:v1\0';
20
+
21
+ // ── Unsigned record helpers ─────────────────────────────────────────────
22
+
23
+ /**
24
+ * Returns the record payload used for recordId computation.
25
+ * Strips `recordId` and `signature` — these are derived, not inputs.
26
+ *
27
+ * @param {Record<string, *>} record
28
+ * @returns {Record<string, *>}
29
+ */
30
+ export function unsignedRecordForId(record) {
31
+ const out = { ...record };
32
+ delete out.recordId;
33
+ delete out.signature;
34
+ return out;
35
+ }
36
+
37
+ /**
38
+ * Returns the record payload used for signature computation.
39
+ * Strips `signature` only — `recordId` is included in signed payload.
40
+ *
41
+ * @param {Record<string, *>} record
42
+ * @returns {Record<string, *>}
43
+ */
44
+ export function unsignedRecordForSignature(record) {
45
+ const out = { ...record };
46
+ delete out.signature;
47
+ return out;
48
+ }
49
+
50
+ /**
51
+ * Computes the canonical string for recordId hashing.
52
+ *
53
+ * @param {Record<string, *>} record - Full record (recordId and signature will be stripped)
54
+ * @returns {string} Domain-separated canonical JSON string
55
+ */
56
+ export function recordIdPayload(record) {
57
+ return TRUST_RECORD_ID_DOMAIN + canonicalStringify(unsignedRecordForId(record));
58
+ }
59
+
60
+ /**
61
+ * Computes the canonical string for signature verification.
62
+ *
63
+ * @param {Record<string, *>} record - Full record (signature will be stripped)
64
+ * @returns {string} Domain-separated canonical JSON string
65
+ */
66
+ export function signaturePayload(record) {
67
+ return TRUST_SIGN_DOMAIN + canonicalStringify(unsignedRecordForSignature(record));
68
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Trust V1 reason code registry.
3
+ *
4
+ * Every trust explanation MUST include a reasonCode from this registry.
5
+ * Codes are stable — renaming or removing a code is a breaking change
6
+ * that requires a spec version bump.
7
+ *
8
+ * @module domain/trust/reasonCodes
9
+ * @see docs/specs/TRUST_V1_CRYPTO.md Section 15
10
+ */
11
+
12
+ /** @type {Readonly<Record<string, string>>} */
13
+ export const TRUST_REASON_CODES = Object.freeze({
14
+ // ── Positive ────────────────────────────────────────────────────────────
15
+ /** Writer has at least one active binding to an active key. */
16
+ WRITER_BOUND_TO_ACTIVE_KEY: 'WRITER_BOUND_TO_ACTIVE_KEY',
17
+
18
+ // ── Negative ────────────────────────────────────────────────────────────
19
+ /** Writer has no active bindings. */
20
+ WRITER_HAS_NO_ACTIVE_BINDING: 'WRITER_HAS_NO_ACTIVE_BINDING',
21
+ /** Writer's binding references a revoked key. */
22
+ WRITER_BOUND_KEY_REVOKED: 'WRITER_BOUND_KEY_REVOKED',
23
+ /** Writer's binding has been explicitly revoked. */
24
+ BINDING_REVOKED: 'BINDING_REVOKED',
25
+ /** Binding references a keyId not found in record log. */
26
+ KEY_UNKNOWN: 'KEY_UNKNOWN',
27
+
28
+ // ── System ──────────────────────────────────────────────────────────────
29
+ /** Trust record ref does not exist. */
30
+ TRUST_REF_MISSING: 'TRUST_REF_MISSING',
31
+ /** Pinned commit does not exist or is invalid. */
32
+ TRUST_PIN_INVALID: 'TRUST_PIN_INVALID',
33
+ /** Record fails schema validation. */
34
+ TRUST_RECORD_SCHEMA_INVALID: 'TRUST_RECORD_SCHEMA_INVALID',
35
+ /** Record signature verification failed. */
36
+ TRUST_SIGNATURE_INVALID: 'TRUST_SIGNATURE_INVALID',
37
+ /** Record chain linking is broken. */
38
+ TRUST_RECORD_CHAIN_INVALID: 'TRUST_RECORD_CHAIN_INVALID',
39
+ /** Policy value is unknown or unsupported. */
40
+ TRUST_POLICY_INVALID: 'TRUST_POLICY_INVALID',
41
+ });
42
+
43
+ /** @type {ReadonlySet<string>} */
44
+ export const POSITIVE_CODES = Object.freeze(new Set([
45
+ TRUST_REASON_CODES.WRITER_BOUND_TO_ACTIVE_KEY,
46
+ ]));
47
+
48
+ /** @type {ReadonlySet<string>} */
49
+ export const NEGATIVE_CODES = Object.freeze(new Set([
50
+ TRUST_REASON_CODES.WRITER_HAS_NO_ACTIVE_BINDING,
51
+ TRUST_REASON_CODES.WRITER_BOUND_KEY_REVOKED,
52
+ TRUST_REASON_CODES.BINDING_REVOKED,
53
+ TRUST_REASON_CODES.KEY_UNKNOWN,
54
+ ]));
55
+
56
+ /** @type {ReadonlySet<string>} */
57
+ export const SYSTEM_CODES = Object.freeze(new Set([
58
+ TRUST_REASON_CODES.TRUST_REF_MISSING,
59
+ TRUST_REASON_CODES.TRUST_PIN_INVALID,
60
+ TRUST_REASON_CODES.TRUST_RECORD_SCHEMA_INVALID,
61
+ TRUST_REASON_CODES.TRUST_SIGNATURE_INVALID,
62
+ TRUST_REASON_CODES.TRUST_RECORD_CHAIN_INVALID,
63
+ TRUST_REASON_CODES.TRUST_POLICY_INVALID,
64
+ ]));