import assert from "node:assert/strict"; import test from "node:test"; import { createBuiltinFinalHandler, DecisionJournal, TrustRunner, } from "../src/index.js"; import { unknownLocalAuthority, validPublicCertificate, } from "../fixtures/tls.js"; test("built-in handler rejects an unresolved certificate failure", async () => { const result = await new TrustRunner().evaluate(unknownLocalAuthority); assert.equal(result.verdict.trusted, false); assert.equal(result.verdict.lifetime.kind, "connection"); assert.equal(result.journal.entries.length, 0); }); test("built-in handler preserves an ordinary successful validation", async () => { const result = await new TrustRunner().evaluate(validPublicCertificate); assert.equal(result.verdict.trusted, true); }); test("a failure hook can trust an exact certificate for a host", async () => { const configured = configuredPlugin({ async onTlsFailure({ facts }) { return verdictFor(facts, true); }, }); const result = await new TrustRunner({ plugins: [configured] }).evaluate( unknownLocalAuthority, ); assert.equal(result.verdict.trusted, true); assert.equal(result.verdict.scope.kind, "certificate-for-host"); assert.equal(result.journal.entries.at(-1).kind, "resolution"); assert.equal( result.journal.entries.at(-1).pluginId, configured.plugin.manifest.id, ); }); test("evidence collection is independent but appended in plugin order", async () => { const slowFirst = configuredPlugin({ id: "test.first", mode: "observer", async collectEvidence() { await new Promise((resolve) => setTimeout(resolve, 5)); return { entries: [{ kind: "evidence", code: "first" }] }; }, }); const fastSecond = configuredPlugin({ id: "test.second", mode: "observer", collectEvidence() { return { entries: [{ kind: "warning", code: "second" }] }; }, }); const result = await new TrustRunner({ plugins: [slowFirst, fastSecond], }).evaluate(validPublicCertificate); assert.deepEqual( result.journal.entries.map((entry) => entry.code), ["first", "second"], ); }); test("asynchronous failure hook timeout is recorded and falls through safely", async () => { const configured = configuredPlugin({ async onTlsFailure() { await new Promise((resolve) => setTimeout(resolve, 30)); return undefined; }, }); const result = await new TrustRunner({ plugins: [configured], timeoutMs: 5, }).evaluate(unknownLocalAuthority); assert.equal(result.verdict.trusted, false); assert.equal(result.journal.entries.at(-1).code, "plugin-timeout"); }); test("successful-path hook must return synchronously", async () => { const configured = configuredPlugin({ async onBeforeTlsAccept() { return undefined; }, }); const result = await new TrustRunner({ plugins: [configured] }).evaluate( validPublicCertificate, ); assert.equal(result.verdict.trusted, true); assert.match(result.journal.entries.at(-1).message, /must return synchronously/); }); test("a verdict cannot escape the active hostname, port, or certificate", async () => { const configured = configuredPlugin({ async onTlsFailure({ facts }) { return { ...verdictFor(facts, true), scope: { ...exactScope(facts), hostname: "different.test", }, }; }, }); const result = await new TrustRunner({ plugins: [configured] }).evaluate( unknownLocalAuthority, ); assert.equal(result.verdict.trusted, false); assert.match(result.journal.entries.at(-1).message, /scope does not match/); }); test("observer and advisor modes cannot issue terminal verdicts", async () => { for (const mode of ["observer", "advisor"]) { const configured = configuredPlugin({ mode, async onTlsFailure({ facts }) { return verdictFor(facts, true); }, }); const result = await new TrustRunner({ plugins: [configured] }).evaluate( unknownLocalAuthority, ); assert.equal(result.verdict.trusted, false); assert.match(result.journal.entries.at(-1).message, /cannot issue/); } }); test("veto authority can reject but cannot trust", async () => { const configured = configuredPlugin({ mode: "veto-authority", async onTlsFailure({ facts }) { return verdictFor(facts, true); }, }); const result = await new TrustRunner({ plugins: [configured] }).evaluate( unknownLocalAuthority, ); assert.equal(result.verdict.trusted, false); assert.match(result.journal.entries.at(-1).message, /cannot issue a trusted/); }); test("authority scope must identify a certificate in the active chain", async () => { const configured = configuredPlugin({ async onTlsFailure({ facts }) { return { trusted: true, scope: { kind: "authority-for-host", hostname: facts.hostname, port: facts.port, authorityCertificateSha256: "not-in-this-chain", includeSubdomains: false, }, lifetime: { kind: "session" }, reasonEntryIds: [], overriddenErrors: [...facts.errors], }; }, }); const result = await new TrustRunner({ plugins: [configured] }).evaluate( unknownLocalAuthority, ); assert.equal(result.verdict.trusted, false); assert.match(result.journal.entries.at(-1).message, /does not match the certificate chain/); }); test("journal snapshots and entries are immutable", () => { const journal = new DecisionJournal(); journal.append( { id: "test.plugin", name: "Test plugin" }, [{ kind: "evidence", data: { nested: true } }], ); const snapshot = journal.snapshot(); assert.throws(() => snapshot.entries.push({}), TypeError); assert.throws(() => { snapshot.entries[0].data.nested = false; }, TypeError); }); test("only Browsec's immutable handler can occupy the final position", () => { assert.throws( () => new TrustRunner({ finalHandler: { manifest: pluginManifest("test.impostor", ["decision-authority"]), finalize() {}, }, }), /must be Browsec's built-in handler/, ); assert.doesNotThrow(() => new TrustRunner({ finalHandler: createBuiltinFinalHandler() }), ); }); function configuredPlugin({ id = "test.plugin", mode = "decision-authority", collectEvidence, onBeforeTlsAccept, onTlsFailure, }) { return { mode, plugin: { manifest: pluginManifest(id, [mode]), hooks: { collectEvidence, onBeforeTlsAccept, onTlsFailure }, }, }; } function pluginManifest(id, supportedModes) { return { manifestVersion: 1, trustApiVersion: "0.1", id, name: id, version: "0.0.0", supportedModes, capabilities: {}, }; } function verdictFor(facts, trusted) { return { trusted, scope: exactScope(facts), lifetime: { kind: "connection" }, reasonEntryIds: [], overriddenErrors: trusted ? [...facts.errors] : [], }; } function exactScope(facts) { return { kind: "certificate-for-host", hostname: facts.hostname, port: facts.port, certificateSha256: facts.presentedChain[0].sha256, }; }