import { SmrtClassOptions } from '@happyvertical/smrt-core'; import { MobileAuthCompleteRequest, MobileAuthSession, MobileAuthStartRequest, MobileAuthStartResponse, MobileSessionBootstrap, MobileTenantOption } from '@happyvertical/smrt-mobile-contract'; import { OidcClaims } from '../collections/UserCollection.js'; import { OidcProviderResolutionOptions, OidcTokenSet } from './OidcLoginService.js'; import { SessionContext, SessionService } from './SessionService.js'; /** * Machine-readable error codes carried on {@link MobileAuthError} and in the * JSON error body (`{ error, code }`) the SvelteKit handlers emit. */ export type MobileAuthErrorCode = 'invalid_request' | 'unknown_provider' | 'invalid_redirect_uri' | 'missing_code_verifier' | 'invalid_state' | 'expired_transaction' | 'exchange_failed' | 'signin_not_permitted' | 'missing_bearer_token' | 'invalid_bearer_token' | 'provider_unavailable' | 'server_misconfigured'; /** * HTTP-mapped mobile-auth failure. `status` drives the response code; `code` * gives clients a stable discriminator (messages may change). */ export declare class MobileAuthError extends Error { readonly status: number; readonly code: MobileAuthErrorCode; constructor(status: number, code: MobileAuthErrorCode, message: string); } /** Context handed to the {@link MobileAuthServiceOptions.resolveUser} hook. */ export interface MobileLoginContext { claims: OidcClaims; tokens: OidcTokenSet; providerName: string; } /** Minimal user identity a {@link MobileAuthServiceOptions.resolveUser} hook returns. */ export interface MobileResolvedUser { id: string; email?: string | null; } /** Context handed to the {@link MobileAuthServiceOptions.resolveTenantId} hook. */ export interface MobileTenantContext { userId: string; claims: OidcClaims; providerName: string; } /** Context handed to the {@link MobileAuthServiceOptions.buildExtras} hook. */ export interface MobileBootstrapContext { /** Full session context (user, membership, tenantId, sessionId). */ session: SessionContext; /** * The session's resolved permission slugs. Any MODEL JSON placed into * `extras` must be projected with `toPublicJSON({ permissions })` using * THIS set, or the response leaks fields the generated routes redact * (`@field({ readPermission })`, #1822). Fail closed. */ permissions: string[]; tenants: MobileTenantOption[]; activeTenant: MobileTenantOption | null; } /** Request metadata recorded onto the minted session. */ export interface MobileRequestMeta { userAgent?: string; ipAddress?: string; } export interface MobileAuthServiceOptions extends SmrtClassOptions, OidcProviderResolutionOptions { /** Optional fetch override for tests or custom runtimes. */ fetch?: typeof fetch; /** JWT clock tolerance passed to jose. */ clockTolerance?: number | string; /** * Allowed mobile redirect URIs. Entries are exact matches, except entries * ending in `/` which allow any sub-path of that prefix. When omitted or * empty, any structurally valid mobile redirect URI is accepted (https, * loopback http, or a private app scheme — RFC 8252); configure this in * production so authorization responses cannot be pointed at an * attacker-controlled URI. */ redirectUris?: string[]; /** Mobile bearer session TTL in seconds. Default: 30 days. */ sessionTtl?: number; /** * Auth handshake (start → complete) TTL in seconds. Default: 10 minutes, * matching the web flow's transaction cookie. */ transactionTtl?: number; /** * HMAC secret for state-token integrity. Defaults to the resolved * provider's `clientSecret`. * * State signing binds the `nonce`/provider/createdAt in the OAuth `state` * so they cannot be forged, so it is REQUIRED by default: if neither * `stateSecret` nor the provider's `clientSecret` is available, sign-in * fails closed (500 `server_misconfigured`). A public OIDC client (no * client secret — the PKCE case) just supplies a `stateSecret`; it is a * server-side HMAC key unrelated to OAuth client authentication, so any * deployment can set one. Set {@link allowUnsignedState} to opt into * unsigned tokens (NOT recommended — then `redirectUris` is the only * defense against state forgery). */ stateSecret?: string; /** * Permit unsigned state tokens when no `stateSecret`/`clientSecret` is * configured. Default false (fail closed). Only enable for local * development or a deployment that accepts the state-forgery risk; * production should configure a `stateSecret` instead. */ allowUnsignedState?: boolean; /** * Include descendant tenants reachable through ACTIVE memberships whose * role has `inheritsToDescendants: true` (#1867) in the bootstrap tenant * list. Default true. */ includeInheritedTenants?: boolean; /** * Provision a user even when the IdP explicitly reported the email as * unverified. Passed through to `UserCollection.getOrCreateFromOidc`. */ allowUnverifiedEmail?: boolean; /** * Map verified IdP claims to a SMRT user. The default provisions (or * resolves) the user via `UserCollection.getOrCreateFromOidc`. Return * `null`/`undefined` to REFUSE sign-in (403 `signin_not_permitted`) — * invite-gated apps resolve against their own membership rules here * without any user row being created. THROWING (vs returning null) is * treated as an unexpected server error and surfaces as a generic 500; * translate expected denials into a `null` return or a thrown * {@link MobileAuthError}. */ resolveUser?: (context: MobileLoginContext) => Promise; /** * Choose the tenant the minted session is bound to. Return a tenant id, * `null` for an explicitly tenant-less session, or `undefined` to fall * back to the default (the first direct ACTIVE membership's tenant, * sorted by tenant name). The session's tenant is the isolation key for * every `@TenantScoped` query, so only return tenants the user can * actually resolve permissions in. */ resolveTenantId?: (context: MobileTenantContext) => Promise; /** * App-defined `extras` object for the session bootstrap. Must be a plain * JSON object (the Kotlin client decodes it as `JsonObject`). See * {@link MobileBootstrapContext.permissions} for the read-permission * redaction requirement on model JSON. */ buildExtras?: (context: MobileBootstrapContext) => Promise | null | undefined>; } /** Result of {@link MobileAuthService.logout}. */ export interface MobileLogoutResult { ok: true; /** Whether a live session was actually revoked. */ destroyed: boolean; } /** * Pull the token out of an `Authorization: Bearer ` header. Returns * `null` when the header is missing or malformed. */ export declare function readMobileBearerToken(authorizationHeader: string | null | undefined): string | null; /** * Validate a mobile redirect URI: absolute, a safe scheme (https, loopback * http, or a private app scheme per RFC 8252), and — when an allow list is * configured — present on it. Returns the normalized URI. */ export declare function validateMobileRedirectUri(value: unknown, allowList?: string[]): string; /** * Server implementation of the `/api/mobile` auth + session contract. * * Framework-agnostic: methods take plain wire DTOs and header strings and * return wire DTOs or throw {@link MobileAuthError}. The SvelteKit adapters * live in `@happyvertical/smrt-users/sveltekit` * (`createMobileAuthHandlers`). */ export declare class MobileAuthService { private readonly options; private readonly classOptions; private readonly sessionTtl; private readonly transactionTtl; private sessionService; private userCollection; private membershipCollection; private roleCollection; private tenantCollection; /** Per-(provider, redirectUri) service cache preserving metadata caches. */ private readonly oidcServices; constructor(options: MobileAuthServiceOptions); private initialize; static create(options: MobileAuthServiceOptions): Promise; /** * The underlying {@link SessionService}. Exposed so route guards can share * it with `withSessionPermissionContext` instead of minting a second one. */ getSessionService(): SessionService; private resolveProvider; private getOidcService; /** * The HMAC secret for state signing, or `undefined` when unsigned tokens * are explicitly permitted via {@link MobileAuthServiceOptions.allowUnsignedState}. * Fails closed (throws → 500 `server_misconfigured`) when no secret is * available and unsigned tokens are not opted in, so a deployment can never * silently fall back to forgeable state tokens. */ private stateSecretFor; /** * `POST /api/mobile/auth/start` — begin the server-brokered PKCE * handshake. Returns the authorization URL plus the `state` and * `codeVerifier` the client must persist and echo back on complete. */ start(input: MobileAuthStartRequest): Promise; /** * `POST /api/mobile/auth/complete` — exchange the authorization code (with * the echoed `state` + `codeVerifier`) for a mobile bearer session. */ complete(input: MobileAuthCompleteRequest, meta?: MobileRequestMeta): Promise; private resolveUserFromLogin; private resolveSessionTenantId; /** * `GET /api/mobile/session` — bootstrap the app from a bearer token. * Throws 401 when the token is missing, unknown, expired, or revoked. */ bootstrap(authorizationHeader: string | null | undefined): Promise; /** * Resolve a bearer header into a full {@link SessionContext} (user, * membership, resolved permissions, tenant). Throws {@link MobileAuthError} * with the 401 semantics the mobile client's re-auth flow expects. */ resolveSessionContext(authorizationHeader: string | null | undefined): Promise; /** * `DELETE /api/mobile/session` — revoke the bearer session. Idempotent: * a missing or unknown token reports `destroyed: false` with 200. */ logout(authorizationHeader: string | null | undefined): Promise; /** * The user's selectable tenants: direct ACTIVE memberships plus — when * `includeInheritedTenants` is on (default) — descendant tenants reachable * through an ACTIVE membership whose role has `inheritsToDescendants: * true` (#1867). Selection mirrors the permission resolver: the NEAREST * flagged ancestor membership labels an inherited option, unflagged or * inactive ancestors neither confer nor block, and ANY direct membership * row on a tenant pins it (active → its own role; inactive → excluded, * since a pinned inactive row resolves to the empty permission set). * * This list is informational — per-request authorization always re-runs * through `PermissionResolver`, which additionally fail-closes on * malformed hierarchy paths. */ listTenantOptions(userId: string): Promise; private buildTenantOptions; private loadTenantOptionSources; private toTenantOption; private toUserSummary; } //# sourceMappingURL=MobileAuthService.d.ts.map