@peac/capture-core 0.10.9 → 0.10.10

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/dist/index.mjs ADDED
@@ -0,0 +1,423 @@
1
+ import { canonicalize } from '@peac/crypto';
2
+
3
+ // src/types.ts
4
+ var GENESIS_DIGEST = "0".repeat(64);
5
+ var SIZE_CONSTANTS = {
6
+ K: 1024,
7
+ // 1 KB
8
+ M: 1024 * 1024,
9
+ // 1 MB
10
+ TRUNC_64K: 64 * 1024,
11
+ // 64 KB
12
+ TRUNC_1M: 1024 * 1024
13
+ // 1 MB
14
+ };
15
+ var VALID_TRUNCATE_THRESHOLDS = [SIZE_CONSTANTS.TRUNC_64K, SIZE_CONSTANTS.TRUNC_1M];
16
+ function getSubtle() {
17
+ const subtle = globalThis.crypto?.subtle;
18
+ if (!subtle) {
19
+ throw new Error(
20
+ "WebCrypto (crypto.subtle) is required but not available. Ensure you are running in Node.js 18+, Deno, Bun, or a modern browser."
21
+ );
22
+ }
23
+ return subtle;
24
+ }
25
+ async function sha256Hex(data) {
26
+ const subtle = getSubtle();
27
+ const hashBuffer = await subtle.digest("SHA-256", data);
28
+ const hashArray = new Uint8Array(hashBuffer);
29
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
30
+ }
31
+ var ActionHasher = class {
32
+ truncateThreshold;
33
+ constructor(config = {}) {
34
+ const threshold = config.truncateThreshold ?? SIZE_CONSTANTS.TRUNC_1M;
35
+ if (!VALID_TRUNCATE_THRESHOLDS.includes(threshold)) {
36
+ throw new RangeError(
37
+ `truncateThreshold must be 64k (${SIZE_CONSTANTS.TRUNC_64K}) or 1m (${SIZE_CONSTANTS.TRUNC_1M}), got ${threshold}`
38
+ );
39
+ }
40
+ this.truncateThreshold = threshold;
41
+ getSubtle();
42
+ }
43
+ /**
44
+ * Compute digest for payload bytes.
45
+ * Automatically truncates if payload exceeds threshold.
46
+ */
47
+ async digest(payload) {
48
+ const bytes = payload.length;
49
+ if (bytes <= this.truncateThreshold) {
50
+ return {
51
+ alg: "sha-256",
52
+ value: await sha256Hex(payload),
53
+ bytes
54
+ };
55
+ }
56
+ const truncated = payload.slice(0, this.truncateThreshold);
57
+ let alg;
58
+ if (this.truncateThreshold === SIZE_CONSTANTS.TRUNC_64K) {
59
+ alg = "sha-256:trunc-64k";
60
+ } else {
61
+ alg = "sha-256:trunc-1m";
62
+ }
63
+ return {
64
+ alg,
65
+ value: await sha256Hex(truncated),
66
+ bytes
67
+ // Original size for audit
68
+ };
69
+ }
70
+ /**
71
+ * Compute digest for a spool entry (for chaining).
72
+ * Uses JCS (RFC 8785) for deterministic serialization.
73
+ */
74
+ async digestEntry(entry) {
75
+ const canonical = canonicalize(entry);
76
+ const bytes = new TextEncoder().encode(canonical);
77
+ return sha256Hex(bytes);
78
+ }
79
+ };
80
+ function createHasher(config) {
81
+ return new ActionHasher(config);
82
+ }
83
+
84
+ // src/mapper.ts
85
+ function buildExecutor(action) {
86
+ const executor = {
87
+ platform: action.platform
88
+ };
89
+ if (action.platform_version) {
90
+ executor.version = action.platform_version;
91
+ }
92
+ if (action.plugin_id) {
93
+ executor.plugin_id = action.plugin_id;
94
+ }
95
+ return executor;
96
+ }
97
+ function buildToolTarget(action) {
98
+ if (!action.tool_name) {
99
+ return void 0;
100
+ }
101
+ const tool = {
102
+ name: action.tool_name
103
+ };
104
+ if (action.tool_provider) {
105
+ tool.provider = action.tool_provider;
106
+ }
107
+ return tool;
108
+ }
109
+ function buildResourceTarget(action) {
110
+ if (!action.resource_uri && !action.resource_method) {
111
+ return void 0;
112
+ }
113
+ const resource = {};
114
+ if (action.resource_uri) {
115
+ resource.uri = action.resource_uri;
116
+ }
117
+ if (action.resource_method) {
118
+ resource.method = action.resource_method;
119
+ }
120
+ return resource;
121
+ }
122
+ function buildPayloadRef(digest, redaction) {
123
+ if (!digest) {
124
+ return void 0;
125
+ }
126
+ return {
127
+ digest,
128
+ redaction
129
+ };
130
+ }
131
+ function buildResult(action) {
132
+ if (!action.status) {
133
+ return void 0;
134
+ }
135
+ const result = {
136
+ status: action.status
137
+ };
138
+ if (action.error_code) {
139
+ result.error_code = action.error_code;
140
+ }
141
+ if (action.retryable !== void 0) {
142
+ result.retryable = action.retryable;
143
+ }
144
+ return result;
145
+ }
146
+ function buildPolicyContext(action) {
147
+ if (!action.policy) {
148
+ return void 0;
149
+ }
150
+ const policy = {
151
+ decision: action.policy.decision
152
+ };
153
+ if (action.policy.sandbox_enabled !== void 0) {
154
+ policy.sandbox_enabled = action.policy.sandbox_enabled;
155
+ }
156
+ if (action.policy.elevated !== void 0) {
157
+ policy.elevated = action.policy.elevated;
158
+ }
159
+ return policy;
160
+ }
161
+ function toInteractionEvidence(entry, options = {}) {
162
+ const { defaultRedaction = "hash_only", includeSpoolAnchor = false } = options;
163
+ const { action } = entry;
164
+ const evidence = {
165
+ interaction_id: action.id,
166
+ kind: action.kind,
167
+ executor: buildExecutor(action),
168
+ started_at: action.started_at
169
+ };
170
+ const tool = buildToolTarget(action);
171
+ if (tool) {
172
+ evidence.tool = tool;
173
+ }
174
+ const resource = buildResourceTarget(action);
175
+ if (resource) {
176
+ evidence.resource = resource;
177
+ }
178
+ const input = buildPayloadRef(entry.input_digest, defaultRedaction);
179
+ if (input) {
180
+ evidence.input = input;
181
+ }
182
+ const output = buildPayloadRef(entry.output_digest, defaultRedaction);
183
+ if (output) {
184
+ evidence.output = output;
185
+ }
186
+ if (action.completed_at) {
187
+ evidence.completed_at = action.completed_at;
188
+ }
189
+ if (action.duration_ms !== void 0) {
190
+ evidence.duration_ms = action.duration_ms;
191
+ }
192
+ const result = buildResult(action);
193
+ if (result) {
194
+ evidence.result = result;
195
+ }
196
+ const policy = buildPolicyContext(action);
197
+ if (policy) {
198
+ evidence.policy = policy;
199
+ }
200
+ const extensions = {};
201
+ if (action.metadata && Object.keys(action.metadata).length > 0) {
202
+ const EXTENSION_KEY_PATTERN = /^[a-z0-9-]+\.[a-z0-9-]+\//;
203
+ const genericMetadata = {};
204
+ for (const [key, value] of Object.entries(action.metadata)) {
205
+ if (EXTENSION_KEY_PATTERN.test(key)) {
206
+ extensions[key] = value;
207
+ } else {
208
+ genericMetadata[key] = value;
209
+ }
210
+ }
211
+ if (Object.keys(genericMetadata).length > 0) {
212
+ extensions["org.peacprotocol/capture-metadata@0.1"] = genericMetadata;
213
+ }
214
+ }
215
+ if (includeSpoolAnchor) {
216
+ const anchor = {
217
+ spool_head_digest: entry.entry_digest,
218
+ sequence: entry.sequence,
219
+ anchored_at: entry.captured_at
220
+ };
221
+ extensions["org.peacprotocol/spool-anchor@0.1"] = anchor;
222
+ }
223
+ if (Object.keys(extensions).length > 0) {
224
+ evidence.extensions = extensions;
225
+ }
226
+ return evidence;
227
+ }
228
+ function toInteractionEvidenceBatch(entries, options = {}) {
229
+ return entries.map((entry) => toInteractionEvidence(entry, options));
230
+ }
231
+
232
+ // src/session.ts
233
+ var DefaultCaptureSession = class {
234
+ store;
235
+ dedupe;
236
+ hasher;
237
+ closed = false;
238
+ // Concurrency serialization: queue captures to prevent race conditions
239
+ // on sequence numbers and chain linkage
240
+ captureQueue = Promise.resolve();
241
+ constructor(config) {
242
+ this.store = config.store;
243
+ this.dedupe = config.dedupe;
244
+ this.hasher = config.hasher;
245
+ }
246
+ /**
247
+ * Capture an action.
248
+ *
249
+ * Process:
250
+ * 1. Validate action
251
+ * 2. Serialize via queue (prevents race conditions)
252
+ * 3. Check for duplicate (by action.id)
253
+ * 4. Hash input/output payloads
254
+ * 5. Create spool entry with chain link
255
+ * 6. Append to spool
256
+ * 7. Update dedupe index
257
+ *
258
+ * Concurrency: Capture calls are serialized to prevent race conditions
259
+ * on sequence numbers and chain linkage.
260
+ *
261
+ * GUARANTEE: This method NEVER throws. All failures are returned as
262
+ * CaptureResult with success=false. This ensures the capture queue
263
+ * remains healthy and subsequent captures can proceed.
264
+ */
265
+ async capture(action) {
266
+ try {
267
+ if (this.closed) {
268
+ return {
269
+ success: false,
270
+ code: "E_CAPTURE_SESSION_CLOSED",
271
+ message: "CaptureSession is closed"
272
+ };
273
+ }
274
+ const validationError = this.validateAction(action);
275
+ if (validationError) {
276
+ return {
277
+ success: false,
278
+ code: "E_CAPTURE_INVALID_ACTION",
279
+ message: validationError
280
+ };
281
+ }
282
+ let result;
283
+ const capturePromise = this.captureQueue.then(async () => {
284
+ result = await this.captureInternal(action);
285
+ });
286
+ this.captureQueue = capturePromise.catch(() => {
287
+ });
288
+ await capturePromise;
289
+ return result;
290
+ } catch (error) {
291
+ const message = error instanceof Error ? error.message : String(error);
292
+ return {
293
+ success: false,
294
+ code: "E_CAPTURE_INTERNAL",
295
+ message: `Internal capture error: ${message}`
296
+ };
297
+ }
298
+ }
299
+ /**
300
+ * Internal capture logic (runs serialized).
301
+ *
302
+ * IMPORTANT: This method must NEVER throw - it always returns CaptureResult.
303
+ * This is critical for queue safety: if this throws, the queue chain would
304
+ * propagate the rejection to the caller while the queue itself stays healthy.
305
+ */
306
+ async captureInternal(action) {
307
+ try {
308
+ if (await this.dedupe.has(action.id)) {
309
+ return {
310
+ success: false,
311
+ code: "E_CAPTURE_DUPLICATE",
312
+ message: `Action ${action.id} already captured`
313
+ };
314
+ }
315
+ const inputDigest = action.input_bytes ? await this.hasher.digest(action.input_bytes) : void 0;
316
+ const outputDigest = action.output_bytes ? await this.hasher.digest(action.output_bytes) : void 0;
317
+ const prevDigest = await this.store.getHeadDigest();
318
+ const sequence = await this.store.getSequence() + 1;
319
+ const capturedAt = action.completed_at ?? action.started_at;
320
+ const partialEntry = {
321
+ captured_at: capturedAt,
322
+ action: this.stripPayloadBytes(action),
323
+ input_digest: inputDigest,
324
+ output_digest: outputDigest,
325
+ prev_entry_digest: prevDigest,
326
+ sequence
327
+ };
328
+ const entryDigest = await this.hasher.digestEntry(partialEntry);
329
+ const entry = {
330
+ ...partialEntry,
331
+ entry_digest: entryDigest
332
+ };
333
+ await this.store.append(entry);
334
+ await this.dedupe.set(action.id, {
335
+ sequence,
336
+ entry_digest: entryDigest,
337
+ captured_at: capturedAt,
338
+ emitted: false
339
+ });
340
+ return { success: true, entry };
341
+ } catch (error) {
342
+ const message = error instanceof Error ? error.message : String(error);
343
+ if (message.includes("hash") || message.includes("digest")) {
344
+ return {
345
+ success: false,
346
+ code: "E_CAPTURE_HASH_FAILED",
347
+ message: `Hash failed: ${message}`
348
+ };
349
+ }
350
+ return {
351
+ success: false,
352
+ code: "E_CAPTURE_STORE_FAILED",
353
+ message: `Store failed: ${message}`
354
+ };
355
+ }
356
+ }
357
+ /**
358
+ * Commit pending writes to durable storage.
359
+ */
360
+ async commit() {
361
+ this.assertNotClosed();
362
+ await this.store.commit();
363
+ }
364
+ /**
365
+ * Get the current spool head digest.
366
+ */
367
+ async getHeadDigest() {
368
+ this.assertNotClosed();
369
+ return this.store.getHeadDigest();
370
+ }
371
+ /**
372
+ * Close the session and release resources.
373
+ */
374
+ async close() {
375
+ if (!this.closed) {
376
+ await this.store.close();
377
+ this.closed = true;
378
+ }
379
+ }
380
+ // =============================================================================
381
+ // Private Helpers
382
+ // =============================================================================
383
+ /**
384
+ * Validate action has required fields.
385
+ */
386
+ validateAction(action) {
387
+ if (!action.id || action.id.trim() === "") {
388
+ return "Missing action.id";
389
+ }
390
+ if (!action.kind || action.kind.trim() === "") {
391
+ return "Missing action.kind";
392
+ }
393
+ if (!action.platform || action.platform.trim() === "") {
394
+ return "Missing action.platform";
395
+ }
396
+ if (!action.started_at || action.started_at.trim() === "") {
397
+ return "Missing action.started_at";
398
+ }
399
+ return null;
400
+ }
401
+ /**
402
+ * Strip payload bytes from action for storage.
403
+ */
404
+ stripPayloadBytes(action) {
405
+ const { input_bytes, output_bytes, ...rest } = action;
406
+ return rest;
407
+ }
408
+ /**
409
+ * Assert session is not closed.
410
+ */
411
+ assertNotClosed() {
412
+ if (this.closed) {
413
+ throw new Error("CaptureSession is closed");
414
+ }
415
+ }
416
+ };
417
+ function createCaptureSession(config) {
418
+ return new DefaultCaptureSession(config);
419
+ }
420
+
421
+ export { ActionHasher, DefaultCaptureSession, GENESIS_DIGEST, SIZE_CONSTANTS, createCaptureSession, createHasher, toInteractionEvidence, toInteractionEvidenceBatch };
422
+ //# sourceMappingURL=index.mjs.map
423
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/hasher.ts","../src/mapper.ts","../src/session.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @peac/capture-core - Type Definitions\n *\n * Runtime-neutral types for the capture pipeline.\n * NO Node.js APIs, NO fs, NO path - pure types and interfaces only.\n *\n * Filesystem implementations belong in @peac/capture-node.\n */\n\nimport type { Digest } from '@peac/schema';\n\n// =============================================================================\n// Captured Action (Input to Pipeline)\n// =============================================================================\n\n/**\n * Execution status of a captured action.\n */\nexport type ActionStatus = 'ok' | 'error' | 'timeout' | 'canceled';\n\n/**\n * Policy snapshot at capture time.\n */\nexport interface PolicySnapshot {\n /** Policy decision that allowed/denied the action */\n decision: 'allow' | 'deny' | 'constrained';\n /** Whether sandbox mode was enabled */\n sandbox_enabled?: boolean;\n /** Whether elevated permissions were granted */\n elevated?: boolean;\n /** Hash of the effective policy document (64 lowercase hex) */\n policy_digest?: string;\n}\n\n/**\n * Runtime-neutral captured action.\n *\n * This is the input to the capture pipeline, before any hashing or\n * transformation. Platform-specific adapters convert their events\n * to this common format.\n *\n * IMPORTANT: Timestamps are ISO 8601 strings for deterministic serialization.\n */\nexport interface CapturedAction {\n /** Stable ID for idempotency/dedupe (REQUIRED) */\n id: string;\n\n /** Event kind - \"tool.call\", \"http.request\", etc. */\n kind: string;\n\n /** Platform identifier - \"openclaw\", \"mcp\", \"a2a\", \"claude-code\" */\n platform: string;\n\n /** Platform version (optional) */\n platform_version?: string;\n\n /** Plugin that captured this (optional) */\n plugin_id?: string;\n\n /** Tool name (for tool.call kind) */\n tool_name?: string;\n\n /** Tool provider (optional) */\n tool_provider?: string;\n\n /** Resource URI (for http/fs kinds) */\n resource_uri?: string;\n\n /** HTTP method (for http.request kind) */\n resource_method?: string;\n\n /** Raw input bytes (will be hashed, then discarded) */\n input_bytes?: Uint8Array;\n\n /** Raw output bytes (will be hashed, then discarded) */\n output_bytes?: Uint8Array;\n\n /** Start time (ISO 8601 string for determinism) */\n started_at: string;\n\n /** Completion time (ISO 8601 string) */\n completed_at?: string;\n\n /** Duration in milliseconds from monotonic clock */\n duration_ms?: number;\n\n /** Execution status */\n status?: ActionStatus;\n\n /** Error code if status is 'error' */\n error_code?: string;\n\n /** Whether the error is retryable */\n retryable?: boolean;\n\n /** Policy snapshot at execution time */\n policy?: PolicySnapshot;\n\n /** Platform-specific metadata (will be stored in extensions) */\n metadata?: Record<string, unknown>;\n}\n\n// =============================================================================\n// Spool Entry (Serializable Record)\n// =============================================================================\n\n/**\n * Spool entry - the post-hashing record that can be serialized.\n *\n * This contains computed digests but NOT raw payload bytes (privacy-preserving).\n * The format is deterministic for tamper-evident chaining.\n */\nexport interface SpoolEntry {\n /** When this entry was captured (RFC 3339) */\n captured_at: string;\n\n /** The captured action (without raw bytes) */\n action: Omit<CapturedAction, 'input_bytes' | 'output_bytes'>;\n\n /** Input payload digest (computed inline) */\n input_digest?: Digest;\n\n /** Output payload digest (computed inline) */\n output_digest?: Digest;\n\n // Tamper-evident chain fields\n\n /** Digest of previous entry in spool (chain link) */\n prev_entry_digest: string;\n\n /** Digest of this entry (for next entry's prev) */\n entry_digest: string;\n\n /** Sequence number in the spool (monotonic) */\n sequence: number;\n}\n\n// =============================================================================\n// Spool Store Interface (Abstract - No Implementation)\n// =============================================================================\n\n/**\n * Abstract spool storage interface.\n *\n * Implementations handle the actual storage mechanism:\n * - InMemorySpoolStore (for tests, in this package)\n * - FsSpoolStore (in @peac/capture-node)\n * - CloudSpoolStore (future, for serverless)\n */\nexport interface SpoolStore {\n /**\n * Append an entry to the spool.\n * Returns the assigned sequence number.\n */\n append(entry: SpoolEntry): Promise<number>;\n\n /**\n * Commit/sync the spool to durable storage.\n * No-op for in-memory stores.\n */\n commit(): Promise<void>;\n\n /**\n * Read entries starting from a sequence number.\n * Returns entries in order.\n */\n read(fromSequence: number, limit?: number): Promise<SpoolEntry[]>;\n\n /**\n * Get the current head digest (last entry's digest).\n * Returns genesis digest if spool is empty.\n */\n getHeadDigest(): Promise<string>;\n\n /**\n * Get the current sequence number (last entry's sequence).\n * Returns 0 if spool is empty.\n */\n getSequence(): Promise<number>;\n\n /**\n * Close the store and release resources.\n */\n close(): Promise<void>;\n}\n\n// =============================================================================\n// Dedupe Index Interface (Abstract - No Implementation)\n// =============================================================================\n\n/**\n * Dedupe entry - tracks captured actions to prevent duplicates.\n */\nexport interface DedupeEntry {\n /** Sequence number in spool */\n sequence: number;\n\n /** Entry digest for verification */\n entry_digest: string;\n\n /** When the action was captured */\n captured_at: string;\n\n /** Whether a receipt has been emitted */\n emitted: boolean;\n}\n\n/**\n * Abstract dedupe index interface.\n *\n * All methods are async to support durable backends (sqlite, kv, etc.)\n * without forcing implementers to use sync filesystem calls.\n *\n * Implementations handle the actual storage:\n * - InMemoryDedupeIndex (for tests, in this package)\n * - PersistentDedupeIndex (in @peac/capture-node)\n */\nexport interface DedupeIndex {\n /** Get entry by action ID */\n get(actionId: string): Promise<DedupeEntry | undefined>;\n\n /** Set entry for action ID */\n set(actionId: string, entry: DedupeEntry): Promise<void>;\n\n /** Check if action ID exists */\n has(actionId: string): Promise<boolean>;\n\n /** Mark an entry as emitted */\n markEmitted(actionId: string): Promise<boolean>;\n\n /** Delete entry (for cleanup) */\n delete(actionId: string): Promise<boolean>;\n\n /** Get count of entries */\n size(): Promise<number>;\n\n /** Clear all entries */\n clear(): Promise<void>;\n}\n\n// =============================================================================\n// Hasher Interface\n// =============================================================================\n\n/**\n * Hasher configuration.\n */\nexport interface HasherConfig {\n /** Maximum bytes to hash before truncating (default: 1MB) */\n truncateThreshold?: number;\n}\n\n/**\n * Hasher interface for computing payload digests.\n */\nexport interface Hasher {\n /**\n * Compute digest for payload bytes.\n * Automatically truncates if payload exceeds threshold.\n */\n digest(payload: Uint8Array): Promise<Digest>;\n\n /**\n * Compute digest for a spool entry (for chaining).\n * Uses deterministic serialization (JCS).\n */\n digestEntry(entry: Omit<SpoolEntry, 'entry_digest'>): Promise<string>;\n}\n\n// =============================================================================\n// Capture Session Interface\n// =============================================================================\n\n/**\n * Capture session configuration.\n */\nexport interface CaptureSessionConfig {\n /** Spool store implementation */\n store: SpoolStore;\n\n /** Dedupe index implementation */\n dedupe: DedupeIndex;\n\n /** Hasher implementation */\n hasher: Hasher;\n}\n\n/**\n * Capture result for a single action.\n */\nexport type CaptureResult =\n | { success: true; entry: SpoolEntry }\n | { success: false; code: CaptureErrorCode; message: string };\n\n/**\n * Capture error codes.\n *\n * Layer-separated error codes:\n * - E_CAPTURE_* codes are for capture pipeline failures, NOT schema validation\n * - E_INTERACTION_* codes (in @peac/schema) are for receipt/profile validation\n */\nexport type CaptureErrorCode =\n | 'E_CAPTURE_DUPLICATE'\n | 'E_CAPTURE_HASH_FAILED'\n | 'E_CAPTURE_STORE_FAILED'\n | 'E_CAPTURE_INVALID_ACTION'\n | 'E_CAPTURE_SESSION_CLOSED'\n | 'E_CAPTURE_INTERNAL';\n\n/**\n * Capture session - stateful capture pipeline instance.\n */\nexport interface CaptureSession {\n /**\n * Capture an action.\n * Returns success with entry, or failure with error code.\n */\n capture(action: CapturedAction): Promise<CaptureResult>;\n\n /**\n * Commit any pending writes to durable storage.\n */\n commit(): Promise<void>;\n\n /**\n * Get the current spool head digest.\n */\n getHeadDigest(): Promise<string>;\n\n /**\n * Close the session and release resources.\n */\n close(): Promise<void>;\n}\n\n// =============================================================================\n// Spool Anchor (for External Verifiability)\n// =============================================================================\n\n/**\n * Spool anchor extension data.\n *\n * When included in a receipt, this allows external verifiers to\n * check the spool chain without access to the full spool file.\n */\nexport interface SpoolAnchor {\n /** Current head digest of the spool chain */\n spool_head_digest: string;\n\n /** Sequence number in the spool */\n sequence: number;\n\n /** Timestamp of the anchor */\n anchored_at: string;\n}\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/**\n * Genesis digest - the \"prev\" digest for the first entry in a spool.\n * 64 zeros represents \"no previous entry\".\n */\nexport const GENESIS_DIGEST = '0'.repeat(64);\n\n/**\n * Default truncation threshold: 1MB (1024 * 1024 bytes).\n */\nexport const DEFAULT_TRUNCATE_THRESHOLD = 1024 * 1024;\n\n/**\n * Size constants for truncation algorithms.\n */\nexport const SIZE_CONSTANTS = {\n K: 1024, // 1 KB\n M: 1024 * 1024, // 1 MB\n TRUNC_64K: 64 * 1024, // 64 KB\n TRUNC_1M: 1024 * 1024, // 1 MB\n} as const;\n","/**\n * @peac/capture-core - Action Hasher\n *\n * Deterministic hashing for capture pipeline.\n * Uses @peac/crypto for JCS (RFC 8785) and SHA-256.\n *\n * RUNTIME REQUIREMENT: WebCrypto (crypto.subtle) must be available.\n * Works in: Node.js 18+, Deno, Bun, modern browsers, Cloudflare Workers.\n */\n\nimport { canonicalize } from '@peac/crypto';\nimport type { Digest, DigestAlg } from '@peac/schema';\nimport type { Hasher, HasherConfig, SpoolEntry } from './types';\nimport { SIZE_CONSTANTS } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/**\n * Valid truncation thresholds (only these are supported).\n * Using other values would produce digests that don't match any declared algorithm.\n */\nconst VALID_TRUNCATE_THRESHOLDS = [SIZE_CONSTANTS.TRUNC_64K, SIZE_CONSTANTS.TRUNC_1M] as const;\ntype ValidTruncateThreshold = (typeof VALID_TRUNCATE_THRESHOLDS)[number];\n\n// =============================================================================\n// WebCrypto Runtime Check\n// =============================================================================\n\n/**\n * Get WebCrypto subtle interface with explicit runtime check.\n * Prefer globalThis.crypto over bare crypto to avoid bundler ambiguity.\n *\n * @throws Error if WebCrypto is not available\n */\nfunction getSubtle() {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new Error(\n 'WebCrypto (crypto.subtle) is required but not available. ' +\n 'Ensure you are running in Node.js 18+, Deno, Bun, or a modern browser.'\n );\n }\n return subtle;\n}\n\n// =============================================================================\n// SHA-256 Helper (Web Crypto API - Runtime Neutral)\n// =============================================================================\n\n/**\n * Compute SHA-256 hash of bytes using Web Crypto API.\n * Returns lowercase hex string.\n */\nasync function sha256Hex(data: Uint8Array): Promise<string> {\n const subtle = getSubtle();\n const hashBuffer = await subtle.digest('SHA-256', data);\n const hashArray = new Uint8Array(hashBuffer);\n return Array.from(hashArray)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n// =============================================================================\n// Action Hasher Implementation\n// =============================================================================\n\n/**\n * Default hasher implementation using JCS + SHA-256.\n *\n * Determinism guarantees:\n * - Same input bytes -> same digest\n * - Same SpoolEntry (minus entry_digest) -> same chain hash\n * - Truncation algorithm is deterministic (first N bytes)\n *\n * Supported truncation thresholds:\n * - 64k (65536 bytes) -> alg: 'sha-256:trunc-64k'\n * - 1m (1048576 bytes) -> alg: 'sha-256:trunc-1m'\n */\nexport class ActionHasher implements Hasher {\n private readonly truncateThreshold: ValidTruncateThreshold;\n\n constructor(config: HasherConfig = {}) {\n const threshold = config.truncateThreshold ?? SIZE_CONSTANTS.TRUNC_1M;\n\n // Validate threshold is one of the supported values\n if (!VALID_TRUNCATE_THRESHOLDS.includes(threshold as ValidTruncateThreshold)) {\n throw new RangeError(\n `truncateThreshold must be 64k (${SIZE_CONSTANTS.TRUNC_64K}) or 1m (${SIZE_CONSTANTS.TRUNC_1M}), ` +\n `got ${threshold}`\n );\n }\n\n this.truncateThreshold = threshold as ValidTruncateThreshold;\n\n // Validate WebCrypto is available at construction time\n getSubtle();\n }\n\n /**\n * Compute digest for payload bytes.\n * Automatically truncates if payload exceeds threshold.\n */\n async digest(payload: Uint8Array): Promise<Digest> {\n const bytes = payload.length;\n\n // No truncation needed - full SHA-256\n if (bytes <= this.truncateThreshold) {\n return {\n alg: 'sha-256' as DigestAlg,\n value: await sha256Hex(payload),\n bytes,\n };\n }\n\n // Large payload: truncate to threshold\n const truncated = payload.slice(0, this.truncateThreshold);\n\n // Determine algorithm label based on truncation size\n let alg: DigestAlg;\n if (this.truncateThreshold === SIZE_CONSTANTS.TRUNC_64K) {\n alg = 'sha-256:trunc-64k' as DigestAlg;\n } else {\n // SIZE_CONSTANTS.TRUNC_1M\n alg = 'sha-256:trunc-1m' as DigestAlg;\n }\n\n return {\n alg,\n value: await sha256Hex(truncated),\n bytes, // Original size for audit\n };\n }\n\n /**\n * Compute digest for a spool entry (for chaining).\n * Uses JCS (RFC 8785) for deterministic serialization.\n */\n async digestEntry(entry: Omit<SpoolEntry, 'entry_digest'>): Promise<string> {\n // JCS canonicalization ensures deterministic serialization\n const canonical = canonicalize(entry);\n const bytes = new TextEncoder().encode(canonical);\n return sha256Hex(bytes);\n }\n}\n\n/**\n * Create a default hasher instance.\n */\nexport function createHasher(config?: HasherConfig): Hasher {\n return new ActionHasher(config);\n}\n","/**\n * @peac/capture-core - Evidence Mapper\n *\n * Transforms SpoolEntry into InteractionEvidenceV01.\n * This is a pure transformation with no side effects.\n */\n\nimport type {\n InteractionEvidenceV01,\n Digest,\n PayloadRef,\n Executor,\n ToolTarget,\n ResourceTarget,\n Result,\n PolicyContext,\n} from '@peac/schema';\nimport type { SpoolEntry, SpoolAnchor } from './types';\n\n// =============================================================================\n// Mapper Types\n// =============================================================================\n\n/**\n * Stored action type (action without raw bytes).\n * This is the exact type stored in SpoolEntry.action.\n */\ntype StoredAction = SpoolEntry['action'];\n\n/**\n * Options for mapping SpoolEntry to InteractionEvidence.\n */\nexport interface MapperOptions {\n /**\n * Default redaction mode for payloads.\n * @default 'hash_only'\n */\n defaultRedaction?: 'hash_only' | 'redacted' | 'plaintext_allowlisted';\n\n /**\n * Include spool anchor in evidence extensions.\n * @default false\n */\n includeSpoolAnchor?: boolean;\n}\n\n// =============================================================================\n// Pure Transformation Functions\n// =============================================================================\n\n/**\n * Build executor from action.\n */\nfunction buildExecutor(action: StoredAction): Executor {\n const executor: Executor = {\n platform: action.platform,\n };\n\n if (action.platform_version) {\n executor.version = action.platform_version;\n }\n\n if (action.plugin_id) {\n executor.plugin_id = action.plugin_id;\n }\n\n return executor;\n}\n\n/**\n * Build tool target from action (if applicable).\n */\nfunction buildToolTarget(action: StoredAction): ToolTarget | undefined {\n if (!action.tool_name) {\n return undefined;\n }\n\n const tool: ToolTarget = {\n name: action.tool_name,\n };\n\n if (action.tool_provider) {\n tool.provider = action.tool_provider;\n }\n\n return tool;\n}\n\n/**\n * Build resource target from action (if applicable).\n */\nfunction buildResourceTarget(action: StoredAction): ResourceTarget | undefined {\n if (!action.resource_uri && !action.resource_method) {\n return undefined;\n }\n\n const resource: ResourceTarget = {};\n\n if (action.resource_uri) {\n resource.uri = action.resource_uri;\n }\n\n if (action.resource_method) {\n resource.method = action.resource_method;\n }\n\n return resource;\n}\n\n/**\n * Build payload reference from digest.\n */\nfunction buildPayloadRef(\n digest: Digest | undefined,\n redaction: 'hash_only' | 'redacted' | 'plaintext_allowlisted'\n): PayloadRef | undefined {\n if (!digest) {\n return undefined;\n }\n\n return {\n digest,\n redaction,\n };\n}\n\n/**\n * Build result from action.\n */\nfunction buildResult(action: StoredAction): Result | undefined {\n if (!action.status) {\n return undefined;\n }\n\n const result: Result = {\n status: action.status,\n };\n\n if (action.error_code) {\n result.error_code = action.error_code;\n }\n\n if (action.retryable !== undefined) {\n result.retryable = action.retryable;\n }\n\n return result;\n}\n\n/**\n * Build policy context from action.\n */\nfunction buildPolicyContext(action: StoredAction): PolicyContext | undefined {\n if (!action.policy) {\n return undefined;\n }\n\n const policy: PolicyContext = {\n decision: action.policy.decision,\n };\n\n if (action.policy.sandbox_enabled !== undefined) {\n policy.sandbox_enabled = action.policy.sandbox_enabled;\n }\n\n if (action.policy.elevated !== undefined) {\n policy.elevated = action.policy.elevated;\n }\n\n // Note: effective_policy_digest would require converting the string to Digest\n // This is left for the adapter to handle if needed\n\n return policy;\n}\n\n// =============================================================================\n// Main Mapper\n// =============================================================================\n\n/**\n * Convert a SpoolEntry to InteractionEvidenceV01.\n *\n * This is a pure, deterministic transformation.\n * The same SpoolEntry will always produce the same InteractionEvidence.\n */\nexport function toInteractionEvidence(\n entry: SpoolEntry,\n options: MapperOptions = {}\n): InteractionEvidenceV01 {\n const { defaultRedaction = 'hash_only', includeSpoolAnchor = false } = options;\n\n const { action } = entry;\n\n // Build the evidence object\n const evidence: InteractionEvidenceV01 = {\n interaction_id: action.id,\n kind: action.kind,\n executor: buildExecutor(action),\n started_at: action.started_at,\n };\n\n // Optional tool target\n const tool = buildToolTarget(action);\n if (tool) {\n evidence.tool = tool;\n }\n\n // Optional resource target\n const resource = buildResourceTarget(action);\n if (resource) {\n evidence.resource = resource;\n }\n\n // Optional input payload reference\n const input = buildPayloadRef(entry.input_digest, defaultRedaction);\n if (input) {\n evidence.input = input;\n }\n\n // Optional output payload reference\n const output = buildPayloadRef(entry.output_digest, defaultRedaction);\n if (output) {\n evidence.output = output;\n }\n\n // Optional completed_at\n if (action.completed_at) {\n evidence.completed_at = action.completed_at;\n }\n\n // Optional duration\n if (action.duration_ms !== undefined) {\n evidence.duration_ms = action.duration_ms;\n }\n\n // Optional result\n const result = buildResult(action);\n if (result) {\n evidence.result = result;\n }\n\n // Optional policy context\n const policy = buildPolicyContext(action);\n if (policy) {\n evidence.policy = policy;\n }\n\n // Build extensions\n const extensions: Record<string, unknown> = {};\n\n // Add platform-specific metadata if present\n if (action.metadata && Object.keys(action.metadata).length > 0) {\n // Check if metadata keys are already properly namespaced\n // Namespaced keys (e.g., org.openclaw/context) go directly into extensions\n // Non-namespaced keys go under the generic capture-metadata key\n const EXTENSION_KEY_PATTERN = /^[a-z0-9-]+\\.[a-z0-9-]+\\//;\n const genericMetadata: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(action.metadata)) {\n if (EXTENSION_KEY_PATTERN.test(key)) {\n // Already namespaced - add directly to extensions\n extensions[key] = value;\n } else {\n // Not namespaced - collect for generic key\n genericMetadata[key] = value;\n }\n }\n\n // Add non-namespaced metadata under generic key\n if (Object.keys(genericMetadata).length > 0) {\n extensions['org.peacprotocol/capture-metadata@0.1'] = genericMetadata;\n }\n }\n\n // Add spool anchor if requested\n if (includeSpoolAnchor) {\n const anchor: SpoolAnchor = {\n spool_head_digest: entry.entry_digest,\n sequence: entry.sequence,\n anchored_at: entry.captured_at,\n };\n extensions['org.peacprotocol/spool-anchor@0.1'] = anchor;\n }\n\n // Only add extensions if non-empty\n if (Object.keys(extensions).length > 0) {\n evidence.extensions = extensions;\n }\n\n return evidence;\n}\n\n/**\n * Batch convert SpoolEntries to InteractionEvidence array.\n */\nexport function toInteractionEvidenceBatch(\n entries: SpoolEntry[],\n options: MapperOptions = {}\n): InteractionEvidenceV01[] {\n return entries.map((entry) => toInteractionEvidence(entry, options));\n}\n","/**\n * @peac/capture-core - Capture Session\n *\n * Stateful capture pipeline that orchestrates hashing, deduplication,\n * and spool storage.\n */\n\nimport type {\n CaptureSession,\n CaptureSessionConfig,\n CapturedAction,\n CaptureResult,\n SpoolEntry,\n Hasher,\n SpoolStore,\n DedupeIndex,\n} from './types';\n\n// =============================================================================\n// Capture Session Implementation\n// =============================================================================\n\n/**\n * Default capture session implementation.\n *\n * Orchestrates:\n * 1. Deduplication check\n * 2. Payload hashing (input/output)\n * 3. Entry creation with chain linking\n * 4. Spool storage\n * 5. Dedupe index update\n */\nexport class DefaultCaptureSession implements CaptureSession {\n private readonly store: SpoolStore;\n private readonly dedupe: DedupeIndex;\n private readonly hasher: Hasher;\n private closed: boolean = false;\n\n // Concurrency serialization: queue captures to prevent race conditions\n // on sequence numbers and chain linkage\n private captureQueue: Promise<void> = Promise.resolve();\n\n constructor(config: CaptureSessionConfig) {\n this.store = config.store;\n this.dedupe = config.dedupe;\n this.hasher = config.hasher;\n }\n\n /**\n * Capture an action.\n *\n * Process:\n * 1. Validate action\n * 2. Serialize via queue (prevents race conditions)\n * 3. Check for duplicate (by action.id)\n * 4. Hash input/output payloads\n * 5. Create spool entry with chain link\n * 6. Append to spool\n * 7. Update dedupe index\n *\n * Concurrency: Capture calls are serialized to prevent race conditions\n * on sequence numbers and chain linkage.\n *\n * GUARANTEE: This method NEVER throws. All failures are returned as\n * CaptureResult with success=false. This ensures the capture queue\n * remains healthy and subsequent captures can proceed.\n */\n async capture(action: CapturedAction): Promise<CaptureResult> {\n try {\n // Check session state (convert throw to result)\n if (this.closed) {\n return {\n success: false,\n code: 'E_CAPTURE_SESSION_CLOSED',\n message: 'CaptureSession is closed',\n };\n }\n\n // 1. Validate action (can run before serialization)\n const validationError = this.validateAction(action);\n if (validationError) {\n return {\n success: false,\n code: 'E_CAPTURE_INVALID_ACTION',\n message: validationError,\n };\n }\n\n // 2. Serialize capture operations to prevent race conditions\n let result: CaptureResult;\n const capturePromise = this.captureQueue.then(async () => {\n result = await this.captureInternal(action);\n });\n\n // Keep queue alive regardless of outcome\n this.captureQueue = capturePromise.catch(() => {\n // Swallow errors to keep queue alive for subsequent captures\n });\n\n // Await result, but catch any unexpected throws\n await capturePromise;\n\n return result!;\n } catch (error) {\n // Last-resort catch: convert ANY unexpected throw to CaptureResult\n // This should never happen if captureInternal is correct, but provides safety\n const message = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n code: 'E_CAPTURE_INTERNAL',\n message: `Internal capture error: ${message}`,\n };\n }\n }\n\n /**\n * Internal capture logic (runs serialized).\n *\n * IMPORTANT: This method must NEVER throw - it always returns CaptureResult.\n * This is critical for queue safety: if this throws, the queue chain would\n * propagate the rejection to the caller while the queue itself stays healthy.\n */\n private async captureInternal(action: CapturedAction): Promise<CaptureResult> {\n try {\n // 3. Check for duplicate (inside try/catch for queue safety)\n if (await this.dedupe.has(action.id)) {\n return {\n success: false,\n code: 'E_CAPTURE_DUPLICATE',\n message: `Action ${action.id} already captured`,\n };\n }\n // 4. Hash payloads\n const inputDigest = action.input_bytes\n ? await this.hasher.digest(action.input_bytes)\n : undefined;\n\n const outputDigest = action.output_bytes\n ? await this.hasher.digest(action.output_bytes)\n : undefined;\n\n // 5. Get current chain state\n const prevDigest = await this.store.getHeadDigest();\n const sequence = (await this.store.getSequence()) + 1;\n\n // 6. Build entry (without entry_digest)\n // DETERMINISM: Derive captured_at from action timestamps, not wall clock.\n // This ensures the same action stream produces identical chain digests.\n const capturedAt = action.completed_at ?? action.started_at;\n const partialEntry: Omit<SpoolEntry, 'entry_digest'> = {\n captured_at: capturedAt,\n action: this.stripPayloadBytes(action),\n input_digest: inputDigest,\n output_digest: outputDigest,\n prev_entry_digest: prevDigest,\n sequence,\n };\n\n // 7. Compute entry digest\n const entryDigest = await this.hasher.digestEntry(partialEntry);\n\n // 8. Complete entry\n const entry: SpoolEntry = {\n ...partialEntry,\n entry_digest: entryDigest,\n };\n\n // 9. Append to spool\n await this.store.append(entry);\n\n // 10. Update dedupe index\n await this.dedupe.set(action.id, {\n sequence,\n entry_digest: entryDigest,\n captured_at: capturedAt,\n emitted: false,\n });\n\n return { success: true, entry };\n } catch (error) {\n // Determine error type\n const message = error instanceof Error ? error.message : String(error);\n\n if (message.includes('hash') || message.includes('digest')) {\n return {\n success: false,\n code: 'E_CAPTURE_HASH_FAILED',\n message: `Hash failed: ${message}`,\n };\n }\n\n return {\n success: false,\n code: 'E_CAPTURE_STORE_FAILED',\n message: `Store failed: ${message}`,\n };\n }\n }\n\n /**\n * Commit pending writes to durable storage.\n */\n async commit(): Promise<void> {\n this.assertNotClosed();\n await this.store.commit();\n }\n\n /**\n * Get the current spool head digest.\n */\n async getHeadDigest(): Promise<string> {\n this.assertNotClosed();\n return this.store.getHeadDigest();\n }\n\n /**\n * Close the session and release resources.\n */\n async close(): Promise<void> {\n if (!this.closed) {\n await this.store.close();\n this.closed = true;\n }\n }\n\n // =============================================================================\n // Private Helpers\n // =============================================================================\n\n /**\n * Validate action has required fields.\n */\n private validateAction(action: CapturedAction): string | null {\n if (!action.id || action.id.trim() === '') {\n return 'Missing action.id';\n }\n\n if (!action.kind || action.kind.trim() === '') {\n return 'Missing action.kind';\n }\n\n if (!action.platform || action.platform.trim() === '') {\n return 'Missing action.platform';\n }\n\n if (!action.started_at || action.started_at.trim() === '') {\n return 'Missing action.started_at';\n }\n\n return null;\n }\n\n /**\n * Strip payload bytes from action for storage.\n */\n private stripPayloadBytes(\n action: CapturedAction\n ): Omit<CapturedAction, 'input_bytes' | 'output_bytes'> {\n const { input_bytes, output_bytes, ...rest } = action;\n return rest;\n }\n\n /**\n * Assert session is not closed.\n */\n private assertNotClosed(): void {\n if (this.closed) {\n throw new Error('CaptureSession is closed');\n }\n }\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Create a capture session.\n */\nexport function createCaptureSession(config: CaptureSessionConfig): CaptureSession {\n return new DefaultCaptureSession(config);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,aAAa,EAKd,MAAM,SAAS,CAAC;AAMjB;;;;;;;;;GASG;AACH,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,MAAM,CAAkB;IAIhC,OAAO,CAAC,YAAY,CAAoC;gBAE5C,MAAM,EAAE,oBAAoB;IAMxC;;;;;;;;;;;;;;;;;;OAkBG;IACG,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAgD7D;;;;;;OAMG;YACW,eAAe;IA6E7B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAK7B;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC;IAKtC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAoBtB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;OAEG;IACH,OAAO,CAAC,eAAe;CAKxB;AAMD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAEjF"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,aAAa,EAKd,MAAM,SAAS,CAAC;AAMjB;;;;;;;;;GASG;AACH,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,MAAM,CAAkB;IAIhC,OAAO,CAAC,YAAY,CAAoC;gBAE5C,MAAM,EAAE,oBAAoB;IAMxC;;;;;;;;;;;;;;;;;;OAkBG;IACG,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAgD7D;;;;;;OAMG;YACW,eAAe;IA6E7B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAK7B;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC;IAKtC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAoBtB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAOzB;;OAEG;IACH,OAAO,CAAC,eAAe;CAKxB;AAMD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAEjF"}
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ // src/types.ts
4
+ var GENESIS_DIGEST = "0".repeat(64);
5
+
6
+ // src/memory.ts
7
+ var InMemorySpoolStore = class {
8
+ entries = [];
9
+ headDigest = GENESIS_DIGEST;
10
+ currentSequence = 0;
11
+ closed = false;
12
+ /**
13
+ * Append an entry to the spool.
14
+ */
15
+ async append(entry) {
16
+ this.assertNotClosed();
17
+ if (entry.sequence !== this.currentSequence + 1) {
18
+ throw new Error(
19
+ `Invalid sequence: expected ${this.currentSequence + 1}, got ${entry.sequence}`
20
+ );
21
+ }
22
+ if (entry.prev_entry_digest !== this.headDigest) {
23
+ throw new Error(
24
+ `Invalid chain: expected prev_entry_digest ${this.headDigest}, got ${entry.prev_entry_digest}`
25
+ );
26
+ }
27
+ this.entries.push(entry);
28
+ this.headDigest = entry.entry_digest;
29
+ this.currentSequence = entry.sequence;
30
+ return entry.sequence;
31
+ }
32
+ /**
33
+ * Commit is a no-op for in-memory store.
34
+ */
35
+ async commit() {
36
+ this.assertNotClosed();
37
+ }
38
+ /**
39
+ * Read entries starting from a sequence number.
40
+ */
41
+ async read(fromSequence, limit) {
42
+ this.assertNotClosed();
43
+ const startIndex = fromSequence > 0 ? fromSequence - 1 : 0;
44
+ const entries = this.entries.slice(startIndex);
45
+ if (limit !== void 0 && limit > 0) {
46
+ return entries.slice(0, limit);
47
+ }
48
+ return entries;
49
+ }
50
+ /**
51
+ * Get the current head digest.
52
+ */
53
+ async getHeadDigest() {
54
+ this.assertNotClosed();
55
+ return this.headDigest;
56
+ }
57
+ /**
58
+ * Get the current sequence number.
59
+ */
60
+ async getSequence() {
61
+ this.assertNotClosed();
62
+ return this.currentSequence;
63
+ }
64
+ /**
65
+ * Close the store.
66
+ */
67
+ async close() {
68
+ this.closed = true;
69
+ }
70
+ /**
71
+ * Check if store is closed.
72
+ */
73
+ assertNotClosed() {
74
+ if (this.closed) {
75
+ throw new Error("SpoolStore is closed");
76
+ }
77
+ }
78
+ // Test helpers (not part of interface)
79
+ /**
80
+ * Get all entries (for testing).
81
+ */
82
+ getAllEntries() {
83
+ return [...this.entries];
84
+ }
85
+ /**
86
+ * Clear all entries (for testing).
87
+ */
88
+ clear() {
89
+ this.entries = [];
90
+ this.headDigest = GENESIS_DIGEST;
91
+ this.currentSequence = 0;
92
+ }
93
+ };
94
+ var InMemoryDedupeIndex = class {
95
+ entries = /* @__PURE__ */ new Map();
96
+ /**
97
+ * Get entry by action ID.
98
+ */
99
+ async get(actionId) {
100
+ return this.entries.get(actionId);
101
+ }
102
+ /**
103
+ * Set entry for action ID.
104
+ */
105
+ async set(actionId, entry) {
106
+ this.entries.set(actionId, entry);
107
+ }
108
+ /**
109
+ * Check if action ID exists.
110
+ */
111
+ async has(actionId) {
112
+ return this.entries.has(actionId);
113
+ }
114
+ /**
115
+ * Mark an entry as emitted.
116
+ */
117
+ async markEmitted(actionId) {
118
+ const entry = this.entries.get(actionId);
119
+ if (!entry) {
120
+ return false;
121
+ }
122
+ entry.emitted = true;
123
+ return true;
124
+ }
125
+ /**
126
+ * Delete entry.
127
+ */
128
+ async delete(actionId) {
129
+ return this.entries.delete(actionId);
130
+ }
131
+ /**
132
+ * Get count of entries.
133
+ */
134
+ async size() {
135
+ return this.entries.size;
136
+ }
137
+ /**
138
+ * Clear all entries.
139
+ */
140
+ async clear() {
141
+ this.entries.clear();
142
+ }
143
+ // Test helpers (not part of interface)
144
+ /**
145
+ * Get all entries as array (for testing).
146
+ */
147
+ getAllEntries() {
148
+ return [...this.entries.entries()];
149
+ }
150
+ };
151
+ function createInMemorySpoolStore() {
152
+ return new InMemorySpoolStore();
153
+ }
154
+ function createInMemoryDedupeIndex() {
155
+ return new InMemoryDedupeIndex();
156
+ }
157
+
158
+ exports.InMemoryDedupeIndex = InMemoryDedupeIndex;
159
+ exports.InMemorySpoolStore = InMemorySpoolStore;
160
+ exports.createInMemoryDedupeIndex = createInMemoryDedupeIndex;
161
+ exports.createInMemorySpoolStore = createInMemorySpoolStore;
162
+ //# sourceMappingURL=testkit.cjs.map
163
+ //# sourceMappingURL=testkit.cjs.map