import { describe, expect, it } from "vitest"; import { COMPACT_PROOF_PREFIX, ED25519_SHA512_ALG, Ed25519Secret, Identity, IdentityError, NOSTR_SCHNORR_ALG, NostrSecret, VerificationError, canonicalBytes, dnsTxtName, dnsTxtValue, extractMarkdownProof, fromCompact, fromJson, newClaimPayload, nostrPubkeyHex, parseDnsTxtValue, signClaim, toCompact, toMarkdown, toPrettyJson, verifyClaim, } from "../src/index.js"; function makeSigned(subjectStr: string) { const secret = NostrSecret.generate(); const primary = secret.identity(); const subject = Identity.parse(subjectStr); const payload = newClaimPayload(subject, primary, new Date()); return { secret, primary, subject, signed: signClaim(payload, secret) }; } describe("Identity", () => { it("parses bare npub as nostr identity", () => { const secret = NostrSecret.generate(); const npub = secret.npub(); const id = Identity.parse(npub); expect(id.toString()).toBe(`nostr:${npub}`); expect(id.scheme).toBe("nostr"); }); it("rejects invalid inputs", () => { expect(() => Identity.parse("")).toThrow(IdentityError); expect(() => Identity.parse(" ")).toThrow(IdentityError); expect(() => Identity.parse("no-colon")).toThrow(IdentityError); expect(() => Identity.parse(":missing-scheme")).toThrow(IdentityError); expect(() => Identity.parse("scheme:")).toThrow(IdentityError); expect(() => Identity.parse("nostr:not-a-real-npub")).toThrow(IdentityError); }); it("splits scheme and id", () => { const id = Identity.parse("github:jason"); expect(id.scheme).toBe("github"); expect(id.id).toBe("jason"); expect(id.toString()).toBe("github:jason"); }); }); describe("NostrSecret", () => { it("round-trips nsec", () => { const secret = NostrSecret.generate(); const nsec = secret.nsec(); const restored = NostrSecret.fromNsec(nsec); expect(restored.npub()).toBe(secret.npub()); expect(restored.pubkeyHex()).toBe(secret.pubkeyHex()); }); it("nostrPubkeyHex returns 32-byte lowercase", () => { const secret = NostrSecret.generate(); const hex = nostrPubkeyHex(secret.identity()); expect(hex).toHaveLength(64); expect(hex).toBe(hex.toLowerCase()); }); }); describe("signClaim / verifyClaim", () => { it("signs and verifies", () => { const { signed, primary } = makeSigned("github:jason"); const status = verifyClaim(signed); expect(status.status).toBe("valid"); expect(status.primary.equals(primary)).toBe(true); expect(status.verified[0].toString()).toBe("github:jason"); }); it("rejects tampered subject", () => { const { signed } = makeSigned("github:jason"); signed.payload.subject = "github:mallory"; expect(() => verifyClaim(signed)).toThrow(VerificationError); }); it("rejects unsupported algorithm", () => { const { signed } = makeSigned("github:jason"); signed.signature.alg = "made-up-suite"; expect(() => verifyClaim(signed)).toThrow(/unsupported algorithm/); }); it("rejects signature.key mismatched against payload.primary", () => { const { signed } = makeSigned("github:jason"); const other = NostrSecret.generate(); signed.signature.key = `nostr:${other.npub()}`; expect(() => verifyClaim(signed)).toThrow(/does not match payload.primary/); }); it("uses the expected algorithm string", () => { const { signed } = makeSigned("github:jason"); expect(signed.signature.alg).toBe(NOSTR_SCHNORR_ALG); }); }); describe("encodings", () => { it("round-trips JSON", () => { const { signed } = makeSigned("github:jason"); const json = toPrettyJson(signed); const back = fromJson(json); expect(back).toEqual(signed); verifyClaim(back); }); it("round-trips compact", () => { const { signed } = makeSigned("github:jason"); const compact = toCompact(signed); expect(compact.startsWith(COMPACT_PROOF_PREFIX)).toBe(true); const back = fromCompact(compact); expect(back).toEqual(signed); verifyClaim(back); }); it("round-trips markdown", () => { const { signed } = makeSigned("github:jason"); const md = toMarkdown(signed); expect(md).toContain("```kez"); const back = extractMarkdownProof(md); expect(back).toEqual(signed); verifyClaim(back); }); it("round-trips legacy DNS TXT", () => { const { signed } = makeSigned("dns:jason.example.com"); const txt = dnsTxtValue(signed); expect(txt.startsWith("kez1:")).toBe(true); const back = parseDnsTxtValue(txt); expect(back).toEqual(signed); }); it("extractMarkdownProof rejects malformed input", () => { expect(() => extractMarkdownProof("no fence here")).toThrow(); expect(() => extractMarkdownProof("```kez\n{ unterminated")).toThrow(); }); it("fromCompact rejects missing prefix", () => { expect(() => fromCompact("hello")).toThrow(); expect(() => fromCompact("kez1:foo")).toThrow(); }); }); describe("dns helpers", () => { it("dnsTxtName requires dns: scheme", () => { expect(dnsTxtName(Identity.parse("dns:jason.example.com"))).toBe( "_kez.jason.example.com", ); expect(() => dnsTxtName(Identity.parse("github:jason"))).toThrow(); }); }); describe("JCS", () => { it("produces stable bytes for object regardless of key ordering", () => { const a = canonicalBytes({ b: 1, a: 2, c: [3, 4] }); const b = canonicalBytes({ c: [3, 4], a: 2, b: 1 }); expect(a).toEqual(b); }); }); describe("Ed25519Secret", () => { it("round-trips seed hex", () => { const secret = Ed25519Secret.generate(); const seed = secret.seedHex(); const restored = Ed25519Secret.fromSeedHex(seed); expect(restored.pubkeyHex()).toBe(secret.pubkeyHex()); expect(restored.seedHex()).toBe(seed); }); it("identity is lowercase 64-hex with ed25519: scheme", () => { const secret = Ed25519Secret.generate(); const id = secret.identity(); expect(id.scheme).toBe("ed25519"); expect(id.id).toHaveLength(64); expect(id.id).toBe(id.id.toLowerCase()); }); it("rejects malformed seed", () => { expect(() => Ed25519Secret.fromSeedHex("notHex")).toThrow(); expect(() => Ed25519Secret.fromSeedHex("ab".repeat(31))).toThrow(/32 bytes/); }); }); describe("Identity ed25519 validation", () => { it("accepts well-formed identifiers", () => { expect(() => Identity.parse(`ed25519:${"ab".repeat(32)}`), ).not.toThrow(); }); it("rejects malformed identifiers", () => { expect(() => Identity.parse("ed25519:tooshort")).toThrow(); expect(() => Identity.parse(`ed25519:${"AB".repeat(32)}`)).toThrow(); expect(() => Identity.parse(`ed25519:${"Z".repeat(64)}`)).toThrow(); }); }); describe("Ed25519 signClaim / verifyClaim", () => { function signEd25519(subjectStr: string) { const secret = Ed25519Secret.generate(); const primary = secret.identity(); const subject = Identity.parse(subjectStr); const payload = newClaimPayload(subject, primary, new Date()); return { secret, primary, subject, signed: signClaim(payload, secret) }; } it("uses the ed25519 alg string", () => { const { signed } = signEd25519("github:jason"); expect(signed.signature.alg).toBe(ED25519_SHA512_ALG); }); it("signs and verifies", () => { const { signed, primary } = signEd25519("github:jason"); const status = verifyClaim(signed); expect(status.status).toBe("valid"); expect(status.primary.equals(primary)).toBe(true); }); it("rejects tampered subject", () => { const { signed } = signEd25519("github:jason"); signed.payload.subject = "github:mallory"; expect(() => verifyClaim(signed)).toThrow(VerificationError); }); it("rejects ed25519 sig over non-ed25519 primary", () => { const { signed } = signEd25519("github:jason"); // Forge a nostr-shaped primary while keeping the ed25519 alg. const other = NostrSecret.generate(); signed.payload.primary = `nostr:${other.npub()}`; signed.signature.key = signed.payload.primary; expect(() => verifyClaim(signed)).toThrow(/ed25519/); }); });