Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PEAC Protocol Architecture

**Version:** 0.10.10
**Version:** 0.10.11
**Status:** Authoritative

This document describes the kernel-first architecture of the PEAC Protocol monorepo.
Expand Down Expand Up @@ -313,11 +313,12 @@ interface PaymentEvidence {

## Version History

| Version | Date | Changes |
| ------- | ---------- | -------------------------------------------------------------- |
| 0.10.10 | 2026-02-11 | Dev toolchain modernization, Node 22 baseline |
| 0.9.18 | 2025-12-19 | TAP, HTTP signatures, surfaces, examples, schema normalization |
| 0.9.17 | 2025-12-14 | x402 v2, Policy Kit, RSL alignment, subject binding |
| 0.9.16 | 2025-12-07 | CAL semantics, PaymentEvidence, SubjectProfile |
| 0.9.15 | 2025-11-18 | Kernel-first architecture, vendor neutrality |
| 0.9.14 | - | Initial wire format freeze |
| Version | Date | Changes |
| ------- | ---------- | ------------------------------------------------------------------------- |
| 0.10.11 | 2026-02-13 | Runtime deps (@noble/ed25519 v3, OTel v2), Stripe crypto, registry v0.3.0 |
| 0.10.10 | 2026-02-11 | Dev toolchain modernization, Node 22 baseline |
| 0.9.18 | 2025-12-19 | TAP, HTTP signatures, surfaces, examples, schema normalization |
| 0.9.17 | 2025-12-14 | x402 v2, Policy Kit, RSL alignment, subject binding |
| 0.9.16 | 2025-12-07 | CAL semantics, PaymentEvidence, SubjectProfile |
| 0.9.15 | 2025-11-18 | Kernel-first architecture, vendor neutrality |
| 0.9.14 | - | Initial wire format freeze |
97 changes: 97 additions & 0 deletions packages/capture/node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# @peac/capture-node

Node.js durable storage for the PEAC capture pipeline. Provides filesystem-backed implementations of the `SpoolStore` and `DedupeIndex` interfaces from `@peac/capture-core`.

## Install

```bash
npm install @peac/capture-node @peac/capture-core
```

## Usage

```typescript
import { createFsSpoolStore, createFsDedupeIndex } from '@peac/capture-node';
import { createCaptureSession, createHasher } from '@peac/capture-core';

const store = await createFsSpoolStore({
filePath: '/var/peac/spool.jsonl',
autoCommitIntervalMs: 5000,
});

const dedupe = await createFsDedupeIndex({
filePath: '/var/peac/dedupe.idx',
});

const session = createCaptureSession({
store,
dedupe,
hasher: createHasher(),
});
```

## Durability Contract

- **`append()`** writes to the OS page cache (no fsync). Fast, but not crash-safe on its own.
- **`commit()`** calls fsync -- the explicit durability point. Entries written before the last `commit()` survive crashes. Entries after may be lost.
- **Auto-commit timer** (default 5s) calls `commit()` periodically when dirty. Prevents long unflushed windows. Set `autoCommitIntervalMs: 0` to disable.

### Commit Ordering

When used with a dedupe index:

1. Spool `commit()` first (authoritative evidence log)
2. Dedupe `commit()` second (best-effort optimization index)

If dedupe commit fails after spool commit, worst case is re-emitting some receipts after restart. No evidence is lost. The dedupe index is disposable -- it can be deleted and rebuilt from the spool.

## Corruption Boundaries

- **Incomplete last line** (crash artifact): automatically truncated on startup. `onWarning` callback fired.
- **Malformed JSON mid-file**: spool marked corrupt. No auto-repair -- mid-file corruption could indicate tampering.
- **Chain linkage broken**: spool marked corrupt. `prev_entry_digest` chain failed verification.
- **Oversized line** (exceeds `maxLineBytes`): spool marked corrupt. Line was never materialized as a JS string.

When corrupt, the spool enters **read-only mode**: new captures are blocked, but export/verify/query tools still operate so the operator can recover salvageable data.

### Pre-Materialization Line Guard

The streaming line parser enforces `maxLineBytes` (default 4MB) at the Buffer level, BEFORE converting to a JS string. A single giant line in a spool file cannot cause an OOM crash.

## Diagnostics

```typescript
import { getFsSpoolDiagnostics } from '@peac/capture-node';

const diag = getFsSpoolDiagnostics(store);
// {
// mode: 'active' | 'read_only',
// spoolFull: boolean,
// spoolCorrupt: boolean,
// corruptReason?: 'CHAIN_BROKEN' | 'MALFORMED_JSON' | 'LINE_TOO_LARGE',
// corruptAtSequence?: number,
// entryCount, fileBytes, maxEntries, maxFileBytes, filePath
// }
```

## Hard-Cap Limits

- `maxEntries` (default: 100,000)
- `maxFileBytes` (default: 100MB)

When exceeded, `append()` throws `SpoolFullError`. The session returns `E_CAPTURE_STORE_FAILED` with a clear message. The adapter stays running (hooks, tools) -- only new captures are blocked.

## Reset Procedure

1. Export the evidence bundle (if any salvageable data)
2. Stop the plugin/adapter
3. Delete: `spool.jsonl`, `spool.jsonl.meta.json`, `dedupe.idx`, `*.lock`
4. Restart

## Single-Writer Guard

A lockfile (`spool.jsonl.lock`) prevents concurrent writers. Default: fail loudly if lock exists. Stale lock break is opt-in via `lockOptions.allowStaleLockBreak`.

## License

Apache-2.0
57 changes: 57 additions & 0 deletions packages/capture/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@peac/capture-node",
"version": "0.10.11",
"description": "Node.js durable storage for PEAC capture pipeline (filesystem spool store and dedupe index)",
"main": "dist/index.cjs",
"types": "dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"repository": {
"type": "git",
"url": "https://github.com/peacprotocol/peac.git",
"directory": "packages/capture/node"
},
"author": "jithinraj <7850727+jithinraj@users.noreply.github.com>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/peacprotocol/peac/issues"
},
"homepage": "https://github.com/peacprotocol/peac#readme",
"engines": {
"node": ">=22.0.0"
},
"files": [
"dist",
"README.md"
],
"publishConfig": {
"access": "public",
"provenance": true
},
"scripts": {
"prebuild": "rm -rf dist",
"build": "pnpm run build:js && pnpm run build:types",
"test": "pnpm -w vitest run packages/capture/node/tests",
"test:watch": "pnpm -w vitest packages/capture/node/tests",
"clean": "rm -rf dist",
"build:js": "tsup",
"build:types": "rm -f dist/.tsbuildinfo && tsc && rm -f dist/.tsbuildinfo"
},
"dependencies": {
"@peac/capture-core": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.3.3",
"vitest": "^4.0.0",
"tsup": "^8.0.0"
}
}
76 changes: 76 additions & 0 deletions packages/capture/node/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @peac/capture-node - Error Types
*/

// =============================================================================
// Types
// =============================================================================

/**
* Why the spool was marked corrupt. Exposed in SpoolDiagnostics.corruptReason.
*/
export type CorruptReason = 'CHAIN_BROKEN' | 'MALFORMED_JSON' | 'LINE_TOO_LARGE';

// =============================================================================
// Errors
// =============================================================================

/**
* Thrown when a spool reaches its hard-cap limits (maxEntries or maxFileBytes).
*
* CaptureSession catches this and returns E_CAPTURE_STORE_FAILED.
* The adapter stays running (hooks fire, tools work) -- only new captures are blocked.
*/
export class SpoolFullError extends Error {
readonly code = 'E_SPOOL_FULL' as const;

constructor(
readonly current: number,
readonly max: number,
readonly unit: 'entries' | 'bytes'
) {
super(`Spool full: ${current}/${max} ${unit}`);
this.name = 'SpoolFullError';
}
}

/**
* Thrown when spool corruption is detected (linkage, malformed JSON, oversized line).
*
* Blocks new appends. Tools still work so the user can export/inspect
* salvageable data. Operator must take explicit action to clear/reset.
*/
export class SpoolCorruptError extends Error {
readonly code = 'E_SPOOL_CORRUPT' as const;

constructor(
readonly reason: CorruptReason,
readonly corruptAtSequence?: number,
readonly details?: string
) {
super(
`Spool corrupt: ${reason}` +
(corruptAtSequence !== undefined ? ` at sequence ${corruptAtSequence}` : '') +
(details ? ` -- ${details}` : '')
);
this.name = 'SpoolCorruptError';
}
}

/**
* Thrown when a lockfile cannot be acquired (another instance holds it).
*/
export class LockfileError extends Error {
readonly code = 'E_LOCKFILE' as const;

constructor(
readonly lockPath: string,
readonly holderPid?: number
) {
const pidInfo = holderPid !== undefined ? ` PID: ${holderPid}.` : '';
super(
`Another PEAC instance holds the lock.${pidInfo} If stale, delete ${lockPath} or set allowStaleLockBreak: true`
);
this.name = 'LockfileError';
}
}
Loading
Loading