export type EventType = "signup" | "trial_start" | "trial_convert" | "checkout_attempt" | "login"; export type Verdict = "allow" | "review" | "block"; /** * Optional advisory that may ride alongside a verdict — currently only on * `login`. It does not change what a verdict means: a caller who ignores it is * unaffected, which is why this is a field rather than a fourth `Verdict`. * * Open, like `Reason.code`: a server sending an action this SDK predates must * not break the caller's types. The literals still autocomplete. */ export type RecommendedAction = "none" | "challenge" | "hold" | (string & {}); export interface Reason { /** Documented known set lives in the contract; treat unknown codes as opaque. */ code: string; detail?: Record; } export interface Payment { card_fingerprint?: string; card_funding?: "credit" | "debit" | "prepaid" | "unknown"; /** Stripe `payment_method.card.country` — ISO-3166-1 alpha-2, any case. Feeds `velocity_card_countries`. */ card_country?: string; amount?: number; currency?: string; } export interface VerdictRequest { event_type: EventType; external_user_id?: string; email?: string; /** Required by the API for signup/trial_start/checkout_attempt; optional for trial_convert. */ ip?: string; user_agent?: string; device_token?: string; identity?: Record; payment?: Payment; context?: Record; } export interface VerdictResponse { id: string; verdict: Verdict; reasons: Reason[]; policy_version: string; /** Absent when there is nothing to advise, which is most events. */ recommended_action?: RecommendedAction; } /** * What `verdict()` resolves with: the real API response (`degraded: false`) or, * when Portreeve is unreachable/slow/broken, a synthetic fail-open/fail-closed * result (`degraded: true`, `id`/`policy_version` null). */ export type VerdictResult = (VerdictResponse & { degraded: false; }) | { id: null; verdict: "allow" | "block"; reasons: Reason[]; policy_version: null; degraded: true; }; export type FeedbackOutcome = "confirmed_abuse" | "false_positive" | "chargeback"; /** * What feedback is about. A closed set on purpose: feedback convicts (marks * identity clusters), so only identifiers that can safely convict are * addressable — soft signals like IPs never will be. */ export type FeedbackSubjectKind = "event_id" | "external_user_id" | "email"; export interface FeedbackSubject { kind: FeedbackSubjectKind; value: string; } export interface ReviewResolvedEvent { type: "review.resolved"; event_id: string; external_user_id?: string; /** The event's email, when one was sent — fallback resolution handle. */ email?: string; resolution: "approved" | "denied"; final_verdict: Verdict; resolved_at: string; } export type FailMode = "allow" | "block" | "throw"; export interface VerdictOptions { /** * Overrides the auto-generated per-call Idempotency-Key. Reused verbatim * across the SDK's internal retry, exactly like the auto-generated key. */ idempotencyKey?: string; /** * One logical action = one verdict, with zero conflict handling on your * side. Derive it from what identifies the ACTION — the email being signed * up, your order id at checkout. A byte-identical retry replays the * original verdict (no velocity double-count); a retry whose request * CHANGED (new device token, new IP) is automatically re-screened as a * fresh event, so context rotation stays visible to the engine. Prefer * this over idempotencyKey unless you need raw replay semantics. */ dedupeKey?: string; } export interface PortreeveOptions { /** Default: https://api.portreeve.com */ baseUrl?: string; /** Overall per-call budget (spans the retry). Default: 400. */ timeoutMs?: number; /** What a degraded verdict resolves to. Default: "allow" (fail-open). */ failMode?: FailMode; /** Injectable for tests and non-Node runtimes. Default: globalThis.fetch. */ fetch?: typeof globalThis.fetch; }