first commit
This commit is contained in:
28
docs/networking/network/network-plugin-audit-mesh/README.md
Normal file
28
docs/networking/network/network-plugin-audit-mesh/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# @suaveplan/network-plugin-audit-mesh - Tamper-evident, cryptographically signed, append-only audit log chain for the Suaveplan mesh: SignedLog, LogReplicator (R-copy with tier-aware target selection), LogRetriever (range queries + pagination), IntegrityChecker, ConflictResolver, LogCompaction
|
||||
|
||||
## Summary
|
||||
|
||||
Tamper-evident, cryptographically signed, append-only audit log chain for the Suaveplan mesh. `SignedLog` produces hash-chained `LogEntry` records keyed by monotonic sequence with caller-injected `SignFn` (Ed25519 typically) so each entry's signature covers the deterministic SHA-256 of the prior entry plus the current payload; `LogReplicator` ships entries to `R` sibling hosts (default 2) with tier-aware target selection and synchronous-for-CRITICAL / async-for-NORMAL semantics; `LogRetriever` answers paginated `getLogs(meshId, fromSeq, toSeq)` range queries; `IntegrityChecker` walks the chain, detecting gaps / forks / tampered entries and emitting `audit:corruption-detected`; `ConflictResolver` reconciles divergent chains via longest-chain-wins with deterministic lex tiebreak; `LogCompaction` snapshots state into signed checkpoints to reduce reconciliation volume after server reconnect. Wrapped in a single `AuditMeshPlugin` satisfying the `NetworkPlugin` lifecycle.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [compaction](./src/compaction/log-compaction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [log-compaction](./src/compaction/log-compaction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [conflict](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [conflict-resolver](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [integrity](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [integrity-checker](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [signed-log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [audit-mesh-plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [log-replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [log-retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [error-codes](./src/error-codes.md) — Frozen `NET_AUDIT` error registry for `@suaveplan/network-plugin-audit-mesh`. Every failure path the audit subsystem ca…
|
||||
<!-- end:auto-docs --> - [log-compaction](./src/compaction/log-compaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [conflict](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_- [conflict-resolver](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity-checker](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_- [log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_- [signed-log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [audit-mesh-plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_- [retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen `NET_AUDIT` error registry for `@suaveplan/network-plugin-audit-mesh`. Every failure path the audit subsystem ca…
|
||||
- [log-compaction](./src/compaction/log-compaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [conflict](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_- [conflict-resolver](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity-checker](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_- [log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_- [signed-log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [audit-mesh-plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_- [retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen `NET_AUDIT` error registry for `@suaveplan/network-plugin-audit-mesh`. Every failure path the audit subsystem ca…
|
||||
- [log-compaction](./src/compaction/log-compaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [conflict](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_- [conflict-resolver](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity-checker](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_- [log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_- [signed-log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [audit-mesh-plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_- [retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_- [log-retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen `NET_AUDIT` error registry for `@suaveplan/network-plugin-audit-mesh`. Every failure path the audit subsystem ca…
|
||||
- [log-compaction](./src/compaction/log-compaction.md) — <!-- begin:auto-docs --> _(no submodules)_- [conflict](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_ - [conflict-resolver](./src/conflict/conflict-resolver.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_ - [integrity-checker](./src/integrity/integrity-checker.md) — <!-- begin:auto-docs --> _(no submodules)_- [log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_ - [signed-log](./src/log/signed-log.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [audit-mesh-plugin](./src/plugin/audit-mesh-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [log-replicator](./src/replicator/log-replicator.md) — <!-- begin:auto-docs --> _(no submodules)_- [retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_ - [log-retriever](./src/retriever/log-retriever.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen `NET_AUDIT` error registry for `@suaveplan/network-plugin-audit-mesh`. Every failure path the audit subsystem ca…
|
||||
@@ -0,0 +1,75 @@
|
||||
# LogCompaction
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Periodically compacts a long signed audit chain into a single signed `LogCheckpoint`, dramatically reducing the volume of entries the server has to replay after a long outage. A checkpoint is a snapshot of mesh/session state at a particular sequence number, signed by the same host (and the same key) that authored the surrounding chain — so consumers can verify the snapshot in isolation without walking back to the suaveplan entry.
|
||||
|
||||
The compaction module is deliberately stateless. It does not hold the chain or the snapshots; it just produces signed checkpoints from the inputs you give it and provides a `truncate` helper to discard already-checkpointed entries. This keeps the storage policy decoupled from the cryptographic policy.
|
||||
|
||||
## Features
|
||||
|
||||
- Builds a `LogCheckpoint` over `(seq, timestamp, meshId, hostId, stateHash, entryHashAtSeq)` and signs the canonical bytes.
|
||||
- Two anchors per checkpoint — `entryHashAtSeq` ties the checkpoint to a specific entry in the chain (so the server can verify it against its retrieval), and `stateHash` ties it to the application-level snapshot.
|
||||
- `verify(checkpoint, verifyFn)` round-trips the canonical bytes and asks the injected verifier whether the signature is valid.
|
||||
- `truncate(entries, checkpoint)` returns the entries with `seq > checkpoint.seq` so callers can prune their hot log to just the post-checkpoint suffix.
|
||||
- Empty entry list and missing-anchor errors raise `NET_AUDIT.CHECKPOINT_INVALID` with explanatory `error.context`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto-curve";
|
||||
import { LogCompaction } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const compactor = new LogCompaction({
|
||||
meshId: "mesh-a",
|
||||
hostId: "host-1",
|
||||
sign: (bytes) => ed25519.sign(bytes, hostPrivateKey),
|
||||
});
|
||||
|
||||
const checkpoint = await compactor.createCheckpoint({
|
||||
atSeq: 1024,
|
||||
entries: log.entries(),
|
||||
stateSnapshot: { peers: snapshot.peers, sessions: snapshot.sessions },
|
||||
});
|
||||
|
||||
const remaining = compactor.truncate(log.entries(), checkpoint);
|
||||
console.log(remaining.length); // post-1024 only
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto-curve";
|
||||
import { LogCompaction } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const compactor = new LogCompaction({
|
||||
meshId: "mesh-a",
|
||||
hostId: "host-1",
|
||||
clock: () => 1_700_000_000_000,
|
||||
sign: async (bytes) => await hardwareSigner.sign(bytes),
|
||||
});
|
||||
|
||||
const checkpoint = await compactor.createCheckpoint({ atSeq, entries, stateSnapshot });
|
||||
const valid = await compactor.verify(
|
||||
checkpoint,
|
||||
(bytes, sig, hostId) => ed25519.verify(sig, bytes, hostKeys.get(hostId)!)
|
||||
);
|
||||
if (!valid) throw new Error("checkpoint signature failed");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LogCompaction(config)` — `config: { meshId; hostId; sign: SignFn; clock?: () => number }`.
|
||||
- `createCheckpoint(input)` — `input: { atSeq; entries; stateSnapshot }`. Returns `Promise<LogCheckpoint>`. Raises `NET_AUDIT.CHECKPOINT_INVALID` on empty entries or missing anchor.
|
||||
- `verify(checkpoint, verify)` — `verify: CheckpointVerifyFn`. Returns `Promise<boolean>`.
|
||||
- `truncate(entries, checkpoint)` — returns the entries whose `seq` is strictly greater than `checkpoint.seq`.
|
||||
- `checkpointSigningBytes(value)` — exported helper that returns the canonical bytes a verifier should hash and signature-check. Useful when the caller already has a checkpoint and just wants to validate it without instantiating `LogCompaction`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The canonical bytes for a checkpoint are the same sorted-key JSON encoding used for entries; we keep two helpers (`entryHashBytes` for entries, `checkpointSigningBytes` for checkpoints) so that one cannot be accidentally substituted for the other.
|
||||
- `stateHash` is computed over the caller-supplied `stateSnapshot` value via the same canonicalize routine, so two snapshots with semantically equal contents but different key insertion order produce identical hashes.
|
||||
- `truncate` returns a fresh array so the caller can keep using the original entries for archival.
|
||||
@@ -0,0 +1,63 @@
|
||||
# ConflictResolver
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Picks the canonical chain when the server pulls audit logs from multiple Tier 0 hosts and finds disagreement. The rule is two-step and deterministic: (1) longest chain wins; (2) on equal length, the chain whose `lastHash` sorts lexicographically smallest wins. The deterministic tiebreak matters because the reconciler is itself replicated — two reconciler instances seeing the same divergent set must reach the same conclusion or the mesh keeps thrashing on every server reconnection.
|
||||
|
||||
The resolver is pure: input goes in, an outcome comes out, no side effects, no time, no randomness. That makes it the safest possible thing to call inside a property test or replay scenario.
|
||||
|
||||
## Features
|
||||
|
||||
- Longest-chain-wins primary rule.
|
||||
- Lexicographic-tiebreak secondary rule on `lastHash`, with a final stable sort key on `hostId` so two chains that somehow share both length and `lastHash` still produce a deterministic answer.
|
||||
- Returns the full winning chain, the winning host, the discriminating `reason` (`"longest-chain"` vs `"lex-tiebreak"`), and the list of `losers` so callers can rollback those hosts via the mesh.
|
||||
- Empty input throws `NET_AUDIT.CONFLICT_UNRESOLVED` rather than returning a sentinel — there is no meaningful "no chains" winner.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ConflictResolver } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const resolver = new ConflictResolver();
|
||||
const result = resolver.resolve([
|
||||
{ hostId: "host-1", entries: chain1, lastHash: "abc..." },
|
||||
{ hostId: "host-2", entries: chain2, lastHash: "def..." },
|
||||
]);
|
||||
console.log(result.winningHostId, result.reason, result.losers);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ConflictResolver,
|
||||
type ConflictResolution,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
function reconcile(
|
||||
pulled: ReadonlyMap<string, LogEntry[]>
|
||||
): ConflictResolution {
|
||||
const chains = Array.from(pulled.entries()).map(([hostId, entries]) => ({
|
||||
hostId,
|
||||
entries,
|
||||
lastHash: entries.length === 0
|
||||
? ""
|
||||
: entryDigestHex(entries[entries.length - 1]!),
|
||||
}));
|
||||
return new ConflictResolver().resolve(chains);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ConflictResolver` — stateless class; the no-arg constructor is intentional so all callers get the same rule set.
|
||||
- `resolve(chains)` — `chains: readonly DivergentChain[]`. Returns `ConflictResolution`. Throws `NET_AUDIT.CONFLICT_UNRESOLVED` if `chains.length === 0`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The "longest" reduce uses `>` rather than `>=` so the first chain wins on equal length; the `tied` filter then catches the equal-length case explicitly. This avoids a subtle bug where a strict-greater reduce would leak the iteration order into the result.
|
||||
- `hostId` is the final sort key because two chains with identical content (and therefore identical `lastHash`) can in theory exist when the same content was independently produced by two hosts; this is rare but not impossible, and we want the resolver to be total.
|
||||
- The losers list is produced by filtering the input by `winningHostId`, not by negating the tiebreak result, so a five-way conflict produces exactly four losers regardless of how deep the tiebreak chain went.
|
||||
@@ -0,0 +1,53 @@
|
||||
# error-codes
|
||||
|
||||
## Purpose
|
||||
|
||||
Frozen `NET_AUDIT` error registry for `@suaveplan/network-plugin-audit-mesh`. Every failure path the audit subsystem can surface — a missing sequence number in the chain, a tampered entry, a checkpoint that fails signature verification, a server reconciliation that cannot decide between divergent chains — is mapped onto one of these stable codes. Codes are emitted by `defineCodes("NET_AUDIT", [...] as const)` so the registry is locked at module load time and consumers downstream (logger redaction maps, i18n catalogs, the OpenSpec conformance checker) can rely on the set never silently growing.
|
||||
|
||||
The registry exists in its own module so the `index.ts` barrel can do `import "./error-codes.js"` first to register the codes via side effect before any subsystem starts referencing them.
|
||||
|
||||
## Features
|
||||
|
||||
- Eight codes covering chain integrity (`LOG_GAP`, `CHAIN_TAMPERED`, `SIGNATURE_INVALID`), sequencing (`INVALID_SEQ`), replication failure (`REPLICATION_FAILED`), checkpoint failures (`CHECKPOINT_INVALID`), conflict resolution failures (`CONFLICT_UNRESOLVED`), and range-query input validation (`RANGE_INVALID`).
|
||||
- Sibling `NetworkAuditError` extends `SuaveplanError<NetworkAuditErrorCode>` so consumers can write `instanceof NetworkAuditError` checks and access `error.code` with full type narrowing.
|
||||
- The registry is frozen by `defineCodes`; attempting to mutate it throws at runtime, and TypeScript's `as const` narrowing prevents adding new keys without a code change.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NETWORK_AUDIT_CODES,
|
||||
NetworkAuditError,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
throw new NetworkAuditError(
|
||||
NETWORK_AUDIT_CODES.LOG_GAP,
|
||||
"audit.log_gap",
|
||||
{ context: { meshId: "mesh-a", missingSeq: 7 } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkAuditError,
|
||||
type NetworkAuditErrorCode,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
function classify(err: unknown): NetworkAuditErrorCode | null {
|
||||
return err instanceof NetworkAuditError ? err.code : null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NETWORK_AUDIT_CODES` — frozen object with eight keys: `LOG_GAP`, `CHAIN_TAMPERED`, `SIGNATURE_INVALID`, `REPLICATION_FAILED`, `CHECKPOINT_INVALID`, `CONFLICT_UNRESOLVED`, `INVALID_SEQ`, `RANGE_INVALID`. Values are `"NET_AUDIT.<KEY>"` strings.
|
||||
- `NetworkAuditErrorCode` — string-literal union of the registry values.
|
||||
- `NetworkAuditError` — error class extending `SuaveplanError<NetworkAuditErrorCode>`. Constructor: `(code, messageKey, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The list grows only by adding to the array literal in `error-codes.ts`; there is no programmatic `register()` API by design — the `as const` plus `defineCodes` arrangement is the contract.
|
||||
- Every code in the registry must be referenced from `src/` somewhere — typically as the argument to `new NetworkAuditError(...)` (thrown classes), but `LOG_GAP` / `CHAIN_TAMPERED` / `SIGNATURE_INVALID` are **report-only** codes carried on `CorruptionReport.code` rather than thrown. The `CORRUPTION_KIND_TO_CODE` map in `integrity-checker.ts` is the single source of truth wiring those three codes onto the integrity-report stream. The conformance gate enforces that the codes are referenced at least once so dead codes cannot accumulate.
|
||||
- Code messages use dotted i18n keys (e.g. `audit.log_gap`) so the logger's i18n adapter can resolve them at format time without coupling this package to a specific locale catalog.
|
||||
@@ -0,0 +1,74 @@
|
||||
# IntegrityChecker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Walks a sequence of `LogEntry` records and decides whether the chain is intact: every `seq` is present (no gaps), every `prevHash` matches the SHA-256 of the previous entry's canonical bytes (no tamper, no fork), and — when an injected `verifySignature` is supplied — every `hostSignature` is a valid Ed25519 signature over those same bytes (no tamper of fields under signature). Any failure produces a `CorruptionReport` and emits `audit:corruption-detected` on the optional bus, so observability adapters can dashboard corruption rates.
|
||||
|
||||
The checker is the only module in audit-mesh that owns the canonical chain-walk algorithm. Both the integrity self-check on the originating host and the server-side cross-host validation step in `ConflictResolver` go through this module — keeping the rules in one place keeps the system honest.
|
||||
|
||||
## Features
|
||||
|
||||
- Detects **gaps** — when `entry.seq !== expectedSeq` (skipped sequences in a delivered chunk). Reports with `code: NETWORK_AUDIT_CODES.LOG_GAP`.
|
||||
- Detects **forks** — when an entry's `prevHash` is empty but the chain has continued (a re-suaveplan attempt) or differs from what the chain so far implies. Reports with `code: NETWORK_AUDIT_CODES.CHAIN_TAMPERED`.
|
||||
- Detects **tampered prev-hash** — when an entry's `prevHash` doesn't match the actual SHA-256 of the previous entry. Reports with `code: NETWORK_AUDIT_CODES.CHAIN_TAMPERED`.
|
||||
- Detects **tampered signatures** — when the injected verifier returns false, with the exact `seq` and `hostId` in the report. Reports with `code: NETWORK_AUDIT_CODES.SIGNATURE_INVALID`.
|
||||
- Each `CorruptionReport` carries both a granular `kind` (string discriminant for debugging) and a stable `code` (registry-backed identifier from `NETWORK_AUDIT_CODES`) so downstream consumers (logger, alerting, server reconciler) can match on the registry code without coupling to the human-readable string.
|
||||
- Optional `verifySignature` (skip if signing keys aren't available — e.g. integrity check on a single-host log) keeps the checker reusable in test contexts.
|
||||
- Optional `bus` emits `audit:corruption-detected` per finding for fan-out to alerting/observability.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
SignedLog,
|
||||
IntegrityChecker,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const checker = new IntegrityChecker();
|
||||
const report = await checker.check(log.entries());
|
||||
if (!report.ok) console.warn(report.issues);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto-curve";
|
||||
import { IntegrityChecker } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const hostKeys = new Map<string, Uint8Array>([
|
||||
["host-1", host1Public],
|
||||
["host-2", host2Public],
|
||||
]);
|
||||
|
||||
const checker = new IntegrityChecker({
|
||||
verifySignature: (bytes, signature, hostId) => {
|
||||
const pub = hostKeys.get(hostId);
|
||||
if (pub === undefined) return false;
|
||||
return ed25519.verify(signature, bytes, pub);
|
||||
},
|
||||
bus: { emit: (topic, payload) => observability.publish(topic, payload) },
|
||||
});
|
||||
|
||||
const report = await checker.check(retriever.getLogs(query).entries);
|
||||
metrics.record("audit.integrity.issues", report.issues.length);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `IntegrityChecker(config?)` — `config: { verifySignature?: SigVerifyFn; bus?: IntegrityBus }`.
|
||||
- `check(entries)` — returns `Promise<IntegrityReport>`. Walks entries in iteration order; the first entry's expected `prevHash` is `""`.
|
||||
- `CorruptionKind` — `"gap" | "fork" | "tampered-prev-hash" | "tampered-signature"`.
|
||||
- `CorruptionReport` — `{ kind; code: NetworkAuditErrorCode; seq; hostId; detail? }`.
|
||||
- `CORRUPTION_KIND_TO_CODE` — frozen `Record<CorruptionKind, NetworkAuditErrorCode>` mapping each kind to its registry code; the integrity checker uses this so the kind→code mapping has a single source of truth.
|
||||
- `IntegrityReport` — `{ ok; inspected; issues }`.
|
||||
- `SigVerifyFn` — `(bytes, signature, hostId) => boolean | Promise<boolean>`.
|
||||
- `IntegrityBus` — `emit("audit:corruption-detected", report)` shape.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The checker continues past a discovered issue rather than throwing on first error — a single retrieval pass surfaces every problem at once, so the operator does not need to re-run after each fix.
|
||||
- "Fork at first entry" (a re-suaveplan when the chain already has history) is a special case of `prevHash` mismatch and is reported as `kind: "fork"` to distinguish it from a mid-chain tamper. All other prevHash mismatches are `kind: "tampered-prev-hash"`.
|
||||
- Signature verification is async-aware; even though `ed25519.verify` is synchronous, the verifier signature accepts `Promise<boolean>` so a hardware-backed verifier can plug in without forcing the caller to wrap.
|
||||
@@ -0,0 +1,86 @@
|
||||
# SignedLog
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Append-only, hash-chained, cryptographically signed audit log per `(meshId, hostId)` pair. Every entry's `prevHash` is the hex-encoded SHA-256 of the previous entry's canonical bytes, and every entry's `hostSignature` covers those same canonical bytes — so flipping any field (including `seq`, `timestamp`, or `prevHash`) breaks both the chain and the signature.
|
||||
|
||||
The class deliberately decouples the signing primitive: callers inject a `SignFn` (typically `(bytes) => ed25519.sign(bytes, hostPrivateKey)`) so the log itself depends only on `@suaveplan/crypto-hash`. This keeps audit-mesh independent of any specific signature engine and makes property-based tests trivial — substitute a deterministic test signer and the chain becomes byte-stable.
|
||||
|
||||
## Features
|
||||
|
||||
- Monotonic sequence (`seq` starts at 0 and increments per append, never resets except via a fresh `SignedLog` instance).
|
||||
- Hash-chained — first entry has `prevHash = ""`; subsequent entries have `prevHash = hex(sha256(canonicalBytes(prev)))`.
|
||||
- Canonical serialization uses sorted-key JSON over the entry-without-signature; non-finite numbers serialize as `null` to keep bytes deterministic.
|
||||
- Injectable `SignFn` may return either a `Uint8Array` synchronously or a `Promise<Uint8Array>` — `append` awaits the result, so async signers (e.g. hardware-backed Ed25519) work transparently.
|
||||
- Injectable clock (defaults to `Date.now`) keeps tests deterministic.
|
||||
- `getBySeq` raises `NET_AUDIT.INVALID_SEQ` on out-of-range lookups.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto-curve";
|
||||
import { SignedLog } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const privateKey = ed25519.randomPrivateKey();
|
||||
const log = new SignedLog({
|
||||
meshId: "mesh-a",
|
||||
hostId: "host-1",
|
||||
sign: (bytes) => ed25519.sign(bytes, privateKey),
|
||||
});
|
||||
|
||||
const entry = await log.append({
|
||||
eventType: "host-elected",
|
||||
sessionId: null,
|
||||
severity: "critical",
|
||||
payload: { electedHost: "host-1" },
|
||||
});
|
||||
|
||||
console.log(entry.seq, entry.prevHash, entry.hostSignature.length);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
SignedLog,
|
||||
entryDigestHex,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const log = new SignedLog({
|
||||
meshId: "mesh-a",
|
||||
hostId: "host-1",
|
||||
clock: () => 1_700_000_000_000,
|
||||
sign: async (bytes) => await hardwareSigner.sign(bytes),
|
||||
});
|
||||
|
||||
await log.append({
|
||||
eventType: "session-created",
|
||||
sessionId: "sess-1",
|
||||
severity: "critical",
|
||||
payload: { peers: ["peer-a", "peer-b"] },
|
||||
});
|
||||
|
||||
const digest = entryDigestHex(log.entries()[0]!);
|
||||
console.log(`tip digest = ${digest}; chain size = ${log.size()}`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `SignedLog(config)` — constructs an empty log. `config: { meshId; hostId; sign: SignFn; clock?: () => number }`.
|
||||
- `append(input)` — appends one entry. `input: { eventType; sessionId; severity; payload }`. Returns the persisted `LogEntry` after signing.
|
||||
- `entries()` — returns a defensive shallow copy of all entries in append order.
|
||||
- `size()` — number of entries appended.
|
||||
- `tipHash()` — hex SHA-256 of the most recent entry, or `""` for an empty log.
|
||||
- `getBySeq(seq)` — returns the entry at sequence `seq` or throws `NET_AUDIT.INVALID_SEQ`.
|
||||
- `entryHashBytes(entry)` — exported helper that returns the canonical bytes a verifier should hash and signature-check.
|
||||
- `entryDigestHex(entry)` — exported helper that returns the hex SHA-256 of those bytes (handy for debug/inspection).
|
||||
- `bytesToHex(bytes)` — exported lowercase-hex utility used by both this module and `IntegrityChecker`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `prevHash` is a hex string rather than `Uint8Array` so chain entries are inspectable in JSON dumps and equality comparisons fall back on string compare without a length-prefix dance.
|
||||
- `canonicalize` walks the entry-without-signature in sorted-key order; the sort is stable across all V8/JSC versions so two correct implementations produce byte-identical output.
|
||||
- The `Omit<LogEntry, "hostSignature">` shape is the implicit "thing under signature" — any future field added to `LogEntry` is included automatically because canonicalization is structural.
|
||||
@@ -0,0 +1,88 @@
|
||||
# AuditMeshPlugin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Single `NetworkPlugin` facade that wires `SignedLog`, `LogReplicator`, `LogRetriever`, `IntegrityChecker`, `ConflictResolver`, and `LogCompaction` into a cohesive subsystem and registers them with a `NetworkNode` via the standard plugin lifecycle (`install` / `start` / `stop` / `dispose`). Consumers register exactly one of these per host; everything else is reachable via the public properties on the plugin instance.
|
||||
|
||||
The plugin keeps its own state minimal — installation just remembers the node, and the start/stop pair is a flag — because the subsystems it composes are independently useful and testable. The plugin is the wire-up; the modules are the work.
|
||||
|
||||
## Features
|
||||
|
||||
- One-shot construction wires every subsystem from a single config object.
|
||||
- Public read-only access to `log`, `replicator`, `retriever`, `integrity`, `conflicts`, and `compaction` so consumers can drop directly into any of them.
|
||||
- Lifecycle hooks are idempotent and side-effect-light; `start` only flips the started flag (replication is per-append, not background).
|
||||
- `dispose` clears the node reference so the plugin can be re-installed later if the host's `NetworkNode` is rebuilt.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto-curve";
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
import { AuditMeshPlugin } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const privateKey = ed25519.randomPrivateKey();
|
||||
const node = new NetworkNode({ id: "host-1" });
|
||||
const audit = new AuditMeshPlugin({
|
||||
meshId: "mesh-a",
|
||||
hostId: "host-1",
|
||||
sign: (bytes) => ed25519.sign(bytes, privateKey),
|
||||
replication: {
|
||||
replicationFactor: 2,
|
||||
localTier: 0,
|
||||
localMeshId: "mesh-a",
|
||||
localHostId: "host-1",
|
||||
},
|
||||
send: async (target, entry) => network.send(target.hostId, entry),
|
||||
});
|
||||
|
||||
await node.use(audit).start();
|
||||
await audit.log.append({
|
||||
eventType: "host-elected",
|
||||
sessionId: null,
|
||||
severity: "critical",
|
||||
payload: { electedHost: "host-1" },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto-curve";
|
||||
import { AuditMeshPlugin } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const audit = new AuditMeshPlugin({
|
||||
meshId: "mesh-a",
|
||||
hostId: "host-1",
|
||||
sign: hardwareSigner.sign,
|
||||
replication: { replicationFactor: 3, localTier: 1, localMeshId: "mesh-a", localHostId: "subhost-c" },
|
||||
send: transportManager.relay,
|
||||
candidates: peerRegistry.snapshot(),
|
||||
verifySignature: (bytes, sig, hostId) =>
|
||||
ed25519.verify(sig, bytes, hostKeys.get(hostId)!),
|
||||
bus: { emit: (topic, payload) => observability.publish(topic, payload) },
|
||||
defaultPageSize: 250,
|
||||
maxPageSize: 5000,
|
||||
});
|
||||
|
||||
const entry = await audit.log.append({ ... });
|
||||
await audit.replicator.replicate(entry);
|
||||
const report = await audit.integrity.check(audit.log.entries());
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AuditMeshPlugin(config)` — `config: AuditMeshPluginConfig`. See type definition for the full surface.
|
||||
- `name` — `"audit-mesh"` (constant).
|
||||
- Subsystem properties: `log`, `replicator`, `retriever`, `integrity`, `conflicts`, `compaction`.
|
||||
- `install(node)` / `start()` / `stop()` / `dispose()` — `NetworkPlugin` lifecycle.
|
||||
- `host()` — returns the installed `NetworkNode` or `null`.
|
||||
- `isStarted()` — boolean lifecycle inspector.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plugin uses spread-with-conditional-key tricks (`...(config.x === undefined ? {} : { x: config.x })`) to honour `exactOptionalPropertyTypes` — passing `undefined` to a sub-component that expects an _absent_ key would type-error at construction time, so we elide the key entirely instead.
|
||||
- The `bus` property is reused as both `ReplicatorBus` and `IntegrityBus` (their `emit` shapes are compatible in practice); if a consumer ever needs to route the two streams separately they can construct the subsystems individually rather than going through the plugin facade.
|
||||
- Replication is intentionally not awaited inside `log.append`; the caller decides whether to `await replicator.replicate(entry)` immediately (sync semantics) or fire-and-forget (async semantics is also achieved by passing a `severity: "normal"` entry).
|
||||
@@ -0,0 +1,80 @@
|
||||
# LogReplicator
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Ships every signed log entry to `R` sibling hosts so a single host failure cannot erase audit history. The replicator is tier-aware: a Tier 0 host preferentially replicates to other Tier 0s (cross-mesh durability) and falls back to a Tier 1 child within its own mesh (local redundancy); a Tier 1 host replicates to its Tier 0 parent and a sibling Tier 1. The mode is determined by the entry's severity — critical events (host election, session creation) ship synchronously, normal events (peer churn, state updates) ship asynchronously.
|
||||
|
||||
The replicator is intentionally side-effect-light: routing decisions and retry policy are pure; the only outward I/O is the injected `SendToReplicaFn`. This makes it composable with any transport (`@suaveplan/network-plugin-transport-manager`, a real WebRTC mesh, or a memory-only test harness).
|
||||
|
||||
## Features
|
||||
|
||||
- R-copy replication; default `replicationFactor` is 2 (so each entry lives on 3 hosts: originator + 2 replicas).
|
||||
- Tier-aware target scoring (Tier 0 → other Tier 0s preferred; Tier 1 → Tier 0 parent preferred); deterministic alphabetical tiebreak on score collisions so replication is reproducible across runs.
|
||||
- Severity-mode mapping: `"critical"` → synchronous (all R awaited; throws `NET_AUDIT.REPLICATION_FAILED` if every replica fails), `"normal"` → fire-and-forget with bus failure events.
|
||||
- Per-target failures emit `audit:replication-failed` on the optional bus rather than throwing — keeps the originating host's append loop fast.
|
||||
- `registerCandidate` / `unregisterCandidate` so topology changes (peer joins / disconnects) flow into the replicator without rebuilding it.
|
||||
- Self-exclusion: a candidate matching `localHostId` is silently dropped at registration time and at selection time.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { LogReplicator } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const replicator = new LogReplicator({
|
||||
config: {
|
||||
replicationFactor: 2,
|
||||
localTier: 0,
|
||||
localMeshId: "mesh-a",
|
||||
localHostId: "host-1",
|
||||
},
|
||||
candidates: [
|
||||
{ hostId: "host-2", tier: 0, meshId: "mesh-b" },
|
||||
{ hostId: "host-3", tier: 1, meshId: "mesh-a" },
|
||||
],
|
||||
send: async (target, entry) => transport.send(target.hostId, entry),
|
||||
});
|
||||
|
||||
await replicator.replicate(entry);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { LogReplicator } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const replicator = new LogReplicator({
|
||||
config: { replicationFactor: 3, localTier: 1, localMeshId: "mesh-a", localHostId: "subhost-c" },
|
||||
bus: { emit: (topic, payload) => observability.publish(topic, payload) },
|
||||
send: async (target, entry) => {
|
||||
try {
|
||||
await network.send({ to: target.hostId, payload: entry });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
replicator.registerCandidate({ hostId: "host-1", tier: 0, meshId: "mesh-a" });
|
||||
const outcome = await replicator.replicate(criticalEntry);
|
||||
console.log(outcome.mode, outcome.succeeded.length, outcome.failed.length);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LogReplicator(input)` — `input: { config: ReplicationConfig; send: SendToReplicaFn; bus?: ReplicatorBus; candidates?: readonly ReplicationTarget[] }`. Throws `NET_AUDIT.REPLICATION_FAILED` if `replicationFactor < 1`.
|
||||
- `replicate(entry)` — chooses targets, dispatches, returns `ReplicationOutcome`. Synchronous mode throws `NET_AUDIT.REPLICATION_FAILED` if every replica fails. Asynchronous mode returns an outcome with empty `succeeded`/`failed` (failures arrive on the bus).
|
||||
- `selectTargets()` — pure target-ranking helper; returns up to `replicationFactor` candidates ordered by tier-aware score then by `hostId`.
|
||||
- `registerCandidate(target)` / `unregisterCandidate(hostId)` / `listCandidates()` — candidate-set management.
|
||||
- `ReplicationOutcome` — `{ mode; attempted; succeeded; failed }`.
|
||||
- `ReplicatorBus` — minimal `emit(topic, payload)` shape so the package does not couple to `@suaveplan/events`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The scoring function gives `100` to the most-preferred class and `50` to the fallback class; any other candidate scores `0` and is dropped only if there are enough higher-scored candidates. This keeps the mesh "best-effort R" rather than "exact R or fail" — durability degrades gracefully.
|
||||
- Sync mode awaits all replicas via `Promise.all` so the originator can record both the success count and the failure list in a single round-trip.
|
||||
- Async mode dispatches via `void this.sendOne(...).then(...)`; thrown errors inside the send function are caught and surfaced as failures rather than uncaught promise rejections.
|
||||
- A bus is optional. If one is wired, the replicator emits `audit:replication-failed` for every per-target failure (even in sync mode where the throw also fires), so the observability adapter can count failures without needing access to the throw site.
|
||||
@@ -0,0 +1,75 @@
|
||||
# LogRetriever
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Answers range queries against a `SignedLog`, the natural query shape for the server-bridge reconciliation flow: "give me every entry in mesh X with sequence between A and B". Pagination is the surface that keeps reconciliation tractable when a host has accumulated tens of thousands of entries during a long server outage — the server iterates page-by-page rather than buffering the entire chain.
|
||||
|
||||
The retriever is deliberately a thin read facade. It has no signature verification logic (that lives in `IntegrityChecker`) and no authorization logic (that lives upstream in `network-plugin-auth`). Its only job is to take a well-formed range request and return well-paginated results.
|
||||
|
||||
## Features
|
||||
|
||||
- `getLogs(query)` returns one page of entries plus pagination metadata (`page`, `pageSize`, `totalPages`).
|
||||
- `iterate(query)` is a generator that yields every entry across all pages, so callers can stream a long chain without juggling pagination state.
|
||||
- Per-call `pageSize` is clamped to `maxPageSize` (default 1000) so a malformed request cannot exhaust memory.
|
||||
- Mesh-mismatch (`requested.meshId !== log.meshId`) raises `NET_AUDIT.RANGE_INVALID` rather than silently returning an empty result, so misrouted queries fail loudly.
|
||||
- Negative `fromSeq`, `toSeq < fromSeq`, negative page index, and zero-or-negative `pageSize` all raise `NET_AUDIT.RANGE_INVALID` with explanatory `error.context`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
SignedLog,
|
||||
LogRetriever,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const log = new SignedLog({
|
||||
meshId: "mesh-a",
|
||||
hostId: "host-1",
|
||||
sign: signFn,
|
||||
});
|
||||
const retriever = new LogRetriever({ log });
|
||||
|
||||
const result = retriever.getLogs({
|
||||
meshId: "mesh-a",
|
||||
fromSeq: 0,
|
||||
toSeq: 99,
|
||||
});
|
||||
console.log(result.page, result.totalPages, result.entries.length);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { LogRetriever } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const retriever = new LogRetriever({
|
||||
log,
|
||||
defaultPageSize: 250,
|
||||
maxPageSize: 5000,
|
||||
});
|
||||
|
||||
for (const entry of retriever.iterate({
|
||||
meshId: "mesh-a",
|
||||
fromSeq: 1000,
|
||||
toSeq: 9999,
|
||||
pageSize: 500,
|
||||
})) {
|
||||
await server.ingest(entry);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LogRetriever(config)` — `config: { log: SignedLog; defaultPageSize?: number; maxPageSize?: number }`. Defaults: 100 / 1000.
|
||||
- `getLogs(query)` — `query: { meshId; fromSeq; toSeq; pageSize?; page? }`. Returns `RangeResult`.
|
||||
- `iterate(query)` — `Generator<LogEntry>`; auto-advances pages until the range is exhausted.
|
||||
- `RangeResult` — `{ meshId; fromSeq; toSeq; page; pageSize; totalPages; entries }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The mesh-mismatch check is the first guard so observability for "wrong mesh queried" is unambiguous in error logs.
|
||||
- `pageSize` is normalised at the call site (not at construction time) so a single retriever instance can serve heterogeneous query shapes.
|
||||
- The retriever filters by sequence range rather than slicing by index because `SignedLog.entries()` is `seq`-aligned (entry index === seq) but we treat that as an implementation detail rather than a contract — should `SignedLog` ever support truncation/checkpoint reset, the retriever continues to work correctly without changes.
|
||||
@@ -0,0 +1,48 @@
|
||||
# conflict.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Captures the inputs and outputs of `ConflictResolver`. When the central server reconciles state after an outage, it pulls the per-host audit chains from every Tier 0 host. Hosts that were partitioned during the outage may have produced divergent suffixes, so the server needs a deterministic tiebreaking rule to pick the canonical chain. These types describe that rule's surface area.
|
||||
|
||||
The two-step resolution (longest chain wins; if equal length, the chain whose last entry hash sorts lexicographically smallest wins) is intentionally simple and side-effect-free so the same input is guaranteed to produce the same output across servers — important when the reconciler is itself replicated.
|
||||
|
||||
## Features
|
||||
|
||||
- `DivergentChain` carries one host's contribution to the conflict: the chain of entries it produced and a `lastHash` (the SHA-256 hex of the final entry) used as the tiebreak key.
|
||||
- `ConflictResolution` carries the winning chain plus a `reason` discriminant (`"longest-chain"` vs `"lex-tiebreak"`) for audit log output, and a `losers` array so callers can issue rollback instructions to the hosts whose chain was discarded.
|
||||
- All fields are `readonly` and the chain arrays are `readonly LogEntry[]` to keep resolver semantics referentially transparent.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
ConflictResolution,
|
||||
DivergentChain,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const chains: DivergentChain[] = [
|
||||
{ hostId: "host-1", entries: [...], lastHash: "abc..." },
|
||||
{ hostId: "host-2", entries: [...], lastHash: "def..." },
|
||||
];
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ConflictResolver } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const resolver = new ConflictResolver();
|
||||
const result: ConflictResolution = resolver.resolve(chains);
|
||||
console.log(result.winningHostId, result.reason, result.losers);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ConflictResolutionReason` — `"longest-chain" | "lex-tiebreak"`.
|
||||
- `DivergentChain` — `{ hostId; entries: readonly LogEntry[]; lastHash: string }`.
|
||||
- `ConflictResolution` — `{ winningHostId; winningChain: readonly LogEntry[]; reason; losers: readonly string[] }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `lastHash` is provided by the caller (typically computed by the integrity checker as it walks the chain) so the resolver does not depend on hashing utilities and stays trivially testable.
|
||||
- An empty chain set throws `NET_AUDIT.CONFLICT_UNRESOLVED` rather than returning a sentinel — there is no meaningful "no winner" answer.
|
||||
@@ -0,0 +1,66 @@
|
||||
# log.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the wire-stable shape of every record that flows through the audit-mesh subsystem: append-only `LogEntry` records, the seven `LogEventType` discriminants the mesh emits, the two-level `LogSeverity` axis that drives synchronous-vs-asynchronous replication, and the periodic `LogCheckpoint` that compaction emits to bound replay volume after a long server outage.
|
||||
|
||||
These types are the contract every other module in `@suaveplan/network-plugin-audit-mesh` agrees on, so they live in a shared `types/` directory rather than next to any one consumer.
|
||||
|
||||
## Features
|
||||
|
||||
- Discriminated `LogEventType` covering peer lifecycle, host election, session lifecycle, and state changes
|
||||
- Two-tier `LogSeverity` (`"critical"` / `"normal"`) drives replicator mode selection (sync for critical, async for normal)
|
||||
- `LogEntry.prevHash` is a hex string (not bytes) to keep equality checks straightforward and make the chain inspectable in logs
|
||||
- `LogEntry.hostSignature` is the raw `Uint8Array` Ed25519 signature over `entryHashBytes(entry)` so consumers do not pay for base64 round-trips
|
||||
- `LogCheckpoint` carries both `stateHash` (snapshot fingerprint) and `entryHashAtSeq` (chain anchor) so the server can validate the checkpoint without replaying the chain
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
LogEntry,
|
||||
LogEventType,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const entry: LogEntry = {
|
||||
seq: 1,
|
||||
timestamp: 1_700_000_000_000,
|
||||
meshId: "mesh-a",
|
||||
sessionId: null,
|
||||
eventType: "host-elected" satisfies LogEventType,
|
||||
severity: "critical",
|
||||
payload: { electedHost: "host-1", term: 7 },
|
||||
prevHash: "",
|
||||
hostId: "host-1",
|
||||
hostSignature: new Uint8Array(64),
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { LogCheckpoint } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const checkpoint: LogCheckpoint = {
|
||||
seq: 1024,
|
||||
timestamp: Date.now(),
|
||||
meshId: "mesh-a",
|
||||
hostId: "host-1",
|
||||
stateHash: "abcd1234...",
|
||||
entryHashAtSeq: "ef567890...",
|
||||
signature: new Uint8Array(64),
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LogEventType` — union of `"peer-joined" | "peer-left" | "host-elected" | "session-created" | "session-closed" | "state-update" | "policy-change"`.
|
||||
- `LogSeverity` — `"critical" | "normal"`.
|
||||
- `LogEntry` — append-only record with `seq`, `timestamp`, `meshId`, `sessionId`, `eventType`, `severity`, `payload`, `prevHash`, `hostId`, `hostSignature`.
|
||||
- `LogCheckpoint` — signed snapshot `{ seq, timestamp, meshId, hostId, stateHash, entryHashAtSeq, signature }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- All fields are `readonly`. Mutation should never happen post-append; downstream replicators copy entries by reference.
|
||||
- `prevHash` of the suaveplan entry is the empty string (`""`). Hash chain validators must allow this exactly once per `(meshId, hostId)` chain.
|
||||
- `hostSignature` covers the SHA-256 of every other field on the entry, so flipping any bit (including `seq` or `prevHash`) breaks signature verification — the integrity checker relies on this.
|
||||
@@ -0,0 +1,65 @@
|
||||
# replication.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Describes the data the `LogReplicator` needs to pick replication targets and to route an entry through the mesh under the right delivery guarantees. The two-mode `ReplicationMode` axis is what bridges audit-mesh to the broader topology contract: critical events (host election, session creation) ship synchronously so the originating host has confirmed durability before acknowledging the action upstream; normal events (peer join/leave, state updates) ship asynchronously to keep the mesh fast.
|
||||
|
||||
These types are a thin shape contract — no logic lives here. The replicator implementation reads `ReplicationConfig.localTier` to compute `tier-aware` routing rules (Tier 0 to other Tier 0s + a Tier 1 child; Tier 1 to its Tier 0 parent + a sibling Tier 1).
|
||||
|
||||
## Features
|
||||
|
||||
- `ReplicationMode` discriminates the two delivery guarantees (`"sync"` blocks the caller until R replicas ack; `"async"` fires-and-forgets and surfaces failures via OTel + bus events).
|
||||
- `ReplicationTarget` carries the minimum information the replicator needs to make a routing decision: peer identity + tier (for cross-tier rules) + mesh.
|
||||
- `ReplicationConfig` is read once at plugin construction time and supplies the local host's identity and the replication factor.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
ReplicationConfig,
|
||||
ReplicationTarget,
|
||||
} from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const config: ReplicationConfig = {
|
||||
replicationFactor: 2,
|
||||
localTier: 0,
|
||||
localMeshId: "mesh-a",
|
||||
localHostId: "host-1",
|
||||
};
|
||||
|
||||
const candidate: ReplicationTarget = {
|
||||
hostId: "host-2",
|
||||
tier: 0,
|
||||
meshId: "mesh-b",
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { LogReplicator } from "@suaveplan/network-plugin-audit-mesh";
|
||||
|
||||
const replicator = new LogReplicator({
|
||||
config: {
|
||||
replicationFactor: 3,
|
||||
localTier: 1,
|
||||
localMeshId: "mesh-a",
|
||||
localHostId: "subhost-c",
|
||||
},
|
||||
sendFn: async (target, entry) => {
|
||||
await network.send({ to: target.hostId, payload: entry });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ReplicationMode` — `"sync" | "async"`.
|
||||
- `ReplicationTarget` — `{ hostId: string; tier: number; meshId: string }`.
|
||||
- `ReplicationConfig` — `{ replicationFactor: number; localTier: number; localMeshId: string; localHostId: string }`. The `replicationFactor` is the `R` in "R-copy"; the entry is stored on `R + 1` hosts (originator + R replicas).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Replication factor is intentionally not bounded at the type level. Callers should validate at construction time (`>= 1`) — the `LogReplicator` raises `NET_AUDIT.REPLICATION_FAILED` if no candidates remain after filtering.
|
||||
- `tier` is a number rather than an enum so future N-tier topologies (Tier 2+) can extend without a breaking change.
|
||||
Reference in New Issue
Block a user