import { describe, expect, it } from "vitest"; import { COMPACT_CHAIN_PREFIX, Ed25519Secret, Identity, NostrSecret, Sigchain, SigchainError, newAddPayload, signSigchainEvent, } from "../src/index.js"; function fresh() { const s = NostrSecret.generate(); const id = Identity.parse(`nostr:${s.npub()}`); return { secret: s, primary: id }; } describe("Sigchain", () => { it("appends and validates an add event", () => { const { secret, primary } = fresh(); const chain = Sigchain.create(primary); expect(chain.isEmpty).toBe(true); expect(chain.nextSeq()).toBe(0); const subject = Identity.parse("github:jason"); chain.signAdd(subject, undefined, secret); expect(chain.length).toBe(1); expect(chain.nextSeq()).toBe(1); expect(chain.isActive(subject)).toBe(true); expect(chain.isRevoked(subject)).toBe(false); expect(() => chain.validate()).not.toThrow(); }); it("revoke flips isActive/isRevoked", () => { const { secret, primary } = fresh(); const chain = Sigchain.create(primary); const subject = Identity.parse("github:jason"); chain.signAdd(subject, undefined, secret); chain.signRevoke(subject, secret); expect(chain.isRevoked(subject)).toBe(true); expect(chain.isActive(subject)).toBe(false); expect(() => chain.validate()).not.toThrow(); }); it("rejects events for a different primary", () => { const a = fresh(); const b = fresh(); const chain = Sigchain.create(a.primary); const payload = newAddPayload( b.primary, // wrong 0, undefined, Identity.parse("github:jason"), undefined, new Date(), ); const signed = signSigchainEvent(payload, a.secret); expect(() => chain.append(signed)).toThrow(SigchainError); try { chain.append(signed); } catch (e) { expect((e as SigchainError).code).toBe("WrongPrimary"); } }); it("rejects seq skip", () => { const { secret, primary } = fresh(); const chain = Sigchain.create(primary); chain.signAdd(Identity.parse("github:a"), undefined, secret); // hand-craft seq=2 with correct prev const payload = newAddPayload( primary, 2, chain.headHash(), Identity.parse("github:b"), undefined, new Date(), ); const signed = signSigchainEvent(payload, secret); try { chain.append(signed); expect.fail("expected SigchainError"); } catch (e) { expect((e as SigchainError).code).toBe("SeqMismatch"); } }); it("rejects bad prev hash", () => { const { secret, primary } = fresh(); const chain = Sigchain.create(primary); chain.signAdd(Identity.parse("github:a"), undefined, secret); const payload = newAddPayload( primary, 1, "sha256:0000", Identity.parse("github:b"), undefined, new Date(), ); const signed = signSigchainEvent(payload, secret); try { chain.append(signed); expect.fail("expected SigchainError"); } catch (e) { expect((e as SigchainError).code).toBe("PrevMismatch"); } }); it("round-trips JSONL", () => { const { secret, primary } = fresh(); const chain = Sigchain.create(primary); const subject = Identity.parse("github:jason"); chain.signAdd(subject, undefined, secret); chain.signRevoke(subject, secret); const jsonl = chain.toJsonl(); const restored = Sigchain.fromJsonl(jsonl); expect(restored.length).toBe(chain.length); expect(restored.isRevoked(subject)).toBe(true); expect(restored.headHash()).toBe(chain.headHash()); }); it("round-trips compact bundle", () => { const { secret, primary } = fresh(); const chain = Sigchain.create(primary); chain.signAdd(Identity.parse("github:jason"), undefined, secret); chain.signAdd(Identity.parse("dns:example.com"), undefined, secret); const compact = chain.toCompactBundle(); expect(compact.startsWith(COMPACT_CHAIN_PREFIX)).toBe(true); const restored = Sigchain.fromCompactBundle(compact); expect(restored.length).toBe(2); expect(restored.headHash()).toBe(chain.headHash()); }); it("fromJsonl detects tampering", () => { const { secret, primary } = fresh(); const chain = Sigchain.create(primary); chain.signAdd(Identity.parse("github:a"), undefined, secret); chain.signAdd(Identity.parse("github:b"), undefined, secret); let jsonl = chain.toJsonl(); jsonl = jsonl.replace("github:b", "github:c"); try { Sigchain.fromJsonl(jsonl); expect.fail("expected SigchainError"); } catch (e) { const code = (e as SigchainError).code; expect(["BadSignature", "PrevMismatch"]).toContain(code); } }); it("works with ed25519 signer", () => { const secret = Ed25519Secret.generate(); const primary = secret.identity(); const chain = Sigchain.create(primary); const subject = Identity.parse("github:jason"); chain.signAdd(subject, undefined, secret); expect(chain.isActive(subject)).toBe(true); expect(() => chain.validate()).not.toThrow(); }); });