@peac/capture-core 0.10.7
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/README.md +291 -0
- package/dist/hasher.d.ts +42 -0
- package/dist/hasher.d.ts.map +1 -0
- package/dist/hasher.js +133 -0
- package/dist/hasher.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/mapper.d.ts +35 -0
- package/dist/mapper.d.ts.map +1 -0
- package/dist/mapper.js +212 -0
- package/dist/mapper.js.map +1 -0
- package/dist/memory.d.ts +110 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +196 -0
- package/dist/memory.js.map +1 -0
- package/dist/session.d.ts +82 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +244 -0
- package/dist/session.js.map +1 -0
- package/dist/testkit.d.ts +16 -0
- package/dist/testkit.d.ts.map +1 -0
- package/dist/testkit.js +23 -0
- package/dist/testkit.js.map +1 -0
- package/dist/types.d.ts +282 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +33 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# @peac/capture-core
|
|
2
|
+
|
|
3
|
+
Runtime-neutral capture pipeline for PEAC interaction evidence.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@peac/capture-core` provides a deterministic, tamper-evident capture pipeline for recording
|
|
8
|
+
agent interactions. It is designed to be runtime-agnostic (no Node.js/filesystem dependencies)
|
|
9
|
+
and can run in any JavaScript environment with WebCrypto support.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @peac/capture-core
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { createCaptureSession, createHasher } from '@peac/capture-core';
|
|
21
|
+
import { createInMemorySpoolStore, createInMemoryDedupeIndex } from '@peac/capture-core/testkit';
|
|
22
|
+
|
|
23
|
+
// Create a capture session
|
|
24
|
+
const session = createCaptureSession({
|
|
25
|
+
store: createInMemorySpoolStore(),
|
|
26
|
+
dedupe: createInMemoryDedupeIndex(),
|
|
27
|
+
hasher: createHasher(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Capture an action
|
|
31
|
+
const result = await session.capture({
|
|
32
|
+
id: 'action-001',
|
|
33
|
+
kind: 'tool.call',
|
|
34
|
+
platform: 'my-agent',
|
|
35
|
+
started_at: new Date().toISOString(),
|
|
36
|
+
tool_name: 'web_search',
|
|
37
|
+
input_bytes: new TextEncoder().encode('{"query": "hello"}'),
|
|
38
|
+
output_bytes: new TextEncoder().encode('{"results": []}'),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (result.success) {
|
|
42
|
+
console.log('Captured:', result.entry.entry_digest);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await session.close();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Determinism Contract
|
|
49
|
+
|
|
50
|
+
This package guarantees deterministic output for identical inputs. The following behaviors
|
|
51
|
+
are normative and MUST NOT change without a wire format version bump.
|
|
52
|
+
|
|
53
|
+
### Entry Digest Computation
|
|
54
|
+
|
|
55
|
+
The `entry_digest` is computed by:
|
|
56
|
+
|
|
57
|
+
1. Serializing the entry (minus `entry_digest` field) using JCS (RFC 8785)
|
|
58
|
+
2. Computing SHA-256 of the canonical JSON bytes
|
|
59
|
+
3. Encoding as lowercase hex (64 characters)
|
|
60
|
+
|
|
61
|
+
**Fields included in hash:**
|
|
62
|
+
|
|
63
|
+
- `captured_at` (RFC 3339 timestamp)
|
|
64
|
+
- `action` (full action object, minus `input_bytes`/`output_bytes`)
|
|
65
|
+
- `input_digest` (if present)
|
|
66
|
+
- `output_digest` (if present)
|
|
67
|
+
- `prev_entry_digest` (chain linkage)
|
|
68
|
+
- `sequence` (monotonic counter)
|
|
69
|
+
|
|
70
|
+
### Genesis Digest
|
|
71
|
+
|
|
72
|
+
The first entry in a chain has `prev_entry_digest` set to `GENESIS_DIGEST`, a
|
|
73
|
+
**protocol-defined sentinel value** consisting of 64 zero characters:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
0000000000000000000000000000000000000000000000000000000000000000
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This is NOT the SHA-256 hash of an empty string (which would be `e3b0c44...`).
|
|
80
|
+
It is an arbitrary constant chosen to be obviously distinguishable and to
|
|
81
|
+
simplify chain verification (check for all-zeros rather than compute a hash).
|
|
82
|
+
|
|
83
|
+
### Timestamp Derivation
|
|
84
|
+
|
|
85
|
+
`captured_at` is derived deterministically from action timestamps:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
captured_at = action.completed_at ?? action.started_at;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This ensures the same action stream produces identical chain digests across sessions.
|
|
92
|
+
Wall-clock time is NOT used.
|
|
93
|
+
|
|
94
|
+
**Monotonicity caveat:** `captured_at` values may be non-monotonic (out of order) even
|
|
95
|
+
though the chain is strictly ordered by sequence number. This can happen when actions
|
|
96
|
+
complete in a different order than they started. The chain ordering is by invocation
|
|
97
|
+
order, NOT by timestamp order.
|
|
98
|
+
|
|
99
|
+
### Payload Hashing
|
|
100
|
+
|
|
101
|
+
Payloads are hashed according to truncation thresholds:
|
|
102
|
+
|
|
103
|
+
| Size | Algorithm | Label |
|
|
104
|
+
| ------ | ----------------- | ------------------ |
|
|
105
|
+
| <= 1MB | Full SHA-256 | `sha-256` |
|
|
106
|
+
| > 1MB | First 1MB SHA-256 | `sha-256:trunc-1m` |
|
|
107
|
+
|
|
108
|
+
The `bytes` field always contains the original payload size (for audit).
|
|
109
|
+
|
|
110
|
+
### JCS Canonicalization
|
|
111
|
+
|
|
112
|
+
JSON canonicalization follows RFC 8785 with JavaScript-specific `undefined` handling:
|
|
113
|
+
|
|
114
|
+
- Object properties with `undefined` values are **omitted**
|
|
115
|
+
- Array elements that are `undefined` become **`null`**
|
|
116
|
+
- Top-level `undefined` **throws an error**
|
|
117
|
+
|
|
118
|
+
This matches `JSON.stringify` behavior. See `@peac/crypto` documentation for details.
|
|
119
|
+
|
|
120
|
+
## Concurrency Contract
|
|
121
|
+
|
|
122
|
+
### Single-Writer Per Session
|
|
123
|
+
|
|
124
|
+
Each `CaptureSession` instance maintains internal state (sequence number, head digest)
|
|
125
|
+
that is NOT thread-safe across multiple sessions. For concurrent agents:
|
|
126
|
+
|
|
127
|
+
- Create one session per agent/workflow
|
|
128
|
+
- Do NOT share sessions across async boundaries without serialization
|
|
129
|
+
|
|
130
|
+
### Capture Serialization
|
|
131
|
+
|
|
132
|
+
Concurrent `capture()` calls on the same session are automatically serialized:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// These run sequentially (not in parallel) to maintain chain integrity
|
|
136
|
+
const [r1, r2, r3] = await Promise.all([
|
|
137
|
+
session.capture(action1),
|
|
138
|
+
session.capture(action2),
|
|
139
|
+
session.capture(action3),
|
|
140
|
+
]);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Ordering:** Captures are ordered by invocation time (when `capture()` was called),
|
|
144
|
+
NOT by action timestamps. If timestamp-ordered chains are required, sort actions
|
|
145
|
+
before capturing.
|
|
146
|
+
|
|
147
|
+
### Never-Throw Guarantee
|
|
148
|
+
|
|
149
|
+
`capture()` NEVER throws exceptions. All failures are returned as `CaptureResult`:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const result = await session.capture(action);
|
|
153
|
+
if (!result.success) {
|
|
154
|
+
console.error(result.code, result.message);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Error codes:
|
|
159
|
+
|
|
160
|
+
- `E_CAPTURE_DUPLICATE` - Action ID already captured
|
|
161
|
+
- `E_CAPTURE_INVALID_ACTION` - Missing required fields
|
|
162
|
+
- `E_CAPTURE_HASH_FAILED` - Hashing operation failed
|
|
163
|
+
- `E_CAPTURE_STORE_FAILED` - Storage backend failed
|
|
164
|
+
- `E_CAPTURE_SESSION_CLOSED` - Session was closed
|
|
165
|
+
- `E_CAPTURE_INTERNAL` - Unexpected internal error
|
|
166
|
+
|
|
167
|
+
### Queue Recovery
|
|
168
|
+
|
|
169
|
+
If a capture fails, subsequent captures can still succeed. The queue is designed to
|
|
170
|
+
be resilient:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const r1 = await session.capture(badAction); // Fails
|
|
174
|
+
const r2 = await session.capture(goodAction); // Succeeds (queue not wedged)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Session Lifecycle and close()
|
|
178
|
+
|
|
179
|
+
The `close()` method releases session resources. Its behavior is:
|
|
180
|
+
|
|
181
|
+
**Semantics:**
|
|
182
|
+
|
|
183
|
+
- **Immediate:** `close()` does NOT wait for in-flight captures to drain. Any
|
|
184
|
+
capture already in progress may complete or fail.
|
|
185
|
+
- **Idempotent:** Multiple `close()` calls are safe and have no additional effect.
|
|
186
|
+
- **Terminal:** After `close()`, all subsequent `capture()` calls return
|
|
187
|
+
`E_CAPTURE_SESSION_CLOSED` (never throw).
|
|
188
|
+
|
|
189
|
+
**Best practice:** Wait for all captures to complete before closing:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// Good: wait for captures, then close
|
|
193
|
+
const results = await Promise.all([session.capture(action1), session.capture(action2)]);
|
|
194
|
+
await session.close();
|
|
195
|
+
|
|
196
|
+
// Risky: closing while captures in-flight
|
|
197
|
+
session.capture(action1); // May or may not complete
|
|
198
|
+
await session.close(); // Immediate - doesn't wait
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Resource cleanup:** `close()` calls `store.close()` on the underlying SpoolStore.
|
|
202
|
+
Custom SpoolStore implementations should release file handles, database connections,
|
|
203
|
+
or other resources in their `close()` method.
|
|
204
|
+
|
|
205
|
+
## API Reference
|
|
206
|
+
|
|
207
|
+
### Main Exports
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import {
|
|
211
|
+
// Constants
|
|
212
|
+
GENESIS_DIGEST, // Protocol-defined sentinel: 64 zeros (NOT sha256 of empty)
|
|
213
|
+
SIZE_CONSTANTS, // { TRUNC_64K: 65536, TRUNC_1M: 1048576 }
|
|
214
|
+
|
|
215
|
+
// Factories
|
|
216
|
+
createHasher, // Create a Hasher instance
|
|
217
|
+
createCaptureSession, // Create a CaptureSession
|
|
218
|
+
|
|
219
|
+
// Mappers
|
|
220
|
+
toInteractionEvidence, // SpoolEntry -> InteractionEvidenceV01
|
|
221
|
+
toInteractionEvidenceBatch, // SpoolEntry[] -> InteractionEvidenceV01[]
|
|
222
|
+
|
|
223
|
+
// Types
|
|
224
|
+
type CapturedAction,
|
|
225
|
+
type SpoolEntry,
|
|
226
|
+
type CaptureResult,
|
|
227
|
+
type Hasher,
|
|
228
|
+
type SpoolStore,
|
|
229
|
+
type DedupeIndex,
|
|
230
|
+
} from '@peac/capture-core';
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Testkit Exports
|
|
234
|
+
|
|
235
|
+
For testing only. Do NOT use in production:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import {
|
|
239
|
+
createInMemorySpoolStore,
|
|
240
|
+
createInMemoryDedupeIndex,
|
|
241
|
+
InMemorySpoolStore,
|
|
242
|
+
InMemoryDedupeIndex,
|
|
243
|
+
} from '@peac/capture-core/testkit';
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Implementing Custom Backends
|
|
247
|
+
|
|
248
|
+
### SpoolStore
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
interface SpoolStore {
|
|
252
|
+
append(entry: SpoolEntry): Promise<void>;
|
|
253
|
+
getHeadDigest(): Promise<string>;
|
|
254
|
+
getSequence(): Promise<number>;
|
|
255
|
+
commit(): Promise<void>;
|
|
256
|
+
close(): Promise<void>;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### DedupeIndex
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
interface DedupeIndex {
|
|
264
|
+
has(actionId: string): Promise<boolean>;
|
|
265
|
+
get(actionId: string): Promise<DedupeEntry | undefined>;
|
|
266
|
+
set(actionId: string, entry: DedupeEntry): Promise<void>;
|
|
267
|
+
markEmitted(actionId: string): Promise<boolean>;
|
|
268
|
+
delete(actionId: string): Promise<boolean>;
|
|
269
|
+
size(): Promise<number>;
|
|
270
|
+
clear(): Promise<void>;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Module Format
|
|
275
|
+
|
|
276
|
+
This package ships **CommonJS** output. ESM `import` is supported via Node's CJS interop:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
// Both work
|
|
280
|
+
import { createCaptureSession } from '@peac/capture-core'; // ESM (Node synthesizes default)
|
|
281
|
+
const { createCaptureSession } = require('@peac/capture-core'); // CJS
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Runtime Requirements
|
|
285
|
+
|
|
286
|
+
- **WebCrypto API**: `crypto.subtle` must be available
|
|
287
|
+
- Supported environments: Node.js 18+, Deno, Bun, modern browsers, Cloudflare Workers
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
Apache-2.0
|
package/dist/hasher.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @peac/capture-core - Action Hasher
|
|
3
|
+
*
|
|
4
|
+
* Deterministic hashing for capture pipeline.
|
|
5
|
+
* Uses @peac/crypto for JCS (RFC 8785) and SHA-256.
|
|
6
|
+
*
|
|
7
|
+
* RUNTIME REQUIREMENT: WebCrypto (crypto.subtle) must be available.
|
|
8
|
+
* Works in: Node.js 18+, Deno, Bun, modern browsers, Cloudflare Workers.
|
|
9
|
+
*/
|
|
10
|
+
import type { Digest } from '@peac/schema';
|
|
11
|
+
import type { Hasher, HasherConfig, SpoolEntry } from './types';
|
|
12
|
+
/**
|
|
13
|
+
* Default hasher implementation using JCS + SHA-256.
|
|
14
|
+
*
|
|
15
|
+
* Determinism guarantees:
|
|
16
|
+
* - Same input bytes -> same digest
|
|
17
|
+
* - Same SpoolEntry (minus entry_digest) -> same chain hash
|
|
18
|
+
* - Truncation algorithm is deterministic (first N bytes)
|
|
19
|
+
*
|
|
20
|
+
* Supported truncation thresholds:
|
|
21
|
+
* - 64k (65536 bytes) -> alg: 'sha-256:trunc-64k'
|
|
22
|
+
* - 1m (1048576 bytes) -> alg: 'sha-256:trunc-1m'
|
|
23
|
+
*/
|
|
24
|
+
export declare class ActionHasher implements Hasher {
|
|
25
|
+
private readonly truncateThreshold;
|
|
26
|
+
constructor(config?: HasherConfig);
|
|
27
|
+
/**
|
|
28
|
+
* Compute digest for payload bytes.
|
|
29
|
+
* Automatically truncates if payload exceeds threshold.
|
|
30
|
+
*/
|
|
31
|
+
digest(payload: Uint8Array): Promise<Digest>;
|
|
32
|
+
/**
|
|
33
|
+
* Compute digest for a spool entry (for chaining).
|
|
34
|
+
* Uses JCS (RFC 8785) for deterministic serialization.
|
|
35
|
+
*/
|
|
36
|
+
digestEntry(entry: Omit<SpoolEntry, 'entry_digest'>): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a default hasher instance.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createHasher(config?: HasherConfig): Hasher;
|
|
42
|
+
//# sourceMappingURL=hasher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hasher.d.ts","sourceRoot":"","sources":["../src/hasher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAwDhE;;;;;;;;;;;GAWG;AACH,qBAAa,YAAa,YAAW,MAAM;IACzC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAyB;gBAE/C,MAAM,GAAE,YAAiB;IAiBrC;;;OAGG;IACG,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IA+BlD;;;OAGG;IACG,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;CAM5E;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAE1D"}
|
package/dist/hasher.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @peac/capture-core - Action Hasher
|
|
4
|
+
*
|
|
5
|
+
* Deterministic hashing for capture pipeline.
|
|
6
|
+
* Uses @peac/crypto for JCS (RFC 8785) and SHA-256.
|
|
7
|
+
*
|
|
8
|
+
* RUNTIME REQUIREMENT: WebCrypto (crypto.subtle) must be available.
|
|
9
|
+
* Works in: Node.js 18+, Deno, Bun, modern browsers, Cloudflare Workers.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ActionHasher = void 0;
|
|
13
|
+
exports.createHasher = createHasher;
|
|
14
|
+
const crypto_1 = require("@peac/crypto");
|
|
15
|
+
const types_1 = require("./types");
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// =============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Valid truncation thresholds (only these are supported).
|
|
21
|
+
* Using other values would produce digests that don't match any declared algorithm.
|
|
22
|
+
*/
|
|
23
|
+
const VALID_TRUNCATE_THRESHOLDS = [types_1.SIZE_CONSTANTS.TRUNC_64K, types_1.SIZE_CONSTANTS.TRUNC_1M];
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// WebCrypto Runtime Check
|
|
26
|
+
// =============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Get WebCrypto subtle interface with explicit runtime check.
|
|
29
|
+
* Prefer globalThis.crypto over bare crypto to avoid bundler ambiguity.
|
|
30
|
+
*
|
|
31
|
+
* @throws Error if WebCrypto is not available
|
|
32
|
+
*/
|
|
33
|
+
function getSubtle() {
|
|
34
|
+
const subtle = globalThis.crypto?.subtle;
|
|
35
|
+
if (!subtle) {
|
|
36
|
+
throw new Error('WebCrypto (crypto.subtle) is required but not available. ' +
|
|
37
|
+
'Ensure you are running in Node.js 18+, Deno, Bun, or a modern browser.');
|
|
38
|
+
}
|
|
39
|
+
return subtle;
|
|
40
|
+
}
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// SHA-256 Helper (Web Crypto API - Runtime Neutral)
|
|
43
|
+
// =============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Compute SHA-256 hash of bytes using Web Crypto API.
|
|
46
|
+
* Returns lowercase hex string.
|
|
47
|
+
*/
|
|
48
|
+
async function sha256Hex(data) {
|
|
49
|
+
const subtle = getSubtle();
|
|
50
|
+
const hashBuffer = await subtle.digest('SHA-256', data);
|
|
51
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
52
|
+
return Array.from(hashArray)
|
|
53
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
54
|
+
.join('');
|
|
55
|
+
}
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Action Hasher Implementation
|
|
58
|
+
// =============================================================================
|
|
59
|
+
/**
|
|
60
|
+
* Default hasher implementation using JCS + SHA-256.
|
|
61
|
+
*
|
|
62
|
+
* Determinism guarantees:
|
|
63
|
+
* - Same input bytes -> same digest
|
|
64
|
+
* - Same SpoolEntry (minus entry_digest) -> same chain hash
|
|
65
|
+
* - Truncation algorithm is deterministic (first N bytes)
|
|
66
|
+
*
|
|
67
|
+
* Supported truncation thresholds:
|
|
68
|
+
* - 64k (65536 bytes) -> alg: 'sha-256:trunc-64k'
|
|
69
|
+
* - 1m (1048576 bytes) -> alg: 'sha-256:trunc-1m'
|
|
70
|
+
*/
|
|
71
|
+
class ActionHasher {
|
|
72
|
+
truncateThreshold;
|
|
73
|
+
constructor(config = {}) {
|
|
74
|
+
const threshold = config.truncateThreshold ?? types_1.SIZE_CONSTANTS.TRUNC_1M;
|
|
75
|
+
// Validate threshold is one of the supported values
|
|
76
|
+
if (!VALID_TRUNCATE_THRESHOLDS.includes(threshold)) {
|
|
77
|
+
throw new RangeError(`truncateThreshold must be 64k (${types_1.SIZE_CONSTANTS.TRUNC_64K}) or 1m (${types_1.SIZE_CONSTANTS.TRUNC_1M}), ` +
|
|
78
|
+
`got ${threshold}`);
|
|
79
|
+
}
|
|
80
|
+
this.truncateThreshold = threshold;
|
|
81
|
+
// Validate WebCrypto is available at construction time
|
|
82
|
+
getSubtle();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compute digest for payload bytes.
|
|
86
|
+
* Automatically truncates if payload exceeds threshold.
|
|
87
|
+
*/
|
|
88
|
+
async digest(payload) {
|
|
89
|
+
const bytes = payload.length;
|
|
90
|
+
// No truncation needed - full SHA-256
|
|
91
|
+
if (bytes <= this.truncateThreshold) {
|
|
92
|
+
return {
|
|
93
|
+
alg: 'sha-256',
|
|
94
|
+
value: await sha256Hex(payload),
|
|
95
|
+
bytes,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// Large payload: truncate to threshold
|
|
99
|
+
const truncated = payload.slice(0, this.truncateThreshold);
|
|
100
|
+
// Determine algorithm label based on truncation size
|
|
101
|
+
let alg;
|
|
102
|
+
if (this.truncateThreshold === types_1.SIZE_CONSTANTS.TRUNC_64K) {
|
|
103
|
+
alg = 'sha-256:trunc-64k';
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// SIZE_CONSTANTS.TRUNC_1M
|
|
107
|
+
alg = 'sha-256:trunc-1m';
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
alg,
|
|
111
|
+
value: await sha256Hex(truncated),
|
|
112
|
+
bytes, // Original size for audit
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Compute digest for a spool entry (for chaining).
|
|
117
|
+
* Uses JCS (RFC 8785) for deterministic serialization.
|
|
118
|
+
*/
|
|
119
|
+
async digestEntry(entry) {
|
|
120
|
+
// JCS canonicalization ensures deterministic serialization
|
|
121
|
+
const canonical = (0, crypto_1.canonicalize)(entry);
|
|
122
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
123
|
+
return sha256Hex(bytes);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.ActionHasher = ActionHasher;
|
|
127
|
+
/**
|
|
128
|
+
* Create a default hasher instance.
|
|
129
|
+
*/
|
|
130
|
+
function createHasher(config) {
|
|
131
|
+
return new ActionHasher(config);
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=hasher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hasher.js","sourceRoot":"","sources":["../src/hasher.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @peac/capture-core
|
|
3
|
+
*
|
|
4
|
+
* Runtime-neutral capture pipeline for PEAC interaction evidence.
|
|
5
|
+
*
|
|
6
|
+
* This package provides:
|
|
7
|
+
* - Types for captured actions and spool entries
|
|
8
|
+
* - Interfaces for storage (SpoolStore) and deduplication (DedupeIndex)
|
|
9
|
+
* - Hasher for deterministic payload hashing
|
|
10
|
+
* - Mapper for converting to InteractionEvidence
|
|
11
|
+
* - CaptureSession for orchestrating the pipeline
|
|
12
|
+
*
|
|
13
|
+
* NO FILESYSTEM OPERATIONS - those belong in @peac/capture-node.
|
|
14
|
+
*
|
|
15
|
+
* For in-memory test implementations, import from '@peac/capture-core/testkit'.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import {
|
|
20
|
+
* createCaptureSession,
|
|
21
|
+
* createHasher,
|
|
22
|
+
* toInteractionEvidence,
|
|
23
|
+
* } from '@peac/capture-core';
|
|
24
|
+
* import {
|
|
25
|
+
* createInMemorySpoolStore,
|
|
26
|
+
* createInMemoryDedupeIndex,
|
|
27
|
+
* } from '@peac/capture-core/testkit';
|
|
28
|
+
*
|
|
29
|
+
* const session = createCaptureSession({
|
|
30
|
+
* store: createInMemorySpoolStore(),
|
|
31
|
+
* dedupe: createInMemoryDedupeIndex(),
|
|
32
|
+
* hasher: createHasher(),
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* const result = await session.capture({
|
|
36
|
+
* id: 'action-123',
|
|
37
|
+
* kind: 'tool.call',
|
|
38
|
+
* platform: 'my-platform',
|
|
39
|
+
* started_at: new Date().toISOString(),
|
|
40
|
+
* tool_name: 'search',
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* if (result.success) {
|
|
44
|
+
* const evidence = toInteractionEvidence(result.entry);
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export type { CapturedAction, ActionStatus, PolicySnapshot, SpoolEntry, SpoolStore, DedupeIndex, DedupeEntry, Hasher, HasherConfig, CaptureSession, CaptureSessionConfig, CaptureResult, CaptureErrorCode, SpoolAnchor, } from './types';
|
|
49
|
+
export { GENESIS_DIGEST, SIZE_CONSTANTS } from './types';
|
|
50
|
+
export { ActionHasher, createHasher } from './hasher';
|
|
51
|
+
export { toInteractionEvidence, toInteractionEvidenceBatch } from './mapper';
|
|
52
|
+
export type { MapperOptions } from './mapper';
|
|
53
|
+
export { DefaultCaptureSession, createCaptureSession } from './session';
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAMH,YAAY,EAEV,cAAc,EACd,YAAY,EACZ,cAAc,EACd,UAAU,EAGV,UAAU,EACV,WAAW,EACX,WAAW,EACX,MAAM,EACN,YAAY,EACZ,cAAc,EACd,oBAAoB,EAGpB,aAAa,EACb,gBAAgB,EAGhB,WAAW,GACZ,MAAM,SAAS,CAAC;AAMjB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMzD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAMtD,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;AAC7E,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAM9C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @peac/capture-core
|
|
4
|
+
*
|
|
5
|
+
* Runtime-neutral capture pipeline for PEAC interaction evidence.
|
|
6
|
+
*
|
|
7
|
+
* This package provides:
|
|
8
|
+
* - Types for captured actions and spool entries
|
|
9
|
+
* - Interfaces for storage (SpoolStore) and deduplication (DedupeIndex)
|
|
10
|
+
* - Hasher for deterministic payload hashing
|
|
11
|
+
* - Mapper for converting to InteractionEvidence
|
|
12
|
+
* - CaptureSession for orchestrating the pipeline
|
|
13
|
+
*
|
|
14
|
+
* NO FILESYSTEM OPERATIONS - those belong in @peac/capture-node.
|
|
15
|
+
*
|
|
16
|
+
* For in-memory test implementations, import from '@peac/capture-core/testkit'.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import {
|
|
21
|
+
* createCaptureSession,
|
|
22
|
+
* createHasher,
|
|
23
|
+
* toInteractionEvidence,
|
|
24
|
+
* } from '@peac/capture-core';
|
|
25
|
+
* import {
|
|
26
|
+
* createInMemorySpoolStore,
|
|
27
|
+
* createInMemoryDedupeIndex,
|
|
28
|
+
* } from '@peac/capture-core/testkit';
|
|
29
|
+
*
|
|
30
|
+
* const session = createCaptureSession({
|
|
31
|
+
* store: createInMemorySpoolStore(),
|
|
32
|
+
* dedupe: createInMemoryDedupeIndex(),
|
|
33
|
+
* hasher: createHasher(),
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* const result = await session.capture({
|
|
37
|
+
* id: 'action-123',
|
|
38
|
+
* kind: 'tool.call',
|
|
39
|
+
* platform: 'my-platform',
|
|
40
|
+
* started_at: new Date().toISOString(),
|
|
41
|
+
* tool_name: 'search',
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* if (result.success) {
|
|
45
|
+
* const evidence = toInteractionEvidence(result.entry);
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.createCaptureSession = exports.DefaultCaptureSession = exports.toInteractionEvidenceBatch = exports.toInteractionEvidence = exports.createHasher = exports.ActionHasher = exports.SIZE_CONSTANTS = exports.GENESIS_DIGEST = void 0;
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Constants (public API)
|
|
53
|
+
// =============================================================================
|
|
54
|
+
var types_1 = require("./types");
|
|
55
|
+
Object.defineProperty(exports, "GENESIS_DIGEST", { enumerable: true, get: function () { return types_1.GENESIS_DIGEST; } });
|
|
56
|
+
Object.defineProperty(exports, "SIZE_CONSTANTS", { enumerable: true, get: function () { return types_1.SIZE_CONSTANTS; } });
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// Hasher (public API)
|
|
59
|
+
// =============================================================================
|
|
60
|
+
var hasher_1 = require("./hasher");
|
|
61
|
+
Object.defineProperty(exports, "ActionHasher", { enumerable: true, get: function () { return hasher_1.ActionHasher; } });
|
|
62
|
+
Object.defineProperty(exports, "createHasher", { enumerable: true, get: function () { return hasher_1.createHasher; } });
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Mapper (public API)
|
|
65
|
+
// =============================================================================
|
|
66
|
+
var mapper_1 = require("./mapper");
|
|
67
|
+
Object.defineProperty(exports, "toInteractionEvidence", { enumerable: true, get: function () { return mapper_1.toInteractionEvidence; } });
|
|
68
|
+
Object.defineProperty(exports, "toInteractionEvidenceBatch", { enumerable: true, get: function () { return mapper_1.toInteractionEvidenceBatch; } });
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// Session (public API)
|
|
71
|
+
// =============================================================================
|
|
72
|
+
var session_1 = require("./session");
|
|
73
|
+
Object.defineProperty(exports, "DefaultCaptureSession", { enumerable: true, get: function () { return session_1.DefaultCaptureSession; } });
|
|
74
|
+
Object.defineProperty(exports, "createCaptureSession", { enumerable: true, get: function () { return session_1.createCaptureSession; } });
|
|
75
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;;;AA8BH,gFAAgF;AAChF,yBAAyB;AACzB,gFAAgF;AAEhF,iCAAyD;AAAhD,uGAAA,cAAc,OAAA;AAAE,uGAAA,cAAc,OAAA;AAEvC,gFAAgF;AAChF,sBAAsB;AACtB,gFAAgF;AAEhF,mCAAsD;AAA7C,sGAAA,YAAY,OAAA;AAAE,sGAAA,YAAY,OAAA;AAEnC,gFAAgF;AAChF,sBAAsB;AACtB,gFAAgF;AAEhF,mCAA6E;AAApE,+GAAA,qBAAqB,OAAA;AAAE,oHAAA,0BAA0B,OAAA;AAG1D,gFAAgF;AAChF,uBAAuB;AACvB,gFAAgF;AAEhF,qCAAwE;AAA/D,gHAAA,qBAAqB,OAAA;AAAE,+GAAA,oBAAoB,OAAA"}
|
package/dist/mapper.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @peac/capture-core - Evidence Mapper
|
|
3
|
+
*
|
|
4
|
+
* Transforms SpoolEntry into InteractionEvidenceV01.
|
|
5
|
+
* This is a pure transformation with no side effects.
|
|
6
|
+
*/
|
|
7
|
+
import type { InteractionEvidenceV01 } from '@peac/schema';
|
|
8
|
+
import type { SpoolEntry } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* Options for mapping SpoolEntry to InteractionEvidence.
|
|
11
|
+
*/
|
|
12
|
+
export interface MapperOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Default redaction mode for payloads.
|
|
15
|
+
* @default 'hash_only'
|
|
16
|
+
*/
|
|
17
|
+
defaultRedaction?: 'hash_only' | 'redacted' | 'plaintext_allowlisted';
|
|
18
|
+
/**
|
|
19
|
+
* Include spool anchor in evidence extensions.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
includeSpoolAnchor?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Convert a SpoolEntry to InteractionEvidenceV01.
|
|
26
|
+
*
|
|
27
|
+
* This is a pure, deterministic transformation.
|
|
28
|
+
* The same SpoolEntry will always produce the same InteractionEvidence.
|
|
29
|
+
*/
|
|
30
|
+
export declare function toInteractionEvidence(entry: SpoolEntry, options?: MapperOptions): InteractionEvidenceV01;
|
|
31
|
+
/**
|
|
32
|
+
* Batch convert SpoolEntries to InteractionEvidence array.
|
|
33
|
+
*/
|
|
34
|
+
export declare function toInteractionEvidenceBatch(entries: SpoolEntry[], options?: MapperOptions): InteractionEvidenceV01[];
|
|
35
|
+
//# sourceMappingURL=mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mapper.d.ts","sourceRoot":"","sources":["../src/mapper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,sBAAsB,EAQvB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,SAAS,CAAC;AAYvD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,uBAAuB,CAAC;IAEtE;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAuID;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,aAAkB,GAC1B,sBAAsB,CAsGxB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,GAAE,aAAkB,GAC1B,sBAAsB,EAAE,CAE1B"}
|