@better-auth/infra 0.3.7 → 0.4.1

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
@@ -1,15 +1,15 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-AfApXLhx.mjs";
2
- import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto--3ycICW4.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-CtvGRrlI.mjs";
2
+ import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-CV91nSbp.mjs";
3
3
  import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
4
- import { getCurrentAuthContext } from "@better-auth/core/context";
4
+ import { getCurrentAdapter, getCurrentAuthContext, getCurrentDBAdapterAsyncLocalStorage, runWithTransaction } from "@better-auth/core/context";
5
5
  import { APIError, generateId, getAuthTables, logger } from "better-auth";
6
6
  import { env } from "@better-auth/core/env";
7
7
  import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
8
+ import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
8
9
  import { createFetch } from "@better-fetch/fetch";
9
10
  import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js";
11
+ import z, { z as z$1 } from "zod";
10
12
  import { createLocalJWKSet, jwtVerify } from "jose";
11
- import z$1, { z } from "zod";
12
- import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
13
13
  import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
14
14
  import { createOTP } from "@better-auth/utils/otp";
15
15
  //#region src/options.ts
@@ -18,25 +18,38 @@ function resolveConnectionOptions(options) {
18
18
  apiUrl: options?.apiUrl || INFRA_API_URL,
19
19
  kvUrl: options?.kvUrl || INFRA_KV_URL,
20
20
  apiKey: options?.apiKey || env.BETTER_AUTH_API_KEY || "",
21
- apiTimeout: options?.apiTimeout ?? 3e3,
22
- kvTimeout: options?.kvTimeout ?? 1e3
21
+ apiOptions: { timeout: options?.apiOptions?.timeout ?? options?.apiTimeout ?? 3e3 },
22
+ kvOptions: {
23
+ timeout: options?.kvOptions?.timeout ?? options?.kvTimeout ?? 1e3,
24
+ retry: {
25
+ attempts: options?.kvOptions?.retry?.attempts ?? 2,
26
+ baseDelay: options?.kvOptions?.retry?.baseDelay ?? 400,
27
+ maxDelay: options?.kvOptions?.retry?.maxDelay ?? 600
28
+ }
29
+ }
23
30
  };
24
31
  }
25
32
  function resolveDashOptions(options) {
26
- const activityUpdateInterval = options?.activityTracking?.updateInterval ?? 3e5;
27
33
  return {
28
34
  ...resolveConnectionOptions(options),
29
- ...options,
30
35
  activityTracking: {
31
36
  ...options?.activityTracking,
32
- updateInterval: activityUpdateInterval
37
+ updateInterval: options?.activityTracking?.updateInterval ?? 3e5
38
+ },
39
+ managedDirectorySync: {
40
+ enabled: options?.managedDirectorySync?.enabled ?? false,
41
+ ssoPairing: options?.managedDirectorySync?.ssoPairing ?? true,
42
+ membershipProjection: {
43
+ enabled: options?.managedDirectorySync?.membershipProjection?.enabled ?? true,
44
+ role: options?.managedDirectorySync?.membershipProjection?.role ?? "member"
45
+ }
33
46
  }
34
47
  };
35
48
  }
36
49
  function resolveSentinelOptions(options) {
37
50
  return {
38
51
  ...resolveConnectionOptions(options),
39
- ...options
52
+ security: options?.security
40
53
  };
41
54
  }
42
55
  //#endregion
@@ -149,7 +162,12 @@ const USER_EVENT_TYPES = {
149
162
  //#endregion
150
163
  //#region src/events/core/adapter.ts
151
164
  function resolveUserFromContext(userId, ctx) {
152
- for (const candidate of [ctx.context.session?.user, ctx.context.newSession?.user]) if (candidate?.id === userId) return {
165
+ const returnedUser = ctx.context.returned?.user;
166
+ for (const candidate of [
167
+ ctx.context.session?.user,
168
+ ctx.context.newSession?.user,
169
+ returnedUser
170
+ ]) if (candidate?.id === userId) return {
153
171
  id: candidate.id,
154
172
  name: candidate.name,
155
173
  email: candidate.email ?? null,
@@ -295,7 +313,8 @@ function tryDecode(value) {
295
313
  const stripQuery = (value) => value.split("?")[0] || value;
296
314
  const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
297
315
  const routeToRegex = (route) => {
298
- const pattern = escapeRegex(stripQuery(route)).replace(/\/:([^/]+)/g, "/[^/]+");
316
+ const normalized = stripQuery(route);
317
+ const pattern = escapeRegex(normalized).replace(/\/:([^/]+)/g, "/[^/]+");
299
318
  return new RegExp(`^${pattern}(?:$|[/?])`);
300
319
  };
301
320
  const matchesAnyRoute = (path, routes) => {
@@ -341,8 +360,80 @@ const getLoginMethod = (ctx) => {
341
360
  return null;
342
361
  };
343
362
  //#endregion
363
+ //#region src/compat/account.ts
364
+ /**
365
+ * Mirrors better-auth 1.7+ `createLocalAccountIssuer` so infra can
366
+ * typecheck against older core packages while emitting the same issuer strings.
367
+ */
368
+ function createLocalAccountIssuer(providerId) {
369
+ return `local:${encodeURIComponent(providerId)}`;
370
+ }
371
+ /**
372
+ * Mirrors better-auth 1.7+ `createOAuthAccountIssuer`.
373
+ */
374
+ function createOAuthAccountIssuer(providerId) {
375
+ return `local:oauth:${encodeURIComponent(providerId)}`;
376
+ }
377
+ /** True when the installed better-auth build scopes accounts by issuer. */
378
+ function supportsIssuerScopedAccounts(adapter) {
379
+ return typeof adapter.findAccountByKey === "function";
380
+ }
381
+ /** Creates a credential account across pre/post issuer-scoped account schemas. */
382
+ async function createCredentialAccountCompat(adapter, params) {
383
+ if (supportsIssuerScopedAccounts(adapter)) {
384
+ await adapter.createAccount({
385
+ userId: params.userId,
386
+ providerId: "credential",
387
+ accountId: params.userId,
388
+ issuer: createLocalAccountIssuer("credential"),
389
+ password: params.password
390
+ });
391
+ return;
392
+ }
393
+ await adapter.createAccount({
394
+ userId: params.userId,
395
+ providerId: "credential",
396
+ accountId: params.userId,
397
+ password: params.password
398
+ });
399
+ }
400
+ /** Finds an unlink target by provider and local account row id. */
401
+ function resolveAccountForUnlink(accounts, selector) {
402
+ return accounts.find((account) => account.providerId === selector.providerId && account.id === selector.accountId);
403
+ }
404
+ //#endregion
344
405
  //#region src/events/core/oauth-callback-user.ts
345
406
  const OAUTH_CALLBACK_USER = Symbol.for("dash.oauthCallbackUser");
407
+ function normalizeProviderSubject(subject) {
408
+ if (!subject || subject === "undefined" || subject === "null") return;
409
+ return subject;
410
+ }
411
+ async function resolveAccountKey(provider, tokens, profile) {
412
+ const keyedProvider = provider;
413
+ const context = {
414
+ tokens,
415
+ profile
416
+ };
417
+ try {
418
+ let rawSubject;
419
+ if (keyedProvider.accountSubject) rawSubject = String(await keyedProvider.accountSubject(context));
420
+ else {
421
+ const { id, sub } = profile;
422
+ const value = id ?? sub;
423
+ rawSubject = value == null ? void 0 : String(value);
424
+ }
425
+ const accountId = rawSubject ? normalizeProviderSubject(rawSubject) : void 0;
426
+ if (!accountId) return void 0;
427
+ const issuer = typeof keyedProvider.accountIssuer === "function" ? await keyedProvider.accountIssuer(context) : keyedProvider.accountIssuer ?? createOAuthAccountIssuer(provider.id);
428
+ if (!issuer) return void 0;
429
+ return {
430
+ issuer,
431
+ accountId
432
+ };
433
+ } catch {
434
+ return;
435
+ }
436
+ }
346
437
  /**
347
438
  * Stash OAuth profile on the request context when better-auth calls getUserInfo
348
439
  * during the callback, before the authorization code is consumed.
@@ -355,19 +446,36 @@ function instrumentSocialProviders(providers) {
355
446
  const user = result?.user;
356
447
  if (user) try {
357
448
  const endpointCtx = await getCurrentAuthContext();
358
- endpointCtx.context[OAUTH_CALLBACK_USER] = user;
449
+ const adapter = endpointCtx.context.internalAdapter;
450
+ const accountKey = result.data && typeof adapter.findAccountOwnerByKey === "function" ? await resolveAccountKey(provider, token, result.data) : void 0;
451
+ endpointCtx.context[OAUTH_CALLBACK_USER] = {
452
+ user,
453
+ accountKey
454
+ };
359
455
  } catch {}
360
456
  return result;
361
457
  };
362
458
  }
363
459
  }
364
460
  async function resolveOAuthUser(providerId, ctx) {
365
- const oauthUser = ctx.context[OAUTH_CALLBACK_USER];
366
- if (!oauthUser) return null;
461
+ const stashed = ctx.context[OAUTH_CALLBACK_USER];
462
+ if (!stashed) return null;
463
+ const { user: oauthUser, accountKey } = stashed;
367
464
  const email = oauthUser.email?.toLowerCase();
465
+ const adapter = ctx.context.internalAdapter;
466
+ if (accountKey && typeof adapter.findAccountOwnerByKey === "function") try {
467
+ const owned = await adapter.findAccountOwnerByKey(accountKey);
468
+ if (owned?.kind === "owned") return {
469
+ id: owned.user.id,
470
+ email: owned.user.email,
471
+ name: owned.user.name
472
+ };
473
+ } catch (error) {
474
+ logger.debug("[Dash] Failed to find OAuth user by account key:", error);
475
+ }
368
476
  const accountId = oauthUser.id !== void 0 && oauthUser.id !== null ? String(oauthUser.id) : void 0;
369
- if (email && accountId) try {
370
- const result = await ctx.context.internalAdapter.findOAuthUser(email, accountId, providerId);
477
+ if (email && accountId && typeof adapter.findOAuthUser === "function") try {
478
+ const result = await adapter.findOAuthUser(email, accountId, providerId);
371
479
  if (result?.user) return {
372
480
  id: result.user.id,
373
481
  email: result.user.email,
@@ -941,7 +1049,7 @@ function resolveClientIpFromHeaders(headers, ipAddressHeaders) {
941
1049
  /**
942
1050
  * Identification Service
943
1051
  *
944
- * Fetches identification data from the durable-kv service
1052
+ * Fetches identification data from the identify API
945
1053
  * when a request includes an X-Request-Id header.
946
1054
  */
947
1055
  const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
@@ -957,31 +1065,38 @@ function cleanupCache() {
957
1065
  function maybeCleanup() {
958
1066
  if (Date.now() - lastCleanup > CACHE_TTL_MS || identificationCache.size > CACHE_MAX_SIZE) cleanupCache();
959
1067
  }
960
- /** Retries 404 races with exponential backoff (≤1s total wait across retries). */
961
- const IDENTIFY_GET_RETRY = {
962
- type: "exponential",
1068
+ const DEFAULT_KV_RETRY = {
963
1069
  attempts: 2,
964
1070
  baseDelay: 400,
965
- maxDelay: 600,
966
- shouldRetry(response) {
967
- if (response === null) return true;
968
- return response.status === 404;
969
- }
1071
+ maxDelay: 600
970
1072
  };
971
- function identifyGetRetryDelay(attempt) {
972
- return Math.min(600, 400 * 2 ** attempt);
1073
+ function resolveIdentifyGetRetry(retry) {
1074
+ return {
1075
+ type: "exponential",
1076
+ attempts: retry.attempts,
1077
+ baseDelay: retry.baseDelay,
1078
+ maxDelay: retry.maxDelay,
1079
+ shouldRetry(response) {
1080
+ if (response === null) return true;
1081
+ return response.status === 404;
1082
+ }
1083
+ };
1084
+ }
1085
+ function identifyGetRetryDelay(retry, attempt) {
1086
+ return Math.min(retry.maxDelay, retry.baseDelay * 2 ** attempt);
973
1087
  }
974
1088
  /**
975
- * Fetch identification data from durable-kv by requestId
1089
+ * Fetch identification data from the identify API by requestId
976
1090
  */
977
- async function getIdentification(requestId, $kv) {
1091
+ async function getIdentification(requestId, $kv, retryOptions = DEFAULT_KV_RETRY) {
978
1092
  maybeCleanup();
979
1093
  const cached = identificationCache.get(requestId);
980
1094
  if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
1095
+ const retry = resolveIdentifyGetRetry(retryOptions);
981
1096
  for (let networkAttempt = 0;; networkAttempt++) try {
982
1097
  const { data, error } = await $kv(`/identify/${requestId}`, {
983
1098
  method: "GET",
984
- retry: IDENTIFY_GET_RETRY
1099
+ retry
985
1100
  });
986
1101
  if (data && !error) {
987
1102
  identificationCache.set(requestId, {
@@ -996,11 +1111,11 @@ async function getIdentification(requestId, $kv) {
996
1111
  });
997
1112
  return null;
998
1113
  } catch (error) {
999
- if (networkAttempt >= IDENTIFY_GET_RETRY.attempts) {
1114
+ if (networkAttempt >= retry.attempts) {
1000
1115
  logger.error("[Dash] Failed to fetch identification:", error);
1001
1116
  return null;
1002
1117
  }
1003
- await new Promise((resolve) => setTimeout(resolve, identifyGetRetryDelay(networkAttempt)));
1118
+ await new Promise((resolve) => setTimeout(resolve, identifyGetRetryDelay(retryOptions, networkAttempt)));
1004
1119
  }
1005
1120
  }
1006
1121
  /**
@@ -1046,6 +1161,7 @@ function resolveUntrustedVisitorId(visitorId, ip, headerVisitorId) {
1046
1161
  * @param $kv — KV client from {@link createKV} for the same plugin options (one instance per plugin).
1047
1162
  */
1048
1163
  function createIdentificationMiddleware($kv, options) {
1164
+ const retry = options?.retry ?? DEFAULT_KV_RETRY;
1049
1165
  return createAuthMiddleware(async (ctx) => {
1050
1166
  const skipIdentification = options?.skipIdentification?.(ctx) ?? false;
1051
1167
  let headerVisitorId = null;
@@ -1058,7 +1174,7 @@ function createIdentificationMiddleware($kv, options) {
1058
1174
  ctx.context.requestId = requestId;
1059
1175
  if (skipIdentification) ctx.context.identification = null;
1060
1176
  else if (requestId) {
1061
- if (ctx.context.identification === void 0) ctx.context.identification = await getIdentification(requestId, $kv) ?? null;
1177
+ if (ctx.context.identification === void 0) ctx.context.identification = await getIdentification(requestId, $kv, retry) ?? null;
1062
1178
  } else ctx.context.identification = null;
1063
1179
  const identification = ctx.context.identification;
1064
1180
  const visitorId = resolveSecurityVisitorId(headerVisitorId, identification);
@@ -1181,8 +1297,9 @@ function isEmailNormalizationEnabled(security) {
1181
1297
  * @param $api — Dash client from `createAPI(opts, { throw: true })`.
1182
1298
  */
1183
1299
  function createSecurityClient(conn, $api, options, onSecurityEvent) {
1300
+ const resolvedApiUrl = conn.apiUrl || INFRA_API_URL;
1184
1301
  const emailSender = createEmailSender({
1185
- apiUrl: conn.apiUrl || INFRA_API_URL,
1302
+ apiUrl: resolvedApiUrl,
1186
1303
  apiKey: conn.apiKey
1187
1304
  });
1188
1305
  function logEvent(event) {
@@ -1626,7 +1743,7 @@ const authPaths = [
1626
1743
  "/email-otp/send-verification-otp"
1627
1744
  ];
1628
1745
  const registration = new Set(registrationPaths);
1629
- const all = new Set([...registrationPaths, ...authPaths]);
1746
+ const all = /* @__PURE__ */ new Set([...registrationPaths, ...authPaths]);
1630
1747
  /** Path carries an email we hook for normalization + syntax validation. */
1631
1748
  const allEmail = ({ path }) => !!path && all.has(path);
1632
1749
  /**
@@ -1639,11 +1756,11 @@ const registrationEmail = ({ path }) => !!path && registration.has(path);
1639
1756
  /**
1640
1757
  * Gmail-like providers that ignore dots in the local part
1641
1758
  */
1642
- const GMAIL_LIKE_DOMAINS = new Set(["gmail.com", "googlemail.com"]);
1759
+ const GMAIL_LIKE_DOMAINS = /* @__PURE__ */ new Set(["gmail.com", "googlemail.com"]);
1643
1760
  /**
1644
1761
  * Providers known to support plus addressing
1645
1762
  */
1646
- const PLUS_ADDRESSING_DOMAINS = new Set([
1763
+ const PLUS_ADDRESSING_DOMAINS = /* @__PURE__ */ new Set([
1647
1764
  "gmail.com",
1648
1765
  "googlemail.com",
1649
1766
  "outlook.com",
@@ -1913,7 +2030,7 @@ function createEmailHooks(options = {}) {
1913
2030
  * Common fake/test phone numbers that should be blocked
1914
2031
  * These are numbers commonly used in testing, movies, documentation, etc.
1915
2032
  */
1916
- const INVALID_PHONE_NUMBERS = new Set([
2033
+ const INVALID_PHONE_NUMBERS = /* @__PURE__ */ new Set([
1917
2034
  "+15550000000",
1918
2035
  "+15550001111",
1919
2036
  "+15550001234",
@@ -2002,7 +2119,7 @@ const INVALID_PHONE_PATTERNS = [
2002
2119
  * Key: country code, Value: set of invalid prefixes
2003
2120
  */
2004
2121
  const INVALID_PREFIXES_BY_COUNTRY = {
2005
- US: new Set([
2122
+ US: /* @__PURE__ */ new Set([
2006
2123
  "555",
2007
2124
  "000",
2008
2125
  "111",
@@ -2010,17 +2127,17 @@ const INVALID_PREFIXES_BY_COUNTRY = {
2010
2127
  "411",
2011
2128
  "611"
2012
2129
  ]),
2013
- CA: new Set([
2130
+ CA: /* @__PURE__ */ new Set([
2014
2131
  "555",
2015
2132
  "000",
2016
2133
  "911"
2017
2134
  ]),
2018
- GB: new Set([
2135
+ GB: /* @__PURE__ */ new Set([
2019
2136
  "7700900",
2020
2137
  "1632960",
2021
2138
  "1134960"
2022
2139
  ]),
2023
- AU: new Set([
2140
+ AU: /* @__PURE__ */ new Set([
2024
2141
  "0491570",
2025
2142
  "0491571",
2026
2143
  "0491572"
@@ -2090,7 +2207,7 @@ const validatePhone = (phone, options = {}) => {
2090
2207
  if (blockVoip && phoneType === "VOIP") return false;
2091
2208
  return true;
2092
2209
  };
2093
- const allPhonePaths = new Set([
2210
+ const allPhonePaths = /* @__PURE__ */ new Set([
2094
2211
  "/phone-number/send-otp",
2095
2212
  "/phone-number/verify",
2096
2213
  "/sign-in/phone-number",
@@ -2188,9 +2305,24 @@ const sentinel = (options) => {
2188
2305
  const opts = resolveSentinelOptions(options);
2189
2306
  const $api = createAPI(opts);
2190
2307
  const $apiThrowing = createAPI(opts, { throw: true });
2191
- const $kv = createKV(opts);
2308
+ const $kv = createKV({
2309
+ kvUrl: opts.kvUrl,
2310
+ apiKey: opts.apiKey,
2311
+ timeout: opts.kvOptions.timeout
2312
+ });
2192
2313
  const { tracker } = initTrackEvents($api);
2193
2314
  const { trackEvent } = tracker;
2315
+ const STALE_ACCOUNT_BLOCK_ERROR = {
2316
+ message: "This account has been inactive for an extended period. Please contact support to reactivate.",
2317
+ code: "STALE_ACCOUNT"
2318
+ };
2319
+ function isStaleAccountError(returned) {
2320
+ if (!(returned instanceof Error)) return false;
2321
+ const err = returned;
2322
+ if (err.code === STALE_ACCOUNT_BLOCK_ERROR.code) return true;
2323
+ return typeof err.body === "object" && err.body !== null && "code" in err.body && err.body.code === STALE_ACCOUNT_BLOCK_ERROR.code;
2324
+ }
2325
+ let activityTrackingEnabled = false;
2194
2326
  if (!opts.apiKey) logger.warn("[Sentinel] Missing BETTER_AUTH_API_KEY. Security checks may fall back to allow mode when the Infra API rejects requests.");
2195
2327
  const securityService = createSecurityClient(opts, $apiThrowing, opts.security || {}, (event) => {
2196
2328
  trackEvent({
@@ -2220,7 +2352,7 @@ const sentinel = (options) => {
2220
2352
  return {
2221
2353
  id: "sentinel",
2222
2354
  init(ctx) {
2223
- const activityTrackingEnabled = (ctx.getPlugin("dash")?.options)?.activityTracking?.enabled === true;
2355
+ activityTrackingEnabled = (ctx.getPlugin("dash")?.options)?.activityTracking?.enabled === true;
2224
2356
  return { options: {
2225
2357
  emailValidation: opts.security?.emailValidation,
2226
2358
  emailNormalization: opts.security?.emailNormalization,
@@ -2324,35 +2456,6 @@ const sentinel = (options) => {
2324
2456
  if (visitorId) {
2325
2457
  if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
2326
2458
  }
2327
- if (opts.security?.staleUsers?.enabled && user) {
2328
- recordCheck(ctx, "stale_users");
2329
- const lastActiveAtForStale = activityTrackingEnabled ? user.lastActiveAt ?? null : null;
2330
- const staleCheck = await securityService.checkStaleUser(session.userId, lastActiveAtForStale);
2331
- if (staleCheck.isStale) {
2332
- const staleOpts = opts.security.staleUsers;
2333
- const notificationPromises = [];
2334
- if (staleCheck.notifyUser && user.email) notificationPromises.push(securityService.notifyStaleAccountUser(user.email, user.name || null, staleCheck.daysSinceLastActive || 0, identification));
2335
- if (staleCheck.notifyAdmin && staleOpts.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, session.userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
2336
- if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
2337
- logger.error("[Sentinel] Failed to send stale account notifications:", error);
2338
- });
2339
- if (staleCheck.action === "block") {
2340
- setOutcome(ctx, "blocked", "stale_users", {
2341
- userId: session.userId,
2342
- daysSinceLastActive: staleCheck.daysSinceLastActive,
2343
- staleDays: staleCheck.staleDays,
2344
- lastActiveAt: staleCheck.lastActiveAt,
2345
- notifyUser: staleCheck.notifyUser,
2346
- notifyAdmin: staleCheck.notifyAdmin
2347
- });
2348
- emitEvaluation(ctx, trackEvent);
2349
- throw new APIError("FORBIDDEN", {
2350
- message: "This account has been inactive for an extended period. Please contact support to reactivate.",
2351
- code: "STALE_ACCOUNT"
2352
- });
2353
- }
2354
- }
2355
- }
2356
2459
  if (opts.security?.impossibleTravel?.enabled && identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location, identification.ip));
2357
2460
  }
2358
2461
  } }
@@ -2363,7 +2466,10 @@ const sentinel = (options) => {
2363
2466
  before: [
2364
2467
  {
2365
2468
  matcher: (ctx) => ctx.request?.method !== "GET",
2366
- handler: createIdentificationMiddleware($kv, { skipIdentification: (ctx) => isDashRoute(ctx.path) })
2469
+ handler: createIdentificationMiddleware($kv, {
2470
+ skipIdentification: (ctx) => isDashRoute(ctx.path),
2471
+ retry: opts.kvOptions.retry
2472
+ })
2367
2473
  },
2368
2474
  ...emailHooks.before,
2369
2475
  ...phoneValidationHooks.before,
@@ -2462,6 +2568,52 @@ const sentinel = (options) => {
2462
2568
  }
2463
2569
  ],
2464
2570
  after: [{
2571
+ matcher: (ctx) => !!opts.security?.staleUsers?.enabled && !isDashRoute(ctx.path),
2572
+ handler: createAuthMiddleware(async (ctx) => {
2573
+ if (ctx.context.returned instanceof Error) return;
2574
+ const created = ctx.context.newSession;
2575
+ const userId = created?.user?.id ?? created?.session?.userId;
2576
+ const sessionToken = created?.session?.token;
2577
+ if (!userId || !sessionToken) return;
2578
+ let user = created?.user ?? null;
2579
+ try {
2580
+ user = await getUserById(userId, ctx, { includeLastActiveAt: activityTrackingEnabled }) ?? user;
2581
+ } catch (error) {
2582
+ logger.warn("[Sentinel] Failed to fetch user for stale-account check:", error);
2583
+ if (!user) return;
2584
+ }
2585
+ if (!user) return;
2586
+ recordCheck(ctx, "stale_users");
2587
+ const staleCheck = await securityService.checkStaleUser(userId, activityTrackingEnabled ? user.lastActiveAt ?? null : null);
2588
+ if (!staleCheck.isStale) return;
2589
+ const identification = ctx.context.identification;
2590
+ const staleOpts = opts.security?.staleUsers;
2591
+ const notificationPromises = [];
2592
+ if (staleCheck.notifyUser && user.email) notificationPromises.push(securityService.notifyStaleAccountUser(user.email, user.name || null, staleCheck.daysSinceLastActive || 0, identification));
2593
+ if (staleCheck.notifyAdmin && staleOpts?.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
2594
+ if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
2595
+ logger.error("[Sentinel] Failed to send stale account notifications:", error);
2596
+ });
2597
+ if (staleCheck.action !== "block") return;
2598
+ setOutcome(ctx, "blocked", "stale_users", {
2599
+ userId,
2600
+ daysSinceLastActive: staleCheck.daysSinceLastActive,
2601
+ staleDays: staleCheck.staleDays,
2602
+ lastActiveAt: staleCheck.lastActiveAt,
2603
+ notifyUser: staleCheck.notifyUser,
2604
+ notifyAdmin: staleCheck.notifyAdmin
2605
+ });
2606
+ emitEvaluation(ctx, trackEvent);
2607
+ try {
2608
+ await ctx.context.internalAdapter.deleteSession(sessionToken);
2609
+ } catch (error) {
2610
+ logger.warn("[Sentinel] Failed to delete stale-blocked session:", error);
2611
+ }
2612
+ deleteSessionCookie(ctx);
2613
+ ctx.context.setNewSession(null);
2614
+ throw new APIError("FORBIDDEN", STALE_ACCOUNT_BLOCK_ERROR);
2615
+ })
2616
+ }, {
2465
2617
  matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
2466
2618
  handler: createAuthMiddleware(async (ctx) => {
2467
2619
  const untrustedVisitorId = ctx.context.untrustedVisitorId;
@@ -2477,14 +2629,433 @@ const sentinel = (options) => {
2477
2629
  identifier: loginId,
2478
2630
  userAgent: ctx.headers?.get?.("user-agent") || ""
2479
2631
  });
2480
- if (isPasswordSignInRoute && ctx.context.returned instanceof Error && loginId && body?.password && untrustedVisitorId) await ctx.context.runInBackgroundOrAwait(securityService.trackFailedAttempt(loginId, untrustedVisitorId, body.password, ip, ctx.context.requestId ?? null));
2481
- if (isPasswordSignInRoute && !(ctx.context.returned instanceof Error) && loginId) await ctx.context.runInBackgroundOrAwait(securityService.clearFailedAttempts(loginId));
2632
+ const returned = ctx.context.returned;
2633
+ const staleBlocked = isStaleAccountError(returned);
2634
+ if (isPasswordSignInRoute && returned instanceof Error && !staleBlocked && loginId && body?.password && untrustedVisitorId) await ctx.context.runInBackgroundOrAwait(securityService.trackFailedAttempt(loginId, untrustedVisitorId, body.password, ip, ctx.context.requestId ?? null));
2635
+ if (isPasswordSignInRoute && (!(returned instanceof Error) || staleBlocked) && loginId) await ctx.context.runInBackgroundOrAwait(securityService.clearFailedAttempts(loginId));
2482
2636
  })
2483
2637
  }]
2484
2638
  }
2485
2639
  };
2486
2640
  };
2487
2641
  //#endregion
2642
+ //#region src/directory-sync/membership-projection.ts
2643
+ async function createMembershipKey(provisioningDomainId, userId) {
2644
+ return `directory-sync-membership:${await hash$1(JSON.stringify([provisioningDomainId, userId]))}`;
2645
+ }
2646
+ async function findDirectory(input, context) {
2647
+ const row = await context.database.findOne({
2648
+ model: "directorySyncConnection",
2649
+ where: [{
2650
+ field: "provisioningDomainId",
2651
+ value: input.provisioningDomainId
2652
+ }]
2653
+ });
2654
+ if (!row || row.status !== "active" && row.status !== "decommissioning" && row.status !== "decommissioned" || !row.connectionId) return null;
2655
+ return row;
2656
+ }
2657
+ async function findMembership(context, organizationId, userId) {
2658
+ const memberships = await context.database.findMany({
2659
+ model: "member",
2660
+ where: [{
2661
+ field: "organizationId",
2662
+ value: organizationId
2663
+ }, {
2664
+ field: "userId",
2665
+ value: userId
2666
+ }]
2667
+ });
2668
+ if (memberships.length > 1) throw new Error("Directory sync membership projection requires a unique organization and user membership");
2669
+ return memberships.at(0) ?? null;
2670
+ }
2671
+ async function findProvenance(context, membershipKey) {
2672
+ return await context.database.findOne({
2673
+ model: "directorySyncMembershipProvenance",
2674
+ where: [{
2675
+ field: "membershipKey",
2676
+ value: membershipKey
2677
+ }]
2678
+ });
2679
+ }
2680
+ async function createProvenance(context, input) {
2681
+ const now = /* @__PURE__ */ new Date();
2682
+ await context.database.create({
2683
+ model: "directorySyncMembershipProvenance",
2684
+ data: {
2685
+ ...input,
2686
+ createdAt: now,
2687
+ updatedAt: now
2688
+ }
2689
+ });
2690
+ }
2691
+ function createOrganizationMembershipProjection(options) {
2692
+ if (!options.role.trim()) throw new Error("Directory sync membership role must not be empty");
2693
+ return async (input, context) => {
2694
+ const directory = await findDirectory(input, context);
2695
+ if (!directory?.connectionId) return;
2696
+ const membershipKey = await createMembershipKey(input.provisioningDomainId, input.userId);
2697
+ const [membership, provenance] = await Promise.all([findMembership(context, directory.organizationId, input.userId), findProvenance(context, membershipKey)]);
2698
+ if (provenance && (provenance.ownership !== "created" && provenance.ownership !== "observed" || provenance.provisioningDomainId !== input.provisioningDomainId || provenance.organizationId !== directory.organizationId || provenance.userId !== input.userId)) throw new Error("Directory sync membership provenance does not match its provisioning domain");
2699
+ if (!input.active) {
2700
+ if (!provenance) return;
2701
+ if (provenance.ownership === "created" && membership?.id === provenance.memberId && membership.organizationId === provenance.organizationId && membership.userId === provenance.userId) await context.database.delete({
2702
+ model: "member",
2703
+ where: [
2704
+ {
2705
+ field: "id",
2706
+ value: provenance.memberId
2707
+ },
2708
+ {
2709
+ field: "organizationId",
2710
+ value: provenance.organizationId
2711
+ },
2712
+ {
2713
+ field: "userId",
2714
+ value: provenance.userId
2715
+ }
2716
+ ]
2717
+ });
2718
+ await context.database.delete({
2719
+ model: "directorySyncMembershipProvenance",
2720
+ where: [{
2721
+ field: "id",
2722
+ value: provenance.id
2723
+ }, {
2724
+ field: "membershipKey",
2725
+ value: membershipKey
2726
+ }]
2727
+ });
2728
+ return;
2729
+ }
2730
+ if (membership) {
2731
+ if (provenance?.memberId === membership.id) return;
2732
+ if (provenance) await context.database.delete({
2733
+ model: "directorySyncMembershipProvenance",
2734
+ where: [{
2735
+ field: "id",
2736
+ value: provenance.id
2737
+ }, {
2738
+ field: "membershipKey",
2739
+ value: membershipKey
2740
+ }]
2741
+ });
2742
+ await createProvenance(context, {
2743
+ membershipKey,
2744
+ organizationId: directory.organizationId,
2745
+ userId: input.userId,
2746
+ memberId: membership.id,
2747
+ ownership: "observed",
2748
+ provisioningDomainId: input.provisioningDomainId
2749
+ });
2750
+ return;
2751
+ }
2752
+ if (provenance) await context.database.delete({
2753
+ model: "directorySyncMembershipProvenance",
2754
+ where: [{
2755
+ field: "id",
2756
+ value: provenance.id
2757
+ }, {
2758
+ field: "membershipKey",
2759
+ value: membershipKey
2760
+ }]
2761
+ });
2762
+ const now = /* @__PURE__ */ new Date();
2763
+ const createdMembership = await context.database.create({
2764
+ model: "member",
2765
+ data: {
2766
+ organizationId: directory.organizationId,
2767
+ userId: input.userId,
2768
+ role: options.role,
2769
+ createdAt: now
2770
+ }
2771
+ });
2772
+ await createProvenance(context, {
2773
+ membershipKey,
2774
+ organizationId: directory.organizationId,
2775
+ userId: input.userId,
2776
+ memberId: createdMembership.id,
2777
+ ownership: "created",
2778
+ provisioningDomainId: input.provisioningDomainId
2779
+ });
2780
+ };
2781
+ }
2782
+ //#endregion
2783
+ //#region src/routes/directory-sync/contract.ts
2784
+ const DIRECTORY_SYNC_PURPOSE = "directory-sync-management";
2785
+ const ALL_SCIM_SCOPES = [
2786
+ "scim.users.read",
2787
+ "scim.users.write",
2788
+ "scim.groups.read",
2789
+ "scim.groups.write"
2790
+ ];
2791
+ const scimScopeSchema = z.enum(ALL_SCIM_SCOPES);
2792
+ const credentialPolicySchema = {
2793
+ scopes: z.array(scimScopeSchema).min(1).optional(),
2794
+ expiresAt: z.coerce.date().optional()
2795
+ };
2796
+ const directorySyncSSOPairingSchema = z.discriminatedUnion("protocol", [z.object({
2797
+ ssoProviderId: z.string().trim().min(1).max(255),
2798
+ protocol: z.literal("oidc"),
2799
+ externalIdSource: z.discriminatedUnion("kind", [z.object({ kind: z.literal("subject") }), z.object({
2800
+ kind: z.literal("verifiedIdTokenClaim"),
2801
+ name: z.string().trim().min(1).max(255)
2802
+ })])
2803
+ }), z.object({
2804
+ ssoProviderId: z.string().trim().min(1).max(255),
2805
+ protocol: z.literal("saml"),
2806
+ externalIdSource: z.discriminatedUnion("kind", [z.object({ kind: z.literal("nameId") }), z.object({
2807
+ kind: z.literal("attribute"),
2808
+ name: z.string().trim().min(1).max(255)
2809
+ })])
2810
+ })]);
2811
+ const createDirectoryBodySchema = z.object({
2812
+ providerId: z.string().trim().min(1).max(255),
2813
+ pairing: directorySyncSSOPairingSchema.optional(),
2814
+ ...credentialPolicySchema
2815
+ });
2816
+ const rotateCredentialBodySchema = z.object(credentialPolicySchema);
2817
+ const emptyBodySchema = z.object({});
2818
+ function setCredentialResponseSecurityHeaders(ctx) {
2819
+ ctx.setHeader("Cache-Control", "no-store, max-age=0");
2820
+ ctx.setHeader("Pragma", "no-cache");
2821
+ ctx.setHeader("Referrer-Policy", "no-referrer");
2822
+ }
2823
+ //#endregion
2824
+ //#region src/directory-sync/pairing.ts
2825
+ let samlPolicyModule;
2826
+ function loadSAMLPolicy() {
2827
+ samlPolicyModule ??= import("./saml-policy-DQR0MBLk.mjs");
2828
+ return samlPolicyModule;
2829
+ }
2830
+ function isRecord$1(value) {
2831
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2832
+ }
2833
+ function parseConfiguration(value) {
2834
+ if (isRecord$1(value)) return value;
2835
+ if (typeof value !== "string") return null;
2836
+ try {
2837
+ const parsed = JSON.parse(value);
2838
+ return isRecord$1(parsed) ? parsed : null;
2839
+ } catch {
2840
+ return null;
2841
+ }
2842
+ }
2843
+ function parseDirectorySyncSSOPairing(value) {
2844
+ if (!value) return null;
2845
+ let parsed;
2846
+ try {
2847
+ parsed = JSON.parse(value);
2848
+ } catch {
2849
+ return null;
2850
+ }
2851
+ const result = directorySyncSSOPairingSchema.safeParse(parsed);
2852
+ return result.success ? result.data : null;
2853
+ }
2854
+ async function createActiveSSOProviderKey(ssoProviderRecordId) {
2855
+ return `directory-sync-sso-active:${await hash$1(ssoProviderRecordId)}`;
2856
+ }
2857
+ function createInactiveSSOProviderKey(aliasKey) {
2858
+ return `directory-sync-sso-inactive:${aliasKey}`;
2859
+ }
2860
+ function createTerminalSSOProviderKey(aliasKey) {
2861
+ return `directory-sync-sso-terminal:${aliasKey}`;
2862
+ }
2863
+ async function resolveDirectorySyncSSOPairing(ctx, organizationId, pairing) {
2864
+ const exactProvider = await ctx.context.adapter.update({
2865
+ model: "ssoProvider",
2866
+ where: [{
2867
+ field: "providerId",
2868
+ value: pairing.ssoProviderId
2869
+ }, {
2870
+ field: "organizationId",
2871
+ value: organizationId
2872
+ }],
2873
+ update: { providerId: pairing.ssoProviderId }
2874
+ });
2875
+ if (!exactProvider || exactProvider.providerId !== pairing.ssoProviderId || exactProvider.organizationId !== organizationId) throw ctx.error("BAD_REQUEST", { message: "The selected SSO provider is not available" });
2876
+ const oidcConfiguration = parseConfiguration(exactProvider.oidcConfig);
2877
+ const samlConfiguration = parseConfiguration(exactProvider.samlConfig);
2878
+ if (pairing.protocol === "oidc") {
2879
+ if (!oidcConfiguration || samlConfiguration) throw ctx.error("BAD_REQUEST", { message: "The selected SSO provider is not an OIDC provider" });
2880
+ } else {
2881
+ if (!samlConfiguration || oidcConfiguration) throw ctx.error("BAD_REQUEST", { message: "The selected SSO provider is not a SAML provider" });
2882
+ const samlPolicy = await loadSAMLPolicy();
2883
+ let wantAssertionsSigned = false;
2884
+ try {
2885
+ wantAssertionsSigned = await samlPolicy.requiresSignedSAMLAssertions({
2886
+ spMetadata: isRecord$1(samlConfiguration.spMetadata) ? { metadata: typeof samlConfiguration.spMetadata.metadata === "string" ? samlConfiguration.spMetadata.metadata : void 0 } : void 0,
2887
+ wantAssertionsSigned: samlConfiguration.wantAssertionsSigned === true
2888
+ });
2889
+ } catch {
2890
+ throw ctx.error("BAD_REQUEST", { message: "The selected SAML provider metadata is invalid" });
2891
+ }
2892
+ if (!wantAssertionsSigned) throw ctx.error("BAD_REQUEST", { message: "Paired SAML providers must require cryptographically signed assertions" });
2893
+ }
2894
+ return {
2895
+ pairing,
2896
+ ssoProviderId: exactProvider.providerId,
2897
+ ssoProviderRecordId: exactProvider.id,
2898
+ activeSsoProviderKey: await createActiveSSOProviderKey(exactProvider.id)
2899
+ };
2900
+ }
2901
+ function serializePairingRow(row) {
2902
+ return parseDirectorySyncSSOPairing(row.serializedSsoPairing);
2903
+ }
2904
+ async function guardDirectorySyncSSOProviderMutation(input, context) {
2905
+ if (input.providerReference.source.type !== "persisted" || input.providerReference.source.recordId !== input.provider.id) throw new Error("SSO provider mutation reference is not exact");
2906
+ if (await context.database.findOne({
2907
+ model: "directorySyncConnection",
2908
+ where: [
2909
+ {
2910
+ field: "ssoProviderRecordId",
2911
+ value: input.provider.id
2912
+ },
2913
+ {
2914
+ field: "ssoProviderId",
2915
+ value: input.provider.providerId
2916
+ },
2917
+ {
2918
+ field: "organizationId",
2919
+ value: input.provider.organizationId
2920
+ },
2921
+ {
2922
+ field: "pairingEnforced",
2923
+ value: true
2924
+ }
2925
+ ]
2926
+ })) {
2927
+ if (input.action === "update" && "isAuthenticationBoundaryChange" in input && input.isAuthenticationBoundaryChange === false) return;
2928
+ throw new Error("SSO provider is paired with directory sync");
2929
+ }
2930
+ }
2931
+ //#endregion
2932
+ //#region src/directory-sync/sso-user-resolution.ts
2933
+ const GENERIC_REJECTION = {
2934
+ action: "reject",
2935
+ code: "DIRECTORY_SYNC_AUTHENTICATION_FAILED",
2936
+ message: "Unable to sign in with this SSO connection"
2937
+ };
2938
+ let scimCatalogModule;
2939
+ function loadSCIMCatalog() {
2940
+ scimCatalogModule ??= import("@better-auth/scim");
2941
+ return scimCatalogModule;
2942
+ }
2943
+ function readStringExternalId(value) {
2944
+ if (typeof value === "string" && value.length > 0) return value;
2945
+ return null;
2946
+ }
2947
+ function readSAMLExternalId(value) {
2948
+ const scalar = readStringExternalId(value);
2949
+ if (scalar) return scalar;
2950
+ if (Array.isArray(value) && value.length === 1 && typeof value[0] === "string" && value[0].length > 0) return value[0];
2951
+ return null;
2952
+ }
2953
+ function readExternalId(input, pairing) {
2954
+ if (input.protocol !== pairing.protocol) return null;
2955
+ if (pairing.protocol === "oidc" && input.protocol === "oidc") return readStringExternalId(pairing.externalIdSource.kind === "subject" ? input.accountKey.accountId : input.verifiedIdTokenClaims[pairing.externalIdSource.name]);
2956
+ if (pairing.protocol === "saml" && input.protocol === "saml") return readSAMLExternalId(pairing.externalIdSource.kind === "nameId" ? input.accountKey.accountId : input.providerAttributes[pairing.externalIdSource.name]);
2957
+ return null;
2958
+ }
2959
+ async function resolveOrganizationDirectorySyncUser(input, context) {
2960
+ if (input.providerReference.source.type !== "persisted") return { action: "continue" };
2961
+ const directories = await context.database.findMany({
2962
+ model: "directorySyncConnection",
2963
+ where: [
2964
+ {
2965
+ field: "ssoProviderRecordId",
2966
+ value: input.providerReference.source.recordId
2967
+ },
2968
+ {
2969
+ field: "ssoProviderId",
2970
+ value: input.providerId
2971
+ },
2972
+ {
2973
+ field: "pairingEnforced",
2974
+ value: true
2975
+ }
2976
+ ]
2977
+ });
2978
+ if (directories.length === 0) return { action: "continue" };
2979
+ if (directories.length !== 1) return GENERIC_REJECTION;
2980
+ const directory = directories[0];
2981
+ if (directory?.status !== "active") return GENERIC_REJECTION;
2982
+ if (directory.activeSsoProviderKey !== await createActiveSSOProviderKey(input.providerReference.source.recordId)) return GENERIC_REJECTION;
2983
+ const pairing = parseDirectorySyncSSOPairing(directory.serializedSsoPairing);
2984
+ if (!pairing || pairing.ssoProviderId !== input.providerId || pairing.protocol !== input.protocol || !directory.connectionId) return GENERIC_REJECTION;
2985
+ const externalId = readExternalId(input, pairing);
2986
+ if (!externalId) return GENERIC_REJECTION;
2987
+ try {
2988
+ const catalog = await loadSCIMCatalog();
2989
+ if (typeof catalog.acquireActiveSCIMUserLink !== "function") return GENERIC_REJECTION;
2990
+ const link = await catalog.acquireActiveSCIMUserLink({
2991
+ connectionId: directory.connectionId,
2992
+ externalId
2993
+ }, { database: context.database });
2994
+ if (!link) return GENERIC_REJECTION;
2995
+ return {
2996
+ action: "link",
2997
+ userId: link.userId,
2998
+ profile: "preserve"
2999
+ };
3000
+ } catch {
3001
+ return GENERIC_REJECTION;
3002
+ }
3003
+ }
3004
+ //#endregion
3005
+ //#region src/directory-sync/instrument.ts
3006
+ /**
3007
+ * When managed directory sync is enabled, optionally install SSO/SCIM
3008
+ * callbacks on sibling plugins (same pattern as organization hook
3009
+ * instrumentation).
3010
+ */
3011
+ function instrumentDirectorySyncIntegration(ctx, options) {
3012
+ if (!options.ssoPairing && !options.membershipProjection.enabled) {
3013
+ logger.debug("[Dash] Managed directory sync instrumentation skipped (ssoPairing and membershipProjection are both disabled)");
3014
+ return;
3015
+ }
3016
+ const ssoPlugin = ctx.getPlugin("sso");
3017
+ const scimPlugin = ctx.getPlugin("scim");
3018
+ if (!ssoPlugin && !scimPlugin) {
3019
+ logger.debug("[Dash] Managed directory sync enabled but SSO/SCIM plugins are not active. Skipping integration instrumentation");
3020
+ return;
3021
+ }
3022
+ if (options.ssoPairing) {
3023
+ if (ssoPlugin) {
3024
+ if (!ssoPlugin.options) logger.error("[Dash] Managed directory sync ssoPairing requires sso({ ... }) (at least sso({})) so pairing callbacks can be installed on the shared options object.");
3025
+ else {
3026
+ const ssoOptions = ssoPlugin.options;
3027
+ const previousResolveUser = ssoOptions.resolveUser;
3028
+ ssoOptions.resolveUser = async (input, context) => {
3029
+ const result = await resolveOrganizationDirectorySyncUser(input, context);
3030
+ if (result.action !== "continue") return result;
3031
+ if (previousResolveUser) return previousResolveUser(input, context);
3032
+ return { action: "continue" };
3033
+ };
3034
+ const previousGuard = ssoOptions.guardProviderMutation;
3035
+ ssoOptions.guardProviderMutation = async (input, context) => {
3036
+ await guardDirectorySyncSSOProviderMutation(input, context);
3037
+ if (previousGuard) await previousGuard(input, context);
3038
+ };
3039
+ }
3040
+ } else logger.debug("[Dash] Managed directory sync ssoPairing enabled but SSO plugin is not active. Skipping SSO pairing instrumentation");
3041
+ }
3042
+ if (options.membershipProjection.enabled) {
3043
+ if (scimPlugin) {
3044
+ if (!scimPlugin.options) logger.error("[Dash] Managed directory sync membershipProjection requires scim({ ... }) with an options object so membership projection can be installed.");
3045
+ else {
3046
+ const scimOptions = scimPlugin.options;
3047
+ const projection = scimOptions.projection ??= {};
3048
+ const previousReconcileUser = projection.reconcileUser;
3049
+ const reconcileUser = createOrganizationMembershipProjection({ role: options.membershipProjection.role });
3050
+ projection.reconcileUser = async (input, context) => {
3051
+ await reconcileUser(input, context);
3052
+ if (previousReconcileUser) await previousReconcileUser(input, context);
3053
+ };
3054
+ }
3055
+ } else logger.debug("[Dash] Managed directory sync membershipProjection enabled but SCIM plugin is not active. Skipping membership projection instrumentation");
3056
+ }
3057
+ }
3058
+ //#endregion
2488
3059
  //#region src/events/organization/events-invitation.ts
2489
3060
  const initInvitationEvents = (tracker) => {
2490
3061
  const { trackEvent } = tracker;
@@ -2840,61 +3411,58 @@ const initTeamEvents = (tracker) => {
2840
3411
  };
2841
3412
  //#endregion
2842
3413
  //#region ../utils/dist/redact.mjs
2843
- const EXACT_SENSITIVE_STRING_KEYS_LOWER = new Set([...new Set([
2844
- "accessToken",
2845
- "apiKey",
2846
- "apiSecret",
2847
- "authorization",
2848
- "authToken",
2849
- "bearerToken",
2850
- "backupCodes",
2851
- "clientSecret",
2852
- "consumerSecret",
2853
- "credentialID",
2854
- "deviceCode",
2855
- "encPrivateKey",
2856
- "encPrivateKeyPass",
2857
- "encryptionKey",
2858
- "encryptionSecret",
2859
- "forwardHeaders",
2860
- "idToken",
2861
- "oidcConfig",
2862
- "pass",
2863
- "passwd",
2864
- "password",
2865
- "privateKey",
2866
- "privateKeyPass",
2867
- "pwd",
2868
- "refreshToken",
2869
- "samlConfig",
2870
- "secret",
2871
- "secretAccessKey",
2872
- "secretKey",
2873
- "signingSecret",
2874
- "stripeWebhookSecret",
2875
- "userCode",
2876
- "webhookSecret"
2877
- ])].map((k) => k.toLowerCase()));
2878
- /**
2879
- * Case-insensitive suffixes for unknown property names.
2880
- */
2881
- const SENSITIVE_KEY_SUFFIXES_LOWER = [
2882
- "accesskey",
2883
- "secret",
2884
- "password",
2885
- "passphrase",
2886
- "privatekey",
2887
- "keypass",
2888
- "apikey",
2889
- "token",
2890
- "signingkey",
2891
- "credentials",
2892
- "authheader"
2893
- ];
2894
- /**
2895
- * Embedded secret substrings checked against the normalized (compact, lowercased) key.
2896
- */
2897
- const SENSITIVE_KEY_SUBSTRINGS_LOWER = ["secretaccess", "accesskeysecret"];
3414
+ /** Built-in sensitive key patterns for consumers that want the defaults. */
3415
+ const SENSITIVE_KEY_PATTERNS = {
3416
+ keys: [
3417
+ "accessToken",
3418
+ "apiKey",
3419
+ "apiSecret",
3420
+ "authorization",
3421
+ "authToken",
3422
+ "bearerToken",
3423
+ "backupCodes",
3424
+ "clientSecret",
3425
+ "consumerSecret",
3426
+ "credentialID",
3427
+ "deviceCode",
3428
+ "encPrivateKey",
3429
+ "encPrivateKeyPass",
3430
+ "encryptionKey",
3431
+ "encryptionSecret",
3432
+ "forwardHeaders",
3433
+ "idToken",
3434
+ "oidcConfig",
3435
+ "pass",
3436
+ "passwd",
3437
+ "password",
3438
+ "privateKey",
3439
+ "privateKeyPass",
3440
+ "pwd",
3441
+ "refreshToken",
3442
+ "samlConfig",
3443
+ "secret",
3444
+ "secretAccessKey",
3445
+ "secretKey",
3446
+ "signingSecret",
3447
+ "stripeWebhookSecret",
3448
+ "userCode",
3449
+ "webhookSecret"
3450
+ ],
3451
+ suffixes: [
3452
+ "accesskey",
3453
+ "secret",
3454
+ "password",
3455
+ "passphrase",
3456
+ "privatekey",
3457
+ "keypass",
3458
+ "apikey",
3459
+ "token",
3460
+ "signingkey",
3461
+ "credentials",
3462
+ "authheader"
3463
+ ],
3464
+ substrings: ["secretaccess", "accesskeysecret"]
3465
+ };
2898
3466
  const REDACTED_SIMPLE_STRING = "[REDACTED]";
2899
3467
  function snakeCaseToCamelCase(key) {
2900
3468
  return key.replace(/_([a-zA-Z])/g, (_, ch) => ch.toUpperCase());
@@ -2902,24 +3470,36 @@ function snakeCaseToCamelCase(key) {
2902
3470
  function keyVariantsForMatching(key) {
2903
3471
  const trimmed = key.replace(/^[\s._-]+/, "");
2904
3472
  if (!trimmed) return [key];
2905
- const out = new Set([key, trimmed]);
3473
+ const out = /* @__PURE__ */ new Set([key, trimmed]);
2906
3474
  if (trimmed.includes("_")) out.add(snakeCaseToCamelCase(trimmed));
2907
3475
  return [...out];
2908
3476
  }
2909
- /** Whether a property name should have string values redacted in `redact()`. */
2910
- function isSensitiveStringKey(key) {
3477
+ function normalizePatterns(patterns) {
3478
+ const keysLower = /* @__PURE__ */ new Set();
3479
+ for (const key of patterns?.keys ?? []) keysLower.add(key.toLowerCase());
3480
+ return {
3481
+ keysLower,
3482
+ suffixesLower: [...patterns?.suffixes ?? []].map((s) => s.toLowerCase()),
3483
+ substringsLower: [...patterns?.substrings ?? []].map((s) => s.toLowerCase())
3484
+ };
3485
+ }
3486
+ function hasAnyPatterns(patterns) {
3487
+ return patterns.keysLower.size > 0 || patterns.suffixesLower.length > 0 || patterns.substringsLower.length > 0;
3488
+ }
3489
+ function matchesNormalizedPatterns(key, patterns) {
3490
+ if (!hasAnyPatterns(patterns)) return false;
2911
3491
  const variants = keyVariantsForMatching(key);
2912
3492
  const compactLower = key.replace(/[\s._-]/g, "").toLowerCase();
2913
- const forms = new Set([compactLower]);
3493
+ const forms = /* @__PURE__ */ new Set([compactLower]);
2914
3494
  for (const raw of variants) {
2915
3495
  forms.add(raw);
2916
3496
  forms.add(raw.toLowerCase());
2917
3497
  }
2918
3498
  for (const form of forms) {
2919
3499
  const fl = form.toLowerCase();
2920
- if (EXACT_SENSITIVE_STRING_KEYS_LOWER.has(fl)) return true;
2921
- for (const suffix of SENSITIVE_KEY_SUFFIXES_LOWER) if (fl.endsWith(suffix)) return true;
2922
- for (const substring of SENSITIVE_KEY_SUBSTRINGS_LOWER) if (compactLower.includes(substring)) return true;
3500
+ if (patterns.keysLower.has(fl)) return true;
3501
+ for (const suffix of patterns.suffixesLower) if (fl.endsWith(suffix)) return true;
3502
+ for (const substring of patterns.substringsLower) if (compactLower.includes(substring)) return true;
2923
3503
  }
2924
3504
  return false;
2925
3505
  }
@@ -2931,7 +3511,7 @@ function isPlainSerializable(value) {
2931
3511
  if (constructor && constructor.name !== "Object" && constructor.name !== "Array") return false;
2932
3512
  return true;
2933
3513
  }
2934
- function redactInner(value, visiting, options) {
3514
+ function redactInner(value, visiting, options, patterns) {
2935
3515
  if (value === null || value === void 0) return value;
2936
3516
  if (typeof value === "function") return void 0;
2937
3517
  if (typeof value !== "object") return value;
@@ -2939,17 +3519,17 @@ function redactInner(value, visiting, options) {
2939
3519
  if (visiting.has(obj)) return void 0;
2940
3520
  visiting.add(obj);
2941
3521
  try {
2942
- if (Array.isArray(value)) return value.map((item) => redactInner(item, visiting, options)).filter((item) => item !== void 0);
3522
+ if (Array.isArray(value)) return value.map((item) => redactInner(item, visiting, options, patterns)).filter((item) => item !== void 0);
2943
3523
  const result = {};
2944
3524
  for (const [key, val] of Object.entries(value)) {
2945
3525
  if (options?.excludeKeys?.has(key)) continue;
2946
3526
  if (typeof val === "function") continue;
2947
- if (typeof val === "string" && isSensitiveStringKey(key)) {
3527
+ if (typeof val === "string" && matchesNormalizedPatterns(key, patterns) && !options?.ignoreKeys?.has(key)) {
2948
3528
  result[key] = REDACTED_SIMPLE_STRING;
2949
3529
  continue;
2950
3530
  }
2951
3531
  if (options?.skipNonPlainSerializable && val !== null && typeof val === "object" && !isPlainSerializable(val)) continue;
2952
- const redacted = redactInner(val, visiting, options);
3532
+ const redacted = redactInner(val, visiting, options, patterns);
2953
3533
  if (redacted !== void 0) result[key] = redacted;
2954
3534
  }
2955
3535
  return result;
@@ -2957,9 +3537,55 @@ function redactInner(value, visiting, options) {
2957
3537
  visiting.delete(obj);
2958
3538
  }
2959
3539
  }
2960
- /** Recursively redact sensitive string values in a JSON-like object tree. */
3540
+ /**
3541
+ * Recursively walk a JSON-like object tree.
3542
+ *
3543
+ * String values are redacted only when `patterns` is provided. With no patterns,
3544
+ * this only applies structural options (`excludeKeys`,
3545
+ * `skipNonPlainSerializable`, dropping functions).
3546
+ */
2961
3547
  function redact(value, options) {
2962
- return redactInner(value, /* @__PURE__ */ new WeakSet(), options);
3548
+ const patterns = normalizePatterns(options?.patterns);
3549
+ return redactInner(value, /* @__PURE__ */ new WeakSet(), options, patterns);
3550
+ }
3551
+ const DASH_PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: /* @__PURE__ */ new Set(["stripeClient"]) };
3552
+ /** Storage-strategy enums that match sensitive key suffixes but are not secrets. */
3553
+ const DASH_PLUGIN_OPTIONS_IGNORE_KEYS = /* @__PURE__ */ new Set([
3554
+ "storeApiKey",
3555
+ "storeClientSecret",
3556
+ "storeSCIMToken",
3557
+ "storeToken"
3558
+ ]);
3559
+ function redactDashPluginOptions(pluginId, options, redactOptions) {
3560
+ return redact(options, {
3561
+ patterns: SENSITIVE_KEY_PATTERNS,
3562
+ ...redactOptions,
3563
+ excludeKeys: DASH_PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId],
3564
+ ignoreKeys: DASH_PLUGIN_OPTIONS_IGNORE_KEYS
3565
+ });
3566
+ }
3567
+ /**
3568
+ * Redact a dash settings payload: `emailAndPassword` and each plugin's `options`.
3569
+ */
3570
+ function redactDashSettings(data, redactOptions) {
3571
+ if (data === null || typeof data !== "object") return data;
3572
+ const d = data;
3573
+ const next = { ...d };
3574
+ if ("emailAndPassword" in d) next.emailAndPassword = redact(d.emailAndPassword, {
3575
+ patterns: SENSITIVE_KEY_PATTERNS,
3576
+ ...redactOptions
3577
+ });
3578
+ const plugins = d.plugins;
3579
+ if (Array.isArray(plugins)) next.plugins = plugins.map((plugin) => {
3580
+ if (plugin === null || typeof plugin !== "object") return plugin;
3581
+ const p = plugin;
3582
+ const id = typeof p.id === "string" ? p.id : "";
3583
+ return {
3584
+ ...p,
3585
+ options: redactDashPluginOptions(id, p.options, redactOptions)
3586
+ };
3587
+ });
3588
+ return next;
2963
3589
  }
2964
3590
  //#endregion
2965
3591
  //#region ../utils/dist/crypto/index.mjs
@@ -3005,7 +3631,7 @@ function timingSafeEqualHash(a, b) {
3005
3631
  * A freshly issued token is almost certainly legitimate.
3006
3632
  */
3007
3633
  const JTI_CHECK_GRACE_PERIOD_SECONDS = 30;
3008
- const JWKS_CACHE_TTL_MS = 900 * 1e3;
3634
+ const JWKS_CACHE_TTL_MS = 9e5;
3009
3635
  const jwksCache = /* @__PURE__ */ new Map();
3010
3636
  const inflightRequests = /* @__PURE__ */ new Map();
3011
3637
  async function fetchJWKS(ctx, cacheKey, $api) {
@@ -3054,7 +3680,8 @@ const jwtMiddleware = (options, schema, getJWT) => {
3054
3680
  ctx.context.logger.warn("[Dash] JWT is missing from header");
3055
3681
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
3056
3682
  }
3057
- const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(ctx, cacheKey, $api), { maxTokenAge: "5m" }).catch((e) => {
3683
+ const remoteJWKs = await getJWKs(ctx, cacheKey, $api);
3684
+ const { payload } = await jwtVerify(jwsFromHeader, remoteJWKs, { maxTokenAge: "5m" }).catch((e) => {
3058
3685
  ctx.context.logger.warn("[Dash] JWT verification failed:", e);
3059
3686
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
3060
3687
  });
@@ -3108,7 +3735,8 @@ const jwtValidateMiddleware = (options) => {
3108
3735
  ctx.context.logger.warn("[Dash] JWT is missing from header");
3109
3736
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
3110
3737
  }
3111
- const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(ctx, cacheKey, $api), { maxTokenAge: "5m" }).catch((e) => {
3738
+ const remoteJWKs = await getJWKs(ctx, cacheKey, $api);
3739
+ const { payload } = await jwtVerify(jwsFromHeader, remoteJWKs, { maxTokenAge: "5m" }).catch((e) => {
3112
3740
  ctx.context.logger.error("[Dash] JWT verification failed:", e);
3113
3741
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
3114
3742
  });
@@ -3128,13 +3756,61 @@ const jwtValidateMiddleware = (options) => {
3128
3756
  });
3129
3757
  };
3130
3758
  //#endregion
3131
- //#region src/lib/redact-plugin-options.ts
3132
- const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
3133
- function redactPluginOptions(pluginId, options, optionsConfig) {
3134
- return redact(options, {
3135
- ...optionsConfig,
3136
- excludeKeys: PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId]
3137
- });
3759
+ //#region ../utils/dist/semver.mjs
3760
+ /**
3761
+ * Parses a semver string into components.
3762
+ * Accepts `major.minor`, `major.minor.patch`, and optional prerelease (`-rc.2`).
3763
+ * Returns `null` when the input is missing or not a valid semver prefix.
3764
+ */
3765
+ function parseSemver(version) {
3766
+ if (!version) return null;
3767
+ const match = version.trim().match(/^(\d+)\.(\d+)(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/);
3768
+ if (!match) return null;
3769
+ return {
3770
+ major: Number(match[1]),
3771
+ minor: Number(match[2]),
3772
+ patch: Number(match[3] ?? 0),
3773
+ prerelease: match[4]
3774
+ };
3775
+ }
3776
+ function resolve(version) {
3777
+ if (typeof version === "string" || version == null) return parseSemver(version);
3778
+ return version;
3779
+ }
3780
+ /**
3781
+ * True when `version`'s core identity (`major.minor.patch`) is ≥ `target`.
3782
+ * Prerelease on `version` is ignored (e.g. `1.7.0-rc.1` ≥ `1.7.0`).
3783
+ * `target` may be `major.minor` or `major.minor.patch` (missing patch → 0).
3784
+ */
3785
+ function isSemverAtLeast(version, target) {
3786
+ const parsed = resolve(version);
3787
+ const targetParsed = parseSemver(target);
3788
+ if (!parsed || !targetParsed) return false;
3789
+ if (parsed.major !== targetParsed.major) return parsed.major > targetParsed.major;
3790
+ if (parsed.minor !== targetParsed.minor) return parsed.minor > targetParsed.minor;
3791
+ return parsed.patch >= targetParsed.patch;
3792
+ }
3793
+ //#endregion
3794
+ //#region src/semver.ts
3795
+ /** Compare semver strings (e.g. "1.7.0-beta.8" against "1.7" or "1.7.0"). */
3796
+ function isVersionAtLeast(version, target) {
3797
+ return isSemverAtLeast(version, target);
3798
+ }
3799
+ /**
3800
+ * True for better-auth 1.7+ (including 1.8+).
3801
+ * Marks the cutover for issuer-scoped accounts, protocol-defined SSO subjects,
3802
+ * `advanced.database.joins`, and post-legacy SCIM connections.
3803
+ * Keep in sync with `apps/web/lib/better-auth-compat.ts`.
3804
+ */
3805
+ function isBetterAuth17OrLater(version) {
3806
+ const parsed = parseSemver(version);
3807
+ if (!parsed) return false;
3808
+ if (isSemverAtLeast(parsed, "1.8.0")) return true;
3809
+ if (!isSemverAtLeast(parsed, "1.7.0")) return false;
3810
+ if (!parsed.prerelease || parsed.patch > 0) return true;
3811
+ const prereleaseMatch = parsed.prerelease.match(/^rc\.(\d+)/);
3812
+ if (prereleaseMatch) return Number(prereleaseMatch[1]) >= 4;
3813
+ return false;
3138
3814
  }
3139
3815
  //#endregion
3140
3816
  //#region src/routes/auth/config.ts
@@ -3150,16 +3826,16 @@ const getConfig = (options) => {
3150
3826
  }, async (ctx) => {
3151
3827
  const advancedOptions = ctx.context.options.advanced;
3152
3828
  const organizationPlugin = ctx.context.getPlugin("organization");
3153
- return {
3829
+ return redactDashSettings({
3154
3830
  version: ctx.context.version || null,
3155
3831
  socialProviders: Object.keys(ctx.context.options.socialProviders || {}),
3156
- emailAndPassword: redact(ctx.context.options.emailAndPassword, { skipNonPlainSerializable: true }),
3832
+ emailAndPassword: ctx.context.options.emailAndPassword,
3157
3833
  plugins: ctx.context.options.plugins?.map((plugin) => {
3158
3834
  const base = {
3159
3835
  id: plugin.id,
3160
3836
  schema: plugin.schema,
3161
3837
  version: plugin.version,
3162
- options: redactPluginOptions(plugin.id, plugin.options, { skipNonPlainSerializable: true })
3838
+ options: plugin.options
3163
3839
  };
3164
3840
  if (plugin.id === "dash" && !plugin.version) return {
3165
3841
  ...base,
@@ -3262,76 +3938,970 @@ const getConfig = (options) => {
3262
3938
  secure: typeof ctx.context.options.advanced?.defaultCookieAttributes?.secure !== "undefined" ? ctx.context.options.advanced?.defaultCookieAttributes?.secure : null
3263
3939
  } : null,
3264
3940
  appName: ctx.context.options.appName || null,
3265
- hasJoinsEnabled: ctx.context.options.experimental?.joins === true,
3941
+ hasJoinsEnabled: (() => {
3942
+ const advancedJoins = (ctx.context.options.advanced?.database)?.joins === true;
3943
+ if (isBetterAuth17OrLater(ctx.context.version)) return advancedJoins;
3944
+ return advancedJoins || ctx.context.options.experimental?.joins === true;
3945
+ })(),
3266
3946
  hasErrorURLConfigured: !!ctx.context.options.onAPIError?.errorURL
3267
3947
  }
3948
+ }, { skipNonPlainSerializable: true });
3949
+ });
3950
+ };
3951
+ //#endregion
3952
+ //#region src/routes/auth/validate.ts
3953
+ /**
3954
+ * Lightweight endpoint to verify API key ownership during onboarding
3955
+ */
3956
+ const getValidate = (options) => {
3957
+ return createAuthEndpoint("/dash/validate", {
3958
+ method: "GET",
3959
+ use: [jwtValidateMiddleware(options)]
3960
+ }, async () => {
3961
+ return { valid: true };
3962
+ });
3963
+ };
3964
+ //#endregion
3965
+ //#region src/routes/organization-guards.ts
3966
+ /** Returns true if organization plugin is enabled. */
3967
+ function isOrganizationEnabled(ctx) {
3968
+ return !!ctx.context.getPlugin("organization");
3969
+ }
3970
+ /** Returns the organization plugin, throws if not enabled. Use for write endpoints. */
3971
+ function requireOrganizationPlugin(ctx) {
3972
+ const plugin = ctx.context.getPlugin("organization");
3973
+ if (!plugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
3974
+ return plugin;
3975
+ }
3976
+ /** Returns true if organization plugin and teams feature are enabled. */
3977
+ function isTeamsEnabled(ctx) {
3978
+ return !!ctx.context.getPlugin("organization")?.options?.teams?.enabled;
3979
+ }
3980
+ /**
3981
+ * Validates that the organization plugin is enabled and teams feature is enabled.
3982
+ *
3983
+ * @returns The organization options for use in team logic (maximumTeams, hooks, etc.)
3984
+ */
3985
+ function requireTeamsEnabled(ctx) {
3986
+ const orgOptions = requireOrganizationPlugin(ctx).options || {};
3987
+ if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
3988
+ return orgOptions;
3989
+ }
3990
+ //#endregion
3991
+ //#region src/routes/directory-sync/managed-catalog.ts
3992
+ function getRequestHeaders(ctx) {
3993
+ return ctx.request?.headers ?? new Headers();
3994
+ }
3995
+ function getManagedEndpoints(ctx) {
3996
+ const plugin = ctx.context.getPlugin("scim");
3997
+ if (!plugin?.endpoints) throw ctx.error("BAD_REQUEST", { message: "SCIM managed connections are unavailable. Install a Better Auth release that includes the managed SCIM catalog and configure managedConnections." });
3998
+ return plugin.endpoints;
3999
+ }
4000
+ function requireManagedEndpoint(ctx, endpoint, name) {
4001
+ if (!endpoint) throw ctx.error("BAD_REQUEST", { message: `SCIM managed connection operation "${name}" is unavailable. Publish and install the required Better Auth SCIM release.` });
4002
+ return endpoint;
4003
+ }
4004
+ function assertManagedConnectionLifecycleConfigured(ctx) {
4005
+ const endpoints = getManagedEndpoints(ctx);
4006
+ requireManagedEndpoint(ctx, endpoints.createSCIMManagedConnection, "create");
4007
+ requireManagedEndpoint(ctx, endpoints.listSCIMManagedConnections, "list");
4008
+ requireManagedEndpoint(ctx, endpoints.getSCIMManagedConnection, "get");
4009
+ requireManagedEndpoint(ctx, endpoints.rotateSCIMManagedCredential, "rotate");
4010
+ requireManagedEndpoint(ctx, endpoints.revokeSCIMManagedCredential, "revoke");
4011
+ requireManagedEndpoint(ctx, endpoints.decommissionSCIMManagedConnection, "decommission");
4012
+ return endpoints;
4013
+ }
4014
+ async function getManagedState(ctx, connection) {
4015
+ if (!connection.connectionId) return;
4016
+ return await requireManagedEndpoint(ctx, getManagedEndpoints(ctx).getSCIMManagedConnection, "get")({
4017
+ body: {
4018
+ connectionId: connection.connectionId,
4019
+ provisioningDomainId: connection.provisioningDomainId
4020
+ },
4021
+ context: ctx.context,
4022
+ headers: getRequestHeaders(ctx)
4023
+ });
4024
+ }
4025
+ //#endregion
4026
+ //#region src/routes/directory-sync/mode.ts
4027
+ function getSCIMPlugin(ctx) {
4028
+ return ctx.context.getPlugin("scim");
4029
+ }
4030
+ function getDashPluginOptions(ctx) {
4031
+ return ctx.context.getPlugin("dash")?.options;
4032
+ }
4033
+ /** True when dash was configured with `managedDirectorySync: { enabled: true }`. */
4034
+ function isManagedDirectorySyncEnabled(ctx) {
4035
+ return getDashPluginOptions(ctx)?.managedDirectorySync?.enabled === true;
4036
+ }
4037
+ function usesLegacyScimDirectorySync(scimPlugin) {
4038
+ return typeof scimPlugin?.endpoints.generateSCIMToken === "function";
4039
+ }
4040
+ /** True when the SCIM plugin exposes the 1.7+ managed connection endpoints. */
4041
+ function hasManagedScimSurface(scimPlugin) {
4042
+ return typeof scimPlugin?.endpoints.createSCIMManagedConnection === "function" && typeof scimPlugin?.endpoints.listSCIMManagedConnections === "function";
4043
+ }
4044
+ function usesManagedScimDirectorySync(scimPlugin) {
4045
+ return hasManagedScimSurface(scimPlugin) && scimPlugin?.options?.managedConnections != null;
4046
+ }
4047
+ /**
4048
+ * Resolve which directory-sync experience the dashboard should use.
4049
+ * Managed mode also requires dash `managedDirectorySync.enabled`.
4050
+ * Legacy SCIM mode ignores that option.
4051
+ */
4052
+ function resolveDirectorySyncMode(scimPlugin, managedDirectorySyncEnabled = false) {
4053
+ if (!scimPlugin) return "unavailable";
4054
+ if (hasManagedScimSurface(scimPlugin)) return usesManagedScimDirectorySync(scimPlugin) && managedDirectorySyncEnabled ? "managed" : "unavailable";
4055
+ if (usesLegacyScimDirectorySync(scimPlugin)) {
4056
+ const ownership = scimPlugin.options?.providerOwnership;
4057
+ if (ownership != null && ownership.enabled !== true) return "unavailable";
4058
+ return "legacy";
4059
+ }
4060
+ return "unavailable";
4061
+ }
4062
+ function assertManagedDirectorySyncEnabled(ctx) {
4063
+ if (isManagedDirectorySyncEnabled(ctx)) return;
4064
+ throw ctx.error("BAD_REQUEST", { message: "Managed directory sync is disabled. Enable dash({ managedDirectorySync: { enabled: true } }) and migrate the directory sync schema. Legacy SCIM directory sync does not use this option." });
4065
+ }
4066
+ //#endregion
4067
+ //#region src/routes/directory-sync/route-contract.ts
4068
+ const DIRECTORY_SYNC_CREDENTIAL_LIFETIME_MS = 31536e6;
4069
+ const directorySyncClaimsSchema = z.object({
4070
+ purpose: z.literal(DIRECTORY_SYNC_PURPOSE),
4071
+ organizationId: z.string().trim().min(1),
4072
+ actorId: z.string().trim().min(1),
4073
+ setupOperationId: z.string().trim().min(16).max(255).optional()
4074
+ });
4075
+ function getScimEndpoint$1(baseUrl) {
4076
+ return `${baseUrl}/scim/v2`;
4077
+ }
4078
+ function assertDirectorySyncClaims(ctx, organizationId) {
4079
+ const claims = ctx.context.payload;
4080
+ if (claims.purpose !== "directory-sync-management" || claims.organizationId !== organizationId || !claims.actorId) throw ctx.error("UNAUTHORIZED", { message: "Invalid directory sync management authorization" });
4081
+ return claims;
4082
+ }
4083
+ function assertDirectorySyncManagementClaims(ctx, organizationId) {
4084
+ const claims = assertDirectorySyncClaims(ctx, organizationId);
4085
+ if (claims.setupOperationId) throw ctx.error("FORBIDDEN", { message: "Directory sync setup authorization cannot perform management operations" });
4086
+ const { setupOperationId: _, ...managementClaims } = claims;
4087
+ return managementClaims;
4088
+ }
4089
+ async function assertTargetOrganizationExists(ctx, organizationId) {
4090
+ if (!await ctx.context.adapter.findOne({
4091
+ model: "organization",
4092
+ where: [{
4093
+ field: "id",
4094
+ value: organizationId
4095
+ }],
4096
+ select: ["id"]
4097
+ })) throw ctx.error("NOT_FOUND", { message: "Target organization not found" });
4098
+ }
4099
+ function resolveCredentialPolicy(input) {
4100
+ const expiresAt = input.expiresAt ?? new Date(Date.now() + DIRECTORY_SYNC_CREDENTIAL_LIFETIME_MS);
4101
+ if (expiresAt.getTime() <= Date.now()) throw new APIError$1("BAD_REQUEST", { message: "Directory sync credential expiry must be in the future" });
4102
+ const scopes = input.scopes ?? ALL_SCIM_SCOPES;
4103
+ if (new Set(scopes).size !== scopes.length) throw new APIError$1("BAD_REQUEST", { message: "Directory sync credential scopes must be unique" });
4104
+ return {
4105
+ scopes,
4106
+ expiresAt
4107
+ };
4108
+ }
4109
+ function serializeDate(value) {
4110
+ return value instanceof Date ? value.toISOString() : value;
4111
+ }
4112
+ function serializeNullableDate(value) {
4113
+ return value == null ? null : serializeDate(value);
4114
+ }
4115
+ function serializeCredential(credential) {
4116
+ return {
4117
+ credentialId: credential.credentialId,
4118
+ status: credential.status,
4119
+ scopes: credential.scopes,
4120
+ expiresAt: serializeDate(credential.expiresAt),
4121
+ createdAt: serializeDate(credential.createdAt),
4122
+ createdBy: credential.createdBy,
4123
+ lastUsedAt: serializeNullableDate(credential.lastUsedAt),
4124
+ revokedAt: serializeNullableDate(credential.revokedAt),
4125
+ revokedBy: credential.revokedBy
4126
+ };
4127
+ }
4128
+ function serializeDirectory(row, scimEndpoint, state) {
4129
+ return {
4130
+ connectionId: row.connectionId ?? null,
4131
+ organizationId: row.organizationId,
4132
+ providerId: row.providerId,
4133
+ provisioningDomainId: row.provisioningDomainId,
4134
+ status: row.status,
4135
+ scimEndpoint,
4136
+ credentials: state?.credentials.map(serializeCredential) ?? [],
4137
+ createdAt: serializeDate(row.createdAt),
4138
+ updatedAt: serializeDate(row.updatedAt),
4139
+ pairing: serializePairingRow(row),
4140
+ pairingEnforced: row.pairingEnforced === true,
4141
+ unpairedAt: serializeNullableDate(row.unpairedAt ?? null),
4142
+ unpairedBy: row.unpairedBy ?? null,
4143
+ decommissionedAt: serializeNullableDate(row.decommissionedAt ?? null)
4144
+ };
4145
+ }
4146
+ //#endregion
4147
+ //#region src/routes/directory-sync/transaction.ts
4148
+ /**
4149
+ * `better-auth` and `@better-auth/core` can resolve to distinct physical
4150
+ * copies of the same version. Endpoint adapters are typed from the former;
4151
+ * `runWithTransaction` / `getCurrentAdapter` are typed from the latter.
4152
+ */
4153
+ function runWithTransaction$1(adapter, fn) {
4154
+ return runWithTransaction(adapter, fn);
4155
+ }
4156
+ function getCurrentAdapter$1(adapter) {
4157
+ return getCurrentAdapter(adapter);
4158
+ }
4159
+ //#endregion
4160
+ //#region src/routes/directory-sync/reservation.ts
4161
+ function withTransactionAdapter(ctx, adapter) {
4162
+ return {
4163
+ ...ctx,
4164
+ context: {
4165
+ ...ctx.context,
4166
+ adapter
4167
+ }
4168
+ };
4169
+ }
4170
+ async function createAliasKey(organizationId, providerId) {
4171
+ return `directory-sync-alias:${await hash$1(JSON.stringify([organizationId, providerId]))}`;
4172
+ }
4173
+ async function createProvisioningDomainId(organizationId, providerId) {
4174
+ return `dash_scim_domain_${await hash$1(JSON.stringify({
4175
+ purpose: DIRECTORY_SYNC_PURPOSE,
4176
+ organizationId,
4177
+ providerId
4178
+ }))}`;
4179
+ }
4180
+ async function createActiveOrganizationKey(organizationId) {
4181
+ return `directory-sync-active:${await hash$1(organizationId)}`;
4182
+ }
4183
+ function createInactiveOrganizationKey(aliasKey) {
4184
+ return `directory-sync-inactive:${aliasKey}`;
4185
+ }
4186
+ function createCreationRequestId() {
4187
+ return generateRandomString(32, "a-z", "A-Z", "0-9", "-_");
4188
+ }
4189
+ function isUniqueConstraintError(error) {
4190
+ return error instanceof Error && /unique|duplicate|constraint|P2002/i.test(error.message);
4191
+ }
4192
+ async function assertManagedDirectoryTransactionsConfigured(ctx) {
4193
+ if (typeof ctx.context.adapter.options?.adapterConfig.transaction !== "function") throw ctx.error("NOT_IMPLEMENTED", {
4194
+ code: "DIRECTORY_SYNC_REQUIRES_NATIVE_TRANSACTIONS",
4195
+ message: "Managed directory sync requires a database adapter with native transaction support"
4196
+ });
4197
+ try {
4198
+ await getCurrentDBAdapterAsyncLocalStorage();
4199
+ } catch {
4200
+ throw ctx.error("NOT_IMPLEMENTED", {
4201
+ code: "DIRECTORY_SYNC_REQUIRES_ASYNC_CONTEXT",
4202
+ message: "Managed directory sync requires database transaction async context support"
4203
+ });
4204
+ }
4205
+ }
4206
+ async function assertDirectorySyncSSOIntegrationConfigured(ctx) {
4207
+ const ssoPlugin = ctx.context.getPlugin("sso");
4208
+ if (typeof ssoPlugin?.options?.resolveUser !== "function" || typeof ssoPlugin.options.guardProviderMutation !== "function") throw ctx.error("BAD_REQUEST", { message: "Paired directory sync requires SSO resolveUser and guardProviderMutation callbacks. Enable dash({ managedDirectorySync: { enabled: true, ssoPairing: true } }) and pass sso({}) (or richer SSO options) so dash can install them." });
4209
+ await assertManagedDirectoryTransactionsConfigured(ctx);
4210
+ }
4211
+ async function findDirectorySyncConnection(ctx, organizationId, providerId) {
4212
+ return await (await getCurrentAdapter$1(ctx.context.adapter)).findOne({
4213
+ model: "directorySyncConnection",
4214
+ where: [{
4215
+ field: "organizationId",
4216
+ value: organizationId
4217
+ }, {
4218
+ field: "providerId",
4219
+ value: providerId
4220
+ }]
4221
+ });
4222
+ }
4223
+ async function getDirectorySyncConnection(ctx, organizationId, providerId) {
4224
+ const row = await findDirectorySyncConnection(ctx, organizationId, providerId);
4225
+ if (!row) throw ctx.error("NOT_FOUND", { message: "Directory sync connection not found" });
4226
+ return row;
4227
+ }
4228
+ function isExactManagedConnection(issued, input) {
4229
+ return issued.connection.creationRequestId === input.creationRequestId && issued.connection.provisioningDomainId === input.provisioningDomainId;
4230
+ }
4231
+ async function lockExactPairedSSOProvider(adapter, row) {
4232
+ if (!row.pairingEnforced) return;
4233
+ if (!row.ssoProviderRecordId || !row.ssoProviderId) throw new Error("Enforced directory sync pairing is incomplete");
4234
+ if (!await adapter.update({
4235
+ model: "ssoProvider",
4236
+ where: [
4237
+ {
4238
+ field: "id",
4239
+ value: row.ssoProviderRecordId
4240
+ },
4241
+ {
4242
+ field: "providerId",
4243
+ value: row.ssoProviderId
4244
+ },
4245
+ {
4246
+ field: "organizationId",
4247
+ value: row.organizationId
4248
+ }
4249
+ ],
4250
+ update: { providerId: row.ssoProviderId }
4251
+ })) throw new Error("Paired SSO provider changed during directory sync");
4252
+ }
4253
+ async function recoverManagedDirectoryConnection(ctx, row, input, policy) {
4254
+ if (row.organizationId !== input.organizationId || row.providerId !== input.providerId || row.creationRequestId !== input.creationRequestId || row.status !== "active" || !row.connectionId) throw ctx.error("CONFLICT", { message: "This directory sync alias belongs to a different setup operation" });
4255
+ const connectionId = row.connectionId;
4256
+ const serializedPairing = input.pairing ? JSON.stringify(input.pairing) : null;
4257
+ if ((row.serializedSsoPairing ?? null) !== serializedPairing) throw ctx.error("CONFLICT", { message: "Directory sync setup recovery must use the original SSO pairing" });
4258
+ const endpoints = getManagedEndpoints(ctx);
4259
+ const getManagedConnection = requireManagedEndpoint(ctx, endpoints.getSCIMManagedConnection, "get");
4260
+ const revokeManagedCredential = requireManagedEndpoint(ctx, endpoints.revokeSCIMManagedCredential, "revoke");
4261
+ const rotateManagedCredential = requireManagedEndpoint(ctx, endpoints.rotateSCIMManagedCredential, "rotate");
4262
+ return await runWithTransaction$1(ctx.context.adapter, async () => {
4263
+ const database = await getCurrentAdapter$1(ctx.context.adapter);
4264
+ await lockExactPairedSSOProvider(database, row);
4265
+ const locked = await database.update({
4266
+ model: "directorySyncConnection",
4267
+ where: [
4268
+ {
4269
+ field: "id",
4270
+ value: row.id
4271
+ },
4272
+ {
4273
+ field: "status",
4274
+ value: "active"
4275
+ },
4276
+ {
4277
+ field: "revision",
4278
+ value: row.revision
4279
+ },
4280
+ {
4281
+ field: "connectionId",
4282
+ value: connectionId
4283
+ },
4284
+ {
4285
+ field: "creationRequestId",
4286
+ value: input.creationRequestId
4287
+ }
4288
+ ],
4289
+ update: {
4290
+ revision: row.revision + 1,
4291
+ updatedAt: /* @__PURE__ */ new Date(),
4292
+ lastActorId: input.actorId,
4293
+ lastError: null
4294
+ }
4295
+ });
4296
+ if (!locked) throw ctx.error("CONFLICT", { message: "Directory sync connection changed before setup recovery started" });
4297
+ const state = await getManagedConnection({
4298
+ body: {
4299
+ connectionId,
4300
+ provisioningDomainId: row.provisioningDomainId
4301
+ },
4302
+ context: ctx.context,
4303
+ headers: getRequestHeaders(ctx)
4304
+ });
4305
+ if (state.connection.connectionId !== connectionId || state.connection.creationRequestId !== input.creationRequestId || state.connection.provisioningDomainId !== row.provisioningDomainId || state.connection.status !== "active") throw ctx.error("CONFLICT", { message: "Managed connection ownership changed before setup recovery" });
4306
+ for (const credential of state.credentials) {
4307
+ if (credential.status !== "active") continue;
4308
+ await revokeManagedCredential({
4309
+ body: {
4310
+ connectionId,
4311
+ provisioningDomainId: row.provisioningDomainId,
4312
+ credentialId: credential.credentialId,
4313
+ actorId: input.actorId
4314
+ },
4315
+ context: ctx.context,
4316
+ headers: getRequestHeaders(ctx)
4317
+ });
4318
+ }
4319
+ const issued = await rotateManagedCredential({
4320
+ body: {
4321
+ connectionId,
4322
+ provisioningDomainId: row.provisioningDomainId,
4323
+ actorId: input.actorId,
4324
+ ...policy
4325
+ },
4326
+ context: ctx.context,
4327
+ headers: getRequestHeaders(ctx)
4328
+ });
4329
+ if (issued.connection.connectionId !== connectionId || issued.connection.creationRequestId !== input.creationRequestId || issued.connection.provisioningDomainId !== row.provisioningDomainId) throw ctx.error("CONFLICT", { message: "Managed connection recovery returned mismatched ownership correlation" });
4330
+ return {
4331
+ row: locked,
4332
+ issued
4333
+ };
4334
+ });
4335
+ }
4336
+ async function createManagedDirectoryConnection(ctx, input, policy) {
4337
+ await assertManagedDirectoryTransactionsConfigured(ctx);
4338
+ if (input.pairing) await assertDirectorySyncSSOIntegrationConfigured(ctx);
4339
+ if (input.pairing?.protocol === "saml") try {
4340
+ await loadSAMLPolicy();
4341
+ } catch {
4342
+ throw ctx.error("NOT_IMPLEMENTED", {
4343
+ code: "DIRECTORY_SYNC_SAML_POLICY_UNAVAILABLE",
4344
+ message: "SAML pairing requires a compatible SSO package with service provider metadata policy support"
4345
+ });
4346
+ }
4347
+ const aliasKey = await createAliasKey(input.organizationId, input.providerId);
4348
+ const provisioningDomainId = await createProvisioningDomainId(input.organizationId, input.providerId);
4349
+ const activeOrganizationKey = await createActiveOrganizationKey(input.organizationId);
4350
+ const creationRequestId = input.creationRequestId ?? createCreationRequestId();
4351
+ const createManagedConnection = requireManagedEndpoint(ctx, getManagedEndpoints(ctx).createSCIMManagedConnection, "create");
4352
+ try {
4353
+ return await runWithTransaction$1(ctx.context.adapter, async () => {
4354
+ const database = await getCurrentAdapter$1(ctx.context.adapter);
4355
+ const transactionContext = withTransactionAdapter(ctx, database);
4356
+ const pairing = input.pairing ? await resolveDirectorySyncSSOPairing(transactionContext, input.organizationId, input.pairing) : null;
4357
+ const now = /* @__PURE__ */ new Date();
4358
+ const row = await database.create({
4359
+ model: "directorySyncConnection",
4360
+ data: {
4361
+ organizationId: input.organizationId,
4362
+ providerId: input.providerId,
4363
+ aliasKey,
4364
+ provisioningDomainId,
4365
+ activeOrganizationKey,
4366
+ creationRequestId,
4367
+ status: "active",
4368
+ revision: 0,
4369
+ createdAt: now,
4370
+ createdByActorId: input.actorId,
4371
+ updatedAt: now,
4372
+ lastActorId: input.actorId,
4373
+ activeSsoProviderKey: pairing?.activeSsoProviderKey ?? createInactiveSSOProviderKey(aliasKey),
4374
+ pairingEnforced: pairing != null,
4375
+ ...pairing ? {
4376
+ ssoProviderId: pairing.ssoProviderId,
4377
+ ssoProviderRecordId: pairing.ssoProviderRecordId,
4378
+ serializedSsoPairing: JSON.stringify(pairing.pairing)
4379
+ } : {}
4380
+ }
4381
+ });
4382
+ const issued = await createManagedConnection({
4383
+ body: {
4384
+ creationRequestId,
4385
+ provisioningDomainId,
4386
+ actorId: input.actorId,
4387
+ ...policy
4388
+ },
4389
+ context: ctx.context,
4390
+ headers: getRequestHeaders(ctx)
4391
+ });
4392
+ if (!isExactManagedConnection(issued, {
4393
+ creationRequestId,
4394
+ provisioningDomainId
4395
+ })) throw ctx.error("CONFLICT", { message: "Managed connection creation returned mismatched ownership correlation" });
4396
+ await lockExactPairedSSOProvider(database, row);
4397
+ const bound = await database.update({
4398
+ model: "directorySyncConnection",
4399
+ where: [
4400
+ {
4401
+ field: "id",
4402
+ value: row.id
4403
+ },
4404
+ {
4405
+ field: "status",
4406
+ value: "active"
4407
+ },
4408
+ {
4409
+ field: "revision",
4410
+ value: 0
4411
+ },
4412
+ {
4413
+ field: "connectionId",
4414
+ value: null
4415
+ }
4416
+ ],
4417
+ update: {
4418
+ connectionId: issued.connection.connectionId,
4419
+ revision: 1,
4420
+ updatedAt: /* @__PURE__ */ new Date()
4421
+ }
4422
+ });
4423
+ if (!bound) throw ctx.error("CONFLICT", { message: "Directory sync connection changed before catalog binding completed" });
4424
+ await supersedeTerminalDirectorySyncPairings(transactionContext, bound);
4425
+ return {
4426
+ row: bound,
4427
+ issued
4428
+ };
4429
+ });
4430
+ } catch (error) {
4431
+ if (isUniqueConstraintError(error)) {
4432
+ const aliasConflict = await ctx.context.adapter.findOne({
4433
+ model: "directorySyncConnection",
4434
+ where: [{
4435
+ field: "aliasKey",
4436
+ value: aliasKey
4437
+ }]
4438
+ });
4439
+ if (aliasConflict) {
4440
+ if (input.creationRequestId && aliasConflict.creationRequestId === input.creationRequestId && aliasConflict.organizationId === input.organizationId && aliasConflict.providerId === input.providerId && aliasConflict.provisioningDomainId === provisioningDomainId) return await recoverManagedDirectoryConnection(ctx, aliasConflict, {
4441
+ ...input,
4442
+ creationRequestId: input.creationRequestId
4443
+ }, policy);
4444
+ throw ctx.error("CONFLICT", { message: "This organization already has a directory sync connection for the requested provider" });
4445
+ }
4446
+ if (await ctx.context.adapter.findOne({
4447
+ model: "directorySyncConnection",
4448
+ where: [{
4449
+ field: "activeOrganizationKey",
4450
+ value: activeOrganizationKey
4451
+ }]
4452
+ })) throw ctx.error("CONFLICT", { message: "This organization already has an active directory sync connection" });
4453
+ throw error;
4454
+ }
4455
+ throw error;
4456
+ }
4457
+ }
4458
+ function createTerminalOrganizationKey(aliasKey) {
4459
+ return createInactiveOrganizationKey(aliasKey);
4460
+ }
4461
+ function createTerminalPairingKey(aliasKey) {
4462
+ return createTerminalSSOProviderKey(aliasKey);
4463
+ }
4464
+ async function supersedeTerminalDirectorySyncPairings(ctx, activeRow) {
4465
+ if (!activeRow.ssoProviderRecordId || !activeRow.pairingEnforced) return;
4466
+ const terminalRows = await ctx.context.adapter.findMany({
4467
+ model: "directorySyncConnection",
4468
+ where: [{
4469
+ field: "ssoProviderRecordId",
4470
+ value: activeRow.ssoProviderRecordId
4471
+ }, {
4472
+ field: "pairingEnforced",
4473
+ value: true
4474
+ }]
4475
+ });
4476
+ const now = /* @__PURE__ */ new Date();
4477
+ for (const row of terminalRows) {
4478
+ if (row.id === activeRow.id || row.status !== "decommissioned") continue;
4479
+ if (!await ctx.context.adapter.update({
4480
+ model: "directorySyncConnection",
4481
+ where: [
4482
+ {
4483
+ field: "id",
4484
+ value: row.id
4485
+ },
4486
+ {
4487
+ field: "status",
4488
+ value: "decommissioned"
4489
+ },
4490
+ {
4491
+ field: "revision",
4492
+ value: row.revision
4493
+ },
4494
+ {
4495
+ field: "pairingEnforced",
4496
+ value: true
4497
+ }
4498
+ ],
4499
+ update: {
4500
+ pairingEnforced: false,
4501
+ revision: row.revision + 1,
4502
+ unpairedAt: now,
4503
+ unpairedBy: activeRow.lastActorId,
4504
+ updatedAt: now,
4505
+ lastActorId: activeRow.lastActorId
4506
+ }
4507
+ })) throw ctx.error("CONFLICT", { message: "Directory sync SSO pairing changed before replacement activation" });
4508
+ }
4509
+ }
4510
+ async function startDirectorySyncDecommission(ctx, row, actorId) {
4511
+ if (row.status !== "active") return row;
4512
+ await assertManagedDirectoryTransactionsConfigured(ctx);
4513
+ return await runWithTransaction$1(ctx.context.adapter, async () => {
4514
+ const database = await getCurrentAdapter$1(ctx.context.adapter);
4515
+ await lockExactPairedSSOProvider(database, row);
4516
+ const updated = await database.update({
4517
+ model: "directorySyncConnection",
4518
+ where: [
4519
+ {
4520
+ field: "id",
4521
+ value: row.id
4522
+ },
4523
+ {
4524
+ field: "status",
4525
+ value: "active"
4526
+ },
4527
+ {
4528
+ field: "revision",
4529
+ value: row.revision
4530
+ }
4531
+ ],
4532
+ update: {
4533
+ status: "decommissioning",
4534
+ revision: row.revision + 1,
4535
+ decommissionStartedAt: /* @__PURE__ */ new Date(),
4536
+ updatedAt: /* @__PURE__ */ new Date(),
4537
+ lastActorId: actorId
4538
+ }
4539
+ });
4540
+ if (!updated) throw ctx.error("CONFLICT", { message: "Directory sync connection changed while decommissioning started" });
4541
+ return updated;
4542
+ });
4543
+ }
4544
+ async function unpairTerminalDirectorySyncConnection(ctx, row, actorId) {
4545
+ if (row.status !== "decommissioned") throw ctx.error("CONFLICT", { message: "Directory sync can only be unpaired after it has decommissioned" });
4546
+ if (!row.pairingEnforced) return row;
4547
+ await assertManagedDirectoryTransactionsConfigured(ctx);
4548
+ return await runWithTransaction$1(ctx.context.adapter, async () => {
4549
+ const database = await getCurrentAdapter$1(ctx.context.adapter);
4550
+ await lockExactPairedSSOProvider(database, row);
4551
+ const now = /* @__PURE__ */ new Date();
4552
+ const updated = await database.update({
4553
+ model: "directorySyncConnection",
4554
+ where: [
4555
+ {
4556
+ field: "id",
4557
+ value: row.id
4558
+ },
4559
+ {
4560
+ field: "status",
4561
+ value: "decommissioned"
4562
+ },
4563
+ {
4564
+ field: "revision",
4565
+ value: row.revision
4566
+ },
4567
+ {
4568
+ field: "pairingEnforced",
4569
+ value: true
4570
+ }
4571
+ ],
4572
+ update: {
4573
+ activeSsoProviderKey: createTerminalSSOProviderKey(row.aliasKey),
4574
+ pairingEnforced: false,
4575
+ revision: row.revision + 1,
4576
+ unpairedAt: now,
4577
+ unpairedBy: actorId,
4578
+ updatedAt: now,
4579
+ lastActorId: actorId
4580
+ }
4581
+ });
4582
+ if (!updated) throw ctx.error("CONFLICT", { message: "Directory sync SSO pairing changed before it was unpaired" });
4583
+ return updated;
4584
+ });
4585
+ }
4586
+ //#endregion
4587
+ //#region src/routes/directory-sync/managed-directories.ts
4588
+ /** Shared list handler for mode-dispatch on GET /:id/directories. */
4589
+ async function listOrganizationDirectoriesManagedHandler(ctx) {
4590
+ requireOrganizationPlugin(ctx);
4591
+ assertManagedDirectorySyncEnabled(ctx);
4592
+ const organizationId = tryDecode(ctx.params?.id ?? "");
4593
+ assertDirectorySyncManagementClaims(ctx, organizationId);
4594
+ await assertTargetOrganizationExists(ctx, organizationId);
4595
+ const rows = await ctx.context.adapter.findMany({
4596
+ model: "directorySyncConnection",
4597
+ where: [{
4598
+ field: "organizationId",
4599
+ value: organizationId
4600
+ }],
4601
+ sortBy: {
4602
+ field: "createdAt",
4603
+ direction: "desc"
4604
+ }
4605
+ });
4606
+ return await Promise.all(rows.map(async (row) => serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), await getManagedState(ctx, row))));
4607
+ }
4608
+ const getOrganizationDirectory = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId", {
4609
+ method: "GET",
4610
+ use: [jwtMiddleware(options, directorySyncClaimsSchema)]
4611
+ }, async (ctx) => {
4612
+ requireOrganizationPlugin(ctx);
4613
+ assertManagedDirectorySyncEnabled(ctx);
4614
+ const organizationId = tryDecode(ctx.params.id);
4615
+ assertDirectorySyncManagementClaims(ctx, organizationId);
4616
+ await assertTargetOrganizationExists(ctx, organizationId);
4617
+ const row = await getDirectorySyncConnection(ctx, organizationId, tryDecode(ctx.params.providerId));
4618
+ return serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), await getManagedState(ctx, row));
4619
+ });
4620
+ const createOrganizationDirectory = (options) => createAuthEndpoint("/dash/organization/:id/directories", {
4621
+ method: "POST",
4622
+ use: [jwtMiddleware(options, directorySyncClaimsSchema)],
4623
+ body: createDirectoryBodySchema,
4624
+ metadata: { noStore: true }
4625
+ }, async (ctx) => {
4626
+ requireOrganizationPlugin(ctx);
4627
+ assertManagedDirectorySyncEnabled(ctx);
4628
+ const organizationId = tryDecode(ctx.params.id);
4629
+ const claims = assertDirectorySyncClaims(ctx, organizationId);
4630
+ await assertTargetOrganizationExists(ctx, organizationId);
4631
+ const policy = resolveCredentialPolicy(ctx.body);
4632
+ assertManagedConnectionLifecycleConfigured(ctx);
4633
+ const { row, issued } = await createManagedDirectoryConnection(ctx, {
4634
+ organizationId,
4635
+ providerId: ctx.body.providerId,
4636
+ actorId: claims.actorId,
4637
+ ...claims.setupOperationId ? { creationRequestId: claims.setupOperationId } : {},
4638
+ ...ctx.body.pairing ? { pairing: ctx.body.pairing } : {}
4639
+ }, policy);
4640
+ setCredentialResponseSecurityHeaders(ctx);
4641
+ return {
4642
+ ...serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), {
4643
+ connection: issued.connection,
4644
+ credentials: [issued.credential]
4645
+ }),
4646
+ connectionId: issued.connection.connectionId,
4647
+ credential: serializeCredential(issued.credential),
4648
+ scimToken: issued.token
4649
+ };
4650
+ });
4651
+ const rotateDirectoryCredential = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/credentials/rotate", {
4652
+ method: "POST",
4653
+ use: [jwtMiddleware(options, directorySyncClaimsSchema)],
4654
+ body: rotateCredentialBodySchema,
4655
+ metadata: { noStore: true }
4656
+ }, async (ctx) => {
4657
+ requireOrganizationPlugin(ctx);
4658
+ assertManagedDirectorySyncEnabled(ctx);
4659
+ const organizationId = tryDecode(ctx.params.id);
4660
+ const claims = assertDirectorySyncManagementClaims(ctx, organizationId);
4661
+ await assertTargetOrganizationExists(ctx, organizationId);
4662
+ const policy = resolveCredentialPolicy(ctx.body);
4663
+ await assertManagedDirectoryTransactionsConfigured(ctx);
4664
+ const providerId = tryDecode(ctx.params.providerId);
4665
+ const endpoint = requireManagedEndpoint(ctx, getManagedEndpoints(ctx).rotateSCIMManagedCredential, "rotate");
4666
+ const { connectionId, issued } = await runWithTransaction$1(ctx.context.adapter, async () => {
4667
+ const database = await getCurrentAdapter$1(ctx.context.adapter);
4668
+ const row = await getDirectorySyncConnection(ctx, organizationId, providerId);
4669
+ if (row.status !== "active" || !row.connectionId) throw ctx.error("CONFLICT", { message: "Directory sync connection is not active" });
4670
+ if (!await database.update({
4671
+ model: "directorySyncConnection",
4672
+ where: [
4673
+ {
4674
+ field: "id",
4675
+ value: row.id
4676
+ },
4677
+ {
4678
+ field: "status",
4679
+ value: "active"
4680
+ },
4681
+ {
4682
+ field: "revision",
4683
+ value: row.revision
4684
+ },
4685
+ {
4686
+ field: "connectionId",
4687
+ value: row.connectionId
4688
+ }
4689
+ ],
4690
+ update: {
4691
+ revision: row.revision + 1,
4692
+ updatedAt: /* @__PURE__ */ new Date(),
4693
+ lastActorId: claims.actorId,
4694
+ lastError: null
4695
+ }
4696
+ })) throw ctx.error("CONFLICT", { message: "Directory sync connection changed before credential rotation started" });
4697
+ const issued = await endpoint({
4698
+ body: {
4699
+ connectionId: row.connectionId,
4700
+ provisioningDomainId: row.provisioningDomainId,
4701
+ actorId: claims.actorId,
4702
+ ...policy
4703
+ },
4704
+ context: ctx.context,
4705
+ headers: getRequestHeaders(ctx)
4706
+ });
4707
+ return {
4708
+ connectionId: row.connectionId,
4709
+ issued
3268
4710
  };
3269
4711
  });
3270
- };
3271
- //#endregion
3272
- //#region src/routes/auth/validate.ts
3273
- /**
3274
- * Lightweight endpoint to verify API key ownership during onboarding
3275
- */
3276
- const getValidate = (options) => {
3277
- return createAuthEndpoint("/dash/validate", {
3278
- method: "GET",
3279
- use: [jwtValidateMiddleware(options)]
3280
- }, async () => {
3281
- return { valid: true };
4712
+ setCredentialResponseSecurityHeaders(ctx);
4713
+ return {
4714
+ connectionId,
4715
+ credential: serializeCredential(issued.credential),
4716
+ scimToken: issued.token,
4717
+ scimEndpoint: getScimEndpoint$1(ctx.context.baseURL)
4718
+ };
4719
+ });
4720
+ const revokeDirectoryCredential = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/credentials/:credentialId/revoke", {
4721
+ method: "POST",
4722
+ use: [jwtMiddleware(options, directorySyncClaimsSchema)],
4723
+ body: emptyBodySchema
4724
+ }, async (ctx) => {
4725
+ requireOrganizationPlugin(ctx);
4726
+ assertManagedDirectorySyncEnabled(ctx);
4727
+ const organizationId = tryDecode(ctx.params.id);
4728
+ const claims = assertDirectorySyncManagementClaims(ctx, organizationId);
4729
+ await assertTargetOrganizationExists(ctx, organizationId);
4730
+ await assertManagedDirectoryTransactionsConfigured(ctx);
4731
+ const providerId = tryDecode(ctx.params.providerId);
4732
+ const credentialId = tryDecode(ctx.params.credentialId);
4733
+ const endpoint = requireManagedEndpoint(ctx, getManagedEndpoints(ctx).revokeSCIMManagedCredential, "revoke");
4734
+ const { state, updated } = await runWithTransaction$1(ctx.context.adapter, async () => {
4735
+ const database = await getCurrentAdapter$1(ctx.context.adapter);
4736
+ const row = await getDirectorySyncConnection(ctx, organizationId, providerId);
4737
+ if (row.status !== "active" || !row.connectionId) throw ctx.error("CONFLICT", { message: "Directory sync connection is not active" });
4738
+ const updated = await database.update({
4739
+ model: "directorySyncConnection",
4740
+ where: [
4741
+ {
4742
+ field: "id",
4743
+ value: row.id
4744
+ },
4745
+ {
4746
+ field: "status",
4747
+ value: "active"
4748
+ },
4749
+ {
4750
+ field: "revision",
4751
+ value: row.revision
4752
+ },
4753
+ {
4754
+ field: "connectionId",
4755
+ value: row.connectionId
4756
+ }
4757
+ ],
4758
+ update: {
4759
+ revision: row.revision + 1,
4760
+ updatedAt: /* @__PURE__ */ new Date(),
4761
+ lastActorId: claims.actorId,
4762
+ lastError: null
4763
+ }
4764
+ });
4765
+ if (!updated) throw ctx.error("CONFLICT", { message: "Directory sync connection changed before credential revocation started" });
4766
+ return {
4767
+ state: await endpoint({
4768
+ body: {
4769
+ connectionId: row.connectionId,
4770
+ provisioningDomainId: row.provisioningDomainId,
4771
+ credentialId,
4772
+ actorId: claims.actorId
4773
+ },
4774
+ context: ctx.context,
4775
+ headers: getRequestHeaders(ctx)
4776
+ }),
4777
+ updated
4778
+ };
3282
4779
  });
3283
- };
3284
- //#endregion
3285
- //#region src/semver.ts
3286
- function parseVersion(version) {
3287
- if (!version) return null;
3288
- const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
3289
- if (!match) return null;
3290
- return [
3291
- Number(match[1]),
3292
- Number(match[2]),
3293
- Number(match[3])
3294
- ];
3295
- }
3296
- /** Compare semver strings (e.g. "1.7.0-beta.8" against "1.7" or "1.7.0"). */
3297
- function isVersionAtLeast(version, target) {
3298
- const parsed = parseVersion(version);
3299
- if (!parsed) return false;
3300
- const targetParts = target.split(".").map(Number);
3301
- const major = targetParts[0] ?? 0;
3302
- const minor = targetParts[1] ?? 0;
3303
- const patch = targetParts[2] ?? 0;
3304
- const [vMajor, vMinor, vPatch] = parsed;
3305
- if (vMajor !== major) return vMajor > major;
3306
- if (vMinor !== minor) return vMinor > minor;
3307
- return vPatch >= patch;
3308
- }
3309
- //#endregion
3310
- //#region src/routes/organization-guards.ts
3311
- /** Returns true if organization plugin is enabled. */
3312
- function isOrganizationEnabled(ctx) {
3313
- return !!ctx.context.getPlugin("organization");
3314
- }
3315
- /** Returns the organization plugin, throws if not enabled. Use for write endpoints. */
3316
- function requireOrganizationPlugin(ctx) {
3317
- const plugin = ctx.context.getPlugin("organization");
3318
- if (!plugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
3319
- return plugin;
3320
- }
3321
- /** Returns true if organization plugin and teams feature are enabled. */
3322
- function isTeamsEnabled(ctx) {
3323
- return !!ctx.context.getPlugin("organization")?.options?.teams?.enabled;
3324
- }
3325
- /**
3326
- * Validates that the organization plugin is enabled and teams feature is enabled.
3327
- *
3328
- * @returns The organization options for use in team logic (maximumTeams, hooks, etc.)
3329
- */
3330
- function requireTeamsEnabled(ctx) {
3331
- const orgOptions = requireOrganizationPlugin(ctx).options || {};
3332
- if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
3333
- return orgOptions;
4780
+ return serializeDirectory(updated, getScimEndpoint$1(ctx.context.baseURL), state);
4781
+ });
4782
+ const DIRECTORY_EVENTS_DEFAULT_LIMIT = 10;
4783
+ const DIRECTORY_PAGE_MAX_LIMIT = 100;
4784
+ const DIRECTORY_EVENTS_DEFAULT_SORT_DIRECTION = "desc";
4785
+ const directoryEventsQuerySchema = z.object({
4786
+ limit: z.number().or(z.string().transform(Number)).optional(),
4787
+ offset: z.number().or(z.string().transform(Number)).optional(),
4788
+ sortDirection: z.enum(["asc", "desc"]).optional()
4789
+ }).optional();
4790
+ function resolveDirectoryEventsPage(query) {
4791
+ const requestedLimit = query?.limit;
4792
+ const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(1, Math.floor(requestedLimit)), DIRECTORY_PAGE_MAX_LIMIT) : DIRECTORY_EVENTS_DEFAULT_LIMIT;
4793
+ const requestedOffset = query?.offset;
4794
+ return {
4795
+ limit,
4796
+ offset: Number.isFinite(requestedOffset) ? Math.max(0, Math.floor(requestedOffset)) : 0,
4797
+ sortDirection: query?.sortDirection ?? DIRECTORY_EVENTS_DEFAULT_SORT_DIRECTION
4798
+ };
3334
4799
  }
4800
+ const listDirectoryEvents = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/events", {
4801
+ method: "GET",
4802
+ use: [jwtMiddleware(options, directorySyncClaimsSchema)],
4803
+ query: directoryEventsQuerySchema
4804
+ }, async (ctx) => {
4805
+ requireOrganizationPlugin(ctx);
4806
+ assertManagedDirectorySyncEnabled(ctx);
4807
+ const organizationId = tryDecode(ctx.params.id);
4808
+ assertDirectorySyncManagementClaims(ctx, organizationId);
4809
+ await assertTargetOrganizationExists(ctx, organizationId);
4810
+ const { limit, offset, sortDirection } = resolveDirectoryEventsPage(ctx.query);
4811
+ const row = await getDirectorySyncConnection(ctx, organizationId, tryDecode(ctx.params.providerId));
4812
+ if (!row.connectionId) return {
4813
+ events: [],
4814
+ total: 0,
4815
+ limit,
4816
+ offset
4817
+ };
4818
+ const result = await requireManagedEndpoint(ctx, getManagedEndpoints(ctx).listSCIMManagedConnectionEvents, "events")({
4819
+ body: {
4820
+ connectionId: row.connectionId,
4821
+ provisioningDomainId: row.provisioningDomainId,
4822
+ limit,
4823
+ offset,
4824
+ sortDirection
4825
+ },
4826
+ context: ctx.context,
4827
+ headers: getRequestHeaders(ctx)
4828
+ });
4829
+ const events = result.events.map((event) => ({
4830
+ ...event,
4831
+ createdAt: event.createdAt instanceof Date ? event.createdAt.toISOString() : event.createdAt
4832
+ }));
4833
+ if (typeof result.total === "number" && typeof result.limit === "number" && typeof result.offset === "number") return {
4834
+ events,
4835
+ total: result.total,
4836
+ limit: result.limit,
4837
+ offset: result.offset
4838
+ };
4839
+ const sorted = [...events].sort((a, b) => sortDirection === "asc" ? a.sequence - b.sequence : b.sequence - a.sequence);
4840
+ return {
4841
+ events: sorted.slice(offset, offset + limit),
4842
+ total: sorted.length,
4843
+ limit,
4844
+ offset
4845
+ };
4846
+ });
4847
+ const decommissionOrganizationDirectory = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/decommission", {
4848
+ method: "POST",
4849
+ use: [jwtMiddleware(options, directorySyncClaimsSchema)],
4850
+ body: emptyBodySchema
4851
+ }, async (ctx) => {
4852
+ requireOrganizationPlugin(ctx);
4853
+ assertManagedDirectorySyncEnabled(ctx);
4854
+ const organizationId = tryDecode(ctx.params.id);
4855
+ const claims = assertDirectorySyncManagementClaims(ctx, organizationId);
4856
+ await assertTargetOrganizationExists(ctx, organizationId);
4857
+ let row = await getDirectorySyncConnection(ctx, organizationId, tryDecode(ctx.params.providerId));
4858
+ if (row.status === "decommissioned") return serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), await getManagedState(ctx, row));
4859
+ if (!row.connectionId) throw ctx.error("CONFLICT", { message: "Directory sync connection has an invalid catalog binding" });
4860
+ const connectionId = row.connectionId;
4861
+ if (row.status === "active") row = await startDirectorySyncDecommission(ctx, row, claims.actorId);
4862
+ if (row.status !== "decommissioning") throw ctx.error("CONFLICT", { message: "Directory sync connection cannot be decommissioned" });
4863
+ const state = await requireManagedEndpoint(ctx, getManagedEndpoints(ctx).decommissionSCIMManagedConnection, "decommission")({
4864
+ body: {
4865
+ connectionId,
4866
+ provisioningDomainId: row.provisioningDomainId,
4867
+ actorId: claims.actorId
4868
+ },
4869
+ context: ctx.context,
4870
+ headers: getRequestHeaders(ctx)
4871
+ });
4872
+ if (state.connection.status === "decommissioned" || state.decommission.status === "complete") row = await ctx.context.adapter.update({
4873
+ model: "directorySyncConnection",
4874
+ where: [{
4875
+ field: "id",
4876
+ value: row.id
4877
+ }, {
4878
+ field: "status",
4879
+ value: "decommissioning"
4880
+ }],
4881
+ update: {
4882
+ activeOrganizationKey: createTerminalOrganizationKey(row.aliasKey),
4883
+ activeSsoProviderKey: createTerminalPairingKey(row.aliasKey),
4884
+ status: "decommissioned",
4885
+ decommissionedAt: /* @__PURE__ */ new Date(),
4886
+ updatedAt: /* @__PURE__ */ new Date(),
4887
+ lastActorId: claims.actorId
4888
+ }
4889
+ }) ?? row;
4890
+ return serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), state);
4891
+ });
4892
+ const unpairOrganizationDirectory = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/unpair", {
4893
+ method: "POST",
4894
+ use: [jwtMiddleware(options, directorySyncClaimsSchema)],
4895
+ body: emptyBodySchema
4896
+ }, async (ctx) => {
4897
+ requireOrganizationPlugin(ctx);
4898
+ assertManagedDirectorySyncEnabled(ctx);
4899
+ const organizationId = tryDecode(ctx.params.id);
4900
+ const claims = assertDirectorySyncManagementClaims(ctx, organizationId);
4901
+ await assertTargetOrganizationExists(ctx, organizationId);
4902
+ const updated = await unpairTerminalDirectorySyncConnection(ctx, await getDirectorySyncConnection(ctx, organizationId, tryDecode(ctx.params.providerId)), claims.actorId);
4903
+ return serializeDirectory(updated, getScimEndpoint$1(ctx.context.baseURL), await getManagedState(ctx, updated));
4904
+ });
3335
4905
  //#endregion
3336
4906
  //#region src/routes/directory-sync/session.ts
3337
4907
  async function buildSyntheticSession(ctx, ownerUserId) {
@@ -3367,7 +4937,7 @@ function parseMemberRoles(role) {
3367
4937
  }
3368
4938
  function getScimManagementRoles(ctx) {
3369
4939
  const creatorRole = ctx.context.getPlugin("organization")?.options?.creatorRole ?? "owner";
3370
- return Array.from(new Set(["admin", creatorRole]));
4940
+ return Array.from(/* @__PURE__ */ new Set(["admin", creatorRole]));
3371
4941
  }
3372
4942
  async function findOrganizationScimManagerUserId(ctx, organizationId) {
3373
4943
  const requiredRoles = getScimManagementRoles(ctx);
@@ -3415,13 +4985,12 @@ async function scimProviderExists(ctx, organizationId, providerId) {
3415
4985
  function getScimEndpoint(baseUrl) {
3416
4986
  return `${baseUrl}/scim/v2`;
3417
4987
  }
3418
- function getSCIMPlugin(ctx) {
3419
- return ctx.context.getPlugin("scim");
4988
+ function resolveMode(ctx) {
4989
+ return resolveDirectorySyncMode(getSCIMPlugin(ctx), isManagedDirectorySyncEnabled(ctx));
3420
4990
  }
3421
- function isScimDirectorySyncEnabled(scimPlugin, authVersion) {
3422
- if (!scimPlugin) return false;
3423
- if (isVersionAtLeast(authVersion, "1.7.0")) return true;
3424
- return scimPlugin.options?.providerOwnership?.enabled === true;
4991
+ /** Legacy provider-ownership APIs. Never true when the 1.7+ managed surface exists. */
4992
+ function isLegacyDirectorySyncEnabled(ctx) {
4993
+ return resolveMode(ctx) === "legacy";
3425
4994
  }
3426
4995
  const DIRECTORY_SYNC_DUPLICATE_MESSAGE = "A directory sync connection with this provider ID already exists. Remove the existing connection or choose a different provider ID.";
3427
4996
  function isDuplicateDirectorySyncError(e) {
@@ -3434,14 +5003,15 @@ const listOrganizationDirectories = (options) => {
3434
5003
  method: "GET",
3435
5004
  use: [jwtMiddleware(options)]
3436
5005
  }, async (ctx) => {
5006
+ if (resolveMode(ctx) === "managed") return listOrganizationDirectoriesManagedHandler(ctx);
3437
5007
  if (!isOrganizationEnabled(ctx)) {
3438
5008
  ctx.context.logger.warn("[Dash] Organization plugin not enabled, returning empty directories list");
3439
5009
  return [];
3440
5010
  }
3441
5011
  const organizationId = tryDecode(ctx.params.id);
3442
5012
  const scimPlugin = getSCIMPlugin(ctx);
3443
- if (!scimPlugin?.endpoints.listSCIMProviderConnections || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) {
3444
- ctx.context.logger.warn("[Dash] SCIM plugin not available or provider ownership disabled, returning empty directories list", { organizationId });
5013
+ if (!isLegacyDirectorySyncEnabled(ctx) || !scimPlugin?.endpoints.listSCIMProviderConnections) {
5014
+ ctx.context.logger.warn("[Dash] Legacy SCIM directory sync is unavailable, returning empty directories list", { organizationId });
3445
5015
  return [];
3446
5016
  }
3447
5017
  const managerUserId = await resolveScimManagementUserId(ctx, organizationId);
@@ -3465,19 +5035,19 @@ const listOrganizationDirectories = (options) => {
3465
5035
  }
3466
5036
  });
3467
5037
  };
3468
- const createOrganizationDirectory = (options) => {
5038
+ const createOrganizationDirectoryLegacy = (options) => {
3469
5039
  return createAuthEndpoint("/dash/organization/directory/create", {
3470
5040
  method: "POST",
3471
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
3472
- body: z$1.object({
3473
- providerId: z$1.string().min(1, "Provider ID is required"),
3474
- ownerUserId: z$1.string().min(1, "Owner user ID is required")
5041
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
5042
+ body: z.object({
5043
+ providerId: z.string().min(1, "Provider ID is required"),
5044
+ ownerUserId: z.string().min(1, "Owner user ID is required")
3475
5045
  })
3476
5046
  }, async (ctx) => {
3477
5047
  requireOrganizationPlugin(ctx);
3478
5048
  const { organizationId } = ctx.context.payload;
3479
5049
  const scimPlugin = getSCIMPlugin(ctx);
3480
- if (!scimPlugin?.endpoints.generateSCIMToken || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
5050
+ if (!isLegacyDirectorySyncEnabled(ctx) || !scimPlugin?.endpoints.generateSCIMToken) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
3481
5051
  const { providerId, ownerUserId } = ctx.body;
3482
5052
  if (await scimProviderExists(ctx, organizationId, providerId)) throw ctx.error("BAD_REQUEST", { message: DIRECTORY_SYNC_DUPLICATE_MESSAGE });
3483
5053
  let scimToken;
@@ -3505,15 +5075,15 @@ const createOrganizationDirectory = (options) => {
3505
5075
  };
3506
5076
  });
3507
5077
  };
3508
- const deleteOrganizationDirectory = (options) => {
5078
+ const deleteOrganizationDirectoryLegacy = (options) => {
3509
5079
  return createAuthEndpoint("/dash/organization/directory/delete", {
3510
5080
  method: "POST",
3511
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
3512
- body: z$1.object({ providerId: z$1.string().min(1, "Provider ID is required") })
5081
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
5082
+ body: z.object({ providerId: z.string().min(1, "Provider ID is required") })
3513
5083
  }, async (ctx) => {
3514
5084
  requireOrganizationPlugin(ctx);
3515
5085
  const scimPlugin = getSCIMPlugin(ctx);
3516
- if (!scimPlugin?.endpoints.deleteSCIMProviderConnection || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
5086
+ if (!isLegacyDirectorySyncEnabled(ctx) || !scimPlugin?.endpoints.deleteSCIMProviderConnection) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
3517
5087
  const { organizationId } = ctx.context.payload;
3518
5088
  const { providerId } = ctx.body;
3519
5089
  const managerUserId = await resolveScimManagementUserId(ctx, organizationId, providerId);
@@ -3529,15 +5099,15 @@ const deleteOrganizationDirectory = (options) => {
3529
5099
  return { success: true };
3530
5100
  });
3531
5101
  };
3532
- const regenerateDirectoryToken = (options) => {
5102
+ const regenerateDirectoryTokenLegacy = (options) => {
3533
5103
  return createAuthEndpoint("/dash/organization/directory/regenerate-token", {
3534
5104
  method: "POST",
3535
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
3536
- body: z$1.object({ providerId: z$1.string().min(1, "Provider ID is required") })
5105
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
5106
+ body: z.object({ providerId: z.string().min(1, "Provider ID is required") })
3537
5107
  }, async (ctx) => {
3538
5108
  requireOrganizationPlugin(ctx);
3539
5109
  const scimPlugin = getSCIMPlugin(ctx);
3540
- if (!scimPlugin?.endpoints.generateSCIMToken || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
5110
+ if (!isLegacyDirectorySyncEnabled(ctx) || !scimPlugin?.endpoints.generateSCIMToken) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
3541
5111
  const { organizationId } = ctx.context.payload;
3542
5112
  const { providerId } = ctx.body;
3543
5113
  const managerUserId = await resolveScimManagementUserId(ctx, organizationId, providerId);
@@ -3603,13 +5173,13 @@ const getUserEvents = (options) => {
3603
5173
  return createAuthEndpoint("/events/list", {
3604
5174
  method: "GET",
3605
5175
  use: [sessionMiddleware],
3606
- query: z$1.object({
5176
+ query: z.object({
3607
5177
  /** Maximum number of events to return (default: 50, max: 100) */
3608
- limit: z$1.number().or(z$1.string().transform(Number)).optional(),
5178
+ limit: z.number().or(z.string().transform(Number)).optional(),
3609
5179
  /** Number of events to skip for pagination (default: 0) */
3610
- offset: z$1.number().or(z$1.string().transform(Number)).optional(),
5180
+ offset: z.number().or(z.string().transform(Number)).optional(),
3611
5181
  /** Filter by event type (e.g., "user_signed_in") */
3612
- eventType: z$1.string().optional()
5182
+ eventType: z.string().optional()
3613
5183
  }).optional()
3614
5184
  }, async (ctx) => {
3615
5185
  const session = ctx.context.session;
@@ -3672,13 +5242,13 @@ const getAuditLogs = (options) => {
3672
5242
  return createAuthEndpoint("/events/audit-logs", {
3673
5243
  method: "GET",
3674
5244
  use: [sessionMiddleware],
3675
- query: z$1.object({
3676
- limit: z$1.number().or(z$1.string().transform(Number)).optional(),
3677
- offset: z$1.number().or(z$1.string().transform(Number)).optional(),
3678
- userId: z$1.string().optional(),
3679
- organizationId: z$1.string().optional(),
3680
- identifier: z$1.string().optional(),
3681
- eventType: z$1.string().optional()
5245
+ query: z.object({
5246
+ limit: z.number().or(z.string().transform(Number)).optional(),
5247
+ offset: z.number().or(z.string().transform(Number)).optional(),
5248
+ userId: z.string().optional(),
5249
+ organizationId: z.string().optional(),
5250
+ identifier: z.string().optional(),
5251
+ eventType: z.string().optional()
3682
5252
  }).optional()
3683
5253
  }, async (ctx) => {
3684
5254
  const session = ctx.context.session;
@@ -3753,7 +5323,7 @@ const getAuditLogs = (options) => {
3753
5323
  };
3754
5324
  });
3755
5325
  };
3756
- const OWNER_ADMIN_ROLES = new Set(["owner", "admin"]);
5326
+ const OWNER_ADMIN_ROLES = /* @__PURE__ */ new Set(["owner", "admin"]);
3757
5327
  function isOwnerOrAdminRole(role) {
3758
5328
  return role !== void 0 && OWNER_ADMIN_ROLES.has(role);
3759
5329
  }
@@ -3794,15 +5364,15 @@ const getAllAuditLogs = (options) => {
3794
5364
  return createAuthEndpoint("/events/all-audit-logs", {
3795
5365
  method: "GET",
3796
5366
  use: [sessionMiddleware],
3797
- query: z$1.object({
3798
- limit: z$1.number().or(z$1.string().transform(Number)).optional(),
3799
- offset: z$1.number().or(z$1.string().transform(Number)).optional(),
3800
- userId: z$1.string().optional(),
3801
- organizationId: z$1.string().optional(),
5367
+ query: z.object({
5368
+ limit: z.number().or(z.string().transform(Number)).optional(),
5369
+ offset: z.number().or(z.string().transform(Number)).optional(),
5370
+ userId: z.string().optional(),
5371
+ organizationId: z.string().optional(),
3802
5372
  /** Filter by event type (e.g. `organization_member_added`) */
3803
- eventType: z$1.string().optional(),
5373
+ eventType: z.string().optional(),
3804
5374
  /** Match `eventData.identifier` (organization-scoped actor identity) */
3805
- identifier: z$1.string().optional()
5375
+ identifier: z.string().optional()
3806
5376
  }).refine((q) => {
3807
5377
  const u = q.userId?.trim();
3808
5378
  const o = q.organizationId?.trim();
@@ -3859,10 +5429,10 @@ const getAllAuditLogs = (options) => {
3859
5429
  };
3860
5430
  //#endregion
3861
5431
  //#region src/routes/execute-adapter/index.ts
3862
- const whereClause = z.object({
3863
- field: z.string(),
3864
- value: z.unknown(),
3865
- operator: z.enum([
5432
+ const whereClause = z$1.object({
5433
+ field: z$1.string(),
5434
+ value: z$1.unknown(),
5435
+ operator: z$1.enum([
3866
5436
  "eq",
3867
5437
  "ne",
3868
5438
  "gt",
@@ -3874,44 +5444,44 @@ const whereClause = z.object({
3874
5444
  "starts_with",
3875
5445
  "ends_with"
3876
5446
  ]).optional(),
3877
- connector: z.enum(["AND", "OR"]).optional()
5447
+ connector: z$1.enum(["AND", "OR"]).optional()
3878
5448
  });
3879
- const sortBySchema = z.object({
3880
- field: z.string(),
3881
- direction: z.enum(["asc", "desc"])
5449
+ const sortBySchema = z$1.object({
5450
+ field: z$1.string(),
5451
+ direction: z$1.enum(["asc", "desc"])
3882
5452
  });
3883
- const actionSchema = z.discriminatedUnion("action", [
3884
- z.object({
3885
- action: z.literal("findOne"),
3886
- model: z.string(),
3887
- where: z.array(whereClause).optional(),
3888
- select: z.array(z.string()).optional(),
3889
- join: z.record(z.string(), z.boolean()).optional()
5453
+ const actionSchema = z$1.discriminatedUnion("action", [
5454
+ z$1.object({
5455
+ action: z$1.literal("findOne"),
5456
+ model: z$1.string(),
5457
+ where: z$1.array(whereClause).optional(),
5458
+ select: z$1.array(z$1.string()).optional(),
5459
+ join: z$1.record(z$1.string(), z$1.boolean()).optional()
3890
5460
  }),
3891
- z.object({
3892
- action: z.literal("findMany"),
3893
- model: z.string(),
3894
- where: z.array(whereClause).optional(),
3895
- limit: z.number().optional(),
3896
- offset: z.number().optional(),
5461
+ z$1.object({
5462
+ action: z$1.literal("findMany"),
5463
+ model: z$1.string(),
5464
+ where: z$1.array(whereClause).optional(),
5465
+ limit: z$1.number().optional(),
5466
+ offset: z$1.number().optional(),
3897
5467
  sortBy: sortBySchema.optional(),
3898
- join: z.record(z.string(), z.boolean()).optional()
5468
+ join: z$1.record(z$1.string(), z$1.boolean()).optional()
3899
5469
  }),
3900
- z.object({
3901
- action: z.literal("create"),
3902
- model: z.string(),
3903
- data: z.record(z.string(), z.unknown())
5470
+ z$1.object({
5471
+ action: z$1.literal("create"),
5472
+ model: z$1.string(),
5473
+ data: z$1.record(z$1.string(), z$1.unknown())
3904
5474
  }),
3905
- z.object({
3906
- action: z.literal("update"),
3907
- model: z.string(),
3908
- where: z.array(whereClause),
3909
- update: z.record(z.string(), z.unknown())
5475
+ z$1.object({
5476
+ action: z$1.literal("update"),
5477
+ model: z$1.string(),
5478
+ where: z$1.array(whereClause),
5479
+ update: z$1.record(z$1.string(), z$1.unknown())
3910
5480
  }),
3911
- z.object({
3912
- action: z.literal("count"),
3913
- model: z.string(),
3914
- where: z.array(whereClause).optional()
5481
+ z$1.object({
5482
+ action: z$1.literal("count"),
5483
+ model: z$1.string(),
5484
+ where: z$1.array(whereClause).optional()
3915
5485
  })
3916
5486
  ]);
3917
5487
  const executeAdapter = (options) => {
@@ -4029,8 +5599,8 @@ function isSafeHttpUrl(value) {
4029
5599
  }
4030
5600
  }
4031
5601
  /** http(s) absolute URL without embedded credentials. */
4032
- const safeUrlSchema = z$1.string().trim().min(1).pipe(z$1.url().refine(isSafeHttpUrl, { message: "URL must be a valid http(s) URL without credentials" })).transform((value) => new URL(value).href);
4033
- const optionalSafeUrlSchema = z$1.union([safeUrlSchema, z$1.literal("")]).optional();
5602
+ const safeUrlSchema = z.string().trim().min(1).pipe(z.url().refine(isSafeHttpUrl, { message: "URL must be a valid http(s) URL without credentials" })).transform((value) => new URL(value).href);
5603
+ const optionalSafeUrlSchema = z.union([safeUrlSchema, z.literal("")]).optional();
4034
5604
  function getAuthBaseUrl(ctx) {
4035
5605
  const baseURL = ctx.context.options.baseURL;
4036
5606
  return typeof baseURL === "string" && baseURL.trim() ? baseURL : "/";
@@ -4109,10 +5679,13 @@ async function executePlatformInvitationCompletion(ctx, $api, invitation, args)
4109
5679
  }
4110
5680
  });
4111
5681
  if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
4112
- if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
4113
- session: await ctx.context.internalAdapter.createSession(existingUser.id),
4114
- user: existingUser
4115
- });
5682
+ if (shouldCreateSessionOnInviteComplete(invitation.authMode)) {
5683
+ const session = await ctx.context.internalAdapter.createSession(existingUser.id);
5684
+ await setSessionCookie(ctx, {
5685
+ session,
5686
+ user: existingUser
5687
+ });
5688
+ }
4116
5689
  return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)) };
4117
5690
  }
4118
5691
  if (!password && !allowsPasswordlessInviteComplete(invitation.authMode)) throw new APIError("BAD_REQUEST", { message: "Password is required to complete this invitation." });
@@ -4125,10 +5698,8 @@ async function executePlatformInvitationCompletion(ctx, $api, invitation, args)
4125
5698
  };
4126
5699
  const adapter = ctx.context.internalAdapter;
4127
5700
  const user = isVersionAtLeast(ctx.context.version, "1.7.0") ? await adapter.createUser(userPayload, { method: "admin" }) : await adapter.createUser(userPayload);
4128
- if (password) await ctx.context.internalAdapter.createAccount({
5701
+ if (password) await createCredentialAccountCompat(ctx.context.internalAdapter, {
4129
5702
  userId: user.id,
4130
- providerId: "credential",
4131
- accountId: user.id,
4132
5703
  password: await ctx.context.password.hash(password)
4133
5704
  });
4134
5705
  const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
@@ -4139,10 +5710,13 @@ async function executePlatformInvitationCompletion(ctx, $api, invitation, args)
4139
5710
  }
4140
5711
  });
4141
5712
  if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
4142
- if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
4143
- session: await ctx.context.internalAdapter.createSession(user.id),
4144
- user
4145
- });
5713
+ if (shouldCreateSessionOnInviteComplete(invitation.authMode)) {
5714
+ const session = await ctx.context.internalAdapter.createSession(user.id);
5715
+ await setSessionCookie(ctx, {
5716
+ session,
5717
+ user
5718
+ });
5719
+ }
4146
5720
  return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)) };
4147
5721
  }
4148
5722
  /**
@@ -4154,7 +5728,7 @@ const acceptInvitation = (options) => {
4154
5728
  const { $api } = options;
4155
5729
  return createAuthEndpoint("/dash/accept-invitation", {
4156
5730
  method: "GET",
4157
- query: z$1.object({ token: z$1.string() })
5731
+ query: z.object({ token: z.string() })
4158
5732
  }, async (ctx) => {
4159
5733
  const { token } = ctx.query;
4160
5734
  const invitation = await verifyPendingInvitation(token, $api, ctx);
@@ -4169,10 +5743,13 @@ const acceptInvitation = (options) => {
4169
5743
  }
4170
5744
  });
4171
5745
  if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted (existing user)", markError);
4172
- if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
4173
- session: await ctx.context.internalAdapter.createSession(existingUser.id),
4174
- user: existingUser
4175
- });
5746
+ if (shouldCreateSessionOnInviteComplete(invitation.authMode)) {
5747
+ const session = await ctx.context.internalAdapter.createSession(existingUser.id);
5748
+ await setSessionCookie(ctx, {
5749
+ session,
5750
+ user: existingUser
5751
+ });
5752
+ }
4176
5753
  const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx));
4177
5754
  return ctx.redirect(redirectUrl);
4178
5755
  }
@@ -4201,10 +5778,13 @@ const acceptInvitation = (options) => {
4201
5778
  }
4202
5779
  });
4203
5780
  if (markError) ctx.context.logger.warn("[Dash] Failed to mark invitation as accepted", markError);
4204
- if (shouldCreateSessionOnInviteComplete(invitation.authMode)) await setSessionCookie(ctx, {
4205
- session: await ctx.context.internalAdapter.createSession(user.id),
4206
- user
4207
- });
5781
+ if (shouldCreateSessionOnInviteComplete(invitation.authMode)) {
5782
+ const session = await ctx.context.internalAdapter.createSession(user.id);
5783
+ await setSessionCookie(ctx, {
5784
+ session,
5785
+ user
5786
+ });
5787
+ }
4208
5788
  const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx));
4209
5789
  return ctx.redirect(redirectUrl);
4210
5790
  });
@@ -4218,13 +5798,14 @@ const completeInvitation = (options) => {
4218
5798
  const { $api } = options;
4219
5799
  return createAuthEndpoint("/dash/complete-invitation", {
4220
5800
  method: "POST",
4221
- body: z$1.object({
4222
- token: z$1.string(),
4223
- password: z$1.string().optional()
5801
+ body: z.object({
5802
+ token: z.string(),
5803
+ password: z.string().optional()
4224
5804
  })
4225
5805
  }, async (ctx) => {
4226
5806
  const { token, password } = ctx.body;
4227
- const { redirectUrl } = await executePlatformInvitationCompletion(ctx, $api, await verifyPendingInvitation(token, $api, ctx), {
5807
+ const invitation = await verifyPendingInvitation(token, $api, ctx);
5808
+ const { redirectUrl } = await executePlatformInvitationCompletion(ctx, $api, invitation, {
4228
5809
  token,
4229
5810
  password
4230
5811
  });
@@ -4242,14 +5823,15 @@ const completeInvitationHandoff = (options) => {
4242
5823
  const { $api } = options;
4243
5824
  return createAuthEndpoint("/dash/complete-invitation-handoff", {
4244
5825
  method: "GET",
4245
- query: z$1.object({ handoff: z$1.string().min(1) })
5826
+ query: z.object({ handoff: z.string().min(1) })
4246
5827
  }, async (ctx) => {
4247
5828
  const { data, error } = await $api("/api/internal/invitations/redeem-handoff", {
4248
5829
  method: "POST",
4249
5830
  body: { handoff: ctx.query.handoff }
4250
5831
  });
4251
5832
  if (error || !data?.invitationToken) throw new APIError("BAD_REQUEST", { message: "This invitation link has expired. Please try again." });
4252
- const { redirectUrl } = await executePlatformInvitationCompletion(ctx, $api, await verifyPendingInvitation(data.invitationToken, $api, ctx), {
5833
+ const invitation = await verifyPendingInvitation(data.invitationToken, $api, ctx);
5834
+ const { redirectUrl } = await executePlatformInvitationCompletion(ctx, $api, invitation, {
4253
5835
  token: data.invitationToken,
4254
5836
  password: data.password ?? void 0
4255
5837
  });
@@ -4260,7 +5842,7 @@ const completeInvitationSocial = (options) => {
4260
5842
  const { $api } = options;
4261
5843
  return createAuthEndpoint("/dash/complete-invitation-social", {
4262
5844
  method: "GET",
4263
- query: z$1.object({ token: z$1.string() }),
5845
+ query: z.object({ token: z.string() }),
4264
5846
  use: [sessionMiddleware]
4265
5847
  }, async (ctx) => {
4266
5848
  const sessionUser = ctx.context.session?.user;
@@ -4295,7 +5877,7 @@ const checkUserExists = (options) => {
4295
5877
  return createAuthEndpoint("/dash/check-user-exists", {
4296
5878
  method: "POST",
4297
5879
  use: [jwtMiddleware(options)],
4298
- body: z$1.object({ email: z$1.email() })
5880
+ body: z.object({ email: z.email() })
4299
5881
  }, async (ctx) => {
4300
5882
  const { email } = ctx.body;
4301
5883
  const normalizedEmail = normalizeEmail(email, ctx.context);
@@ -4325,7 +5907,7 @@ function getOrganizationRoleKeys(orgOptions) {
4325
5907
  if (roles && typeof roles === "object" && Object.keys(roles).length > 0) return Object.keys(roles);
4326
5908
  return [...DEFAULT_ORG_MEMBER_ROLES];
4327
5909
  }
4328
- const organizationMemberRoleInputSchema = z$1.string().trim().min(1, "Role is required").max(64, "Role is too long").regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, "Role must start with a letter and contain only letters, numbers, hyphens, and underscores");
5910
+ const organizationMemberRoleInputSchema = z.string().trim().min(1, "Role is required").max(64, "Role is too long").regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, "Role must start with a letter and contain only letters, numbers, hyphens, and underscores");
4329
5911
  function validateOrganizationMemberRole(ctx, role, orgOptions) {
4330
5912
  const allowedRoles = getOrganizationRoleKeys(orgOptions);
4331
5913
  if (!allowedRoles.includes(role)) throw ctx.error("BAD_REQUEST", { message: `Invalid role. Allowed roles: ${allowedRoles.join(", ")}` });
@@ -4387,14 +5969,14 @@ const listOrganizationInvitations = (options) => {
4387
5969
  const inviteMember = (options) => {
4388
5970
  return createAuthEndpoint("/dash/organization/invite-member", {
4389
5971
  method: "POST",
4390
- body: z$1.object({
4391
- email: z$1.string(),
5972
+ body: z.object({
5973
+ email: z.string(),
4392
5974
  role: organizationMemberRoleInputSchema,
4393
- invitedBy: z$1.string()
5975
+ invitedBy: z.string()
4394
5976
  }),
4395
- use: [jwtMiddleware(options, z$1.object({
4396
- organizationId: z$1.string(),
4397
- invitedBy: z$1.string()
5977
+ use: [jwtMiddleware(options, z.object({
5978
+ organizationId: z.string(),
5979
+ invitedBy: z.string()
4398
5980
  }))]
4399
5981
  }, async (ctx) => {
4400
5982
  const { organizationId } = ctx.context.payload;
@@ -4431,8 +6013,8 @@ const inviteMember = (options) => {
4431
6013
  const checkUserByEmail = (options) => {
4432
6014
  return createAuthEndpoint("/dash/organization/check-user-by-email", {
4433
6015
  method: "POST",
4434
- body: z$1.object({ email: z$1.string() }),
4435
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))]
6016
+ body: z.object({ email: z.string() }),
6017
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))]
4436
6018
  }, async (ctx) => {
4437
6019
  requireOrganizationPlugin(ctx);
4438
6020
  const { organizationId } = ctx.context.payload;
@@ -4475,11 +6057,11 @@ const checkUserByEmail = (options) => {
4475
6057
  const cancelInvitation = (options) => {
4476
6058
  return createAuthEndpoint("/dash/organization/cancel-invitation", {
4477
6059
  method: "POST",
4478
- use: [jwtMiddleware(options, z$1.object({
4479
- organizationId: z$1.string(),
4480
- invitationId: z$1.string()
6060
+ use: [jwtMiddleware(options, z.object({
6061
+ organizationId: z.string(),
6062
+ invitationId: z.string()
4481
6063
  }))],
4482
- body: z$1.object({ invitationId: z$1.string() })
6064
+ body: z.object({ invitationId: z.string() })
4483
6065
  }, async (ctx) => {
4484
6066
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
4485
6067
  const { invitationId, organizationId } = ctx.context.payload;
@@ -4533,11 +6115,11 @@ const cancelInvitation = (options) => {
4533
6115
  const resendInvitation = (options) => {
4534
6116
  return createAuthEndpoint("/dash/organization/resend-invitation", {
4535
6117
  method: "POST",
4536
- use: [jwtMiddleware(options, z$1.object({
4537
- organizationId: z$1.string(),
4538
- invitationId: z$1.string()
6118
+ use: [jwtMiddleware(options, z.object({
6119
+ organizationId: z.string(),
6120
+ invitationId: z.string()
4539
6121
  }))],
4540
- body: z$1.object({ invitationId: z$1.string() })
6122
+ body: z.object({ invitationId: z.string() })
4541
6123
  }, async (ctx) => {
4542
6124
  const organizationPlugin = requireOrganizationPlugin(ctx);
4543
6125
  const { invitationId, organizationId } = ctx.context.payload;
@@ -4646,9 +6228,9 @@ const listOrganizationMembers = (options) => {
4646
6228
  const addMember = (options) => {
4647
6229
  return createAuthEndpoint("/dash/organization/add-member", {
4648
6230
  method: "POST",
4649
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
4650
- body: z$1.object({
4651
- userId: z$1.string(),
6231
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
6232
+ body: z.object({
6233
+ userId: z.string(),
4652
6234
  role: organizationMemberRoleInputSchema
4653
6235
  })
4654
6236
  }, async (ctx) => {
@@ -4703,8 +6285,8 @@ const addMember = (options) => {
4703
6285
  const removeMember = (options) => {
4704
6286
  return createAuthEndpoint("/dash/organization/remove-member", {
4705
6287
  method: "POST",
4706
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
4707
- body: z$1.object({ memberId: z$1.string() })
6288
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
6289
+ body: z.object({ memberId: z.string() })
4708
6290
  }, async (ctx) => {
4709
6291
  const { organizationId } = ctx.context.payload;
4710
6292
  const orgOptions = requireOrganizationPlugin(ctx).options || {};
@@ -4773,9 +6355,9 @@ const removeMember = (options) => {
4773
6355
  const updateMemberRole = (options) => {
4774
6356
  return createAuthEndpoint("/dash/organization/update-member-role", {
4775
6357
  method: "POST",
4776
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
4777
- body: z$1.object({
4778
- memberId: z$1.string(),
6358
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
6359
+ body: z.object({
6360
+ memberId: z.string(),
4779
6361
  role: organizationMemberRoleInputSchema
4780
6362
  })
4781
6363
  }, async (ctx) => {
@@ -4834,7 +6416,7 @@ const updateMemberRole = (options) => {
4834
6416
  //#region src/export-factory.ts
4835
6417
  const exportFactory = (input, options) => async (ctx) => {
4836
6418
  const batchSize = options?.batchSize || 1e4;
4837
- const staleMs = options?.staleMs || 300 * 1e3;
6419
+ const staleMs = options?.staleMs || 3e5;
4838
6420
  const enabledFields = options?.enabledFields || [];
4839
6421
  const userLimit = input.limit;
4840
6422
  const userOffset = input.offset || 0;
@@ -4931,16 +6513,16 @@ async function withConcurrency(items, fn, options) {
4931
6513
  return results;
4932
6514
  }
4933
6515
  /** Zod schema for validating organization/project slug format. */
4934
- const slugSchema = z.string().min(1, "Slug is required").regex(/^[a-z0-9-]+$/, "Slug can only contain lowercase letters, numbers, and hyphens");
6516
+ const slugSchema = z$1.string().min(1, "Slug is required").regex(/^[a-z0-9-]+$/, "Slug can only contain lowercase letters, numbers, and hyphens");
4935
6517
  //#endregion
4936
6518
  //#region src/routes/organizations/schemas.ts
4937
- const DENIED_ORG_WRITE_KEYS = new Set([
6519
+ const DENIED_ORG_WRITE_KEYS = /* @__PURE__ */ new Set([
4938
6520
  "id",
4939
6521
  "createdAt",
4940
6522
  "updatedAt"
4941
6523
  ]);
4942
- const CREATE_NON_PERSISTED_KEYS = new Set(["defaultTeamName"]);
4943
- const CREATE_REQUEST_ONLY_KEYS$1 = new Set(["userId", "skipDefaultTeam"]);
6524
+ const CREATE_NON_PERSISTED_KEYS = /* @__PURE__ */ new Set(["defaultTeamName"]);
6525
+ const CREATE_REQUEST_ONLY_KEYS$1 = /* @__PURE__ */ new Set(["userId", "skipDefaultTeam"]);
4944
6526
  const CORE_CREATE_KEYS$1 = [
4945
6527
  "name",
4946
6528
  "slug",
@@ -4972,7 +6554,7 @@ function getWritableOrganizationFieldNames(orgOptions, mode) {
4972
6554
  */
4973
6555
  function pickWritableOrganizationFields(body, orgOptions, mode) {
4974
6556
  const allowed = getWritableOrganizationFieldNames(orgOptions, mode);
4975
- const skip = new Set([
6557
+ const skip = /* @__PURE__ */ new Set([
4976
6558
  ...DENIED_ORG_WRITE_KEYS,
4977
6559
  ...mode === "create" ? CREATE_NON_PERSISTED_KEYS : [],
4978
6560
  ...mode === "create" ? CREATE_REQUEST_ONLY_KEYS$1 : []
@@ -4984,26 +6566,26 @@ function pickWritableOrganizationFields(body, orgOptions, mode) {
4984
6566
  }
4985
6567
  return result;
4986
6568
  }
4987
- const BaseCreateOrgCoreBodySchema = z$1.object({
4988
- name: z$1.string(),
6569
+ const BaseCreateOrgCoreBodySchema = z.object({
6570
+ name: z.string(),
4989
6571
  slug: slugSchema,
4990
- logo: z$1.string().optional(),
4991
- defaultTeamName: z$1.string().optional()
6572
+ logo: z.string().optional(),
6573
+ defaultTeamName: z.string().optional()
4992
6574
  });
4993
- const BaseUpdateOrgCoreBodySchema = z$1.object({
6575
+ const BaseUpdateOrgCoreBodySchema = z.object({
4994
6576
  logo: optionalSafeUrlSchema,
4995
- name: z$1.string().optional(),
6577
+ name: z.string().optional(),
4996
6578
  slug: slugSchema.optional(),
4997
- metadata: z$1.string().optional()
6579
+ metadata: z.string().optional()
4998
6580
  });
4999
- const CreateOrganizationBodySchema = BaseCreateOrgCoreBodySchema.catchall(z$1.unknown());
5000
- const UpdateOrganizationBodySchema = BaseUpdateOrgCoreBodySchema.catchall(z$1.unknown());
6581
+ const CreateOrganizationBodySchema = BaseCreateOrgCoreBodySchema.catchall(z.unknown());
6582
+ const UpdateOrganizationBodySchema = BaseUpdateOrgCoreBodySchema.catchall(z.unknown());
5001
6583
  function createSchemaForDBField$1(field) {
5002
6584
  switch (field.type) {
5003
- case "number": return field.required ? z$1.coerce.number() : z$1.coerce.number().optional();
5004
- case "boolean": return field.required ? z$1.coerce.boolean() : z$1.coerce.boolean().optional();
5005
- case "date": return field.required ? z$1.union([z$1.string().min(1), z$1.coerce.date()]) : z$1.union([z$1.string(), z$1.coerce.date()]).optional();
5006
- default: return field.required ? z$1.string().min(1) : z$1.string().optional();
6585
+ case "number": return field.required ? z.coerce.number() : z.coerce.number().optional();
6586
+ case "boolean": return field.required ? z.coerce.boolean() : z.coerce.boolean().optional();
6587
+ case "date": return field.required ? z.union([z.string().min(1), z.coerce.date()]) : z.union([z.string(), z.coerce.date()]).optional();
6588
+ default: return field.required ? z.string().min(1) : z.string().optional();
5007
6589
  }
5008
6590
  }
5009
6591
  /**
@@ -5012,39 +6594,39 @@ function createSchemaForDBField$1(field) {
5012
6594
  */
5013
6595
  function validateWritableCreateOrganizationFields(data, options) {
5014
6596
  const shape = {
5015
- name: z$1.string().min(1),
6597
+ name: z.string().min(1),
5016
6598
  slug: slugSchema,
5017
- logo: z$1.string().optional()
6599
+ logo: z.string().optional()
5018
6600
  };
5019
6601
  const additional = getOrganizationAdditionalFields(options);
5020
6602
  for (const [name, field] of Object.entries(additional)) {
5021
6603
  if (field.input === false) continue;
5022
6604
  shape[name] = createSchemaForDBField$1(field);
5023
6605
  }
5024
- const result = z$1.object(shape).strict().safeParse(data);
6606
+ const result = z.object(shape).strict().safeParse(data);
5025
6607
  if (!result.success) throw result.error;
5026
6608
  }
5027
6609
  /**
5028
6610
  * Ensures at least one field is present on update and validates additional field types.
5029
6611
  */
5030
6612
  function validateWritableOrganizationUpdateFields(data, orgOptions) {
5031
- if (Object.keys(data).length === 0) throw new z$1.ZodError([{
6613
+ if (Object.keys(data).length === 0) throw new z.ZodError([{
5032
6614
  code: "custom",
5033
6615
  message: "No valid fields to update",
5034
6616
  path: []
5035
6617
  }]);
5036
6618
  const shape = {
5037
- name: z$1.string().min(1).optional(),
6619
+ name: z.string().min(1).optional(),
5038
6620
  slug: slugSchema.optional(),
5039
6621
  logo: optionalSafeUrlSchema,
5040
- metadata: z$1.string().optional()
6622
+ metadata: z.string().optional()
5041
6623
  };
5042
6624
  const additional = getOrganizationAdditionalFields(orgOptions);
5043
6625
  for (const [name, field] of Object.entries(additional)) {
5044
6626
  if (field.input === false) continue;
5045
6627
  shape[name] = createSchemaForDBField$1(field);
5046
6628
  }
5047
- const result = z$1.object(shape).partial().strict().safeParse(data);
6629
+ const result = z.object(shape).partial().strict().safeParse(data);
5048
6630
  if (!result.success) throw result.error;
5049
6631
  }
5050
6632
  //#endregion
@@ -5086,26 +6668,26 @@ const listOrganizations = (options) => {
5086
6668
  return createAuthEndpoint("/dash/list-organizations", {
5087
6669
  method: "GET",
5088
6670
  use: [jwtMiddleware(options)],
5089
- query: z$1.object({
5090
- limit: z$1.number().or(z$1.string().transform(Number)).optional(),
5091
- offset: z$1.number().or(z$1.string().transform(Number)).optional(),
5092
- sortBy: z$1.enum([
6671
+ query: z.object({
6672
+ limit: z.number().or(z.string().transform(Number)).optional(),
6673
+ offset: z.number().or(z.string().transform(Number)).optional(),
6674
+ sortBy: z.enum([
5093
6675
  "createdAt",
5094
6676
  "name",
5095
6677
  "slug",
5096
6678
  "members"
5097
6679
  ]).optional(),
5098
- sortOrder: z$1.enum(["asc", "desc"]).optional(),
5099
- filterMembers: z$1.enum([
6680
+ sortOrder: z.enum(["asc", "desc"]).optional(),
6681
+ filterMembers: z.enum([
5100
6682
  "abandoned",
5101
6683
  "eq1",
5102
6684
  "gt1",
5103
6685
  "gt5",
5104
6686
  "gt10"
5105
6687
  ]).optional(),
5106
- search: z$1.string().optional(),
5107
- startDate: z$1.date().or(z$1.string().transform((val) => new Date(val))).optional(),
5108
- endDate: z$1.date().or(z$1.string().transform((val) => new Date(val))).optional()
6688
+ search: z.string().optional(),
6689
+ startDate: z.date().or(z.string().transform((val) => new Date(val))).optional(),
6690
+ endDate: z.date().or(z.string().transform((val) => new Date(val))).optional()
5109
6691
  }).optional()
5110
6692
  }, async (ctx) => {
5111
6693
  const { limit = 10, offset = 0, sortBy = "createdAt", sortOrder = "desc", search, filterMembers } = ctx.query || {};
@@ -5226,12 +6808,12 @@ function parseWhereClause$1(val) {
5226
6808
  if (!Array.isArray(parsed)) return [];
5227
6809
  return parsed;
5228
6810
  }
5229
- const exportOrganizationsQuerySchema = z$1.object({
5230
- limit: z$1.number().or(z$1.string().transform(Number)).optional(),
5231
- offset: z$1.number().or(z$1.string().transform(Number)).optional(),
5232
- sortBy: z$1.string().optional(),
5233
- sortOrder: z$1.enum(["asc", "desc"]).optional(),
5234
- where: z$1.string().transform(parseWhereClause$1).optional()
6811
+ const exportOrganizationsQuerySchema = z.object({
6812
+ limit: z.number().or(z.string().transform(Number)).optional(),
6813
+ offset: z.number().or(z.string().transform(Number)).optional(),
6814
+ sortBy: z.string().optional(),
6815
+ sortOrder: z.enum(["asc", "desc"]).optional(),
6816
+ where: z.string().transform(parseWhereClause$1).optional()
5235
6817
  }).optional();
5236
6818
  const exportOrganizations = (options) => {
5237
6819
  return createAuthEndpoint("/dash/export-organizations", {
@@ -5296,8 +6878,8 @@ const getOrganization = (options) => {
5296
6878
  const deleteOrganization = (options) => {
5297
6879
  return createAuthEndpoint("/dash/organization/delete", {
5298
6880
  method: "POST",
5299
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
5300
- body: z$1.object({ organizationId: z$1.string() })
6881
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
6882
+ body: z.object({ organizationId: z.string() })
5301
6883
  }, async (ctx) => {
5302
6884
  const { organizationId } = ctx.context.payload;
5303
6885
  const { organizationId: bodyOrganizationId } = ctx.body;
@@ -5346,7 +6928,7 @@ const deleteOrganization = (options) => {
5346
6928
  const deleteManyOrganizations = (options) => {
5347
6929
  return createAuthEndpoint("/dash/organization/delete-many", {
5348
6930
  method: "POST",
5349
- use: [jwtMiddleware(options, z$1.object({ organizationIds: z$1.string().array() }))]
6931
+ use: [jwtMiddleware(options, z.object({ organizationIds: z.string().array() }))]
5350
6932
  }, async (ctx) => {
5351
6933
  requireOrganizationPlugin(ctx);
5352
6934
  const { organizationIds } = ctx.context.payload;
@@ -5383,9 +6965,9 @@ const deleteManyOrganizations = (options) => {
5383
6965
  const createOrganization = (options) => {
5384
6966
  return createAuthEndpoint("/dash/organization/create", {
5385
6967
  method: "POST",
5386
- use: [jwtMiddleware(options, z$1.object({
5387
- userId: z$1.string(),
5388
- skipDefaultTeam: z$1.boolean().optional().default(false)
6968
+ use: [jwtMiddleware(options, z.object({
6969
+ userId: z.string(),
6970
+ skipDefaultTeam: z.boolean().optional().default(false)
5389
6971
  }))],
5390
6972
  body: CreateOrganizationBodySchema
5391
6973
  }, async (ctx) => {
@@ -5411,7 +6993,7 @@ const createOrganization = (options) => {
5411
6993
  try {
5412
6994
  validateWritableCreateOrganizationFields(orgData, orgOptions);
5413
6995
  } catch (error) {
5414
- if (error instanceof z$1.ZodError) throw ctx.error("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid organization data" });
6996
+ if (error instanceof z.ZodError) throw ctx.error("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid organization data" });
5415
6997
  throw error;
5416
6998
  }
5417
6999
  if (orgOptions.organizationCreation?.beforeCreate) {
@@ -5547,7 +7129,7 @@ const createOrganization = (options) => {
5547
7129
  const updateOrganization = (options) => {
5548
7130
  return createAuthEndpoint("/dash/organization/update", {
5549
7131
  method: "POST",
5550
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
7132
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
5551
7133
  body: UpdateOrganizationBodySchema
5552
7134
  }, async (ctx) => {
5553
7135
  const { organizationId } = ctx.context.payload;
@@ -5587,7 +7169,7 @@ const updateOrganization = (options) => {
5587
7169
  try {
5588
7170
  validateWritableOrganizationUpdateFields(updateData, orgOptions);
5589
7171
  } catch (error) {
5590
- if (error instanceof z$1.ZodError) throw ctx.error("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid organization data" });
7172
+ if (error instanceof z.ZodError) throw ctx.error("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid organization data" });
5591
7173
  throw error;
5592
7174
  }
5593
7175
  if (typeof updateData.metadata === "string") try {
@@ -5674,10 +7256,10 @@ const listOrganizationTeams = (options) => {
5674
7256
  const updateTeam = (options) => {
5675
7257
  return createAuthEndpoint("/dash/organization/update-team", {
5676
7258
  method: "POST",
5677
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
5678
- body: z$1.object({
5679
- teamId: z$1.string(),
5680
- name: z$1.string().optional()
7259
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
7260
+ body: z.object({
7261
+ teamId: z.string(),
7262
+ name: z.string().optional()
5681
7263
  })
5682
7264
  }, async (ctx) => {
5683
7265
  const { organizationId } = ctx.context.payload;
@@ -5748,8 +7330,8 @@ const updateTeam = (options) => {
5748
7330
  const deleteTeam = (options) => {
5749
7331
  return createAuthEndpoint("/dash/organization/delete-team", {
5750
7332
  method: "POST",
5751
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
5752
- body: z$1.object({ teamId: z$1.string() })
7333
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
7334
+ body: z.object({ teamId: z.string() })
5753
7335
  }, async (ctx) => {
5754
7336
  const { organizationId } = ctx.context.payload;
5755
7337
  const orgOptions = requireTeamsEnabled(ctx);
@@ -5817,8 +7399,8 @@ const deleteTeam = (options) => {
5817
7399
  const createTeam = (options) => {
5818
7400
  return createAuthEndpoint("/dash/organization/create-team", {
5819
7401
  method: "POST",
5820
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
5821
- body: z$1.object({ name: z$1.string() })
7402
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
7403
+ body: z.object({ name: z.string() })
5822
7404
  }, async (ctx) => {
5823
7405
  const { organizationId } = ctx.context.payload;
5824
7406
  const orgOptions = requireTeamsEnabled(ctx);
@@ -5936,10 +7518,10 @@ const listTeamMembers = (options) => {
5936
7518
  const addTeamMember = (options) => {
5937
7519
  return createAuthEndpoint("/dash/organization/add-team-member", {
5938
7520
  method: "POST",
5939
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
5940
- body: z$1.object({
5941
- teamId: z$1.string(),
5942
- userId: z$1.string()
7521
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
7522
+ body: z.object({
7523
+ teamId: z.string(),
7524
+ userId: z.string()
5943
7525
  })
5944
7526
  }, async (ctx) => {
5945
7527
  const { organizationId } = ctx.context.payload;
@@ -6029,10 +7611,10 @@ const addTeamMember = (options) => {
6029
7611
  const removeTeamMember = (options) => {
6030
7612
  return createAuthEndpoint("/dash/organization/remove-team-member", {
6031
7613
  method: "POST",
6032
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
6033
- body: z$1.object({
6034
- teamId: z$1.string(),
6035
- userId: z$1.string()
7614
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
7615
+ body: z.object({
7616
+ teamId: z.string(),
7617
+ userId: z.string()
6036
7618
  })
6037
7619
  }, async (ctx) => {
6038
7620
  const { organizationId } = ctx.context.payload;
@@ -6135,7 +7717,7 @@ const revokeSession = (options) => createAuthEndpoint("/dash/sessions/revoke", {
6135
7717
  const revokeAllSessions = (options) => createAuthEndpoint("/dash/sessions/revoke-all", {
6136
7718
  method: "POST",
6137
7719
  use: [jwtMiddleware(options)],
6138
- body: z$1.object({ userId: z$1.string() })
7720
+ body: z.object({ userId: z.string() })
6139
7721
  }, async (ctx) => {
6140
7722
  const { userId } = ctx.body;
6141
7723
  if (!await ctx.context.internalAdapter.findUserById(userId)) throw ctx.error("NOT_FOUND", { message: "User not found" });
@@ -6144,7 +7726,7 @@ const revokeAllSessions = (options) => createAuthEndpoint("/dash/sessions/revoke
6144
7726
  });
6145
7727
  const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revoke-many", {
6146
7728
  method: "POST",
6147
- use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))]
7729
+ use: [jwtMiddleware(options, z.object({ userIds: z.string().array() }))]
6148
7730
  }, async (ctx) => {
6149
7731
  const { userIds } = ctx.context.payload;
6150
7732
  await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
@@ -6189,7 +7771,7 @@ function getSSOPlugin(ctx) {
6189
7771
  * (see packages/sso dist constants). Inlined so consumers (e.g. Metro) never
6190
7772
  * statically pull @better-auth/sso for validation-only code paths.
6191
7773
  */
6192
- const DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024;
7774
+ const DEFAULT_MAX_SAML_METADATA_SIZE = 102400;
6193
7775
  const RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
6194
7776
  const SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1";
6195
7777
  /** @public */
@@ -6233,48 +7815,84 @@ function validateSAMLMetadataAlgorithms(metadataXml) {
6233
7815
  return warnings;
6234
7816
  }
6235
7817
  //#endregion
7818
+ //#region src/routes/sso/verify-domain-error.ts
7819
+ const DOMAIN_ALREADY_VERIFIED = {
7820
+ verified: true,
7821
+ message: "Domain has already been verified"
7822
+ };
7823
+ const PROVIDER_CHANGED = {
7824
+ verified: false,
7825
+ message: "SSO provider changed while domain verification was in progress. Reload the provider and try again."
7826
+ };
7827
+ const DNS_RECORD_MISSING = {
7828
+ verified: false,
7829
+ message: "Unable to verify domain ownership. The TXT record was not found. It may take up to 48 hours for DNS changes to propagate."
7830
+ };
7831
+ function isRecord(value) {
7832
+ return typeof value === "object" && value !== null;
7833
+ }
7834
+ function getApiErrorCode(error) {
7835
+ const withExtras = error;
7836
+ if (typeof withExtras.code === "string") return withExtras.code;
7837
+ if (isRecord(withExtras.body) && typeof withExtras.body.code === "string") return withExtras.body.code;
7838
+ }
7839
+ /**
7840
+ * Maps SSO plugin `verifyDomain` API errors to dash responses.
7841
+ * Returns `null` when the caller should rethrow.
7842
+ */
7843
+ function mapSsoVerifyDomainApiError(error) {
7844
+ if (error.status === "CONFLICT") {
7845
+ if (getApiErrorCode(error) === "SSO_PROVIDER_CHANGED") return PROVIDER_CHANGED;
7846
+ return DOMAIN_ALREADY_VERIFIED;
7847
+ }
7848
+ if (error.status === "BAD_GATEWAY") return DNS_RECORD_MISSING;
7849
+ return null;
7850
+ }
7851
+ //#endregion
6236
7852
  //#region src/routes/sso/index.ts
6237
7853
  function requireOrganizationAccess(ctx) {
6238
7854
  const orgIdFromUrl = tryDecode(ctx.params.id);
6239
7855
  const orgIdFromToken = ctx.context.payload?.organizationId;
6240
7856
  if (!orgIdFromToken || orgIdFromUrl !== orgIdFromToken) throw ctx.error("FORBIDDEN", { message: "You do not have access to this organization" });
6241
7857
  }
6242
- const samlConfigSchema = z$1.object({
6243
- idpMetadata: z$1.object({
6244
- metadata: z$1.string().optional(),
6245
- metadataUrl: z$1.string().optional()
7858
+ const samlConfigSchema = z.object({
7859
+ idpMetadata: z.object({
7860
+ metadata: z.string().optional(),
7861
+ metadataUrl: z.string().optional()
6246
7862
  }).optional(),
6247
- entryPoint: z$1.string().optional(),
6248
- cert: z$1.string().optional(),
6249
- entityId: z$1.string().optional(),
6250
- mapping: z$1.object({
6251
- id: z$1.string().optional(),
6252
- email: z$1.string().optional(),
6253
- emailVerified: z$1.string().optional(),
6254
- name: z$1.string().optional(),
6255
- firstName: z$1.string().optional(),
6256
- lastName: z$1.string().optional(),
6257
- extraFields: z$1.record(z$1.string(), z$1.any()).optional()
7863
+ entryPoint: z.string().optional(),
7864
+ cert: z.string().optional(),
7865
+ entityId: z.string().optional(),
7866
+ mapping: z.object({
7867
+ /** @deprecated Removed in better-auth 1.7+; SAML subject is NameID. */
7868
+ id: z.string().optional(),
7869
+ email: z.string().optional(),
7870
+ emailVerified: z.string().optional(),
7871
+ name: z.string().optional(),
7872
+ firstName: z.string().optional(),
7873
+ lastName: z.string().optional(),
7874
+ extraFields: z.record(z.string(), z.any()).optional()
6258
7875
  }).optional()
6259
7876
  });
6260
- const oidcConfigSchema = z$1.object({
6261
- clientId: z$1.string(),
6262
- clientSecret: z$1.string().optional(),
6263
- discoveryUrl: z$1.string().optional(),
6264
- issuer: z$1.string().optional(),
6265
- discoveryEndpoint: z$1.string().optional(),
6266
- authorizationEndpoint: z$1.string().optional(),
6267
- tokenEndpoint: z$1.string().optional(),
6268
- jwksEndpoint: z$1.string().optional(),
6269
- userInfoEndpoint: z$1.string().optional(),
6270
- tokenEndpointAuthentication: z$1.enum(["client_secret_post", "client_secret_basic"]).optional(),
6271
- mapping: z$1.object({
6272
- id: z$1.string().optional(),
6273
- email: z$1.string().optional(),
6274
- emailVerified: z$1.string().optional(),
6275
- name: z$1.string().optional(),
6276
- image: z$1.string().optional(),
6277
- extraFields: z$1.record(z$1.string(), z$1.any()).optional()
7877
+ const oidcConfigSchema = z.object({
7878
+ clientId: z.string(),
7879
+ clientSecret: z.string().optional(),
7880
+ discoveryUrl: z.string().optional(),
7881
+ issuer: z.string().optional(),
7882
+ discoveryEndpoint: z.string().optional(),
7883
+ authorizationEndpoint: z.string().optional(),
7884
+ tokenEndpoint: z.string().optional(),
7885
+ jwksEndpoint: z.string().optional(),
7886
+ userInfoEndpoint: z.string().optional(),
7887
+ tokenEndpointAuthentication: z.enum(["client_secret_post", "client_secret_basic"]).optional(),
7888
+ mapping: z.object({
7889
+ /** @deprecated Removed in better-auth 1.7+; OIDC subject is `sub`. */
7890
+ id: z.string().optional(),
7891
+ email: z.string().optional(),
7892
+ emailVerified: z.string().optional(),
7893
+ name: z.string().optional(),
7894
+ image: z.string().optional(),
7895
+ extraFields: z.record(z.string(), z.any()).optional()
6278
7896
  }).optional()
6279
7897
  });
6280
7898
  async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
@@ -6294,20 +7912,24 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
6294
7912
  ctx.context.logger.warn("[Dash] SAML IdP metadata uses deprecated algorithms:", providerId, warnings);
6295
7913
  }
6296
7914
  }
7915
+ const m = samlConfig.mapping;
7916
+ const resolvedEntryPoint = samlConfig.entryPoint?.trim() || (idpMetadataXml ? extractEntryPointFromSAMLMetadata(idpMetadataXml) : void 0);
7917
+ const saml17OrNewer = isVersionAtLeast(ctx.context.version, "1.7.0");
7918
+ const ba17OrLater = isBetterAuth17OrLater(ctx.context.version);
7919
+ if (saml17OrNewer && !resolvedEntryPoint) throw ctx.error("BAD_REQUEST", { message: "SAML entry point URL is required; provide entryPoint or IdP metadata with SingleSignOnService Location" });
7920
+ if (ba17OrLater && !idpMetadataXml && !samlConfig.entityId?.trim()) throw ctx.error("BAD_REQUEST", { message: "IdP entity ID is required when IdP metadata XML is not provided" });
7921
+ const spIssuer = `${baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`;
6297
7922
  const idpMetadata = idpMetadataXml ? { metadata: idpMetadataXml } : {
7923
+ ...ba17OrLater ? { entityID: samlConfig.entityId } : {},
6298
7924
  ...samlConfig.entryPoint ? { singleSignOnService: [{
6299
7925
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
6300
7926
  Location: samlConfig.entryPoint
6301
7927
  }] } : {},
6302
7928
  ...samlConfig.cert ? { cert: samlConfig.cert } : {}
6303
7929
  };
6304
- const m = samlConfig.mapping;
6305
- const resolvedEntryPoint = samlConfig.entryPoint?.trim() || (idpMetadataXml ? extractEntryPointFromSAMLMetadata(idpMetadataXml) : void 0);
6306
- const saml17OrNewer = isVersionAtLeast(ctx.context.version, "1.7.0");
6307
- if (saml17OrNewer && !resolvedEntryPoint) throw ctx.error("BAD_REQUEST", { message: "SAML entry point URL is required; provide entryPoint or IdP metadata with SingleSignOnService Location" });
6308
7930
  return {
6309
7931
  config: {
6310
- issuer: samlConfig.entityId ?? `${baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`,
7932
+ issuer: ba17OrLater && !idpMetadataXml ? spIssuer : samlConfig.entityId ?? spIssuer,
6311
7933
  idpMetadata,
6312
7934
  ...saml17OrNewer ? {
6313
7935
  entryPoint: resolvedEntryPoint,
@@ -6319,7 +7941,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
6319
7941
  cert: samlConfig.cert ?? ""
6320
7942
  },
6321
7943
  ...m ? { mapping: {
6322
- id: m.id ?? "nameID",
7944
+ ...!ba17OrLater ? { id: m.id ?? "nameID" } : {},
6323
7945
  email: m.email ?? "email",
6324
7946
  name: m.name ?? "name",
6325
7947
  emailVerified: m.emailVerified,
@@ -6335,6 +7957,7 @@ async function resolveOIDCConfig(oidcConfig, _domain, ctx) {
6335
7957
  if (!oidcConfig.issuer || !oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint) throw ctx.error("BAD_REQUEST", { message: "OIDC discovery must be resolved before submitting; provide issuer, authorizationEndpoint, tokenEndpoint, and jwksEndpoint" });
6336
7958
  const om = oidcConfig.mapping;
6337
7959
  const discoveryEndpoint = oidcConfig.discoveryEndpoint ?? oidcConfig.discoveryUrl ?? oidcConfig.issuer;
7960
+ const ba17OrLater = isBetterAuth17OrLater(ctx.context.version);
6338
7961
  return {
6339
7962
  config: {
6340
7963
  clientId: oidcConfig.clientId,
@@ -6348,7 +7971,7 @@ async function resolveOIDCConfig(oidcConfig, _domain, ctx) {
6348
7971
  tokenEndpointAuthentication: oidcConfig.tokenEndpointAuthentication,
6349
7972
  pkce: true,
6350
7973
  ...om ? { mapping: {
6351
- id: om.id ?? "sub",
7974
+ ...!ba17OrLater ? { id: om.id ?? "sub" } : {},
6352
7975
  email: om.email ?? "email",
6353
7976
  name: om.name ?? "name",
6354
7977
  emailVerified: om.emailVerified,
@@ -6362,7 +7985,7 @@ async function resolveOIDCConfig(oidcConfig, _domain, ctx) {
6362
7985
  const listOrganizationSsoProviders = (options) => {
6363
7986
  return createAuthEndpoint("/dash/organization/:id/sso-providers", {
6364
7987
  method: "GET",
6365
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))]
7988
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))]
6366
7989
  }, async (ctx) => {
6367
7990
  if (!isOrganizationEnabled(ctx)) {
6368
7991
  ctx.context.logger.warn("[Dash] Organization plugin not enabled, returning empty SSO providers list");
@@ -6391,12 +8014,12 @@ const listOrganizationSsoProviders = (options) => {
6391
8014
  const createSsoProvider = (options) => {
6392
8015
  return createAuthEndpoint("/dash/organization/:id/sso-provider/create", {
6393
8016
  method: "POST",
6394
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
6395
- body: z$1.object({
6396
- providerId: z$1.string(),
6397
- domain: z$1.string(),
6398
- protocol: z$1.enum(["SAML", "OIDC"]),
6399
- userId: z$1.string(),
8017
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
8018
+ body: z.object({
8019
+ providerId: z.string(),
8020
+ domain: z.string(),
8021
+ protocol: z.enum(["SAML", "OIDC"]),
8022
+ userId: z.string(),
6400
8023
  samlConfig: samlConfigSchema.optional(),
6401
8024
  oidcConfig: oidcConfigSchema.optional()
6402
8025
  })
@@ -6415,7 +8038,7 @@ const createSsoProvider = (options) => {
6415
8038
  };
6416
8039
  if (protocol === "SAML" && samlConfig) {
6417
8040
  const samlResult = await resolveSAMLConfig(samlConfig, providerId, ctx.context.baseURL, ctx);
6418
- registerBody.issuer = samlResult.config.issuer ?? (samlConfig.entityId || `${ctx.context.baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`);
8041
+ registerBody.issuer = samlResult.config.issuer;
6419
8042
  registerBody.samlConfig = samlResult.config;
6420
8043
  }
6421
8044
  if (protocol === "OIDC" && oidcConfig) {
@@ -6434,15 +8057,17 @@ const createSsoProvider = (options) => {
6434
8057
  });
6435
8058
  let verificationToken = null;
6436
8059
  if ("domainVerificationToken" in result && typeof result.domainVerificationToken === "string") verificationToken = result.domainVerificationToken;
8060
+ const tokenPrefix = ssoPlugin.options?.domainVerification?.tokenPrefix || "better-auth-token";
8061
+ const resolvedProviderId = result.providerId || providerId;
6437
8062
  return {
6438
8063
  success: true,
6439
8064
  provider: {
6440
8065
  id: result.providerId,
6441
- providerId: result.providerId || providerId,
8066
+ providerId: resolvedProviderId,
6442
8067
  domain: result.domain || domain
6443
8068
  },
6444
8069
  domainVerification: {
6445
- txtRecordName: `better-auth-token-${providerId}`,
8070
+ txtRecordName: `_${tokenPrefix}-${resolvedProviderId}`,
6446
8071
  verificationToken
6447
8072
  }
6448
8073
  };
@@ -6456,11 +8081,11 @@ const createSsoProvider = (options) => {
6456
8081
  const updateSsoProvider = (options) => {
6457
8082
  return createAuthEndpoint("/dash/organization/:id/sso-provider/update", {
6458
8083
  method: "POST",
6459
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
6460
- body: z$1.object({
6461
- providerId: z$1.string(),
6462
- domain: z$1.string(),
6463
- protocol: z$1.enum(["SAML", "OIDC"]),
8084
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
8085
+ body: z.object({
8086
+ providerId: z.string(),
8087
+ domain: z.string(),
8088
+ protocol: z.enum(["SAML", "OIDC"]),
6464
8089
  samlConfig: samlConfigSchema.optional(),
6465
8090
  oidcConfig: oidcConfigSchema.optional()
6466
8091
  })
@@ -6486,7 +8111,7 @@ const updateSsoProvider = (options) => {
6486
8111
  if (domain && domain !== existingProvider.domain) updateBody.domain = domain;
6487
8112
  if (protocol === "SAML" && samlConfig) {
6488
8113
  const samlResult = await resolveSAMLConfig(samlConfig, providerId, ctx.context.baseURL, ctx);
6489
- updateBody.issuer = samlResult.config.issuer ?? (samlConfig.entityId || `${ctx.context.baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`);
8114
+ updateBody.issuer = samlResult.config.issuer;
6490
8115
  updateBody.samlConfig = samlResult.config;
6491
8116
  }
6492
8117
  if (protocol === "OIDC" && oidcConfig) {
@@ -6539,8 +8164,8 @@ const updateSsoProvider = (options) => {
6539
8164
  const requestSsoVerificationToken = (options) => {
6540
8165
  return createAuthEndpoint("/dash/organization/:id/sso-provider/request-verification-token", {
6541
8166
  method: "POST",
6542
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
6543
- body: z$1.object({ providerId: z$1.string() })
8167
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
8168
+ body: z.object({ providerId: z.string() })
6544
8169
  }, async (ctx) => {
6545
8170
  requireOrganizationPlugin(ctx);
6546
8171
  requireOrganizationAccess(ctx);
@@ -6561,7 +8186,7 @@ const requestSsoVerificationToken = (options) => {
6561
8186
  }]
6562
8187
  });
6563
8188
  if (!provider) throw ctx.error("NOT_FOUND", { message: "SSO provider not found" });
6564
- const txtRecordName = `${ssoPlugin.options?.domainVerification?.tokenPrefix || "better-auth-token"}-${provider.providerId}`;
8189
+ const txtRecordName = `_${ssoPlugin.options?.domainVerification?.tokenPrefix || "better-auth-token"}-${provider.providerId}`;
6565
8190
  try {
6566
8191
  const result = await endpoints.requestDomainVerification({
6567
8192
  body: { providerId },
@@ -6587,8 +8212,8 @@ const requestSsoVerificationToken = (options) => {
6587
8212
  const verifySsoProviderDomain = (options) => {
6588
8213
  return createAuthEndpoint("/dash/organization/:id/sso-provider/verify-domain", {
6589
8214
  method: "POST",
6590
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
6591
- body: z$1.object({ providerId: z$1.string() })
8215
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
8216
+ body: z.object({ providerId: z.string() })
6592
8217
  }, async (ctx) => {
6593
8218
  requireOrganizationPlugin(ctx);
6594
8219
  requireOrganizationAccess(ctx);
@@ -6623,14 +8248,8 @@ const verifySsoProviderDomain = (options) => {
6623
8248
  };
6624
8249
  } catch (e) {
6625
8250
  if (e instanceof APIError$1) {
6626
- if (e.status === "CONFLICT") return {
6627
- verified: true,
6628
- message: "Domain has already been verified"
6629
- };
6630
- if (e.status === "BAD_GATEWAY") return {
6631
- verified: false,
6632
- message: "Unable to verify domain ownership. The TXT record was not found. It may take up to 48 hours for DNS changes to propagate."
6633
- };
8251
+ const mapped = mapSsoVerifyDomainApiError(e);
8252
+ if (mapped) return mapped;
6634
8253
  throw e;
6635
8254
  }
6636
8255
  throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to verify domain" });
@@ -6640,8 +8259,8 @@ const verifySsoProviderDomain = (options) => {
6640
8259
  const deleteSsoProvider = (options) => {
6641
8260
  return createAuthEndpoint("/dash/organization/:id/sso-provider/delete", {
6642
8261
  method: "POST",
6643
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
6644
- body: z$1.object({ providerId: z$1.string() })
8262
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
8263
+ body: z.object({ providerId: z.string() })
6645
8264
  }, async (ctx) => {
6646
8265
  requireOrganizationPlugin(ctx);
6647
8266
  requireOrganizationAccess(ctx);
@@ -6683,10 +8302,10 @@ const deleteSsoProvider = (options) => {
6683
8302
  const markSsoProviderDomainVerified = (options) => {
6684
8303
  return createAuthEndpoint("/dash/organization/:id/sso-provider/mark-domain-verified", {
6685
8304
  method: "POST",
6686
- use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
6687
- body: z$1.object({
6688
- providerId: z$1.string(),
6689
- verified: z$1.literal(false).describe("Clears domain verification. Domains can only be marked verified via DNS (verify-domain).")
8305
+ use: [jwtMiddleware(options, z.object({ organizationId: z.string() }))],
8306
+ body: z.object({
8307
+ providerId: z.string(),
8308
+ verified: z.literal(false).describe("Clears domain verification. Domains can only be marked verified via DNS (verify-domain).")
6690
8309
  })
6691
8310
  }, async (ctx) => {
6692
8311
  requireOrganizationPlugin(ctx);
@@ -6782,7 +8401,7 @@ function buildTotpUri(params) {
6782
8401
  //#region src/routes/two-factor/index.ts
6783
8402
  const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor", {
6784
8403
  method: "POST",
6785
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
8404
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
6786
8405
  }, async (ctx) => {
6787
8406
  const { userId } = ctx.context.payload;
6788
8407
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
@@ -6843,7 +8462,7 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
6843
8462
  });
6844
8463
  const completeTwoFactorSetup = (options) => createAuthEndpoint("/dash/complete-two-factor-setup", {
6845
8464
  method: "POST",
6846
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
8465
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
6847
8466
  }, async (ctx) => {
6848
8467
  const { userId } = ctx.context.payload;
6849
8468
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
@@ -6871,7 +8490,7 @@ const completeTwoFactorSetup = (options) => createAuthEndpoint("/dash/complete-t
6871
8490
  const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-factor-totp-uri", {
6872
8491
  method: "POST",
6873
8492
  metadata: { scope: "http" },
6874
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
8493
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
6875
8494
  }, async (ctx) => {
6876
8495
  const { userId } = ctx.context.payload;
6877
8496
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
@@ -6909,13 +8528,13 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
6909
8528
  });
6910
8529
  const viewBackupCodes = (options) => createAuthEndpoint("/dash/view-backup-codes", {
6911
8530
  method: "POST",
6912
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
8531
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
6913
8532
  }, async () => {
6914
8533
  throw new APIError("FORBIDDEN", { message: "Backup codes cannot be viewed after initial setup. Generate new codes instead." });
6915
8534
  });
6916
8535
  const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-factor", {
6917
8536
  method: "POST",
6918
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
8537
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
6919
8538
  }, async (ctx) => {
6920
8539
  const { userId } = ctx.context.payload;
6921
8540
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
@@ -6936,7 +8555,7 @@ const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-fact
6936
8555
  });
6937
8556
  const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-backup-codes", {
6938
8557
  method: "POST",
6939
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
8558
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
6940
8559
  }, async (ctx) => {
6941
8560
  const { userId } = ctx.context.payload;
6942
8561
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
@@ -7001,13 +8620,13 @@ async function resolveTwoFactorStatus(context, userId, user) {
7001
8620
  }
7002
8621
  //#endregion
7003
8622
  //#region src/routes/users/schemas.ts
7004
- const DENIED_USER_WRITE_KEYS = new Set([
8623
+ const DENIED_USER_WRITE_KEYS = /* @__PURE__ */ new Set([
7005
8624
  "id",
7006
8625
  "createdAt",
7007
8626
  "updatedAt"
7008
8627
  ]);
7009
8628
  /** Fields that must be changed through dedicated permission-checked endpoints. */
7010
- const DENIED_SENSITIVE_USER_KEYS = new Set([
8629
+ const DENIED_SENSITIVE_USER_KEYS = /* @__PURE__ */ new Set([
7011
8630
  "banned",
7012
8631
  "banReason",
7013
8632
  "banExpires",
@@ -7016,7 +8635,7 @@ const DENIED_SENSITIVE_USER_KEYS = new Set([
7016
8635
  "twoFactorEnabled",
7017
8636
  "twoFactorSecret"
7018
8637
  ]);
7019
- const CREATE_REQUEST_ONLY_KEYS = new Set([
8638
+ const CREATE_REQUEST_ONLY_KEYS = /* @__PURE__ */ new Set([
7020
8639
  "password",
7021
8640
  "generatePassword",
7022
8641
  "sendVerificationEmail",
@@ -7072,7 +8691,7 @@ function getWritableUserFieldNames(options, mode) {
7072
8691
  */
7073
8692
  function pickWritableUserFields(body, options, mode) {
7074
8693
  const allowed = getWritableUserFieldNames(options, mode);
7075
- const skip = new Set([
8694
+ const skip = /* @__PURE__ */ new Set([
7076
8695
  ...DENIED_USER_WRITE_KEYS,
7077
8696
  ...DENIED_SENSITIVE_USER_KEYS,
7078
8697
  ...mode === "create" ? CREATE_REQUEST_ONLY_KEYS : []
@@ -7084,32 +8703,32 @@ function pickWritableUserFields(body, options, mode) {
7084
8703
  }
7085
8704
  return result;
7086
8705
  }
7087
- const BaseCreateUserCoreBodySchema = z$1.object({
7088
- name: z$1.string(),
7089
- email: z$1.email(),
7090
- image: z$1.string().optional(),
7091
- password: z$1.string().optional(),
7092
- generatePassword: z$1.boolean().optional(),
7093
- emailVerified: z$1.boolean().optional(),
7094
- sendVerificationEmail: z$1.boolean().optional(),
7095
- sendOrganizationInvite: z$1.boolean().optional(),
7096
- organizationRole: z$1.string().optional(),
7097
- organizationId: z$1.string().optional()
8706
+ const BaseCreateUserCoreBodySchema = z.object({
8707
+ name: z.string(),
8708
+ email: z.email(),
8709
+ image: z.string().optional(),
8710
+ password: z.string().optional(),
8711
+ generatePassword: z.boolean().optional(),
8712
+ emailVerified: z.boolean().optional(),
8713
+ sendVerificationEmail: z.boolean().optional(),
8714
+ sendOrganizationInvite: z.boolean().optional(),
8715
+ organizationRole: z.string().optional(),
8716
+ organizationId: z.string().optional()
7098
8717
  });
7099
- const BaseUpdateUserCoreBodySchema = z$1.object({
7100
- name: z$1.string().nullable().optional(),
7101
- email: z$1.email().optional(),
7102
- image: z$1.string().nullable().optional(),
7103
- emailVerified: z$1.boolean().optional()
8718
+ const BaseUpdateUserCoreBodySchema = z.object({
8719
+ name: z.string().nullable().optional(),
8720
+ email: z.email().optional(),
8721
+ image: z.string().nullable().optional(),
8722
+ emailVerified: z.boolean().optional()
7104
8723
  });
7105
- const CreateUserBodySchema = BaseCreateUserCoreBodySchema.catchall(z$1.unknown());
7106
- const UpdateUserBodySchema = BaseUpdateUserCoreBodySchema.catchall(z$1.unknown());
8724
+ const CreateUserBodySchema = BaseCreateUserCoreBodySchema.catchall(z.unknown());
8725
+ const UpdateUserBodySchema = BaseUpdateUserCoreBodySchema.catchall(z.unknown());
7107
8726
  function createSchemaForDBField(field) {
7108
8727
  switch (field.type) {
7109
- case "number": return field.required ? z$1.coerce.number() : z$1.coerce.number().optional();
7110
- case "boolean": return field.required ? z$1.coerce.boolean() : z$1.coerce.boolean().optional();
7111
- case "date": return field.required ? z$1.union([z$1.string().min(1), z$1.coerce.date()]) : z$1.union([z$1.string(), z$1.coerce.date()]).optional();
7112
- default: return field.required ? z$1.string().min(1) : z$1.string().optional();
8728
+ case "number": return field.required ? z.coerce.number() : z.coerce.number().optional();
8729
+ case "boolean": return field.required ? z.coerce.boolean() : z.coerce.boolean().optional();
8730
+ case "date": return field.required ? z.union([z.string().min(1), z.coerce.date()]) : z.union([z.string(), z.coerce.date()]).optional();
8731
+ default: return field.required ? z.string().min(1) : z.string().optional();
7113
8732
  }
7114
8733
  }
7115
8734
  /**
@@ -7117,38 +8736,38 @@ function createSchemaForDBField(field) {
7117
8736
  */
7118
8737
  function validateWritableCreateUserFields(data, options) {
7119
8738
  const shape = {
7120
- name: z$1.string().min(1),
7121
- email: z$1.email(),
7122
- image: z$1.string().optional(),
7123
- emailVerified: z$1.boolean().optional()
8739
+ name: z.string().min(1),
8740
+ email: z.email(),
8741
+ image: z.string().optional(),
8742
+ emailVerified: z.boolean().optional()
7124
8743
  };
7125
8744
  for (const [name, field] of Object.entries(getUserInputFields(options))) {
7126
8745
  if (!isWritableUserInputField(field)) continue;
7127
8746
  shape[name] = createSchemaForDBField(field);
7128
8747
  }
7129
- const result = z$1.object(shape).strict().safeParse(data);
8748
+ const result = z.object(shape).strict().safeParse(data);
7130
8749
  if (!result.success) throw result.error;
7131
8750
  }
7132
8751
  /**
7133
8752
  * Ensures at least one field is present on update and validates additional field types.
7134
8753
  */
7135
8754
  function validateWritableUserUpdateFields(data, options) {
7136
- if (Object.keys(data).length === 0) throw new z$1.ZodError([{
8755
+ if (Object.keys(data).length === 0) throw new z.ZodError([{
7137
8756
  code: "custom",
7138
8757
  message: "No valid fields to update",
7139
8758
  path: []
7140
8759
  }]);
7141
8760
  const shape = {
7142
- name: z$1.string().nullable().optional(),
7143
- email: z$1.email().optional(),
7144
- image: z$1.string().nullable().optional(),
7145
- emailVerified: z$1.boolean().optional()
8761
+ name: z.string().nullable().optional(),
8762
+ email: z.email().optional(),
8763
+ image: z.string().nullable().optional(),
8764
+ emailVerified: z.boolean().optional()
7146
8765
  };
7147
8766
  for (const [name, field] of Object.entries(getUserInputFields(options))) {
7148
8767
  if (!isWritableUserInputField(field)) continue;
7149
8768
  shape[name] = createSchemaForDBField(field);
7150
8769
  }
7151
- const result = z$1.object(shape).partial().strict().safeParse(data);
8770
+ const result = z.object(shape).partial().strict().safeParse(data);
7152
8771
  if (!result.success) throw result.error;
7153
8772
  }
7154
8773
  //#endregion
@@ -7171,13 +8790,13 @@ function clampUserListOffset(value, fallback = 0) {
7171
8790
  if (!Number.isFinite(n)) return fallback;
7172
8791
  return Math.max(0, Math.floor(n));
7173
8792
  }
7174
- const getUsersQuerySchema = z$1.object({
7175
- limit: z$1.number().or(z$1.string().transform(Number)).optional(),
7176
- offset: z$1.number().or(z$1.string().transform(Number)).optional(),
7177
- sortBy: z$1.string().optional(),
7178
- sortOrder: z$1.enum(["asc", "desc"]).optional(),
7179
- where: z$1.string().transform(parseWhereClause).optional(),
7180
- countWhere: z$1.string().transform(parseWhereClause).optional()
8793
+ const getUsersQuerySchema = z.object({
8794
+ limit: z.number().or(z.string().transform(Number)).optional(),
8795
+ offset: z.number().or(z.string().transform(Number)).optional(),
8796
+ sortBy: z.string().optional(),
8797
+ sortOrder: z.enum(["asc", "desc"]).optional(),
8798
+ where: z.string().transform(parseWhereClause).optional(),
8799
+ countWhere: z.string().transform(parseWhereClause).optional()
7181
8800
  }).optional();
7182
8801
  const getUsers = (options) => {
7183
8802
  return createAuthEndpoint("/dash/list-users", {
@@ -7214,7 +8833,7 @@ const getUsers = (options) => {
7214
8833
  model: "user",
7215
8834
  where: [{
7216
8835
  field: "lastActiveAt",
7217
- value: /* @__PURE__ */ new Date(Date.now() - 1e3 * 60 * 2),
8836
+ value: /* @__PURE__ */ new Date(Date.now() - 12e4),
7218
8837
  operator: "gte"
7219
8838
  }]
7220
8839
  }).catch((e) => {
@@ -7272,7 +8891,7 @@ const exportUsers = (options) => {
7272
8891
  const deleteUser = (options) => {
7273
8892
  return createAuthEndpoint("/dash/delete-user", {
7274
8893
  method: "POST",
7275
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
8894
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
7276
8895
  }, async (ctx) => {
7277
8896
  try {
7278
8897
  await ctx.context.adapter.delete({
@@ -7291,7 +8910,7 @@ const deleteUser = (options) => {
7291
8910
  const deleteManyUsers = (options) => {
7292
8911
  return createAuthEndpoint("/dash/delete-many-users", {
7293
8912
  method: "POST",
7294
- use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))]
8913
+ use: [jwtMiddleware(options, z.object({ userIds: z.string().array() }))]
7295
8914
  }, async (ctx) => {
7296
8915
  const { userIds } = ctx.context.payload;
7297
8916
  const deletedUserIds = /* @__PURE__ */ new Set();
@@ -7327,11 +8946,11 @@ const deleteManyUsers = (options) => {
7327
8946
  const impersonateUser = (options) => {
7328
8947
  return createAuthEndpoint("/dash/impersonate-user", {
7329
8948
  method: "GET",
7330
- query: z$1.object({ impersonation_token: z$1.string() }),
7331
- use: [jwtMiddleware(options, z$1.object({
7332
- userId: z$1.string(),
8949
+ query: z.object({ impersonation_token: z.string() }),
8950
+ use: [jwtMiddleware(options, z.object({
8951
+ userId: z.string(),
7333
8952
  redirectUrl: safeUrlSchema,
7334
- impersonatedBy: z$1.string().optional()
8953
+ impersonatedBy: z.string().optional()
7335
8954
  }), async (ctx) => {
7336
8955
  return ctx.query.impersonation_token;
7337
8956
  })]
@@ -7341,7 +8960,7 @@ const impersonateUser = (options) => {
7341
8960
  const trustedRedirectUrl = parseTrustedAuthRedirectUrl(ctx, redirectUrl);
7342
8961
  if (!trustedRedirectUrl) throw ctx.error("BAD_REQUEST", { message: "Invalid redirect URL" });
7343
8962
  const session = await ctx.context.internalAdapter.createSession(userId, true, {
7344
- expiresAt: new Date(Date.now() + 1e3 * 60 * 10),
8963
+ expiresAt: new Date(Date.now() + 6e5),
7345
8964
  impersonatedBy: impersonatedBy || void 0
7346
8965
  });
7347
8966
  const user = await ctx.context.internalAdapter.findUserById(userId);
@@ -7356,9 +8975,9 @@ const impersonateUser = (options) => {
7356
8975
  const createUser = (options) => {
7357
8976
  return createAuthEndpoint("/dash/create-user", {
7358
8977
  method: "POST",
7359
- use: [jwtMiddleware(options, z$1.object({
7360
- organizationId: z$1.string().optional(),
7361
- organizationRole: z$1.string().optional()
8978
+ use: [jwtMiddleware(options, z.object({
8979
+ organizationId: z.string().optional(),
8980
+ organizationRole: z.string().optional()
7362
8981
  }))],
7363
8982
  body: CreateUserBodySchema
7364
8983
  }, async (ctx) => {
@@ -7367,7 +8986,7 @@ const createUser = (options) => {
7367
8986
  try {
7368
8987
  validateWritableCreateUserFields(userData, ctx.context.options);
7369
8988
  } catch (error) {
7370
- if (error instanceof z$1.ZodError) throw new APIError("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid user data" });
8989
+ if (error instanceof z.ZodError) throw new APIError("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid user data" });
7371
8990
  throw error;
7372
8991
  }
7373
8992
  const email = normalizeEmail(userData.email, ctx.context);
@@ -7402,10 +9021,8 @@ const createUser = (options) => {
7402
9021
  };
7403
9022
  const adapter = ctx.context.internalAdapter;
7404
9023
  const user = isVersionAtLeast(ctx.context.version, "1.7.0") ? await adapter.createUser(userPayload, { method: "admin" }) : await adapter.createUser(userPayload);
7405
- if (password) await ctx.context.internalAdapter.createAccount({
9024
+ if (password) await createCredentialAccountCompat(ctx.context.internalAdapter, {
7406
9025
  userId: user.id,
7407
- providerId: "credential",
7408
- accountId: user.id,
7409
9026
  password: await ctx.context.password.hash(password)
7410
9027
  });
7411
9028
  if (body.sendVerificationEmail && !emailVerified) {
@@ -7458,8 +9075,8 @@ const createUser = (options) => {
7458
9075
  const setPassword = (options) => {
7459
9076
  return createAuthEndpoint("/dash/set-password", {
7460
9077
  method: "POST",
7461
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
7462
- body: z$1.object({ password: z$1.string().min(8) })
9078
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))],
9079
+ body: z.object({ password: z.string().min(8) })
7463
9080
  }, async (ctx) => {
7464
9081
  const { userId } = ctx.context.payload;
7465
9082
  const { password } = ctx.body;
@@ -7470,10 +9087,8 @@ const setPassword = (options) => {
7470
9087
  password: hashed,
7471
9088
  updatedAt: /* @__PURE__ */ new Date()
7472
9089
  });
7473
- else await ctx.context.internalAdapter.createAccount({
9090
+ else await createCredentialAccountCompat(ctx.context.internalAdapter, {
7474
9091
  userId,
7475
- providerId: "credential",
7476
- accountId: userId,
7477
9092
  password: hashed
7478
9093
  });
7479
9094
  return { success: true };
@@ -7482,10 +9097,10 @@ const setPassword = (options) => {
7482
9097
  const unlinkAccount = (options) => {
7483
9098
  return createAuthEndpoint("/dash/unlink-account", {
7484
9099
  method: "POST",
7485
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
7486
- body: z$1.object({
7487
- providerId: z$1.string(),
7488
- accountId: z$1.string().optional()
9100
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))],
9101
+ body: z.object({
9102
+ providerId: z.string(),
9103
+ accountId: z.string()
7489
9104
  })
7490
9105
  }, async (ctx) => {
7491
9106
  const { userId } = ctx.context.payload;
@@ -7494,22 +9109,25 @@ const unlinkAccount = (options) => {
7494
9109
  const accounts = await ctx.context.internalAdapter.findAccounts(userId);
7495
9110
  const allowUnlinkingAll = ctx.context.options.account?.accountLinking?.allowUnlinkingAll ?? false;
7496
9111
  if (accounts.length === 1 && !allowUnlinkingAll) throw new APIError("BAD_REQUEST", { message: "Cannot unlink the last account. This would lock the user out." });
7497
- const accountToUnlink = accounts.find((account) => accountId ? account.accountId === accountId && account.providerId === providerId : account.providerId === providerId);
9112
+ const accountToUnlink = resolveAccountForUnlink(accounts, {
9113
+ providerId,
9114
+ accountId
9115
+ });
7498
9116
  if (!accountToUnlink) throw new APIError("NOT_FOUND", { message: "Account not found" });
7499
9117
  await ctx.context.internalAdapter.deleteAccount(accountToUnlink.id);
7500
9118
  return { success: true };
7501
9119
  });
7502
9120
  };
7503
- const getUserDetailsJwtSchema = z$1.object({
7504
- userId: z$1.string(),
7505
- sessionOnly: z$1.boolean().optional(),
7506
- accountOnly: z$1.boolean().optional()
9121
+ const getUserDetailsJwtSchema = z.object({
9122
+ userId: z.string(),
9123
+ sessionOnly: z.boolean().optional(),
9124
+ accountOnly: z.boolean().optional()
7507
9125
  });
7508
9126
  const getUserDetails = (options) => {
7509
9127
  return createAuthEndpoint("/dash/user", {
7510
9128
  method: "GET",
7511
9129
  use: [jwtMiddleware(options, getUserDetailsJwtSchema)],
7512
- query: z$1.object({ minimal: z$1.boolean().or(z$1.string().transform((val) => val === "true")).optional() }).optional()
9130
+ query: z.object({ minimal: z.boolean().or(z.string().transform((val) => val === "true")).optional() }).optional()
7513
9131
  }, async (ctx) => {
7514
9132
  const { userId, sessionOnly, accountOnly } = ctx.context.payload;
7515
9133
  const minimal = !!ctx.query?.minimal;
@@ -7606,7 +9224,7 @@ const getUserDetails = (options) => {
7606
9224
  const getUserOrganizations = (options) => {
7607
9225
  return createAuthEndpoint("/dash/user-organizations", {
7608
9226
  method: "GET",
7609
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
9227
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
7610
9228
  }, async (ctx) => {
7611
9229
  const { userId } = ctx.context.payload;
7612
9230
  if (!isOrganizationEnabled(ctx)) {
@@ -7651,7 +9269,7 @@ const getUserOrganizations = (options) => {
7651
9269
  };
7652
9270
  const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
7653
9271
  method: "POST",
7654
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
9272
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))],
7655
9273
  body: UpdateUserBodySchema
7656
9274
  }, async (ctx) => {
7657
9275
  const userId = ctx.context.payload?.userId;
@@ -7660,7 +9278,7 @@ const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
7660
9278
  try {
7661
9279
  validateWritableUserUpdateFields(updateData, ctx.context.options);
7662
9280
  } catch (error) {
7663
- if (error instanceof z$1.ZodError) throw new APIError("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid user data" });
9281
+ if (error instanceof z.ZodError) throw new APIError("BAD_REQUEST", { message: error.issues[0]?.message ?? "Invalid user data" });
7664
9282
  throw error;
7665
9283
  }
7666
9284
  const user = await ctx.context.internalAdapter.updateUser(userId, {
@@ -7746,12 +9364,12 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
7746
9364
  use: [jwtMiddleware(options)]
7747
9365
  }, async (ctx) => {
7748
9366
  const now = /* @__PURE__ */ new Date();
7749
- const oneDayAgo = /* @__PURE__ */ new Date(now.getTime() - 1440 * 60 * 1e3);
7750
- const twoDaysAgo = /* @__PURE__ */ new Date(now.getTime() - 2880 * 60 * 1e3);
7751
- const oneWeekAgo = /* @__PURE__ */ new Date(now.getTime() - 10080 * 60 * 1e3);
7752
- const twoWeeksAgo = /* @__PURE__ */ new Date(now.getTime() - 336 * 60 * 60 * 1e3);
7753
- const oneMonthAgo = /* @__PURE__ */ new Date(now.getTime() - 720 * 60 * 60 * 1e3);
7754
- const twoMonthsAgo = /* @__PURE__ */ new Date(now.getTime() - 1440 * 60 * 60 * 1e3);
9367
+ const oneDayAgo = /* @__PURE__ */ new Date(now.getTime() - 864e5);
9368
+ const twoDaysAgo = /* @__PURE__ */ new Date(now.getTime() - 1728e5);
9369
+ const oneWeekAgo = /* @__PURE__ */ new Date(now.getTime() - 6048e5);
9370
+ const twoWeeksAgo = /* @__PURE__ */ new Date(now.getTime() - 12096e5);
9371
+ const oneMonthAgo = /* @__PURE__ */ new Date(now.getTime() - 2592e6);
9372
+ const twoMonthsAgo = /* @__PURE__ */ new Date(now.getTime() - 5184e6);
7755
9373
  const activityTrackingEnabled = !!options.activityTracking?.enabled;
7756
9374
  const storeInSecondaryStorageOnly = isSessionInSecondaryStorageOnly(ctx.context);
7757
9375
  const [rDailySignups, rPrevDaySignups, rWeeklySignups, rPrevWeekSignups, rMonthlySignups, rPrevMonthSignups, rTotalUsers, rActiveDaily, rActivePrevDay, rActiveWeekly, rActivePrevWeek, rActiveMonthly, rActivePrevMonth] = await withConcurrency([
@@ -7886,7 +9504,7 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
7886
9504
  const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data", {
7887
9505
  method: "GET",
7888
9506
  use: [jwtMiddleware(options)],
7889
- query: z$1.object({ period: z$1.enum([
9507
+ query: z.object({ period: z.enum([
7890
9508
  "daily",
7891
9509
  "weekly",
7892
9510
  "monthly"
@@ -7897,7 +9515,7 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
7897
9515
  const activityTrackingEnabled = !!options.activityTracking?.enabled;
7898
9516
  const storeInSecondaryStorageOnly = isSessionInSecondaryStorageOnly(ctx.context);
7899
9517
  const intervals = period === "daily" ? 7 : period === "weekly" ? 8 : 6;
7900
- const msPerInterval = period === "daily" ? 1440 * 60 * 1e3 : period === "weekly" ? 10080 * 60 * 1e3 : 720 * 60 * 60 * 1e3;
9518
+ const msPerInterval = period === "daily" ? 864e5 : period === "weekly" ? 6048e5 : 2592e6;
7901
9519
  const intervalData = [];
7902
9520
  for (let i = intervals - 1; i >= 0; i--) {
7903
9521
  const endDate = new Date(now.getTime() - i * msPerInterval);
@@ -7958,7 +9576,7 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
7958
9576
  const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retention-data", {
7959
9577
  method: "GET",
7960
9578
  use: [jwtMiddleware(options)],
7961
- query: z$1.object({ period: z$1.enum([
9579
+ query: z.object({ period: z.enum([
7962
9580
  "daily",
7963
9581
  "weekly",
7964
9582
  "monthly"
@@ -8130,11 +9748,11 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
8130
9748
  });
8131
9749
  const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
8132
9750
  method: "POST",
8133
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
8134
- body: z$1.object({
8135
- banReason: z$1.string().optional(),
8136
- banExpires: z$1.number().optional(),
8137
- deleteAllSessions: z$1.boolean().optional().default(true)
9751
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))],
9752
+ body: z.object({
9753
+ banReason: z.string().optional(),
9754
+ banExpires: z.number().optional(),
9755
+ deleteAllSessions: z.boolean().optional().default(true)
8138
9756
  })
8139
9757
  }, async (ctx) => {
8140
9758
  const { userId } = ctx.context.payload;
@@ -8152,11 +9770,11 @@ const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
8152
9770
  const banManyUsers = (options) => {
8153
9771
  return createAuthEndpoint("/dash/ban-many-users", {
8154
9772
  method: "POST",
8155
- use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))],
8156
- body: z$1.object({
8157
- banReason: z$1.string().optional(),
8158
- banExpires: z$1.number().optional(),
8159
- deleteAllSessions: z$1.boolean().optional().default(true)
9773
+ use: [jwtMiddleware(options, z.object({ userIds: z.string().array() }))],
9774
+ body: z.object({
9775
+ banReason: z.string().optional(),
9776
+ banExpires: z.number().optional(),
9777
+ deleteAllSessions: z.boolean().optional().default(true)
8160
9778
  })
8161
9779
  }, async (ctx) => {
8162
9780
  const { userIds } = ctx.context.payload;
@@ -8206,7 +9824,7 @@ const banManyUsers = (options) => {
8206
9824
  };
8207
9825
  const unbanUser = (options) => createAuthEndpoint("/dash/unban-user", {
8208
9826
  method: "POST",
8209
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
9827
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))]
8210
9828
  }, async (ctx) => {
8211
9829
  const { userId } = ctx.context.payload;
8212
9830
  if (!await ctx.context.internalAdapter.findUserById(userId)) throw new APIError("NOT_FOUND", { message: "User not found" });
@@ -8220,8 +9838,8 @@ const unbanUser = (options) => createAuthEndpoint("/dash/unban-user", {
8220
9838
  });
8221
9839
  const sendVerificationEmail = (options) => createAuthEndpoint("/dash/send-verification-email", {
8222
9840
  method: "POST",
8223
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
8224
- body: z$1.object({ callbackUrl: safeUrlSchema })
9841
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))],
9842
+ body: z.object({ callbackUrl: safeUrlSchema })
8225
9843
  }, async (ctx) => {
8226
9844
  const { userId } = ctx.context.payload;
8227
9845
  const callbackUrl = requireTrustedAuthCallbackUrl(ctx, ctx.body.callbackUrl);
@@ -8230,20 +9848,21 @@ const sendVerificationEmail = (options) => createAuthEndpoint("/dash/send-verifi
8230
9848
  if (user.emailVerified) throw ctx.error("BAD_REQUEST", { message: "Email is already verified" });
8231
9849
  if (!user.email) throw ctx.error("BAD_REQUEST", { message: "User has no associated email address" });
8232
9850
  if (!ctx.context.options.emailVerification?.sendVerificationEmail) throw ctx.error("BAD_REQUEST", { message: "Email verification is not enabled" });
8233
- await sendVerificationEmailFn({
9851
+ const modifiedCtx = {
8234
9852
  ...ctx,
8235
9853
  body: {
8236
9854
  ...ctx.body,
8237
9855
  callbackURL: callbackUrl
8238
9856
  }
8239
- }, user);
9857
+ };
9858
+ await sendVerificationEmailFn(modifiedCtx, user);
8240
9859
  return { success: true };
8241
9860
  });
8242
9861
  const sendManyVerificationEmails = (options) => {
8243
9862
  return createAuthEndpoint("/dash/send-many-verification-emails", {
8244
9863
  method: "POST",
8245
- use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))],
8246
- body: z$1.object({ callbackUrl: safeUrlSchema })
9864
+ use: [jwtMiddleware(options, z.object({ userIds: z.string().array() }))],
9865
+ body: z.object({ callbackUrl: safeUrlSchema })
8247
9866
  }, async (ctx) => {
8248
9867
  if (!ctx.context.options.emailVerification?.sendVerificationEmail) throw ctx.error("BAD_REQUEST", { message: "Email verification is not enabled" });
8249
9868
  const { userIds } = ctx.context.payload;
@@ -8287,9 +9906,10 @@ const sendManyVerificationEmails = (options) => {
8287
9906
  success: true,
8288
9907
  id: user.id
8289
9908
  };
8290
- }))) if (result.status === "fulfilled") if (result.value.success) sentEmailUserIds.add(result.value.id);
8291
- else skippedEmailUserIds.add(result.value.id);
8292
- else for (const { id } of users) skippedEmailUserIds.add(id);
9909
+ }))) if (result.status === "fulfilled") {
9910
+ if (result.value.success) sentEmailUserIds.add(result.value.id);
9911
+ else skippedEmailUserIds.add(result.value.id);
9912
+ } else for (const { id } of users) skippedEmailUserIds.add(id);
8293
9913
  }, { concurrency: 2 });
8294
9914
  const end = performance.now();
8295
9915
  console.log(`Time taken to send verification emails to ${sentEmailUserIds.size} users: ${Math.round((end - start) / 1e3)}s`, skippedEmailUserIds.size > 0 ? `Skipped: ${skippedEmailUserIds.size}` : "");
@@ -8302,8 +9922,8 @@ const sendManyVerificationEmails = (options) => {
8302
9922
  };
8303
9923
  const sendResetPasswordEmail = (options) => createAuthEndpoint("/dash/send-reset-password-email", {
8304
9924
  method: "POST",
8305
- use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
8306
- body: z$1.object({ callbackUrl: safeUrlSchema })
9925
+ use: [jwtMiddleware(options, z.object({ userId: z.string() }))],
9926
+ body: z.object({ callbackUrl: safeUrlSchema })
8307
9927
  }, async (ctx) => {
8308
9928
  const { userId } = ctx.context.payload;
8309
9929
  const callbackUrl = requireTrustedAuthCallbackUrl(ctx, ctx.body.callbackUrl);
@@ -8316,19 +9936,17 @@ const sendResetPasswordEmail = (options) => createAuthEndpoint("/dash/send-reset
8316
9936
  });
8317
9937
  //#endregion
8318
9938
  //#region src/pow.ts
9939
+ /**
9940
+ * Proof of Work Challenge System - Client Side
9941
+ *
9942
+ * Client-side PoW solver and encoding utilities.
9943
+ * Server-side challenge generation and verification moved to Infra API.
9944
+ */
8319
9945
  /** Default difficulty in bits (18 = ~500ms solve time) */
8320
9946
  const DEFAULT_DIFFICULTY = 18;
8321
9947
  /** Challenge TTL in seconds */
8322
9948
  const CHALLENGE_TTL = 60;
8323
9949
  /**
8324
- * SHA-256 hash function that works in both Node.js and browser
8325
- */
8326
- async function sha256(message) {
8327
- const msgBuffer = new TextEncoder().encode(message);
8328
- const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
8329
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
8330
- }
8331
- /**
8332
9950
  * Check if a hash has the required number of leading zero bits
8333
9951
  */
8334
9952
  function hasLeadingZeroBits(hash, bits) {
@@ -8348,7 +9966,8 @@ async function solvePoWChallenge(challenge) {
8348
9966
  const { nonce, difficulty } = challenge;
8349
9967
  let counter = 0;
8350
9968
  while (true) {
8351
- if (hasLeadingZeroBits(await sha256(`${nonce}:${counter}`), difficulty)) return {
9969
+ const input = `${nonce}:${counter}`;
9970
+ if (hasLeadingZeroBits(await hash$1(input), difficulty)) return {
8352
9971
  nonce,
8353
9972
  counter
8354
9973
  };
@@ -8378,7 +9997,8 @@ function encodePoWSolution(solution) {
8378
9997
  * Verify a PoW solution locally (for testing purposes)
8379
9998
  */
8380
9999
  async function verifyPoWSolution(nonce, counter, difficulty) {
8381
- return hasLeadingZeroBits(await sha256(`${nonce}:${counter}`), difficulty);
10000
+ const input = `${nonce}:${counter}`;
10001
+ return hasLeadingZeroBits(await hash$1(input), difficulty);
8382
10002
  }
8383
10003
  //#endregion
8384
10004
  //#region src/sms.ts
@@ -8412,7 +10032,7 @@ function createSMSSender(config) {
8412
10032
  "user-agent": INFRA_USER_AGENT,
8413
10033
  Authorization: `Bearer ${apiKey}`
8414
10034
  },
8415
- timeout: config?.apiTimeout ?? 3e3
10035
+ timeout: config?.apiOptions?.timeout ?? config?.apiTimeout ?? 3e3
8416
10036
  });
8417
10037
  /**
8418
10038
  * Send an SMS with OTP code
@@ -8491,6 +10111,18 @@ async function sendSMS(options, config) {
8491
10111
  }
8492
10112
  //#endregion
8493
10113
  //#region src/index.ts
10114
+ /**
10115
+ * Stashed when session.create after runs before newSession/returned user is
10116
+ * available (1.7+ / better-auth#10473). Avoids a fallback user DB lookup by
10117
+ * flushing tracking once the endpoint after hook can resolve the user.
10118
+ */
10119
+ const PENDING_SESSION_TRACKING = Symbol.for("dash.pendingSessionTracking");
10120
+ function getPendingSessionTracking(ctx) {
10121
+ return ctx.context[PENDING_SESSION_TRACKING];
10122
+ }
10123
+ function setPendingSessionTracking(ctx, pending) {
10124
+ ctx.context[PENDING_SESSION_TRACKING] = pending;
10125
+ }
8494
10126
  async function getRequestLocation() {
8495
10127
  try {
8496
10128
  return (await getCurrentAuthContext()).context.location;
@@ -8506,7 +10138,11 @@ const dash = (options) => {
8506
10138
  ...opts,
8507
10139
  $api
8508
10140
  };
8509
- const $kv = createKV(opts);
10141
+ const $kv = createKV({
10142
+ kvUrl: opts.kvUrl,
10143
+ apiKey: opts.apiKey,
10144
+ timeout: opts.kvOptions.timeout
10145
+ });
8510
10146
  const activityUpdateInterval = opts.activityTracking?.updateInterval ?? 3e5;
8511
10147
  const scheduleLastActiveUpdate = async (ctx, userId) => {
8512
10148
  await ctx.context.runInBackgroundOrAwait(ctx.context.adapter.updateMany({
@@ -8523,6 +10159,21 @@ const dash = (options) => {
8523
10159
  const { tracker } = initTrackEvents($api);
8524
10160
  const { trackUserSignedUp, trackUserProfileUpdated, trackUserProfileImageUpdated, trackUserEmailVerified, trackUserBanned, trackUserUnBanned, trackUserDeleted } = initUserEvents(tracker);
8525
10161
  const { trackEmailVerificationSent, trackEmailSignInAttempt, trackUserSignedIn, trackUserSignedOut, trackSessionCreated, trackSocialSignInAttempt, trackSocialSignInRedirectionAttempt, trackUserImpersonated, trackUserImpersonationStop, trackSessionRevoked, trackSessionRevokedAll } = initSessionEvents(tracker);
10162
+ const trackSessionLifecycle = (enrichedSession, userId, ctx, location, eventUser, trackSignIn, impersonatedBy) => {
10163
+ let trigger = null;
10164
+ if (trackSignIn) {
10165
+ trigger = getTriggerInfo(ctx, userId, enrichedSession);
10166
+ trackUserSignedIn(enrichedSession, trigger, ctx, location, eventUser);
10167
+ } else trigger = getTriggerInfo(ctx, userId);
10168
+ trackSessionCreated(enrichedSession, trigger, ctx, location, eventUser);
10169
+ if (impersonatedBy) {
10170
+ trigger = {
10171
+ ...trigger,
10172
+ triggeredBy: impersonatedBy
10173
+ };
10174
+ trackUserImpersonated(enrichedSession, trigger, ctx, location, eventUser, resolveUserFromContext(impersonatedBy, ctx));
10175
+ }
10176
+ };
8526
10177
  const { trackAccountLinking, trackAccountUnlink, trackAccountPasswordChange } = initAccountEvents(tracker);
8527
10178
  const { trackPasswordResetRequest, trackPasswordResetRequestCompletion } = initVerificationEvents(tracker);
8528
10179
  const { trackOrganizationCreated, trackOrganizationUpdated } = initOrganizationEvents(tracker);
@@ -8542,90 +10193,130 @@ const dash = (options) => {
8542
10193
  const afterCreateOrganization = organizationHooks.afterCreateOrganization;
8543
10194
  organizationHooks.afterCreateOrganization = async (...args) => {
8544
10195
  const [{ organization, user }] = args;
8545
- trackOrganizationCreated(organization, getOrganizationTriggerInfo(user), await getRequestLocation());
10196
+ const trigger = getOrganizationTriggerInfo(user);
10197
+ const location = await getRequestLocation();
10198
+ trackOrganizationCreated(organization, trigger, location);
8546
10199
  if (afterCreateOrganization) return afterCreateOrganization(...args);
8547
10200
  };
8548
10201
  const afterUpdateOrganization = organizationHooks.afterUpdateOrganization;
8549
10202
  organizationHooks.afterUpdateOrganization = async (...args) => {
8550
10203
  const [{ organization, user }] = args;
8551
- if (organization) trackOrganizationUpdated(organization, getOrganizationTriggerInfo(user), await getRequestLocation());
10204
+ if (organization) {
10205
+ const trigger = getOrganizationTriggerInfo(user);
10206
+ const location = await getRequestLocation();
10207
+ trackOrganizationUpdated(organization, trigger, location);
10208
+ }
8552
10209
  if (afterUpdateOrganization) return afterUpdateOrganization(...args);
8553
10210
  };
8554
10211
  const afterAddMember = organizationHooks.afterAddMember;
8555
10212
  organizationHooks.afterAddMember = async (...args) => {
8556
10213
  const [{ organization, member, user }] = args;
8557
- trackOrganizationMemberAdded(organization, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
10214
+ const trigger = getOrganizationTriggerInfo(user);
10215
+ const location = await getRequestLocation();
10216
+ trackOrganizationMemberAdded(organization, member, user, trigger, location);
8558
10217
  if (afterAddMember) return afterAddMember(...args);
8559
10218
  };
8560
10219
  const afterRemoveMember = organizationHooks.afterRemoveMember;
8561
10220
  organizationHooks.afterRemoveMember = async (...args) => {
8562
10221
  const [{ organization, member, user }] = args;
8563
- trackOrganizationMemberRemoved(organization, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
10222
+ const trigger = getOrganizationTriggerInfo(user);
10223
+ const location = await getRequestLocation();
10224
+ trackOrganizationMemberRemoved(organization, member, user, trigger, location);
8564
10225
  if (afterRemoveMember) return afterRemoveMember(...args);
8565
10226
  };
8566
10227
  const afterUpdateMemberRole = organizationHooks.afterUpdateMemberRole;
8567
10228
  organizationHooks.afterUpdateMemberRole = async (...args) => {
8568
10229
  const [{ organization, member, user, previousRole }] = args;
8569
- trackOrganizationMemberRoleUpdated(organization, member, user, previousRole, getOrganizationTriggerInfo(user), await getRequestLocation());
10230
+ const trigger = getOrganizationTriggerInfo(user);
10231
+ const location = await getRequestLocation();
10232
+ trackOrganizationMemberRoleUpdated(organization, member, user, previousRole, trigger, location);
8570
10233
  if (afterUpdateMemberRole) return afterUpdateMemberRole(...args);
8571
10234
  };
8572
10235
  const afterCreateInvitation = organizationHooks.afterCreateInvitation;
8573
10236
  organizationHooks.afterCreateInvitation = async (...args) => {
8574
10237
  const [{ organization, invitation, inviter }] = args;
8575
- trackOrganizationMemberInvited(organization, invitation, inviter, getOrganizationTriggerInfo(inviter), await getRequestLocation());
10238
+ const trigger = getOrganizationTriggerInfo(inviter);
10239
+ const location = await getRequestLocation();
10240
+ trackOrganizationMemberInvited(organization, invitation, inviter, trigger, location);
8576
10241
  if (afterCreateInvitation) return afterCreateInvitation(...args);
8577
10242
  };
8578
10243
  const afterAcceptInvitation = organizationHooks.afterAcceptInvitation;
8579
10244
  organizationHooks.afterAcceptInvitation = async (...args) => {
8580
10245
  const [{ organization, invitation, member, user }] = args;
8581
- trackOrganizationMemberInviteAccepted(organization, invitation, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
10246
+ const trigger = getOrganizationTriggerInfo(user);
10247
+ const location = await getRequestLocation();
10248
+ trackOrganizationMemberInviteAccepted(organization, invitation, member, user, trigger, location);
8582
10249
  if (afterAcceptInvitation) return afterAcceptInvitation(...args);
8583
10250
  };
8584
10251
  const afterRejectInvitation = organizationHooks.afterRejectInvitation;
8585
10252
  organizationHooks.afterRejectInvitation = async (...args) => {
8586
10253
  const [{ organization, invitation, user }] = args;
8587
- trackOrganizationMemberInviteRejected(organization, invitation, user, getOrganizationTriggerInfo(user), await getRequestLocation());
10254
+ const trigger = getOrganizationTriggerInfo(user);
10255
+ const location = await getRequestLocation();
10256
+ trackOrganizationMemberInviteRejected(organization, invitation, user, trigger, location);
8588
10257
  if (afterRejectInvitation) return afterRejectInvitation(...args);
8589
10258
  };
8590
10259
  const afterCancelInvitation = organizationHooks.afterCancelInvitation;
8591
10260
  organizationHooks.afterCancelInvitation = async (...args) => {
8592
10261
  const [{ organization, invitation, cancelledBy }] = args;
8593
- trackOrganizationMemberInviteCanceled(organization, invitation, cancelledBy, getOrganizationTriggerInfo(cancelledBy), await getRequestLocation());
10262
+ const trigger = getOrganizationTriggerInfo(cancelledBy);
10263
+ const location = await getRequestLocation();
10264
+ trackOrganizationMemberInviteCanceled(organization, invitation, cancelledBy, trigger, location);
8594
10265
  if (afterCancelInvitation) return afterCancelInvitation(...args);
8595
10266
  };
8596
10267
  const afterCreateTeam = organizationHooks.afterCreateTeam;
8597
10268
  organizationHooks.afterCreateTeam = async (...args) => {
8598
10269
  const [{ organization, team, user }] = args;
8599
- trackOrganizationTeamCreated(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
10270
+ const trigger = getOrganizationTriggerInfo(user);
10271
+ const location = await getRequestLocation();
10272
+ trackOrganizationTeamCreated(organization, team, trigger, location);
8600
10273
  if (afterCreateTeam) return afterCreateTeam(...args);
8601
10274
  };
8602
10275
  const afterUpdateTeam = organizationHooks.afterUpdateTeam;
8603
10276
  organizationHooks.afterUpdateTeam = async (...args) => {
8604
10277
  const [{ organization, team, user }] = args;
8605
- if (team) trackOrganizationTeamUpdated(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
10278
+ if (team) {
10279
+ const trigger = getOrganizationTriggerInfo(user);
10280
+ const location = await getRequestLocation();
10281
+ trackOrganizationTeamUpdated(organization, team, trigger, location);
10282
+ }
8606
10283
  if (afterUpdateTeam) return afterUpdateTeam(...args);
8607
10284
  };
8608
10285
  const afterDeleteTeam = organizationHooks.afterDeleteTeam;
8609
10286
  organizationHooks.afterDeleteTeam = async (...args) => {
8610
10287
  const [{ organization, team, user }] = args;
8611
- trackOrganizationTeamDeleted(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
10288
+ const trigger = getOrganizationTriggerInfo(user);
10289
+ const location = await getRequestLocation();
10290
+ trackOrganizationTeamDeleted(organization, team, trigger, location);
8612
10291
  if (afterDeleteTeam) return afterDeleteTeam(...args);
8613
10292
  };
8614
10293
  const afterAddTeamMember = organizationHooks.afterAddTeamMember;
8615
10294
  organizationHooks.afterAddTeamMember = async (...args) => {
8616
10295
  const [{ organization, team, user, teamMember }] = args;
8617
- trackOrganizationTeamMemberAdded(organization, team, user, teamMember, getOrganizationTriggerInfo(user), await getRequestLocation());
10296
+ const trigger = getOrganizationTriggerInfo(user);
10297
+ const location = await getRequestLocation();
10298
+ trackOrganizationTeamMemberAdded(organization, team, user, teamMember, trigger, location);
8618
10299
  if (afterAddTeamMember) return afterAddTeamMember(...args);
8619
10300
  };
8620
10301
  const afterRemoveTeamMember = organizationHooks.afterRemoveTeamMember;
8621
10302
  organizationHooks.afterRemoveTeamMember = async (...args) => {
8622
10303
  const [{ organization, team, user, teamMember }] = args;
8623
- trackOrganizationTeamMemberRemoved(organization, team, user, teamMember, getOrganizationTriggerInfo(user), await getRequestLocation());
10304
+ const trigger = getOrganizationTriggerInfo(user);
10305
+ const location = await getRequestLocation();
10306
+ trackOrganizationTeamMemberRemoved(organization, team, user, teamMember, trigger, location);
8624
10307
  if (afterRemoveTeamMember) return afterRemoveTeamMember(...args);
8625
10308
  };
8626
10309
  };
8627
10310
  instrumentOrganizationHooks(organizationPlugin.options = organizationPlugin.options ?? {});
8628
10311
  } else logger.debug("[Dash] Organization plugin not active. Skipping instrumentation");
10312
+ const managedDirectorySync = opts.managedDirectorySync;
10313
+ if (managedDirectorySync?.enabled) instrumentDirectorySyncIntegration(ctx, {
10314
+ ssoPairing: managedDirectorySync.ssoPairing ?? true,
10315
+ membershipProjection: {
10316
+ enabled: managedDirectorySync.membershipProjection?.enabled ?? true,
10317
+ role: managedDirectorySync.membershipProjection?.role ?? "member"
10318
+ }
10319
+ });
8629
10320
  return { options: {
8630
10321
  databaseHooks: {
8631
10322
  user: {
@@ -8687,25 +10378,25 @@ const dash = (options) => {
8687
10378
  countryCode: location?.countryCode
8688
10379
  };
8689
10380
  const eventUser = resolveUserFromContext(session.userId, ctx);
8690
- let trigger = null;
8691
- if (matchesAnyRoute(ctx.path, [
10381
+ const trackSignIn = matchesAnyRoute(ctx.path, [
8692
10382
  routes.SIGN_IN,
8693
10383
  routes.SIGN_UP,
8694
10384
  routes.SIGN_IN_SOCIAL_CALLBACK,
8695
10385
  routes.SIGN_IN_OAUTH_CALLBACK
8696
- ])) {
8697
- trigger = getTriggerInfo(ctx, session.userId, enrichedSession);
8698
- trackUserSignedIn(enrichedSession, trigger, ctx, location, eventUser);
8699
- } else trigger = getTriggerInfo(ctx, session.userId);
8700
- trackSessionCreated(enrichedSession, trigger, ctx, location, eventUser);
8701
- if ("impersonatedBy" in session && session.impersonatedBy) {
8702
- trigger = {
8703
- ...trigger,
8704
- triggeredBy: session.impersonatedBy
8705
- };
8706
- const knownImpersonator = resolveUserFromContext(session.impersonatedBy, ctx);
8707
- trackUserImpersonated(enrichedSession, trigger, ctx, location, eventUser, knownImpersonator);
10386
+ ]);
10387
+ const impersonatedBy = "impersonatedBy" in session && session.impersonatedBy ? session.impersonatedBy : void 0;
10388
+ if (!eventUser) {
10389
+ setPendingSessionTracking(ctx, {
10390
+ enrichedSession,
10391
+ location,
10392
+ userId: session.userId,
10393
+ trackSignIn,
10394
+ impersonatedBy
10395
+ });
10396
+ if (opts.activityTracking?.enabled) await scheduleLastActiveUpdate(ctx, session.userId);
10397
+ return;
8708
10398
  }
10399
+ trackSessionLifecycle(enrichedSession, session.userId, ctx, location, eventUser, trackSignIn, impersonatedBy);
8709
10400
  if (opts.activityTracking?.enabled) await scheduleLastActiveUpdate(ctx, session.userId);
8710
10401
  }
8711
10402
  },
@@ -8735,7 +10426,10 @@ const dash = (options) => {
8735
10426
  }
8736
10427
  } else if (matchesAnyRoute(path, [routes.SIGN_OUT])) trackUserSignedOut(enrichedSession, trigger, ctx, location, eventUser);
8737
10428
  else trackSessionRevoked(enrichedSession, trigger, ctx, location, eventUser);
8738
- if ("impersonatedBy" in session && session.impersonatedBy) trackUserImpersonationStop(enrichedSession, trigger, ctx, location, eventUser, resolveUserFromContext(session.impersonatedBy, ctx));
10429
+ if ("impersonatedBy" in session && session.impersonatedBy) {
10430
+ const knownImpersonator = resolveUserFromContext(session.impersonatedBy, ctx);
10431
+ trackUserImpersonationStop(enrichedSession, trigger, ctx, location, eventUser, knownImpersonator);
10432
+ }
8739
10433
  } }
8740
10434
  },
8741
10435
  account: {
@@ -8774,14 +10468,16 @@ const dash = (options) => {
8774
10468
  const ctx = _ctx;
8775
10469
  if (!ctx) return;
8776
10470
  const path = ctx.path;
8777
- const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? "unknown");
10471
+ const maybeUserId = ctx.context.session?.user.id ?? "unknown";
10472
+ const trigger = getTriggerInfo(ctx, maybeUserId);
8778
10473
  const location = ctx.context.location;
8779
10474
  if (matchesAnyRoute(path, [routes.REQUEST_PASSWORD_RESET])) trackPasswordResetRequest(verification, trigger, ctx, location);
8780
10475
  } },
8781
10476
  delete: { async after(verification, ctx) {
8782
10477
  if (!ctx) return;
8783
10478
  const path = ctx.path;
8784
- const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? "unknown");
10479
+ const maybeUserId = ctx.context.session?.user.id ?? "unknown";
10480
+ const trigger = getTriggerInfo(ctx, maybeUserId);
8785
10481
  const location = ctx.context.location;
8786
10482
  if (matchesAnyRoute(path, [routes.RESET_PASSWORD])) trackPasswordResetRequestCompletion(verification, trigger, ctx, location);
8787
10483
  } }
@@ -8804,53 +10500,68 @@ const dash = (options) => {
8804
10500
  routes.DASH_COMPLETE_INVITATION_SOCIAL
8805
10501
  ]);
8806
10502
  },
8807
- handler: createIdentificationMiddleware($kv, { skipIdentification: (ctx) => isDashRoute(ctx.path) })
10503
+ handler: createIdentificationMiddleware($kv, {
10504
+ skipIdentification: (ctx) => isDashRoute(ctx.path),
10505
+ retry: opts.kvOptions.retry
10506
+ })
8808
10507
  }],
8809
- after: [{
8810
- matcher: (ctx) => {
8811
- if (ctx.request?.method !== "GET") return true;
8812
- return matchesAnyRoute(ctx.path, [
8813
- routes.SIGN_IN_SOCIAL_CALLBACK,
8814
- routes.SIGN_IN_OAUTH_CALLBACK,
8815
- routes.DASH_IMPERSONATE_USER
8816
- ]);
10508
+ after: [
10509
+ {
10510
+ matcher: (ctx) => !!getPendingSessionTracking(ctx),
10511
+ handler: createAuthMiddleware(async (ctx) => {
10512
+ const pending = getPendingSessionTracking(ctx);
10513
+ if (!pending) return;
10514
+ trackSessionLifecycle(pending.enrichedSession, pending.userId, ctx, pending.location, resolveUserFromContext(pending.userId, ctx), pending.trackSignIn, pending.impersonatedBy);
10515
+ })
8817
10516
  },
8818
- handler: createAuthMiddleware(async (_ctx) => {
8819
- const ctx = _ctx;
8820
- const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? "unknown");
8821
- if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL, routes.DASH_SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx.context.location);
8822
- const body = ctx.body;
8823
- if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger, ctx.context.location);
8824
- if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger, ctx.context.location);
8825
- if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]) && ctx.request?.method === "GET" && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger, ctx.context.location);
8826
- const headerRequestId = ctx.request?.headers.get("X-Request-Id");
8827
- if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
8828
- maxAge: 600,
8829
- sameSite: "lax",
8830
- httpOnly: true,
8831
- path: "/"
8832
- });
8833
- else if (ctx.context.requestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, "", {
8834
- maxAge: 0,
8835
- path: "/"
8836
- });
8837
- })
8838
- }, {
8839
- handler: createAuthMiddleware(async (ctx) => {
8840
- if (!opts.activityTracking?.enabled) return;
8841
- if (activityUpdateInterval === 0) return;
8842
- const session = ctx.context.session || ctx.context.newSession;
8843
- if (!session?.user?.id) return;
8844
- const userId = session.user.id;
8845
- const now = Date.now();
8846
- const lastUpdate = session.user.lastActiveAt;
8847
- if (lastUpdate) {
8848
- if (now - new Date(lastUpdate).getTime() < activityUpdateInterval) return;
8849
- }
8850
- await scheduleLastActiveUpdate(ctx, userId);
8851
- }),
8852
- matcher: (ctx) => ctx.request?.method !== "GET"
8853
- }]
10517
+ {
10518
+ matcher: (ctx) => {
10519
+ if (ctx.request?.method !== "GET") return true;
10520
+ return matchesAnyRoute(ctx.path, [
10521
+ routes.SIGN_IN_SOCIAL_CALLBACK,
10522
+ routes.SIGN_IN_OAUTH_CALLBACK,
10523
+ routes.DASH_IMPERSONATE_USER
10524
+ ]);
10525
+ },
10526
+ handler: createAuthMiddleware(async (_ctx) => {
10527
+ const ctx = _ctx;
10528
+ const maybeUserId = ctx.context.session?.user.id ?? "unknown";
10529
+ const trigger = getTriggerInfo(ctx, maybeUserId);
10530
+ if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL, routes.DASH_SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx.context.location);
10531
+ const body = ctx.body;
10532
+ if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger, ctx.context.location);
10533
+ if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger, ctx.context.location);
10534
+ if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]) && ctx.request?.method === "GET" && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger, ctx.context.location);
10535
+ const headerRequestId = ctx.request?.headers.get("X-Request-Id");
10536
+ if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
10537
+ maxAge: 600,
10538
+ sameSite: "lax",
10539
+ httpOnly: true,
10540
+ path: "/"
10541
+ });
10542
+ else if (ctx.context.requestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, "", {
10543
+ maxAge: 0,
10544
+ path: "/"
10545
+ });
10546
+ })
10547
+ },
10548
+ {
10549
+ handler: createAuthMiddleware(async (ctx) => {
10550
+ if (!opts.activityTracking?.enabled) return;
10551
+ if (activityUpdateInterval === 0) return;
10552
+ const session = ctx.context.session || ctx.context.newSession;
10553
+ if (!session?.user?.id) return;
10554
+ const userId = session.user.id;
10555
+ const now = Date.now();
10556
+ const lastUpdate = session.user.lastActiveAt;
10557
+ if (lastUpdate) {
10558
+ if (now - new Date(lastUpdate).getTime() < activityUpdateInterval) return;
10559
+ }
10560
+ await scheduleLastActiveUpdate(ctx, userId);
10561
+ }),
10562
+ matcher: (ctx) => ctx.request?.method !== "GET"
10563
+ }
10564
+ ]
8854
10565
  },
8855
10566
  endpoints: {
8856
10567
  getDashConfig: getConfig(settings),
@@ -8925,15 +10636,178 @@ const dash = (options) => {
8925
10636
  dashCompleteInvitationSocial: completeInvitationSocial(settings),
8926
10637
  dashCheckUserExists: checkUserExists(settings),
8927
10638
  listDashOrganizationDirectories: listOrganizationDirectories(settings),
8928
- createDashOrganizationDirectory: createOrganizationDirectory(settings),
8929
- deleteDashOrganizationDirectory: deleteOrganizationDirectory(settings),
8930
- regenerateDashDirectoryToken: regenerateDirectoryToken(settings),
10639
+ createDashOrganizationDirectory: createOrganizationDirectoryLegacy(settings),
10640
+ deleteDashOrganizationDirectory: deleteOrganizationDirectoryLegacy(settings),
10641
+ regenerateDashDirectoryToken: regenerateDirectoryTokenLegacy(settings),
10642
+ getDashManagedOrganizationDirectory: getOrganizationDirectory(settings),
10643
+ createDashManagedOrganizationDirectory: createOrganizationDirectory(settings),
10644
+ rotateDashManagedDirectoryCredential: rotateDirectoryCredential(settings),
10645
+ revokeDashManagedDirectoryCredential: revokeDirectoryCredential(settings),
10646
+ listDashManagedDirectoryEvents: listDirectoryEvents(settings),
10647
+ decommissionDashManagedOrganizationDirectory: decommissionOrganizationDirectory(settings),
10648
+ unpairDashManagedOrganizationDirectory: unpairOrganizationDirectory(settings),
8931
10649
  dashExecuteAdapter: executeAdapter(settings)
8932
10650
  },
8933
- schema: opts.activityTracking?.enabled ? { user: { fields: { lastActiveAt: {
8934
- type: "date",
8935
- required: false
8936
- } } } } : {}
10651
+ schema: {
10652
+ ...opts.activityTracking?.enabled ? { user: { fields: { lastActiveAt: {
10653
+ type: "date",
10654
+ required: false
10655
+ } } } } : {},
10656
+ ...opts.managedDirectorySync?.enabled ? {
10657
+ directorySyncConnection: { fields: {
10658
+ organizationId: {
10659
+ type: "string",
10660
+ required: true,
10661
+ index: true
10662
+ },
10663
+ providerId: {
10664
+ type: "string",
10665
+ required: true
10666
+ },
10667
+ aliasKey: {
10668
+ type: "string",
10669
+ required: true,
10670
+ unique: true,
10671
+ returned: false
10672
+ },
10673
+ provisioningDomainId: {
10674
+ type: "string",
10675
+ required: true,
10676
+ unique: true
10677
+ },
10678
+ activeOrganizationKey: {
10679
+ type: "string",
10680
+ required: true,
10681
+ unique: true,
10682
+ returned: false
10683
+ },
10684
+ connectionId: {
10685
+ type: "string",
10686
+ required: false,
10687
+ unique: true
10688
+ },
10689
+ creationRequestId: {
10690
+ type: "string",
10691
+ required: true,
10692
+ unique: true,
10693
+ returned: false
10694
+ },
10695
+ status: {
10696
+ type: "string",
10697
+ required: true
10698
+ },
10699
+ revision: {
10700
+ type: "number",
10701
+ required: true,
10702
+ defaultValue: 0,
10703
+ returned: false
10704
+ },
10705
+ createdAt: {
10706
+ type: "date",
10707
+ required: true
10708
+ },
10709
+ createdByActorId: {
10710
+ type: "string",
10711
+ required: true
10712
+ },
10713
+ updatedAt: {
10714
+ type: "date",
10715
+ required: true
10716
+ },
10717
+ lastActorId: {
10718
+ type: "string",
10719
+ required: true
10720
+ },
10721
+ ssoProviderId: {
10722
+ type: "string",
10723
+ required: false
10724
+ },
10725
+ ssoProviderRecordId: {
10726
+ type: "string",
10727
+ required: false,
10728
+ index: true
10729
+ },
10730
+ activeSsoProviderKey: {
10731
+ type: "string",
10732
+ required: true,
10733
+ unique: true,
10734
+ returned: false
10735
+ },
10736
+ serializedSsoPairing: {
10737
+ type: "string",
10738
+ required: false,
10739
+ returned: false
10740
+ },
10741
+ pairingEnforced: {
10742
+ type: "boolean",
10743
+ required: true,
10744
+ defaultValue: false
10745
+ },
10746
+ unpairedAt: {
10747
+ type: "date",
10748
+ required: false
10749
+ },
10750
+ unpairedBy: {
10751
+ type: "string",
10752
+ required: false
10753
+ },
10754
+ decommissionStartedAt: {
10755
+ type: "date",
10756
+ required: false
10757
+ },
10758
+ decommissionedAt: {
10759
+ type: "date",
10760
+ required: false
10761
+ },
10762
+ lastError: {
10763
+ type: "string",
10764
+ required: false,
10765
+ returned: false
10766
+ }
10767
+ } },
10768
+ directorySyncMembershipProvenance: { fields: {
10769
+ membershipKey: {
10770
+ type: "string",
10771
+ required: true,
10772
+ unique: true,
10773
+ returned: false
10774
+ },
10775
+ organizationId: {
10776
+ type: "string",
10777
+ required: true,
10778
+ index: true
10779
+ },
10780
+ userId: {
10781
+ type: "string",
10782
+ required: true,
10783
+ index: true
10784
+ },
10785
+ memberId: {
10786
+ type: "string",
10787
+ required: true,
10788
+ unique: true
10789
+ },
10790
+ ownership: {
10791
+ type: "string",
10792
+ required: true,
10793
+ returned: false
10794
+ },
10795
+ provisioningDomainId: {
10796
+ type: "string",
10797
+ required: true,
10798
+ index: true
10799
+ },
10800
+ createdAt: {
10801
+ type: "date",
10802
+ required: true
10803
+ },
10804
+ updatedAt: {
10805
+ type: "date",
10806
+ required: true
10807
+ }
10808
+ } }
10809
+ } : {}
10810
+ }
8937
10811
  };
8938
10812
  };
8939
10813
  //#endregion