@objectstack/plugin-auth 10.3.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.ts CHANGED
@@ -169,6 +169,34 @@ interface AuthManagerOptions extends Partial<AuthConfig> {
169
169
  * Required for database operations using ObjectQL instead of third-party ORMs
170
170
  */
171
171
  dataEngine?: IDataEngine;
172
+ /**
173
+ * Optional callback invoked AFTER an organization is created via better-auth's
174
+ * `createOrganization` (the org-plugin `afterCreateOrganization` hook). Lets a
175
+ * host stack run org-creation side effects that core `databaseHooks` can't —
176
+ * better-auth's org-plugin models (`organization`/`member`) do NOT fire those.
177
+ * The cloud control plane uses it to provision an org's born-with production
178
+ * environment. Failure-isolated: org creation is never rolled back.
179
+ */
180
+ onOrganizationCreated?: (data: {
181
+ organizationId: string;
182
+ userId?: string;
183
+ name?: string;
184
+ slug?: string;
185
+ }) => void | Promise<void>;
186
+ /**
187
+ * D5.1 — OIDC OP authorization gate (cloud-as-IdP app-assignment).
188
+ * When set, it is called for an AUTHENTICATED subject on
189
+ * `/oauth2/authorize` before an authorization code is issued, with the
190
+ * subject + the requesting `clientId`. Return `false` to DENY (no code).
191
+ * The cloud control plane uses it to require org-membership: a cloud user
192
+ * may only obtain a code for an env client (`project_<envId>`) of an org
193
+ * they belong to. Unset (open editions / self-host, where the OP is not a
194
+ * multi-tenant issuer) = allow. Host is expected to fail CLOSED on error.
195
+ */
196
+ oidcAuthorizeGate?: (params: {
197
+ userId: string;
198
+ clientId: string;
199
+ }) => boolean | Promise<boolean>;
172
200
  /**
173
201
  * Base path for auth routes
174
202
  * Forwarded to better-auth's basePath option so it can match incoming
@@ -231,22 +259,82 @@ interface AuthManagerOptions extends Partial<AuthConfig> {
231
259
  * "create organization" screen.
232
260
  */
233
261
  databaseHooks?: BetterAuthOptions['databaseHooks'];
262
+ /**
263
+ * ADR-0069 D2 — account lockout (anti-brute-force). After this many
264
+ * consecutive failed sign-ins the account is locked for
265
+ * {@link lockoutDurationMinutes}. `0` (default) disables lockout.
266
+ * Enforced per-identity in the `/sign-in/email` before/after hooks
267
+ * (survives IP rotation, unlike the per-IP {@link rateLimit}).
268
+ */
269
+ lockoutThreshold?: number;
270
+ /** Minutes an account stays locked once the threshold is crossed. Default 15. */
271
+ lockoutDurationMinutes?: number;
272
+ /**
273
+ * ADR-0069 D1 — password complexity. When `passwordRequireComplexity` is on,
274
+ * a new password must contain at least `passwordMinClasses` (1-4) of the
275
+ * character classes upper / lower / digit / symbol. Enforced by a validator
276
+ * in the `/sign-up/email`, `/reset-password`, `/change-password` before hook
277
+ * (better-auth only enforces min/max length natively).
278
+ */
279
+ passwordRequireComplexity?: boolean;
280
+ /** Minimum distinct character classes required (1-4). Default 3. */
281
+ passwordMinClasses?: number;
282
+ /**
283
+ * ADR-0069 D1 — password history depth. When > 0, a password change/reset is
284
+ * rejected if the new password matches the current or any of the last
285
+ * `passwordHistoryCount` hashes (`sys_account.previous_password_hashes`).
286
+ * Reuses better-auth's native hash/verify — no bespoke crypto.
287
+ */
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[];
323
+ /**
324
+ * ADR-0069 D2 — better-auth-native per-IP rate limiting, passed through to
325
+ * better-auth's core `rateLimit`. The settings bind tightens `customRules`
326
+ * for the auth endpoints (`/sign-in/email`, `/sign-up/email`,
327
+ * `/reset-password`). Multi-node deployments need a shared `storage`.
328
+ */
329
+ rateLimit?: BetterAuthOptions['rateLimit'];
234
330
  }
235
- /**
236
- * Authentication Manager
237
- *
238
- * Wraps better-auth and provides authentication services for ObjectStack.
239
- * Supports multiple authentication methods:
240
- * - Email/password
241
- * - OAuth providers (Google, GitHub, etc.)
242
- * - Magic links
243
- * - Two-factor authentication
244
- * - Passkeys
245
- * - Organization/teams
246
- */
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;
247
333
  declare class AuthManager {
248
334
  private auth;
249
335
  private config;
336
+ private _orgMfaCache;
337
+ private _orgMfaRefreshing;
250
338
  /**
251
339
  * Result of the dev-only admin seed (set by `AuthPlugin.maybeSeedDevAdmin`
252
340
  * when it provisions the well-known admin on an empty DB). The `serve`
@@ -391,6 +479,16 @@ declare class AuthManager {
391
479
  * sign in with email/password going forward.
392
480
  */
393
481
  getAuthContext(): Promise<any>;
482
+ /**
483
+ * SSO-only ("enforced") login mode: the login UI hides the local password
484
+ * form + self-registration so the team signs in via the IdP only.
485
+ * `OS_AUTH_SSO_ONLY` (when set) wins over the `ssoOnlyMode` config knob —
486
+ * parity with the `disableSignUp` env override — so a deployment can force
487
+ * it regardless of the per-env/config value. Break-glass is preserved: this
488
+ * NEVER disables `emailAndPassword.enabled`; it only forces `disableSignUp`
489
+ * and signals the UI to hide the password form. Generic over the IdP.
490
+ */
491
+ private resolveSsoOnly;
394
492
  getPublicConfig(): {
395
493
  emailPassword: {
396
494
  enabled: boolean;
@@ -417,10 +515,215 @@ declare class AuthManager {
417
515
  organization: boolean;
418
516
  multiOrgEnabled: boolean;
419
517
  oidcProvider: boolean;
518
+ sso: boolean;
519
+ ssoEnforced: boolean;
420
520
  deviceAuthorization: boolean;
421
521
  admin: boolean;
422
522
  };
423
523
  };
524
+ /**
525
+ * Coarse "is the domain-routed `@better-auth/sso` plugin wired" flag.
526
+ * Resolved with the EXACT logic that decides whether the plugin is mounted
527
+ * in `buildPlugins()` (`ssoFromEnv ?? pluginConfig.sso ?? false`) so the
528
+ * advertised capability can never disagree with the actual `/sign-in/sso`
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`).
533
+ */
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;
545
+ /**
546
+ * Whether enterprise SSO is actually *usable*, not merely wired: the plugin
547
+ * is on AND at least one `sys_sso_provider` row exists. Per-email domain→IdP
548
+ * matching still happens at `/sign-in/sso`; this answers the coarser "is
549
+ * there any point showing the SSO button at all", so a freshly-enabled but
550
+ * unconfigured SSO setup doesn't advertise a button that errors for everyone.
551
+ *
552
+ * Fails OPEN to the wired flag when providers can't be counted (no data
553
+ * engine, query error) — a config-introspection hiccup must never make the
554
+ * login page hide a button that genuinely works.
555
+ */
556
+ isSsoUsable(): Promise<boolean>;
557
+ /**
558
+ * Extra `trustedOrigins` entries derived from an external-SSO registration
559
+ * request. For a `POST /sso/register` | `/sso/update-provider`, parse the
560
+ * (cloned) body and return the PUBLIC-ROUTABLE origins of the declared
561
+ * `issuer` / `oidcConfig` endpoints so `@better-auth/sso`'s discovery
562
+ * validation accepts a customer IdP registered at runtime (ADR-0024) without
563
+ * the operator pre-listing it in boot config. Only public-routable hosts are
564
+ * returned — private / internal / loopback hosts are never auto-trusted
565
+ * (better-auth's `isPublicRoutableHost`, the same predicate its own
566
+ * sub-endpoint check uses). Best-effort: any parse error yields `[]`.
567
+ */
568
+ private ssoDiscoveryTrustedOrigins;
569
+ /**
570
+ * Resolve the acting user (+ their active org) for a before-hook gate,
571
+ * hook-order-independent. Tries the standard cookie session first, then falls
572
+ * back to explicit token resolution (bearer or the session cookie's token
573
+ * part) — the bearer plugin may convert `Authorization: Bearer` to a session
574
+ * AFTER this global before-hook runs. Returns `null` when no valid session
575
+ * can be resolved (→ caller lets `sessionMiddleware` issue the 401).
576
+ */
577
+ private resolveActor;
578
+ /**
579
+ * True when `userId` is a platform admin (a `sys_user_permission_set` row
580
+ * pointing at `admin_full_access` with `organization_id = null`) OR an
581
+ * owner/admin member of `activeOrgId` (any org membership with role
582
+ * owner/admin when no active org is set). Mirrors the role-derivation in
583
+ * `customSession`; reads through `withSystemReadContext` so the lookups are
584
+ * not themselves RLS-scoped to the acting (possibly non-privileged) user.
585
+ * Fails CLOSED (returns false) on any lookup error — this backs a security
586
+ * gate, so an unverifiable actor must never pass.
587
+ */
588
+ private isOrgOrPlatformAdmin;
589
+ /**
590
+ * Compose the framework's identity-source stamp (`account.create.after`)
591
+ * with any host-supplied `databaseHooks`, preserving BOTH. The cloud passes
592
+ * `user.create.after` (personal-org provisioning) + `session.create.before`
593
+ * (active-org) — different model/op, so no collision — but if a host ever
594
+ * adds its own `account.create.after` we chain it after the stamp rather
595
+ * than silently dropping one.
596
+ */
597
+ private composeDatabaseHooks;
598
+ /**
599
+ * Maintain `sys_user.source` (ADR-0024 D4 provenance) as accounts are linked.
600
+ * Drives the managed-vs-native user-mgmt gating: a managed (`idp-provisioned`)
601
+ * user holds no local credential, so the password / identity-edit actions
602
+ * hide for them — preventing a managed user from self-minting a local
603
+ * password that would bypass enforced SSO.
604
+ *
605
+ * Two cases, both break-glass safe and idempotent (only writes on a real
606
+ * change, so trackHistory stays quiet):
607
+ *
608
+ * • A **federated** account (any non-`credential` provider — the cloud-as-IdP
609
+ * `objectstack-cloud` provider OR a customer's own OIDC/SAML IdP) is
610
+ * linked AND the user holds NO local credential → mark `idp-provisioned`.
611
+ * A user who already has a `credential` account (an env-native user who
612
+ * linked SSO) is left `env-native` — they keep a usable password.
613
+ *
614
+ * • A **credential** account is created (local signup, or the break-glass
615
+ * owner's password set via set-initial-password — which can land AFTER the
616
+ * first SSO link) → ensure `env-native`. This flips a previously-stamped
617
+ * owner back, so the break-glass admin never loses self-service password
618
+ * management.
619
+ *
620
+ * Best-effort: any failure leaves the prior value (the gate fails open — a
621
+ * managed user might transiently show a password action that simply errors —
622
+ * never a hard login failure).
623
+ */
624
+ private stampIdentitySource;
625
+ /**
626
+ * ADR-0069 D1 — reject a password that doesn't meet the configured character-
627
+ * class complexity. No-op when `passwordRequireComplexity` is off. Counts the
628
+ * four classes (upper / lower / digit / symbol) present and throws
629
+ * `PASSWORD_POLICY_VIOLATION` when fewer than `passwordMinClasses` are used.
630
+ */
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;
659
+ /**
660
+ * ADR-0069 D1 — parse the bounded `previous_password_hashes` JSON column into
661
+ * a string[] of hashes, tolerating null / malformed values.
662
+ */
663
+ private parseHashes;
664
+ /**
665
+ * ADR-0069 D1 — resolve the user whose password is being changed. For
666
+ * `/change-password` the caller is authenticated (session); for
667
+ * `/reset-password` the user is carried by the reset token's verification
668
+ * value (the same lookup better-auth's own handler uses).
669
+ */
670
+ private resolvePasswordChangeUserId;
671
+ /**
672
+ * ADR-0069 D1 — throw `PASSWORD_REUSE` when `candidate` matches the user's
673
+ * current password or any hash in the bounded history. Reuses better-auth's
674
+ * native `password.verify` (passed in) rather than re-hashing. Returns the
675
+ * current hash (for the after-hook to append) when the candidate is fresh, or
676
+ * undefined when the feature is off / nothing to compare.
677
+ */
678
+ private assertPasswordNotReused;
679
+ /**
680
+ * ADR-0069 D1 — append `oldHash` to the bounded password-history ring after a
681
+ * successful change/reset. Best-effort; never throws.
682
+ */
683
+ private recordPasswordHistory;
684
+ /**
685
+ * ADR-0069 D2 — throw `ACCOUNT_LOCKED` when the identity is currently locked
686
+ * out (brute-force protection). No-op when lockout is disabled
687
+ * (`lockoutThreshold <= 0`) or no data engine is wired. Fails OPEN on a
688
+ * lookup error: an infra hiccup must never block every login.
689
+ */
690
+ private assertAccountNotLocked;
691
+ /**
692
+ * ADR-0069 D2 — record a sign-in outcome for lockout accounting. On failure
693
+ * increments `failed_login_count` and, once it reaches `lockoutThreshold`,
694
+ * stamps `locked_until = now + lockoutDurationMinutes`. On success resets
695
+ * both (only writing when there is something to clear, to avoid a no-op
696
+ * history row on every login). No-op when lockout is disabled. Never throws —
697
+ * a counter write must not turn a valid login into an error.
698
+ */
699
+ private recordSignInOutcome;
700
+ /**
701
+ * ADR-0069 D2 — clear a user's lockout state (admin "Unlock" action).
702
+ * Resets `failed_login_count` and `locked_until`. Returns false when no data
703
+ * engine is wired or the user does not exist.
704
+ */
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;
424
727
  /**
425
728
  * Returns the data engine wired into this auth manager. Used by route
426
729
  * handlers (e.g. bootstrap-status) that need to query identity tables
@@ -480,6 +783,98 @@ interface SetInitialPasswordResult {
480
783
  */
481
784
  declare function runSetInitialPassword(authApi: SetPasswordCapableApi, request: Request): Promise<SetInitialPasswordResult>;
482
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
+
483
878
  /**
484
879
  * Mapping from better-auth model names to ObjectStack protocol object names.
485
880
  *
@@ -492,6 +887,18 @@ declare const AUTH_MODEL_TO_PROTOCOL: Record<string, string>;
492
887
  * Falls back to the original model name for custom / non-core models.
493
888
  */
494
889
  declare function resolveProtocolName(model: string): string;
890
+ /**
891
+ * Wrap a data engine so its READ operations (find / findOne / count) run as
892
+ * SYSTEM reads — injecting `context.isSystem: true` (merged; any caller-supplied
893
+ * context still wins on other keys). better-auth has already authenticated the
894
+ * session and scopes every query by its OWN where-clauses (e.g. member.userId =
895
+ * session.user). A deployment's control-plane org-scope read hook, however, keys
896
+ * off the CALLER's user id, and these adapter reads carry no caller context — so
897
+ * without isSystem that hook filters sys_member / sys_organization reads down to
898
+ * zero and `organization.list()` returns no orgs for a real member. Writes pass
899
+ * through untouched (org-scope is a read-only hook).
900
+ */
901
+ declare function withSystemReadContext(engine: IDataEngine): IDataEngine;
495
902
  /**
496
903
  * Create an ObjectQL adapter **factory** for better-auth.
497
904
  *
@@ -508,7 +915,7 @@ declare function resolveProtocolName(model: string): string;
508
915
  * @param dataEngine - ObjectQL data engine instance
509
916
  * @returns better-auth AdapterFactory
510
917
  */
511
- declare function createObjectQLAdapterFactory(dataEngine: IDataEngine): better_auth_adapters.AdapterFactory<better_auth.BetterAuthOptions>;
918
+ declare function createObjectQLAdapterFactory(rawDataEngine: IDataEngine): better_auth_adapters.AdapterFactory<better_auth.BetterAuthOptions>;
512
919
  /**
513
920
  * Create a raw ObjectQL adapter for better-auth (without factory wrapping).
514
921
  *
@@ -523,7 +930,7 @@ declare function createObjectQLAdapterFactory(dataEngine: IDataEngine): better_a
523
930
  * @param dataEngine - ObjectQL data engine instance
524
931
  * @returns better-auth CustomAdapter (raw, without factory wrapping)
525
932
  */
526
- declare function createObjectQLAdapter(dataEngine: IDataEngine): {
933
+ declare function createObjectQLAdapter(rawDataEngine: IDataEngine): {
527
934
  create: <T extends Record<string, any>>({ model, data, select: _select }: {
528
935
  model: string;
529
936
  data: T;
@@ -1225,6 +1632,60 @@ declare function buildOauthProviderPluginSchema(): {
1225
1632
  * from the deprecated `better-auth/plugins/oidc-provider` plugin.
1226
1633
  */
1227
1634
  declare const buildOidcProviderPluginSchema: typeof buildOauthProviderPluginSchema;
1635
+ /**
1636
+ * `@better-auth/sso` plugin `ssoProvider` model mapping.
1637
+ *
1638
+ * Each row is an external OIDC/SAML IdP this environment federates login to
1639
+ * (the relying-party side — ADR-0024's OPEN per-env SSO mechanism). The
1640
+ * protocol detail lives in JSON blobs (`oidcConfig` / `samlConfig`); the model
1641
+ * itself is thin. Mirrors @better-auth/sso@1.6.20's `BaseSSOProvider`.
1642
+ *
1643
+ * | camelCase (better-auth) | snake_case (ObjectStack) |
1644
+ * |:------------------------|:-------------------------|
1645
+ * | providerId | provider_id |
1646
+ * | oidcConfig | oidc_config |
1647
+ * | samlConfig | saml_config |
1648
+ * | userId | user_id |
1649
+ * | organizationId | organization_id |
1650
+ * | issuer / domain | (same name — no remap) |
1651
+ */
1652
+ declare const AUTH_SSO_PROVIDER_SCHEMA: {
1653
+ readonly modelName: "sys_sso_provider";
1654
+ readonly fields: {
1655
+ readonly providerId: "provider_id";
1656
+ readonly oidcConfig: "oidc_config";
1657
+ readonly samlConfig: "saml_config";
1658
+ readonly userId: "user_id";
1659
+ readonly organizationId: "organization_id";
1660
+ readonly domainVerified: "domain_verified";
1661
+ };
1662
+ };
1663
+ /**
1664
+ * `@better-auth/scim` plugin `scimProvider` model mapping.
1665
+ *
1666
+ * Each row is a SCIM connection: a bearer token an external IdP (Okta / Entra)
1667
+ * uses to auto-provision / deprovision THIS environment's users — the env is
1668
+ * the SCIM Service Provider (ADR-0071). Like `@better-auth/sso`, the plugin
1669
+ * hardcodes its model and exposes NO `schema` option, so the mapping is
1670
+ * consumed at the ADAPTER layer (AUTH_MODEL_TO_PROTOCOL + field resolution in
1671
+ * objectql-adapter.ts), NOT handed to the plugin.
1672
+ *
1673
+ * | camelCase (better-auth) | snake_case (ObjectStack) |
1674
+ * |:------------------------|:-------------------------|
1675
+ * | providerId | provider_id |
1676
+ * | scimToken | scim_token |
1677
+ * | organizationId | organization_id |
1678
+ * | userId | user_id |
1679
+ */
1680
+ declare const AUTH_SCIM_PROVIDER_SCHEMA: {
1681
+ readonly modelName: "sys_scim_provider";
1682
+ readonly fields: {
1683
+ readonly providerId: "provider_id";
1684
+ readonly scimToken: "scim_token";
1685
+ readonly organizationId: "organization_id";
1686
+ readonly userId: "user_id";
1687
+ };
1688
+ };
1228
1689
  /**
1229
1690
  * Builds the `schema` option for better-auth's `deviceAuthorization()` plugin.
1230
1691
  *
@@ -1249,4 +1710,4 @@ declare function buildDeviceAuthorizationPluginSchema(): {
1249
1710
  };
1250
1711
  };
1251
1712
 
1252
- 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_SESSION_CONFIG, 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 };
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 };