@objectstack/plugin-auth 11.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -286,6 +286,40 @@ interface AuthManagerOptions extends Partial<AuthConfig> {
286
286
  * Reuses better-auth's native hash/verify — no bespoke crypto.
287
287
  */
288
288
  passwordHistoryCount?: number;
289
+ /**
290
+ * ADR-0069 D1 — password expiry (days). When > 0, an authenticated user whose
291
+ * `sys_user.password_changed_at` is older than this is gated out of protected
292
+ * resources (`PASSWORD_EXPIRED`) until they change their password. Computed in
293
+ * `customSession` (→ `user.authGate`) and enforced at the transport seam. 0 =
294
+ * off. A null `password_changed_at` never expires (existing users on upgrade).
295
+ */
296
+ passwordExpiryDays?: number;
297
+ /**
298
+ * ADR-0069 D3 — enforced MFA. When true, an authenticated user without TOTP
299
+ * enrolled (`sys_user.two_factor_enabled`) is gated out of protected resources
300
+ * (`MFA_REQUIRED`) once their grace window elapses, until they enroll. Shares
301
+ * the `customSession` → `user.authGate` seam with password expiry.
302
+ */
303
+ mfaRequired?: boolean;
304
+ /** Days a user may defer MFA enrollment before the hard block. Default 7. */
305
+ mfaGracePeriodDays?: number;
306
+ /**
307
+ * ADR-0069 D4 — session controls. Enforced in `customSession` (idle/absolute)
308
+ * and the sign-in hook (concurrent). 0 = off for each. A revoked session is
309
+ * expired in place (`sys_session.expires_at` past + `revoked_at`/`revoke_reason`)
310
+ * so better-auth returns no session on the next request (→ 401 → re-login).
311
+ */
312
+ sessionIdleTimeoutMinutes?: number;
313
+ sessionAbsoluteMaxHours?: number;
314
+ maxConcurrentSessions?: number;
315
+ /**
316
+ * ADR-0069 D5 — network gating. When non-empty, auth requests (sign-in,
317
+ * session) from a client IP outside these CIDR / exact ranges are rejected
318
+ * with `IP_NOT_ALLOWED` at the auth-route middleware. Requires a trusted proxy
319
+ * to set `x-forwarded-for` / `cf-connecting-ip`; fails OPEN when the client IP
320
+ * can't be determined (so a missing proxy header is a no-op, not a lockout).
321
+ */
322
+ allowedIpRanges?: string[];
289
323
  /**
290
324
  * ADR-0069 D2 — better-auth-native per-IP rate limiting, passed through to
291
325
  * better-auth's core `rateLimit`. The settings bind tightens `customRules`
@@ -294,21 +328,13 @@ interface AuthManagerOptions extends Partial<AuthConfig> {
294
328
  */
295
329
  rateLimit?: BetterAuthOptions['rateLimit'];
296
330
  }
297
- /**
298
- * Authentication Manager
299
- *
300
- * Wraps better-auth and provides authentication services for ObjectStack.
301
- * Supports multiple authentication methods:
302
- * - Email/password
303
- * - OAuth providers (Google, GitHub, etc.)
304
- * - Magic links
305
- * - Two-factor authentication
306
- * - Passkeys
307
- * - Organization/teams
308
- */
331
+ /** ADR-0069 D5 — does `ip` match `range` (IPv4 CIDR `a.b.c.d/n`, or exact IP)? */
332
+ declare function ipMatchesRange(ip: string, range: string): boolean;
309
333
  declare class AuthManager {
310
334
  private auth;
311
335
  private config;
336
+ private _orgMfaCache;
337
+ private _orgMfaRefreshing;
312
338
  /**
313
339
  * Result of the dev-only admin seed (set by `AuthPlugin.maybeSeedDevAdmin`
314
340
  * when it provisions the well-known admin on an empty DB). The `serve`
@@ -501,8 +527,21 @@ declare class AuthManager {
501
527
  * in `buildPlugins()` (`ssoFromEnv ?? pluginConfig.sso ?? false`) so the
502
528
  * advertised capability can never disagree with the actual `/sign-in/sso`
503
529
  * route. `OS_SSO_ENABLED` (when set) wins over the config-file setting.
530
+ * Public so `AuthPlugin` can gate the Setup-nav "SSO Providers" entry on it
531
+ * (captures both self-host `OS_SSO_ENABLED` and the cloud per-env
532
+ * `planAllowsSso` config, since that arrives via `plugins.sso`).
504
533
  */
505
- private isSsoWired;
534
+ isSsoWired(): boolean;
535
+ /**
536
+ * Whether opt-in DNS domain-verification (ADR-0024 ②) is wired — i.e. the
537
+ * `/sso/request-domain-verification` + `/sso/verify-domain` endpoints are
538
+ * mounted (and the hard "domain must be verified to log in" gate is active).
539
+ * Resolved with the EXACT logic `buildPluginList` uses for the `sso()`
540
+ * `domainVerification.enabled` option, so the bridge can return a clear
541
+ * "not enabled for this environment" instead of a bare 404 when off.
542
+ * Implies `isSsoWired()` (the sso plugin must be loaded to honor it).
543
+ */
544
+ isSsoDomainVerificationEnabled(): boolean;
506
545
  /**
507
546
  * Whether enterprise SSO is actually *usable*, not merely wired: the plugin
508
547
  * is on AND at least one `sys_sso_provider` row exists. Per-email domain→IdP
@@ -590,6 +629,33 @@ declare class AuthManager {
590
629
  * `PASSWORD_POLICY_VIOLATION` when fewer than `passwordMinClasses` are used.
591
630
  */
592
631
  private assertPasswordComplexity;
632
+ /**
633
+ * ADR-0069 — is any authentication-policy gate enabled? Cheap, synchronous;
634
+ * lets the transport seams skip session lookups entirely when off (the
635
+ * default), keeping the gate zero-overhead until an admin opts in.
636
+ */
637
+ isAuthGateActive(): boolean;
638
+ /**
639
+ * ADR-0069 — refresh the "any org requires MFA" cache in the background when
640
+ * stale (60s TTL). Fire-and-forget: a brand-new per-org requirement activates
641
+ * the gate on the next request, never blocking this one. No-op when global MFA
642
+ * is already on (the gate is active regardless).
643
+ */
644
+ private refreshOrgMfaCacheIfStale;
645
+ /**
646
+ * ADR-0069 — compute the auth-policy gate posture for a session. Returns an
647
+ * `{ code, message }` when the user is currently blocked (e.g. password
648
+ * expired), else undefined. No-op (and no DB read) when no gate feature is
649
+ * enabled. Fails OPEN on any lookup error — a transient hiccup must never lock
650
+ * a compliant user out.
651
+ */
652
+ private computeAuthGate;
653
+ /**
654
+ * ADR-0069 D1 — stamp `sys_user.password_changed_at = now` after a password is
655
+ * set (sign-up / change / reset). Best-effort; never throws. Written as a Date
656
+ * (never epoch-ms) per ADR-0074.
657
+ */
658
+ private stampPasswordChangedAt;
593
659
  /**
594
660
  * ADR-0069 D1 — parse the bounded `previous_password_hashes` JSON column into
595
661
  * a string[] of hashes, tolerating null / malformed values.
@@ -637,6 +703,27 @@ declare class AuthManager {
637
703
  * engine is wired or the user does not exist.
638
704
  */
639
705
  unlockUser(userId: string): Promise<boolean>;
706
+ /**
707
+ * ADR-0069 D4 — idle / absolute session enforcement, run per request from
708
+ * `customSession`. No-op when both are off. Revokes (expires in place +
709
+ * stamps revoked_at/revoke_reason) when a limit is exceeded so better-auth
710
+ * returns no session on the NEXT request; otherwise touches `last_activity_at`
711
+ * (throttled to once a minute). Best-effort — never throws.
712
+ */
713
+ private enforceSessionControls;
714
+ /**
715
+ * ADR-0069 D4 — concurrent-session cap, run from the sign-in after-hook.
716
+ * Keeps the newest `maxConcurrentSessions` live sessions for the user and
717
+ * revokes the rest (oldest first). No-op when off. Best-effort.
718
+ */
719
+ private enforceConcurrentCap;
720
+ /**
721
+ * ADR-0069 D5 — is `ip` within the configured allow-list? True (allow) when no
722
+ * ranges are configured, OR when the IP can't be determined (fail-open so a
723
+ * misconfigured proxy never locks everyone out — an admin enabling this must
724
+ * ensure forwarded headers are trusted). Supports IPv4 CIDR + exact IPv4/IPv6.
725
+ */
726
+ isClientIpAllowed(ip: string | undefined): boolean;
640
727
  /**
641
728
  * Returns the data engine wired into this auth manager. Used by route
642
729
  * handlers (e.g. bootstrap-status) that need to query identity tables
@@ -696,6 +783,98 @@ interface SetInitialPasswordResult {
696
783
  */
697
784
  declare function runSetInitialPassword(authApi: SetPasswordCapableApi, request: Request): Promise<SetInitialPasswordResult>;
698
785
 
786
+ /**
787
+ * Shared `register-sso-provider` (form) handler.
788
+ *
789
+ * `@better-auth/sso`'s `POST /sso/register` expects the OIDC protocol fields
790
+ * NESTED under `oidcConfig` ({ clientId, clientSecret, discoveryEndpoint,
791
+ * scopes, mapping }). The `sys_sso_provider` `register_sso_provider` UI action
792
+ * collects FLAT form fields (the action param schema has no nested-path
793
+ * support), so posting them straight to `/sso/register` drops
794
+ * clientId/clientSecret at the top level (Zod-stripped) and persists an
795
+ * unusable `oidc_config = null` provider that can never complete a login
796
+ * (ADR-0024).
797
+ *
798
+ * This helper reshapes the flat form body into the nested shape and
799
+ * RE-DISPATCHES it through the real `/sso/register` endpoint (via the
800
+ * better-auth universal handler passed in) so the admin gate, the
801
+ * public-routable `trustedOrigins` allowance, discovery hydration, and secret
802
+ * handling all still run — no logic is duplicated. It is the single source of
803
+ * truth for the two mount points that must stay in lockstep: the full
804
+ * `AuthPlugin` (self-host / OSS host kernel) and the cloud `AuthProxyPlugin`
805
+ * (per-environment runtime) — mirroring `runSetInitialPassword`.
806
+ */
807
+ interface RegisterSsoFormResult {
808
+ /** HTTP status to return to the caller. */
809
+ status: number;
810
+ /** JSON body; mirrors the `{ success, data?, error? }` envelope the client parses. */
811
+ body: {
812
+ success: boolean;
813
+ data?: {
814
+ providerId: string;
815
+ };
816
+ error?: {
817
+ code: string;
818
+ message: string;
819
+ };
820
+ };
821
+ }
822
+ /** A better-auth universal handler: `(request) => Response`. */
823
+ type AuthRequestHandler = (request: Request) => Promise<Response>;
824
+ /**
825
+ * Reshape a flat SSO-provider registration form body and register it.
826
+ *
827
+ * @param handle the better-auth universal handler (`AuthManager.handleRequest`
828
+ * on the host kernel, or the resolved per-env handler in the
829
+ * cloud proxy). Used to re-dispatch the nested body to the real
830
+ * `/sso/register` route so all of its gates run.
831
+ * @param request the raw Web `Request` — its headers carry the caller's session
832
+ * cookie / bearer + Origin; its body carries the flat form
833
+ * fields ({ providerId, issuer, domain, clientId, clientSecret,
834
+ * discoveryEndpoint?, scopes?, mapId?, mapEmail?, mapName? }).
835
+ */
836
+ declare function runRegisterSsoProviderFromForm(handle: AuthRequestHandler, request: Request): Promise<RegisterSsoFormResult>;
837
+ /**
838
+ * ADR-0069 P3 — SAML 2.0 sibling of {@link runRegisterSsoProviderFromForm}.
839
+ *
840
+ * `@better-auth/sso` (samlify-backed) registers a SAML IdP via the SAME
841
+ * `/sso/register` endpoint, with the protocol fields nested under `samlConfig`
842
+ * ({ entryPoint, cert, callbackUrl, identifierFormat? }) instead of `oidcConfig`.
843
+ * The UI action collects FLAT fields; this helper reshapes them, derives the
844
+ * per-provider ACS callback URL (`/sso/saml2/sp/acs/<providerId>`), and
845
+ * re-dispatches through `/sso/register` so the admin gate + provisioning run.
846
+ * Returns the SP ACS + metadata URLs the admin must configure on the IdP.
847
+ */
848
+ declare function runRegisterSamlProviderFromForm(handle: AuthRequestHandler, request: Request): Promise<RegisterSsoFormResult & {
849
+ body: RegisterSsoFormResult['body'] & {
850
+ acsUrl?: string;
851
+ spMetadataUrl?: string;
852
+ };
853
+ }>;
854
+ /**
855
+ * Request a DNS-TXT domain-verification challenge for a registered provider and
856
+ * return the ready-to-paste DNS record (for a one-shot `resultDialog`).
857
+ *
858
+ * Body: `{ providerId, domain? }` (domain only shapes the displayed record name).
859
+ */
860
+ declare function runRequestDomainVerification(handle: AuthRequestHandler, request: Request): Promise<RegisterSsoFormResult & {
861
+ body: RegisterSsoFormResult['body'] & {
862
+ data?: any;
863
+ };
864
+ }>;
865
+ /**
866
+ * Verify a provider's domain ownership (re-checks the DNS-TXT record). Reshapes
867
+ * @better-auth/sso's empty `204` / `502` into a `{ success, data:{ message } }`
868
+ * envelope so the action surfaces a clear toast.
869
+ *
870
+ * Body: `{ providerId }`.
871
+ */
872
+ declare function runVerifyDomain(handle: AuthRequestHandler, request: Request): Promise<RegisterSsoFormResult & {
873
+ body: RegisterSsoFormResult['body'] & {
874
+ data?: any;
875
+ };
876
+ }>;
877
+
699
878
  /**
700
879
  * Mapping from better-auth model names to ObjectStack protocol object names.
701
880
  *
@@ -1478,6 +1657,7 @@ declare const AUTH_SSO_PROVIDER_SCHEMA: {
1478
1657
  readonly samlConfig: "saml_config";
1479
1658
  readonly userId: "user_id";
1480
1659
  readonly organizationId: "organization_id";
1660
+ readonly domainVerified: "domain_verified";
1481
1661
  };
1482
1662
  };
1483
1663
  /**
@@ -1530,4 +1710,4 @@ declare function buildDeviceAuthorizationPluginSchema(): {
1530
1710
  };
1531
1711
  };
1532
1712
 
1533
- export { AUTH_ACCOUNT_CONFIG, AUTH_ADMIN_SESSION_FIELDS, AUTH_ADMIN_USER_FIELDS, AUTH_DEVICE_CODE_SCHEMA, AUTH_INVITATION_SCHEMA, AUTH_JWKS_SCHEMA, AUTH_MEMBER_SCHEMA, AUTH_MODEL_TO_PROTOCOL, AUTH_OAUTH_ACCESS_TOKEN_SCHEMA, AUTH_OAUTH_APPLICATION_SCHEMA, AUTH_OAUTH_CLIENT_SCHEMA, AUTH_OAUTH_CONSENT_SCHEMA, AUTH_OAUTH_REFRESH_TOKEN_SCHEMA, AUTH_ORGANIZATION_SCHEMA, AUTH_ORG_SESSION_FIELDS, AUTH_SCIM_PROVIDER_SCHEMA, AUTH_SESSION_CONFIG, AUTH_SSO_PROVIDER_SCHEMA, AUTH_TEAM_MEMBER_SCHEMA, AUTH_TEAM_SCHEMA, AUTH_TWO_FACTOR_SCHEMA, AUTH_TWO_FACTOR_USER_FIELDS, AUTH_USER_CONFIG, AUTH_VERIFICATION_CONFIG, AuthManager, type AuthManagerOptions, AuthPlugin, type AuthPluginOptions, type SetInitialPasswordResult, type SetPasswordCapableApi, buildAdminPluginSchema, buildDeviceAuthorizationPluginSchema, buildJwtPluginSchema, buildOauthProviderPluginSchema, buildOidcProviderPluginSchema, buildOrganizationPluginSchema, buildTwoFactorPluginSchema, createObjectQLAdapter, createObjectQLAdapterFactory, resolveProtocolName, runSetInitialPassword, withSystemReadContext };
1713
+ export { AUTH_ACCOUNT_CONFIG, AUTH_ADMIN_SESSION_FIELDS, AUTH_ADMIN_USER_FIELDS, AUTH_DEVICE_CODE_SCHEMA, AUTH_INVITATION_SCHEMA, AUTH_JWKS_SCHEMA, AUTH_MEMBER_SCHEMA, AUTH_MODEL_TO_PROTOCOL, AUTH_OAUTH_ACCESS_TOKEN_SCHEMA, AUTH_OAUTH_APPLICATION_SCHEMA, AUTH_OAUTH_CLIENT_SCHEMA, AUTH_OAUTH_CONSENT_SCHEMA, AUTH_OAUTH_REFRESH_TOKEN_SCHEMA, AUTH_ORGANIZATION_SCHEMA, AUTH_ORG_SESSION_FIELDS, AUTH_SCIM_PROVIDER_SCHEMA, AUTH_SESSION_CONFIG, AUTH_SSO_PROVIDER_SCHEMA, AUTH_TEAM_MEMBER_SCHEMA, AUTH_TEAM_SCHEMA, AUTH_TWO_FACTOR_SCHEMA, AUTH_TWO_FACTOR_USER_FIELDS, AUTH_USER_CONFIG, AUTH_VERIFICATION_CONFIG, AuthManager, type AuthManagerOptions, AuthPlugin, type AuthPluginOptions, type AuthRequestHandler, type RegisterSsoFormResult, type SetInitialPasswordResult, type SetPasswordCapableApi, buildAdminPluginSchema, buildDeviceAuthorizationPluginSchema, buildJwtPluginSchema, buildOauthProviderPluginSchema, buildOidcProviderPluginSchema, buildOrganizationPluginSchema, buildTwoFactorPluginSchema, createObjectQLAdapter, createObjectQLAdapterFactory, ipMatchesRange, resolveProtocolName, runRegisterSamlProviderFromForm, runRegisterSsoProviderFromForm, runRequestDomainVerification, runSetInitialPassword, runVerifyDomain, withSystemReadContext };
package/dist/index.d.ts CHANGED
@@ -286,6 +286,40 @@ interface AuthManagerOptions extends Partial<AuthConfig> {
286
286
  * Reuses better-auth's native hash/verify — no bespoke crypto.
287
287
  */
288
288
  passwordHistoryCount?: number;
289
+ /**
290
+ * ADR-0069 D1 — password expiry (days). When > 0, an authenticated user whose
291
+ * `sys_user.password_changed_at` is older than this is gated out of protected
292
+ * resources (`PASSWORD_EXPIRED`) until they change their password. Computed in
293
+ * `customSession` (→ `user.authGate`) and enforced at the transport seam. 0 =
294
+ * off. A null `password_changed_at` never expires (existing users on upgrade).
295
+ */
296
+ passwordExpiryDays?: number;
297
+ /**
298
+ * ADR-0069 D3 — enforced MFA. When true, an authenticated user without TOTP
299
+ * enrolled (`sys_user.two_factor_enabled`) is gated out of protected resources
300
+ * (`MFA_REQUIRED`) once their grace window elapses, until they enroll. Shares
301
+ * the `customSession` → `user.authGate` seam with password expiry.
302
+ */
303
+ mfaRequired?: boolean;
304
+ /** Days a user may defer MFA enrollment before the hard block. Default 7. */
305
+ mfaGracePeriodDays?: number;
306
+ /**
307
+ * ADR-0069 D4 — session controls. Enforced in `customSession` (idle/absolute)
308
+ * and the sign-in hook (concurrent). 0 = off for each. A revoked session is
309
+ * expired in place (`sys_session.expires_at` past + `revoked_at`/`revoke_reason`)
310
+ * so better-auth returns no session on the next request (→ 401 → re-login).
311
+ */
312
+ sessionIdleTimeoutMinutes?: number;
313
+ sessionAbsoluteMaxHours?: number;
314
+ maxConcurrentSessions?: number;
315
+ /**
316
+ * ADR-0069 D5 — network gating. When non-empty, auth requests (sign-in,
317
+ * session) from a client IP outside these CIDR / exact ranges are rejected
318
+ * with `IP_NOT_ALLOWED` at the auth-route middleware. Requires a trusted proxy
319
+ * to set `x-forwarded-for` / `cf-connecting-ip`; fails OPEN when the client IP
320
+ * can't be determined (so a missing proxy header is a no-op, not a lockout).
321
+ */
322
+ allowedIpRanges?: string[];
289
323
  /**
290
324
  * ADR-0069 D2 — better-auth-native per-IP rate limiting, passed through to
291
325
  * better-auth's core `rateLimit`. The settings bind tightens `customRules`
@@ -294,21 +328,13 @@ interface AuthManagerOptions extends Partial<AuthConfig> {
294
328
  */
295
329
  rateLimit?: BetterAuthOptions['rateLimit'];
296
330
  }
297
- /**
298
- * Authentication Manager
299
- *
300
- * Wraps better-auth and provides authentication services for ObjectStack.
301
- * Supports multiple authentication methods:
302
- * - Email/password
303
- * - OAuth providers (Google, GitHub, etc.)
304
- * - Magic links
305
- * - Two-factor authentication
306
- * - Passkeys
307
- * - Organization/teams
308
- */
331
+ /** ADR-0069 D5 — does `ip` match `range` (IPv4 CIDR `a.b.c.d/n`, or exact IP)? */
332
+ declare function ipMatchesRange(ip: string, range: string): boolean;
309
333
  declare class AuthManager {
310
334
  private auth;
311
335
  private config;
336
+ private _orgMfaCache;
337
+ private _orgMfaRefreshing;
312
338
  /**
313
339
  * Result of the dev-only admin seed (set by `AuthPlugin.maybeSeedDevAdmin`
314
340
  * when it provisions the well-known admin on an empty DB). The `serve`
@@ -501,8 +527,21 @@ declare class AuthManager {
501
527
  * in `buildPlugins()` (`ssoFromEnv ?? pluginConfig.sso ?? false`) so the
502
528
  * advertised capability can never disagree with the actual `/sign-in/sso`
503
529
  * route. `OS_SSO_ENABLED` (when set) wins over the config-file setting.
530
+ * Public so `AuthPlugin` can gate the Setup-nav "SSO Providers" entry on it
531
+ * (captures both self-host `OS_SSO_ENABLED` and the cloud per-env
532
+ * `planAllowsSso` config, since that arrives via `plugins.sso`).
504
533
  */
505
- private isSsoWired;
534
+ isSsoWired(): boolean;
535
+ /**
536
+ * Whether opt-in DNS domain-verification (ADR-0024 ②) is wired — i.e. the
537
+ * `/sso/request-domain-verification` + `/sso/verify-domain` endpoints are
538
+ * mounted (and the hard "domain must be verified to log in" gate is active).
539
+ * Resolved with the EXACT logic `buildPluginList` uses for the `sso()`
540
+ * `domainVerification.enabled` option, so the bridge can return a clear
541
+ * "not enabled for this environment" instead of a bare 404 when off.
542
+ * Implies `isSsoWired()` (the sso plugin must be loaded to honor it).
543
+ */
544
+ isSsoDomainVerificationEnabled(): boolean;
506
545
  /**
507
546
  * Whether enterprise SSO is actually *usable*, not merely wired: the plugin
508
547
  * is on AND at least one `sys_sso_provider` row exists. Per-email domain→IdP
@@ -590,6 +629,33 @@ declare class AuthManager {
590
629
  * `PASSWORD_POLICY_VIOLATION` when fewer than `passwordMinClasses` are used.
591
630
  */
592
631
  private assertPasswordComplexity;
632
+ /**
633
+ * ADR-0069 — is any authentication-policy gate enabled? Cheap, synchronous;
634
+ * lets the transport seams skip session lookups entirely when off (the
635
+ * default), keeping the gate zero-overhead until an admin opts in.
636
+ */
637
+ isAuthGateActive(): boolean;
638
+ /**
639
+ * ADR-0069 — refresh the "any org requires MFA" cache in the background when
640
+ * stale (60s TTL). Fire-and-forget: a brand-new per-org requirement activates
641
+ * the gate on the next request, never blocking this one. No-op when global MFA
642
+ * is already on (the gate is active regardless).
643
+ */
644
+ private refreshOrgMfaCacheIfStale;
645
+ /**
646
+ * ADR-0069 — compute the auth-policy gate posture for a session. Returns an
647
+ * `{ code, message }` when the user is currently blocked (e.g. password
648
+ * expired), else undefined. No-op (and no DB read) when no gate feature is
649
+ * enabled. Fails OPEN on any lookup error — a transient hiccup must never lock
650
+ * a compliant user out.
651
+ */
652
+ private computeAuthGate;
653
+ /**
654
+ * ADR-0069 D1 — stamp `sys_user.password_changed_at = now` after a password is
655
+ * set (sign-up / change / reset). Best-effort; never throws. Written as a Date
656
+ * (never epoch-ms) per ADR-0074.
657
+ */
658
+ private stampPasswordChangedAt;
593
659
  /**
594
660
  * ADR-0069 D1 — parse the bounded `previous_password_hashes` JSON column into
595
661
  * a string[] of hashes, tolerating null / malformed values.
@@ -637,6 +703,27 @@ declare class AuthManager {
637
703
  * engine is wired or the user does not exist.
638
704
  */
639
705
  unlockUser(userId: string): Promise<boolean>;
706
+ /**
707
+ * ADR-0069 D4 — idle / absolute session enforcement, run per request from
708
+ * `customSession`. No-op when both are off. Revokes (expires in place +
709
+ * stamps revoked_at/revoke_reason) when a limit is exceeded so better-auth
710
+ * returns no session on the NEXT request; otherwise touches `last_activity_at`
711
+ * (throttled to once a minute). Best-effort — never throws.
712
+ */
713
+ private enforceSessionControls;
714
+ /**
715
+ * ADR-0069 D4 — concurrent-session cap, run from the sign-in after-hook.
716
+ * Keeps the newest `maxConcurrentSessions` live sessions for the user and
717
+ * revokes the rest (oldest first). No-op when off. Best-effort.
718
+ */
719
+ private enforceConcurrentCap;
720
+ /**
721
+ * ADR-0069 D5 — is `ip` within the configured allow-list? True (allow) when no
722
+ * ranges are configured, OR when the IP can't be determined (fail-open so a
723
+ * misconfigured proxy never locks everyone out — an admin enabling this must
724
+ * ensure forwarded headers are trusted). Supports IPv4 CIDR + exact IPv4/IPv6.
725
+ */
726
+ isClientIpAllowed(ip: string | undefined): boolean;
640
727
  /**
641
728
  * Returns the data engine wired into this auth manager. Used by route
642
729
  * handlers (e.g. bootstrap-status) that need to query identity tables
@@ -696,6 +783,98 @@ interface SetInitialPasswordResult {
696
783
  */
697
784
  declare function runSetInitialPassword(authApi: SetPasswordCapableApi, request: Request): Promise<SetInitialPasswordResult>;
698
785
 
786
+ /**
787
+ * Shared `register-sso-provider` (form) handler.
788
+ *
789
+ * `@better-auth/sso`'s `POST /sso/register` expects the OIDC protocol fields
790
+ * NESTED under `oidcConfig` ({ clientId, clientSecret, discoveryEndpoint,
791
+ * scopes, mapping }). The `sys_sso_provider` `register_sso_provider` UI action
792
+ * collects FLAT form fields (the action param schema has no nested-path
793
+ * support), so posting them straight to `/sso/register` drops
794
+ * clientId/clientSecret at the top level (Zod-stripped) and persists an
795
+ * unusable `oidc_config = null` provider that can never complete a login
796
+ * (ADR-0024).
797
+ *
798
+ * This helper reshapes the flat form body into the nested shape and
799
+ * RE-DISPATCHES it through the real `/sso/register` endpoint (via the
800
+ * better-auth universal handler passed in) so the admin gate, the
801
+ * public-routable `trustedOrigins` allowance, discovery hydration, and secret
802
+ * handling all still run — no logic is duplicated. It is the single source of
803
+ * truth for the two mount points that must stay in lockstep: the full
804
+ * `AuthPlugin` (self-host / OSS host kernel) and the cloud `AuthProxyPlugin`
805
+ * (per-environment runtime) — mirroring `runSetInitialPassword`.
806
+ */
807
+ interface RegisterSsoFormResult {
808
+ /** HTTP status to return to the caller. */
809
+ status: number;
810
+ /** JSON body; mirrors the `{ success, data?, error? }` envelope the client parses. */
811
+ body: {
812
+ success: boolean;
813
+ data?: {
814
+ providerId: string;
815
+ };
816
+ error?: {
817
+ code: string;
818
+ message: string;
819
+ };
820
+ };
821
+ }
822
+ /** A better-auth universal handler: `(request) => Response`. */
823
+ type AuthRequestHandler = (request: Request) => Promise<Response>;
824
+ /**
825
+ * Reshape a flat SSO-provider registration form body and register it.
826
+ *
827
+ * @param handle the better-auth universal handler (`AuthManager.handleRequest`
828
+ * on the host kernel, or the resolved per-env handler in the
829
+ * cloud proxy). Used to re-dispatch the nested body to the real
830
+ * `/sso/register` route so all of its gates run.
831
+ * @param request the raw Web `Request` — its headers carry the caller's session
832
+ * cookie / bearer + Origin; its body carries the flat form
833
+ * fields ({ providerId, issuer, domain, clientId, clientSecret,
834
+ * discoveryEndpoint?, scopes?, mapId?, mapEmail?, mapName? }).
835
+ */
836
+ declare function runRegisterSsoProviderFromForm(handle: AuthRequestHandler, request: Request): Promise<RegisterSsoFormResult>;
837
+ /**
838
+ * ADR-0069 P3 — SAML 2.0 sibling of {@link runRegisterSsoProviderFromForm}.
839
+ *
840
+ * `@better-auth/sso` (samlify-backed) registers a SAML IdP via the SAME
841
+ * `/sso/register` endpoint, with the protocol fields nested under `samlConfig`
842
+ * ({ entryPoint, cert, callbackUrl, identifierFormat? }) instead of `oidcConfig`.
843
+ * The UI action collects FLAT fields; this helper reshapes them, derives the
844
+ * per-provider ACS callback URL (`/sso/saml2/sp/acs/<providerId>`), and
845
+ * re-dispatches through `/sso/register` so the admin gate + provisioning run.
846
+ * Returns the SP ACS + metadata URLs the admin must configure on the IdP.
847
+ */
848
+ declare function runRegisterSamlProviderFromForm(handle: AuthRequestHandler, request: Request): Promise<RegisterSsoFormResult & {
849
+ body: RegisterSsoFormResult['body'] & {
850
+ acsUrl?: string;
851
+ spMetadataUrl?: string;
852
+ };
853
+ }>;
854
+ /**
855
+ * Request a DNS-TXT domain-verification challenge for a registered provider and
856
+ * return the ready-to-paste DNS record (for a one-shot `resultDialog`).
857
+ *
858
+ * Body: `{ providerId, domain? }` (domain only shapes the displayed record name).
859
+ */
860
+ declare function runRequestDomainVerification(handle: AuthRequestHandler, request: Request): Promise<RegisterSsoFormResult & {
861
+ body: RegisterSsoFormResult['body'] & {
862
+ data?: any;
863
+ };
864
+ }>;
865
+ /**
866
+ * Verify a provider's domain ownership (re-checks the DNS-TXT record). Reshapes
867
+ * @better-auth/sso's empty `204` / `502` into a `{ success, data:{ message } }`
868
+ * envelope so the action surfaces a clear toast.
869
+ *
870
+ * Body: `{ providerId }`.
871
+ */
872
+ declare function runVerifyDomain(handle: AuthRequestHandler, request: Request): Promise<RegisterSsoFormResult & {
873
+ body: RegisterSsoFormResult['body'] & {
874
+ data?: any;
875
+ };
876
+ }>;
877
+
699
878
  /**
700
879
  * Mapping from better-auth model names to ObjectStack protocol object names.
701
880
  *
@@ -1478,6 +1657,7 @@ declare const AUTH_SSO_PROVIDER_SCHEMA: {
1478
1657
  readonly samlConfig: "saml_config";
1479
1658
  readonly userId: "user_id";
1480
1659
  readonly organizationId: "organization_id";
1660
+ readonly domainVerified: "domain_verified";
1481
1661
  };
1482
1662
  };
1483
1663
  /**
@@ -1530,4 +1710,4 @@ declare function buildDeviceAuthorizationPluginSchema(): {
1530
1710
  };
1531
1711
  };
1532
1712
 
1533
- export { AUTH_ACCOUNT_CONFIG, AUTH_ADMIN_SESSION_FIELDS, AUTH_ADMIN_USER_FIELDS, AUTH_DEVICE_CODE_SCHEMA, AUTH_INVITATION_SCHEMA, AUTH_JWKS_SCHEMA, AUTH_MEMBER_SCHEMA, AUTH_MODEL_TO_PROTOCOL, AUTH_OAUTH_ACCESS_TOKEN_SCHEMA, AUTH_OAUTH_APPLICATION_SCHEMA, AUTH_OAUTH_CLIENT_SCHEMA, AUTH_OAUTH_CONSENT_SCHEMA, AUTH_OAUTH_REFRESH_TOKEN_SCHEMA, AUTH_ORGANIZATION_SCHEMA, AUTH_ORG_SESSION_FIELDS, AUTH_SCIM_PROVIDER_SCHEMA, AUTH_SESSION_CONFIG, AUTH_SSO_PROVIDER_SCHEMA, AUTH_TEAM_MEMBER_SCHEMA, AUTH_TEAM_SCHEMA, AUTH_TWO_FACTOR_SCHEMA, AUTH_TWO_FACTOR_USER_FIELDS, AUTH_USER_CONFIG, AUTH_VERIFICATION_CONFIG, AuthManager, type AuthManagerOptions, AuthPlugin, type AuthPluginOptions, type SetInitialPasswordResult, type SetPasswordCapableApi, buildAdminPluginSchema, buildDeviceAuthorizationPluginSchema, buildJwtPluginSchema, buildOauthProviderPluginSchema, buildOidcProviderPluginSchema, buildOrganizationPluginSchema, buildTwoFactorPluginSchema, createObjectQLAdapter, createObjectQLAdapterFactory, resolveProtocolName, runSetInitialPassword, withSystemReadContext };
1713
+ export { AUTH_ACCOUNT_CONFIG, AUTH_ADMIN_SESSION_FIELDS, AUTH_ADMIN_USER_FIELDS, AUTH_DEVICE_CODE_SCHEMA, AUTH_INVITATION_SCHEMA, AUTH_JWKS_SCHEMA, AUTH_MEMBER_SCHEMA, AUTH_MODEL_TO_PROTOCOL, AUTH_OAUTH_ACCESS_TOKEN_SCHEMA, AUTH_OAUTH_APPLICATION_SCHEMA, AUTH_OAUTH_CLIENT_SCHEMA, AUTH_OAUTH_CONSENT_SCHEMA, AUTH_OAUTH_REFRESH_TOKEN_SCHEMA, AUTH_ORGANIZATION_SCHEMA, AUTH_ORG_SESSION_FIELDS, AUTH_SCIM_PROVIDER_SCHEMA, AUTH_SESSION_CONFIG, AUTH_SSO_PROVIDER_SCHEMA, AUTH_TEAM_MEMBER_SCHEMA, AUTH_TEAM_SCHEMA, AUTH_TWO_FACTOR_SCHEMA, AUTH_TWO_FACTOR_USER_FIELDS, AUTH_USER_CONFIG, AUTH_VERIFICATION_CONFIG, AuthManager, type AuthManagerOptions, AuthPlugin, type AuthPluginOptions, type AuthRequestHandler, type RegisterSsoFormResult, type SetInitialPasswordResult, type SetPasswordCapableApi, buildAdminPluginSchema, buildDeviceAuthorizationPluginSchema, buildJwtPluginSchema, buildOauthProviderPluginSchema, buildOidcProviderPluginSchema, buildOrganizationPluginSchema, buildTwoFactorPluginSchema, createObjectQLAdapter, createObjectQLAdapterFactory, ipMatchesRange, resolveProtocolName, runRegisterSamlProviderFromForm, runRegisterSsoProviderFromForm, runRequestDomainVerification, runSetInitialPassword, runVerifyDomain, withSystemReadContext };