@objectstack/plugin-auth 11.0.0 → 11.2.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.mjs CHANGED
@@ -522,7 +522,13 @@ var AUTH_SSO_PROVIDER_SCHEMA = {
522
522
  oidcConfig: "oidc_config",
523
523
  samlConfig: "saml_config",
524
524
  userId: "user_id",
525
- organizationId: "organization_id"
525
+ organizationId: "organization_id",
526
+ // DNS domain-ownership proof (ADR-0024 ②). @better-auth/sso writes
527
+ // `domainVerified` on its `ssoProvider` model when domain verification is
528
+ // enabled; map it so the env can surface a verified/unverified badge. The
529
+ // one-time `domainVerificationToken` is NOT a provider column — it lives in
530
+ // the verification table and is returned only from request-domain-verification.
531
+ domainVerified: "domain_verified"
526
532
  }
527
533
  };
528
534
  var AUTH_SCIM_PROVIDER_SCHEMA = {
@@ -601,9 +607,36 @@ function readDisableSignUpEnv() {
601
607
  function readSsoOnlyEnv() {
602
608
  return readBooleanEnv("OS_AUTH_SSO_ONLY");
603
609
  }
610
+ function ipv4ToInt(ip) {
611
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip.trim());
612
+ if (!m) return null;
613
+ const p = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
614
+ if (p.some((n) => n > 255)) return null;
615
+ return (p[0] << 24 >>> 0) + (p[1] << 16) + (p[2] << 8) + p[3] >>> 0;
616
+ }
617
+ function ipMatchesRange(ip, range) {
618
+ const r = (range || "").trim();
619
+ if (!r) return false;
620
+ if (r.includes("/")) {
621
+ const [base, bitsStr] = r.split("/");
622
+ const bits = Number(bitsStr);
623
+ const ipInt = ipv4ToInt(ip);
624
+ const baseInt = ipv4ToInt(base);
625
+ if (ipInt === null || baseInt === null || !(bits >= 0 && bits <= 32)) {
626
+ return ip.trim() === base.trim();
627
+ }
628
+ const mask = bits === 0 ? 0 : ~0 << 32 - bits >>> 0;
629
+ return (ipInt & mask) >>> 0 === (baseInt & mask) >>> 0;
630
+ }
631
+ return ip.trim() === r;
632
+ }
604
633
  var AuthManager = class {
605
634
  constructor(config) {
606
635
  this.auth = null;
636
+ // ADR-0069 — cached "does any org require MFA" flag (per-org tightening).
637
+ // Refreshed lazily with a TTL so isAuthGateActive() stays synchronous + cheap.
638
+ this._orgMfaCache = { value: false, at: 0 };
639
+ this._orgMfaRefreshing = false;
607
640
  this.config = config;
608
641
  installWebContainerRequestStatePolyfill();
609
642
  if (config.authInstance) {
@@ -808,6 +841,7 @@ var AuthManager = class {
808
841
  if (candidate && (ctx?.path === "/reset-password" || ctx?.path === "/change-password")) {
809
842
  const userId = await this.resolvePasswordChangeUserId(ctx).catch(() => void 0);
810
843
  if (userId) {
844
+ ctx.context.__osPwChangeUserId = userId;
811
845
  const pw = ctx?.context?.password;
812
846
  const verify = typeof pw?.verify === "function" ? pw.verify.bind(pw) : void 0;
813
847
  const oldHash = await this.assertPasswordNotReused(userId, candidate, verify);
@@ -947,25 +981,43 @@ var AuthManager = class {
947
981
  succeeded = !(ctx?.context?.returned instanceof Error);
948
982
  }
949
983
  await this.recordSignInOutcome(email, succeeded);
984
+ if (succeeded) {
985
+ const uid = ctx?.context?.returned?.user?.id;
986
+ if (typeof uid === "string") await this.enforceConcurrentCap(uid);
987
+ }
950
988
  }
951
989
  return;
952
990
  }
953
991
  if (ctx?.path === "/change-password" || ctx?.path === "/reset-password") {
954
- const stash = ctx?.context?.__osPwHistory;
955
- if (stash?.userId) {
956
- let succeeded = true;
957
- try {
958
- const { isAPIError } = await import("better-auth/api");
959
- succeeded = !isAPIError(ctx?.context?.returned);
960
- } catch {
961
- succeeded = !(ctx?.context?.returned instanceof Error);
962
- }
963
- if (succeeded) await this.recordPasswordHistory(stash.userId, stash.oldHash);
964
- delete ctx.context.__osPwHistory;
992
+ let succeeded;
993
+ try {
994
+ const { isAPIError } = await import("better-auth/api");
995
+ succeeded = !isAPIError(ctx?.context?.returned);
996
+ } catch {
997
+ succeeded = !(ctx?.context?.returned instanceof Error);
965
998
  }
999
+ if (succeeded) {
1000
+ const stampId = ctx?.context?.__osPwChangeUserId;
1001
+ if (stampId) await this.stampPasswordChangedAt(stampId);
1002
+ const stash = ctx?.context?.__osPwHistory;
1003
+ if (stash?.userId) await this.recordPasswordHistory(stash.userId, stash.oldHash);
1004
+ }
1005
+ delete ctx.context.__osPwChangeUserId;
1006
+ delete ctx.context.__osPwHistory;
966
1007
  return;
967
1008
  }
968
1009
  if (ctx?.path !== "/sign-up/email") return;
1010
+ {
1011
+ const newUserId = ctx?.context?.returned?.user?.id;
1012
+ let signupOk;
1013
+ try {
1014
+ const { isAPIError } = await import("better-auth/api");
1015
+ signupOk = !isAPIError(ctx?.context?.returned);
1016
+ } catch {
1017
+ signupOk = !(ctx?.context?.returned instanceof Error);
1018
+ }
1019
+ if (signupOk && typeof newUserId === "string") await this.stampPasswordChangedAt(newUserId);
1020
+ }
969
1021
  const ep = ctx?.context?.options?.emailAndPassword;
970
1022
  if (ep && ctx.context.__osDisableSignUpOrig !== void 0) {
971
1023
  ep.disableSignUp = ctx.context.__osDisableSignUpOrig;
@@ -977,7 +1029,7 @@ var AuthManager = class {
977
1029
  // Auto-includes origins from OS_CORS_ORIGIN env var so CORS and CSRF stay in sync.
978
1030
  ...(() => {
979
1031
  const origins = [...this.config.trustedOrigins || []];
980
- const corsOrigin = readEnvWithDeprecation("OS_CORS_ORIGIN", "CORS_ORIGIN");
1032
+ const corsOrigin = readEnvWithDeprecation("OS_CORS_ORIGIN", "CORS_ORIGIN", { silent: true });
981
1033
  if (corsOrigin && corsOrigin !== "*") {
982
1034
  corsOrigin.split(",").map((s) => s.trim()).filter(Boolean).forEach((o) => {
983
1035
  if (!origins.includes(o)) origins.push(o);
@@ -1077,12 +1129,10 @@ var AuthManager = class {
1077
1129
  async buildPluginList() {
1078
1130
  const pluginConfig = this.config.plugins ?? {};
1079
1131
  const plugins = [];
1080
- const oidcEnv = globalThis?.process?.env?.OS_OIDC_PROVIDER_ENABLED;
1081
- const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === "true" : void 0;
1082
- const ssoEnv = globalThis?.process?.env?.OS_SSO_ENABLED;
1083
- const ssoFromEnv = ssoEnv != null ? String(ssoEnv).toLowerCase() === "true" : void 0;
1084
- const scimEnv = globalThis?.process?.env?.OS_SCIM_ENABLED;
1085
- const scimFromEnv = scimEnv != null ? String(scimEnv).toLowerCase() === "true" : void 0;
1132
+ const oidcFromEnv = readBooleanEnv("OS_OIDC_PROVIDER_ENABLED");
1133
+ const ssoFromEnv = readBooleanEnv("OS_SSO_ENABLED");
1134
+ const scimFromEnv = readBooleanEnv("OS_SCIM_ENABLED");
1135
+ const ssoDomainVerifyFromEnv = readBooleanEnv("OS_SSO_DOMAIN_VERIFICATION");
1086
1136
  const scimEffective = scimFromEnv ?? pluginConfig.scim ?? false;
1087
1137
  const twoFactorFromEnv = readBooleanEnv("OS_AUTH_TWO_FACTOR");
1088
1138
  const hibpFromEnv = readBooleanEnv("OS_AUTH_PASSWORD_REJECT_BREACHED");
@@ -1096,6 +1146,7 @@ var AuthManager = class {
1096
1146
  deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
1097
1147
  admin: pluginConfig.admin ?? scimEffective,
1098
1148
  sso: ssoFromEnv ?? pluginConfig.sso ?? false,
1149
+ ssoDomainVerification: ssoDomainVerifyFromEnv ?? pluginConfig.ssoDomainVerification ?? false,
1099
1150
  scim: scimEffective
1100
1151
  };
1101
1152
  const { bearer } = await import("better-auth/plugins/bearer");
@@ -1179,9 +1230,8 @@ var AuthManager = class {
1179
1230
  // The plugin itself is always installed (so list/update/invite endpoints
1180
1231
  // keep responding); only the `create` operation is denied when the
1181
1232
  // deployment is provisioned in single-org mode. Resolution order:
1182
- // 1. explicit `OS_MULTI_ORG_ENABLED` (wins for backwards compat),
1183
- // 2. else `OS_MULTI_TENANT` (multi-tenant deployments are always
1184
- // multi-org), default `'false'` → single-org / per-env runtime.
1233
+ // `OS_MULTI_ORG_ENABLED` (default `'false'` single-org /
1234
+ // per-env runtime).
1185
1235
  beforeCreateOrganization: async () => {
1186
1236
  if (!resolveMultiOrgEnabled()) {
1187
1237
  const { APIError } = await import("better-auth/api");
@@ -1362,7 +1412,8 @@ var AuthManager = class {
1362
1412
  if (enabled.sso) {
1363
1413
  const { sso } = await import("@better-auth/sso");
1364
1414
  plugins.push(sso({
1365
- organizationProvisioning: { defaultRole: "member" }
1415
+ organizationProvisioning: { defaultRole: "member" },
1416
+ ...enabled.ssoDomainVerification ? { domainVerification: { enabled: true } } : {}
1366
1417
  }));
1367
1418
  }
1368
1419
  if (enabled.scim) {
@@ -1434,7 +1485,16 @@ var AuthManager = class {
1434
1485
  ...orgRoles,
1435
1486
  ...platformAdmin ? [BUILTIN_ROLE_PLATFORM_ADMIN] : []
1436
1487
  ]));
1437
- return { user: { ...user, roles, isPlatformAdmin: platformAdmin }, session };
1488
+ await this.enforceSessionControls(session?.id, session?.createdAt);
1489
+ const authGate = await this.computeAuthGate(
1490
+ user.id,
1491
+ session?.activeOrganizationId,
1492
+ user?.twoFactorEnabled === true
1493
+ );
1494
+ return {
1495
+ user: { ...user, roles, isPlatformAdmin: platformAdmin, ...authGate ? { authGate } : {} },
1496
+ session
1497
+ };
1438
1498
  }));
1439
1499
  }
1440
1500
  return plugins;
@@ -1464,7 +1524,7 @@ var AuthManager = class {
1464
1524
  * Generate a secure secret if not provided
1465
1525
  */
1466
1526
  generateSecret() {
1467
- const envSecret = readEnvWithDeprecation("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]);
1527
+ const envSecret = readEnvWithDeprecation("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"], { silent: true });
1468
1528
  if (envSecret) return envSecret;
1469
1529
  if (process.env.NODE_ENV === "production") {
1470
1530
  throw new Error(
@@ -1739,12 +1799,28 @@ var AuthManager = class {
1739
1799
  * in `buildPlugins()` (`ssoFromEnv ?? pluginConfig.sso ?? false`) so the
1740
1800
  * advertised capability can never disagree with the actual `/sign-in/sso`
1741
1801
  * route. `OS_SSO_ENABLED` (when set) wins over the config-file setting.
1802
+ * Public so `AuthPlugin` can gate the Setup-nav "SSO Providers" entry on it
1803
+ * (captures both self-host `OS_SSO_ENABLED` and the cloud per-env
1804
+ * `planAllowsSso` config, since that arrives via `plugins.sso`).
1742
1805
  */
1743
1806
  isSsoWired() {
1744
- const ssoEnv = globalThis?.process?.env?.OS_SSO_ENABLED;
1745
- const ssoFromEnv = ssoEnv != null ? String(ssoEnv).toLowerCase() === "true" : void 0;
1807
+ const ssoFromEnv = readBooleanEnv("OS_SSO_ENABLED");
1746
1808
  return ssoFromEnv ?? this.config.plugins?.sso ?? false;
1747
1809
  }
1810
+ /**
1811
+ * Whether opt-in DNS domain-verification (ADR-0024 ②) is wired — i.e. the
1812
+ * `/sso/request-domain-verification` + `/sso/verify-domain` endpoints are
1813
+ * mounted (and the hard "domain must be verified to log in" gate is active).
1814
+ * Resolved with the EXACT logic `buildPluginList` uses for the `sso()`
1815
+ * `domainVerification.enabled` option, so the bridge can return a clear
1816
+ * "not enabled for this environment" instead of a bare 404 when off.
1817
+ * Implies `isSsoWired()` (the sso plugin must be loaded to honor it).
1818
+ */
1819
+ isSsoDomainVerificationEnabled() {
1820
+ if (!this.isSsoWired()) return false;
1821
+ const fromEnv = readBooleanEnv("OS_SSO_DOMAIN_VERIFICATION");
1822
+ return fromEnv ?? this.config.plugins?.ssoDomainVerification ?? false;
1823
+ }
1748
1824
  /**
1749
1825
  * Whether enterprise SSO is actually *usable*, not merely wired: the plugin
1750
1826
  * is on AND at least one `sys_sso_provider` row exists. Per-email domain→IdP
@@ -2004,6 +2080,119 @@ var AuthManager = class {
2004
2080
  });
2005
2081
  }
2006
2082
  }
2083
+ /**
2084
+ * ADR-0069 — is any authentication-policy gate enabled? Cheap, synchronous;
2085
+ * lets the transport seams skip session lookups entirely when off (the
2086
+ * default), keeping the gate zero-overhead until an admin opts in.
2087
+ */
2088
+ isAuthGateActive() {
2089
+ this.refreshOrgMfaCacheIfStale();
2090
+ return Math.floor(Number(this.config.passwordExpiryDays) || 0) > 0 || this.config.mfaRequired === true || this._orgMfaCache.value;
2091
+ }
2092
+ /**
2093
+ * ADR-0069 — refresh the "any org requires MFA" cache in the background when
2094
+ * stale (60s TTL). Fire-and-forget: a brand-new per-org requirement activates
2095
+ * the gate on the next request, never blocking this one. No-op when global MFA
2096
+ * is already on (the gate is active regardless).
2097
+ */
2098
+ refreshOrgMfaCacheIfStale() {
2099
+ if (this.config.mfaRequired === true) return;
2100
+ if (this._orgMfaRefreshing) return;
2101
+ if (Date.now() - this._orgMfaCache.at < 6e4) return;
2102
+ const engine = this.getDataEngine();
2103
+ if (!engine) return;
2104
+ this._orgMfaRefreshing = true;
2105
+ void (async () => {
2106
+ try {
2107
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2108
+ const n = await engine.count("sys_organization", {
2109
+ where: { require_mfa: true },
2110
+ context: SYSTEM_CTX
2111
+ });
2112
+ this._orgMfaCache = { value: typeof n === "number" && n > 0, at: Date.now() };
2113
+ } catch {
2114
+ } finally {
2115
+ this._orgMfaRefreshing = false;
2116
+ }
2117
+ })();
2118
+ }
2119
+ /**
2120
+ * ADR-0069 — compute the auth-policy gate posture for a session. Returns an
2121
+ * `{ code, message }` when the user is currently blocked (e.g. password
2122
+ * expired), else undefined. No-op (and no DB read) when no gate feature is
2123
+ * enabled. Fails OPEN on any lookup error — a transient hiccup must never lock
2124
+ * a compliant user out.
2125
+ */
2126
+ async computeAuthGate(userId, _activeOrgId, _twoFactorEnabledHint) {
2127
+ const expiryDays = Math.floor(Number(this.config.passwordExpiryDays) || 0);
2128
+ const mfaGlobal = this.config.mfaRequired === true;
2129
+ const orgMaybeRequires = !mfaGlobal && !!_activeOrgId && this._orgMfaCache.value;
2130
+ if (expiryDays <= 0 && !mfaGlobal && !orgMaybeRequires) return void 0;
2131
+ const engine = this.getDataEngine();
2132
+ if (!engine || !userId) return void 0;
2133
+ try {
2134
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2135
+ const u = await engine.findOne("sys_user", {
2136
+ where: { id: userId },
2137
+ fields: ["password_changed_at", "two_factor_enabled", "mfa_required_at"],
2138
+ context: SYSTEM_CTX
2139
+ });
2140
+ let mfaRequired = mfaGlobal;
2141
+ if (!mfaRequired && orgMaybeRequires) {
2142
+ const org = await engine.findOne("sys_organization", {
2143
+ where: { id: _activeOrgId },
2144
+ fields: ["require_mfa"],
2145
+ context: SYSTEM_CTX
2146
+ });
2147
+ mfaRequired = org?.require_mfa === true || org?.require_mfa === 1;
2148
+ }
2149
+ if (expiryDays > 0) {
2150
+ const changed = u?.password_changed_at;
2151
+ if (changed && Date.now() - new Date(changed).getTime() > expiryDays * 864e5) {
2152
+ return {
2153
+ code: "PASSWORD_EXPIRED",
2154
+ message: "Your password has expired. Please change it to continue."
2155
+ };
2156
+ }
2157
+ }
2158
+ if (mfaRequired && !(u?.two_factor_enabled === true || u?.two_factor_enabled === 1)) {
2159
+ const graceDays = Math.max(0, Math.floor(Number(this.config.mfaGracePeriodDays ?? 7)));
2160
+ let requiredAt = u?.mfa_required_at;
2161
+ if (!requiredAt) {
2162
+ requiredAt = /* @__PURE__ */ new Date();
2163
+ engine.update("sys_user", { id: userId, mfa_required_at: requiredAt }, { context: SYSTEM_CTX }).catch(() => void 0);
2164
+ }
2165
+ const elapsedMs = Date.now() - new Date(requiredAt).getTime();
2166
+ if (elapsedMs > graceDays * 864e5) {
2167
+ return {
2168
+ code: "MFA_REQUIRED",
2169
+ message: "Multi-factor authentication is required. Please set up an authenticator app to continue."
2170
+ };
2171
+ }
2172
+ }
2173
+ } catch {
2174
+ return void 0;
2175
+ }
2176
+ return void 0;
2177
+ }
2178
+ /**
2179
+ * ADR-0069 D1 — stamp `sys_user.password_changed_at = now` after a password is
2180
+ * set (sign-up / change / reset). Best-effort; never throws. Written as a Date
2181
+ * (never epoch-ms) per ADR-0074.
2182
+ */
2183
+ async stampPasswordChangedAt(userId) {
2184
+ const engine = this.getDataEngine();
2185
+ if (!engine || !userId) return;
2186
+ try {
2187
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2188
+ await engine.update(
2189
+ "sys_user",
2190
+ { id: userId, password_changed_at: /* @__PURE__ */ new Date() },
2191
+ { context: SYSTEM_CTX }
2192
+ );
2193
+ } catch {
2194
+ }
2195
+ }
2007
2196
  /**
2008
2197
  * ADR-0069 D1 — parse the bounded `previous_password_hashes` JSON column into
2009
2198
  * a string[] of hashes, tolerating null / malformed values.
@@ -2220,6 +2409,96 @@ var AuthManager = class {
2220
2409
  );
2221
2410
  return true;
2222
2411
  }
2412
+ /**
2413
+ * ADR-0069 D4 — idle / absolute session enforcement, run per request from
2414
+ * `customSession`. No-op when both are off. Revokes (expires in place +
2415
+ * stamps revoked_at/revoke_reason) when a limit is exceeded so better-auth
2416
+ * returns no session on the NEXT request; otherwise touches `last_activity_at`
2417
+ * (throttled to once a minute). Best-effort — never throws.
2418
+ */
2419
+ async enforceSessionControls(sessionId, createdAtHint) {
2420
+ const idleMin = Math.floor(Number(this.config.sessionIdleTimeoutMinutes) || 0);
2421
+ const absHrs = Math.floor(Number(this.config.sessionAbsoluteMaxHours) || 0);
2422
+ if (idleMin <= 0 && absHrs <= 0) return;
2423
+ const engine = this.getDataEngine();
2424
+ if (!engine || !sessionId) return;
2425
+ try {
2426
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2427
+ const srow = await engine.findOne("sys_session", {
2428
+ where: { id: sessionId },
2429
+ fields: ["id", "created_at", "last_activity_at", "revoked_at"],
2430
+ context: SYSTEM_CTX
2431
+ });
2432
+ if (!srow?.id || srow.revoked_at) return;
2433
+ const now = Date.now();
2434
+ let reason;
2435
+ if (absHrs > 0) {
2436
+ const created = srow.created_at ?? createdAtHint;
2437
+ if (created && now - new Date(created).getTime() > absHrs * 36e5) reason = "absolute_max";
2438
+ }
2439
+ if (!reason && idleMin > 0) {
2440
+ const last = srow.last_activity_at ?? srow.created_at ?? createdAtHint;
2441
+ if (last && now - new Date(last).getTime() > idleMin * 6e4) reason = "idle_timeout";
2442
+ }
2443
+ if (reason) {
2444
+ await engine.update(
2445
+ "sys_session",
2446
+ { id: sessionId, expires_at: new Date(now - 1e3), revoked_at: new Date(now), revoke_reason: reason },
2447
+ { context: SYSTEM_CTX }
2448
+ ).catch(() => void 0);
2449
+ return;
2450
+ }
2451
+ if (idleMin > 0) {
2452
+ const la = srow.last_activity_at ? new Date(srow.last_activity_at).getTime() : 0;
2453
+ if (now - la > 6e4) {
2454
+ await engine.update("sys_session", { id: sessionId, last_activity_at: new Date(now) }, { context: SYSTEM_CTX }).catch(() => void 0);
2455
+ }
2456
+ }
2457
+ } catch {
2458
+ }
2459
+ }
2460
+ /**
2461
+ * ADR-0069 D4 — concurrent-session cap, run from the sign-in after-hook.
2462
+ * Keeps the newest `maxConcurrentSessions` live sessions for the user and
2463
+ * revokes the rest (oldest first). No-op when off. Best-effort.
2464
+ */
2465
+ async enforceConcurrentCap(userId) {
2466
+ const cap = Math.floor(Number(this.config.maxConcurrentSessions) || 0);
2467
+ if (cap <= 0 || !userId) return;
2468
+ const engine = this.getDataEngine();
2469
+ if (!engine) return;
2470
+ try {
2471
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2472
+ const rows = await engine.find("sys_session", {
2473
+ where: { user_id: userId },
2474
+ fields: ["id", "created_at", "expires_at", "revoked_at"],
2475
+ limit: 200,
2476
+ context: SYSTEM_CTX
2477
+ });
2478
+ const now = Date.now();
2479
+ const live = (Array.isArray(rows) ? rows : []).filter((sn) => !sn.revoked_at && (!sn.expires_at || new Date(sn.expires_at).getTime() > now)).sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
2480
+ for (const sn of live.slice(cap)) {
2481
+ await engine.update(
2482
+ "sys_session",
2483
+ { id: sn.id, expires_at: new Date(now - 1e3), revoked_at: new Date(now), revoke_reason: "concurrent_cap" },
2484
+ { context: SYSTEM_CTX }
2485
+ ).catch(() => void 0);
2486
+ }
2487
+ } catch {
2488
+ }
2489
+ }
2490
+ /**
2491
+ * ADR-0069 D5 — is `ip` within the configured allow-list? True (allow) when no
2492
+ * ranges are configured, OR when the IP can't be determined (fail-open so a
2493
+ * misconfigured proxy never locks everyone out — an admin enabling this must
2494
+ * ensure forwarded headers are trusted). Supports IPv4 CIDR + exact IPv4/IPv6.
2495
+ */
2496
+ isClientIpAllowed(ip) {
2497
+ const ranges = this.config.allowedIpRanges;
2498
+ if (!ranges || ranges.length === 0) return true;
2499
+ if (!ip) return true;
2500
+ return ranges.some((r) => ipMatchesRange(ip, r));
2501
+ }
2223
2502
  /**
2224
2503
  * Returns the data engine wired into this auth manager. Used by route
2225
2504
  * handlers (e.g. bootstrap-status) that need to query identity tables
@@ -2261,6 +2540,283 @@ function mapSetPasswordError(error) {
2261
2540
  return { status, body: { success: false, error: { code, message } } };
2262
2541
  }
2263
2542
 
2543
+ // src/register-sso-provider.ts
2544
+ async function resolveActiveOrganizationId(handle, registerUrl, headers) {
2545
+ try {
2546
+ const sessionUrl = registerUrl.replace(/\/sso\/register$/, "/get-session");
2547
+ if (sessionUrl === registerUrl) return void 0;
2548
+ const h = new Headers({ accept: "application/json" });
2549
+ const cookie = headers.get("cookie");
2550
+ if (cookie) h.set("cookie", cookie);
2551
+ const authz = headers.get("authorization");
2552
+ if (authz) h.set("authorization", authz);
2553
+ const resp = await handle(new Request(sessionUrl, { method: "GET", headers: h }));
2554
+ if (!resp.ok) return void 0;
2555
+ const data = await resp.json().catch(() => null);
2556
+ const org = data?.session?.activeOrganizationId ?? data?.activeOrganizationId;
2557
+ return typeof org === "string" && org.length > 0 ? org : void 0;
2558
+ } catch {
2559
+ return void 0;
2560
+ }
2561
+ }
2562
+ async function runRegisterSsoProviderFromForm(handle, request) {
2563
+ let body;
2564
+ try {
2565
+ body = await request.json();
2566
+ } catch {
2567
+ body = {};
2568
+ }
2569
+ const str = (v) => typeof v === "string" ? v.trim() : "";
2570
+ const providerId = str(body?.providerId);
2571
+ const issuer = str(body?.issuer);
2572
+ const domain = str(body?.domain);
2573
+ const clientId = str(body?.clientId);
2574
+ const clientSecret = str(body?.clientSecret);
2575
+ const discoveryEndpoint = str(body?.discoveryEndpoint);
2576
+ const scopesRaw = str(body?.scopes);
2577
+ const missing = [
2578
+ ["providerId", providerId],
2579
+ ["issuer", issuer],
2580
+ ["domain", domain],
2581
+ ["clientId", clientId],
2582
+ ["clientSecret", clientSecret]
2583
+ ].filter(([, v]) => !v).map(([k]) => k);
2584
+ if (missing.length) {
2585
+ return {
2586
+ status: 400,
2587
+ body: { success: false, error: { code: "invalid_request", message: `Missing required field(s): ${missing.join(", ")}` } }
2588
+ };
2589
+ }
2590
+ const oidcConfig = { clientId, clientSecret };
2591
+ if (discoveryEndpoint) oidcConfig.discoveryEndpoint = discoveryEndpoint;
2592
+ oidcConfig.scopes = scopesRaw ? scopesRaw.split(/[\s,]+/).filter(Boolean) : ["openid", "email", "profile"];
2593
+ oidcConfig.mapping = {
2594
+ id: str(body?.mapId) || "sub",
2595
+ email: str(body?.mapEmail) || "email",
2596
+ name: str(body?.mapName) || "name"
2597
+ };
2598
+ let innerUrl;
2599
+ let origin;
2600
+ try {
2601
+ const url = new URL(request.url);
2602
+ origin = url.origin;
2603
+ innerUrl = `${origin}${url.pathname.replace(/\/admin\/sso\/register$/, "/sso/register")}`;
2604
+ } catch {
2605
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Bad request URL" } } };
2606
+ }
2607
+ const headers = new Headers({ "content-type": "application/json" });
2608
+ const cookie = request.headers.get("cookie");
2609
+ if (cookie) headers.set("cookie", cookie);
2610
+ const authz = request.headers.get("authorization");
2611
+ if (authz) headers.set("authorization", authz);
2612
+ headers.set("origin", request.headers.get("origin") || origin);
2613
+ const organizationId = await resolveActiveOrganizationId(handle, innerUrl, headers);
2614
+ const innerReq = new Request(innerUrl, {
2615
+ method: "POST",
2616
+ headers,
2617
+ body: JSON.stringify({ providerId, issuer, domain, oidcConfig, ...organizationId ? { organizationId } : {} })
2618
+ });
2619
+ const resp = await handle(innerReq);
2620
+ let parsed = {};
2621
+ try {
2622
+ const t = await resp.text();
2623
+ parsed = t ? JSON.parse(t) : {};
2624
+ } catch {
2625
+ parsed = {};
2626
+ }
2627
+ if (!resp.ok) {
2628
+ return {
2629
+ status: resp.status,
2630
+ body: { success: false, error: { code: "sso_register_failed", message: parsed?.message || "SSO provider registration failed" } }
2631
+ };
2632
+ }
2633
+ return { status: 200, body: { success: true, data: { providerId: parsed?.providerId ?? providerId } } };
2634
+ }
2635
+ async function runRegisterSamlProviderFromForm(handle, request) {
2636
+ let body;
2637
+ try {
2638
+ body = await request.json();
2639
+ } catch {
2640
+ body = {};
2641
+ }
2642
+ const str = (v) => typeof v === "string" ? v.trim() : "";
2643
+ const providerId = str(body?.providerId);
2644
+ const issuer = str(body?.issuer);
2645
+ const domain = str(body?.domain);
2646
+ const entryPoint = str(body?.entryPoint);
2647
+ const cert = str(body?.cert);
2648
+ const identifierFormat = str(body?.identifierFormat);
2649
+ const missing = [
2650
+ ["providerId", providerId],
2651
+ ["issuer", issuer],
2652
+ ["domain", domain],
2653
+ ["entryPoint", entryPoint],
2654
+ ["cert", cert]
2655
+ ].filter(([, v]) => !v).map(([k]) => k);
2656
+ if (missing.length) {
2657
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: `Missing required field(s): ${missing.join(", ")}` } } };
2658
+ }
2659
+ let origin;
2660
+ let prefix;
2661
+ let innerUrl;
2662
+ try {
2663
+ const url = new URL(request.url);
2664
+ origin = url.origin;
2665
+ prefix = url.pathname.replace(/\/admin\/sso\/register-saml$/, "");
2666
+ innerUrl = `${origin}${prefix}/sso/register`;
2667
+ } catch {
2668
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Bad request URL" } } };
2669
+ }
2670
+ const acsUrl = `${origin}${prefix}/sso/saml2/sp/acs/${encodeURIComponent(providerId)}`;
2671
+ const spMetadataUrl = `${origin}${prefix}/sso/saml2/sp/metadata?providerId=${encodeURIComponent(providerId)}`;
2672
+ const samlConfig = {
2673
+ entryPoint,
2674
+ cert,
2675
+ callbackUrl: acsUrl,
2676
+ // better-auth requires an SP descriptor (its inner fields are optional). Use
2677
+ // the SP metadata URL as our EntityID — the value the IdP keys this SP on.
2678
+ spMetadata: { entityID: spMetadataUrl }
2679
+ };
2680
+ if (identifierFormat) samlConfig.identifierFormat = identifierFormat;
2681
+ const headers = new Headers({ "content-type": "application/json" });
2682
+ const cookie = request.headers.get("cookie");
2683
+ if (cookie) headers.set("cookie", cookie);
2684
+ const authz = request.headers.get("authorization");
2685
+ if (authz) headers.set("authorization", authz);
2686
+ headers.set("origin", request.headers.get("origin") || origin);
2687
+ const organizationId = await resolveActiveOrganizationId(handle, innerUrl, headers);
2688
+ const innerReq = new Request(innerUrl, {
2689
+ method: "POST",
2690
+ headers,
2691
+ body: JSON.stringify({ providerId, issuer, domain, samlConfig, ...organizationId ? { organizationId } : {} })
2692
+ });
2693
+ const resp = await handle(innerReq);
2694
+ let parsed = {};
2695
+ try {
2696
+ const t = await resp.text();
2697
+ parsed = t ? JSON.parse(t) : {};
2698
+ } catch {
2699
+ parsed = {};
2700
+ }
2701
+ if (!resp.ok) {
2702
+ return { status: resp.status, body: { success: false, error: { code: "saml_register_failed", message: parsed?.message || "SAML provider registration failed" } } };
2703
+ }
2704
+ return { status: 200, body: { success: true, data: { providerId: parsed?.providerId ?? providerId }, acsUrl, spMetadataUrl } };
2705
+ }
2706
+ var SSO_DOMAIN_TOKEN_PREFIX = "better-auth-token";
2707
+ function bareHostname(domain) {
2708
+ let d = domain.trim();
2709
+ if (!d) return d;
2710
+ const schemeIdx = d.indexOf("://");
2711
+ if (schemeIdx !== -1) {
2712
+ try {
2713
+ return new URL(d).hostname;
2714
+ } catch {
2715
+ d = d.slice(schemeIdx + 3);
2716
+ }
2717
+ }
2718
+ for (const sep of ["/", ":", "?", "#"]) {
2719
+ const i = d.indexOf(sep);
2720
+ if (i !== -1) d = d.slice(0, i);
2721
+ }
2722
+ return d;
2723
+ }
2724
+ function rewriteSsoAdminUrl(request, fromSuffix, toPath) {
2725
+ try {
2726
+ const url = new URL(request.url);
2727
+ return { origin: url.origin, innerUrl: `${url.origin}${url.pathname.replace(fromSuffix, toPath)}` };
2728
+ } catch {
2729
+ return null;
2730
+ }
2731
+ }
2732
+ function forwardAuthHeaders(request, origin) {
2733
+ const headers = new Headers({ "content-type": "application/json" });
2734
+ const cookie = request.headers.get("cookie");
2735
+ if (cookie) headers.set("cookie", cookie);
2736
+ const authz = request.headers.get("authorization");
2737
+ if (authz) headers.set("authorization", authz);
2738
+ headers.set("origin", request.headers.get("origin") || origin);
2739
+ return headers;
2740
+ }
2741
+ async function runRequestDomainVerification(handle, request) {
2742
+ let body;
2743
+ try {
2744
+ body = await request.json();
2745
+ } catch {
2746
+ body = {};
2747
+ }
2748
+ const str = (v) => typeof v === "string" ? v.trim() : "";
2749
+ const providerId = str(body?.providerId);
2750
+ const domain = bareHostname(str(body?.domain));
2751
+ if (!providerId) {
2752
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Missing required field: providerId" } } };
2753
+ }
2754
+ const rw = rewriteSsoAdminUrl(request, /\/admin\/sso\/request-domain-verification$/, "/sso/request-domain-verification");
2755
+ if (!rw) return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Bad request URL" } } };
2756
+ const headers = forwardAuthHeaders(request, rw.origin);
2757
+ const resp = await handle(new Request(rw.innerUrl, { method: "POST", headers, body: JSON.stringify({ providerId }) }));
2758
+ let parsed = {};
2759
+ try {
2760
+ const t = await resp.text();
2761
+ parsed = t ? JSON.parse(t) : {};
2762
+ } catch {
2763
+ parsed = {};
2764
+ }
2765
+ if (!resp.ok) {
2766
+ if (resp.status === 404 && !parsed?.code) {
2767
+ return { status: 400, body: { success: false, error: { code: "domain_verification_disabled", message: "Domain verification is not enabled for this environment (set OS_SSO_DOMAIN_VERIFICATION)." } } };
2768
+ }
2769
+ return { status: resp.status, body: { success: false, error: { code: parsed?.code || "request_domain_verification_failed", message: parsed?.message || "Failed to request domain verification" } } };
2770
+ }
2771
+ const token = str(parsed?.domainVerificationToken);
2772
+ const label = `_${SSO_DOMAIN_TOKEN_PREFIX}-${providerId}`;
2773
+ const dnsRecordName = domain ? `${label}.${domain}` : label;
2774
+ const dnsRecordValue = `${label}=${token}`;
2775
+ return {
2776
+ status: 200,
2777
+ body: {
2778
+ success: true,
2779
+ data: { providerId, domain, token, dnsRecordType: "TXT", dnsRecordName, dnsRecordValue }
2780
+ }
2781
+ };
2782
+ }
2783
+ async function runVerifyDomain(handle, request) {
2784
+ let body;
2785
+ try {
2786
+ body = await request.json();
2787
+ } catch {
2788
+ body = {};
2789
+ }
2790
+ const str = (v) => typeof v === "string" ? v.trim() : "";
2791
+ const providerId = str(body?.providerId);
2792
+ if (!providerId) {
2793
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Missing required field: providerId" } } };
2794
+ }
2795
+ const rw = rewriteSsoAdminUrl(request, /\/admin\/sso\/verify-domain$/, "/sso/verify-domain");
2796
+ if (!rw) return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Bad request URL" } } };
2797
+ const headers = forwardAuthHeaders(request, rw.origin);
2798
+ const resp = await handle(new Request(rw.innerUrl, { method: "POST", headers, body: JSON.stringify({ providerId }) }));
2799
+ let parsed = {};
2800
+ try {
2801
+ const t = await resp.text();
2802
+ parsed = t ? JSON.parse(t) : {};
2803
+ } catch {
2804
+ parsed = {};
2805
+ }
2806
+ if (resp.ok) {
2807
+ return { status: 200, body: { success: true, data: { providerId, verified: true, message: "Domain ownership verified \u2014 this provider can now sign users in." } } };
2808
+ }
2809
+ let message = parsed?.message || "Domain verification failed";
2810
+ if (resp.status === 404 && !parsed?.code) {
2811
+ message = "Domain verification is not enabled for this environment (set OS_SSO_DOMAIN_VERIFICATION).";
2812
+ } else if (parsed?.code === "NO_PENDING_VERIFICATION") {
2813
+ message = "No pending verification \u2014 click \u201CRequest Domain Verification\u201D first to get the DNS record.";
2814
+ } else if (parsed?.code === "DOMAIN_VERIFICATION_FAILED") {
2815
+ message = "DNS TXT record not found yet. Add the record shown when you requested verification, allow time for DNS to propagate, then retry.";
2816
+ }
2817
+ return { status: resp.status, body: { success: false, error: { code: parsed?.code || "verify_domain_failed", message } } };
2818
+ }
2819
+
2264
2820
  // src/manifest.ts
2265
2821
  import {
2266
2822
  SysAccount,
@@ -2399,7 +2955,33 @@ var AuthPlugin = class {
2399
2955
  // source of truth.
2400
2956
  dashboards: [SystemOverviewDashboard],
2401
2957
  // ADR-0021 — datasets backing the System Overview dashboard's widgets.
2402
- datasets: SystemOverviewDatasets
2958
+ datasets: SystemOverviewDatasets,
2959
+ // ADR-0024 / cloud#551 — surface "SSO Providers" (sys_sso_provider) in the
2960
+ // Setup app's Access Control group, but ONLY when the external-IdP RP is
2961
+ // wired (self-host `OS_SSO_ENABLED`, or the cloud per-env `planAllowsSso`
2962
+ // arriving via `plugins.sso`). Without the gate the entry would render an
2963
+ // empty list + a "Register" button whose endpoint 404s when SSO is off.
2964
+ // Owning-plugin-contributes pattern (ADR-0029 K2), mirroring plugin-security.
2965
+ ...this.authManager.isSsoWired() ? {
2966
+ navigationContributions: [
2967
+ {
2968
+ app: "setup",
2969
+ group: "group_access_control",
2970
+ // After Roles/Permission-Sets (100) and Sharing (200), near API Keys (300).
2971
+ priority: 250,
2972
+ items: [
2973
+ {
2974
+ id: "nav_sso_providers",
2975
+ type: "object",
2976
+ label: "SSO Providers",
2977
+ objectName: "sys_sso_provider",
2978
+ icon: "log-in",
2979
+ requiredPermissions: ["manage_platform_settings"]
2980
+ }
2981
+ ]
2982
+ }
2983
+ ]
2984
+ } : {}
2403
2985
  });
2404
2986
  ctx.logger.info("Auth Plugin initialized successfully");
2405
2987
  }
@@ -2624,6 +3206,24 @@ var AuthPlugin = class {
2624
3206
  const n = Math.floor(Number(values.password_history_count));
2625
3207
  if (Number.isFinite(n) && n >= 0) patch.passwordHistoryCount = Math.min(24, n);
2626
3208
  }
3209
+ if (isExplicit("password_expiry_days")) {
3210
+ const n = Math.floor(Number(values.password_expiry_days));
3211
+ if (Number.isFinite(n) && n >= 0) patch.passwordExpiryDays = Math.min(3650, n);
3212
+ }
3213
+ if (isExplicit("mfa_required")) {
3214
+ const on = asBoolean(values.mfa_required, false);
3215
+ patch.mfaRequired = on;
3216
+ if (on) {
3217
+ patch.plugins = {
3218
+ ...patch.plugins ?? {},
3219
+ twoFactor: true
3220
+ };
3221
+ }
3222
+ }
3223
+ if (isExplicit("mfa_grace_period_days")) {
3224
+ const n = Math.floor(Number(values.mfa_grace_period_days));
3225
+ if (Number.isFinite(n) && n >= 0) patch.mfaGracePeriodDays = Math.min(90, n);
3226
+ }
2627
3227
  const session = {};
2628
3228
  if (isExplicit("session_expiry_days")) {
2629
3229
  const d = asPositiveInt(values.session_expiry_days);
@@ -2636,6 +3236,26 @@ var AuthPlugin = class {
2636
3236
  if (Object.keys(session).length > 0) {
2637
3237
  patch.session = session;
2638
3238
  }
3239
+ const asNonNeg = (v) => {
3240
+ const n = Math.floor(Number(v));
3241
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
3242
+ };
3243
+ if (isExplicit("session_idle_timeout_minutes")) {
3244
+ const n = asNonNeg(values.session_idle_timeout_minutes);
3245
+ if (n !== void 0) patch.sessionIdleTimeoutMinutes = n;
3246
+ }
3247
+ if (isExplicit("session_absolute_max_hours")) {
3248
+ const n = asNonNeg(values.session_absolute_max_hours);
3249
+ if (n !== void 0) patch.sessionAbsoluteMaxHours = n;
3250
+ }
3251
+ if (isExplicit("max_concurrent_sessions_per_user")) {
3252
+ const n = asNonNeg(values.max_concurrent_sessions_per_user);
3253
+ if (n !== void 0) patch.maxConcurrentSessions = n;
3254
+ }
3255
+ if (isExplicit("allowed_ip_ranges")) {
3256
+ const raw = asTrimmedString(values.allowed_ip_ranges) ?? "";
3257
+ patch.allowedIpRanges = raw.split(/[\n,]+/).map((r) => r.trim()).filter(Boolean);
3258
+ }
2639
3259
  const asNonNegativeInt = (value) => {
2640
3260
  const n = Math.floor(Number(value));
2641
3261
  return Number.isFinite(n) && n >= 0 ? n : void 0;
@@ -2784,6 +3404,21 @@ var AuthPlugin = class {
2784
3404
  );
2785
3405
  }
2786
3406
  const rawApp = httpServer.getRawApp();
3407
+ if (typeof rawApp.use === "function") rawApp.use(`${basePath}/*`, async (c, next) => {
3408
+ const mgr = this.authManager;
3409
+ if (!mgr || typeof mgr.isClientIpAllowed !== "function") return next();
3410
+ const path = c.req.path || "";
3411
+ if (path.endsWith("/config") || path.endsWith("/bootstrap-status")) return next();
3412
+ const fwd = c.req.header("x-forwarded-for");
3413
+ const ip = typeof fwd === "string" && fwd.split(",")[0].trim() || c.req.header("cf-connecting-ip") || c.req.header("x-real-ip") || void 0;
3414
+ if (!mgr.isClientIpAllowed(ip)) {
3415
+ return c.json(
3416
+ { success: false, error: { code: "IP_NOT_ALLOWED", message: "Sign-in is not allowed from your network." } },
3417
+ 403
3418
+ );
3419
+ }
3420
+ return next();
3421
+ });
2787
3422
  rawApp.get(`${basePath}/config`, async (c) => {
2788
3423
  try {
2789
3424
  const config = this.authManager.getPublicConfig();
@@ -2875,6 +3510,19 @@ var AuthPlugin = class {
2875
3510
  return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
2876
3511
  }
2877
3512
  });
3513
+ rawApp.post(`${basePath}/admin/sso/register`, async (c) => {
3514
+ try {
3515
+ const { status, body } = await runRegisterSsoProviderFromForm(
3516
+ (req) => this.authManager.handleRequest(req),
3517
+ c.req.raw
3518
+ );
3519
+ return c.json(body, status);
3520
+ } catch (error) {
3521
+ const err = error instanceof Error ? error : new Error(String(error));
3522
+ ctx.logger.error("[AuthPlugin] sso/register bridge failed", err);
3523
+ return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
3524
+ }
3525
+ });
2878
3526
  rawApp.post(`${basePath}/admin/unlock-user`, async (c) => {
2879
3527
  try {
2880
3528
  let body = {};
@@ -2908,6 +3556,45 @@ var AuthPlugin = class {
2908
3556
  return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
2909
3557
  }
2910
3558
  });
3559
+ rawApp.post(`${basePath}/admin/sso/register-saml`, async (c) => {
3560
+ try {
3561
+ const { status, body } = await runRegisterSamlProviderFromForm(
3562
+ (req) => this.authManager.handleRequest(req),
3563
+ c.req.raw
3564
+ );
3565
+ return c.json(body, status);
3566
+ } catch (error) {
3567
+ const err = error instanceof Error ? error : new Error(String(error));
3568
+ ctx.logger.error("[AuthPlugin] sso/register-saml bridge failed", err);
3569
+ return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
3570
+ }
3571
+ });
3572
+ rawApp.post(`${basePath}/admin/sso/request-domain-verification`, async (c) => {
3573
+ try {
3574
+ const { status, body } = await runRequestDomainVerification(
3575
+ (req) => this.authManager.handleRequest(req),
3576
+ c.req.raw
3577
+ );
3578
+ return c.json(body, status);
3579
+ } catch (error) {
3580
+ const err = error instanceof Error ? error : new Error(String(error));
3581
+ ctx.logger.error("[AuthPlugin] sso/request-domain-verification bridge failed", err);
3582
+ return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
3583
+ }
3584
+ });
3585
+ rawApp.post(`${basePath}/admin/sso/verify-domain`, async (c) => {
3586
+ try {
3587
+ const { status, body } = await runVerifyDomain(
3588
+ (req) => this.authManager.handleRequest(req),
3589
+ c.req.raw
3590
+ );
3591
+ return c.json(body, status);
3592
+ } catch (error) {
3593
+ const err = error instanceof Error ? error : new Error(String(error));
3594
+ ctx.logger.error("[AuthPlugin] sso/verify-domain bridge failed", err);
3595
+ return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
3596
+ }
3597
+ });
2911
3598
  rawApp.post(`${basePath}/sys-oauth-application/register`, async (c) => {
2912
3599
  try {
2913
3600
  let body = {};
@@ -3073,8 +3760,13 @@ export {
3073
3760
  buildTwoFactorPluginSchema,
3074
3761
  createObjectQLAdapter,
3075
3762
  createObjectQLAdapterFactory,
3763
+ ipMatchesRange,
3076
3764
  resolveProtocolName,
3765
+ runRegisterSamlProviderFromForm,
3766
+ runRegisterSsoProviderFromForm,
3767
+ runRequestDomainVerification,
3077
3768
  runSetInitialPassword,
3769
+ runVerifyDomain,
3078
3770
  withSystemReadContext
3079
3771
  };
3080
3772
  //# sourceMappingURL=index.mjs.map