import { DataStore } from '@voltro/database'; import { Schema } from 'effect'; /** Pluggable auth strategy. Strategies are SYNC-fast on no-match * (cookie-name lookup) and cache any JWKS / DB roundtrips on match * so steady-state verification stays CPU-local. */ declare interface AuthStrategy { /** Stable id (`'voltro-password'`, `'workos'`, `'kinde'`, …). Used * in logs + `Subject.metadata.provider`. */ readonly id: string; readonly resolve: (input: AuthStrategyInput) => Promise | StrategyResolution; /** * The bearer-token PREFIX this strategy claims, when it gates on one * (`'sk_'`, `'awb_'`). Declared so a collision is DETECTABLE. * * Two strategies claiming the same prefix is not a harmless duplicate: the * chain is first-match-wins, so whichever runs first decides the Subject — * and if they resolve the same token to different authority, which one * answered decides whether authorization works. A downstream app hit exactly * this and had to pin a test asserting it never sets `apiKeys: true`, because * doing so would append the framework strategy alongside its own on the same * `sk_` prefix, with the framework one resolving without the app's team * binding. * * Optional: a cookie or JWKS strategy claims no prefix and omits it. Only * what is declared can be checked — a strategy that gates on a prefix without * saying so is invisible to the boot check, exactly as before. */ readonly claimsBearerPrefix?: string; } /** Per-call input the framework hands every strategy. */ declare interface AuthStrategyInput { readonly headers: Readonly>; readonly clientId: number; /** * The app's DataStore, for a strategy that must READ to identify the caller. * * Without it, a DB-backed strategy — a session row, an API-key record, a PAT * table — had to open a SECOND connection path beside the framework's, to the * same database the request store opens a moment later. One adopter's * `auth/db.ts` is 105 lines of exactly that: a second `ManagedRuntime` plus a * `MysqlClient`, load-bearing for session lookup and their ApiKeyStore. Every * DB-backed OIDC / SAML / PAT integration rebuilds it. * * It is the SAME value `auth.resolveScopes` receives — one store, handed to * both, rather than a second narrower type for the same object. A read-only * surface would be the better guarantee and it is not available cheaply here: * `DataStore` is the driver SPI, and a strategy that writes during subject * resolution is a design mistake the type system is not going to catch for * you. Read users / sessions / keys; do not run domain writes. * * It is the BOOT store, not a request-scoped one — strategies resolve before * a request store exists. `undefined` only while the store is still being * built (`voltro dev` builds it after the auth chain; `voltro serve` before), * and on an app with no store at all. * * **What it does and does not carry.** It applies the STORAGE codec — * `.encrypted()` columns decrypt on read and encrypt on write, and array * columns round-trip on dialects with no native array type. It applies NONE * of the Subject-dependent behaviour: no tenant scope, no soft-delete filter, * no audit-column stamping, no row-level security. That split is not an * omission on either side. Those need a Subject, and a strategy runs BEFORE * one exists — so a read of tenant-owned rows here must derive and apply that * scope itself. Encryption needs no Subject, and handing back `enc:v1:…` * would be a silent wrong answer: the ciphertext is a string, so it compares * and renders and simply never matches. */ readonly store?: DataStore; } /** * A per-strategy transform from the request's COOKIE transport to the bearer * token to verify. It receives `getCookie` — a reader that returns any cookie * by exact name, URL-decoded exactly as `readCookie` does — plus the configured * `cookieName`, and returns the JWT (or `undefined` when the cookie(s) carry no * usable token). * * Why an accessor and not a single value: some SDKs don't store a raw JWT in * one cookie. `@supabase/ssr`, for one, stores a JSON envelope that can be * base64-wrapped AND split across `.0`, `.1`, … chunk cookies — * unwrapping it means reading sibling cookies by name, not just the base one. * * The DEFAULT — used by every raw-JWT IdP (WorkOS / Kinde / Clerk / Auth0 / * OIDC) — is to read the named cookie verbatim, i.e. `getCookie(cookieName)`, * which keeps the raw-JWT cookie path byte-identical to a plain cookie read. * * SECURITY: the cookie is attacker-controlled. An implementation MUST NOT throw * — a malformed / oversized / wrong-shape value must resolve to `undefined` * (the strategy then returns `skip`), never an exception. */ export declare type CookieTokenExtractor = (getCookie: (name: string) => string | undefined, cookieName: string) => string | undefined; /** * Fetch (and per-URL cache) an OIDC discovery document — the * `.well-known/openid-configuration` an IdP publishes so consumers can * resolve `jwks_uri` without hard-coding it. One shared cached fetcher for * every `@voltro/plugin-auth-*` adapter that does OIDC discovery, so the * `Map` lives once at this layer instead of being re-rolled per * adapter. Successful lookups are cached for the process lifetime; FAILED * lookups are evicted so the next call retries (transient DNS / 5xx). Pure * `fetch` — browser-safe, no Node deps. */ export declare const discoverOidc: (url: string) => Promise; /** Pull a JWT from either `Authorization: Bearer …` (always a raw token) or * the named cookie. The optional `cookieToToken` transform unwraps a cookie * transport that is NOT itself a raw JWT (e.g. Supabase's `@supabase/ssr` * session envelope); it is applied to the COOKIE path ONLY — the Bearer header * is a raw token for every IdP. Default: read the named cookie verbatim. * Returns undefined when neither transport yields a token (strategy `skip`s). */ export declare const extractBearerOrCookie: (headers: Readonly>, cookieName: string | null, cookieToToken?: CookieTokenExtractor) => string | undefined; /** * JWKS-backed verification (asymmetric: ES256/RS256/PS256/EdDSA) — the * default for hosted IdPs that publish a JWKS endpoint. Tokens are * verified against the fetched public keys; HMAC algorithms are rejected. */ export declare interface JwtBearerJwksConfig extends JwtBearerStrategyBase { /** JWKS endpoint URL. Plugin sets the provider-specific default. */ readonly jwksUrl: string; readonly jwtSecret?: never; /** JWT algorithm allowlist. Defaults to `['ES256', 'RS256']`. Plugins * override when their IdP signs with PS256/EdDSA. */ readonly algorithms?: ReadonlyArray<'ES256' | 'RS256' | 'PS256' | 'EdDSA'>; } /** * Shared-secret verification (symmetric HMAC: HS256/384/512) — for IdPs * that sign tokens with a shared secret and expose no JWKS, e.g. legacy / * self-hosted Supabase (`JWT_SECRET`). The secret is a SERVER-side value * and must never reach the browser bundle. */ export declare interface JwtBearerSecretConfig extends JwtBearerStrategyBase { /** Shared HMAC secret the IdP signs its tokens with. */ readonly jwtSecret: string; readonly jwksUrl?: never; /** JWT algorithm allowlist. Defaults to `['HS256']`. */ readonly algorithms?: ReadonlyArray<'HS256' | 'HS384' | 'HS512'>; } export declare const jwtBearerStrategy: (config: JwtBearerStrategyConfig) => AuthStrategy; declare interface JwtBearerStrategyBase { /** Stable id — `'workos' | 'kinde' | 'clerk' | …`. Used in logs and * stamped onto `Subject.metadata.provider`. */ readonly id: string; /** Expected issuer claim(s). Passed straight to the verifier. */ readonly issuer?: string | ReadonlyArray; /** Expected audience claim(s). */ readonly audience?: string | ReadonlyArray; /** Cookie name when the JWT travels as a cookie. Pass `null` to * disable cookie fallback (header-only mode). */ readonly cookieName?: string | null; /** Map verified JWT claims to a Voltro tenantId. Most IdPs put an * org reference somewhere in the claims; plugins wire it here. * Returning `null` is treated as `failed` — the strategy IS the * request's owner but it can't satisfy the tenant invariant. */ readonly tenantIdFromClaims: (claims: Record) => string | null; /** Override how the Subject's `id` is derived from the claims. * Defaults to `claims.sub`. Plugins set this when an IdP uses a * custom subject identifier. */ readonly subjectIdFromClaims?: (claims: Record) => string; /** * Map verified JWT claims to the subject's permission scopes. The * returned list lands on `Subject.scopes`, so `requireScope` / * `hasScope` (`@voltro/protocol`) gate handlers off the IdP's claims * with NO second lookup. Typical shapes: a space-delimited OAuth * `scope` string (`(c) => String(c.scope ?? '').split(' ').filter(Boolean)`), * an Auth0 `permissions` array, a Kinde `roles[].key` list. Omitted → * `Subject.scopes` stays undefined (the unchanged default; a * handler-side scope gate then denies). Return `[]` to grant none. */ readonly scopesFromClaims?: (claims: Record) => ReadonlyArray; /** * Transform the request's cookie transport into the JWT to verify. Default: * read the named cookie verbatim — the raw-JWT cookie every hosted IdP here * uses (WorkOS / Kinde / Clerk / Auth0 / OIDC). A strategy whose cookie is * NOT a raw JWT (Supabase's `@supabase/ssr` JSON envelope) supplies one to * unwrap it. Applied to the cookie path ONLY; the Bearer header stays a raw * token. See {@link CookieTokenExtractor} — it MUST NOT throw on hostile * input (it resolves to `undefined` → `skip` instead). */ readonly cookieToToken?: CookieTokenExtractor; } /** JWT-based strategy config — verified EITHER via a JWKS endpoint * (`jwksUrl`, asymmetric) OR a shared HMAC secret (`jwtSecret`, * symmetric). The two are mutually exclusive. */ export declare type JwtBearerStrategyConfig = JwtBearerJwksConfig | JwtBearerSecretConfig; /** Thrown by `verifyJwt` on signature mismatch / expiry / claim * mismatch / unsupported algorithm. Strategies catch + downgrade * to `StrategyResolution.failed`. */ export declare class JwtVerifyError extends Error { readonly code: 'signature_mismatch' | 'expired' | 'not_yet_valid' | 'issuer_mismatch' | 'audience_mismatch' | 'unsupported_algorithm' | 'malformed' | 'jwks_unreachable' | 'other'; constructor(message: string, code: 'signature_mismatch' | 'expired' | 'not_yet_valid' | 'issuer_mismatch' | 'audience_mismatch' | 'unsupported_algorithm' | 'malformed' | 'jwks_unreachable' | 'other'); } /** The subset of an OIDC discovery document the framework reads. Providers * ship many more fields; only `jwks_uri` is load-bearing for verification. */ export declare interface OidcDiscoveryDocument { readonly issuer: string; readonly jwks_uri: string; readonly [key: string]: unknown; } /** * Read a nested claim by dot-path (`'app_metadata.tenant_id'`, * `'https://acme/tenant'`, `'org.id'`). Every IdP adapter that resolves a * tenant / scope from a NESTED claim needs the same safe walk — this is the * one shared implementation so they don't each re-derive it (and subtly * diverge on the null-guard). Pure + browser-safe: no IO, no deps. * * A path with no `.` is a plain top-level key lookup. Any missing / non-object * segment short-circuits to `undefined` (never throws on `null`/primitive * intermediates). The returned value is untyped — callers narrow it. */ export declare const readClaimPath: (claims: Record, path: string) => unknown; /** * Read a cookie value by name from a `Cookie:` header string. * Returns undefined if the cookie isn't present. */ export declare const readCookie: (header: string | undefined, name: string) => string | undefined; /** Test-only: clear the cache. Lets tests + dev-mode hot-reload skip * the per-process global cache between runs. */ export declare const _resetJwksCache: () => void; /** Test-only: clear the OIDC discovery-document cache between runs. */ export declare const _resetOidcDiscoveryCache: () => void; declare type StrategyResolution = { readonly kind: 'matched'; readonly subject: Subject; /** * When this credential expires, in unix SECONDS — if the strategy knows. * * The strategy is the ONLY place in the system that has verified the token * and holds its `exp`, and until now it could not say so. The credential * bound therefore read one source: the `voltro:session` cookie. For an app * authenticating with Bearer JWTs — six of our own catalog strategies do, * and every one of them verifies an `exp` — it was silently `undefined`, * so "a subscription can no longer outlive the credential that authorized * it" was a no-op that read as a guarantee. * * A reporter found it by expecting black screens an hour after a deploy * and getting none. Their conclusion is the one to keep: the guarantee was * not false, it was scoped to an auth shape the sentence did not name. * * Optional, and absent still means no bound — the failure direction is the * behaviour that already existed. */ readonly credentialExpiresAt?: number; } | { readonly kind: 'skip'; } | { readonly kind: 'failed'; readonly reason: string; }; declare const Subject: Schema.Union<[Schema.Struct<{ type: Schema.Literal<["user"]>; id: typeof Schema.String; tenantId: typeof Schema.String; scopes: Schema.optional>; metadata: Schema.optional>; }>, Schema.Struct<{ type: Schema.Literal<["apiKey"]>; id: typeof Schema.String; tenantId: typeof Schema.String; scopes: Schema.optional>; metadata: Schema.optional>; }>, Schema.Struct<{ type: Schema.Literal<["serviceAccount"]>; id: typeof Schema.String; tenantId: typeof Schema.String; scopes: Schema.optional>; metadata: Schema.optional>; }>, Schema.Struct<{ type: Schema.Literal<["anonymous"]>; id: typeof Schema.Null; tenantId: Schema.NullOr; /** * Set when this caller PRESENTED a credential and it was rejected — an * expired token above all. Absent when they presented none. * * The two are the same Subject and must not be the same ANSWER. A deployment * measured the cost: a user's tab outlived their IdP's token lifetime, the * strategy logged `supabase jwt expired`, the caller fell through to * anonymous, and the guard then refused with `missing required scope * 'task:u:o'`. Technically true — an anonymous caller holds no scopes — and * it sent everyone who read it into the permissions system while the problem * was an expired session. They did that round. * * It stays a FALLBACK rather than a hard failure on purpose: a stale cookie * must not break an `openAccess` procedure that needs no session at all. The * fact travels, and only a guard that actually refuses spends it. */ credentialRejected: Schema.optional; }>, Schema.Struct<{ type: Schema.Literal<["system"]>; id: typeof Schema.String; tenantId: typeof Schema.Null; scopes: Schema.optional>; metadata: Schema.optional>; }>]>; declare type Subject = typeof Subject.Type; /** Verify a JWT against a remote JWKS. Returns the decoded claims on * success. Throws `JwtVerifyError` on any verification failure. */ export declare const verifyJwt: (jwt: string, jwksUrl: string, options?: VerifyJwtOptions) => Promise>; export declare interface VerifyJwtOptions { /** Expected `iss` claim. String or list of acceptable issuers. */ readonly issuer?: string | ReadonlyArray; /** Expected `aud` claim. Most IdPs set audience = your `clientId`. */ readonly audience?: string | ReadonlyArray; /** Clock skew tolerance in seconds. Default: 60s. */ readonly clockToleranceS?: number; /** Algorithm allowlist. Default: ['ES256', 'RS256']. */ readonly algorithms?: ReadonlyArray<'ES256' | 'RS256' | 'PS256' | 'EdDSA'>; /** Override the per-JWKS cache TTL. Default 1h. */ readonly jwksCacheTtlMs?: number; } /** Verify a JWT against a shared HMAC secret (HS256/384/512). For IdPs * that sign tokens SYMMETRICALLY and expose no JWKS endpoint — e.g. * legacy / self-hosted Supabase (the project's `JWT_SECRET`). The * asymmetric `verifyJwt` (JWKS) intentionally rejects HMAC algorithms; * this is its symmetric counterpart. The secret is a server-side value * — it must never reach the browser bundle. Returns the decoded claims * on success; throws `JwtVerifyError` (same shape as `verifyJwt`). */ export declare const verifyJwtWithSecret: (jwt: string, secret: string, options?: VerifyJwtWithSecretOptions) => Promise>; export declare interface VerifyJwtWithSecretOptions { /** Expected `iss` claim. String or list of acceptable issuers. */ readonly issuer?: string | ReadonlyArray; /** Expected `aud` claim. */ readonly audience?: string | ReadonlyArray; /** Clock skew tolerance in seconds. Default: 60s. */ readonly clockToleranceS?: number; /** Algorithm allowlist. Default: ['HS256']. */ readonly algorithms?: ReadonlyArray<'HS256' | 'HS384' | 'HS512'>; } export { }