import { SmrtCollection } from '@happyvertical/smrt-core'; import { OidcIdentity, Profile } from '@happyvertical/smrt-profiles'; import { getDatabase } from '@happyvertical/sql'; import { User } from '../models/User.js'; import { UserStatus } from '../types/index.js'; type DatabaseInterface = Awaited>; /** * OIDC claims used for identity resolution */ export interface OidcClaims { /** Subject identifier from IdP */ sub: string; /** Issuer URL */ iss: string; /** User's email address */ email?: string; /** Whether the IdP verified the email address */ email_verified?: boolean; /** User's display name */ name?: string; /** Preferred username */ preferred_username?: string; } /** OIDC claims after the provisioning boundary validates and normalizes email. */ export type NormalizedOidcClaims = Readonly; /** * Result of OIDC identity resolution */ export interface OidcIdentityResult { /** The User record */ user: User; /** The linked Profile */ profile: Profile; /** The OidcIdentity linking profile to IdP */ oidcIdentity: OidcIdentity; /** Whether the profile was newly created */ created: boolean; } /** Context passed after OIDC claims are validated and before provisioning. */ export interface OidcProfileResolverContext { /** Transaction-bound database; use this for every resolver read/write. */ db: DatabaseInterface; /** * Frozen normalized claim snapshot. OidcLoginService supplies * protocol-validated claims; direct collection callers must provide claims * from a trusted boundary. */ claims: NormalizedOidcClaims; /** Configured provider key. */ provider: string; /** Transaction-bound User collection. */ users: UserCollection; } /** * Resolve a consumer-owned canonical Profile before User/session creation. * * Return `undefined` for the secure default, `null` to reject login, or a * Profile to select it explicitly. For a new issuer/subject, a supplied Profile * is still required to be the unique, unowned global Person for the verified * email. For an exact existing issuer/subject, `null` still rejects login and a * supplied Profile must be the already-linked Profile; the hook cannot rebind * identity authority. Stable-link owner and canonical-Person checks still * apply. The hook may be retried after a concurrent unique-key race and must * therefore be idempotent. */ export type OidcProfileResolver = (context: OidcProfileResolverContext) => Profile | null | undefined | Promise; /** Application authorization for binding a new OIDC identity to an owner. */ export interface OidcProfileOwnerAuthorization { /** Canonical global Person selected by the application. */ profile: Profile; /** Existing User the application authorizes as that Profile's owner. */ user: User; } /** Transaction-bound context supplied to the owner authorization callback. */ export type OidcProfileOwnerAuthorizerContext = OidcProfileResolverContext; /** * Explicitly authorize first OIDC binding to a pre-provisioned Profile/User. * * Return `undefined` to preserve SMRT's secure default, `null` to reject the * login, or both the canonical Profile and its existing owning User. SMRT * reloads and verifies both records inside the provisioning transaction; the * returned objects are never trusted as proof of ownership. The callback may * be retried after a concurrent unique-key race and must be idempotent. */ export type OidcProfileOwnerAuthorizer = (context: OidcProfileOwnerAuthorizerContext) => OidcProfileOwnerAuthorization | null | undefined | Promise; export type OidcProvisioningErrorCode = 'ambiguous_identity' | 'concurrency_conflict' | 'profile_owned' | 'rejected' | 'transaction_required' | 'user_email_backfill_required' | 'user_email_conflict'; /** Fail-closed OIDC identity provisioning error. */ export declare class OidcProvisioningError extends Error { readonly code: OidcProvisioningErrorCode; constructor(code: OidcProvisioningErrorCode, message: string, options?: { cause?: unknown; }); } /** * Options for getOrCreateFromOidc */ export interface GetOrCreateFromOidcOptions { /** If false, skip recording login timestamp (default: true) */ recordLogin?: boolean; /** * Provision a user even when the IdP explicitly reported the email as * unverified (`email_verified: false`). Default false (#1400): refuse to * create/resolve a user from a known-unverified address. Has no effect when * the claim is absent — an IdP that omits `email_verified` makes no * assertion, so it cannot be enforced. */ allowUnverifiedEmail?: boolean; /** * Optional application resolver invoked inside the provisioning transaction * after token/claim validation and before OIDC identity, User, or session * creation. Return a canonical Profile, `null` to reject, or `undefined` to * use SMRT's secure default. */ resolveProfile?: OidcProfileResolver; /** * Explicitly authorize a first issuer/subject binding to a pre-provisioned * canonical Profile and its existing owner. A successful authorization * requires `email_verified === true` and is revalidated atomically by SMRT. */ authorizeProfileOwner?: OidcProfileOwnerAuthorizer; } /** * Collection for managing User objects */ export declare class UserCollection extends SmrtCollection { static readonly _itemClass: typeof User; private userEmailKeysReadyPromise; /** * Find user by email address */ findByEmail(email: string): Promise; /** * Find user by profile ID */ findByProfile(profileId: string): Promise; /** * Find users by status */ findByStatus(status: UserStatus): Promise; /** * Find all active users */ findActive(): Promise; /** * Find all pending users */ findPending(): Promise; /** * Get or create user for a profile */ getOrCreateForProfile(profileId: string, email: string, defaults?: Partial<{ status: UserStatus; }>): Promise; /** * Get or create user from OIDC claims * * This is the primary method for resolving identity from an OIDC login. * It handles the full flow: * 1. Find or create Profile from OIDC claims (via smrt-profiles) * 2. Link OidcIdentity to the Profile * 3. Find or create User linked to the Profile * * Direct callers must supply claims from a trusted, already validated token * boundary. OidcLoginService performs discovery, token exchange, issuer and * subject validation, and verified-email source pairing before calling this * collection method. * * @param claims - Trusted OIDC token claims (sub, iss, email, name) * @param provider - Provider name (e.g., 'kanidm', 'keycloak', 'google') * @param options - Login recording, unverified-email compatibility, Profile * resolution, and explicit owner authorization. Hooks run inside the * provisioning transaction, may be retried, and must be idempotent. A * resolver result must still be the unique, global, unowned Person. An owner * authorization must select both the canonical Person and its existing sole * approved User owner. Existing identities can only be confirmed, never * rebound. * @returns User, Profile, OidcIdentity, and whether profile was created * * @example * ```typescript * const userCollection = await UserCollection.create({ db: dbConfig }); * * // In your OIDC callback handler: * const { user, profile } = await userCollection.getOrCreateFromOidc( * { * sub: tokenClaims.sub, * iss: tokenClaims.iss, * email: tokenClaims.email, * name: tokenClaims.name, * }, * 'kanidm' * ); * * // User and profile are now available * // Login was auto-recorded; pass { recordLogin: false } to skip * ``` */ getOrCreateFromOidc(claims: OidcClaims, provider: string, options?: GetOrCreateFromOidcOptions): Promise; private requireProvisioningDatabase; private provisionOidcIdentity; private findProfileOwners; private validateProfileOwnerAuthorization; private rebindOidcProvisioningResult; private findUniqueOidcIdentity; private finishUserProvisioning; private findUsersByNormalizedEmail; /** Require the deploy-time backfill marker before indexed identity reads. */ private ensureUserEmailKeysReady; private checkUserEmailKeysReady; private assertUserEmailKeyCurrent; } export {}; //# sourceMappingURL=UserCollection.d.ts.map