@kashscript/hudhud 0.1.0

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 (138) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +67 -0
  3. package/dist/adapters/prisma.d.ts +170 -0
  4. package/dist/adapters/prisma.d.ts.map +1 -0
  5. package/dist/adapters/prisma.js +185 -0
  6. package/dist/adapters/prisma.js.map +1 -0
  7. package/dist/anchor/index.d.ts +55 -0
  8. package/dist/anchor/index.d.ts.map +1 -0
  9. package/dist/anchor/index.js +97 -0
  10. package/dist/anchor/index.js.map +1 -0
  11. package/dist/certificate/certificate.d.ts +115 -0
  12. package/dist/certificate/certificate.d.ts.map +1 -0
  13. package/dist/certificate/certificate.js +221 -0
  14. package/dist/certificate/certificate.js.map +1 -0
  15. package/dist/certificate/html.d.ts +10 -0
  16. package/dist/certificate/html.d.ts.map +1 -0
  17. package/dist/certificate/html.js +141 -0
  18. package/dist/certificate/html.js.map +1 -0
  19. package/dist/certificate/index.d.ts +3 -0
  20. package/dist/certificate/index.d.ts.map +1 -0
  21. package/dist/certificate/index.js +5 -0
  22. package/dist/certificate/index.js.map +1 -0
  23. package/dist/did-registry/index.d.ts +3 -0
  24. package/dist/did-registry/index.d.ts.map +1 -0
  25. package/dist/did-registry/index.js +10 -0
  26. package/dist/did-registry/index.js.map +1 -0
  27. package/dist/errors.d.ts +35 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +65 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/exception/index.d.ts +97 -0
  32. package/dist/exception/index.d.ts.map +1 -0
  33. package/dist/exception/index.js +144 -0
  34. package/dist/exception/index.js.map +1 -0
  35. package/dist/identity/index.d.ts +33 -0
  36. package/dist/identity/index.d.ts.map +1 -0
  37. package/dist/identity/index.js +36 -0
  38. package/dist/identity/index.js.map +1 -0
  39. package/dist/index.d.ts +15 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +32 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/ledger/event-schema.d.ts +97 -0
  44. package/dist/ledger/event-schema.d.ts.map +1 -0
  45. package/dist/ledger/event-schema.js +54 -0
  46. package/dist/ledger/event-schema.js.map +1 -0
  47. package/dist/ledger/index.d.ts +7 -0
  48. package/dist/ledger/index.d.ts.map +1 -0
  49. package/dist/ledger/index.js +14 -0
  50. package/dist/ledger/index.js.map +1 -0
  51. package/dist/ledger/mutex.d.ts +9 -0
  52. package/dist/ledger/mutex.d.ts.map +1 -0
  53. package/dist/ledger/mutex.js +36 -0
  54. package/dist/ledger/mutex.js.map +1 -0
  55. package/dist/ledger/store.d.ts +31 -0
  56. package/dist/ledger/store.d.ts.map +1 -0
  57. package/dist/ledger/store.js +66 -0
  58. package/dist/ledger/store.js.map +1 -0
  59. package/dist/ledger/writer.d.ts +69 -0
  60. package/dist/ledger/writer.d.ts.map +1 -0
  61. package/dist/ledger/writer.js +167 -0
  62. package/dist/ledger/writer.js.map +1 -0
  63. package/dist/mcp/allowlist.d.ts +53 -0
  64. package/dist/mcp/allowlist.d.ts.map +1 -0
  65. package/dist/mcp/allowlist.js +65 -0
  66. package/dist/mcp/allowlist.js.map +1 -0
  67. package/dist/mcp/client.d.ts +68 -0
  68. package/dist/mcp/client.d.ts.map +1 -0
  69. package/dist/mcp/client.js +198 -0
  70. package/dist/mcp/client.js.map +1 -0
  71. package/dist/mcp/index.d.ts +3 -0
  72. package/dist/mcp/index.d.ts.map +1 -0
  73. package/dist/mcp/index.js +5 -0
  74. package/dist/mcp/index.js.map +1 -0
  75. package/dist/privacy/index.d.ts +21 -0
  76. package/dist/privacy/index.d.ts.map +1 -0
  77. package/dist/privacy/index.js +96 -0
  78. package/dist/privacy/index.js.map +1 -0
  79. package/dist/runtime/dispatcher.d.ts +27 -0
  80. package/dist/runtime/dispatcher.d.ts.map +1 -0
  81. package/dist/runtime/dispatcher.js +66 -0
  82. package/dist/runtime/dispatcher.js.map +1 -0
  83. package/dist/runtime/escalation.d.ts +98 -0
  84. package/dist/runtime/escalation.d.ts.map +1 -0
  85. package/dist/runtime/escalation.js +188 -0
  86. package/dist/runtime/escalation.js.map +1 -0
  87. package/dist/runtime/index.d.ts +7 -0
  88. package/dist/runtime/index.d.ts.map +1 -0
  89. package/dist/runtime/index.js +9 -0
  90. package/dist/runtime/index.js.map +1 -0
  91. package/dist/runtime/runner.d.ts +78 -0
  92. package/dist/runtime/runner.d.ts.map +1 -0
  93. package/dist/runtime/runner.js +145 -0
  94. package/dist/runtime/runner.js.map +1 -0
  95. package/dist/scope/index.d.ts +19 -0
  96. package/dist/scope/index.d.ts.map +1 -0
  97. package/dist/scope/index.js +44 -0
  98. package/dist/scope/index.js.map +1 -0
  99. package/dist/sop/compiler.d.ts +36 -0
  100. package/dist/sop/compiler.d.ts.map +1 -0
  101. package/dist/sop/compiler.js +54 -0
  102. package/dist/sop/compiler.js.map +1 -0
  103. package/dist/sop/index.d.ts +3 -0
  104. package/dist/sop/index.d.ts.map +1 -0
  105. package/dist/sop/index.js +5 -0
  106. package/dist/sop/index.js.map +1 -0
  107. package/dist/sop/service.d.ts +40 -0
  108. package/dist/sop/service.d.ts.map +1 -0
  109. package/dist/sop/service.js +140 -0
  110. package/dist/sop/service.js.map +1 -0
  111. package/package.json +112 -0
  112. package/src/adapters/prisma.ts +310 -0
  113. package/src/anchor/index.ts +149 -0
  114. package/src/certificate/certificate.ts +340 -0
  115. package/src/certificate/html.ts +156 -0
  116. package/src/certificate/index.ts +4 -0
  117. package/src/did-registry/index.ts +11 -0
  118. package/src/errors.ts +79 -0
  119. package/src/exception/index.ts +244 -0
  120. package/src/identity/index.ts +82 -0
  121. package/src/index.ts +33 -0
  122. package/src/ledger/event-schema.ts +61 -0
  123. package/src/ledger/index.ts +22 -0
  124. package/src/ledger/mutex.ts +37 -0
  125. package/src/ledger/store.ts +86 -0
  126. package/src/ledger/writer.ts +249 -0
  127. package/src/mcp/allowlist.ts +102 -0
  128. package/src/mcp/client.ts +284 -0
  129. package/src/mcp/index.ts +4 -0
  130. package/src/privacy/index.ts +123 -0
  131. package/src/runtime/dispatcher.ts +96 -0
  132. package/src/runtime/escalation.ts +292 -0
  133. package/src/runtime/index.ts +14 -0
  134. package/src/runtime/runner.ts +227 -0
  135. package/src/scope/index.ts +59 -0
  136. package/src/sop/compiler.ts +99 -0
  137. package/src/sop/index.ts +4 -0
  138. package/src/sop/service.ts +184 -0
@@ -0,0 +1,310 @@
1
+ // ============================================================================
2
+ // @kashscript/hudhud/adapters/prisma — durable store adapters (schema-free)
3
+ // ----------------------------------------------------------------------------
4
+ // hudhud ships the STORE INTERFACES (ledger, anchor-receipt, exception, DID);
5
+ // the in-memory references live beside them. This subpath ships the DURABLE
6
+ // adapters WITHOUT a `@prisma/client` dependency: each adapter is constructed
7
+ // over a NARROW STRUCTURAL DELEGATE — the small `{ create, findFirst, findMany,
8
+ // count }` surface hudhud actually calls, typed over the row shape hudhud
9
+ // needs. A venture's generated `PrismaClient.<model>` (with matching columns +
10
+ // a `Json` payload column) satisfies the delegate, so the venture wires:
11
+ //
12
+ // new PrismaLedgerStore(prisma.ledgerBlock)
13
+ //
14
+ // The concrete Prisma SCHEMA (migrations, indexes, column types) is venture-side
15
+ // and lands at the swap. Keeping hudhud schema-free is deliberate (guard b):
16
+ // the protocol owns the CONTRACT; the venture owns the storage.
17
+ // ============================================================================
18
+
19
+ import type { LedgerBlockInput, LedgerBlockRecord, LedgerStore } from "../ledger/store";
20
+ import type {
21
+ AnchorRecord,
22
+ AnchorReceiptRecord,
23
+ AnchorReceiptStore,
24
+ AnchorWitness,
25
+ ChainTip,
26
+ } from "../anchor";
27
+ import type {
28
+ ExceptionClassificationRecord,
29
+ ExceptionClassificationStore,
30
+ FailureClass,
31
+ } from "../exception";
32
+ import type { DidPublicKey, DidRegistry } from "../did-registry";
33
+
34
+ /** A JSON scalar as Prisma surfaces it (`Json` column). */
35
+ type Json = unknown;
36
+
37
+ /** Prisma's model methods return `PrismaPromise<T>` (a `Promise<T>`); the
38
+ * narrow delegates below type only the arg subset hudhud passes + a `Promise`
39
+ * return, so a generated delegate is assignable. */
40
+
41
+ // ── Ledger ──────────────────────────────────────────────────────────────────
42
+
43
+ export interface LedgerBlockRow {
44
+ readonly sessionId: string;
45
+ readonly stepIndex: number;
46
+ readonly previousBlockHash: string;
47
+ readonly currentTransactionBlockHash: string;
48
+ readonly actorDid: string;
49
+ readonly payloadMutation: Json;
50
+ readonly signature: string;
51
+ readonly emittedAt: string;
52
+ readonly localId: number;
53
+ }
54
+
55
+ /** The subset of `PrismaClient.ledgerBlock` hudhud calls. `localId`/`emittedAt`
56
+ * are DB-assigned (autoincrement + default now()), so `create` omits them and
57
+ * the created row carries them back. */
58
+ export interface PrismaLedgerDelegate {
59
+ create(args: {
60
+ data: Omit<LedgerBlockRow, "localId" | "emittedAt"> & { emittedAt?: string };
61
+ }): Promise<LedgerBlockRow>;
62
+ findFirst(args: {
63
+ where: { sessionId: string };
64
+ orderBy: { stepIndex: "asc" | "desc" };
65
+ }): Promise<LedgerBlockRow | null>;
66
+ findMany(args: {
67
+ where: { sessionId: string };
68
+ orderBy: { stepIndex: "asc" | "desc" };
69
+ }): Promise<ReadonlyArray<LedgerBlockRow>>;
70
+ count(args: { where: { sessionId: string } }): Promise<number>;
71
+ }
72
+
73
+ function rowToLedgerRecord(row: LedgerBlockRow): LedgerBlockRecord {
74
+ return {
75
+ sessionId: row.sessionId,
76
+ stepIndex: row.stepIndex,
77
+ previousBlockHash: row.previousBlockHash,
78
+ currentTransactionBlockHash: row.currentTransactionBlockHash,
79
+ actorDid: row.actorDid,
80
+ payloadMutation: (row.payloadMutation ?? {}) as Readonly<Record<string, unknown>>,
81
+ signature: row.signature,
82
+ emittedAt: row.emittedAt,
83
+ localId: row.localId,
84
+ };
85
+ }
86
+
87
+ /** {@link LedgerStore} over a Prisma `ledgerBlock` delegate. Append-only; the
88
+ * DB's `(sessionId, stepIndex)` UNIQUE constraint is what enforces the gap-free
89
+ * chain under concurrency (the in-memory store checks it in JS). */
90
+ export class PrismaLedgerStore implements LedgerStore {
91
+ constructor(private readonly delegate: PrismaLedgerDelegate) {}
92
+
93
+ async appendBlock(input: LedgerBlockInput): Promise<LedgerBlockRecord> {
94
+ const row = await this.delegate.create({
95
+ data: {
96
+ sessionId: input.sessionId,
97
+ stepIndex: input.stepIndex,
98
+ previousBlockHash: input.previousBlockHash,
99
+ currentTransactionBlockHash: input.currentTransactionBlockHash,
100
+ actorDid: input.actorDid,
101
+ payloadMutation: input.payloadMutation as Json,
102
+ signature: input.signature,
103
+ },
104
+ });
105
+ return rowToLedgerRecord(row);
106
+ }
107
+
108
+ async latestBlockForSession(sessionId: string): Promise<LedgerBlockRecord | null> {
109
+ const row = await this.delegate.findFirst({ where: { sessionId }, orderBy: { stepIndex: "desc" } });
110
+ return row === null ? null : rowToLedgerRecord(row);
111
+ }
112
+
113
+ async countBlocksForSession(sessionId: string): Promise<number> {
114
+ return this.delegate.count({ where: { sessionId } });
115
+ }
116
+
117
+ async listBlocksForSession(sessionId: string): Promise<ReadonlyArray<LedgerBlockRecord>> {
118
+ const rows = await this.delegate.findMany({ where: { sessionId }, orderBy: { stepIndex: "asc" } });
119
+ return rows.map(rowToLedgerRecord);
120
+ }
121
+ }
122
+
123
+ // ── Anchor receipts ───────────────────────────────────────────────────────────
124
+
125
+ export interface AnchorReceiptRow {
126
+ readonly id: string;
127
+ readonly publishedAt: string;
128
+ readonly merkleRoot: string;
129
+ readonly tipCount: number;
130
+ readonly tips: Json;
131
+ readonly witness: Json;
132
+ readonly createdAt: string;
133
+ }
134
+
135
+ export interface PrismaAnchorReceiptDelegate {
136
+ create(args: { data: Omit<AnchorReceiptRow, "id" | "createdAt"> & { id?: string; createdAt?: string } }): Promise<AnchorReceiptRow>;
137
+ findFirst(args: {
138
+ where?: { merkleRoot?: string };
139
+ orderBy: { createdAt: "asc" | "desc" };
140
+ }): Promise<AnchorReceiptRow | null>;
141
+ findMany(args: { orderBy: { createdAt: "asc" | "desc" } }): Promise<ReadonlyArray<AnchorReceiptRow>>;
142
+ }
143
+
144
+ function rowToAnchorReceipt(row: AnchorReceiptRow): AnchorReceiptRecord {
145
+ return {
146
+ id: row.id,
147
+ publishedAt: row.publishedAt,
148
+ merkleRoot: row.merkleRoot,
149
+ tipCount: row.tipCount,
150
+ tips: (row.tips ?? []) as ReadonlyArray<ChainTip>,
151
+ createdAt: row.createdAt,
152
+ ...(row.witness ? { witness: row.witness as AnchorWitness } : {}),
153
+ };
154
+ }
155
+
156
+ /** {@link AnchorReceiptStore} over a Prisma `anchorReceipt` delegate. */
157
+ export class PrismaAnchorReceiptStore implements AnchorReceiptStore {
158
+ constructor(private readonly delegate: PrismaAnchorReceiptDelegate) {}
159
+
160
+ async append(record: AnchorRecord): Promise<AnchorReceiptRecord> {
161
+ const row = await this.delegate.create({
162
+ data: {
163
+ publishedAt: record.publishedAt,
164
+ merkleRoot: record.merkleRoot,
165
+ tipCount: record.tipCount,
166
+ tips: record.tips as Json,
167
+ witness: (record.witness ?? null) as Json,
168
+ },
169
+ });
170
+ return rowToAnchorReceipt(row);
171
+ }
172
+
173
+ async latest(): Promise<AnchorReceiptRecord | null> {
174
+ const row = await this.delegate.findFirst({ orderBy: { createdAt: "desc" } });
175
+ return row === null ? null : rowToAnchorReceipt(row);
176
+ }
177
+
178
+ async history(): Promise<ReadonlyArray<AnchorReceiptRecord>> {
179
+ const rows = await this.delegate.findMany({ orderBy: { createdAt: "asc" } });
180
+ return rows.map(rowToAnchorReceipt);
181
+ }
182
+
183
+ async latestForRoot(merkleRoot: string): Promise<AnchorReceiptRecord | null> {
184
+ const row = await this.delegate.findFirst({ where: { merkleRoot }, orderBy: { createdAt: "desc" } });
185
+ return row === null ? null : rowToAnchorReceipt(row);
186
+ }
187
+ }
188
+
189
+ // ── Exception classifications ─────────────────────────────────────────────────
190
+
191
+ export interface ExceptionClassificationRow {
192
+ readonly id: string;
193
+ readonly schemaVersion: string;
194
+ readonly timestampIso: string;
195
+ readonly workflowId: string;
196
+ readonly sopVersion: string;
197
+ readonly inputRef: string;
198
+ readonly contentHash: string;
199
+ readonly model: string;
200
+ readonly modelVersion: string;
201
+ readonly failureClass: string;
202
+ readonly failureTag: string;
203
+ readonly operatorDid: string;
204
+ readonly resolutionSummary: string;
205
+ readonly resolutionActionRefs: Json;
206
+ readonly escalationId: string | null;
207
+ readonly signoffLedgerStepIndex: number | null;
208
+ readonly signedEnvelope: Json;
209
+ }
210
+
211
+ export interface PrismaExceptionClassificationDelegate {
212
+ create(args: { data: ExceptionClassificationRow }): Promise<unknown>;
213
+ findMany(args: {
214
+ where?: { workflowId?: string };
215
+ orderBy: { timestampIso: "asc" | "desc" };
216
+ }): Promise<ReadonlyArray<ExceptionClassificationRow>>;
217
+ findFirst(args: { orderBy: { timestampIso: "asc" | "desc" } }): Promise<ExceptionClassificationRow | null>;
218
+ }
219
+
220
+ function rowToClassification(row: ExceptionClassificationRow): ExceptionClassificationRecord {
221
+ return {
222
+ id: row.id,
223
+ schemaVersion: row.schemaVersion,
224
+ timestampIso: row.timestampIso,
225
+ workflowId: row.workflowId,
226
+ sopVersion: row.sopVersion,
227
+ inputRef: row.inputRef,
228
+ contentHash: row.contentHash,
229
+ model: row.model,
230
+ modelVersion: row.modelVersion,
231
+ failureClass: row.failureClass as FailureClass,
232
+ failureTag: row.failureTag,
233
+ operatorDid: row.operatorDid,
234
+ resolutionSummary: row.resolutionSummary,
235
+ resolutionActionRefs: (row.resolutionActionRefs ?? []) as ReadonlyArray<string>,
236
+ escalationId: row.escalationId,
237
+ signoffLedgerStepIndex: row.signoffLedgerStepIndex,
238
+ signedEnvelope: row.signedEnvelope as ExceptionClassificationRecord["signedEnvelope"],
239
+ };
240
+ }
241
+
242
+ /** {@link ExceptionClassificationStore} over a Prisma delegate. Signatures live
243
+ * in the row (`signedEnvelope` JSON), so verification survives the round-trip. */
244
+ export class PrismaExceptionClassificationStore implements ExceptionClassificationStore {
245
+ constructor(private readonly delegate: PrismaExceptionClassificationDelegate) {}
246
+
247
+ async append(record: ExceptionClassificationRecord): Promise<void> {
248
+ await this.delegate.create({
249
+ data: {
250
+ id: record.id,
251
+ schemaVersion: record.schemaVersion,
252
+ timestampIso: record.timestampIso,
253
+ workflowId: record.workflowId,
254
+ sopVersion: record.sopVersion,
255
+ inputRef: record.inputRef,
256
+ contentHash: record.contentHash,
257
+ model: record.model,
258
+ modelVersion: record.modelVersion,
259
+ failureClass: record.failureClass,
260
+ failureTag: record.failureTag,
261
+ operatorDid: record.operatorDid,
262
+ resolutionSummary: record.resolutionSummary,
263
+ resolutionActionRefs: record.resolutionActionRefs as Json,
264
+ escalationId: record.escalationId,
265
+ signoffLedgerStepIndex: record.signoffLedgerStepIndex,
266
+ signedEnvelope: record.signedEnvelope as Json,
267
+ },
268
+ });
269
+ }
270
+
271
+ async history(): Promise<ReadonlyArray<ExceptionClassificationRecord>> {
272
+ const rows = await this.delegate.findMany({ orderBy: { timestampIso: "asc" } });
273
+ return rows.map(rowToClassification);
274
+ }
275
+
276
+ async latest(): Promise<ExceptionClassificationRecord | null> {
277
+ const row = await this.delegate.findFirst({ orderBy: { timestampIso: "desc" } });
278
+ return row === null ? null : rowToClassification(row);
279
+ }
280
+
281
+ async byWorkflow(workflowId: string): Promise<ReadonlyArray<ExceptionClassificationRecord>> {
282
+ const rows = await this.delegate.findMany({ where: { workflowId }, orderBy: { timestampIso: "asc" } });
283
+ return rows.map(rowToClassification);
284
+ }
285
+ }
286
+
287
+ // ── DID registry ──────────────────────────────────────────────────────────────
288
+
289
+ export interface DidKeyRow {
290
+ readonly did: string;
291
+ /** 32-byte Ed25519 public key. Prisma `Bytes` surfaces as a `Uint8Array`. */
292
+ readonly publicKeyBytes: Uint8Array;
293
+ readonly algorithm: string;
294
+ }
295
+
296
+ export interface PrismaDidKeyDelegate {
297
+ findUnique(args: { where: { did: string } }): Promise<DidKeyRow | null>;
298
+ }
299
+
300
+ /** {@link DidRegistry} over a Prisma `didKey` delegate — resolve-only (the ledger
301
+ * needs only the read side; registration/PDS resolution is venture-side). */
302
+ export class PrismaDidRegistry implements DidRegistry {
303
+ constructor(private readonly delegate: PrismaDidKeyDelegate) {}
304
+
305
+ async resolve(did: string): Promise<DidPublicKey | null> {
306
+ const row = await this.delegate.findUnique({ where: { did } });
307
+ if (row === null) return null;
308
+ return { did: row.did, publicKeyBytes: new Uint8Array(row.publicKeyBytes), algorithm: "ed25519" };
309
+ }
310
+ }
@@ -0,0 +1,149 @@
1
+ // ============================================================================
2
+ // @kashscript/hudhud/anchor — external chain-tip anchoring + receipt store
3
+ // ----------------------------------------------------------------------------
4
+ // Periodic out-of-band publication of session chain tips: an attacker who
5
+ // rewrites the whole ledger consistently still can't rewrite an anchor
6
+ // published OUTSIDE the DB, so the auditor cross-checks the DB chain's latest
7
+ // hash against the anchor. Ships the interface + a merkle-root helper + an
8
+ // in-memory anchor and receipt store; the public witness (Rekor/OTS) is
9
+ // attest's `witness`, and the node-fs FileChainAnchor is a deployment adapter
10
+ // (kept out to keep hudhud edge-safe). Ported from the oreoasis-core-host engine.
11
+ // ============================================================================
12
+
13
+ import { sha256Hex } from "@kashscript/attest";
14
+
15
+ export interface ChainTip {
16
+ readonly sessionId: string;
17
+ readonly latestHash: string;
18
+ readonly latestStepIndex: number;
19
+ }
20
+
21
+ export type AnchorWitnessBackend = "rekor" | "opentimestamps" | "none";
22
+
23
+ export interface AnchorWitness {
24
+ readonly backend: AnchorWitnessBackend;
25
+ readonly receipt: Readonly<Record<string, unknown>>;
26
+ readonly witnessedAt: string | null;
27
+ }
28
+
29
+ export interface AnchorRecord {
30
+ readonly publishedAt: string;
31
+ /** Merkle root over (sessionId, latestHash, stepIndex) triples, sorted by sessionId. */
32
+ readonly merkleRoot: string;
33
+ readonly tipCount: number;
34
+ readonly tips: ReadonlyArray<ChainTip>;
35
+ readonly witness?: AnchorWitness;
36
+ }
37
+
38
+ export interface ChainAnchor {
39
+ publish(tips: ReadonlyArray<ChainTip>): Promise<AnchorRecord>;
40
+ history(): Promise<ReadonlyArray<AnchorRecord>>;
41
+ latest(): Promise<AnchorRecord | null>;
42
+ }
43
+
44
+ /** Deterministic SHA-256-over-sorted-tip-triples root. Simpler than a binary
45
+ * Merkle tree — the anchor's job is just to be unrewritable; the cross-check
46
+ * against the per-session chain hash is what proves tamper. */
47
+ export async function computeAnchorMerkleRoot(tips: ReadonlyArray<ChainTip>): Promise<string> {
48
+ const sorted = [...tips].sort((a, b) => (a.sessionId < b.sessionId ? -1 : 1));
49
+ const enc = new TextEncoder();
50
+ let acc = new Uint8Array(0);
51
+ // ⚠️ audit r1 F1 (KNOWN, deferred, wire-compatible with attest's witness anchor):
52
+ // this newline join is not injective. The injective fix is a wire-format change
53
+ // that would invalidate anchors a venture has already witnessed, so it lands
54
+ // behind a coordinated anchor-format v2 at the venture swap — see
55
+ // specs/hudhud-audit-r1.md. Kept byte-identical to attest for now.
56
+ for (const t of sorted) {
57
+ const segment = enc.encode(`${t.sessionId}\n${t.latestHash}\n${t.latestStepIndex}\n`);
58
+ const next = new Uint8Array(acc.length + segment.length);
59
+ next.set(acc);
60
+ next.set(segment, acc.length);
61
+ acc = next;
62
+ }
63
+ return sha256Hex(acc);
64
+ }
65
+
66
+ function frozenRecord(tips: ReadonlyArray<ChainTip>, merkleRoot: string, publishedAt: string): AnchorRecord {
67
+ return Object.freeze({
68
+ publishedAt,
69
+ merkleRoot,
70
+ tipCount: tips.length,
71
+ tips: Object.freeze(tips.map((t) => Object.freeze({ ...t }))),
72
+ });
73
+ }
74
+
75
+ /** In-memory anchor — records publications for inspection. A legitimate
76
+ * dev/test backend (the anchor is then witnessed for the process lifetime). */
77
+ export class NullChainAnchor implements ChainAnchor {
78
+ private readonly records: AnchorRecord[] = [];
79
+
80
+ async publish(tips: ReadonlyArray<ChainTip>): Promise<AnchorRecord> {
81
+ const record = frozenRecord(tips, await computeAnchorMerkleRoot(tips), new Date().toISOString());
82
+ this.records.push(record);
83
+ return record;
84
+ }
85
+
86
+ async history(): Promise<ReadonlyArray<AnchorRecord>> {
87
+ return [...this.records];
88
+ }
89
+
90
+ async latest(): Promise<AnchorRecord | null> {
91
+ return this.records.length === 0 ? null : this.records[this.records.length - 1]!;
92
+ }
93
+ }
94
+
95
+ // ── Anchor receipt store (persistence of witnessed anchors) ────────────────
96
+
97
+ export interface AnchorReceiptRecord extends AnchorRecord {
98
+ readonly id: string;
99
+ readonly createdAt: string;
100
+ }
101
+
102
+ export interface AnchorReceiptStore {
103
+ append(record: AnchorRecord): Promise<AnchorReceiptRecord>;
104
+ latest(): Promise<AnchorReceiptRecord | null>;
105
+ history(): Promise<ReadonlyArray<AnchorReceiptRecord>>;
106
+ latestForRoot(merkleRoot: string): Promise<AnchorReceiptRecord | null>;
107
+ }
108
+
109
+ let lastIdMicros = 0;
110
+ function newReceiptId(): string {
111
+ let micros = Date.now() * 1000;
112
+ if (micros <= lastIdMicros) micros = lastIdMicros + 1;
113
+ lastIdMicros = micros;
114
+ const rand = crypto.getRandomValues(new Uint8Array(6));
115
+ let hex = "";
116
+ for (const b of rand) hex += b.toString(16).padStart(2, "0");
117
+ return `anchor_${micros.toString(36)}_${hex}`;
118
+ }
119
+
120
+ /** In-memory receipt store — the no-DB / test profile. */
121
+ export class InMemoryAnchorReceiptStore implements AnchorReceiptStore {
122
+ private readonly rows: AnchorReceiptRecord[] = [];
123
+
124
+ async append(record: AnchorRecord): Promise<AnchorReceiptRecord> {
125
+ const stored: AnchorReceiptRecord = Object.freeze({
126
+ ...record,
127
+ tips: Object.freeze(record.tips.map((t) => Object.freeze({ ...t }))),
128
+ id: newReceiptId(),
129
+ createdAt: new Date().toISOString(),
130
+ });
131
+ this.rows.push(stored);
132
+ return stored;
133
+ }
134
+
135
+ async latest(): Promise<AnchorReceiptRecord | null> {
136
+ return this.rows.length === 0 ? null : this.rows[this.rows.length - 1]!;
137
+ }
138
+
139
+ async history(): Promise<ReadonlyArray<AnchorReceiptRecord>> {
140
+ return [...this.rows];
141
+ }
142
+
143
+ async latestForRoot(merkleRoot: string): Promise<AnchorReceiptRecord | null> {
144
+ for (let i = this.rows.length - 1; i >= 0; i--) {
145
+ if (this.rows[i]!.merkleRoot === merkleRoot) return this.rows[i]!;
146
+ }
147
+ return null;
148
+ }
149
+ }