v0.10.13Since v0.10.7
@peac/capture-core
Runtime-neutral capture pipeline for PEAC interaction evidence. No filesystem or Node.js dependencies. Runs anywhere with WebCrypto.
Install
pnpm add @peac/capture-core
Quick Start
example.tsTypeScript
import { createCaptureSession, createHasher } from '@peac/capture-core';
import { createInMemorySpoolStore, createInMemoryDedupeIndex } from '@peac/capture-core/testkit';
const session = createCaptureSession({
store: createInMemorySpoolStore(),
dedupe: createInMemoryDedupeIndex(),
hasher: createHasher(),
});
const result = await session.capture({
id: 'action-001',
kind: 'tool.call',
platform: 'my-agent',
started_at: new Date().toISOString(),
tool_name: 'web_search',
input_bytes: new TextEncoder().encode('{"query": "hello"}'),
output_bytes: new TextEncoder().encode('{"results": []}'),
});
if (result.success) {
console.log('Captured:', result.entry.entry_digest);
}
await session.close();Determinism Contract
Identical inputs produce identical outputs. The same action stream produces the same chain digests across sessions.
Entry Digest
- JCS (RFC 8785) serialization
- SHA-256 of canonical JSON bytes
- 64 lowercase hex chars
Timestamp Derivation
captured_at = action.completed_at ?? action.started_at
Wall clock is not used.
Genesis Digest
The first entry has prev_entry_digest set to GENESIS_DIGEST:
0000000000000000000000000000000000000000000000000000000000000000
64 zeros. Not the SHA-256 of an empty string.
Payload Hashing
| Size | Algorithm | Label |
|---|---|---|
| ≤ 1 MB | Full SHA-256 | sha-256 |
| > 1 MB | First 1 MB SHA-256 | sha-256:trunc-1m |
The bytes field contains original size for audit.
Concurrency
Single-Writer Per Session
Each CaptureSession maintains internal state. Create one session per agent/workflow.
Capture Serialization
Concurrent capture() calls are serialized automatically:
// Runs sequentially to maintain chain integrity const [r1, r2, r3] = await Promise.all([ session.capture(action1), session.capture(action2), session.capture(action3), ]);
Never-Throw Guarantee
capture() never throws. All failures return as CaptureResult.
Error Codes
| Code | Description |
|---|---|
E_CAPTURE_DUPLICATE | Action ID already captured |
E_CAPTURE_INVALID_ACTION | Missing required fields |
E_CAPTURE_HASH_FAILED | Hashing operation failed |
E_CAPTURE_STORE_FAILED | Storage backend failed |
E_CAPTURE_SESSION_CLOSED | Session was closed |
E_CAPTURE_INTERNAL | Unexpected internal error |
Exports
import {
// Constants
GENESIS_DIGEST, // 64 zeros
SIZE_CONSTANTS, // { TRUNC_64K, TRUNC_1M }
// Factories
createHasher,
createCaptureSession,
// Mappers
toInteractionEvidence,
toInteractionEvidenceBatch,
// Types
type CapturedAction,
type SpoolEntry,
type CaptureResult,
type Hasher,
type SpoolStore,
type DedupeIndex,
} from '@peac/capture-core';Testkit
In-memory implementations for testing. Not for production.
import {
createInMemorySpoolStore,
createInMemoryDedupeIndex,
} from '@peac/capture-core/testkit';Custom Backends
SpoolStore
interface SpoolStore {
append(entry: SpoolEntry): Promise<void>;
getHeadDigest(): Promise<string>;
getSequence(): Promise<number>;
commit(): Promise<void>;
close(): Promise<void>;
}DedupeIndex
interface DedupeIndex {
has(actionId: string): Promise<boolean>;
get(actionId: string): Promise<DedupeEntry | undefined>;
set(actionId: string, entry: DedupeEntry): Promise<void>;
markEmitted(actionId: string): Promise<boolean>;
delete(actionId: string): Promise<boolean>;
size(): Promise<number>;
clear(): Promise<void>;
}