@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.js CHANGED
@@ -65,8 +65,13 @@ __export(index_exports, {
65
65
  buildTwoFactorPluginSchema: () => buildTwoFactorPluginSchema,
66
66
  createObjectQLAdapter: () => createObjectQLAdapter,
67
67
  createObjectQLAdapterFactory: () => createObjectQLAdapterFactory,
68
+ ipMatchesRange: () => ipMatchesRange,
68
69
  resolveProtocolName: () => resolveProtocolName,
70
+ runRegisterSamlProviderFromForm: () => runRegisterSamlProviderFromForm,
71
+ runRegisterSsoProviderFromForm: () => runRegisterSsoProviderFromForm,
72
+ runRequestDomainVerification: () => runRequestDomainVerification,
69
73
  runSetInitialPassword: () => runSetInitialPassword,
74
+ runVerifyDomain: () => runVerifyDomain,
70
75
  withSystemReadContext: () => withSystemReadContext
71
76
  });
72
77
  module.exports = __toCommonJS(index_exports);
@@ -592,7 +597,13 @@ var AUTH_SSO_PROVIDER_SCHEMA = {
592
597
  oidcConfig: "oidc_config",
593
598
  samlConfig: "saml_config",
594
599
  userId: "user_id",
595
- organizationId: "organization_id"
600
+ organizationId: "organization_id",
601
+ // DNS domain-ownership proof (ADR-0024 ②). @better-auth/sso writes
602
+ // `domainVerified` on its `ssoProvider` model when domain verification is
603
+ // enabled; map it so the env can surface a verified/unverified badge. The
604
+ // one-time `domainVerificationToken` is NOT a provider column — it lives in
605
+ // the verification table and is returned only from request-domain-verification.
606
+ domainVerified: "domain_verified"
596
607
  }
597
608
  };
598
609
  var AUTH_SCIM_PROVIDER_SCHEMA = {
@@ -671,9 +682,36 @@ function readDisableSignUpEnv() {
671
682
  function readSsoOnlyEnv() {
672
683
  return readBooleanEnv("OS_AUTH_SSO_ONLY");
673
684
  }
685
+ function ipv4ToInt(ip) {
686
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip.trim());
687
+ if (!m) return null;
688
+ const p = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
689
+ if (p.some((n) => n > 255)) return null;
690
+ return (p[0] << 24 >>> 0) + (p[1] << 16) + (p[2] << 8) + p[3] >>> 0;
691
+ }
692
+ function ipMatchesRange(ip, range) {
693
+ const r = (range || "").trim();
694
+ if (!r) return false;
695
+ if (r.includes("/")) {
696
+ const [base, bitsStr] = r.split("/");
697
+ const bits = Number(bitsStr);
698
+ const ipInt = ipv4ToInt(ip);
699
+ const baseInt = ipv4ToInt(base);
700
+ if (ipInt === null || baseInt === null || !(bits >= 0 && bits <= 32)) {
701
+ return ip.trim() === base.trim();
702
+ }
703
+ const mask = bits === 0 ? 0 : ~0 << 32 - bits >>> 0;
704
+ return (ipInt & mask) >>> 0 === (baseInt & mask) >>> 0;
705
+ }
706
+ return ip.trim() === r;
707
+ }
674
708
  var AuthManager = class {
675
709
  constructor(config) {
676
710
  this.auth = null;
711
+ // ADR-0069 — cached "does any org require MFA" flag (per-org tightening).
712
+ // Refreshed lazily with a TTL so isAuthGateActive() stays synchronous + cheap.
713
+ this._orgMfaCache = { value: false, at: 0 };
714
+ this._orgMfaRefreshing = false;
677
715
  this.config = config;
678
716
  installWebContainerRequestStatePolyfill();
679
717
  if (config.authInstance) {
@@ -878,6 +916,7 @@ var AuthManager = class {
878
916
  if (candidate && (ctx?.path === "/reset-password" || ctx?.path === "/change-password")) {
879
917
  const userId = await this.resolvePasswordChangeUserId(ctx).catch(() => void 0);
880
918
  if (userId) {
919
+ ctx.context.__osPwChangeUserId = userId;
881
920
  const pw = ctx?.context?.password;
882
921
  const verify = typeof pw?.verify === "function" ? pw.verify.bind(pw) : void 0;
883
922
  const oldHash = await this.assertPasswordNotReused(userId, candidate, verify);
@@ -1017,25 +1056,43 @@ var AuthManager = class {
1017
1056
  succeeded = !(ctx?.context?.returned instanceof Error);
1018
1057
  }
1019
1058
  await this.recordSignInOutcome(email, succeeded);
1059
+ if (succeeded) {
1060
+ const uid = ctx?.context?.returned?.user?.id;
1061
+ if (typeof uid === "string") await this.enforceConcurrentCap(uid);
1062
+ }
1020
1063
  }
1021
1064
  return;
1022
1065
  }
1023
1066
  if (ctx?.path === "/change-password" || ctx?.path === "/reset-password") {
1024
- const stash = ctx?.context?.__osPwHistory;
1025
- if (stash?.userId) {
1026
- let succeeded = true;
1027
- try {
1028
- const { isAPIError } = await import("better-auth/api");
1029
- succeeded = !isAPIError(ctx?.context?.returned);
1030
- } catch {
1031
- succeeded = !(ctx?.context?.returned instanceof Error);
1032
- }
1033
- if (succeeded) await this.recordPasswordHistory(stash.userId, stash.oldHash);
1034
- delete ctx.context.__osPwHistory;
1067
+ let succeeded;
1068
+ try {
1069
+ const { isAPIError } = await import("better-auth/api");
1070
+ succeeded = !isAPIError(ctx?.context?.returned);
1071
+ } catch {
1072
+ succeeded = !(ctx?.context?.returned instanceof Error);
1035
1073
  }
1074
+ if (succeeded) {
1075
+ const stampId = ctx?.context?.__osPwChangeUserId;
1076
+ if (stampId) await this.stampPasswordChangedAt(stampId);
1077
+ const stash = ctx?.context?.__osPwHistory;
1078
+ if (stash?.userId) await this.recordPasswordHistory(stash.userId, stash.oldHash);
1079
+ }
1080
+ delete ctx.context.__osPwChangeUserId;
1081
+ delete ctx.context.__osPwHistory;
1036
1082
  return;
1037
1083
  }
1038
1084
  if (ctx?.path !== "/sign-up/email") return;
1085
+ {
1086
+ const newUserId = ctx?.context?.returned?.user?.id;
1087
+ let signupOk;
1088
+ try {
1089
+ const { isAPIError } = await import("better-auth/api");
1090
+ signupOk = !isAPIError(ctx?.context?.returned);
1091
+ } catch {
1092
+ signupOk = !(ctx?.context?.returned instanceof Error);
1093
+ }
1094
+ if (signupOk && typeof newUserId === "string") await this.stampPasswordChangedAt(newUserId);
1095
+ }
1039
1096
  const ep = ctx?.context?.options?.emailAndPassword;
1040
1097
  if (ep && ctx.context.__osDisableSignUpOrig !== void 0) {
1041
1098
  ep.disableSignUp = ctx.context.__osDisableSignUpOrig;
@@ -1047,7 +1104,7 @@ var AuthManager = class {
1047
1104
  // Auto-includes origins from OS_CORS_ORIGIN env var so CORS and CSRF stay in sync.
1048
1105
  ...(() => {
1049
1106
  const origins = [...this.config.trustedOrigins || []];
1050
- const corsOrigin = (0, import_types.readEnvWithDeprecation)("OS_CORS_ORIGIN", "CORS_ORIGIN");
1107
+ const corsOrigin = (0, import_types.readEnvWithDeprecation)("OS_CORS_ORIGIN", "CORS_ORIGIN", { silent: true });
1051
1108
  if (corsOrigin && corsOrigin !== "*") {
1052
1109
  corsOrigin.split(",").map((s) => s.trim()).filter(Boolean).forEach((o) => {
1053
1110
  if (!origins.includes(o)) origins.push(o);
@@ -1147,12 +1204,10 @@ var AuthManager = class {
1147
1204
  async buildPluginList() {
1148
1205
  const pluginConfig = this.config.plugins ?? {};
1149
1206
  const plugins = [];
1150
- const oidcEnv = globalThis?.process?.env?.OS_OIDC_PROVIDER_ENABLED;
1151
- const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === "true" : void 0;
1152
- const ssoEnv = globalThis?.process?.env?.OS_SSO_ENABLED;
1153
- const ssoFromEnv = ssoEnv != null ? String(ssoEnv).toLowerCase() === "true" : void 0;
1154
- const scimEnv = globalThis?.process?.env?.OS_SCIM_ENABLED;
1155
- const scimFromEnv = scimEnv != null ? String(scimEnv).toLowerCase() === "true" : void 0;
1207
+ const oidcFromEnv = readBooleanEnv("OS_OIDC_PROVIDER_ENABLED");
1208
+ const ssoFromEnv = readBooleanEnv("OS_SSO_ENABLED");
1209
+ const scimFromEnv = readBooleanEnv("OS_SCIM_ENABLED");
1210
+ const ssoDomainVerifyFromEnv = readBooleanEnv("OS_SSO_DOMAIN_VERIFICATION");
1156
1211
  const scimEffective = scimFromEnv ?? pluginConfig.scim ?? false;
1157
1212
  const twoFactorFromEnv = readBooleanEnv("OS_AUTH_TWO_FACTOR");
1158
1213
  const hibpFromEnv = readBooleanEnv("OS_AUTH_PASSWORD_REJECT_BREACHED");
@@ -1166,6 +1221,7 @@ var AuthManager = class {
1166
1221
  deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
1167
1222
  admin: pluginConfig.admin ?? scimEffective,
1168
1223
  sso: ssoFromEnv ?? pluginConfig.sso ?? false,
1224
+ ssoDomainVerification: ssoDomainVerifyFromEnv ?? pluginConfig.ssoDomainVerification ?? false,
1169
1225
  scim: scimEffective
1170
1226
  };
1171
1227
  const { bearer } = await import("better-auth/plugins/bearer");
@@ -1249,9 +1305,8 @@ var AuthManager = class {
1249
1305
  // The plugin itself is always installed (so list/update/invite endpoints
1250
1306
  // keep responding); only the `create` operation is denied when the
1251
1307
  // deployment is provisioned in single-org mode. Resolution order:
1252
- // 1. explicit `OS_MULTI_ORG_ENABLED` (wins for backwards compat),
1253
- // 2. else `OS_MULTI_TENANT` (multi-tenant deployments are always
1254
- // multi-org), default `'false'` → single-org / per-env runtime.
1308
+ // `OS_MULTI_ORG_ENABLED` (default `'false'` single-org /
1309
+ // per-env runtime).
1255
1310
  beforeCreateOrganization: async () => {
1256
1311
  if (!(0, import_types.resolveMultiOrgEnabled)()) {
1257
1312
  const { APIError } = await import("better-auth/api");
@@ -1432,7 +1487,8 @@ var AuthManager = class {
1432
1487
  if (enabled.sso) {
1433
1488
  const { sso } = await import("@better-auth/sso");
1434
1489
  plugins.push(sso({
1435
- organizationProvisioning: { defaultRole: "member" }
1490
+ organizationProvisioning: { defaultRole: "member" },
1491
+ ...enabled.ssoDomainVerification ? { domainVerification: { enabled: true } } : {}
1436
1492
  }));
1437
1493
  }
1438
1494
  if (enabled.scim) {
@@ -1504,7 +1560,16 @@ var AuthManager = class {
1504
1560
  ...orgRoles,
1505
1561
  ...platformAdmin ? [import_spec.BUILTIN_ROLE_PLATFORM_ADMIN] : []
1506
1562
  ]));
1507
- return { user: { ...user, roles, isPlatformAdmin: platformAdmin }, session };
1563
+ await this.enforceSessionControls(session?.id, session?.createdAt);
1564
+ const authGate = await this.computeAuthGate(
1565
+ user.id,
1566
+ session?.activeOrganizationId,
1567
+ user?.twoFactorEnabled === true
1568
+ );
1569
+ return {
1570
+ user: { ...user, roles, isPlatformAdmin: platformAdmin, ...authGate ? { authGate } : {} },
1571
+ session
1572
+ };
1508
1573
  }));
1509
1574
  }
1510
1575
  return plugins;
@@ -1534,7 +1599,7 @@ var AuthManager = class {
1534
1599
  * Generate a secure secret if not provided
1535
1600
  */
1536
1601
  generateSecret() {
1537
- const envSecret = (0, import_types.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]);
1602
+ const envSecret = (0, import_types.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"], { silent: true });
1538
1603
  if (envSecret) return envSecret;
1539
1604
  if (process.env.NODE_ENV === "production") {
1540
1605
  throw new Error(
@@ -1809,12 +1874,28 @@ var AuthManager = class {
1809
1874
  * in `buildPlugins()` (`ssoFromEnv ?? pluginConfig.sso ?? false`) so the
1810
1875
  * advertised capability can never disagree with the actual `/sign-in/sso`
1811
1876
  * route. `OS_SSO_ENABLED` (when set) wins over the config-file setting.
1877
+ * Public so `AuthPlugin` can gate the Setup-nav "SSO Providers" entry on it
1878
+ * (captures both self-host `OS_SSO_ENABLED` and the cloud per-env
1879
+ * `planAllowsSso` config, since that arrives via `plugins.sso`).
1812
1880
  */
1813
1881
  isSsoWired() {
1814
- const ssoEnv = globalThis?.process?.env?.OS_SSO_ENABLED;
1815
- const ssoFromEnv = ssoEnv != null ? String(ssoEnv).toLowerCase() === "true" : void 0;
1882
+ const ssoFromEnv = readBooleanEnv("OS_SSO_ENABLED");
1816
1883
  return ssoFromEnv ?? this.config.plugins?.sso ?? false;
1817
1884
  }
1885
+ /**
1886
+ * Whether opt-in DNS domain-verification (ADR-0024 ②) is wired — i.e. the
1887
+ * `/sso/request-domain-verification` + `/sso/verify-domain` endpoints are
1888
+ * mounted (and the hard "domain must be verified to log in" gate is active).
1889
+ * Resolved with the EXACT logic `buildPluginList` uses for the `sso()`
1890
+ * `domainVerification.enabled` option, so the bridge can return a clear
1891
+ * "not enabled for this environment" instead of a bare 404 when off.
1892
+ * Implies `isSsoWired()` (the sso plugin must be loaded to honor it).
1893
+ */
1894
+ isSsoDomainVerificationEnabled() {
1895
+ if (!this.isSsoWired()) return false;
1896
+ const fromEnv = readBooleanEnv("OS_SSO_DOMAIN_VERIFICATION");
1897
+ return fromEnv ?? this.config.plugins?.ssoDomainVerification ?? false;
1898
+ }
1818
1899
  /**
1819
1900
  * Whether enterprise SSO is actually *usable*, not merely wired: the plugin
1820
1901
  * is on AND at least one `sys_sso_provider` row exists. Per-email domain→IdP
@@ -2074,6 +2155,119 @@ var AuthManager = class {
2074
2155
  });
2075
2156
  }
2076
2157
  }
2158
+ /**
2159
+ * ADR-0069 — is any authentication-policy gate enabled? Cheap, synchronous;
2160
+ * lets the transport seams skip session lookups entirely when off (the
2161
+ * default), keeping the gate zero-overhead until an admin opts in.
2162
+ */
2163
+ isAuthGateActive() {
2164
+ this.refreshOrgMfaCacheIfStale();
2165
+ return Math.floor(Number(this.config.passwordExpiryDays) || 0) > 0 || this.config.mfaRequired === true || this._orgMfaCache.value;
2166
+ }
2167
+ /**
2168
+ * ADR-0069 — refresh the "any org requires MFA" cache in the background when
2169
+ * stale (60s TTL). Fire-and-forget: a brand-new per-org requirement activates
2170
+ * the gate on the next request, never blocking this one. No-op when global MFA
2171
+ * is already on (the gate is active regardless).
2172
+ */
2173
+ refreshOrgMfaCacheIfStale() {
2174
+ if (this.config.mfaRequired === true) return;
2175
+ if (this._orgMfaRefreshing) return;
2176
+ if (Date.now() - this._orgMfaCache.at < 6e4) return;
2177
+ const engine = this.getDataEngine();
2178
+ if (!engine) return;
2179
+ this._orgMfaRefreshing = true;
2180
+ void (async () => {
2181
+ try {
2182
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2183
+ const n = await engine.count("sys_organization", {
2184
+ where: { require_mfa: true },
2185
+ context: SYSTEM_CTX
2186
+ });
2187
+ this._orgMfaCache = { value: typeof n === "number" && n > 0, at: Date.now() };
2188
+ } catch {
2189
+ } finally {
2190
+ this._orgMfaRefreshing = false;
2191
+ }
2192
+ })();
2193
+ }
2194
+ /**
2195
+ * ADR-0069 — compute the auth-policy gate posture for a session. Returns an
2196
+ * `{ code, message }` when the user is currently blocked (e.g. password
2197
+ * expired), else undefined. No-op (and no DB read) when no gate feature is
2198
+ * enabled. Fails OPEN on any lookup error — a transient hiccup must never lock
2199
+ * a compliant user out.
2200
+ */
2201
+ async computeAuthGate(userId, _activeOrgId, _twoFactorEnabledHint) {
2202
+ const expiryDays = Math.floor(Number(this.config.passwordExpiryDays) || 0);
2203
+ const mfaGlobal = this.config.mfaRequired === true;
2204
+ const orgMaybeRequires = !mfaGlobal && !!_activeOrgId && this._orgMfaCache.value;
2205
+ if (expiryDays <= 0 && !mfaGlobal && !orgMaybeRequires) return void 0;
2206
+ const engine = this.getDataEngine();
2207
+ if (!engine || !userId) return void 0;
2208
+ try {
2209
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2210
+ const u = await engine.findOne("sys_user", {
2211
+ where: { id: userId },
2212
+ fields: ["password_changed_at", "two_factor_enabled", "mfa_required_at"],
2213
+ context: SYSTEM_CTX
2214
+ });
2215
+ let mfaRequired = mfaGlobal;
2216
+ if (!mfaRequired && orgMaybeRequires) {
2217
+ const org = await engine.findOne("sys_organization", {
2218
+ where: { id: _activeOrgId },
2219
+ fields: ["require_mfa"],
2220
+ context: SYSTEM_CTX
2221
+ });
2222
+ mfaRequired = org?.require_mfa === true || org?.require_mfa === 1;
2223
+ }
2224
+ if (expiryDays > 0) {
2225
+ const changed = u?.password_changed_at;
2226
+ if (changed && Date.now() - new Date(changed).getTime() > expiryDays * 864e5) {
2227
+ return {
2228
+ code: "PASSWORD_EXPIRED",
2229
+ message: "Your password has expired. Please change it to continue."
2230
+ };
2231
+ }
2232
+ }
2233
+ if (mfaRequired && !(u?.two_factor_enabled === true || u?.two_factor_enabled === 1)) {
2234
+ const graceDays = Math.max(0, Math.floor(Number(this.config.mfaGracePeriodDays ?? 7)));
2235
+ let requiredAt = u?.mfa_required_at;
2236
+ if (!requiredAt) {
2237
+ requiredAt = /* @__PURE__ */ new Date();
2238
+ engine.update("sys_user", { id: userId, mfa_required_at: requiredAt }, { context: SYSTEM_CTX }).catch(() => void 0);
2239
+ }
2240
+ const elapsedMs = Date.now() - new Date(requiredAt).getTime();
2241
+ if (elapsedMs > graceDays * 864e5) {
2242
+ return {
2243
+ code: "MFA_REQUIRED",
2244
+ message: "Multi-factor authentication is required. Please set up an authenticator app to continue."
2245
+ };
2246
+ }
2247
+ }
2248
+ } catch {
2249
+ return void 0;
2250
+ }
2251
+ return void 0;
2252
+ }
2253
+ /**
2254
+ * ADR-0069 D1 — stamp `sys_user.password_changed_at = now` after a password is
2255
+ * set (sign-up / change / reset). Best-effort; never throws. Written as a Date
2256
+ * (never epoch-ms) per ADR-0074.
2257
+ */
2258
+ async stampPasswordChangedAt(userId) {
2259
+ const engine = this.getDataEngine();
2260
+ if (!engine || !userId) return;
2261
+ try {
2262
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2263
+ await engine.update(
2264
+ "sys_user",
2265
+ { id: userId, password_changed_at: /* @__PURE__ */ new Date() },
2266
+ { context: SYSTEM_CTX }
2267
+ );
2268
+ } catch {
2269
+ }
2270
+ }
2077
2271
  /**
2078
2272
  * ADR-0069 D1 — parse the bounded `previous_password_hashes` JSON column into
2079
2273
  * a string[] of hashes, tolerating null / malformed values.
@@ -2290,6 +2484,96 @@ var AuthManager = class {
2290
2484
  );
2291
2485
  return true;
2292
2486
  }
2487
+ /**
2488
+ * ADR-0069 D4 — idle / absolute session enforcement, run per request from
2489
+ * `customSession`. No-op when both are off. Revokes (expires in place +
2490
+ * stamps revoked_at/revoke_reason) when a limit is exceeded so better-auth
2491
+ * returns no session on the NEXT request; otherwise touches `last_activity_at`
2492
+ * (throttled to once a minute). Best-effort — never throws.
2493
+ */
2494
+ async enforceSessionControls(sessionId, createdAtHint) {
2495
+ const idleMin = Math.floor(Number(this.config.sessionIdleTimeoutMinutes) || 0);
2496
+ const absHrs = Math.floor(Number(this.config.sessionAbsoluteMaxHours) || 0);
2497
+ if (idleMin <= 0 && absHrs <= 0) return;
2498
+ const engine = this.getDataEngine();
2499
+ if (!engine || !sessionId) return;
2500
+ try {
2501
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2502
+ const srow = await engine.findOne("sys_session", {
2503
+ where: { id: sessionId },
2504
+ fields: ["id", "created_at", "last_activity_at", "revoked_at"],
2505
+ context: SYSTEM_CTX
2506
+ });
2507
+ if (!srow?.id || srow.revoked_at) return;
2508
+ const now = Date.now();
2509
+ let reason;
2510
+ if (absHrs > 0) {
2511
+ const created = srow.created_at ?? createdAtHint;
2512
+ if (created && now - new Date(created).getTime() > absHrs * 36e5) reason = "absolute_max";
2513
+ }
2514
+ if (!reason && idleMin > 0) {
2515
+ const last = srow.last_activity_at ?? srow.created_at ?? createdAtHint;
2516
+ if (last && now - new Date(last).getTime() > idleMin * 6e4) reason = "idle_timeout";
2517
+ }
2518
+ if (reason) {
2519
+ await engine.update(
2520
+ "sys_session",
2521
+ { id: sessionId, expires_at: new Date(now - 1e3), revoked_at: new Date(now), revoke_reason: reason },
2522
+ { context: SYSTEM_CTX }
2523
+ ).catch(() => void 0);
2524
+ return;
2525
+ }
2526
+ if (idleMin > 0) {
2527
+ const la = srow.last_activity_at ? new Date(srow.last_activity_at).getTime() : 0;
2528
+ if (now - la > 6e4) {
2529
+ await engine.update("sys_session", { id: sessionId, last_activity_at: new Date(now) }, { context: SYSTEM_CTX }).catch(() => void 0);
2530
+ }
2531
+ }
2532
+ } catch {
2533
+ }
2534
+ }
2535
+ /**
2536
+ * ADR-0069 D4 — concurrent-session cap, run from the sign-in after-hook.
2537
+ * Keeps the newest `maxConcurrentSessions` live sessions for the user and
2538
+ * revokes the rest (oldest first). No-op when off. Best-effort.
2539
+ */
2540
+ async enforceConcurrentCap(userId) {
2541
+ const cap = Math.floor(Number(this.config.maxConcurrentSessions) || 0);
2542
+ if (cap <= 0 || !userId) return;
2543
+ const engine = this.getDataEngine();
2544
+ if (!engine) return;
2545
+ try {
2546
+ const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
2547
+ const rows = await engine.find("sys_session", {
2548
+ where: { user_id: userId },
2549
+ fields: ["id", "created_at", "expires_at", "revoked_at"],
2550
+ limit: 200,
2551
+ context: SYSTEM_CTX
2552
+ });
2553
+ const now = Date.now();
2554
+ 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());
2555
+ for (const sn of live.slice(cap)) {
2556
+ await engine.update(
2557
+ "sys_session",
2558
+ { id: sn.id, expires_at: new Date(now - 1e3), revoked_at: new Date(now), revoke_reason: "concurrent_cap" },
2559
+ { context: SYSTEM_CTX }
2560
+ ).catch(() => void 0);
2561
+ }
2562
+ } catch {
2563
+ }
2564
+ }
2565
+ /**
2566
+ * ADR-0069 D5 — is `ip` within the configured allow-list? True (allow) when no
2567
+ * ranges are configured, OR when the IP can't be determined (fail-open so a
2568
+ * misconfigured proxy never locks everyone out — an admin enabling this must
2569
+ * ensure forwarded headers are trusted). Supports IPv4 CIDR + exact IPv4/IPv6.
2570
+ */
2571
+ isClientIpAllowed(ip) {
2572
+ const ranges = this.config.allowedIpRanges;
2573
+ if (!ranges || ranges.length === 0) return true;
2574
+ if (!ip) return true;
2575
+ return ranges.some((r) => ipMatchesRange(ip, r));
2576
+ }
2293
2577
  /**
2294
2578
  * Returns the data engine wired into this auth manager. Used by route
2295
2579
  * handlers (e.g. bootstrap-status) that need to query identity tables
@@ -2331,6 +2615,283 @@ function mapSetPasswordError(error) {
2331
2615
  return { status, body: { success: false, error: { code, message } } };
2332
2616
  }
2333
2617
 
2618
+ // src/register-sso-provider.ts
2619
+ async function resolveActiveOrganizationId(handle, registerUrl, headers) {
2620
+ try {
2621
+ const sessionUrl = registerUrl.replace(/\/sso\/register$/, "/get-session");
2622
+ if (sessionUrl === registerUrl) return void 0;
2623
+ const h = new Headers({ accept: "application/json" });
2624
+ const cookie = headers.get("cookie");
2625
+ if (cookie) h.set("cookie", cookie);
2626
+ const authz = headers.get("authorization");
2627
+ if (authz) h.set("authorization", authz);
2628
+ const resp = await handle(new Request(sessionUrl, { method: "GET", headers: h }));
2629
+ if (!resp.ok) return void 0;
2630
+ const data = await resp.json().catch(() => null);
2631
+ const org = data?.session?.activeOrganizationId ?? data?.activeOrganizationId;
2632
+ return typeof org === "string" && org.length > 0 ? org : void 0;
2633
+ } catch {
2634
+ return void 0;
2635
+ }
2636
+ }
2637
+ async function runRegisterSsoProviderFromForm(handle, request) {
2638
+ let body;
2639
+ try {
2640
+ body = await request.json();
2641
+ } catch {
2642
+ body = {};
2643
+ }
2644
+ const str = (v) => typeof v === "string" ? v.trim() : "";
2645
+ const providerId = str(body?.providerId);
2646
+ const issuer = str(body?.issuer);
2647
+ const domain = str(body?.domain);
2648
+ const clientId = str(body?.clientId);
2649
+ const clientSecret = str(body?.clientSecret);
2650
+ const discoveryEndpoint = str(body?.discoveryEndpoint);
2651
+ const scopesRaw = str(body?.scopes);
2652
+ const missing = [
2653
+ ["providerId", providerId],
2654
+ ["issuer", issuer],
2655
+ ["domain", domain],
2656
+ ["clientId", clientId],
2657
+ ["clientSecret", clientSecret]
2658
+ ].filter(([, v]) => !v).map(([k]) => k);
2659
+ if (missing.length) {
2660
+ return {
2661
+ status: 400,
2662
+ body: { success: false, error: { code: "invalid_request", message: `Missing required field(s): ${missing.join(", ")}` } }
2663
+ };
2664
+ }
2665
+ const oidcConfig = { clientId, clientSecret };
2666
+ if (discoveryEndpoint) oidcConfig.discoveryEndpoint = discoveryEndpoint;
2667
+ oidcConfig.scopes = scopesRaw ? scopesRaw.split(/[\s,]+/).filter(Boolean) : ["openid", "email", "profile"];
2668
+ oidcConfig.mapping = {
2669
+ id: str(body?.mapId) || "sub",
2670
+ email: str(body?.mapEmail) || "email",
2671
+ name: str(body?.mapName) || "name"
2672
+ };
2673
+ let innerUrl;
2674
+ let origin;
2675
+ try {
2676
+ const url = new URL(request.url);
2677
+ origin = url.origin;
2678
+ innerUrl = `${origin}${url.pathname.replace(/\/admin\/sso\/register$/, "/sso/register")}`;
2679
+ } catch {
2680
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Bad request URL" } } };
2681
+ }
2682
+ const headers = new Headers({ "content-type": "application/json" });
2683
+ const cookie = request.headers.get("cookie");
2684
+ if (cookie) headers.set("cookie", cookie);
2685
+ const authz = request.headers.get("authorization");
2686
+ if (authz) headers.set("authorization", authz);
2687
+ headers.set("origin", request.headers.get("origin") || origin);
2688
+ const organizationId = await resolveActiveOrganizationId(handle, innerUrl, headers);
2689
+ const innerReq = new Request(innerUrl, {
2690
+ method: "POST",
2691
+ headers,
2692
+ body: JSON.stringify({ providerId, issuer, domain, oidcConfig, ...organizationId ? { organizationId } : {} })
2693
+ });
2694
+ const resp = await handle(innerReq);
2695
+ let parsed = {};
2696
+ try {
2697
+ const t = await resp.text();
2698
+ parsed = t ? JSON.parse(t) : {};
2699
+ } catch {
2700
+ parsed = {};
2701
+ }
2702
+ if (!resp.ok) {
2703
+ return {
2704
+ status: resp.status,
2705
+ body: { success: false, error: { code: "sso_register_failed", message: parsed?.message || "SSO provider registration failed" } }
2706
+ };
2707
+ }
2708
+ return { status: 200, body: { success: true, data: { providerId: parsed?.providerId ?? providerId } } };
2709
+ }
2710
+ async function runRegisterSamlProviderFromForm(handle, request) {
2711
+ let body;
2712
+ try {
2713
+ body = await request.json();
2714
+ } catch {
2715
+ body = {};
2716
+ }
2717
+ const str = (v) => typeof v === "string" ? v.trim() : "";
2718
+ const providerId = str(body?.providerId);
2719
+ const issuer = str(body?.issuer);
2720
+ const domain = str(body?.domain);
2721
+ const entryPoint = str(body?.entryPoint);
2722
+ const cert = str(body?.cert);
2723
+ const identifierFormat = str(body?.identifierFormat);
2724
+ const missing = [
2725
+ ["providerId", providerId],
2726
+ ["issuer", issuer],
2727
+ ["domain", domain],
2728
+ ["entryPoint", entryPoint],
2729
+ ["cert", cert]
2730
+ ].filter(([, v]) => !v).map(([k]) => k);
2731
+ if (missing.length) {
2732
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: `Missing required field(s): ${missing.join(", ")}` } } };
2733
+ }
2734
+ let origin;
2735
+ let prefix;
2736
+ let innerUrl;
2737
+ try {
2738
+ const url = new URL(request.url);
2739
+ origin = url.origin;
2740
+ prefix = url.pathname.replace(/\/admin\/sso\/register-saml$/, "");
2741
+ innerUrl = `${origin}${prefix}/sso/register`;
2742
+ } catch {
2743
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Bad request URL" } } };
2744
+ }
2745
+ const acsUrl = `${origin}${prefix}/sso/saml2/sp/acs/${encodeURIComponent(providerId)}`;
2746
+ const spMetadataUrl = `${origin}${prefix}/sso/saml2/sp/metadata?providerId=${encodeURIComponent(providerId)}`;
2747
+ const samlConfig = {
2748
+ entryPoint,
2749
+ cert,
2750
+ callbackUrl: acsUrl,
2751
+ // better-auth requires an SP descriptor (its inner fields are optional). Use
2752
+ // the SP metadata URL as our EntityID — the value the IdP keys this SP on.
2753
+ spMetadata: { entityID: spMetadataUrl }
2754
+ };
2755
+ if (identifierFormat) samlConfig.identifierFormat = identifierFormat;
2756
+ const headers = new Headers({ "content-type": "application/json" });
2757
+ const cookie = request.headers.get("cookie");
2758
+ if (cookie) headers.set("cookie", cookie);
2759
+ const authz = request.headers.get("authorization");
2760
+ if (authz) headers.set("authorization", authz);
2761
+ headers.set("origin", request.headers.get("origin") || origin);
2762
+ const organizationId = await resolveActiveOrganizationId(handle, innerUrl, headers);
2763
+ const innerReq = new Request(innerUrl, {
2764
+ method: "POST",
2765
+ headers,
2766
+ body: JSON.stringify({ providerId, issuer, domain, samlConfig, ...organizationId ? { organizationId } : {} })
2767
+ });
2768
+ const resp = await handle(innerReq);
2769
+ let parsed = {};
2770
+ try {
2771
+ const t = await resp.text();
2772
+ parsed = t ? JSON.parse(t) : {};
2773
+ } catch {
2774
+ parsed = {};
2775
+ }
2776
+ if (!resp.ok) {
2777
+ return { status: resp.status, body: { success: false, error: { code: "saml_register_failed", message: parsed?.message || "SAML provider registration failed" } } };
2778
+ }
2779
+ return { status: 200, body: { success: true, data: { providerId: parsed?.providerId ?? providerId }, acsUrl, spMetadataUrl } };
2780
+ }
2781
+ var SSO_DOMAIN_TOKEN_PREFIX = "better-auth-token";
2782
+ function bareHostname(domain) {
2783
+ let d = domain.trim();
2784
+ if (!d) return d;
2785
+ const schemeIdx = d.indexOf("://");
2786
+ if (schemeIdx !== -1) {
2787
+ try {
2788
+ return new URL(d).hostname;
2789
+ } catch {
2790
+ d = d.slice(schemeIdx + 3);
2791
+ }
2792
+ }
2793
+ for (const sep of ["/", ":", "?", "#"]) {
2794
+ const i = d.indexOf(sep);
2795
+ if (i !== -1) d = d.slice(0, i);
2796
+ }
2797
+ return d;
2798
+ }
2799
+ function rewriteSsoAdminUrl(request, fromSuffix, toPath) {
2800
+ try {
2801
+ const url = new URL(request.url);
2802
+ return { origin: url.origin, innerUrl: `${url.origin}${url.pathname.replace(fromSuffix, toPath)}` };
2803
+ } catch {
2804
+ return null;
2805
+ }
2806
+ }
2807
+ function forwardAuthHeaders(request, origin) {
2808
+ const headers = new Headers({ "content-type": "application/json" });
2809
+ const cookie = request.headers.get("cookie");
2810
+ if (cookie) headers.set("cookie", cookie);
2811
+ const authz = request.headers.get("authorization");
2812
+ if (authz) headers.set("authorization", authz);
2813
+ headers.set("origin", request.headers.get("origin") || origin);
2814
+ return headers;
2815
+ }
2816
+ async function runRequestDomainVerification(handle, request) {
2817
+ let body;
2818
+ try {
2819
+ body = await request.json();
2820
+ } catch {
2821
+ body = {};
2822
+ }
2823
+ const str = (v) => typeof v === "string" ? v.trim() : "";
2824
+ const providerId = str(body?.providerId);
2825
+ const domain = bareHostname(str(body?.domain));
2826
+ if (!providerId) {
2827
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Missing required field: providerId" } } };
2828
+ }
2829
+ const rw = rewriteSsoAdminUrl(request, /\/admin\/sso\/request-domain-verification$/, "/sso/request-domain-verification");
2830
+ if (!rw) return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Bad request URL" } } };
2831
+ const headers = forwardAuthHeaders(request, rw.origin);
2832
+ const resp = await handle(new Request(rw.innerUrl, { method: "POST", headers, body: JSON.stringify({ providerId }) }));
2833
+ let parsed = {};
2834
+ try {
2835
+ const t = await resp.text();
2836
+ parsed = t ? JSON.parse(t) : {};
2837
+ } catch {
2838
+ parsed = {};
2839
+ }
2840
+ if (!resp.ok) {
2841
+ if (resp.status === 404 && !parsed?.code) {
2842
+ 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)." } } };
2843
+ }
2844
+ return { status: resp.status, body: { success: false, error: { code: parsed?.code || "request_domain_verification_failed", message: parsed?.message || "Failed to request domain verification" } } };
2845
+ }
2846
+ const token = str(parsed?.domainVerificationToken);
2847
+ const label = `_${SSO_DOMAIN_TOKEN_PREFIX}-${providerId}`;
2848
+ const dnsRecordName = domain ? `${label}.${domain}` : label;
2849
+ const dnsRecordValue = `${label}=${token}`;
2850
+ return {
2851
+ status: 200,
2852
+ body: {
2853
+ success: true,
2854
+ data: { providerId, domain, token, dnsRecordType: "TXT", dnsRecordName, dnsRecordValue }
2855
+ }
2856
+ };
2857
+ }
2858
+ async function runVerifyDomain(handle, request) {
2859
+ let body;
2860
+ try {
2861
+ body = await request.json();
2862
+ } catch {
2863
+ body = {};
2864
+ }
2865
+ const str = (v) => typeof v === "string" ? v.trim() : "";
2866
+ const providerId = str(body?.providerId);
2867
+ if (!providerId) {
2868
+ return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Missing required field: providerId" } } };
2869
+ }
2870
+ const rw = rewriteSsoAdminUrl(request, /\/admin\/sso\/verify-domain$/, "/sso/verify-domain");
2871
+ if (!rw) return { status: 400, body: { success: false, error: { code: "invalid_request", message: "Bad request URL" } } };
2872
+ const headers = forwardAuthHeaders(request, rw.origin);
2873
+ const resp = await handle(new Request(rw.innerUrl, { method: "POST", headers, body: JSON.stringify({ providerId }) }));
2874
+ let parsed = {};
2875
+ try {
2876
+ const t = await resp.text();
2877
+ parsed = t ? JSON.parse(t) : {};
2878
+ } catch {
2879
+ parsed = {};
2880
+ }
2881
+ if (resp.ok) {
2882
+ return { status: 200, body: { success: true, data: { providerId, verified: true, message: "Domain ownership verified \u2014 this provider can now sign users in." } } };
2883
+ }
2884
+ let message = parsed?.message || "Domain verification failed";
2885
+ if (resp.status === 404 && !parsed?.code) {
2886
+ message = "Domain verification is not enabled for this environment (set OS_SSO_DOMAIN_VERIFICATION).";
2887
+ } else if (parsed?.code === "NO_PENDING_VERIFICATION") {
2888
+ message = "No pending verification \u2014 click \u201CRequest Domain Verification\u201D first to get the DNS record.";
2889
+ } else if (parsed?.code === "DOMAIN_VERIFICATION_FAILED") {
2890
+ message = "DNS TXT record not found yet. Add the record shown when you requested verification, allow time for DNS to propagate, then retry.";
2891
+ }
2892
+ return { status: resp.status, body: { success: false, error: { code: parsed?.code || "verify_domain_failed", message } } };
2893
+ }
2894
+
2334
2895
  // src/manifest.ts
2335
2896
  var import_identity = require("@objectstack/platform-objects/identity");
2336
2897
  var AUTH_PLUGIN_ID = "com.objectstack.plugin-auth";
@@ -2448,7 +3009,33 @@ var AuthPlugin = class {
2448
3009
  // source of truth.
2449
3010
  dashboards: [import_apps.SystemOverviewDashboard],
2450
3011
  // ADR-0021 — datasets backing the System Overview dashboard's widgets.
2451
- datasets: import_apps.SystemOverviewDatasets
3012
+ datasets: import_apps.SystemOverviewDatasets,
3013
+ // ADR-0024 / cloud#551 — surface "SSO Providers" (sys_sso_provider) in the
3014
+ // Setup app's Access Control group, but ONLY when the external-IdP RP is
3015
+ // wired (self-host `OS_SSO_ENABLED`, or the cloud per-env `planAllowsSso`
3016
+ // arriving via `plugins.sso`). Without the gate the entry would render an
3017
+ // empty list + a "Register" button whose endpoint 404s when SSO is off.
3018
+ // Owning-plugin-contributes pattern (ADR-0029 K2), mirroring plugin-security.
3019
+ ...this.authManager.isSsoWired() ? {
3020
+ navigationContributions: [
3021
+ {
3022
+ app: "setup",
3023
+ group: "group_access_control",
3024
+ // After Roles/Permission-Sets (100) and Sharing (200), near API Keys (300).
3025
+ priority: 250,
3026
+ items: [
3027
+ {
3028
+ id: "nav_sso_providers",
3029
+ type: "object",
3030
+ label: "SSO Providers",
3031
+ objectName: "sys_sso_provider",
3032
+ icon: "log-in",
3033
+ requiredPermissions: ["manage_platform_settings"]
3034
+ }
3035
+ ]
3036
+ }
3037
+ ]
3038
+ } : {}
2452
3039
  });
2453
3040
  ctx.logger.info("Auth Plugin initialized successfully");
2454
3041
  }
@@ -2673,6 +3260,24 @@ var AuthPlugin = class {
2673
3260
  const n = Math.floor(Number(values.password_history_count));
2674
3261
  if (Number.isFinite(n) && n >= 0) patch.passwordHistoryCount = Math.min(24, n);
2675
3262
  }
3263
+ if (isExplicit("password_expiry_days")) {
3264
+ const n = Math.floor(Number(values.password_expiry_days));
3265
+ if (Number.isFinite(n) && n >= 0) patch.passwordExpiryDays = Math.min(3650, n);
3266
+ }
3267
+ if (isExplicit("mfa_required")) {
3268
+ const on = asBoolean(values.mfa_required, false);
3269
+ patch.mfaRequired = on;
3270
+ if (on) {
3271
+ patch.plugins = {
3272
+ ...patch.plugins ?? {},
3273
+ twoFactor: true
3274
+ };
3275
+ }
3276
+ }
3277
+ if (isExplicit("mfa_grace_period_days")) {
3278
+ const n = Math.floor(Number(values.mfa_grace_period_days));
3279
+ if (Number.isFinite(n) && n >= 0) patch.mfaGracePeriodDays = Math.min(90, n);
3280
+ }
2676
3281
  const session = {};
2677
3282
  if (isExplicit("session_expiry_days")) {
2678
3283
  const d = asPositiveInt(values.session_expiry_days);
@@ -2685,6 +3290,26 @@ var AuthPlugin = class {
2685
3290
  if (Object.keys(session).length > 0) {
2686
3291
  patch.session = session;
2687
3292
  }
3293
+ const asNonNeg = (v) => {
3294
+ const n = Math.floor(Number(v));
3295
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
3296
+ };
3297
+ if (isExplicit("session_idle_timeout_minutes")) {
3298
+ const n = asNonNeg(values.session_idle_timeout_minutes);
3299
+ if (n !== void 0) patch.sessionIdleTimeoutMinutes = n;
3300
+ }
3301
+ if (isExplicit("session_absolute_max_hours")) {
3302
+ const n = asNonNeg(values.session_absolute_max_hours);
3303
+ if (n !== void 0) patch.sessionAbsoluteMaxHours = n;
3304
+ }
3305
+ if (isExplicit("max_concurrent_sessions_per_user")) {
3306
+ const n = asNonNeg(values.max_concurrent_sessions_per_user);
3307
+ if (n !== void 0) patch.maxConcurrentSessions = n;
3308
+ }
3309
+ if (isExplicit("allowed_ip_ranges")) {
3310
+ const raw = asTrimmedString(values.allowed_ip_ranges) ?? "";
3311
+ patch.allowedIpRanges = raw.split(/[\n,]+/).map((r) => r.trim()).filter(Boolean);
3312
+ }
2688
3313
  const asNonNegativeInt = (value) => {
2689
3314
  const n = Math.floor(Number(value));
2690
3315
  return Number.isFinite(n) && n >= 0 ? n : void 0;
@@ -2833,6 +3458,21 @@ var AuthPlugin = class {
2833
3458
  );
2834
3459
  }
2835
3460
  const rawApp = httpServer.getRawApp();
3461
+ if (typeof rawApp.use === "function") rawApp.use(`${basePath}/*`, async (c, next) => {
3462
+ const mgr = this.authManager;
3463
+ if (!mgr || typeof mgr.isClientIpAllowed !== "function") return next();
3464
+ const path = c.req.path || "";
3465
+ if (path.endsWith("/config") || path.endsWith("/bootstrap-status")) return next();
3466
+ const fwd = c.req.header("x-forwarded-for");
3467
+ const ip = typeof fwd === "string" && fwd.split(",")[0].trim() || c.req.header("cf-connecting-ip") || c.req.header("x-real-ip") || void 0;
3468
+ if (!mgr.isClientIpAllowed(ip)) {
3469
+ return c.json(
3470
+ { success: false, error: { code: "IP_NOT_ALLOWED", message: "Sign-in is not allowed from your network." } },
3471
+ 403
3472
+ );
3473
+ }
3474
+ return next();
3475
+ });
2836
3476
  rawApp.get(`${basePath}/config`, async (c) => {
2837
3477
  try {
2838
3478
  const config = this.authManager.getPublicConfig();
@@ -2924,6 +3564,19 @@ var AuthPlugin = class {
2924
3564
  return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
2925
3565
  }
2926
3566
  });
3567
+ rawApp.post(`${basePath}/admin/sso/register`, async (c) => {
3568
+ try {
3569
+ const { status, body } = await runRegisterSsoProviderFromForm(
3570
+ (req) => this.authManager.handleRequest(req),
3571
+ c.req.raw
3572
+ );
3573
+ return c.json(body, status);
3574
+ } catch (error) {
3575
+ const err = error instanceof Error ? error : new Error(String(error));
3576
+ ctx.logger.error("[AuthPlugin] sso/register bridge failed", err);
3577
+ return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
3578
+ }
3579
+ });
2927
3580
  rawApp.post(`${basePath}/admin/unlock-user`, async (c) => {
2928
3581
  try {
2929
3582
  let body = {};
@@ -2957,6 +3610,45 @@ var AuthPlugin = class {
2957
3610
  return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
2958
3611
  }
2959
3612
  });
3613
+ rawApp.post(`${basePath}/admin/sso/register-saml`, async (c) => {
3614
+ try {
3615
+ const { status, body } = await runRegisterSamlProviderFromForm(
3616
+ (req) => this.authManager.handleRequest(req),
3617
+ c.req.raw
3618
+ );
3619
+ return c.json(body, status);
3620
+ } catch (error) {
3621
+ const err = error instanceof Error ? error : new Error(String(error));
3622
+ ctx.logger.error("[AuthPlugin] sso/register-saml bridge failed", err);
3623
+ return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
3624
+ }
3625
+ });
3626
+ rawApp.post(`${basePath}/admin/sso/request-domain-verification`, async (c) => {
3627
+ try {
3628
+ const { status, body } = await runRequestDomainVerification(
3629
+ (req) => this.authManager.handleRequest(req),
3630
+ c.req.raw
3631
+ );
3632
+ return c.json(body, status);
3633
+ } catch (error) {
3634
+ const err = error instanceof Error ? error : new Error(String(error));
3635
+ ctx.logger.error("[AuthPlugin] sso/request-domain-verification bridge failed", err);
3636
+ return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
3637
+ }
3638
+ });
3639
+ rawApp.post(`${basePath}/admin/sso/verify-domain`, async (c) => {
3640
+ try {
3641
+ const { status, body } = await runVerifyDomain(
3642
+ (req) => this.authManager.handleRequest(req),
3643
+ c.req.raw
3644
+ );
3645
+ return c.json(body, status);
3646
+ } catch (error) {
3647
+ const err = error instanceof Error ? error : new Error(String(error));
3648
+ ctx.logger.error("[AuthPlugin] sso/verify-domain bridge failed", err);
3649
+ return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
3650
+ }
3651
+ });
2960
3652
  rawApp.post(`${basePath}/sys-oauth-application/register`, async (c) => {
2961
3653
  try {
2962
3654
  let body = {};
@@ -3123,8 +3815,13 @@ var AuthPlugin = class {
3123
3815
  buildTwoFactorPluginSchema,
3124
3816
  createObjectQLAdapter,
3125
3817
  createObjectQLAdapterFactory,
3818
+ ipMatchesRange,
3126
3819
  resolveProtocolName,
3820
+ runRegisterSamlProviderFromForm,
3821
+ runRegisterSsoProviderFromForm,
3822
+ runRequestDomainVerification,
3127
3823
  runSetInitialPassword,
3824
+ runVerifyDomain,
3128
3825
  withSystemReadContext
3129
3826
  });
3130
3827
  //# sourceMappingURL=index.js.map