import type { Persona, PersonaName } from "../types.js"; /** * Identity providers that keep the users somewhere other than this database. * * Clerk, Auth0, Cognito and Firebase Auth leave the same shape behind: the * application's owner column is a bare string holding the provider's subject, * and there is no `auth.users` for Crossline to insert a persona into. Until * now such an application was refused outright, and refusing was the honest * answer — but it is also the largest population whose authorization lives in * route handlers rather than in the database, which is precisely the population * that needs to be two signed-in users to be checked at all. * * All four have an admin API that creates a user and then produces a credential * for them. That is the `supabase_admin` shape applied to somebody else's * provider, and it is what this file and its four siblings implement. * * ## The ordering, which is the whole problem * * A run normally goes: generate two persona ids → plant rows under them → * become one of them. Every provider here assigns the id itself. Done in that * order the session would belong to a user who owns none of the planted rows, * every probe would come back empty, and **empty is exactly what a correctly * secured application returns**. The check would not have run and the run would * say it passed, which is worse than the gap it was closing. * * So the order is inverted for these strategies: create the provider's users * first, take the ids the provider assigned as the personas' ids, and only then * seed. `provisionPersonas` in `session.ts` is that step, and it happens before * `seed()` rather than beside it. Every persona that comes back carries * `assignedBy`, so a later stage can tell a provider-assigned id from one of * ours instead of trusting that the ordering was right. * * ## What counts as an identity * * A 200 is not proof. Every provider here is asked, in its own words, who the * credential it just issued belongs to — Clerk's session object names a * `user_id`, Auth0's `/userinfo` names a `sub`, Cognito's `GetUser` names the * `sub` attribute, Firebase's `accounts:lookup` names a `localId` — and that * answer has to be the user we asked for or nothing is claimed at all. The * identity is a fact the provider stated, never one we inferred from a status * code or read out of an unverified token. * * ## The credential * * Each admin key is used against its own provider's API and never against the * application, so it cannot reach a reproduction command, `last-run.json` or a * report — all of which are built from the headers the probes actually sent. * Nothing in this file or its siblings ever puts a request body or an * `Authorization` header into a reason string; only the status and the * provider's own error text travel outwards. `resolveApiAuth` refuses to read * any of these keys from anywhere but `env:NAME`. */ /** An account this run created at a provider, so it can be named and removed. */ export interface ProviderAccount { /** The provider, as it appears in `api.auth.kind`. */ provider: string; /** The id the provider assigned. */ userId: string; email: string; } export interface ProviderIdentity { /** * The personas, under the ids the provider assigned. Null when no identity * could be established, in which case `unavailable` says exactly what was * missing and the run seeds under its own ids and checks only the signed-out * half. */ personas: Record | null; /** Headers for this persona, or for an anonymous client when null. */ headersFor(persona: Persona | null): Record; /** How the identity was established and how it was proven. For the report. */ established: string | null; /** Why no request can be made as a specific signed-in user. Null when one can. */ unavailable: string | null; /** Accounts left at the provider, including ones created before a later step failed. */ accounts: ProviderAccount[]; /** * Remove the accounts this run created at the provider, best effort. * * Returns the ones that survived, described well enough to be found by hand. * A signup is a real write and there is nothing in the database to cascade it * away, so this is the only thing that cleans up after a provider run. */ dispose(): Promise; } /** The names the run works with, in the order they are created. */ export declare const PERSONA_NAMES: PersonaName[]; /** How long a request to a provider's API may take before it counts as failed. */ export declare const PROVIDER_TIMEOUT_MS = 15000; export interface Answer { status: number; body: unknown; /** The provider's own error code, when it gave one. */ code: string | null; /** The provider's own message, when it gave one. */ message: string | null; /** Set when the request did not complete at all. */ error: string | null; } /** * One JSON request to a provider's API. * * Deliberately returns rather than throws: every caller here turns a failure * into a refusal that says what is missing, and an exception would have to be * caught and reworded at each site anyway. */ export declare function providerRequest(method: string, url: string, headers: Record, body?: unknown): Promise; /** A form-encoded request, which is what OAuth token endpoints take. */ export declare function providerForm(url: string, form: Record): Promise; /** * Describe an answer without ever quoting what we sent. * * Only the status and the provider's own words are used, so an admin key * cannot travel into a reason string and from there into the report. */ export declare function describeAnswer(answer: Answer): string; /** An identity that emits nothing, and says why. */ export declare function providerDeclined(reason: string, accounts: ProviderAccount[], dispose: () => Promise): ProviderIdentity; /** * The email a persona is created with at the provider. * * `@example.test` is reserved by RFC 6761 and can never be delivered to, which * matters here in a way it does not in the database: these providers send mail. */ export declare function providerEmail(name: PersonaName, run: string): string; /** * A password only this run knows, for the providers whose only route to a * session is to sign in as the user. * * Long, mixed-class and random, because every one of these providers enforces a * password policy and a rejected password is a check that did not run. */ export declare function providerPassword(random: (bytes: number) => Buffer): string; /** * The claims of a JWT, read without verifying it. * * Used only to cross-check a token against what the provider *separately* told * us the identity was — never as the proof itself. A forged token would fail * the provider's own answer, so nothing here rests on the payload being true. */ export declare function claimsOf(token: string): Record | null;