diff --git a/CHANGELOG.md b/CHANGELOG.md index e93f8af..5aea2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Reopen large audit logs without overflowing the JavaScript function argument limit, preserving all history and subsequent appends. - Make concurrent sign-in starts share one account and reject missing production email configuration without logging sign-in tokens. - Snapshot protocol messages across asynchronous checks so caller mutations cannot replace guarded plaintext, verified ciphertext, recipients, or replay identifiers. - Add receiver-owned directional friendship permissions with signed updates, bidirectional migration defaults, and preserved queued delivery and receipts; thanks @jason-allen-oneal (#12). diff --git a/docs/security.md b/docs/security.md index 9225675..4cae366 100644 --- a/docs/security.md +++ b/docs/security.md @@ -35,4 +35,6 @@ discarding an incomplete final record. A complete final record without its newli is preserved and durably separated before further writes. Corrupt middle records remain errors. +Reopening an audit log verifies and retains its full chain, including histories larger than the JavaScript function argument limit; subsequent appends continue from the verified head. + Read the full [design](DESIGN.md) for envelope fields, review semantics, and key recovery. diff --git a/packages/protocol/src/node.test.ts b/packages/protocol/src/node.test.ts index 2244792..0804fca 100644 --- a/packages/protocol/src/node.test.ts +++ b/packages/protocol/src/node.test.ts @@ -2,7 +2,7 @@ import { appendFile, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { appendAudit, verifyChain } from "./audit.js"; +import { appendAudit, createAuditEntry, verifyChain } from "./audit.js"; import { generateIdentity } from "./identity.js"; import { JsonlAuditStore, FileReplayStore } from "./node.js"; import { signReceipt } from "./receipts.js"; @@ -24,6 +24,27 @@ async function temporaryDirectory(prefix: string): Promise { } describe("Node stores", () => { + it("reopens and extends an audit log larger than the function argument limit", async () => { + const directory = await temporaryDirectory("reef-audit-large-"); + const path = join(directory, "audit.jsonl"); + const count = 150_000; + const lines: string[] = []; + let head = { hash: "", seq: 0 }; + for (let index = 0; index < count; index++) { + const entry = createAuditEntry("synthetic", { id: index }, 10, auditKey, head); + lines.push(JSON.stringify(entry)); + head = { hash: entry.entryHash, seq: entry.event.seq }; + } + await writeFile(path, `${lines.join("\n")}\n`); + const reopened = new JsonlAuditStore(path, auditKey); + const appended = await reopened.appendEvent("after-reopen", { id: count }, 11); + expect(appended.event.seq).toBe(count + 1); + expect(appended.prevHash).toBe(head.hash); + const entries = await reopened.entries(); + expect(entries).toHaveLength(count + 1); + expect(verifyChain(entries)).toBe(true); + }, 30_000); + it("persists serialized audit JSONL", async () => { const directory = await temporaryDirectory("reef-audit-"); const path = join(directory, "audit.jsonl"); diff --git a/packages/protocol/src/node.ts b/packages/protocol/src/node.ts index 78decd2..95e5dbe 100644 --- a/packages/protocol/src/node.ts +++ b/packages/protocol/src/node.ts @@ -47,7 +47,7 @@ export class JsonlAuditStore implements AuditStore { if (this.#loaded) return; const entries = await readJsonl(this.path); if (!verifyChain(entries)) throw new Error("invalid audit chain"); - this.#entries.push(...entries); + for (const entry of entries) this.#entries.push(entry); const last = entries.at(-1); this.#head = { hash: last?.entryHash ?? "", seq: last?.event.seq ?? 0 }; this.#loaded = true;