@azmxailabs/agent-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AZMX AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # @azmxailabs/agent-sdk
2
+
3
+ > Build approval-gated AI agents with the same primitives that power **[AZMX AI](https://azmx.ai)** — BYOK provider router, approval gate, deny-list, hash-chained audit log. Secure by default. BYOK direct (no proxy). No telemetry.
4
+
5
+ ```bash
6
+ npm install @azmxailabs/agent-sdk
7
+ ```
8
+
9
+ ## Why this exists
10
+
11
+ Most "AI agent" frameworks bolt safety on as middleware. AZMX builds it in. This SDK ships the four primitives that make that possible as standalone, dependency-free TypeScript so you can use them in your own agent — whether that's a CI script, a CLI, a desktop app, or a server.
12
+
13
+ - **Approval gate** — every side-effecting action passes through a configurable policy chain before it runs.
14
+ - **Deny-list** — refuses `.env`, `.ssh`, credentials, and any other path you care about by glob.
15
+ - **Hash-chained audit log** — every entry includes the previous entry's hash; tampering breaks the chain detectably.
16
+ - **Provider router** — one ergonomic interface across Anthropic, Ollama, and any other backend you plug in.
17
+
18
+ Zero runtime dependencies. Node ≥ 18. ESM.
19
+
20
+ ## Quick start
21
+
22
+ ```ts
23
+ import {
24
+ ApprovalGate,
25
+ standardPolicy,
26
+ destructiveShellDenyPolicy,
27
+ DenyList,
28
+ denyListPolicy,
29
+ HashChainedAuditLog,
30
+ ProviderRouter,
31
+ AnthropicProvider,
32
+ OllamaProvider,
33
+ } from "@azmxailabs/agent-sdk";
34
+
35
+ // 1. Audit log (hash-chained, tamper-evident)
36
+ const log = new HashChainedAuditLog({ path: "./agent-audit.jsonl" });
37
+
38
+ // 2. Deny-list (refuses sensitive paths)
39
+ const deny = new DenyList(); // ships with sensible defaults
40
+
41
+ // 3. Approval gate (the heart of the safety model)
42
+ const gate = new ApprovalGate({
43
+ policies: [
44
+ denyListPolicy(deny),
45
+ destructiveShellDenyPolicy(),
46
+ standardPolicy(),
47
+ ],
48
+ onPrompt: async ({ action, reasons }) => {
49
+ // Your UI shows the action; user picks. Reasons = the policies that asked.
50
+ console.log(`\n[approval needed] ${action.kind}: ${action.summary}`);
51
+ console.log(`reasons: ${reasons.join(", ")}`);
52
+ // Real code: prompt the user. Here we auto-approve for the example.
53
+ return "approve";
54
+ },
55
+ onDecision: (event) => log.append({ type: "approval", ...event }),
56
+ });
57
+
58
+ // 4. Provider router (BYOK — direct, no proxy)
59
+ const router = new ProviderRouter()
60
+ .register("claude", new AnthropicProvider({
61
+ apiKey: process.env.ANTHROPIC_API_KEY!,
62
+ model: "claude-opus-4-7",
63
+ }))
64
+ .register("local", new OllamaProvider({ model: "qwen2.5-coder:14b" }));
65
+
66
+ // Use it
67
+ const decision = await gate.check({
68
+ kind: "shell",
69
+ summary: "ls -la /tmp",
70
+ target: "/tmp",
71
+ });
72
+
73
+ if (decision === "approved") {
74
+ const result = await router.complete({
75
+ model: "claude",
76
+ messages: [{ role: "user", content: "Summarize what `ls -la` shows." }],
77
+ });
78
+ console.log(result.text);
79
+ }
80
+
81
+ // Verify the audit log later
82
+ const verification = await log.verify();
83
+ if (!verification.ok) {
84
+ console.error("Audit log tampered at seq", verification.brokenAtSeq);
85
+ }
86
+ ```
87
+
88
+ ## API
89
+
90
+ ### `ApprovalGate`
91
+
92
+ Every side-effecting action passes through here. Policies vote (`auto` / `ask` / `deny`); most-restrictive wins.
93
+
94
+ ```ts
95
+ const gate = new ApprovalGate({
96
+ policies: [denyListPolicy(), destructiveShellDenyPolicy(), standardPolicy()],
97
+ onPrompt: async ({ action, reasons }) => "approve" | "approve-and-trust" | "reject",
98
+ onDecision: (event) => auditLog.append(event), // optional sink
99
+ });
100
+
101
+ const decision = await gate.check({
102
+ kind: "shell" | "file:write" | "file:read" | "file:delete" | "network" | "git" | "process:spawn" | "tool" | (string & {}),
103
+ summary: "human-readable one-liner",
104
+ target: "/path or URL",
105
+ payload: anyObject, // optional structured verb
106
+ });
107
+ // → "approved" | "denied"
108
+ ```
109
+
110
+ **Built-in policies** (importable from `@azmxailabs/agent-sdk/approval`):
111
+
112
+ | Policy | Behavior |
113
+ |---|---|
114
+ | `standardPolicy()` | The AZMX default. Reads auto; writes / deletes / shell / spawns ask; destructive shell verbs always ask. |
115
+ | `paranoidPolicy()` | Asks for everything — even reads. For untrusted code, classified work, compliance demos. |
116
+ | `permissivePolicy()` | Auto-approves everything. For trusted CI agents with their own external guardrails. |
117
+ | `destructiveShellDenyPolicy(extra?)` | Hard-blocks `rm`, `dd`, `shutdown`, etc. — no prompt. Adds your verbs to the list. |
118
+
119
+ ### `DenyList` + `denyListPolicy`
120
+
121
+ ```ts
122
+ import { DenyList, DEFAULT_DENY_LIST, denyListPolicy } from "@azmxailabs/agent-sdk/security";
123
+
124
+ const deny = new DenyList(); // defaults: .env, .ssh, credentials, .aws/credentials, .kube/config, cookies, keychain files, ...
125
+ deny.add("**/proprietary/**");
126
+ deny.matches("/Users/me/.ssh/id_rsa"); // true
127
+ deny.matching("/Users/me/.ssh/id_rsa"); // ["**/.ssh/**", "**/id_rsa"]
128
+
129
+ // Plug into the gate:
130
+ const policy = denyListPolicy(deny);
131
+ ```
132
+
133
+ Globs: `*` (any chars except `/`), `**` (any chars), `?` (single char), `[abc]` (char class).
134
+
135
+ ### `HashChainedAuditLog`
136
+
137
+ Append-only log where each entry's hash includes the previous entry's hash. Tampering with any past entry breaks the chain.
138
+
139
+ ```ts
140
+ import { HashChainedAuditLog, FileStorage, InMemoryStorage } from "@azmxailabs/agent-sdk/audit";
141
+
142
+ const log = new HashChainedAuditLog({ path: "./audit.jsonl" }); // FileStorage by default
143
+ await log.append({ type: "shell", cmd: "ls -la /tmp" });
144
+ await log.append({ type: "approval", decision: "approve" });
145
+
146
+ const v = await log.verify();
147
+ // v.ok === true → all entries valid
148
+ // v.ok === false → { brokenAtSeq, reason, expected?, found? }
149
+ ```
150
+
151
+ Genesis prevHash = 64 zero bytes. Each entry's hash = `sha256(JSON.stringify({seq, ts, prevHash, data}))`. File mode is 0600.
152
+
153
+ ### `ProviderRouter`
154
+
155
+ ```ts
156
+ import {
157
+ ProviderRouter,
158
+ AnthropicProvider,
159
+ OllamaProvider,
160
+ } from "@azmxailabs/agent-sdk/providers";
161
+
162
+ const router = new ProviderRouter()
163
+ .register("claude-fast", new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY!, model: "claude-haiku-4-5" }))
164
+ .register("claude-smart", new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY!, model: "claude-opus-4-7" }))
165
+ .register("local", new OllamaProvider({ model: "qwen2.5-coder:14b" }));
166
+
167
+ const r = await router.complete({
168
+ model: "claude-fast",
169
+ messages: [
170
+ { role: "system", content: "You are concise." },
171
+ { role: "user", content: "Hello" },
172
+ ],
173
+ temperature: 0.2,
174
+ maxTokens: 200,
175
+ });
176
+ console.log(r.text, r.usage);
177
+
178
+ // Streaming
179
+ for await (const chunk of router.stream({ model: "local", messages: [...] })) {
180
+ process.stdout.write(chunk.delta);
181
+ if (chunk.done) console.log("\n[finishReason]", chunk.finishReason);
182
+ }
183
+ ```
184
+
185
+ **Built-in adapters:**
186
+
187
+ | Adapter | API | BYOK / local |
188
+ |---|---|---|
189
+ | `AnthropicProvider` | POST `/v1/messages` | BYOK direct |
190
+ | `OllamaProvider` | POST `/api/chat` | Local |
191
+
192
+ **Adding your own:** implement the `Provider` interface — `name`, `complete`, `stream`. ~50 lines of code per provider; see `src/providers/ollama.ts` for a minimal reference.
193
+
194
+ ## Design choices
195
+
196
+ - **No runtime dependencies.** Provider adapters call HTTP via `fetch` directly. Adding the official SDK for any provider is a 5-line wrapper at most — but the SDK doesn't need it.
197
+ - **Approval is the first-class concept.** Every other primitive plugs into the gate (deny-list → policy; audit log → onDecision sink). The brand-DNA path is "gate then act."
198
+ - **BYOK direct.** Provider adapters take an `apiKey` and call the provider's own endpoint. AZMX servers never see your prompts or tokens.
199
+ - **Audit log is tamper-evident, not tamper-proof.** Hash chains prove that something changed — they don't prevent change. Pair with append-only storage (immutable S3 bucket, WORM volume) for hard guarantees.
200
+
201
+ ## Roadmap (v0.2+)
202
+
203
+ - Tool / function-calling support across the provider interface
204
+ - `OpenAIProvider` (covers OpenAI + most OpenAI-compatible APIs: Groq, Cerebras, xAI, DeepSeek, Azure, NVIDIA NIM)
205
+ - `GoogleProvider` (Gemini)
206
+ - An `MCPClient` for talking to MCP servers (`@modelcontextprotocol/sdk` wrapper)
207
+ - Streaming tool calls
208
+ - Cost tracking middleware
209
+
210
+ File an issue at https://github.com/AzmxAI/azmx/issues if you want any of these prioritized — or open a PR.
211
+
212
+ ## License
213
+
214
+ MIT — see [LICENSE](./LICENSE).
215
+
216
+ ## About AZMX AI
217
+
218
+ AZMX AI is a native (~7 MB) AI coding agent that runs on your machine, with your keys (BYOK across 11+ providers, or fully offline via Ollama / LM Studio). Every write is gated by per-call approval. No account, no telemetry. Free forever for individuals.
219
+
220
+ - Homepage: https://azmx.ai
221
+ - Docs: https://azmx.ai/docs
222
+ - MCP server: https://www.npmjs.com/package/@azmxailabs/mcp
223
+ - Source: https://github.com/AzmxAI/azmx
@@ -0,0 +1,77 @@
1
+ /**
2
+ * ApprovalGate — every side-effecting action passes through here.
3
+ *
4
+ * The pattern AZMX uses in its desktop app: the agent never executes a
5
+ * shell command, writes a file, or hits the network until the action
6
+ * has been classified by every registered policy and approved by the
7
+ * user (or auto-approved if every policy returns "auto").
8
+ *
9
+ * Policies are pure functions: action → "auto" | "ask" | "deny". The
10
+ * gate aggregates them with the most-restrictive-wins rule: any "deny"
11
+ * blocks; any "ask" prompts the user; "auto" only fires if every
12
+ * policy agreed.
13
+ */
14
+ export type ActionKind = "shell" | "file:write" | "file:read" | "file:delete" | "network" | "git" | "process:spawn" | "tool" | (string & {});
15
+ export interface AgentAction {
16
+ /** Coarse category — used by policies for fast routing. */
17
+ kind: ActionKind;
18
+ /** One-line human summary shown to the user in the approval UI. */
19
+ summary: string;
20
+ /** Optional path / URL / target the action touches. */
21
+ target?: string;
22
+ /** Optional structured payload (the verb the agent staged). */
23
+ payload?: unknown;
24
+ /** Arbitrary metadata; passed through to policies + onPrompt. */
25
+ meta?: Record<string, unknown>;
26
+ }
27
+ export type PolicyDecision = "auto" | "ask" | "deny";
28
+ export interface Policy {
29
+ /** Short stable identifier — appears in audit log + denial reason. */
30
+ name: string;
31
+ /** Pure classifier. Sync or async. Throw nothing. */
32
+ classify(action: AgentAction): PolicyDecision | Promise<PolicyDecision>;
33
+ }
34
+ export type UserDecision = "approve" | "approve-and-trust" | "reject";
35
+ export interface PromptContext {
36
+ action: AgentAction;
37
+ /** Names of policies that returned "ask" — the reason we're prompting. */
38
+ reasons: string[];
39
+ }
40
+ export type PromptHandler = (ctx: PromptContext) => UserDecision | Promise<UserDecision>;
41
+ export interface GateOptions {
42
+ policies?: Policy[];
43
+ /** Required when any policy returns "ask". */
44
+ onPrompt?: PromptHandler;
45
+ /** Optional sink — called on every decision (for audit log wiring). */
46
+ onDecision?: (event: DecisionEvent) => void | Promise<void>;
47
+ }
48
+ export interface DecisionEvent {
49
+ action: AgentAction;
50
+ classifications: Array<{
51
+ policy: string;
52
+ decision: PolicyDecision;
53
+ }>;
54
+ finalDecision: "approved" | "denied";
55
+ reason?: string;
56
+ timestamp: string;
57
+ }
58
+ export declare class ApprovalGate {
59
+ private policies;
60
+ private onPrompt?;
61
+ private onDecision?;
62
+ private trustedActions;
63
+ constructor(opts?: GateOptions);
64
+ /** Add a policy at runtime. Order doesn't matter — all policies vote. */
65
+ use(policy: Policy): void;
66
+ /**
67
+ * Classify + (if needed) prompt + return the final decision.
68
+ *
69
+ * Most-restrictive-wins:
70
+ * any "deny" → "denied" without prompting.
71
+ * any "ask" → call onPrompt; user's reject → "denied".
72
+ * all "auto" → "approved" without prompting.
73
+ */
74
+ check(action: AgentAction): Promise<"approved" | "denied">;
75
+ private finalize;
76
+ private trustKeyFor;
77
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ApprovalGate — every side-effecting action passes through here.
3
+ *
4
+ * The pattern AZMX uses in its desktop app: the agent never executes a
5
+ * shell command, writes a file, or hits the network until the action
6
+ * has been classified by every registered policy and approved by the
7
+ * user (or auto-approved if every policy returns "auto").
8
+ *
9
+ * Policies are pure functions: action → "auto" | "ask" | "deny". The
10
+ * gate aggregates them with the most-restrictive-wins rule: any "deny"
11
+ * blocks; any "ask" prompts the user; "auto" only fires if every
12
+ * policy agreed.
13
+ */
14
+ export class ApprovalGate {
15
+ policies;
16
+ onPrompt;
17
+ onDecision;
18
+ trustedActions = new Set();
19
+ constructor(opts = {}) {
20
+ this.policies = opts.policies ?? [];
21
+ this.onPrompt = opts.onPrompt;
22
+ this.onDecision = opts.onDecision;
23
+ }
24
+ /** Add a policy at runtime. Order doesn't matter — all policies vote. */
25
+ use(policy) {
26
+ this.policies.push(policy);
27
+ }
28
+ /**
29
+ * Classify + (if needed) prompt + return the final decision.
30
+ *
31
+ * Most-restrictive-wins:
32
+ * any "deny" → "denied" without prompting.
33
+ * any "ask" → call onPrompt; user's reject → "denied".
34
+ * all "auto" → "approved" without prompting.
35
+ */
36
+ async check(action) {
37
+ const trustKey = this.trustKeyFor(action);
38
+ if (this.trustedActions.has(trustKey)) {
39
+ return this.finalize(action, [], "approved", "trusted-by-user");
40
+ }
41
+ const classifications = [];
42
+ for (const p of this.policies) {
43
+ const d = await p.classify(action);
44
+ classifications.push({ policy: p.name, decision: d });
45
+ }
46
+ const denials = classifications.filter((c) => c.decision === "deny");
47
+ if (denials.length > 0) {
48
+ const reason = `denied by policy: ${denials.map((d) => d.policy).join(", ")}`;
49
+ return this.finalize(action, classifications, "denied", reason);
50
+ }
51
+ const asks = classifications.filter((c) => c.decision === "ask");
52
+ if (asks.length === 0) {
53
+ return this.finalize(action, classifications, "approved", "auto-approved");
54
+ }
55
+ if (!this.onPrompt) {
56
+ const reason = `no onPrompt handler registered; asks=${asks.map((a) => a.policy).join(", ")}`;
57
+ return this.finalize(action, classifications, "denied", reason);
58
+ }
59
+ const user = await this.onPrompt({
60
+ action,
61
+ reasons: asks.map((a) => a.policy),
62
+ });
63
+ if (user === "reject") {
64
+ return this.finalize(action, classifications, "denied", "rejected by user");
65
+ }
66
+ if (user === "approve-and-trust") {
67
+ this.trustedActions.add(trustKey);
68
+ }
69
+ return this.finalize(action, classifications, "approved", `${user}-by-user`);
70
+ }
71
+ async finalize(action, classifications, finalDecision, reason) {
72
+ const event = {
73
+ action,
74
+ classifications,
75
+ finalDecision,
76
+ reason,
77
+ timestamp: new Date().toISOString(),
78
+ };
79
+ if (this.onDecision) {
80
+ try {
81
+ await this.onDecision(event);
82
+ }
83
+ catch {
84
+ // never let an audit sink failure mask the user's decision
85
+ }
86
+ }
87
+ return finalDecision;
88
+ }
89
+ trustKeyFor(action) {
90
+ return `${action.kind}::${action.target ?? ""}::${action.summary}`;
91
+ }
92
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./gate.js";
2
+ export * from "./policies.js";
@@ -0,0 +1,2 @@
1
+ export * from "./gate.js";
2
+ export * from "./policies.js";
@@ -0,0 +1,24 @@
1
+ import type { Policy } from "./gate.js";
2
+ /**
3
+ * Standard mode — the AZMX default.
4
+ *
5
+ * Reads (file:read, network GET-like) are auto-approved.
6
+ * Writes, deletes, shell, process spawns ask once per category-target.
7
+ * Destructive shell verbs (rm, dd, shutdown, etc.) always ask.
8
+ */
9
+ export declare function standardPolicy(): Policy;
10
+ /**
11
+ * Paranoid mode — ask for everything, even reads.
12
+ * Suitable for: untrusted codebases, classified work, compliance demos.
13
+ */
14
+ export declare function paranoidPolicy(): Policy;
15
+ /**
16
+ * Default-allow — opposite of paranoid. Useful for trusted CI agents
17
+ * with their own external guardrails. Use with deliberation.
18
+ */
19
+ export declare function permissivePolicy(): Policy;
20
+ /**
21
+ * Auto-deny a list of dangerous shell verbs regardless of approval flow.
22
+ * Use as an early stop-gap before standardPolicy / paranoidPolicy vote.
23
+ */
24
+ export declare function destructiveShellDenyPolicy(extraVerbs?: string[]): Policy;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Standard mode — the AZMX default.
3
+ *
4
+ * Reads (file:read, network GET-like) are auto-approved.
5
+ * Writes, deletes, shell, process spawns ask once per category-target.
6
+ * Destructive shell verbs (rm, dd, shutdown, etc.) always ask.
7
+ */
8
+ export function standardPolicy() {
9
+ return {
10
+ name: "standard",
11
+ classify(action) {
12
+ switch (action.kind) {
13
+ case "file:read":
14
+ return "auto";
15
+ case "file:write":
16
+ case "file:delete":
17
+ case "git":
18
+ case "process:spawn":
19
+ return "ask";
20
+ case "shell":
21
+ return isDestructiveShell(action) ? "ask" : "ask";
22
+ case "network":
23
+ return isReadOnlyNetwork(action) ? "auto" : "ask";
24
+ case "tool":
25
+ return "ask";
26
+ default:
27
+ return "ask";
28
+ }
29
+ },
30
+ };
31
+ }
32
+ /**
33
+ * Paranoid mode — ask for everything, even reads.
34
+ * Suitable for: untrusted codebases, classified work, compliance demos.
35
+ */
36
+ export function paranoidPolicy() {
37
+ return {
38
+ name: "paranoid",
39
+ classify() {
40
+ return "ask";
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Default-allow — opposite of paranoid. Useful for trusted CI agents
46
+ * with their own external guardrails. Use with deliberation.
47
+ */
48
+ export function permissivePolicy() {
49
+ return {
50
+ name: "permissive",
51
+ classify() {
52
+ return "auto";
53
+ },
54
+ };
55
+ }
56
+ /**
57
+ * Auto-deny a list of dangerous shell verbs regardless of approval flow.
58
+ * Use as an early stop-gap before standardPolicy / paranoidPolicy vote.
59
+ */
60
+ export function destructiveShellDenyPolicy(extraVerbs = []) {
61
+ const dangerous = new Set([
62
+ "rm",
63
+ "dd",
64
+ "mkfs",
65
+ "shutdown",
66
+ "reboot",
67
+ "halt",
68
+ "poweroff",
69
+ "fdisk",
70
+ "shred",
71
+ "chown",
72
+ "chmod",
73
+ ...extraVerbs.map((v) => v.toLowerCase()),
74
+ ]);
75
+ return {
76
+ name: "destructive-shell-deny",
77
+ classify(action) {
78
+ if (action.kind !== "shell")
79
+ return "auto";
80
+ const cmd = String(action.summary || "").trim().toLowerCase();
81
+ const first = cmd.split(/\s+/)[0] || "";
82
+ const verb = first.split("/").pop() || "";
83
+ return dangerous.has(verb) ? "deny" : "auto";
84
+ },
85
+ };
86
+ }
87
+ function isDestructiveShell(action) {
88
+ const cmd = String(action.summary || "").toLowerCase();
89
+ return /\b(rm|dd|shred|mkfs|shutdown|reboot|halt|poweroff|fdisk|chown|chmod)\b/.test(cmd);
90
+ }
91
+ function isReadOnlyNetwork(action) {
92
+ const meta = action.meta;
93
+ const method = String(meta?.method ?? "GET").toUpperCase();
94
+ return method === "GET" || method === "HEAD";
95
+ }
@@ -0,0 +1 @@
1
+ export * from "./log.js";
@@ -0,0 +1 @@
1
+ export * from "./log.js";
@@ -0,0 +1,70 @@
1
+ /**
2
+ * HashChainedAuditLog — append-only log where each entry's hash includes
3
+ * the previous entry's hash. Tampering with any past entry breaks the
4
+ * chain at that point and every entry after it.
5
+ *
6
+ * Format: JSON Lines (one entry per line). Each line is:
7
+ * { "seq": N, "ts": "...", "prevHash": "...", "hash": "...", "data": {...} }
8
+ *
9
+ * Genesis entry has prevHash = "0".repeat(64).
10
+ *
11
+ * Storage adapters: file (Node only, default) or in-memory (for tests).
12
+ * Verify with verify() — walks from genesis, recomputes every hash.
13
+ */
14
+ export interface AuditEntry {
15
+ seq: number;
16
+ ts: string;
17
+ prevHash: string;
18
+ hash: string;
19
+ data: unknown;
20
+ }
21
+ export interface AuditStorage {
22
+ read(): Promise<string>;
23
+ append(line: string): Promise<void>;
24
+ }
25
+ export declare class InMemoryStorage implements AuditStorage {
26
+ private buf;
27
+ read(): Promise<string>;
28
+ append(line: string): Promise<void>;
29
+ /** Test helper — corrupts an existing entry to verify detection works. */
30
+ tamper(replacer: (existing: string) => string): void;
31
+ }
32
+ export declare class FileStorage implements AuditStorage {
33
+ private path;
34
+ constructor(path: string);
35
+ read(): Promise<string>;
36
+ append(line: string): Promise<void>;
37
+ }
38
+ export interface AuditLogOptions {
39
+ storage?: AuditStorage;
40
+ /** File path; ignored if storage is provided. */
41
+ path?: string;
42
+ }
43
+ export declare class HashChainedAuditLog {
44
+ private storage;
45
+ private nextSeq;
46
+ private lastHash;
47
+ private bootstrapped;
48
+ constructor(opts?: AuditLogOptions);
49
+ /** Append a new entry, returning the resulting entry. Thread-safe per instance. */
50
+ append(data: unknown): Promise<AuditEntry>;
51
+ /**
52
+ * Walk every entry from genesis and recompute hashes. Returns:
53
+ * { ok: true, count } on success
54
+ * { ok: false, brokenAtSeq, expected, found } on first breakage
55
+ */
56
+ verify(): Promise<VerifyResult>;
57
+ /** Read everything as parsed entries. Convenience for callers that need to display the log. */
58
+ entries(): Promise<AuditEntry[]>;
59
+ private bootstrap;
60
+ }
61
+ export type VerifyResult = {
62
+ ok: true;
63
+ count: number;
64
+ } | {
65
+ ok: false;
66
+ brokenAtSeq: number;
67
+ reason: string;
68
+ expected?: string;
69
+ found?: string;
70
+ };