@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/LICENSE CHANGED
@@ -175,7 +175,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
175
175
 
176
176
  END OF TERMS AND CONDITIONS
177
177
 
178
- Copyright 2025 PEAC Protocol Contributors
178
+ Copyright 2025-2026 PEAC Protocol Contributors
179
179
 
180
180
  Licensed under the Apache License, Version 2.0 (the "License");
181
181
  you may not use this file except in compliance with the License.
package/dist/index.cjs ADDED
@@ -0,0 +1,432 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('@peac/crypto');
4
+
5
+ // src/types.ts
6
+ var GENESIS_DIGEST = "0".repeat(64);
7
+ var SIZE_CONSTANTS = {
8
+ K: 1024,
9
+ // 1 KB
10
+ M: 1024 * 1024,
11
+ // 1 MB
12
+ TRUNC_64K: 64 * 1024,
13
+ // 64 KB
14
+ TRUNC_1M: 1024 * 1024
15
+ // 1 MB
16
+ };
17
+ var VALID_TRUNCATE_THRESHOLDS = [SIZE_CONSTANTS.TRUNC_64K, SIZE_CONSTANTS.TRUNC_1M];
18
+ function getSubtle() {
19
+ const subtle = globalThis.crypto?.subtle;
20
+ if (!subtle) {
21
+ throw new Error(
22
+ "WebCrypto (crypto.subtle) is required but not available. Ensure you are running in Node.js 18+, Deno, Bun, or a modern browser."
23
+ );
24
+ }
25
+ return subtle;
26
+ }
27
+ async function sha256Hex(data) {
28
+ const subtle = getSubtle();
29
+ const hashBuffer = await subtle.digest("SHA-256", data);
30
+ const hashArray = new Uint8Array(hashBuffer);
31
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
32
+ }
33
+ var ActionHasher = class {
34
+ truncateThreshold;
35
+ constructor(config = {}) {
36
+ const threshold = config.truncateThreshold ?? SIZE_CONSTANTS.TRUNC_1M;
37
+ if (!VALID_TRUNCATE_THRESHOLDS.includes(threshold)) {
38
+ throw new RangeError(
39
+ `truncateThreshold must be 64k (${SIZE_CONSTANTS.TRUNC_64K}) or 1m (${SIZE_CONSTANTS.TRUNC_1M}), got ${threshold}`
40
+ );
41
+ }
42
+ this.truncateThreshold = threshold;
43
+ getSubtle();
44
+ }
45
+ /**
46
+ * Compute digest for payload bytes.
47
+ * Automatically truncates if payload exceeds threshold.
48
+ */
49
+ async digest(payload) {
50
+ const bytes = payload.length;
51
+ if (bytes <= this.truncateThreshold) {
52
+ return {
53
+ alg: "sha-256",
54
+ value: await sha256Hex(payload),
55
+ bytes
56
+ };
57
+ }
58
+ const truncated = payload.slice(0, this.truncateThreshold);
59
+ let alg;
60
+ if (this.truncateThreshold === SIZE_CONSTANTS.TRUNC_64K) {
61
+ alg = "sha-256:trunc-64k";
62
+ } else {
63
+ alg = "sha-256:trunc-1m";
64
+ }
65
+ return {
66
+ alg,
67
+ value: await sha256Hex(truncated),
68
+ bytes
69
+ // Original size for audit
70
+ };
71
+ }
72
+ /**
73
+ * Compute digest for a spool entry (for chaining).
74
+ * Uses JCS (RFC 8785) for deterministic serialization.
75
+ */
76
+ async digestEntry(entry) {
77
+ const canonical = crypto.canonicalize(entry);
78
+ const bytes = new TextEncoder().encode(canonical);
79
+ return sha256Hex(bytes);
80
+ }
81
+ };
82
+ function createHasher(config) {
83
+ return new ActionHasher(config);
84
+ }
85
+
86
+ // src/mapper.ts
87
+ function buildExecutor(action) {
88
+ const executor = {
89
+ platform: action.platform
90
+ };
91
+ if (action.platform_version) {
92
+ executor.version = action.platform_version;
93
+ }
94
+ if (action.plugin_id) {
95
+ executor.plugin_id = action.plugin_id;
96
+ }
97
+ return executor;
98
+ }
99
+ function buildToolTarget(action) {
100
+ if (!action.tool_name) {
101
+ return void 0;
102
+ }
103
+ const tool = {
104
+ name: action.tool_name
105
+ };
106
+ if (action.tool_provider) {
107
+ tool.provider = action.tool_provider;
108
+ }
109
+ return tool;
110
+ }
111
+ function buildResourceTarget(action) {
112
+ if (!action.resource_uri && !action.resource_method) {
113
+ return void 0;
114
+ }
115
+ const resource = {};
116
+ if (action.resource_uri) {
117
+ resource.uri = action.resource_uri;
118
+ }
119
+ if (action.resource_method) {
120
+ resource.method = action.resource_method;
121
+ }
122
+ return resource;
123
+ }
124
+ function buildPayloadRef(digest, redaction) {
125
+ if (!digest) {
126
+ return void 0;
127
+ }
128
+ return {
129
+ digest,
130
+ redaction
131
+ };
132
+ }
133
+ function buildResult(action) {
134
+ if (!action.status) {
135
+ return void 0;
136
+ }
137
+ const result = {
138
+ status: action.status
139
+ };
140
+ if (action.error_code) {
141
+ result.error_code = action.error_code;
142
+ }
143
+ if (action.retryable !== void 0) {
144
+ result.retryable = action.retryable;
145
+ }
146
+ return result;
147
+ }
148
+ function buildPolicyContext(action) {
149
+ if (!action.policy) {
150
+ return void 0;
151
+ }
152
+ const policy = {
153
+ decision: action.policy.decision
154
+ };
155
+ if (action.policy.sandbox_enabled !== void 0) {
156
+ policy.sandbox_enabled = action.policy.sandbox_enabled;
157
+ }
158
+ if (action.policy.elevated !== void 0) {
159
+ policy.elevated = action.policy.elevated;
160
+ }
161
+ return policy;
162
+ }
163
+ function toInteractionEvidence(entry, options = {}) {
164
+ const { defaultRedaction = "hash_only", includeSpoolAnchor = false } = options;
165
+ const { action } = entry;
166
+ const evidence = {
167
+ interaction_id: action.id,
168
+ kind: action.kind,
169
+ executor: buildExecutor(action),
170
+ started_at: action.started_at
171
+ };
172
+ const tool = buildToolTarget(action);
173
+ if (tool) {
174
+ evidence.tool = tool;
175
+ }
176
+ const resource = buildResourceTarget(action);
177
+ if (resource) {
178
+ evidence.resource = resource;
179
+ }
180
+ const input = buildPayloadRef(entry.input_digest, defaultRedaction);
181
+ if (input) {
182
+ evidence.input = input;
183
+ }
184
+ const output = buildPayloadRef(entry.output_digest, defaultRedaction);
185
+ if (output) {
186
+ evidence.output = output;
187
+ }
188
+ if (action.completed_at) {
189
+ evidence.completed_at = action.completed_at;
190
+ }
191
+ if (action.duration_ms !== void 0) {
192
+ evidence.duration_ms = action.duration_ms;
193
+ }
194
+ const result = buildResult(action);
195
+ if (result) {
196
+ evidence.result = result;
197
+ }
198
+ const policy = buildPolicyContext(action);
199
+ if (policy) {
200
+ evidence.policy = policy;
201
+ }
202
+ const extensions = {};
203
+ if (action.metadata && Object.keys(action.metadata).length > 0) {
204
+ const EXTENSION_KEY_PATTERN = /^[a-z0-9-]+\.[a-z0-9-]+\//;
205
+ const genericMetadata = {};
206
+ for (const [key, value] of Object.entries(action.metadata)) {
207
+ if (EXTENSION_KEY_PATTERN.test(key)) {
208
+ extensions[key] = value;
209
+ } else {
210
+ genericMetadata[key] = value;
211
+ }
212
+ }
213
+ if (Object.keys(genericMetadata).length > 0) {
214
+ extensions["org.peacprotocol/capture-metadata@0.1"] = genericMetadata;
215
+ }
216
+ }
217
+ if (includeSpoolAnchor) {
218
+ const anchor = {
219
+ spool_head_digest: entry.entry_digest,
220
+ sequence: entry.sequence,
221
+ anchored_at: entry.captured_at
222
+ };
223
+ extensions["org.peacprotocol/spool-anchor@0.1"] = anchor;
224
+ }
225
+ if (Object.keys(extensions).length > 0) {
226
+ evidence.extensions = extensions;
227
+ }
228
+ return evidence;
229
+ }
230
+ function toInteractionEvidenceBatch(entries, options = {}) {
231
+ return entries.map((entry) => toInteractionEvidence(entry, options));
232
+ }
233
+
234
+ // src/session.ts
235
+ var DefaultCaptureSession = class {
236
+ store;
237
+ dedupe;
238
+ hasher;
239
+ closed = false;
240
+ // Concurrency serialization: queue captures to prevent race conditions
241
+ // on sequence numbers and chain linkage
242
+ captureQueue = Promise.resolve();
243
+ constructor(config) {
244
+ this.store = config.store;
245
+ this.dedupe = config.dedupe;
246
+ this.hasher = config.hasher;
247
+ }
248
+ /**
249
+ * Capture an action.
250
+ *
251
+ * Process:
252
+ * 1. Validate action
253
+ * 2. Serialize via queue (prevents race conditions)
254
+ * 3. Check for duplicate (by action.id)
255
+ * 4. Hash input/output payloads
256
+ * 5. Create spool entry with chain link
257
+ * 6. Append to spool
258
+ * 7. Update dedupe index
259
+ *
260
+ * Concurrency: Capture calls are serialized to prevent race conditions
261
+ * on sequence numbers and chain linkage.
262
+ *
263
+ * GUARANTEE: This method NEVER throws. All failures are returned as
264
+ * CaptureResult with success=false. This ensures the capture queue
265
+ * remains healthy and subsequent captures can proceed.
266
+ */
267
+ async capture(action) {
268
+ try {
269
+ if (this.closed) {
270
+ return {
271
+ success: false,
272
+ code: "E_CAPTURE_SESSION_CLOSED",
273
+ message: "CaptureSession is closed"
274
+ };
275
+ }
276
+ const validationError = this.validateAction(action);
277
+ if (validationError) {
278
+ return {
279
+ success: false,
280
+ code: "E_CAPTURE_INVALID_ACTION",
281
+ message: validationError
282
+ };
283
+ }
284
+ let result;
285
+ const capturePromise = this.captureQueue.then(async () => {
286
+ result = await this.captureInternal(action);
287
+ });
288
+ this.captureQueue = capturePromise.catch(() => {
289
+ });
290
+ await capturePromise;
291
+ return result;
292
+ } catch (error) {
293
+ const message = error instanceof Error ? error.message : String(error);
294
+ return {
295
+ success: false,
296
+ code: "E_CAPTURE_INTERNAL",
297
+ message: `Internal capture error: ${message}`
298
+ };
299
+ }
300
+ }
301
+ /**
302
+ * Internal capture logic (runs serialized).
303
+ *
304
+ * IMPORTANT: This method must NEVER throw - it always returns CaptureResult.
305
+ * This is critical for queue safety: if this throws, the queue chain would
306
+ * propagate the rejection to the caller while the queue itself stays healthy.
307
+ */
308
+ async captureInternal(action) {
309
+ try {
310
+ if (await this.dedupe.has(action.id)) {
311
+ return {
312
+ success: false,
313
+ code: "E_CAPTURE_DUPLICATE",
314
+ message: `Action ${action.id} already captured`
315
+ };
316
+ }
317
+ const inputDigest = action.input_bytes ? await this.hasher.digest(action.input_bytes) : void 0;
318
+ const outputDigest = action.output_bytes ? await this.hasher.digest(action.output_bytes) : void 0;
319
+ const prevDigest = await this.store.getHeadDigest();
320
+ const sequence = await this.store.getSequence() + 1;
321
+ const capturedAt = action.completed_at ?? action.started_at;
322
+ const partialEntry = {
323
+ captured_at: capturedAt,
324
+ action: this.stripPayloadBytes(action),
325
+ input_digest: inputDigest,
326
+ output_digest: outputDigest,
327
+ prev_entry_digest: prevDigest,
328
+ sequence
329
+ };
330
+ const entryDigest = await this.hasher.digestEntry(partialEntry);
331
+ const entry = {
332
+ ...partialEntry,
333
+ entry_digest: entryDigest
334
+ };
335
+ await this.store.append(entry);
336
+ await this.dedupe.set(action.id, {
337
+ sequence,
338
+ entry_digest: entryDigest,
339
+ captured_at: capturedAt,
340
+ emitted: false
341
+ });
342
+ return { success: true, entry };
343
+ } catch (error) {
344
+ const message = error instanceof Error ? error.message : String(error);
345
+ if (message.includes("hash") || message.includes("digest")) {
346
+ return {
347
+ success: false,
348
+ code: "E_CAPTURE_HASH_FAILED",
349
+ message: `Hash failed: ${message}`
350
+ };
351
+ }
352
+ return {
353
+ success: false,
354
+ code: "E_CAPTURE_STORE_FAILED",
355
+ message: `Store failed: ${message}`
356
+ };
357
+ }
358
+ }
359
+ /**
360
+ * Commit pending writes to durable storage.
361
+ */
362
+ async commit() {
363
+ this.assertNotClosed();
364
+ await this.store.commit();
365
+ }
366
+ /**
367
+ * Get the current spool head digest.
368
+ */
369
+ async getHeadDigest() {
370
+ this.assertNotClosed();
371
+ return this.store.getHeadDigest();
372
+ }
373
+ /**
374
+ * Close the session and release resources.
375
+ */
376
+ async close() {
377
+ if (!this.closed) {
378
+ await this.store.close();
379
+ this.closed = true;
380
+ }
381
+ }
382
+ // =============================================================================
383
+ // Private Helpers
384
+ // =============================================================================
385
+ /**
386
+ * Validate action has required fields.
387
+ */
388
+ validateAction(action) {
389
+ if (!action.id || action.id.trim() === "") {
390
+ return "Missing action.id";
391
+ }
392
+ if (!action.kind || action.kind.trim() === "") {
393
+ return "Missing action.kind";
394
+ }
395
+ if (!action.platform || action.platform.trim() === "") {
396
+ return "Missing action.platform";
397
+ }
398
+ if (!action.started_at || action.started_at.trim() === "") {
399
+ return "Missing action.started_at";
400
+ }
401
+ return null;
402
+ }
403
+ /**
404
+ * Strip payload bytes from action for storage.
405
+ */
406
+ stripPayloadBytes(action) {
407
+ const { input_bytes, output_bytes, ...rest } = action;
408
+ return rest;
409
+ }
410
+ /**
411
+ * Assert session is not closed.
412
+ */
413
+ assertNotClosed() {
414
+ if (this.closed) {
415
+ throw new Error("CaptureSession is closed");
416
+ }
417
+ }
418
+ };
419
+ function createCaptureSession(config) {
420
+ return new DefaultCaptureSession(config);
421
+ }
422
+
423
+ exports.ActionHasher = ActionHasher;
424
+ exports.DefaultCaptureSession = DefaultCaptureSession;
425
+ exports.GENESIS_DIGEST = GENESIS_DIGEST;
426
+ exports.SIZE_CONSTANTS = SIZE_CONSTANTS;
427
+ exports.createCaptureSession = createCaptureSession;
428
+ exports.createHasher = createHasher;
429
+ exports.toInteractionEvidence = toInteractionEvidence;
430
+ exports.toInteractionEvidenceBatch = toInteractionEvidenceBatch;
431
+ //# sourceMappingURL=index.cjs.map
432
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/hasher.ts","../src/mapper.ts","../src/session.ts"],"names":["canonicalize"],"mappings":"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","file":"index.cjs","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"]}