/** * verification-extraction.ts, pulling ONE artifact out of a matched message. * * Split out of `verification-expectations.ts`, which had reached the * eight-hundred-line cap exactly, so the fix that stopped a read from reaping * an expectation could not be written there without breaking the gate. The * boundary is not arbitrary: everything here is about the CONTENT of a message * that has already been matched, and nothing here can open, close, match or * expire an expectation. The book keeps the decisions; this keeps the parsing. * * The rules the split preserves, restated because they are the point: * * - a link is followed only when its host validates against the signup * domain, by label boundary and never by substring; * - a message whose links all point elsewhere is a REFUSAL naming both hosts, * not a fallback to whatever code happens to be in the body; * - everything not extracted comes back as an `UntrustedContentEnvelope`, * labelled and inert, and no decision path reads it. * * The two types it needs from the book, `CandidateEmail` and * `VerificationExpectation`, are imported `type`-only, so the module graph has * exactly one runtime edge and it runs this way: the book imports the parsing, * never the reverse. */ import { type UntrustedContentEnvelope } from '../security/untrusted-content.js'; import type { CandidateEmail, VerificationExpectation } from './verification-expectations.js'; export type VerificationArtifact = { readonly kind: 'link'; readonly url: string; readonly linkHost: string; } | { readonly kind: 'code'; readonly code: string; } | { readonly kind: 'none'; readonly reason: string; } | { readonly kind: 'refused'; readonly reason: 'link-host-mismatch'; readonly linkHost: string; readonly expectedDomain: string; readonly message: string; }; export interface VerificationExtraction { /** The one actionable thing, or a refusal. Nothing else from the body reaches here. */ readonly artifact: VerificationArtifact; /** * The rest of the message, inert and labelled. Display only. * * This is the platform's own `UntrustedContentEnvelope`, not a local shape. * It used to be a narrow hand-written mirror, kept while the real module was * elsewhere; the real one is `platform/security/untrusted-content.ts` and the * mirror is gone. The difference is not cosmetic: the envelope carries * `UNTRUSTED_CONTENT_RULE` with the text, so the standing instruction and the * content it applies to cannot be separated by anything downstream, which is * exactly what a hand-written `label` string could not guarantee. */ readonly untrustedBody: UntrustedContentEnvelope; } /** * True when `host` is the registered service domain or a subdomain of it. * * Real label-boundary matching, deliberately not substring matching: * github.com vs github.com -> true * mail.github.com vs github.com -> true (legitimate subdomain) * evil-github.com vs github.com -> false (suffix without a label boundary) * github.com.evil.com vs github.com -> false (registered domain is the attacker's) * notgithub.com vs github.com -> false */ export declare function hostMatchesServiceDomain(host: string, serviceDomain: string): boolean; /** * Pull exactly one verification artifact out of a matched message. * * Precedence: a link whose host is validated against the signup domain, then a code, * then nothing. If the message carries links but none of them are hosted at the signup * domain, the result is a refusal naming both hosts rather than a fallback to a code, * a message pointing somewhere else is not a message to salvage a token from. */ export declare function extractVerification(email: CandidateEmail, expectation: VerificationExpectation, /** Clock seam, so the envelope's `retrievedAt` is assertable under test. */ now?: () => Date): VerificationExtraction; //# sourceMappingURL=verification-extraction.d.ts.map