@better-auth/infra 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +12 -1
- package/dist/client.d.mts +7 -2
- package/dist/client.mjs +4 -14
- package/dist/{constants-CQiIiY9L.mjs → constants-CcJHk5SE.mjs} +1 -1
- package/dist/{crypto-Deqoay7H.mjs → crypto-CAdWFWEz.mjs} +3 -3
- package/dist/email.d.mts +11 -0
- package/dist/email.mjs +2 -2
- package/dist/{dash-client-DXuu2ctl.d.mts → identify-client-options-RI-JiXQx.d.mts} +16 -1
- package/dist/index.d.mts +256 -569
- package/dist/index.mjs +2117 -306
- package/dist/native.d.mts +7 -2
- package/dist/native.mjs +5 -15
- package/dist/{pow-retry-DXiVsZkt.mjs → pow-retry-DZhZiYd_.mjs} +17 -2
- package/dist/saml-policy-BTVLoTyS.mjs +12 -0
- package/dist/types-B673lCib.d.mts +673 -0
- package/package.json +5 -4
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-
|
|
2
|
-
import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto-
|
|
1
|
+
import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-CcJHk5SE.mjs";
|
|
2
|
+
import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-CAdWFWEz.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";
|
|
10
|
-
import { createLocalJWKSet, jwtVerify } from "jose";
|
|
11
11
|
import z$1, { z } from "zod";
|
|
12
|
-
import {
|
|
12
|
+
import { createLocalJWKSet, jwtVerify } from "jose";
|
|
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
|
-
|
|
22
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -341,8 +359,80 @@ const getLoginMethod = (ctx) => {
|
|
|
341
359
|
return null;
|
|
342
360
|
};
|
|
343
361
|
//#endregion
|
|
362
|
+
//#region src/compat/account.ts
|
|
363
|
+
/**
|
|
364
|
+
* Mirrors better-auth 1.7+ `createLocalAccountIssuer` so infra can
|
|
365
|
+
* typecheck against older core packages while emitting the same issuer strings.
|
|
366
|
+
*/
|
|
367
|
+
function createLocalAccountIssuer(providerId) {
|
|
368
|
+
return `local:${encodeURIComponent(providerId)}`;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Mirrors better-auth 1.7+ `createOAuthAccountIssuer`.
|
|
372
|
+
*/
|
|
373
|
+
function createOAuthAccountIssuer(providerId) {
|
|
374
|
+
return `local:oauth:${encodeURIComponent(providerId)}`;
|
|
375
|
+
}
|
|
376
|
+
/** True when the installed better-auth build scopes accounts by issuer. */
|
|
377
|
+
function supportsIssuerScopedAccounts(adapter) {
|
|
378
|
+
return typeof adapter.findAccountByKey === "function";
|
|
379
|
+
}
|
|
380
|
+
/** Creates a credential account across pre/post issuer-scoped account schemas. */
|
|
381
|
+
async function createCredentialAccountCompat(adapter, params) {
|
|
382
|
+
if (supportsIssuerScopedAccounts(adapter)) {
|
|
383
|
+
await adapter.createAccount({
|
|
384
|
+
userId: params.userId,
|
|
385
|
+
providerId: "credential",
|
|
386
|
+
accountId: params.userId,
|
|
387
|
+
issuer: createLocalAccountIssuer("credential"),
|
|
388
|
+
password: params.password
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
await adapter.createAccount({
|
|
393
|
+
userId: params.userId,
|
|
394
|
+
providerId: "credential",
|
|
395
|
+
accountId: params.userId,
|
|
396
|
+
password: params.password
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/** Finds an unlink target by provider and local account row id. */
|
|
400
|
+
function resolveAccountForUnlink(accounts, selector) {
|
|
401
|
+
return accounts.find((account) => account.providerId === selector.providerId && account.id === selector.accountId);
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
344
404
|
//#region src/events/core/oauth-callback-user.ts
|
|
345
405
|
const OAUTH_CALLBACK_USER = Symbol.for("dash.oauthCallbackUser");
|
|
406
|
+
function normalizeProviderSubject(subject) {
|
|
407
|
+
if (!subject || subject === "undefined" || subject === "null") return;
|
|
408
|
+
return subject;
|
|
409
|
+
}
|
|
410
|
+
async function resolveAccountKey(provider, tokens, profile) {
|
|
411
|
+
const keyedProvider = provider;
|
|
412
|
+
const context = {
|
|
413
|
+
tokens,
|
|
414
|
+
profile
|
|
415
|
+
};
|
|
416
|
+
try {
|
|
417
|
+
let rawSubject;
|
|
418
|
+
if (keyedProvider.accountSubject) rawSubject = String(await keyedProvider.accountSubject(context));
|
|
419
|
+
else {
|
|
420
|
+
const { id, sub } = profile;
|
|
421
|
+
const value = id ?? sub;
|
|
422
|
+
rawSubject = value == null ? void 0 : String(value);
|
|
423
|
+
}
|
|
424
|
+
const accountId = rawSubject ? normalizeProviderSubject(rawSubject) : void 0;
|
|
425
|
+
if (!accountId) return void 0;
|
|
426
|
+
const issuer = typeof keyedProvider.accountIssuer === "function" ? await keyedProvider.accountIssuer(context) : keyedProvider.accountIssuer ?? createOAuthAccountIssuer(provider.id);
|
|
427
|
+
if (!issuer) return void 0;
|
|
428
|
+
return {
|
|
429
|
+
issuer,
|
|
430
|
+
accountId
|
|
431
|
+
};
|
|
432
|
+
} catch {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
346
436
|
/**
|
|
347
437
|
* Stash OAuth profile on the request context when better-auth calls getUserInfo
|
|
348
438
|
* during the callback, before the authorization code is consumed.
|
|
@@ -355,19 +445,36 @@ function instrumentSocialProviders(providers) {
|
|
|
355
445
|
const user = result?.user;
|
|
356
446
|
if (user) try {
|
|
357
447
|
const endpointCtx = await getCurrentAuthContext();
|
|
358
|
-
endpointCtx.context
|
|
448
|
+
const adapter = endpointCtx.context.internalAdapter;
|
|
449
|
+
const accountKey = result.data && typeof adapter.findAccountOwnerByKey === "function" ? await resolveAccountKey(provider, token, result.data) : void 0;
|
|
450
|
+
endpointCtx.context[OAUTH_CALLBACK_USER] = {
|
|
451
|
+
user,
|
|
452
|
+
accountKey
|
|
453
|
+
};
|
|
359
454
|
} catch {}
|
|
360
455
|
return result;
|
|
361
456
|
};
|
|
362
457
|
}
|
|
363
458
|
}
|
|
364
459
|
async function resolveOAuthUser(providerId, ctx) {
|
|
365
|
-
const
|
|
366
|
-
if (!
|
|
460
|
+
const stashed = ctx.context[OAUTH_CALLBACK_USER];
|
|
461
|
+
if (!stashed) return null;
|
|
462
|
+
const { user: oauthUser, accountKey } = stashed;
|
|
367
463
|
const email = oauthUser.email?.toLowerCase();
|
|
464
|
+
const adapter = ctx.context.internalAdapter;
|
|
465
|
+
if (accountKey && typeof adapter.findAccountOwnerByKey === "function") try {
|
|
466
|
+
const owned = await adapter.findAccountOwnerByKey(accountKey);
|
|
467
|
+
if (owned?.kind === "owned") return {
|
|
468
|
+
id: owned.user.id,
|
|
469
|
+
email: owned.user.email,
|
|
470
|
+
name: owned.user.name
|
|
471
|
+
};
|
|
472
|
+
} catch (error) {
|
|
473
|
+
logger.debug("[Dash] Failed to find OAuth user by account key:", error);
|
|
474
|
+
}
|
|
368
475
|
const accountId = oauthUser.id !== void 0 && oauthUser.id !== null ? String(oauthUser.id) : void 0;
|
|
369
|
-
if (email && accountId) try {
|
|
370
|
-
const result = await
|
|
476
|
+
if (email && accountId && typeof adapter.findOAuthUser === "function") try {
|
|
477
|
+
const result = await adapter.findOAuthUser(email, accountId, providerId);
|
|
371
478
|
if (result?.user) return {
|
|
372
479
|
id: result.user.id,
|
|
373
480
|
email: result.user.email,
|
|
@@ -941,7 +1048,7 @@ function resolveClientIpFromHeaders(headers, ipAddressHeaders) {
|
|
|
941
1048
|
/**
|
|
942
1049
|
* Identification Service
|
|
943
1050
|
*
|
|
944
|
-
* Fetches identification data from the
|
|
1051
|
+
* Fetches identification data from the identify API
|
|
945
1052
|
* when a request includes an X-Request-Id header.
|
|
946
1053
|
*/
|
|
947
1054
|
const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
|
|
@@ -957,31 +1064,38 @@ function cleanupCache() {
|
|
|
957
1064
|
function maybeCleanup() {
|
|
958
1065
|
if (Date.now() - lastCleanup > CACHE_TTL_MS || identificationCache.size > CACHE_MAX_SIZE) cleanupCache();
|
|
959
1066
|
}
|
|
960
|
-
|
|
961
|
-
const IDENTIFY_GET_RETRY = {
|
|
962
|
-
type: "exponential",
|
|
1067
|
+
const DEFAULT_KV_RETRY = {
|
|
963
1068
|
attempts: 2,
|
|
964
1069
|
baseDelay: 400,
|
|
965
|
-
maxDelay: 600
|
|
966
|
-
shouldRetry(response) {
|
|
967
|
-
if (response === null) return true;
|
|
968
|
-
return response.status === 404;
|
|
969
|
-
}
|
|
1070
|
+
maxDelay: 600
|
|
970
1071
|
};
|
|
971
|
-
function
|
|
972
|
-
return
|
|
1072
|
+
function resolveIdentifyGetRetry(retry) {
|
|
1073
|
+
return {
|
|
1074
|
+
type: "exponential",
|
|
1075
|
+
attempts: retry.attempts,
|
|
1076
|
+
baseDelay: retry.baseDelay,
|
|
1077
|
+
maxDelay: retry.maxDelay,
|
|
1078
|
+
shouldRetry(response) {
|
|
1079
|
+
if (response === null) return true;
|
|
1080
|
+
return response.status === 404;
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function identifyGetRetryDelay(retry, attempt) {
|
|
1085
|
+
return Math.min(retry.maxDelay, retry.baseDelay * 2 ** attempt);
|
|
973
1086
|
}
|
|
974
1087
|
/**
|
|
975
|
-
* Fetch identification data from
|
|
1088
|
+
* Fetch identification data from the identify API by requestId
|
|
976
1089
|
*/
|
|
977
|
-
async function getIdentification(requestId, $kv) {
|
|
1090
|
+
async function getIdentification(requestId, $kv, retryOptions = DEFAULT_KV_RETRY) {
|
|
978
1091
|
maybeCleanup();
|
|
979
1092
|
const cached = identificationCache.get(requestId);
|
|
980
1093
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
|
|
1094
|
+
const retry = resolveIdentifyGetRetry(retryOptions);
|
|
981
1095
|
for (let networkAttempt = 0;; networkAttempt++) try {
|
|
982
1096
|
const { data, error } = await $kv(`/identify/${requestId}`, {
|
|
983
1097
|
method: "GET",
|
|
984
|
-
retry
|
|
1098
|
+
retry
|
|
985
1099
|
});
|
|
986
1100
|
if (data && !error) {
|
|
987
1101
|
identificationCache.set(requestId, {
|
|
@@ -996,11 +1110,11 @@ async function getIdentification(requestId, $kv) {
|
|
|
996
1110
|
});
|
|
997
1111
|
return null;
|
|
998
1112
|
} catch (error) {
|
|
999
|
-
if (networkAttempt >=
|
|
1113
|
+
if (networkAttempt >= retry.attempts) {
|
|
1000
1114
|
logger.error("[Dash] Failed to fetch identification:", error);
|
|
1001
1115
|
return null;
|
|
1002
1116
|
}
|
|
1003
|
-
await new Promise((resolve) => setTimeout(resolve, identifyGetRetryDelay(networkAttempt)));
|
|
1117
|
+
await new Promise((resolve) => setTimeout(resolve, identifyGetRetryDelay(retryOptions, networkAttempt)));
|
|
1004
1118
|
}
|
|
1005
1119
|
}
|
|
1006
1120
|
/**
|
|
@@ -1046,6 +1160,7 @@ function resolveUntrustedVisitorId(visitorId, ip, headerVisitorId) {
|
|
|
1046
1160
|
* @param $kv — KV client from {@link createKV} for the same plugin options (one instance per plugin).
|
|
1047
1161
|
*/
|
|
1048
1162
|
function createIdentificationMiddleware($kv, options) {
|
|
1163
|
+
const retry = options?.retry ?? DEFAULT_KV_RETRY;
|
|
1049
1164
|
return createAuthMiddleware(async (ctx) => {
|
|
1050
1165
|
const skipIdentification = options?.skipIdentification?.(ctx) ?? false;
|
|
1051
1166
|
let headerVisitorId = null;
|
|
@@ -1058,7 +1173,7 @@ function createIdentificationMiddleware($kv, options) {
|
|
|
1058
1173
|
ctx.context.requestId = requestId;
|
|
1059
1174
|
if (skipIdentification) ctx.context.identification = null;
|
|
1060
1175
|
else if (requestId) {
|
|
1061
|
-
if (ctx.context.identification === void 0) ctx.context.identification = await getIdentification(requestId, $kv) ?? null;
|
|
1176
|
+
if (ctx.context.identification === void 0) ctx.context.identification = await getIdentification(requestId, $kv, retry) ?? null;
|
|
1062
1177
|
} else ctx.context.identification = null;
|
|
1063
1178
|
const identification = ctx.context.identification;
|
|
1064
1179
|
const visitorId = resolveSecurityVisitorId(headerVisitorId, identification);
|
|
@@ -2188,9 +2303,24 @@ const sentinel = (options) => {
|
|
|
2188
2303
|
const opts = resolveSentinelOptions(options);
|
|
2189
2304
|
const $api = createAPI(opts);
|
|
2190
2305
|
const $apiThrowing = createAPI(opts, { throw: true });
|
|
2191
|
-
const $kv = createKV(
|
|
2306
|
+
const $kv = createKV({
|
|
2307
|
+
kvUrl: opts.kvUrl,
|
|
2308
|
+
apiKey: opts.apiKey,
|
|
2309
|
+
timeout: opts.kvOptions.timeout
|
|
2310
|
+
});
|
|
2192
2311
|
const { tracker } = initTrackEvents($api);
|
|
2193
2312
|
const { trackEvent } = tracker;
|
|
2313
|
+
const STALE_ACCOUNT_BLOCK_ERROR = {
|
|
2314
|
+
message: "This account has been inactive for an extended period. Please contact support to reactivate.",
|
|
2315
|
+
code: "STALE_ACCOUNT"
|
|
2316
|
+
};
|
|
2317
|
+
function isStaleAccountError(returned) {
|
|
2318
|
+
if (!(returned instanceof Error)) return false;
|
|
2319
|
+
const err = returned;
|
|
2320
|
+
if (err.code === STALE_ACCOUNT_BLOCK_ERROR.code) return true;
|
|
2321
|
+
return typeof err.body === "object" && err.body !== null && "code" in err.body && err.body.code === STALE_ACCOUNT_BLOCK_ERROR.code;
|
|
2322
|
+
}
|
|
2323
|
+
let activityTrackingEnabled = false;
|
|
2194
2324
|
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
2325
|
const securityService = createSecurityClient(opts, $apiThrowing, opts.security || {}, (event) => {
|
|
2196
2326
|
trackEvent({
|
|
@@ -2220,7 +2350,7 @@ const sentinel = (options) => {
|
|
|
2220
2350
|
return {
|
|
2221
2351
|
id: "sentinel",
|
|
2222
2352
|
init(ctx) {
|
|
2223
|
-
|
|
2353
|
+
activityTrackingEnabled = (ctx.getPlugin("dash")?.options)?.activityTracking?.enabled === true;
|
|
2224
2354
|
return { options: {
|
|
2225
2355
|
emailValidation: opts.security?.emailValidation,
|
|
2226
2356
|
emailNormalization: opts.security?.emailNormalization,
|
|
@@ -2324,35 +2454,6 @@ const sentinel = (options) => {
|
|
|
2324
2454
|
if (visitorId) {
|
|
2325
2455
|
if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
|
|
2326
2456
|
}
|
|
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
2457
|
if (opts.security?.impossibleTravel?.enabled && identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location, identification.ip));
|
|
2357
2458
|
}
|
|
2358
2459
|
} }
|
|
@@ -2363,7 +2464,10 @@ const sentinel = (options) => {
|
|
|
2363
2464
|
before: [
|
|
2364
2465
|
{
|
|
2365
2466
|
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2366
|
-
handler: createIdentificationMiddleware($kv, {
|
|
2467
|
+
handler: createIdentificationMiddleware($kv, {
|
|
2468
|
+
skipIdentification: (ctx) => isDashRoute(ctx.path),
|
|
2469
|
+
retry: opts.kvOptions.retry
|
|
2470
|
+
})
|
|
2367
2471
|
},
|
|
2368
2472
|
...emailHooks.before,
|
|
2369
2473
|
...phoneValidationHooks.before,
|
|
@@ -2462,6 +2566,52 @@ const sentinel = (options) => {
|
|
|
2462
2566
|
}
|
|
2463
2567
|
],
|
|
2464
2568
|
after: [{
|
|
2569
|
+
matcher: (ctx) => !!opts.security?.staleUsers?.enabled && !isDashRoute(ctx.path),
|
|
2570
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
2571
|
+
if (ctx.context.returned instanceof Error) return;
|
|
2572
|
+
const created = ctx.context.newSession;
|
|
2573
|
+
const userId = created?.user?.id ?? created?.session?.userId;
|
|
2574
|
+
const sessionToken = created?.session?.token;
|
|
2575
|
+
if (!userId || !sessionToken) return;
|
|
2576
|
+
let user = created?.user ?? null;
|
|
2577
|
+
try {
|
|
2578
|
+
user = await getUserById(userId, ctx, { includeLastActiveAt: activityTrackingEnabled }) ?? user;
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
logger.warn("[Sentinel] Failed to fetch user for stale-account check:", error);
|
|
2581
|
+
if (!user) return;
|
|
2582
|
+
}
|
|
2583
|
+
if (!user) return;
|
|
2584
|
+
recordCheck(ctx, "stale_users");
|
|
2585
|
+
const staleCheck = await securityService.checkStaleUser(userId, activityTrackingEnabled ? user.lastActiveAt ?? null : null);
|
|
2586
|
+
if (!staleCheck.isStale) return;
|
|
2587
|
+
const identification = ctx.context.identification;
|
|
2588
|
+
const staleOpts = opts.security?.staleUsers;
|
|
2589
|
+
const notificationPromises = [];
|
|
2590
|
+
if (staleCheck.notifyUser && user.email) notificationPromises.push(securityService.notifyStaleAccountUser(user.email, user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2591
|
+
if (staleCheck.notifyAdmin && staleOpts?.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2592
|
+
if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
|
|
2593
|
+
logger.error("[Sentinel] Failed to send stale account notifications:", error);
|
|
2594
|
+
});
|
|
2595
|
+
if (staleCheck.action !== "block") return;
|
|
2596
|
+
setOutcome(ctx, "blocked", "stale_users", {
|
|
2597
|
+
userId,
|
|
2598
|
+
daysSinceLastActive: staleCheck.daysSinceLastActive,
|
|
2599
|
+
staleDays: staleCheck.staleDays,
|
|
2600
|
+
lastActiveAt: staleCheck.lastActiveAt,
|
|
2601
|
+
notifyUser: staleCheck.notifyUser,
|
|
2602
|
+
notifyAdmin: staleCheck.notifyAdmin
|
|
2603
|
+
});
|
|
2604
|
+
emitEvaluation(ctx, trackEvent);
|
|
2605
|
+
try {
|
|
2606
|
+
await ctx.context.internalAdapter.deleteSession(sessionToken);
|
|
2607
|
+
} catch (error) {
|
|
2608
|
+
logger.warn("[Sentinel] Failed to delete stale-blocked session:", error);
|
|
2609
|
+
}
|
|
2610
|
+
deleteSessionCookie(ctx);
|
|
2611
|
+
ctx.context.setNewSession(null);
|
|
2612
|
+
throw new APIError("FORBIDDEN", STALE_ACCOUNT_BLOCK_ERROR);
|
|
2613
|
+
})
|
|
2614
|
+
}, {
|
|
2465
2615
|
matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
|
|
2466
2616
|
handler: createAuthMiddleware(async (ctx) => {
|
|
2467
2617
|
const untrustedVisitorId = ctx.context.untrustedVisitorId;
|
|
@@ -2477,14 +2627,427 @@ const sentinel = (options) => {
|
|
|
2477
2627
|
identifier: loginId,
|
|
2478
2628
|
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
2479
2629
|
});
|
|
2480
|
-
|
|
2481
|
-
|
|
2630
|
+
const returned = ctx.context.returned;
|
|
2631
|
+
const staleBlocked = isStaleAccountError(returned);
|
|
2632
|
+
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));
|
|
2633
|
+
if (isPasswordSignInRoute && (!(returned instanceof Error) || staleBlocked) && loginId) await ctx.context.runInBackgroundOrAwait(securityService.clearFailedAttempts(loginId));
|
|
2482
2634
|
})
|
|
2483
2635
|
}]
|
|
2484
2636
|
}
|
|
2485
2637
|
};
|
|
2486
2638
|
};
|
|
2487
2639
|
//#endregion
|
|
2640
|
+
//#region src/directory-sync/membership-projection.ts
|
|
2641
|
+
async function createMembershipKey(provisioningDomainId, userId) {
|
|
2642
|
+
return `directory-sync-membership:${await hash$1(JSON.stringify([provisioningDomainId, userId]))}`;
|
|
2643
|
+
}
|
|
2644
|
+
async function findDirectory(input, context) {
|
|
2645
|
+
const row = await context.database.findOne({
|
|
2646
|
+
model: "directorySyncConnection",
|
|
2647
|
+
where: [{
|
|
2648
|
+
field: "provisioningDomainId",
|
|
2649
|
+
value: input.provisioningDomainId
|
|
2650
|
+
}]
|
|
2651
|
+
});
|
|
2652
|
+
if (!row || row.status !== "active" && row.status !== "decommissioning" && row.status !== "decommissioned" || !row.connectionId) return null;
|
|
2653
|
+
return row;
|
|
2654
|
+
}
|
|
2655
|
+
async function findMembership(context, organizationId, userId) {
|
|
2656
|
+
const memberships = await context.database.findMany({
|
|
2657
|
+
model: "member",
|
|
2658
|
+
where: [{
|
|
2659
|
+
field: "organizationId",
|
|
2660
|
+
value: organizationId
|
|
2661
|
+
}, {
|
|
2662
|
+
field: "userId",
|
|
2663
|
+
value: userId
|
|
2664
|
+
}]
|
|
2665
|
+
});
|
|
2666
|
+
if (memberships.length > 1) throw new Error("Directory sync membership projection requires a unique organization and user membership");
|
|
2667
|
+
return memberships.at(0) ?? null;
|
|
2668
|
+
}
|
|
2669
|
+
async function findProvenance(context, membershipKey) {
|
|
2670
|
+
return await context.database.findOne({
|
|
2671
|
+
model: "directorySyncMembershipProvenance",
|
|
2672
|
+
where: [{
|
|
2673
|
+
field: "membershipKey",
|
|
2674
|
+
value: membershipKey
|
|
2675
|
+
}]
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
async function createProvenance(context, input) {
|
|
2679
|
+
const now = /* @__PURE__ */ new Date();
|
|
2680
|
+
await context.database.create({
|
|
2681
|
+
model: "directorySyncMembershipProvenance",
|
|
2682
|
+
data: {
|
|
2683
|
+
...input,
|
|
2684
|
+
createdAt: now,
|
|
2685
|
+
updatedAt: now
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
function createOrganizationMembershipProjection(options) {
|
|
2690
|
+
if (!options.role.trim()) throw new Error("Directory sync membership role must not be empty");
|
|
2691
|
+
return async (input, context) => {
|
|
2692
|
+
const directory = await findDirectory(input, context);
|
|
2693
|
+
if (!directory?.connectionId) return;
|
|
2694
|
+
const membershipKey = await createMembershipKey(input.provisioningDomainId, input.userId);
|
|
2695
|
+
const [membership, provenance] = await Promise.all([findMembership(context, directory.organizationId, input.userId), findProvenance(context, membershipKey)]);
|
|
2696
|
+
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");
|
|
2697
|
+
if (!input.active) {
|
|
2698
|
+
if (!provenance) return;
|
|
2699
|
+
if (provenance.ownership === "created" && membership?.id === provenance.memberId && membership.organizationId === provenance.organizationId && membership.userId === provenance.userId) await context.database.delete({
|
|
2700
|
+
model: "member",
|
|
2701
|
+
where: [
|
|
2702
|
+
{
|
|
2703
|
+
field: "id",
|
|
2704
|
+
value: provenance.memberId
|
|
2705
|
+
},
|
|
2706
|
+
{
|
|
2707
|
+
field: "organizationId",
|
|
2708
|
+
value: provenance.organizationId
|
|
2709
|
+
},
|
|
2710
|
+
{
|
|
2711
|
+
field: "userId",
|
|
2712
|
+
value: provenance.userId
|
|
2713
|
+
}
|
|
2714
|
+
]
|
|
2715
|
+
});
|
|
2716
|
+
await context.database.delete({
|
|
2717
|
+
model: "directorySyncMembershipProvenance",
|
|
2718
|
+
where: [{
|
|
2719
|
+
field: "id",
|
|
2720
|
+
value: provenance.id
|
|
2721
|
+
}, {
|
|
2722
|
+
field: "membershipKey",
|
|
2723
|
+
value: membershipKey
|
|
2724
|
+
}]
|
|
2725
|
+
});
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
if (membership) {
|
|
2729
|
+
if (provenance?.memberId === membership.id) return;
|
|
2730
|
+
if (provenance) await context.database.delete({
|
|
2731
|
+
model: "directorySyncMembershipProvenance",
|
|
2732
|
+
where: [{
|
|
2733
|
+
field: "id",
|
|
2734
|
+
value: provenance.id
|
|
2735
|
+
}, {
|
|
2736
|
+
field: "membershipKey",
|
|
2737
|
+
value: membershipKey
|
|
2738
|
+
}]
|
|
2739
|
+
});
|
|
2740
|
+
await createProvenance(context, {
|
|
2741
|
+
membershipKey,
|
|
2742
|
+
organizationId: directory.organizationId,
|
|
2743
|
+
userId: input.userId,
|
|
2744
|
+
memberId: membership.id,
|
|
2745
|
+
ownership: "observed",
|
|
2746
|
+
provisioningDomainId: input.provisioningDomainId
|
|
2747
|
+
});
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
if (provenance) await context.database.delete({
|
|
2751
|
+
model: "directorySyncMembershipProvenance",
|
|
2752
|
+
where: [{
|
|
2753
|
+
field: "id",
|
|
2754
|
+
value: provenance.id
|
|
2755
|
+
}, {
|
|
2756
|
+
field: "membershipKey",
|
|
2757
|
+
value: membershipKey
|
|
2758
|
+
}]
|
|
2759
|
+
});
|
|
2760
|
+
const now = /* @__PURE__ */ new Date();
|
|
2761
|
+
const createdMembership = await context.database.create({
|
|
2762
|
+
model: "member",
|
|
2763
|
+
data: {
|
|
2764
|
+
organizationId: directory.organizationId,
|
|
2765
|
+
userId: input.userId,
|
|
2766
|
+
role: options.role,
|
|
2767
|
+
createdAt: now
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
await createProvenance(context, {
|
|
2771
|
+
membershipKey,
|
|
2772
|
+
organizationId: directory.organizationId,
|
|
2773
|
+
userId: input.userId,
|
|
2774
|
+
memberId: createdMembership.id,
|
|
2775
|
+
ownership: "created",
|
|
2776
|
+
provisioningDomainId: input.provisioningDomainId
|
|
2777
|
+
});
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
//#endregion
|
|
2781
|
+
//#region src/routes/directory-sync/contract.ts
|
|
2782
|
+
const DIRECTORY_SYNC_PURPOSE = "directory-sync-management";
|
|
2783
|
+
const ALL_SCIM_SCOPES = [
|
|
2784
|
+
"scim.users.read",
|
|
2785
|
+
"scim.users.write",
|
|
2786
|
+
"scim.groups.read",
|
|
2787
|
+
"scim.groups.write"
|
|
2788
|
+
];
|
|
2789
|
+
const scimScopeSchema = z$1.enum(ALL_SCIM_SCOPES);
|
|
2790
|
+
const credentialPolicySchema = {
|
|
2791
|
+
scopes: z$1.array(scimScopeSchema).min(1).optional(),
|
|
2792
|
+
expiresAt: z$1.coerce.date().optional()
|
|
2793
|
+
};
|
|
2794
|
+
const directorySyncSSOPairingSchema = z$1.discriminatedUnion("protocol", [z$1.object({
|
|
2795
|
+
ssoProviderId: z$1.string().trim().min(1).max(255),
|
|
2796
|
+
protocol: z$1.literal("oidc"),
|
|
2797
|
+
externalIdSource: z$1.discriminatedUnion("kind", [z$1.object({ kind: z$1.literal("subject") }), z$1.object({
|
|
2798
|
+
kind: z$1.literal("verifiedIdTokenClaim"),
|
|
2799
|
+
name: z$1.string().trim().min(1).max(255)
|
|
2800
|
+
})])
|
|
2801
|
+
}), z$1.object({
|
|
2802
|
+
ssoProviderId: z$1.string().trim().min(1).max(255),
|
|
2803
|
+
protocol: z$1.literal("saml"),
|
|
2804
|
+
externalIdSource: z$1.discriminatedUnion("kind", [z$1.object({ kind: z$1.literal("nameId") }), z$1.object({
|
|
2805
|
+
kind: z$1.literal("attribute"),
|
|
2806
|
+
name: z$1.string().trim().min(1).max(255)
|
|
2807
|
+
})])
|
|
2808
|
+
})]);
|
|
2809
|
+
const createDirectoryBodySchema = z$1.object({
|
|
2810
|
+
providerId: z$1.string().trim().min(1).max(255),
|
|
2811
|
+
pairing: directorySyncSSOPairingSchema.optional(),
|
|
2812
|
+
...credentialPolicySchema
|
|
2813
|
+
});
|
|
2814
|
+
const rotateCredentialBodySchema = z$1.object(credentialPolicySchema);
|
|
2815
|
+
const emptyBodySchema = z$1.object({});
|
|
2816
|
+
function setCredentialResponseSecurityHeaders(ctx) {
|
|
2817
|
+
ctx.setHeader("Cache-Control", "no-store, max-age=0");
|
|
2818
|
+
ctx.setHeader("Pragma", "no-cache");
|
|
2819
|
+
ctx.setHeader("Referrer-Policy", "no-referrer");
|
|
2820
|
+
}
|
|
2821
|
+
//#endregion
|
|
2822
|
+
//#region src/directory-sync/pairing.ts
|
|
2823
|
+
let samlPolicyModule;
|
|
2824
|
+
function loadSAMLPolicy() {
|
|
2825
|
+
samlPolicyModule ??= import("./saml-policy-BTVLoTyS.mjs");
|
|
2826
|
+
return samlPolicyModule;
|
|
2827
|
+
}
|
|
2828
|
+
function isRecord$1(value) {
|
|
2829
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2830
|
+
}
|
|
2831
|
+
function parseConfiguration(value) {
|
|
2832
|
+
if (isRecord$1(value)) return value;
|
|
2833
|
+
if (typeof value !== "string") return null;
|
|
2834
|
+
try {
|
|
2835
|
+
const parsed = JSON.parse(value);
|
|
2836
|
+
return isRecord$1(parsed) ? parsed : null;
|
|
2837
|
+
} catch {
|
|
2838
|
+
return null;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
function parseDirectorySyncSSOPairing(value) {
|
|
2842
|
+
if (!value) return null;
|
|
2843
|
+
let parsed;
|
|
2844
|
+
try {
|
|
2845
|
+
parsed = JSON.parse(value);
|
|
2846
|
+
} catch {
|
|
2847
|
+
return null;
|
|
2848
|
+
}
|
|
2849
|
+
const result = directorySyncSSOPairingSchema.safeParse(parsed);
|
|
2850
|
+
return result.success ? result.data : null;
|
|
2851
|
+
}
|
|
2852
|
+
async function createActiveSSOProviderKey(ssoProviderRecordId) {
|
|
2853
|
+
return `directory-sync-sso-active:${await hash$1(ssoProviderRecordId)}`;
|
|
2854
|
+
}
|
|
2855
|
+
function createInactiveSSOProviderKey(aliasKey) {
|
|
2856
|
+
return `directory-sync-sso-inactive:${aliasKey}`;
|
|
2857
|
+
}
|
|
2858
|
+
function createTerminalSSOProviderKey(aliasKey) {
|
|
2859
|
+
return `directory-sync-sso-terminal:${aliasKey}`;
|
|
2860
|
+
}
|
|
2861
|
+
async function resolveDirectorySyncSSOPairing(ctx, organizationId, pairing) {
|
|
2862
|
+
const exactProvider = await ctx.context.adapter.update({
|
|
2863
|
+
model: "ssoProvider",
|
|
2864
|
+
where: [{
|
|
2865
|
+
field: "providerId",
|
|
2866
|
+
value: pairing.ssoProviderId
|
|
2867
|
+
}, {
|
|
2868
|
+
field: "organizationId",
|
|
2869
|
+
value: organizationId
|
|
2870
|
+
}],
|
|
2871
|
+
update: { providerId: pairing.ssoProviderId }
|
|
2872
|
+
});
|
|
2873
|
+
if (!exactProvider || exactProvider.providerId !== pairing.ssoProviderId || exactProvider.organizationId !== organizationId) throw ctx.error("BAD_REQUEST", { message: "The selected SSO provider is not available" });
|
|
2874
|
+
const oidcConfiguration = parseConfiguration(exactProvider.oidcConfig);
|
|
2875
|
+
const samlConfiguration = parseConfiguration(exactProvider.samlConfig);
|
|
2876
|
+
if (pairing.protocol === "oidc") {
|
|
2877
|
+
if (!oidcConfiguration || samlConfiguration) throw ctx.error("BAD_REQUEST", { message: "The selected SSO provider is not an OIDC provider" });
|
|
2878
|
+
} else {
|
|
2879
|
+
if (!samlConfiguration || oidcConfiguration) throw ctx.error("BAD_REQUEST", { message: "The selected SSO provider is not a SAML provider" });
|
|
2880
|
+
const samlPolicy = await loadSAMLPolicy();
|
|
2881
|
+
let wantAssertionsSigned = false;
|
|
2882
|
+
try {
|
|
2883
|
+
wantAssertionsSigned = await samlPolicy.requiresSignedSAMLAssertions({
|
|
2884
|
+
spMetadata: isRecord$1(samlConfiguration.spMetadata) ? { metadata: typeof samlConfiguration.spMetadata.metadata === "string" ? samlConfiguration.spMetadata.metadata : void 0 } : void 0,
|
|
2885
|
+
wantAssertionsSigned: samlConfiguration.wantAssertionsSigned === true
|
|
2886
|
+
});
|
|
2887
|
+
} catch {
|
|
2888
|
+
throw ctx.error("BAD_REQUEST", { message: "The selected SAML provider metadata is invalid" });
|
|
2889
|
+
}
|
|
2890
|
+
if (!wantAssertionsSigned) throw ctx.error("BAD_REQUEST", { message: "Paired SAML providers must require cryptographically signed assertions" });
|
|
2891
|
+
}
|
|
2892
|
+
return {
|
|
2893
|
+
pairing,
|
|
2894
|
+
ssoProviderId: exactProvider.providerId,
|
|
2895
|
+
ssoProviderRecordId: exactProvider.id,
|
|
2896
|
+
activeSsoProviderKey: await createActiveSSOProviderKey(exactProvider.id)
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
function serializePairingRow(row) {
|
|
2900
|
+
return parseDirectorySyncSSOPairing(row.serializedSsoPairing);
|
|
2901
|
+
}
|
|
2902
|
+
async function guardDirectorySyncSSOProviderMutation(input, context) {
|
|
2903
|
+
if (input.providerReference.source.type !== "persisted" || input.providerReference.source.recordId !== input.provider.id) throw new Error("SSO provider mutation reference is not exact");
|
|
2904
|
+
if (await context.database.findOne({
|
|
2905
|
+
model: "directorySyncConnection",
|
|
2906
|
+
where: [
|
|
2907
|
+
{
|
|
2908
|
+
field: "ssoProviderRecordId",
|
|
2909
|
+
value: input.provider.id
|
|
2910
|
+
},
|
|
2911
|
+
{
|
|
2912
|
+
field: "ssoProviderId",
|
|
2913
|
+
value: input.provider.providerId
|
|
2914
|
+
},
|
|
2915
|
+
{
|
|
2916
|
+
field: "organizationId",
|
|
2917
|
+
value: input.provider.organizationId
|
|
2918
|
+
},
|
|
2919
|
+
{
|
|
2920
|
+
field: "pairingEnforced",
|
|
2921
|
+
value: true
|
|
2922
|
+
}
|
|
2923
|
+
]
|
|
2924
|
+
})) {
|
|
2925
|
+
if (input.action === "update" && "isAuthenticationBoundaryChange" in input && input.isAuthenticationBoundaryChange === false) return;
|
|
2926
|
+
throw new Error("SSO provider is paired with directory sync");
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
//#endregion
|
|
2930
|
+
//#region src/directory-sync/sso-user-resolution.ts
|
|
2931
|
+
const GENERIC_REJECTION = {
|
|
2932
|
+
action: "reject",
|
|
2933
|
+
code: "DIRECTORY_SYNC_AUTHENTICATION_FAILED",
|
|
2934
|
+
message: "Unable to sign in with this SSO connection"
|
|
2935
|
+
};
|
|
2936
|
+
let scimCatalogModule;
|
|
2937
|
+
function loadSCIMCatalog() {
|
|
2938
|
+
scimCatalogModule ??= import("@better-auth/scim");
|
|
2939
|
+
return scimCatalogModule;
|
|
2940
|
+
}
|
|
2941
|
+
function readStringExternalId(value) {
|
|
2942
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
2943
|
+
return null;
|
|
2944
|
+
}
|
|
2945
|
+
function readSAMLExternalId(value) {
|
|
2946
|
+
const scalar = readStringExternalId(value);
|
|
2947
|
+
if (scalar) return scalar;
|
|
2948
|
+
if (Array.isArray(value) && value.length === 1 && typeof value[0] === "string" && value[0].length > 0) return value[0];
|
|
2949
|
+
return null;
|
|
2950
|
+
}
|
|
2951
|
+
function readExternalId(input, pairing) {
|
|
2952
|
+
if (input.protocol !== pairing.protocol) return null;
|
|
2953
|
+
if (pairing.protocol === "oidc" && input.protocol === "oidc") return readStringExternalId(pairing.externalIdSource.kind === "subject" ? input.accountKey.accountId : input.verifiedIdTokenClaims[pairing.externalIdSource.name]);
|
|
2954
|
+
if (pairing.protocol === "saml" && input.protocol === "saml") return readSAMLExternalId(pairing.externalIdSource.kind === "nameId" ? input.accountKey.accountId : input.providerAttributes[pairing.externalIdSource.name]);
|
|
2955
|
+
return null;
|
|
2956
|
+
}
|
|
2957
|
+
async function resolveOrganizationDirectorySyncUser(input, context) {
|
|
2958
|
+
if (input.providerReference.source.type !== "persisted") return { action: "continue" };
|
|
2959
|
+
const directories = await context.database.findMany({
|
|
2960
|
+
model: "directorySyncConnection",
|
|
2961
|
+
where: [
|
|
2962
|
+
{
|
|
2963
|
+
field: "ssoProviderRecordId",
|
|
2964
|
+
value: input.providerReference.source.recordId
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
field: "ssoProviderId",
|
|
2968
|
+
value: input.providerId
|
|
2969
|
+
},
|
|
2970
|
+
{
|
|
2971
|
+
field: "pairingEnforced",
|
|
2972
|
+
value: true
|
|
2973
|
+
}
|
|
2974
|
+
]
|
|
2975
|
+
});
|
|
2976
|
+
if (directories.length === 0) return { action: "continue" };
|
|
2977
|
+
if (directories.length !== 1) return GENERIC_REJECTION;
|
|
2978
|
+
const directory = directories[0];
|
|
2979
|
+
if (directory?.status !== "active") return GENERIC_REJECTION;
|
|
2980
|
+
if (directory.activeSsoProviderKey !== await createActiveSSOProviderKey(input.providerReference.source.recordId)) return GENERIC_REJECTION;
|
|
2981
|
+
const pairing = parseDirectorySyncSSOPairing(directory.serializedSsoPairing);
|
|
2982
|
+
if (!pairing || pairing.ssoProviderId !== input.providerId || pairing.protocol !== input.protocol || !directory.connectionId) return GENERIC_REJECTION;
|
|
2983
|
+
const externalId = readExternalId(input, pairing);
|
|
2984
|
+
if (!externalId) return GENERIC_REJECTION;
|
|
2985
|
+
try {
|
|
2986
|
+
const catalog = await loadSCIMCatalog();
|
|
2987
|
+
if (typeof catalog.acquireActiveSCIMUserLink !== "function") return GENERIC_REJECTION;
|
|
2988
|
+
const link = await catalog.acquireActiveSCIMUserLink({
|
|
2989
|
+
connectionId: directory.connectionId,
|
|
2990
|
+
externalId
|
|
2991
|
+
}, { database: context.database });
|
|
2992
|
+
if (!link) return GENERIC_REJECTION;
|
|
2993
|
+
return {
|
|
2994
|
+
action: "link",
|
|
2995
|
+
userId: link.userId,
|
|
2996
|
+
profile: "preserve"
|
|
2997
|
+
};
|
|
2998
|
+
} catch {
|
|
2999
|
+
return GENERIC_REJECTION;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
//#endregion
|
|
3003
|
+
//#region src/directory-sync/instrument.ts
|
|
3004
|
+
/**
|
|
3005
|
+
* When managed directory sync is enabled, optionally install SSO/SCIM
|
|
3006
|
+
* callbacks on sibling plugins (same pattern as organization hook
|
|
3007
|
+
* instrumentation).
|
|
3008
|
+
*/
|
|
3009
|
+
function instrumentDirectorySyncIntegration(ctx, options) {
|
|
3010
|
+
if (!options.ssoPairing && !options.membershipProjection.enabled) {
|
|
3011
|
+
logger.debug("[Dash] Managed directory sync instrumentation skipped (ssoPairing and membershipProjection are both disabled)");
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
const ssoPlugin = ctx.getPlugin("sso");
|
|
3015
|
+
const scimPlugin = ctx.getPlugin("scim");
|
|
3016
|
+
if (!ssoPlugin && !scimPlugin) {
|
|
3017
|
+
logger.debug("[Dash] Managed directory sync enabled but SSO/SCIM plugins are not active. Skipping integration instrumentation");
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
if (options.ssoPairing) if (ssoPlugin) 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.");
|
|
3021
|
+
else {
|
|
3022
|
+
const ssoOptions = ssoPlugin.options;
|
|
3023
|
+
const previousResolveUser = ssoOptions.resolveUser;
|
|
3024
|
+
ssoOptions.resolveUser = async (input, context) => {
|
|
3025
|
+
const result = await resolveOrganizationDirectorySyncUser(input, context);
|
|
3026
|
+
if (result.action !== "continue") return result;
|
|
3027
|
+
if (previousResolveUser) return previousResolveUser(input, context);
|
|
3028
|
+
return { action: "continue" };
|
|
3029
|
+
};
|
|
3030
|
+
const previousGuard = ssoOptions.guardProviderMutation;
|
|
3031
|
+
ssoOptions.guardProviderMutation = async (input, context) => {
|
|
3032
|
+
await guardDirectorySyncSSOProviderMutation(input, context);
|
|
3033
|
+
if (previousGuard) await previousGuard(input, context);
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
else logger.debug("[Dash] Managed directory sync ssoPairing enabled but SSO plugin is not active. Skipping SSO pairing instrumentation");
|
|
3037
|
+
if (options.membershipProjection.enabled) if (scimPlugin) if (!scimPlugin.options) logger.error("[Dash] Managed directory sync membershipProjection requires scim({ ... }) with an options object so membership projection can be installed.");
|
|
3038
|
+
else {
|
|
3039
|
+
const scimOptions = scimPlugin.options;
|
|
3040
|
+
const projection = scimOptions.projection ??= {};
|
|
3041
|
+
const previousReconcileUser = projection.reconcileUser;
|
|
3042
|
+
const reconcileUser = createOrganizationMembershipProjection({ role: options.membershipProjection.role });
|
|
3043
|
+
projection.reconcileUser = async (input, context) => {
|
|
3044
|
+
await reconcileUser(input, context);
|
|
3045
|
+
if (previousReconcileUser) await previousReconcileUser(input, context);
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
else logger.debug("[Dash] Managed directory sync membershipProjection enabled but SCIM plugin is not active. Skipping membership projection instrumentation");
|
|
3049
|
+
}
|
|
3050
|
+
//#endregion
|
|
2488
3051
|
//#region src/events/organization/events-invitation.ts
|
|
2489
3052
|
const initInvitationEvents = (tracker) => {
|
|
2490
3053
|
const { trackEvent } = tracker;
|
|
@@ -2840,61 +3403,58 @@ const initTeamEvents = (tracker) => {
|
|
|
2840
3403
|
};
|
|
2841
3404
|
//#endregion
|
|
2842
3405
|
//#region ../utils/dist/redact.mjs
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
]
|
|
2894
|
-
|
|
2895
|
-
* Embedded secret substrings checked against the normalized (compact, lowercased) key.
|
|
2896
|
-
*/
|
|
2897
|
-
const SENSITIVE_KEY_SUBSTRINGS_LOWER = ["secretaccess", "accesskeysecret"];
|
|
3406
|
+
/** Built-in sensitive key patterns for consumers that want the defaults. */
|
|
3407
|
+
const SENSITIVE_KEY_PATTERNS = {
|
|
3408
|
+
keys: [
|
|
3409
|
+
"accessToken",
|
|
3410
|
+
"apiKey",
|
|
3411
|
+
"apiSecret",
|
|
3412
|
+
"authorization",
|
|
3413
|
+
"authToken",
|
|
3414
|
+
"bearerToken",
|
|
3415
|
+
"backupCodes",
|
|
3416
|
+
"clientSecret",
|
|
3417
|
+
"consumerSecret",
|
|
3418
|
+
"credentialID",
|
|
3419
|
+
"deviceCode",
|
|
3420
|
+
"encPrivateKey",
|
|
3421
|
+
"encPrivateKeyPass",
|
|
3422
|
+
"encryptionKey",
|
|
3423
|
+
"encryptionSecret",
|
|
3424
|
+
"forwardHeaders",
|
|
3425
|
+
"idToken",
|
|
3426
|
+
"oidcConfig",
|
|
3427
|
+
"pass",
|
|
3428
|
+
"passwd",
|
|
3429
|
+
"password",
|
|
3430
|
+
"privateKey",
|
|
3431
|
+
"privateKeyPass",
|
|
3432
|
+
"pwd",
|
|
3433
|
+
"refreshToken",
|
|
3434
|
+
"samlConfig",
|
|
3435
|
+
"secret",
|
|
3436
|
+
"secretAccessKey",
|
|
3437
|
+
"secretKey",
|
|
3438
|
+
"signingSecret",
|
|
3439
|
+
"stripeWebhookSecret",
|
|
3440
|
+
"userCode",
|
|
3441
|
+
"webhookSecret"
|
|
3442
|
+
],
|
|
3443
|
+
suffixes: [
|
|
3444
|
+
"accesskey",
|
|
3445
|
+
"secret",
|
|
3446
|
+
"password",
|
|
3447
|
+
"passphrase",
|
|
3448
|
+
"privatekey",
|
|
3449
|
+
"keypass",
|
|
3450
|
+
"apikey",
|
|
3451
|
+
"token",
|
|
3452
|
+
"signingkey",
|
|
3453
|
+
"credentials",
|
|
3454
|
+
"authheader"
|
|
3455
|
+
],
|
|
3456
|
+
substrings: ["secretaccess", "accesskeysecret"]
|
|
3457
|
+
};
|
|
2898
3458
|
const REDACTED_SIMPLE_STRING = "[REDACTED]";
|
|
2899
3459
|
function snakeCaseToCamelCase(key) {
|
|
2900
3460
|
return key.replace(/_([a-zA-Z])/g, (_, ch) => ch.toUpperCase());
|
|
@@ -2906,8 +3466,20 @@ function keyVariantsForMatching(key) {
|
|
|
2906
3466
|
if (trimmed.includes("_")) out.add(snakeCaseToCamelCase(trimmed));
|
|
2907
3467
|
return [...out];
|
|
2908
3468
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
3469
|
+
function normalizePatterns(patterns) {
|
|
3470
|
+
const keysLower = /* @__PURE__ */ new Set();
|
|
3471
|
+
for (const key of patterns?.keys ?? []) keysLower.add(key.toLowerCase());
|
|
3472
|
+
return {
|
|
3473
|
+
keysLower,
|
|
3474
|
+
suffixesLower: [...patterns?.suffixes ?? []].map((s) => s.toLowerCase()),
|
|
3475
|
+
substringsLower: [...patterns?.substrings ?? []].map((s) => s.toLowerCase())
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
function hasAnyPatterns(patterns) {
|
|
3479
|
+
return patterns.keysLower.size > 0 || patterns.suffixesLower.length > 0 || patterns.substringsLower.length > 0;
|
|
3480
|
+
}
|
|
3481
|
+
function matchesNormalizedPatterns(key, patterns) {
|
|
3482
|
+
if (!hasAnyPatterns(patterns)) return false;
|
|
2911
3483
|
const variants = keyVariantsForMatching(key);
|
|
2912
3484
|
const compactLower = key.replace(/[\s._-]/g, "").toLowerCase();
|
|
2913
3485
|
const forms = new Set([compactLower]);
|
|
@@ -2917,9 +3489,9 @@ function isSensitiveStringKey(key) {
|
|
|
2917
3489
|
}
|
|
2918
3490
|
for (const form of forms) {
|
|
2919
3491
|
const fl = form.toLowerCase();
|
|
2920
|
-
if (
|
|
2921
|
-
for (const suffix of
|
|
2922
|
-
for (const substring of
|
|
3492
|
+
if (patterns.keysLower.has(fl)) return true;
|
|
3493
|
+
for (const suffix of patterns.suffixesLower) if (fl.endsWith(suffix)) return true;
|
|
3494
|
+
for (const substring of patterns.substringsLower) if (compactLower.includes(substring)) return true;
|
|
2923
3495
|
}
|
|
2924
3496
|
return false;
|
|
2925
3497
|
}
|
|
@@ -2931,7 +3503,7 @@ function isPlainSerializable(value) {
|
|
|
2931
3503
|
if (constructor && constructor.name !== "Object" && constructor.name !== "Array") return false;
|
|
2932
3504
|
return true;
|
|
2933
3505
|
}
|
|
2934
|
-
function redactInner(value, visiting, options) {
|
|
3506
|
+
function redactInner(value, visiting, options, patterns) {
|
|
2935
3507
|
if (value === null || value === void 0) return value;
|
|
2936
3508
|
if (typeof value === "function") return void 0;
|
|
2937
3509
|
if (typeof value !== "object") return value;
|
|
@@ -2939,17 +3511,17 @@ function redactInner(value, visiting, options) {
|
|
|
2939
3511
|
if (visiting.has(obj)) return void 0;
|
|
2940
3512
|
visiting.add(obj);
|
|
2941
3513
|
try {
|
|
2942
|
-
if (Array.isArray(value)) return value.map((item) => redactInner(item, visiting, options)).filter((item) => item !== void 0);
|
|
3514
|
+
if (Array.isArray(value)) return value.map((item) => redactInner(item, visiting, options, patterns)).filter((item) => item !== void 0);
|
|
2943
3515
|
const result = {};
|
|
2944
3516
|
for (const [key, val] of Object.entries(value)) {
|
|
2945
3517
|
if (options?.excludeKeys?.has(key)) continue;
|
|
2946
3518
|
if (typeof val === "function") continue;
|
|
2947
|
-
if (typeof val === "string" &&
|
|
3519
|
+
if (typeof val === "string" && matchesNormalizedPatterns(key, patterns) && !options?.ignoreKeys?.has(key)) {
|
|
2948
3520
|
result[key] = REDACTED_SIMPLE_STRING;
|
|
2949
3521
|
continue;
|
|
2950
3522
|
}
|
|
2951
3523
|
if (options?.skipNonPlainSerializable && val !== null && typeof val === "object" && !isPlainSerializable(val)) continue;
|
|
2952
|
-
const redacted = redactInner(val, visiting, options);
|
|
3524
|
+
const redacted = redactInner(val, visiting, options, patterns);
|
|
2953
3525
|
if (redacted !== void 0) result[key] = redacted;
|
|
2954
3526
|
}
|
|
2955
3527
|
return result;
|
|
@@ -2957,9 +3529,55 @@ function redactInner(value, visiting, options) {
|
|
|
2957
3529
|
visiting.delete(obj);
|
|
2958
3530
|
}
|
|
2959
3531
|
}
|
|
2960
|
-
/**
|
|
3532
|
+
/**
|
|
3533
|
+
* Recursively walk a JSON-like object tree.
|
|
3534
|
+
*
|
|
3535
|
+
* String values are redacted only when `patterns` is provided. With no patterns,
|
|
3536
|
+
* this only applies structural options (`excludeKeys`,
|
|
3537
|
+
* `skipNonPlainSerializable`, dropping functions).
|
|
3538
|
+
*/
|
|
2961
3539
|
function redact(value, options) {
|
|
2962
|
-
|
|
3540
|
+
const patterns = normalizePatterns(options?.patterns);
|
|
3541
|
+
return redactInner(value, /* @__PURE__ */ new WeakSet(), options, patterns);
|
|
3542
|
+
}
|
|
3543
|
+
const DASH_PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
3544
|
+
/** Storage-strategy enums that match sensitive key suffixes but are not secrets. */
|
|
3545
|
+
const DASH_PLUGIN_OPTIONS_IGNORE_KEYS = new Set([
|
|
3546
|
+
"storeApiKey",
|
|
3547
|
+
"storeClientSecret",
|
|
3548
|
+
"storeSCIMToken",
|
|
3549
|
+
"storeToken"
|
|
3550
|
+
]);
|
|
3551
|
+
function redactDashPluginOptions(pluginId, options, redactOptions) {
|
|
3552
|
+
return redact(options, {
|
|
3553
|
+
patterns: SENSITIVE_KEY_PATTERNS,
|
|
3554
|
+
...redactOptions,
|
|
3555
|
+
excludeKeys: DASH_PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId],
|
|
3556
|
+
ignoreKeys: DASH_PLUGIN_OPTIONS_IGNORE_KEYS
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Redact a dash settings payload: `emailAndPassword` and each plugin's `options`.
|
|
3561
|
+
*/
|
|
3562
|
+
function redactDashSettings(data, redactOptions) {
|
|
3563
|
+
if (data === null || typeof data !== "object") return data;
|
|
3564
|
+
const d = data;
|
|
3565
|
+
const next = { ...d };
|
|
3566
|
+
if ("emailAndPassword" in d) next.emailAndPassword = redact(d.emailAndPassword, {
|
|
3567
|
+
patterns: SENSITIVE_KEY_PATTERNS,
|
|
3568
|
+
...redactOptions
|
|
3569
|
+
});
|
|
3570
|
+
const plugins = d.plugins;
|
|
3571
|
+
if (Array.isArray(plugins)) next.plugins = plugins.map((plugin) => {
|
|
3572
|
+
if (plugin === null || typeof plugin !== "object") return plugin;
|
|
3573
|
+
const p = plugin;
|
|
3574
|
+
const id = typeof p.id === "string" ? p.id : "";
|
|
3575
|
+
return {
|
|
3576
|
+
...p,
|
|
3577
|
+
options: redactDashPluginOptions(id, p.options, redactOptions)
|
|
3578
|
+
};
|
|
3579
|
+
});
|
|
3580
|
+
return next;
|
|
2963
3581
|
}
|
|
2964
3582
|
//#endregion
|
|
2965
3583
|
//#region ../utils/dist/crypto/index.mjs
|
|
@@ -3128,13 +3746,61 @@ const jwtValidateMiddleware = (options) => {
|
|
|
3128
3746
|
});
|
|
3129
3747
|
};
|
|
3130
3748
|
//#endregion
|
|
3131
|
-
//#region
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3749
|
+
//#region ../utils/dist/semver.mjs
|
|
3750
|
+
/**
|
|
3751
|
+
* Parses a semver string into components.
|
|
3752
|
+
* Accepts `major.minor`, `major.minor.patch`, and optional prerelease (`-rc.2`).
|
|
3753
|
+
* Returns `null` when the input is missing or not a valid semver prefix.
|
|
3754
|
+
*/
|
|
3755
|
+
function parseSemver(version) {
|
|
3756
|
+
if (!version) return null;
|
|
3757
|
+
const match = version.trim().match(/^(\d+)\.(\d+)(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/);
|
|
3758
|
+
if (!match) return null;
|
|
3759
|
+
return {
|
|
3760
|
+
major: Number(match[1]),
|
|
3761
|
+
minor: Number(match[2]),
|
|
3762
|
+
patch: Number(match[3] ?? 0),
|
|
3763
|
+
prerelease: match[4]
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
function resolve(version) {
|
|
3767
|
+
if (typeof version === "string" || version == null) return parseSemver(version);
|
|
3768
|
+
return version;
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* True when `version`'s core identity (`major.minor.patch`) is ≥ `target`.
|
|
3772
|
+
* Prerelease on `version` is ignored (e.g. `1.7.0-rc.1` ≥ `1.7.0`).
|
|
3773
|
+
* `target` may be `major.minor` or `major.minor.patch` (missing patch → 0).
|
|
3774
|
+
*/
|
|
3775
|
+
function isSemverAtLeast(version, target) {
|
|
3776
|
+
const parsed = resolve(version);
|
|
3777
|
+
const targetParsed = parseSemver(target);
|
|
3778
|
+
if (!parsed || !targetParsed) return false;
|
|
3779
|
+
if (parsed.major !== targetParsed.major) return parsed.major > targetParsed.major;
|
|
3780
|
+
if (parsed.minor !== targetParsed.minor) return parsed.minor > targetParsed.minor;
|
|
3781
|
+
return parsed.patch >= targetParsed.patch;
|
|
3782
|
+
}
|
|
3783
|
+
//#endregion
|
|
3784
|
+
//#region src/semver.ts
|
|
3785
|
+
/** Compare semver strings (e.g. "1.7.0-beta.8" against "1.7" or "1.7.0"). */
|
|
3786
|
+
function isVersionAtLeast(version, target) {
|
|
3787
|
+
return isSemverAtLeast(version, target);
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* True for better-auth 1.7+ (including 1.8+).
|
|
3791
|
+
* Marks the cutover for issuer-scoped accounts, protocol-defined SSO subjects,
|
|
3792
|
+
* `advanced.database.joins`, and post-legacy SCIM connections.
|
|
3793
|
+
* Keep in sync with `apps/web/lib/better-auth-compat.ts`.
|
|
3794
|
+
*/
|
|
3795
|
+
function isBetterAuth17OrLater(version) {
|
|
3796
|
+
const parsed = parseSemver(version);
|
|
3797
|
+
if (!parsed) return false;
|
|
3798
|
+
if (isSemverAtLeast(parsed, "1.8.0")) return true;
|
|
3799
|
+
if (!isSemverAtLeast(parsed, "1.7.0")) return false;
|
|
3800
|
+
if (!parsed.prerelease || parsed.patch > 0) return true;
|
|
3801
|
+
const prereleaseMatch = parsed.prerelease.match(/^rc\.(\d+)/);
|
|
3802
|
+
if (prereleaseMatch) return Number(prereleaseMatch[1]) >= 4;
|
|
3803
|
+
return false;
|
|
3138
3804
|
}
|
|
3139
3805
|
//#endregion
|
|
3140
3806
|
//#region src/routes/auth/config.ts
|
|
@@ -3150,16 +3816,16 @@ const getConfig = (options) => {
|
|
|
3150
3816
|
}, async (ctx) => {
|
|
3151
3817
|
const advancedOptions = ctx.context.options.advanced;
|
|
3152
3818
|
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
3153
|
-
return {
|
|
3819
|
+
return redactDashSettings({
|
|
3154
3820
|
version: ctx.context.version || null,
|
|
3155
3821
|
socialProviders: Object.keys(ctx.context.options.socialProviders || {}),
|
|
3156
|
-
emailAndPassword:
|
|
3822
|
+
emailAndPassword: ctx.context.options.emailAndPassword,
|
|
3157
3823
|
plugins: ctx.context.options.plugins?.map((plugin) => {
|
|
3158
3824
|
const base = {
|
|
3159
3825
|
id: plugin.id,
|
|
3160
3826
|
schema: plugin.schema,
|
|
3161
3827
|
version: plugin.version,
|
|
3162
|
-
options:
|
|
3828
|
+
options: plugin.options
|
|
3163
3829
|
};
|
|
3164
3830
|
if (plugin.id === "dash" && !plugin.version) return {
|
|
3165
3831
|
...base,
|
|
@@ -3262,9 +3928,14 @@ const getConfig = (options) => {
|
|
|
3262
3928
|
secure: typeof ctx.context.options.advanced?.defaultCookieAttributes?.secure !== "undefined" ? ctx.context.options.advanced?.defaultCookieAttributes?.secure : null
|
|
3263
3929
|
} : null,
|
|
3264
3930
|
appName: ctx.context.options.appName || null,
|
|
3265
|
-
hasJoinsEnabled:
|
|
3931
|
+
hasJoinsEnabled: (() => {
|
|
3932
|
+
const advancedJoins = (ctx.context.options.advanced?.database)?.joins === true;
|
|
3933
|
+
if (isBetterAuth17OrLater(ctx.context.version)) return advancedJoins;
|
|
3934
|
+
return advancedJoins || ctx.context.options.experimental?.joins === true;
|
|
3935
|
+
})(),
|
|
3936
|
+
hasErrorURLConfigured: !!ctx.context.options.onAPIError?.errorURL
|
|
3266
3937
|
}
|
|
3267
|
-
};
|
|
3938
|
+
}, { skipNonPlainSerializable: true });
|
|
3268
3939
|
});
|
|
3269
3940
|
};
|
|
3270
3941
|
//#endregion
|
|
@@ -3279,58 +3950,948 @@ const getValidate = (options) => {
|
|
|
3279
3950
|
}, async () => {
|
|
3280
3951
|
return { valid: true };
|
|
3281
3952
|
});
|
|
3282
|
-
};
|
|
3283
|
-
//#endregion
|
|
3284
|
-
//#region src/
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3953
|
+
};
|
|
3954
|
+
//#endregion
|
|
3955
|
+
//#region src/routes/organization-guards.ts
|
|
3956
|
+
/** Returns true if organization plugin is enabled. */
|
|
3957
|
+
function isOrganizationEnabled(ctx) {
|
|
3958
|
+
return !!ctx.context.getPlugin("organization");
|
|
3959
|
+
}
|
|
3960
|
+
/** Returns the organization plugin, throws if not enabled. Use for write endpoints. */
|
|
3961
|
+
function requireOrganizationPlugin(ctx) {
|
|
3962
|
+
const plugin = ctx.context.getPlugin("organization");
|
|
3963
|
+
if (!plugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
|
|
3964
|
+
return plugin;
|
|
3965
|
+
}
|
|
3966
|
+
/** Returns true if organization plugin and teams feature are enabled. */
|
|
3967
|
+
function isTeamsEnabled(ctx) {
|
|
3968
|
+
return !!ctx.context.getPlugin("organization")?.options?.teams?.enabled;
|
|
3969
|
+
}
|
|
3970
|
+
/**
|
|
3971
|
+
* Validates that the organization plugin is enabled and teams feature is enabled.
|
|
3972
|
+
*
|
|
3973
|
+
* @returns The organization options for use in team logic (maximumTeams, hooks, etc.)
|
|
3974
|
+
*/
|
|
3975
|
+
function requireTeamsEnabled(ctx) {
|
|
3976
|
+
const orgOptions = requireOrganizationPlugin(ctx).options || {};
|
|
3977
|
+
if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
|
|
3978
|
+
return orgOptions;
|
|
3979
|
+
}
|
|
3980
|
+
//#endregion
|
|
3981
|
+
//#region src/routes/directory-sync/managed-catalog.ts
|
|
3982
|
+
function getRequestHeaders(ctx) {
|
|
3983
|
+
return ctx.request?.headers ?? new Headers();
|
|
3984
|
+
}
|
|
3985
|
+
function getManagedEndpoints(ctx) {
|
|
3986
|
+
const plugin = ctx.context.getPlugin("scim");
|
|
3987
|
+
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." });
|
|
3988
|
+
return plugin.endpoints;
|
|
3989
|
+
}
|
|
3990
|
+
function requireManagedEndpoint(ctx, endpoint, name) {
|
|
3991
|
+
if (!endpoint) throw ctx.error("BAD_REQUEST", { message: `SCIM managed connection operation "${name}" is unavailable. Publish and install the required Better Auth SCIM release.` });
|
|
3992
|
+
return endpoint;
|
|
3993
|
+
}
|
|
3994
|
+
function assertManagedConnectionLifecycleConfigured(ctx) {
|
|
3995
|
+
const endpoints = getManagedEndpoints(ctx);
|
|
3996
|
+
requireManagedEndpoint(ctx, endpoints.createSCIMManagedConnection, "create");
|
|
3997
|
+
requireManagedEndpoint(ctx, endpoints.listSCIMManagedConnections, "list");
|
|
3998
|
+
requireManagedEndpoint(ctx, endpoints.getSCIMManagedConnection, "get");
|
|
3999
|
+
requireManagedEndpoint(ctx, endpoints.rotateSCIMManagedCredential, "rotate");
|
|
4000
|
+
requireManagedEndpoint(ctx, endpoints.revokeSCIMManagedCredential, "revoke");
|
|
4001
|
+
requireManagedEndpoint(ctx, endpoints.decommissionSCIMManagedConnection, "decommission");
|
|
4002
|
+
return endpoints;
|
|
4003
|
+
}
|
|
4004
|
+
async function getManagedState(ctx, connection) {
|
|
4005
|
+
if (!connection.connectionId) return;
|
|
4006
|
+
return await requireManagedEndpoint(ctx, getManagedEndpoints(ctx).getSCIMManagedConnection, "get")({
|
|
4007
|
+
body: {
|
|
4008
|
+
connectionId: connection.connectionId,
|
|
4009
|
+
provisioningDomainId: connection.provisioningDomainId
|
|
4010
|
+
},
|
|
4011
|
+
context: ctx.context,
|
|
4012
|
+
headers: getRequestHeaders(ctx)
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
//#endregion
|
|
4016
|
+
//#region src/routes/directory-sync/mode.ts
|
|
4017
|
+
function getSCIMPlugin(ctx) {
|
|
4018
|
+
return ctx.context.getPlugin("scim");
|
|
4019
|
+
}
|
|
4020
|
+
function getDashPluginOptions(ctx) {
|
|
4021
|
+
return ctx.context.getPlugin("dash")?.options;
|
|
4022
|
+
}
|
|
4023
|
+
/** True when dash was configured with `managedDirectorySync: { enabled: true }`. */
|
|
4024
|
+
function isManagedDirectorySyncEnabled(ctx) {
|
|
4025
|
+
return getDashPluginOptions(ctx)?.managedDirectorySync?.enabled === true;
|
|
4026
|
+
}
|
|
4027
|
+
function usesLegacyScimDirectorySync(scimPlugin) {
|
|
4028
|
+
return typeof scimPlugin?.endpoints.generateSCIMToken === "function";
|
|
4029
|
+
}
|
|
4030
|
+
/** True when the SCIM plugin exposes the 1.7+ managed connection endpoints. */
|
|
4031
|
+
function hasManagedScimSurface(scimPlugin) {
|
|
4032
|
+
return typeof scimPlugin?.endpoints.createSCIMManagedConnection === "function" && typeof scimPlugin?.endpoints.listSCIMManagedConnections === "function";
|
|
4033
|
+
}
|
|
4034
|
+
function usesManagedScimDirectorySync(scimPlugin) {
|
|
4035
|
+
return hasManagedScimSurface(scimPlugin) && scimPlugin?.options?.managedConnections != null;
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Resolve which directory-sync experience the dashboard should use.
|
|
4039
|
+
* Managed mode also requires dash `managedDirectorySync.enabled`.
|
|
4040
|
+
* Legacy SCIM mode ignores that option.
|
|
4041
|
+
*/
|
|
4042
|
+
function resolveDirectorySyncMode(scimPlugin, managedDirectorySyncEnabled = false) {
|
|
4043
|
+
if (!scimPlugin) return "unavailable";
|
|
4044
|
+
if (hasManagedScimSurface(scimPlugin)) return usesManagedScimDirectorySync(scimPlugin) && managedDirectorySyncEnabled ? "managed" : "unavailable";
|
|
4045
|
+
if (usesLegacyScimDirectorySync(scimPlugin)) {
|
|
4046
|
+
const ownership = scimPlugin.options?.providerOwnership;
|
|
4047
|
+
if (ownership != null && ownership.enabled !== true) return "unavailable";
|
|
4048
|
+
return "legacy";
|
|
4049
|
+
}
|
|
4050
|
+
return "unavailable";
|
|
4051
|
+
}
|
|
4052
|
+
function assertManagedDirectorySyncEnabled(ctx) {
|
|
4053
|
+
if (isManagedDirectorySyncEnabled(ctx)) return;
|
|
4054
|
+
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." });
|
|
4055
|
+
}
|
|
4056
|
+
//#endregion
|
|
4057
|
+
//#region src/routes/directory-sync/route-contract.ts
|
|
4058
|
+
const DIRECTORY_SYNC_CREDENTIAL_LIFETIME_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
4059
|
+
const directorySyncClaimsSchema = z$1.object({
|
|
4060
|
+
purpose: z$1.literal(DIRECTORY_SYNC_PURPOSE),
|
|
4061
|
+
organizationId: z$1.string().trim().min(1),
|
|
4062
|
+
actorId: z$1.string().trim().min(1),
|
|
4063
|
+
setupOperationId: z$1.string().trim().min(16).max(255).optional()
|
|
4064
|
+
});
|
|
4065
|
+
function getScimEndpoint$1(baseUrl) {
|
|
4066
|
+
return `${baseUrl}/scim/v2`;
|
|
4067
|
+
}
|
|
4068
|
+
function assertDirectorySyncClaims(ctx, organizationId) {
|
|
4069
|
+
const claims = ctx.context.payload;
|
|
4070
|
+
if (claims.purpose !== "directory-sync-management" || claims.organizationId !== organizationId || !claims.actorId) throw ctx.error("UNAUTHORIZED", { message: "Invalid directory sync management authorization" });
|
|
4071
|
+
return claims;
|
|
4072
|
+
}
|
|
4073
|
+
function assertDirectorySyncManagementClaims(ctx, organizationId) {
|
|
4074
|
+
const claims = assertDirectorySyncClaims(ctx, organizationId);
|
|
4075
|
+
if (claims.setupOperationId) throw ctx.error("FORBIDDEN", { message: "Directory sync setup authorization cannot perform management operations" });
|
|
4076
|
+
const { setupOperationId: _, ...managementClaims } = claims;
|
|
4077
|
+
return managementClaims;
|
|
4078
|
+
}
|
|
4079
|
+
async function assertTargetOrganizationExists(ctx, organizationId) {
|
|
4080
|
+
if (!await ctx.context.adapter.findOne({
|
|
4081
|
+
model: "organization",
|
|
4082
|
+
where: [{
|
|
4083
|
+
field: "id",
|
|
4084
|
+
value: organizationId
|
|
4085
|
+
}],
|
|
4086
|
+
select: ["id"]
|
|
4087
|
+
})) throw ctx.error("NOT_FOUND", { message: "Target organization not found" });
|
|
4088
|
+
}
|
|
4089
|
+
function resolveCredentialPolicy(input) {
|
|
4090
|
+
const expiresAt = input.expiresAt ?? new Date(Date.now() + DIRECTORY_SYNC_CREDENTIAL_LIFETIME_MS);
|
|
4091
|
+
if (expiresAt.getTime() <= Date.now()) throw new APIError$1("BAD_REQUEST", { message: "Directory sync credential expiry must be in the future" });
|
|
4092
|
+
const scopes = input.scopes ?? ALL_SCIM_SCOPES;
|
|
4093
|
+
if (new Set(scopes).size !== scopes.length) throw new APIError$1("BAD_REQUEST", { message: "Directory sync credential scopes must be unique" });
|
|
4094
|
+
return {
|
|
4095
|
+
scopes,
|
|
4096
|
+
expiresAt
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
function serializeDate(value) {
|
|
4100
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
4101
|
+
}
|
|
4102
|
+
function serializeNullableDate(value) {
|
|
4103
|
+
return value == null ? null : serializeDate(value);
|
|
4104
|
+
}
|
|
4105
|
+
function serializeCredential(credential) {
|
|
4106
|
+
return {
|
|
4107
|
+
credentialId: credential.credentialId,
|
|
4108
|
+
status: credential.status,
|
|
4109
|
+
scopes: credential.scopes,
|
|
4110
|
+
expiresAt: serializeDate(credential.expiresAt),
|
|
4111
|
+
createdAt: serializeDate(credential.createdAt),
|
|
4112
|
+
createdBy: credential.createdBy,
|
|
4113
|
+
lastUsedAt: serializeNullableDate(credential.lastUsedAt),
|
|
4114
|
+
revokedAt: serializeNullableDate(credential.revokedAt),
|
|
4115
|
+
revokedBy: credential.revokedBy
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
function serializeDirectory(row, scimEndpoint, state) {
|
|
4119
|
+
return {
|
|
4120
|
+
connectionId: row.connectionId ?? null,
|
|
4121
|
+
organizationId: row.organizationId,
|
|
4122
|
+
providerId: row.providerId,
|
|
4123
|
+
provisioningDomainId: row.provisioningDomainId,
|
|
4124
|
+
status: row.status,
|
|
4125
|
+
scimEndpoint,
|
|
4126
|
+
credentials: state?.credentials.map(serializeCredential) ?? [],
|
|
4127
|
+
createdAt: serializeDate(row.createdAt),
|
|
4128
|
+
updatedAt: serializeDate(row.updatedAt),
|
|
4129
|
+
pairing: serializePairingRow(row),
|
|
4130
|
+
pairingEnforced: row.pairingEnforced === true,
|
|
4131
|
+
unpairedAt: serializeNullableDate(row.unpairedAt ?? null),
|
|
4132
|
+
unpairedBy: row.unpairedBy ?? null,
|
|
4133
|
+
decommissionedAt: serializeNullableDate(row.decommissionedAt ?? null)
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
//#endregion
|
|
4137
|
+
//#region src/routes/directory-sync/transaction.ts
|
|
4138
|
+
/**
|
|
4139
|
+
* `better-auth` and `@better-auth/core` can resolve to distinct physical
|
|
4140
|
+
* copies of the same version. Endpoint adapters are typed from the former;
|
|
4141
|
+
* `runWithTransaction` / `getCurrentAdapter` are typed from the latter.
|
|
4142
|
+
*/
|
|
4143
|
+
function runWithTransaction$1(adapter, fn) {
|
|
4144
|
+
return runWithTransaction(adapter, fn);
|
|
4145
|
+
}
|
|
4146
|
+
function getCurrentAdapter$1(adapter) {
|
|
4147
|
+
return getCurrentAdapter(adapter);
|
|
4148
|
+
}
|
|
4149
|
+
//#endregion
|
|
4150
|
+
//#region src/routes/directory-sync/reservation.ts
|
|
4151
|
+
function withTransactionAdapter(ctx, adapter) {
|
|
4152
|
+
return {
|
|
4153
|
+
...ctx,
|
|
4154
|
+
context: {
|
|
4155
|
+
...ctx.context,
|
|
4156
|
+
adapter
|
|
4157
|
+
}
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
async function createAliasKey(organizationId, providerId) {
|
|
4161
|
+
return `directory-sync-alias:${await hash$1(JSON.stringify([organizationId, providerId]))}`;
|
|
4162
|
+
}
|
|
4163
|
+
async function createProvisioningDomainId(organizationId, providerId) {
|
|
4164
|
+
return `dash_scim_domain_${await hash$1(JSON.stringify({
|
|
4165
|
+
purpose: DIRECTORY_SYNC_PURPOSE,
|
|
4166
|
+
organizationId,
|
|
4167
|
+
providerId
|
|
4168
|
+
}))}`;
|
|
4169
|
+
}
|
|
4170
|
+
async function createActiveOrganizationKey(organizationId) {
|
|
4171
|
+
return `directory-sync-active:${await hash$1(organizationId)}`;
|
|
4172
|
+
}
|
|
4173
|
+
function createInactiveOrganizationKey(aliasKey) {
|
|
4174
|
+
return `directory-sync-inactive:${aliasKey}`;
|
|
4175
|
+
}
|
|
4176
|
+
function createCreationRequestId() {
|
|
4177
|
+
return generateRandomString(32, "a-z", "A-Z", "0-9", "-_");
|
|
4178
|
+
}
|
|
4179
|
+
function isUniqueConstraintError(error) {
|
|
4180
|
+
return error instanceof Error && /unique|duplicate|constraint|P2002/i.test(error.message);
|
|
4181
|
+
}
|
|
4182
|
+
async function assertManagedDirectoryTransactionsConfigured(ctx) {
|
|
4183
|
+
if (typeof ctx.context.adapter.options?.adapterConfig.transaction !== "function") throw ctx.error("NOT_IMPLEMENTED", {
|
|
4184
|
+
code: "DIRECTORY_SYNC_REQUIRES_NATIVE_TRANSACTIONS",
|
|
4185
|
+
message: "Managed directory sync requires a database adapter with native transaction support"
|
|
4186
|
+
});
|
|
4187
|
+
try {
|
|
4188
|
+
await getCurrentDBAdapterAsyncLocalStorage();
|
|
4189
|
+
} catch {
|
|
4190
|
+
throw ctx.error("NOT_IMPLEMENTED", {
|
|
4191
|
+
code: "DIRECTORY_SYNC_REQUIRES_ASYNC_CONTEXT",
|
|
4192
|
+
message: "Managed directory sync requires database transaction async context support"
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
async function assertDirectorySyncSSOIntegrationConfigured(ctx) {
|
|
4197
|
+
const ssoPlugin = ctx.context.getPlugin("sso");
|
|
4198
|
+
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." });
|
|
4199
|
+
await assertManagedDirectoryTransactionsConfigured(ctx);
|
|
4200
|
+
}
|
|
4201
|
+
async function findDirectorySyncConnection(ctx, organizationId, providerId) {
|
|
4202
|
+
return await (await getCurrentAdapter$1(ctx.context.adapter)).findOne({
|
|
4203
|
+
model: "directorySyncConnection",
|
|
4204
|
+
where: [{
|
|
4205
|
+
field: "organizationId",
|
|
4206
|
+
value: organizationId
|
|
4207
|
+
}, {
|
|
4208
|
+
field: "providerId",
|
|
4209
|
+
value: providerId
|
|
4210
|
+
}]
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
async function getDirectorySyncConnection(ctx, organizationId, providerId) {
|
|
4214
|
+
const row = await findDirectorySyncConnection(ctx, organizationId, providerId);
|
|
4215
|
+
if (!row) throw ctx.error("NOT_FOUND", { message: "Directory sync connection not found" });
|
|
4216
|
+
return row;
|
|
4217
|
+
}
|
|
4218
|
+
function isExactManagedConnection(issued, input) {
|
|
4219
|
+
return issued.connection.creationRequestId === input.creationRequestId && issued.connection.provisioningDomainId === input.provisioningDomainId;
|
|
4220
|
+
}
|
|
4221
|
+
async function lockExactPairedSSOProvider(adapter, row) {
|
|
4222
|
+
if (!row.pairingEnforced) return;
|
|
4223
|
+
if (!row.ssoProviderRecordId || !row.ssoProviderId) throw new Error("Enforced directory sync pairing is incomplete");
|
|
4224
|
+
if (!await adapter.update({
|
|
4225
|
+
model: "ssoProvider",
|
|
4226
|
+
where: [
|
|
4227
|
+
{
|
|
4228
|
+
field: "id",
|
|
4229
|
+
value: row.ssoProviderRecordId
|
|
4230
|
+
},
|
|
4231
|
+
{
|
|
4232
|
+
field: "providerId",
|
|
4233
|
+
value: row.ssoProviderId
|
|
4234
|
+
},
|
|
4235
|
+
{
|
|
4236
|
+
field: "organizationId",
|
|
4237
|
+
value: row.organizationId
|
|
4238
|
+
}
|
|
4239
|
+
],
|
|
4240
|
+
update: { providerId: row.ssoProviderId }
|
|
4241
|
+
})) throw new Error("Paired SSO provider changed during directory sync");
|
|
4242
|
+
}
|
|
4243
|
+
async function recoverManagedDirectoryConnection(ctx, row, input, policy) {
|
|
4244
|
+
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" });
|
|
4245
|
+
const connectionId = row.connectionId;
|
|
4246
|
+
const serializedPairing = input.pairing ? JSON.stringify(input.pairing) : null;
|
|
4247
|
+
if ((row.serializedSsoPairing ?? null) !== serializedPairing) throw ctx.error("CONFLICT", { message: "Directory sync setup recovery must use the original SSO pairing" });
|
|
4248
|
+
const endpoints = getManagedEndpoints(ctx);
|
|
4249
|
+
const getManagedConnection = requireManagedEndpoint(ctx, endpoints.getSCIMManagedConnection, "get");
|
|
4250
|
+
const revokeManagedCredential = requireManagedEndpoint(ctx, endpoints.revokeSCIMManagedCredential, "revoke");
|
|
4251
|
+
const rotateManagedCredential = requireManagedEndpoint(ctx, endpoints.rotateSCIMManagedCredential, "rotate");
|
|
4252
|
+
return await runWithTransaction$1(ctx.context.adapter, async () => {
|
|
4253
|
+
const database = await getCurrentAdapter$1(ctx.context.adapter);
|
|
4254
|
+
await lockExactPairedSSOProvider(database, row);
|
|
4255
|
+
const locked = await database.update({
|
|
4256
|
+
model: "directorySyncConnection",
|
|
4257
|
+
where: [
|
|
4258
|
+
{
|
|
4259
|
+
field: "id",
|
|
4260
|
+
value: row.id
|
|
4261
|
+
},
|
|
4262
|
+
{
|
|
4263
|
+
field: "status",
|
|
4264
|
+
value: "active"
|
|
4265
|
+
},
|
|
4266
|
+
{
|
|
4267
|
+
field: "revision",
|
|
4268
|
+
value: row.revision
|
|
4269
|
+
},
|
|
4270
|
+
{
|
|
4271
|
+
field: "connectionId",
|
|
4272
|
+
value: connectionId
|
|
4273
|
+
},
|
|
4274
|
+
{
|
|
4275
|
+
field: "creationRequestId",
|
|
4276
|
+
value: input.creationRequestId
|
|
4277
|
+
}
|
|
4278
|
+
],
|
|
4279
|
+
update: {
|
|
4280
|
+
revision: row.revision + 1,
|
|
4281
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
4282
|
+
lastActorId: input.actorId,
|
|
4283
|
+
lastError: null
|
|
4284
|
+
}
|
|
4285
|
+
});
|
|
4286
|
+
if (!locked) throw ctx.error("CONFLICT", { message: "Directory sync connection changed before setup recovery started" });
|
|
4287
|
+
const state = await getManagedConnection({
|
|
4288
|
+
body: {
|
|
4289
|
+
connectionId,
|
|
4290
|
+
provisioningDomainId: row.provisioningDomainId
|
|
4291
|
+
},
|
|
4292
|
+
context: ctx.context,
|
|
4293
|
+
headers: getRequestHeaders(ctx)
|
|
4294
|
+
});
|
|
4295
|
+
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" });
|
|
4296
|
+
for (const credential of state.credentials) {
|
|
4297
|
+
if (credential.status !== "active") continue;
|
|
4298
|
+
await revokeManagedCredential({
|
|
4299
|
+
body: {
|
|
4300
|
+
connectionId,
|
|
4301
|
+
provisioningDomainId: row.provisioningDomainId,
|
|
4302
|
+
credentialId: credential.credentialId,
|
|
4303
|
+
actorId: input.actorId
|
|
4304
|
+
},
|
|
4305
|
+
context: ctx.context,
|
|
4306
|
+
headers: getRequestHeaders(ctx)
|
|
4307
|
+
});
|
|
4308
|
+
}
|
|
4309
|
+
const issued = await rotateManagedCredential({
|
|
4310
|
+
body: {
|
|
4311
|
+
connectionId,
|
|
4312
|
+
provisioningDomainId: row.provisioningDomainId,
|
|
4313
|
+
actorId: input.actorId,
|
|
4314
|
+
...policy
|
|
4315
|
+
},
|
|
4316
|
+
context: ctx.context,
|
|
4317
|
+
headers: getRequestHeaders(ctx)
|
|
4318
|
+
});
|
|
4319
|
+
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" });
|
|
4320
|
+
return {
|
|
4321
|
+
row: locked,
|
|
4322
|
+
issued
|
|
4323
|
+
};
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4326
|
+
async function createManagedDirectoryConnection(ctx, input, policy) {
|
|
4327
|
+
await assertManagedDirectoryTransactionsConfigured(ctx);
|
|
4328
|
+
if (input.pairing) await assertDirectorySyncSSOIntegrationConfigured(ctx);
|
|
4329
|
+
if (input.pairing?.protocol === "saml") try {
|
|
4330
|
+
await loadSAMLPolicy();
|
|
4331
|
+
} catch {
|
|
4332
|
+
throw ctx.error("NOT_IMPLEMENTED", {
|
|
4333
|
+
code: "DIRECTORY_SYNC_SAML_POLICY_UNAVAILABLE",
|
|
4334
|
+
message: "SAML pairing requires a compatible SSO package with service provider metadata policy support"
|
|
4335
|
+
});
|
|
4336
|
+
}
|
|
4337
|
+
const aliasKey = await createAliasKey(input.organizationId, input.providerId);
|
|
4338
|
+
const provisioningDomainId = await createProvisioningDomainId(input.organizationId, input.providerId);
|
|
4339
|
+
const activeOrganizationKey = await createActiveOrganizationKey(input.organizationId);
|
|
4340
|
+
const creationRequestId = input.creationRequestId ?? createCreationRequestId();
|
|
4341
|
+
const createManagedConnection = requireManagedEndpoint(ctx, getManagedEndpoints(ctx).createSCIMManagedConnection, "create");
|
|
4342
|
+
try {
|
|
4343
|
+
return await runWithTransaction$1(ctx.context.adapter, async () => {
|
|
4344
|
+
const database = await getCurrentAdapter$1(ctx.context.adapter);
|
|
4345
|
+
const transactionContext = withTransactionAdapter(ctx, database);
|
|
4346
|
+
const pairing = input.pairing ? await resolveDirectorySyncSSOPairing(transactionContext, input.organizationId, input.pairing) : null;
|
|
4347
|
+
const now = /* @__PURE__ */ new Date();
|
|
4348
|
+
const row = await database.create({
|
|
4349
|
+
model: "directorySyncConnection",
|
|
4350
|
+
data: {
|
|
4351
|
+
organizationId: input.organizationId,
|
|
4352
|
+
providerId: input.providerId,
|
|
4353
|
+
aliasKey,
|
|
4354
|
+
provisioningDomainId,
|
|
4355
|
+
activeOrganizationKey,
|
|
4356
|
+
creationRequestId,
|
|
4357
|
+
status: "active",
|
|
4358
|
+
revision: 0,
|
|
4359
|
+
createdAt: now,
|
|
4360
|
+
createdByActorId: input.actorId,
|
|
4361
|
+
updatedAt: now,
|
|
4362
|
+
lastActorId: input.actorId,
|
|
4363
|
+
activeSsoProviderKey: pairing?.activeSsoProviderKey ?? createInactiveSSOProviderKey(aliasKey),
|
|
4364
|
+
pairingEnforced: pairing != null,
|
|
4365
|
+
...pairing ? {
|
|
4366
|
+
ssoProviderId: pairing.ssoProviderId,
|
|
4367
|
+
ssoProviderRecordId: pairing.ssoProviderRecordId,
|
|
4368
|
+
serializedSsoPairing: JSON.stringify(pairing.pairing)
|
|
4369
|
+
} : {}
|
|
4370
|
+
}
|
|
4371
|
+
});
|
|
4372
|
+
const issued = await createManagedConnection({
|
|
4373
|
+
body: {
|
|
4374
|
+
creationRequestId,
|
|
4375
|
+
provisioningDomainId,
|
|
4376
|
+
actorId: input.actorId,
|
|
4377
|
+
...policy
|
|
4378
|
+
},
|
|
4379
|
+
context: ctx.context,
|
|
4380
|
+
headers: getRequestHeaders(ctx)
|
|
4381
|
+
});
|
|
4382
|
+
if (!isExactManagedConnection(issued, {
|
|
4383
|
+
creationRequestId,
|
|
4384
|
+
provisioningDomainId
|
|
4385
|
+
})) throw ctx.error("CONFLICT", { message: "Managed connection creation returned mismatched ownership correlation" });
|
|
4386
|
+
await lockExactPairedSSOProvider(database, row);
|
|
4387
|
+
const bound = await database.update({
|
|
4388
|
+
model: "directorySyncConnection",
|
|
4389
|
+
where: [
|
|
4390
|
+
{
|
|
4391
|
+
field: "id",
|
|
4392
|
+
value: row.id
|
|
4393
|
+
},
|
|
4394
|
+
{
|
|
4395
|
+
field: "status",
|
|
4396
|
+
value: "active"
|
|
4397
|
+
},
|
|
4398
|
+
{
|
|
4399
|
+
field: "revision",
|
|
4400
|
+
value: 0
|
|
4401
|
+
},
|
|
4402
|
+
{
|
|
4403
|
+
field: "connectionId",
|
|
4404
|
+
value: null
|
|
4405
|
+
}
|
|
4406
|
+
],
|
|
4407
|
+
update: {
|
|
4408
|
+
connectionId: issued.connection.connectionId,
|
|
4409
|
+
revision: 1,
|
|
4410
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4411
|
+
}
|
|
4412
|
+
});
|
|
4413
|
+
if (!bound) throw ctx.error("CONFLICT", { message: "Directory sync connection changed before catalog binding completed" });
|
|
4414
|
+
await supersedeTerminalDirectorySyncPairings(transactionContext, bound);
|
|
4415
|
+
return {
|
|
4416
|
+
row: bound,
|
|
4417
|
+
issued
|
|
4418
|
+
};
|
|
4419
|
+
});
|
|
4420
|
+
} catch (error) {
|
|
4421
|
+
if (isUniqueConstraintError(error)) {
|
|
4422
|
+
const aliasConflict = await ctx.context.adapter.findOne({
|
|
4423
|
+
model: "directorySyncConnection",
|
|
4424
|
+
where: [{
|
|
4425
|
+
field: "aliasKey",
|
|
4426
|
+
value: aliasKey
|
|
4427
|
+
}]
|
|
4428
|
+
});
|
|
4429
|
+
if (aliasConflict) {
|
|
4430
|
+
if (input.creationRequestId && aliasConflict.creationRequestId === input.creationRequestId && aliasConflict.organizationId === input.organizationId && aliasConflict.providerId === input.providerId && aliasConflict.provisioningDomainId === provisioningDomainId) return await recoverManagedDirectoryConnection(ctx, aliasConflict, {
|
|
4431
|
+
...input,
|
|
4432
|
+
creationRequestId: input.creationRequestId
|
|
4433
|
+
}, policy);
|
|
4434
|
+
throw ctx.error("CONFLICT", { message: "This organization already has a directory sync connection for the requested provider" });
|
|
4435
|
+
}
|
|
4436
|
+
if (await ctx.context.adapter.findOne({
|
|
4437
|
+
model: "directorySyncConnection",
|
|
4438
|
+
where: [{
|
|
4439
|
+
field: "activeOrganizationKey",
|
|
4440
|
+
value: activeOrganizationKey
|
|
4441
|
+
}]
|
|
4442
|
+
})) throw ctx.error("CONFLICT", { message: "This organization already has an active directory sync connection" });
|
|
4443
|
+
throw error;
|
|
4444
|
+
}
|
|
4445
|
+
throw error;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
function createTerminalOrganizationKey(aliasKey) {
|
|
4449
|
+
return createInactiveOrganizationKey(aliasKey);
|
|
4450
|
+
}
|
|
4451
|
+
function createTerminalPairingKey(aliasKey) {
|
|
4452
|
+
return createTerminalSSOProviderKey(aliasKey);
|
|
4453
|
+
}
|
|
4454
|
+
async function supersedeTerminalDirectorySyncPairings(ctx, activeRow) {
|
|
4455
|
+
if (!activeRow.ssoProviderRecordId || !activeRow.pairingEnforced) return;
|
|
4456
|
+
const terminalRows = await ctx.context.adapter.findMany({
|
|
4457
|
+
model: "directorySyncConnection",
|
|
4458
|
+
where: [{
|
|
4459
|
+
field: "ssoProviderRecordId",
|
|
4460
|
+
value: activeRow.ssoProviderRecordId
|
|
4461
|
+
}, {
|
|
4462
|
+
field: "pairingEnforced",
|
|
4463
|
+
value: true
|
|
4464
|
+
}]
|
|
4465
|
+
});
|
|
4466
|
+
const now = /* @__PURE__ */ new Date();
|
|
4467
|
+
for (const row of terminalRows) {
|
|
4468
|
+
if (row.id === activeRow.id || row.status !== "decommissioned") continue;
|
|
4469
|
+
if (!await ctx.context.adapter.update({
|
|
4470
|
+
model: "directorySyncConnection",
|
|
4471
|
+
where: [
|
|
4472
|
+
{
|
|
4473
|
+
field: "id",
|
|
4474
|
+
value: row.id
|
|
4475
|
+
},
|
|
4476
|
+
{
|
|
4477
|
+
field: "status",
|
|
4478
|
+
value: "decommissioned"
|
|
4479
|
+
},
|
|
4480
|
+
{
|
|
4481
|
+
field: "revision",
|
|
4482
|
+
value: row.revision
|
|
4483
|
+
},
|
|
4484
|
+
{
|
|
4485
|
+
field: "pairingEnforced",
|
|
4486
|
+
value: true
|
|
4487
|
+
}
|
|
4488
|
+
],
|
|
4489
|
+
update: {
|
|
4490
|
+
pairingEnforced: false,
|
|
4491
|
+
revision: row.revision + 1,
|
|
4492
|
+
unpairedAt: now,
|
|
4493
|
+
unpairedBy: activeRow.lastActorId,
|
|
4494
|
+
updatedAt: now,
|
|
4495
|
+
lastActorId: activeRow.lastActorId
|
|
4496
|
+
}
|
|
4497
|
+
})) throw ctx.error("CONFLICT", { message: "Directory sync SSO pairing changed before replacement activation" });
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
async function startDirectorySyncDecommission(ctx, row, actorId) {
|
|
4501
|
+
if (row.status !== "active") return row;
|
|
4502
|
+
await assertManagedDirectoryTransactionsConfigured(ctx);
|
|
4503
|
+
return await runWithTransaction$1(ctx.context.adapter, async () => {
|
|
4504
|
+
const database = await getCurrentAdapter$1(ctx.context.adapter);
|
|
4505
|
+
await lockExactPairedSSOProvider(database, row);
|
|
4506
|
+
const updated = await database.update({
|
|
4507
|
+
model: "directorySyncConnection",
|
|
4508
|
+
where: [
|
|
4509
|
+
{
|
|
4510
|
+
field: "id",
|
|
4511
|
+
value: row.id
|
|
4512
|
+
},
|
|
4513
|
+
{
|
|
4514
|
+
field: "status",
|
|
4515
|
+
value: "active"
|
|
4516
|
+
},
|
|
4517
|
+
{
|
|
4518
|
+
field: "revision",
|
|
4519
|
+
value: row.revision
|
|
4520
|
+
}
|
|
4521
|
+
],
|
|
4522
|
+
update: {
|
|
4523
|
+
status: "decommissioning",
|
|
4524
|
+
revision: row.revision + 1,
|
|
4525
|
+
decommissionStartedAt: /* @__PURE__ */ new Date(),
|
|
4526
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
4527
|
+
lastActorId: actorId
|
|
4528
|
+
}
|
|
4529
|
+
});
|
|
4530
|
+
if (!updated) throw ctx.error("CONFLICT", { message: "Directory sync connection changed while decommissioning started" });
|
|
4531
|
+
return updated;
|
|
4532
|
+
});
|
|
3294
4533
|
}
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
4534
|
+
async function unpairTerminalDirectorySyncConnection(ctx, row, actorId) {
|
|
4535
|
+
if (row.status !== "decommissioned") throw ctx.error("CONFLICT", { message: "Directory sync can only be unpaired after it has decommissioned" });
|
|
4536
|
+
if (!row.pairingEnforced) return row;
|
|
4537
|
+
await assertManagedDirectoryTransactionsConfigured(ctx);
|
|
4538
|
+
return await runWithTransaction$1(ctx.context.adapter, async () => {
|
|
4539
|
+
const database = await getCurrentAdapter$1(ctx.context.adapter);
|
|
4540
|
+
await lockExactPairedSSOProvider(database, row);
|
|
4541
|
+
const now = /* @__PURE__ */ new Date();
|
|
4542
|
+
const updated = await database.update({
|
|
4543
|
+
model: "directorySyncConnection",
|
|
4544
|
+
where: [
|
|
4545
|
+
{
|
|
4546
|
+
field: "id",
|
|
4547
|
+
value: row.id
|
|
4548
|
+
},
|
|
4549
|
+
{
|
|
4550
|
+
field: "status",
|
|
4551
|
+
value: "decommissioned"
|
|
4552
|
+
},
|
|
4553
|
+
{
|
|
4554
|
+
field: "revision",
|
|
4555
|
+
value: row.revision
|
|
4556
|
+
},
|
|
4557
|
+
{
|
|
4558
|
+
field: "pairingEnforced",
|
|
4559
|
+
value: true
|
|
4560
|
+
}
|
|
4561
|
+
],
|
|
4562
|
+
update: {
|
|
4563
|
+
activeSsoProviderKey: createTerminalSSOProviderKey(row.aliasKey),
|
|
4564
|
+
pairingEnforced: false,
|
|
4565
|
+
revision: row.revision + 1,
|
|
4566
|
+
unpairedAt: now,
|
|
4567
|
+
unpairedBy: actorId,
|
|
4568
|
+
updatedAt: now,
|
|
4569
|
+
lastActorId: actorId
|
|
4570
|
+
}
|
|
4571
|
+
});
|
|
4572
|
+
if (!updated) throw ctx.error("CONFLICT", { message: "Directory sync SSO pairing changed before it was unpaired" });
|
|
4573
|
+
return updated;
|
|
4574
|
+
});
|
|
3307
4575
|
}
|
|
3308
4576
|
//#endregion
|
|
3309
|
-
//#region src/routes/
|
|
3310
|
-
/**
|
|
3311
|
-
function
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
4577
|
+
//#region src/routes/directory-sync/managed-directories.ts
|
|
4578
|
+
/** Shared list handler for mode-dispatch on GET /:id/directories. */
|
|
4579
|
+
async function listOrganizationDirectoriesManagedHandler(ctx) {
|
|
4580
|
+
requireOrganizationPlugin(ctx);
|
|
4581
|
+
assertManagedDirectorySyncEnabled(ctx);
|
|
4582
|
+
const organizationId = tryDecode(ctx.params?.id ?? "");
|
|
4583
|
+
assertDirectorySyncManagementClaims(ctx, organizationId);
|
|
4584
|
+
await assertTargetOrganizationExists(ctx, organizationId);
|
|
4585
|
+
const rows = await ctx.context.adapter.findMany({
|
|
4586
|
+
model: "directorySyncConnection",
|
|
4587
|
+
where: [{
|
|
4588
|
+
field: "organizationId",
|
|
4589
|
+
value: organizationId
|
|
4590
|
+
}],
|
|
4591
|
+
sortBy: {
|
|
4592
|
+
field: "createdAt",
|
|
4593
|
+
direction: "desc"
|
|
4594
|
+
}
|
|
4595
|
+
});
|
|
4596
|
+
return await Promise.all(rows.map(async (row) => serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), await getManagedState(ctx, row))));
|
|
3323
4597
|
}
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
const
|
|
3331
|
-
|
|
3332
|
-
|
|
4598
|
+
const getOrganizationDirectory = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId", {
|
|
4599
|
+
method: "GET",
|
|
4600
|
+
use: [jwtMiddleware(options, directorySyncClaimsSchema)]
|
|
4601
|
+
}, async (ctx) => {
|
|
4602
|
+
requireOrganizationPlugin(ctx);
|
|
4603
|
+
assertManagedDirectorySyncEnabled(ctx);
|
|
4604
|
+
const organizationId = tryDecode(ctx.params.id);
|
|
4605
|
+
assertDirectorySyncManagementClaims(ctx, organizationId);
|
|
4606
|
+
await assertTargetOrganizationExists(ctx, organizationId);
|
|
4607
|
+
const row = await getDirectorySyncConnection(ctx, organizationId, tryDecode(ctx.params.providerId));
|
|
4608
|
+
return serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), await getManagedState(ctx, row));
|
|
4609
|
+
});
|
|
4610
|
+
const createOrganizationDirectory = (options) => createAuthEndpoint("/dash/organization/:id/directories", {
|
|
4611
|
+
method: "POST",
|
|
4612
|
+
use: [jwtMiddleware(options, directorySyncClaimsSchema)],
|
|
4613
|
+
body: createDirectoryBodySchema,
|
|
4614
|
+
metadata: { noStore: true }
|
|
4615
|
+
}, async (ctx) => {
|
|
4616
|
+
requireOrganizationPlugin(ctx);
|
|
4617
|
+
assertManagedDirectorySyncEnabled(ctx);
|
|
4618
|
+
const organizationId = tryDecode(ctx.params.id);
|
|
4619
|
+
const claims = assertDirectorySyncClaims(ctx, organizationId);
|
|
4620
|
+
await assertTargetOrganizationExists(ctx, organizationId);
|
|
4621
|
+
const policy = resolveCredentialPolicy(ctx.body);
|
|
4622
|
+
assertManagedConnectionLifecycleConfigured(ctx);
|
|
4623
|
+
const { row, issued } = await createManagedDirectoryConnection(ctx, {
|
|
4624
|
+
organizationId,
|
|
4625
|
+
providerId: ctx.body.providerId,
|
|
4626
|
+
actorId: claims.actorId,
|
|
4627
|
+
...claims.setupOperationId ? { creationRequestId: claims.setupOperationId } : {},
|
|
4628
|
+
...ctx.body.pairing ? { pairing: ctx.body.pairing } : {}
|
|
4629
|
+
}, policy);
|
|
4630
|
+
setCredentialResponseSecurityHeaders(ctx);
|
|
4631
|
+
return {
|
|
4632
|
+
...serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), {
|
|
4633
|
+
connection: issued.connection,
|
|
4634
|
+
credentials: [issued.credential]
|
|
4635
|
+
}),
|
|
4636
|
+
connectionId: issued.connection.connectionId,
|
|
4637
|
+
credential: serializeCredential(issued.credential),
|
|
4638
|
+
scimToken: issued.token
|
|
4639
|
+
};
|
|
4640
|
+
});
|
|
4641
|
+
const rotateDirectoryCredential = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/credentials/rotate", {
|
|
4642
|
+
method: "POST",
|
|
4643
|
+
use: [jwtMiddleware(options, directorySyncClaimsSchema)],
|
|
4644
|
+
body: rotateCredentialBodySchema,
|
|
4645
|
+
metadata: { noStore: true }
|
|
4646
|
+
}, async (ctx) => {
|
|
4647
|
+
requireOrganizationPlugin(ctx);
|
|
4648
|
+
assertManagedDirectorySyncEnabled(ctx);
|
|
4649
|
+
const organizationId = tryDecode(ctx.params.id);
|
|
4650
|
+
const claims = assertDirectorySyncManagementClaims(ctx, organizationId);
|
|
4651
|
+
await assertTargetOrganizationExists(ctx, organizationId);
|
|
4652
|
+
const policy = resolveCredentialPolicy(ctx.body);
|
|
4653
|
+
await assertManagedDirectoryTransactionsConfigured(ctx);
|
|
4654
|
+
const providerId = tryDecode(ctx.params.providerId);
|
|
4655
|
+
const endpoint = requireManagedEndpoint(ctx, getManagedEndpoints(ctx).rotateSCIMManagedCredential, "rotate");
|
|
4656
|
+
const { connectionId, issued } = await runWithTransaction$1(ctx.context.adapter, async () => {
|
|
4657
|
+
const database = await getCurrentAdapter$1(ctx.context.adapter);
|
|
4658
|
+
const row = await getDirectorySyncConnection(ctx, organizationId, providerId);
|
|
4659
|
+
if (row.status !== "active" || !row.connectionId) throw ctx.error("CONFLICT", { message: "Directory sync connection is not active" });
|
|
4660
|
+
if (!await database.update({
|
|
4661
|
+
model: "directorySyncConnection",
|
|
4662
|
+
where: [
|
|
4663
|
+
{
|
|
4664
|
+
field: "id",
|
|
4665
|
+
value: row.id
|
|
4666
|
+
},
|
|
4667
|
+
{
|
|
4668
|
+
field: "status",
|
|
4669
|
+
value: "active"
|
|
4670
|
+
},
|
|
4671
|
+
{
|
|
4672
|
+
field: "revision",
|
|
4673
|
+
value: row.revision
|
|
4674
|
+
},
|
|
4675
|
+
{
|
|
4676
|
+
field: "connectionId",
|
|
4677
|
+
value: row.connectionId
|
|
4678
|
+
}
|
|
4679
|
+
],
|
|
4680
|
+
update: {
|
|
4681
|
+
revision: row.revision + 1,
|
|
4682
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
4683
|
+
lastActorId: claims.actorId,
|
|
4684
|
+
lastError: null
|
|
4685
|
+
}
|
|
4686
|
+
})) throw ctx.error("CONFLICT", { message: "Directory sync connection changed before credential rotation started" });
|
|
4687
|
+
const issued = await endpoint({
|
|
4688
|
+
body: {
|
|
4689
|
+
connectionId: row.connectionId,
|
|
4690
|
+
provisioningDomainId: row.provisioningDomainId,
|
|
4691
|
+
actorId: claims.actorId,
|
|
4692
|
+
...policy
|
|
4693
|
+
},
|
|
4694
|
+
context: ctx.context,
|
|
4695
|
+
headers: getRequestHeaders(ctx)
|
|
4696
|
+
});
|
|
4697
|
+
return {
|
|
4698
|
+
connectionId: row.connectionId,
|
|
4699
|
+
issued
|
|
4700
|
+
};
|
|
4701
|
+
});
|
|
4702
|
+
setCredentialResponseSecurityHeaders(ctx);
|
|
4703
|
+
return {
|
|
4704
|
+
connectionId,
|
|
4705
|
+
credential: serializeCredential(issued.credential),
|
|
4706
|
+
scimToken: issued.token,
|
|
4707
|
+
scimEndpoint: getScimEndpoint$1(ctx.context.baseURL)
|
|
4708
|
+
};
|
|
4709
|
+
});
|
|
4710
|
+
const revokeDirectoryCredential = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/credentials/:credentialId/revoke", {
|
|
4711
|
+
method: "POST",
|
|
4712
|
+
use: [jwtMiddleware(options, directorySyncClaimsSchema)],
|
|
4713
|
+
body: emptyBodySchema
|
|
4714
|
+
}, async (ctx) => {
|
|
4715
|
+
requireOrganizationPlugin(ctx);
|
|
4716
|
+
assertManagedDirectorySyncEnabled(ctx);
|
|
4717
|
+
const organizationId = tryDecode(ctx.params.id);
|
|
4718
|
+
const claims = assertDirectorySyncManagementClaims(ctx, organizationId);
|
|
4719
|
+
await assertTargetOrganizationExists(ctx, organizationId);
|
|
4720
|
+
await assertManagedDirectoryTransactionsConfigured(ctx);
|
|
4721
|
+
const providerId = tryDecode(ctx.params.providerId);
|
|
4722
|
+
const credentialId = tryDecode(ctx.params.credentialId);
|
|
4723
|
+
const endpoint = requireManagedEndpoint(ctx, getManagedEndpoints(ctx).revokeSCIMManagedCredential, "revoke");
|
|
4724
|
+
const { state, updated } = await runWithTransaction$1(ctx.context.adapter, async () => {
|
|
4725
|
+
const database = await getCurrentAdapter$1(ctx.context.adapter);
|
|
4726
|
+
const row = await getDirectorySyncConnection(ctx, organizationId, providerId);
|
|
4727
|
+
if (row.status !== "active" || !row.connectionId) throw ctx.error("CONFLICT", { message: "Directory sync connection is not active" });
|
|
4728
|
+
const updated = await database.update({
|
|
4729
|
+
model: "directorySyncConnection",
|
|
4730
|
+
where: [
|
|
4731
|
+
{
|
|
4732
|
+
field: "id",
|
|
4733
|
+
value: row.id
|
|
4734
|
+
},
|
|
4735
|
+
{
|
|
4736
|
+
field: "status",
|
|
4737
|
+
value: "active"
|
|
4738
|
+
},
|
|
4739
|
+
{
|
|
4740
|
+
field: "revision",
|
|
4741
|
+
value: row.revision
|
|
4742
|
+
},
|
|
4743
|
+
{
|
|
4744
|
+
field: "connectionId",
|
|
4745
|
+
value: row.connectionId
|
|
4746
|
+
}
|
|
4747
|
+
],
|
|
4748
|
+
update: {
|
|
4749
|
+
revision: row.revision + 1,
|
|
4750
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
4751
|
+
lastActorId: claims.actorId,
|
|
4752
|
+
lastError: null
|
|
4753
|
+
}
|
|
4754
|
+
});
|
|
4755
|
+
if (!updated) throw ctx.error("CONFLICT", { message: "Directory sync connection changed before credential revocation started" });
|
|
4756
|
+
return {
|
|
4757
|
+
state: await endpoint({
|
|
4758
|
+
body: {
|
|
4759
|
+
connectionId: row.connectionId,
|
|
4760
|
+
provisioningDomainId: row.provisioningDomainId,
|
|
4761
|
+
credentialId,
|
|
4762
|
+
actorId: claims.actorId
|
|
4763
|
+
},
|
|
4764
|
+
context: ctx.context,
|
|
4765
|
+
headers: getRequestHeaders(ctx)
|
|
4766
|
+
}),
|
|
4767
|
+
updated
|
|
4768
|
+
};
|
|
4769
|
+
});
|
|
4770
|
+
return serializeDirectory(updated, getScimEndpoint$1(ctx.context.baseURL), state);
|
|
4771
|
+
});
|
|
4772
|
+
const DIRECTORY_EVENTS_DEFAULT_LIMIT = 10;
|
|
4773
|
+
const DIRECTORY_PAGE_MAX_LIMIT = 100;
|
|
4774
|
+
const DIRECTORY_EVENTS_DEFAULT_SORT_DIRECTION = "desc";
|
|
4775
|
+
const directoryEventsQuerySchema = z$1.object({
|
|
4776
|
+
limit: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
4777
|
+
offset: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
4778
|
+
sortDirection: z$1.enum(["asc", "desc"]).optional()
|
|
4779
|
+
}).optional();
|
|
4780
|
+
function resolveDirectoryEventsPage(query) {
|
|
4781
|
+
const requestedLimit = query?.limit;
|
|
4782
|
+
const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(1, Math.floor(requestedLimit)), DIRECTORY_PAGE_MAX_LIMIT) : DIRECTORY_EVENTS_DEFAULT_LIMIT;
|
|
4783
|
+
const requestedOffset = query?.offset;
|
|
4784
|
+
return {
|
|
4785
|
+
limit,
|
|
4786
|
+
offset: Number.isFinite(requestedOffset) ? Math.max(0, Math.floor(requestedOffset)) : 0,
|
|
4787
|
+
sortDirection: query?.sortDirection ?? DIRECTORY_EVENTS_DEFAULT_SORT_DIRECTION
|
|
4788
|
+
};
|
|
3333
4789
|
}
|
|
4790
|
+
const listDirectoryEvents = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/events", {
|
|
4791
|
+
method: "GET",
|
|
4792
|
+
use: [jwtMiddleware(options, directorySyncClaimsSchema)],
|
|
4793
|
+
query: directoryEventsQuerySchema
|
|
4794
|
+
}, async (ctx) => {
|
|
4795
|
+
requireOrganizationPlugin(ctx);
|
|
4796
|
+
assertManagedDirectorySyncEnabled(ctx);
|
|
4797
|
+
const organizationId = tryDecode(ctx.params.id);
|
|
4798
|
+
assertDirectorySyncManagementClaims(ctx, organizationId);
|
|
4799
|
+
await assertTargetOrganizationExists(ctx, organizationId);
|
|
4800
|
+
const { limit, offset, sortDirection } = resolveDirectoryEventsPage(ctx.query);
|
|
4801
|
+
const row = await getDirectorySyncConnection(ctx, organizationId, tryDecode(ctx.params.providerId));
|
|
4802
|
+
if (!row.connectionId) return {
|
|
4803
|
+
events: [],
|
|
4804
|
+
total: 0,
|
|
4805
|
+
limit,
|
|
4806
|
+
offset
|
|
4807
|
+
};
|
|
4808
|
+
const result = await requireManagedEndpoint(ctx, getManagedEndpoints(ctx).listSCIMManagedConnectionEvents, "events")({
|
|
4809
|
+
body: {
|
|
4810
|
+
connectionId: row.connectionId,
|
|
4811
|
+
provisioningDomainId: row.provisioningDomainId,
|
|
4812
|
+
limit,
|
|
4813
|
+
offset,
|
|
4814
|
+
sortDirection
|
|
4815
|
+
},
|
|
4816
|
+
context: ctx.context,
|
|
4817
|
+
headers: getRequestHeaders(ctx)
|
|
4818
|
+
});
|
|
4819
|
+
const events = result.events.map((event) => ({
|
|
4820
|
+
...event,
|
|
4821
|
+
createdAt: event.createdAt instanceof Date ? event.createdAt.toISOString() : event.createdAt
|
|
4822
|
+
}));
|
|
4823
|
+
if (typeof result.total === "number" && typeof result.limit === "number" && typeof result.offset === "number") return {
|
|
4824
|
+
events,
|
|
4825
|
+
total: result.total,
|
|
4826
|
+
limit: result.limit,
|
|
4827
|
+
offset: result.offset
|
|
4828
|
+
};
|
|
4829
|
+
const sorted = [...events].sort((a, b) => sortDirection === "asc" ? a.sequence - b.sequence : b.sequence - a.sequence);
|
|
4830
|
+
return {
|
|
4831
|
+
events: sorted.slice(offset, offset + limit),
|
|
4832
|
+
total: sorted.length,
|
|
4833
|
+
limit,
|
|
4834
|
+
offset
|
|
4835
|
+
};
|
|
4836
|
+
});
|
|
4837
|
+
const decommissionOrganizationDirectory = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/decommission", {
|
|
4838
|
+
method: "POST",
|
|
4839
|
+
use: [jwtMiddleware(options, directorySyncClaimsSchema)],
|
|
4840
|
+
body: emptyBodySchema
|
|
4841
|
+
}, async (ctx) => {
|
|
4842
|
+
requireOrganizationPlugin(ctx);
|
|
4843
|
+
assertManagedDirectorySyncEnabled(ctx);
|
|
4844
|
+
const organizationId = tryDecode(ctx.params.id);
|
|
4845
|
+
const claims = assertDirectorySyncManagementClaims(ctx, organizationId);
|
|
4846
|
+
await assertTargetOrganizationExists(ctx, organizationId);
|
|
4847
|
+
let row = await getDirectorySyncConnection(ctx, organizationId, tryDecode(ctx.params.providerId));
|
|
4848
|
+
if (row.status === "decommissioned") return serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), await getManagedState(ctx, row));
|
|
4849
|
+
if (!row.connectionId) throw ctx.error("CONFLICT", { message: "Directory sync connection has an invalid catalog binding" });
|
|
4850
|
+
const connectionId = row.connectionId;
|
|
4851
|
+
if (row.status === "active") row = await startDirectorySyncDecommission(ctx, row, claims.actorId);
|
|
4852
|
+
if (row.status !== "decommissioning") throw ctx.error("CONFLICT", { message: "Directory sync connection cannot be decommissioned" });
|
|
4853
|
+
const state = await requireManagedEndpoint(ctx, getManagedEndpoints(ctx).decommissionSCIMManagedConnection, "decommission")({
|
|
4854
|
+
body: {
|
|
4855
|
+
connectionId,
|
|
4856
|
+
provisioningDomainId: row.provisioningDomainId,
|
|
4857
|
+
actorId: claims.actorId
|
|
4858
|
+
},
|
|
4859
|
+
context: ctx.context,
|
|
4860
|
+
headers: getRequestHeaders(ctx)
|
|
4861
|
+
});
|
|
4862
|
+
if (state.connection.status === "decommissioned" || state.decommission.status === "complete") row = await ctx.context.adapter.update({
|
|
4863
|
+
model: "directorySyncConnection",
|
|
4864
|
+
where: [{
|
|
4865
|
+
field: "id",
|
|
4866
|
+
value: row.id
|
|
4867
|
+
}, {
|
|
4868
|
+
field: "status",
|
|
4869
|
+
value: "decommissioning"
|
|
4870
|
+
}],
|
|
4871
|
+
update: {
|
|
4872
|
+
activeOrganizationKey: createTerminalOrganizationKey(row.aliasKey),
|
|
4873
|
+
activeSsoProviderKey: createTerminalPairingKey(row.aliasKey),
|
|
4874
|
+
status: "decommissioned",
|
|
4875
|
+
decommissionedAt: /* @__PURE__ */ new Date(),
|
|
4876
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
4877
|
+
lastActorId: claims.actorId
|
|
4878
|
+
}
|
|
4879
|
+
}) ?? row;
|
|
4880
|
+
return serializeDirectory(row, getScimEndpoint$1(ctx.context.baseURL), state);
|
|
4881
|
+
});
|
|
4882
|
+
const unpairOrganizationDirectory = (options) => createAuthEndpoint("/dash/organization/:id/directories/:providerId/unpair", {
|
|
4883
|
+
method: "POST",
|
|
4884
|
+
use: [jwtMiddleware(options, directorySyncClaimsSchema)],
|
|
4885
|
+
body: emptyBodySchema
|
|
4886
|
+
}, async (ctx) => {
|
|
4887
|
+
requireOrganizationPlugin(ctx);
|
|
4888
|
+
assertManagedDirectorySyncEnabled(ctx);
|
|
4889
|
+
const organizationId = tryDecode(ctx.params.id);
|
|
4890
|
+
const claims = assertDirectorySyncManagementClaims(ctx, organizationId);
|
|
4891
|
+
await assertTargetOrganizationExists(ctx, organizationId);
|
|
4892
|
+
const updated = await unpairTerminalDirectorySyncConnection(ctx, await getDirectorySyncConnection(ctx, organizationId, tryDecode(ctx.params.providerId)), claims.actorId);
|
|
4893
|
+
return serializeDirectory(updated, getScimEndpoint$1(ctx.context.baseURL), await getManagedState(ctx, updated));
|
|
4894
|
+
});
|
|
3334
4895
|
//#endregion
|
|
3335
4896
|
//#region src/routes/directory-sync/session.ts
|
|
3336
4897
|
async function buildSyntheticSession(ctx, ownerUserId) {
|
|
@@ -3414,13 +4975,12 @@ async function scimProviderExists(ctx, organizationId, providerId) {
|
|
|
3414
4975
|
function getScimEndpoint(baseUrl) {
|
|
3415
4976
|
return `${baseUrl}/scim/v2`;
|
|
3416
4977
|
}
|
|
3417
|
-
function
|
|
3418
|
-
return ctx
|
|
4978
|
+
function resolveMode(ctx) {
|
|
4979
|
+
return resolveDirectorySyncMode(getSCIMPlugin(ctx), isManagedDirectorySyncEnabled(ctx));
|
|
3419
4980
|
}
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
return scimPlugin.options?.providerOwnership?.enabled === true;
|
|
4981
|
+
/** Legacy provider-ownership APIs. Never true when the 1.7+ managed surface exists. */
|
|
4982
|
+
function isLegacyDirectorySyncEnabled(ctx) {
|
|
4983
|
+
return resolveMode(ctx) === "legacy";
|
|
3424
4984
|
}
|
|
3425
4985
|
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.";
|
|
3426
4986
|
function isDuplicateDirectorySyncError(e) {
|
|
@@ -3433,14 +4993,15 @@ const listOrganizationDirectories = (options) => {
|
|
|
3433
4993
|
method: "GET",
|
|
3434
4994
|
use: [jwtMiddleware(options)]
|
|
3435
4995
|
}, async (ctx) => {
|
|
4996
|
+
if (resolveMode(ctx) === "managed") return listOrganizationDirectoriesManagedHandler(ctx);
|
|
3436
4997
|
if (!isOrganizationEnabled(ctx)) {
|
|
3437
4998
|
ctx.context.logger.warn("[Dash] Organization plugin not enabled, returning empty directories list");
|
|
3438
4999
|
return [];
|
|
3439
5000
|
}
|
|
3440
5001
|
const organizationId = tryDecode(ctx.params.id);
|
|
3441
5002
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3442
|
-
if (!
|
|
3443
|
-
ctx.context.logger.warn("[Dash] SCIM
|
|
5003
|
+
if (!isLegacyDirectorySyncEnabled(ctx) || !scimPlugin?.endpoints.listSCIMProviderConnections) {
|
|
5004
|
+
ctx.context.logger.warn("[Dash] Legacy SCIM directory sync is unavailable, returning empty directories list", { organizationId });
|
|
3444
5005
|
return [];
|
|
3445
5006
|
}
|
|
3446
5007
|
const managerUserId = await resolveScimManagementUserId(ctx, organizationId);
|
|
@@ -3464,7 +5025,7 @@ const listOrganizationDirectories = (options) => {
|
|
|
3464
5025
|
}
|
|
3465
5026
|
});
|
|
3466
5027
|
};
|
|
3467
|
-
const
|
|
5028
|
+
const createOrganizationDirectoryLegacy = (options) => {
|
|
3468
5029
|
return createAuthEndpoint("/dash/organization/directory/create", {
|
|
3469
5030
|
method: "POST",
|
|
3470
5031
|
use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
|
|
@@ -3476,7 +5037,7 @@ const createOrganizationDirectory = (options) => {
|
|
|
3476
5037
|
requireOrganizationPlugin(ctx);
|
|
3477
5038
|
const { organizationId } = ctx.context.payload;
|
|
3478
5039
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3479
|
-
if (!
|
|
5040
|
+
if (!isLegacyDirectorySyncEnabled(ctx) || !scimPlugin?.endpoints.generateSCIMToken) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3480
5041
|
const { providerId, ownerUserId } = ctx.body;
|
|
3481
5042
|
if (await scimProviderExists(ctx, organizationId, providerId)) throw ctx.error("BAD_REQUEST", { message: DIRECTORY_SYNC_DUPLICATE_MESSAGE });
|
|
3482
5043
|
let scimToken;
|
|
@@ -3504,7 +5065,7 @@ const createOrganizationDirectory = (options) => {
|
|
|
3504
5065
|
};
|
|
3505
5066
|
});
|
|
3506
5067
|
};
|
|
3507
|
-
const
|
|
5068
|
+
const deleteOrganizationDirectoryLegacy = (options) => {
|
|
3508
5069
|
return createAuthEndpoint("/dash/organization/directory/delete", {
|
|
3509
5070
|
method: "POST",
|
|
3510
5071
|
use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
|
|
@@ -3512,7 +5073,7 @@ const deleteOrganizationDirectory = (options) => {
|
|
|
3512
5073
|
}, async (ctx) => {
|
|
3513
5074
|
requireOrganizationPlugin(ctx);
|
|
3514
5075
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3515
|
-
if (!
|
|
5076
|
+
if (!isLegacyDirectorySyncEnabled(ctx) || !scimPlugin?.endpoints.deleteSCIMProviderConnection) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3516
5077
|
const { organizationId } = ctx.context.payload;
|
|
3517
5078
|
const { providerId } = ctx.body;
|
|
3518
5079
|
const managerUserId = await resolveScimManagementUserId(ctx, organizationId, providerId);
|
|
@@ -3528,7 +5089,7 @@ const deleteOrganizationDirectory = (options) => {
|
|
|
3528
5089
|
return { success: true };
|
|
3529
5090
|
});
|
|
3530
5091
|
};
|
|
3531
|
-
const
|
|
5092
|
+
const regenerateDirectoryTokenLegacy = (options) => {
|
|
3532
5093
|
return createAuthEndpoint("/dash/organization/directory/regenerate-token", {
|
|
3533
5094
|
method: "POST",
|
|
3534
5095
|
use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))],
|
|
@@ -3536,7 +5097,7 @@ const regenerateDirectoryToken = (options) => {
|
|
|
3536
5097
|
}, async (ctx) => {
|
|
3537
5098
|
requireOrganizationPlugin(ctx);
|
|
3538
5099
|
const scimPlugin = getSCIMPlugin(ctx);
|
|
3539
|
-
if (!
|
|
5100
|
+
if (!isLegacyDirectorySyncEnabled(ctx) || !scimPlugin?.endpoints.generateSCIMToken) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3540
5101
|
const { organizationId } = ctx.context.payload;
|
|
3541
5102
|
const { providerId } = ctx.body;
|
|
3542
5103
|
const managerUserId = await resolveScimManagementUserId(ctx, organizationId, providerId);
|
|
@@ -4124,10 +5685,8 @@ async function executePlatformInvitationCompletion(ctx, $api, invitation, args)
|
|
|
4124
5685
|
};
|
|
4125
5686
|
const adapter = ctx.context.internalAdapter;
|
|
4126
5687
|
const user = isVersionAtLeast(ctx.context.version, "1.7.0") ? await adapter.createUser(userPayload, { method: "admin" }) : await adapter.createUser(userPayload);
|
|
4127
|
-
if (password) await ctx.context.internalAdapter
|
|
5688
|
+
if (password) await createCredentialAccountCompat(ctx.context.internalAdapter, {
|
|
4128
5689
|
userId: user.id,
|
|
4129
|
-
providerId: "credential",
|
|
4130
|
-
accountId: user.id,
|
|
4131
5690
|
password: await ctx.context.password.hash(password)
|
|
4132
5691
|
});
|
|
4133
5692
|
const { error: markError } = await $api("/api/internal/invitations/mark-accepted", {
|
|
@@ -6232,6 +7791,40 @@ function validateSAMLMetadataAlgorithms(metadataXml) {
|
|
|
6232
7791
|
return warnings;
|
|
6233
7792
|
}
|
|
6234
7793
|
//#endregion
|
|
7794
|
+
//#region src/routes/sso/verify-domain-error.ts
|
|
7795
|
+
const DOMAIN_ALREADY_VERIFIED = {
|
|
7796
|
+
verified: true,
|
|
7797
|
+
message: "Domain has already been verified"
|
|
7798
|
+
};
|
|
7799
|
+
const PROVIDER_CHANGED = {
|
|
7800
|
+
verified: false,
|
|
7801
|
+
message: "SSO provider changed while domain verification was in progress. Reload the provider and try again."
|
|
7802
|
+
};
|
|
7803
|
+
const DNS_RECORD_MISSING = {
|
|
7804
|
+
verified: false,
|
|
7805
|
+
message: "Unable to verify domain ownership. The TXT record was not found. It may take up to 48 hours for DNS changes to propagate."
|
|
7806
|
+
};
|
|
7807
|
+
function isRecord(value) {
|
|
7808
|
+
return typeof value === "object" && value !== null;
|
|
7809
|
+
}
|
|
7810
|
+
function getApiErrorCode(error) {
|
|
7811
|
+
const withExtras = error;
|
|
7812
|
+
if (typeof withExtras.code === "string") return withExtras.code;
|
|
7813
|
+
if (isRecord(withExtras.body) && typeof withExtras.body.code === "string") return withExtras.body.code;
|
|
7814
|
+
}
|
|
7815
|
+
/**
|
|
7816
|
+
* Maps SSO plugin `verifyDomain` API errors to dash responses.
|
|
7817
|
+
* Returns `null` when the caller should rethrow.
|
|
7818
|
+
*/
|
|
7819
|
+
function mapSsoVerifyDomainApiError(error) {
|
|
7820
|
+
if (error.status === "CONFLICT") {
|
|
7821
|
+
if (getApiErrorCode(error) === "SSO_PROVIDER_CHANGED") return PROVIDER_CHANGED;
|
|
7822
|
+
return DOMAIN_ALREADY_VERIFIED;
|
|
7823
|
+
}
|
|
7824
|
+
if (error.status === "BAD_GATEWAY") return DNS_RECORD_MISSING;
|
|
7825
|
+
return null;
|
|
7826
|
+
}
|
|
7827
|
+
//#endregion
|
|
6235
7828
|
//#region src/routes/sso/index.ts
|
|
6236
7829
|
function requireOrganizationAccess(ctx) {
|
|
6237
7830
|
const orgIdFromUrl = tryDecode(ctx.params.id);
|
|
@@ -6247,6 +7840,7 @@ const samlConfigSchema = z$1.object({
|
|
|
6247
7840
|
cert: z$1.string().optional(),
|
|
6248
7841
|
entityId: z$1.string().optional(),
|
|
6249
7842
|
mapping: z$1.object({
|
|
7843
|
+
/** @deprecated Removed in better-auth 1.7+; SAML subject is NameID. */
|
|
6250
7844
|
id: z$1.string().optional(),
|
|
6251
7845
|
email: z$1.string().optional(),
|
|
6252
7846
|
emailVerified: z$1.string().optional(),
|
|
@@ -6268,6 +7862,7 @@ const oidcConfigSchema = z$1.object({
|
|
|
6268
7862
|
userInfoEndpoint: z$1.string().optional(),
|
|
6269
7863
|
tokenEndpointAuthentication: z$1.enum(["client_secret_post", "client_secret_basic"]).optional(),
|
|
6270
7864
|
mapping: z$1.object({
|
|
7865
|
+
/** @deprecated Removed in better-auth 1.7+; OIDC subject is `sub`. */
|
|
6271
7866
|
id: z$1.string().optional(),
|
|
6272
7867
|
email: z$1.string().optional(),
|
|
6273
7868
|
emailVerified: z$1.string().optional(),
|
|
@@ -6293,20 +7888,24 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
6293
7888
|
ctx.context.logger.warn("[Dash] SAML IdP metadata uses deprecated algorithms:", providerId, warnings);
|
|
6294
7889
|
}
|
|
6295
7890
|
}
|
|
7891
|
+
const m = samlConfig.mapping;
|
|
7892
|
+
const resolvedEntryPoint = samlConfig.entryPoint?.trim() || (idpMetadataXml ? extractEntryPointFromSAMLMetadata(idpMetadataXml) : void 0);
|
|
7893
|
+
const saml17OrNewer = isVersionAtLeast(ctx.context.version, "1.7.0");
|
|
7894
|
+
const ba17OrLater = isBetterAuth17OrLater(ctx.context.version);
|
|
7895
|
+
if (saml17OrNewer && !resolvedEntryPoint) throw ctx.error("BAD_REQUEST", { message: "SAML entry point URL is required; provide entryPoint or IdP metadata with SingleSignOnService Location" });
|
|
7896
|
+
if (ba17OrLater && !idpMetadataXml && !samlConfig.entityId?.trim()) throw ctx.error("BAD_REQUEST", { message: "IdP entity ID is required when IdP metadata XML is not provided" });
|
|
7897
|
+
const spIssuer = `${baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`;
|
|
6296
7898
|
const idpMetadata = idpMetadataXml ? { metadata: idpMetadataXml } : {
|
|
7899
|
+
...ba17OrLater ? { entityID: samlConfig.entityId } : {},
|
|
6297
7900
|
...samlConfig.entryPoint ? { singleSignOnService: [{
|
|
6298
7901
|
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
6299
7902
|
Location: samlConfig.entryPoint
|
|
6300
7903
|
}] } : {},
|
|
6301
7904
|
...samlConfig.cert ? { cert: samlConfig.cert } : {}
|
|
6302
7905
|
};
|
|
6303
|
-
const m = samlConfig.mapping;
|
|
6304
|
-
const resolvedEntryPoint = samlConfig.entryPoint?.trim() || (idpMetadataXml ? extractEntryPointFromSAMLMetadata(idpMetadataXml) : void 0);
|
|
6305
|
-
const saml17OrNewer = isVersionAtLeast(ctx.context.version, "1.7.0");
|
|
6306
|
-
if (saml17OrNewer && !resolvedEntryPoint) throw ctx.error("BAD_REQUEST", { message: "SAML entry point URL is required; provide entryPoint or IdP metadata with SingleSignOnService Location" });
|
|
6307
7906
|
return {
|
|
6308
7907
|
config: {
|
|
6309
|
-
issuer: samlConfig.entityId ??
|
|
7908
|
+
issuer: ba17OrLater && !idpMetadataXml ? spIssuer : samlConfig.entityId ?? spIssuer,
|
|
6310
7909
|
idpMetadata,
|
|
6311
7910
|
...saml17OrNewer ? {
|
|
6312
7911
|
entryPoint: resolvedEntryPoint,
|
|
@@ -6318,7 +7917,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
6318
7917
|
cert: samlConfig.cert ?? ""
|
|
6319
7918
|
},
|
|
6320
7919
|
...m ? { mapping: {
|
|
6321
|
-
id: m.id ?? "nameID",
|
|
7920
|
+
...!ba17OrLater ? { id: m.id ?? "nameID" } : {},
|
|
6322
7921
|
email: m.email ?? "email",
|
|
6323
7922
|
name: m.name ?? "name",
|
|
6324
7923
|
emailVerified: m.emailVerified,
|
|
@@ -6334,6 +7933,7 @@ async function resolveOIDCConfig(oidcConfig, _domain, ctx) {
|
|
|
6334
7933
|
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" });
|
|
6335
7934
|
const om = oidcConfig.mapping;
|
|
6336
7935
|
const discoveryEndpoint = oidcConfig.discoveryEndpoint ?? oidcConfig.discoveryUrl ?? oidcConfig.issuer;
|
|
7936
|
+
const ba17OrLater = isBetterAuth17OrLater(ctx.context.version);
|
|
6337
7937
|
return {
|
|
6338
7938
|
config: {
|
|
6339
7939
|
clientId: oidcConfig.clientId,
|
|
@@ -6347,7 +7947,7 @@ async function resolveOIDCConfig(oidcConfig, _domain, ctx) {
|
|
|
6347
7947
|
tokenEndpointAuthentication: oidcConfig.tokenEndpointAuthentication,
|
|
6348
7948
|
pkce: true,
|
|
6349
7949
|
...om ? { mapping: {
|
|
6350
|
-
id: om.id ?? "sub",
|
|
7950
|
+
...!ba17OrLater ? { id: om.id ?? "sub" } : {},
|
|
6351
7951
|
email: om.email ?? "email",
|
|
6352
7952
|
name: om.name ?? "name",
|
|
6353
7953
|
emailVerified: om.emailVerified,
|
|
@@ -6414,7 +8014,7 @@ const createSsoProvider = (options) => {
|
|
|
6414
8014
|
};
|
|
6415
8015
|
if (protocol === "SAML" && samlConfig) {
|
|
6416
8016
|
const samlResult = await resolveSAMLConfig(samlConfig, providerId, ctx.context.baseURL, ctx);
|
|
6417
|
-
registerBody.issuer = samlResult.config.issuer
|
|
8017
|
+
registerBody.issuer = samlResult.config.issuer;
|
|
6418
8018
|
registerBody.samlConfig = samlResult.config;
|
|
6419
8019
|
}
|
|
6420
8020
|
if (protocol === "OIDC" && oidcConfig) {
|
|
@@ -6433,15 +8033,17 @@ const createSsoProvider = (options) => {
|
|
|
6433
8033
|
});
|
|
6434
8034
|
let verificationToken = null;
|
|
6435
8035
|
if ("domainVerificationToken" in result && typeof result.domainVerificationToken === "string") verificationToken = result.domainVerificationToken;
|
|
8036
|
+
const tokenPrefix = ssoPlugin.options?.domainVerification?.tokenPrefix || "better-auth-token";
|
|
8037
|
+
const resolvedProviderId = result.providerId || providerId;
|
|
6436
8038
|
return {
|
|
6437
8039
|
success: true,
|
|
6438
8040
|
provider: {
|
|
6439
8041
|
id: result.providerId,
|
|
6440
|
-
providerId:
|
|
8042
|
+
providerId: resolvedProviderId,
|
|
6441
8043
|
domain: result.domain || domain
|
|
6442
8044
|
},
|
|
6443
8045
|
domainVerification: {
|
|
6444
|
-
txtRecordName: `
|
|
8046
|
+
txtRecordName: `_${tokenPrefix}-${resolvedProviderId}`,
|
|
6445
8047
|
verificationToken
|
|
6446
8048
|
}
|
|
6447
8049
|
};
|
|
@@ -6485,7 +8087,7 @@ const updateSsoProvider = (options) => {
|
|
|
6485
8087
|
if (domain && domain !== existingProvider.domain) updateBody.domain = domain;
|
|
6486
8088
|
if (protocol === "SAML" && samlConfig) {
|
|
6487
8089
|
const samlResult = await resolveSAMLConfig(samlConfig, providerId, ctx.context.baseURL, ctx);
|
|
6488
|
-
updateBody.issuer = samlResult.config.issuer
|
|
8090
|
+
updateBody.issuer = samlResult.config.issuer;
|
|
6489
8091
|
updateBody.samlConfig = samlResult.config;
|
|
6490
8092
|
}
|
|
6491
8093
|
if (protocol === "OIDC" && oidcConfig) {
|
|
@@ -6560,7 +8162,7 @@ const requestSsoVerificationToken = (options) => {
|
|
|
6560
8162
|
}]
|
|
6561
8163
|
});
|
|
6562
8164
|
if (!provider) throw ctx.error("NOT_FOUND", { message: "SSO provider not found" });
|
|
6563
|
-
const txtRecordName =
|
|
8165
|
+
const txtRecordName = `_${ssoPlugin.options?.domainVerification?.tokenPrefix || "better-auth-token"}-${provider.providerId}`;
|
|
6564
8166
|
try {
|
|
6565
8167
|
const result = await endpoints.requestDomainVerification({
|
|
6566
8168
|
body: { providerId },
|
|
@@ -6622,14 +8224,8 @@ const verifySsoProviderDomain = (options) => {
|
|
|
6622
8224
|
};
|
|
6623
8225
|
} catch (e) {
|
|
6624
8226
|
if (e instanceof APIError$1) {
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
message: "Domain has already been verified"
|
|
6628
|
-
};
|
|
6629
|
-
if (e.status === "BAD_GATEWAY") return {
|
|
6630
|
-
verified: false,
|
|
6631
|
-
message: "Unable to verify domain ownership. The TXT record was not found. It may take up to 48 hours for DNS changes to propagate."
|
|
6632
|
-
};
|
|
8227
|
+
const mapped = mapSsoVerifyDomainApiError(e);
|
|
8228
|
+
if (mapped) return mapped;
|
|
6633
8229
|
throw e;
|
|
6634
8230
|
}
|
|
6635
8231
|
throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to verify domain" });
|
|
@@ -7401,10 +8997,8 @@ const createUser = (options) => {
|
|
|
7401
8997
|
};
|
|
7402
8998
|
const adapter = ctx.context.internalAdapter;
|
|
7403
8999
|
const user = isVersionAtLeast(ctx.context.version, "1.7.0") ? await adapter.createUser(userPayload, { method: "admin" }) : await adapter.createUser(userPayload);
|
|
7404
|
-
if (password) await ctx.context.internalAdapter
|
|
9000
|
+
if (password) await createCredentialAccountCompat(ctx.context.internalAdapter, {
|
|
7405
9001
|
userId: user.id,
|
|
7406
|
-
providerId: "credential",
|
|
7407
|
-
accountId: user.id,
|
|
7408
9002
|
password: await ctx.context.password.hash(password)
|
|
7409
9003
|
});
|
|
7410
9004
|
if (body.sendVerificationEmail && !emailVerified) {
|
|
@@ -7469,10 +9063,8 @@ const setPassword = (options) => {
|
|
|
7469
9063
|
password: hashed,
|
|
7470
9064
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7471
9065
|
});
|
|
7472
|
-
else await ctx.context.internalAdapter
|
|
9066
|
+
else await createCredentialAccountCompat(ctx.context.internalAdapter, {
|
|
7473
9067
|
userId,
|
|
7474
|
-
providerId: "credential",
|
|
7475
|
-
accountId: userId,
|
|
7476
9068
|
password: hashed
|
|
7477
9069
|
});
|
|
7478
9070
|
return { success: true };
|
|
@@ -7484,7 +9076,7 @@ const unlinkAccount = (options) => {
|
|
|
7484
9076
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
|
|
7485
9077
|
body: z$1.object({
|
|
7486
9078
|
providerId: z$1.string(),
|
|
7487
|
-
accountId: z$1.string()
|
|
9079
|
+
accountId: z$1.string()
|
|
7488
9080
|
})
|
|
7489
9081
|
}, async (ctx) => {
|
|
7490
9082
|
const { userId } = ctx.context.payload;
|
|
@@ -7493,7 +9085,10 @@ const unlinkAccount = (options) => {
|
|
|
7493
9085
|
const accounts = await ctx.context.internalAdapter.findAccounts(userId);
|
|
7494
9086
|
const allowUnlinkingAll = ctx.context.options.account?.accountLinking?.allowUnlinkingAll ?? false;
|
|
7495
9087
|
if (accounts.length === 1 && !allowUnlinkingAll) throw new APIError("BAD_REQUEST", { message: "Cannot unlink the last account. This would lock the user out." });
|
|
7496
|
-
const accountToUnlink = accounts
|
|
9088
|
+
const accountToUnlink = resolveAccountForUnlink(accounts, {
|
|
9089
|
+
providerId,
|
|
9090
|
+
accountId
|
|
9091
|
+
});
|
|
7497
9092
|
if (!accountToUnlink) throw new APIError("NOT_FOUND", { message: "Account not found" });
|
|
7498
9093
|
await ctx.context.internalAdapter.deleteAccount(accountToUnlink.id);
|
|
7499
9094
|
return { success: true };
|
|
@@ -8411,7 +10006,7 @@ function createSMSSender(config) {
|
|
|
8411
10006
|
"user-agent": INFRA_USER_AGENT,
|
|
8412
10007
|
Authorization: `Bearer ${apiKey}`
|
|
8413
10008
|
},
|
|
8414
|
-
timeout: config?.apiTimeout ?? 3e3
|
|
10009
|
+
timeout: config?.apiOptions?.timeout ?? config?.apiTimeout ?? 3e3
|
|
8415
10010
|
});
|
|
8416
10011
|
/**
|
|
8417
10012
|
* Send an SMS with OTP code
|
|
@@ -8490,6 +10085,18 @@ async function sendSMS(options, config) {
|
|
|
8490
10085
|
}
|
|
8491
10086
|
//#endregion
|
|
8492
10087
|
//#region src/index.ts
|
|
10088
|
+
/**
|
|
10089
|
+
* Stashed when session.create after runs before newSession/returned user is
|
|
10090
|
+
* available (1.7+ / better-auth#10473). Avoids a fallback user DB lookup by
|
|
10091
|
+
* flushing tracking once the endpoint after hook can resolve the user.
|
|
10092
|
+
*/
|
|
10093
|
+
const PENDING_SESSION_TRACKING = Symbol.for("dash.pendingSessionTracking");
|
|
10094
|
+
function getPendingSessionTracking(ctx) {
|
|
10095
|
+
return ctx.context[PENDING_SESSION_TRACKING];
|
|
10096
|
+
}
|
|
10097
|
+
function setPendingSessionTracking(ctx, pending) {
|
|
10098
|
+
ctx.context[PENDING_SESSION_TRACKING] = pending;
|
|
10099
|
+
}
|
|
8493
10100
|
async function getRequestLocation() {
|
|
8494
10101
|
try {
|
|
8495
10102
|
return (await getCurrentAuthContext()).context.location;
|
|
@@ -8505,7 +10112,11 @@ const dash = (options) => {
|
|
|
8505
10112
|
...opts,
|
|
8506
10113
|
$api
|
|
8507
10114
|
};
|
|
8508
|
-
const $kv = createKV(
|
|
10115
|
+
const $kv = createKV({
|
|
10116
|
+
kvUrl: opts.kvUrl,
|
|
10117
|
+
apiKey: opts.apiKey,
|
|
10118
|
+
timeout: opts.kvOptions.timeout
|
|
10119
|
+
});
|
|
8509
10120
|
const activityUpdateInterval = opts.activityTracking?.updateInterval ?? 3e5;
|
|
8510
10121
|
const scheduleLastActiveUpdate = async (ctx, userId) => {
|
|
8511
10122
|
await ctx.context.runInBackgroundOrAwait(ctx.context.adapter.updateMany({
|
|
@@ -8522,6 +10133,21 @@ const dash = (options) => {
|
|
|
8522
10133
|
const { tracker } = initTrackEvents($api);
|
|
8523
10134
|
const { trackUserSignedUp, trackUserProfileUpdated, trackUserProfileImageUpdated, trackUserEmailVerified, trackUserBanned, trackUserUnBanned, trackUserDeleted } = initUserEvents(tracker);
|
|
8524
10135
|
const { trackEmailVerificationSent, trackEmailSignInAttempt, trackUserSignedIn, trackUserSignedOut, trackSessionCreated, trackSocialSignInAttempt, trackSocialSignInRedirectionAttempt, trackUserImpersonated, trackUserImpersonationStop, trackSessionRevoked, trackSessionRevokedAll } = initSessionEvents(tracker);
|
|
10136
|
+
const trackSessionLifecycle = (enrichedSession, userId, ctx, location, eventUser, trackSignIn, impersonatedBy) => {
|
|
10137
|
+
let trigger = null;
|
|
10138
|
+
if (trackSignIn) {
|
|
10139
|
+
trigger = getTriggerInfo(ctx, userId, enrichedSession);
|
|
10140
|
+
trackUserSignedIn(enrichedSession, trigger, ctx, location, eventUser);
|
|
10141
|
+
} else trigger = getTriggerInfo(ctx, userId);
|
|
10142
|
+
trackSessionCreated(enrichedSession, trigger, ctx, location, eventUser);
|
|
10143
|
+
if (impersonatedBy) {
|
|
10144
|
+
trigger = {
|
|
10145
|
+
...trigger,
|
|
10146
|
+
triggeredBy: impersonatedBy
|
|
10147
|
+
};
|
|
10148
|
+
trackUserImpersonated(enrichedSession, trigger, ctx, location, eventUser, resolveUserFromContext(impersonatedBy, ctx));
|
|
10149
|
+
}
|
|
10150
|
+
};
|
|
8525
10151
|
const { trackAccountLinking, trackAccountUnlink, trackAccountPasswordChange } = initAccountEvents(tracker);
|
|
8526
10152
|
const { trackPasswordResetRequest, trackPasswordResetRequestCompletion } = initVerificationEvents(tracker);
|
|
8527
10153
|
const { trackOrganizationCreated, trackOrganizationUpdated } = initOrganizationEvents(tracker);
|
|
@@ -8625,6 +10251,14 @@ const dash = (options) => {
|
|
|
8625
10251
|
};
|
|
8626
10252
|
instrumentOrganizationHooks(organizationPlugin.options = organizationPlugin.options ?? {});
|
|
8627
10253
|
} else logger.debug("[Dash] Organization plugin not active. Skipping instrumentation");
|
|
10254
|
+
const managedDirectorySync = opts.managedDirectorySync;
|
|
10255
|
+
if (managedDirectorySync?.enabled) instrumentDirectorySyncIntegration(ctx, {
|
|
10256
|
+
ssoPairing: managedDirectorySync.ssoPairing ?? true,
|
|
10257
|
+
membershipProjection: {
|
|
10258
|
+
enabled: managedDirectorySync.membershipProjection?.enabled ?? true,
|
|
10259
|
+
role: managedDirectorySync.membershipProjection?.role ?? "member"
|
|
10260
|
+
}
|
|
10261
|
+
});
|
|
8628
10262
|
return { options: {
|
|
8629
10263
|
databaseHooks: {
|
|
8630
10264
|
user: {
|
|
@@ -8686,25 +10320,25 @@ const dash = (options) => {
|
|
|
8686
10320
|
countryCode: location?.countryCode
|
|
8687
10321
|
};
|
|
8688
10322
|
const eventUser = resolveUserFromContext(session.userId, ctx);
|
|
8689
|
-
|
|
8690
|
-
if (matchesAnyRoute(ctx.path, [
|
|
10323
|
+
const trackSignIn = matchesAnyRoute(ctx.path, [
|
|
8691
10324
|
routes.SIGN_IN,
|
|
8692
10325
|
routes.SIGN_UP,
|
|
8693
10326
|
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
8694
10327
|
routes.SIGN_IN_OAUTH_CALLBACK
|
|
8695
|
-
])
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
};
|
|
8705
|
-
|
|
8706
|
-
|
|
10328
|
+
]);
|
|
10329
|
+
const impersonatedBy = "impersonatedBy" in session && session.impersonatedBy ? session.impersonatedBy : void 0;
|
|
10330
|
+
if (!eventUser) {
|
|
10331
|
+
setPendingSessionTracking(ctx, {
|
|
10332
|
+
enrichedSession,
|
|
10333
|
+
location,
|
|
10334
|
+
userId: session.userId,
|
|
10335
|
+
trackSignIn,
|
|
10336
|
+
impersonatedBy
|
|
10337
|
+
});
|
|
10338
|
+
if (opts.activityTracking?.enabled) await scheduleLastActiveUpdate(ctx, session.userId);
|
|
10339
|
+
return;
|
|
8707
10340
|
}
|
|
10341
|
+
trackSessionLifecycle(enrichedSession, session.userId, ctx, location, eventUser, trackSignIn, impersonatedBy);
|
|
8708
10342
|
if (opts.activityTracking?.enabled) await scheduleLastActiveUpdate(ctx, session.userId);
|
|
8709
10343
|
}
|
|
8710
10344
|
},
|
|
@@ -8803,53 +10437,67 @@ const dash = (options) => {
|
|
|
8803
10437
|
routes.DASH_COMPLETE_INVITATION_SOCIAL
|
|
8804
10438
|
]);
|
|
8805
10439
|
},
|
|
8806
|
-
handler: createIdentificationMiddleware($kv, {
|
|
10440
|
+
handler: createIdentificationMiddleware($kv, {
|
|
10441
|
+
skipIdentification: (ctx) => isDashRoute(ctx.path),
|
|
10442
|
+
retry: opts.kvOptions.retry
|
|
10443
|
+
})
|
|
8807
10444
|
}],
|
|
8808
|
-
after: [
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
10445
|
+
after: [
|
|
10446
|
+
{
|
|
10447
|
+
matcher: (ctx) => !!getPendingSessionTracking(ctx),
|
|
10448
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
10449
|
+
const pending = getPendingSessionTracking(ctx);
|
|
10450
|
+
if (!pending) return;
|
|
10451
|
+
trackSessionLifecycle(pending.enrichedSession, pending.userId, ctx, pending.location, resolveUserFromContext(pending.userId, ctx), pending.trackSignIn, pending.impersonatedBy);
|
|
10452
|
+
})
|
|
8816
10453
|
},
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
10454
|
+
{
|
|
10455
|
+
matcher: (ctx) => {
|
|
10456
|
+
if (ctx.request?.method !== "GET") return true;
|
|
10457
|
+
return matchesAnyRoute(ctx.path, [
|
|
10458
|
+
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
10459
|
+
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
10460
|
+
routes.DASH_IMPERSONATE_USER
|
|
10461
|
+
]);
|
|
10462
|
+
},
|
|
10463
|
+
handler: createAuthMiddleware(async (_ctx) => {
|
|
10464
|
+
const ctx = _ctx;
|
|
10465
|
+
const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? "unknown");
|
|
10466
|
+
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);
|
|
10467
|
+
const body = ctx.body;
|
|
10468
|
+
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);
|
|
10469
|
+
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);
|
|
10470
|
+
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);
|
|
10471
|
+
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
10472
|
+
if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
10473
|
+
maxAge: 600,
|
|
10474
|
+
sameSite: "lax",
|
|
10475
|
+
httpOnly: true,
|
|
10476
|
+
path: "/"
|
|
10477
|
+
});
|
|
10478
|
+
else if (ctx.context.requestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, "", {
|
|
10479
|
+
maxAge: 0,
|
|
10480
|
+
path: "/"
|
|
10481
|
+
});
|
|
10482
|
+
})
|
|
10483
|
+
},
|
|
10484
|
+
{
|
|
10485
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
10486
|
+
if (!opts.activityTracking?.enabled) return;
|
|
10487
|
+
if (activityUpdateInterval === 0) return;
|
|
10488
|
+
const session = ctx.context.session || ctx.context.newSession;
|
|
10489
|
+
if (!session?.user?.id) return;
|
|
10490
|
+
const userId = session.user.id;
|
|
10491
|
+
const now = Date.now();
|
|
10492
|
+
const lastUpdate = session.user.lastActiveAt;
|
|
10493
|
+
if (lastUpdate) {
|
|
10494
|
+
if (now - new Date(lastUpdate).getTime() < activityUpdateInterval) return;
|
|
10495
|
+
}
|
|
10496
|
+
await scheduleLastActiveUpdate(ctx, userId);
|
|
10497
|
+
}),
|
|
10498
|
+
matcher: (ctx) => ctx.request?.method !== "GET"
|
|
10499
|
+
}
|
|
10500
|
+
]
|
|
8853
10501
|
},
|
|
8854
10502
|
endpoints: {
|
|
8855
10503
|
getDashConfig: getConfig(settings),
|
|
@@ -8924,15 +10572,178 @@ const dash = (options) => {
|
|
|
8924
10572
|
dashCompleteInvitationSocial: completeInvitationSocial(settings),
|
|
8925
10573
|
dashCheckUserExists: checkUserExists(settings),
|
|
8926
10574
|
listDashOrganizationDirectories: listOrganizationDirectories(settings),
|
|
8927
|
-
createDashOrganizationDirectory:
|
|
8928
|
-
deleteDashOrganizationDirectory:
|
|
8929
|
-
regenerateDashDirectoryToken:
|
|
10575
|
+
createDashOrganizationDirectory: createOrganizationDirectoryLegacy(settings),
|
|
10576
|
+
deleteDashOrganizationDirectory: deleteOrganizationDirectoryLegacy(settings),
|
|
10577
|
+
regenerateDashDirectoryToken: regenerateDirectoryTokenLegacy(settings),
|
|
10578
|
+
getDashManagedOrganizationDirectory: getOrganizationDirectory(settings),
|
|
10579
|
+
createDashManagedOrganizationDirectory: createOrganizationDirectory(settings),
|
|
10580
|
+
rotateDashManagedDirectoryCredential: rotateDirectoryCredential(settings),
|
|
10581
|
+
revokeDashManagedDirectoryCredential: revokeDirectoryCredential(settings),
|
|
10582
|
+
listDashManagedDirectoryEvents: listDirectoryEvents(settings),
|
|
10583
|
+
decommissionDashManagedOrganizationDirectory: decommissionOrganizationDirectory(settings),
|
|
10584
|
+
unpairDashManagedOrganizationDirectory: unpairOrganizationDirectory(settings),
|
|
8930
10585
|
dashExecuteAdapter: executeAdapter(settings)
|
|
8931
10586
|
},
|
|
8932
|
-
schema:
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
10587
|
+
schema: {
|
|
10588
|
+
...opts.activityTracking?.enabled ? { user: { fields: { lastActiveAt: {
|
|
10589
|
+
type: "date",
|
|
10590
|
+
required: false
|
|
10591
|
+
} } } } : {},
|
|
10592
|
+
...opts.managedDirectorySync?.enabled ? {
|
|
10593
|
+
directorySyncConnection: { fields: {
|
|
10594
|
+
organizationId: {
|
|
10595
|
+
type: "string",
|
|
10596
|
+
required: true,
|
|
10597
|
+
index: true
|
|
10598
|
+
},
|
|
10599
|
+
providerId: {
|
|
10600
|
+
type: "string",
|
|
10601
|
+
required: true
|
|
10602
|
+
},
|
|
10603
|
+
aliasKey: {
|
|
10604
|
+
type: "string",
|
|
10605
|
+
required: true,
|
|
10606
|
+
unique: true,
|
|
10607
|
+
returned: false
|
|
10608
|
+
},
|
|
10609
|
+
provisioningDomainId: {
|
|
10610
|
+
type: "string",
|
|
10611
|
+
required: true,
|
|
10612
|
+
unique: true
|
|
10613
|
+
},
|
|
10614
|
+
activeOrganizationKey: {
|
|
10615
|
+
type: "string",
|
|
10616
|
+
required: true,
|
|
10617
|
+
unique: true,
|
|
10618
|
+
returned: false
|
|
10619
|
+
},
|
|
10620
|
+
connectionId: {
|
|
10621
|
+
type: "string",
|
|
10622
|
+
required: false,
|
|
10623
|
+
unique: true
|
|
10624
|
+
},
|
|
10625
|
+
creationRequestId: {
|
|
10626
|
+
type: "string",
|
|
10627
|
+
required: true,
|
|
10628
|
+
unique: true,
|
|
10629
|
+
returned: false
|
|
10630
|
+
},
|
|
10631
|
+
status: {
|
|
10632
|
+
type: "string",
|
|
10633
|
+
required: true
|
|
10634
|
+
},
|
|
10635
|
+
revision: {
|
|
10636
|
+
type: "number",
|
|
10637
|
+
required: true,
|
|
10638
|
+
defaultValue: 0,
|
|
10639
|
+
returned: false
|
|
10640
|
+
},
|
|
10641
|
+
createdAt: {
|
|
10642
|
+
type: "date",
|
|
10643
|
+
required: true
|
|
10644
|
+
},
|
|
10645
|
+
createdByActorId: {
|
|
10646
|
+
type: "string",
|
|
10647
|
+
required: true
|
|
10648
|
+
},
|
|
10649
|
+
updatedAt: {
|
|
10650
|
+
type: "date",
|
|
10651
|
+
required: true
|
|
10652
|
+
},
|
|
10653
|
+
lastActorId: {
|
|
10654
|
+
type: "string",
|
|
10655
|
+
required: true
|
|
10656
|
+
},
|
|
10657
|
+
ssoProviderId: {
|
|
10658
|
+
type: "string",
|
|
10659
|
+
required: false
|
|
10660
|
+
},
|
|
10661
|
+
ssoProviderRecordId: {
|
|
10662
|
+
type: "string",
|
|
10663
|
+
required: false,
|
|
10664
|
+
index: true
|
|
10665
|
+
},
|
|
10666
|
+
activeSsoProviderKey: {
|
|
10667
|
+
type: "string",
|
|
10668
|
+
required: true,
|
|
10669
|
+
unique: true,
|
|
10670
|
+
returned: false
|
|
10671
|
+
},
|
|
10672
|
+
serializedSsoPairing: {
|
|
10673
|
+
type: "string",
|
|
10674
|
+
required: false,
|
|
10675
|
+
returned: false
|
|
10676
|
+
},
|
|
10677
|
+
pairingEnforced: {
|
|
10678
|
+
type: "boolean",
|
|
10679
|
+
required: true,
|
|
10680
|
+
defaultValue: false
|
|
10681
|
+
},
|
|
10682
|
+
unpairedAt: {
|
|
10683
|
+
type: "date",
|
|
10684
|
+
required: false
|
|
10685
|
+
},
|
|
10686
|
+
unpairedBy: {
|
|
10687
|
+
type: "string",
|
|
10688
|
+
required: false
|
|
10689
|
+
},
|
|
10690
|
+
decommissionStartedAt: {
|
|
10691
|
+
type: "date",
|
|
10692
|
+
required: false
|
|
10693
|
+
},
|
|
10694
|
+
decommissionedAt: {
|
|
10695
|
+
type: "date",
|
|
10696
|
+
required: false
|
|
10697
|
+
},
|
|
10698
|
+
lastError: {
|
|
10699
|
+
type: "string",
|
|
10700
|
+
required: false,
|
|
10701
|
+
returned: false
|
|
10702
|
+
}
|
|
10703
|
+
} },
|
|
10704
|
+
directorySyncMembershipProvenance: { fields: {
|
|
10705
|
+
membershipKey: {
|
|
10706
|
+
type: "string",
|
|
10707
|
+
required: true,
|
|
10708
|
+
unique: true,
|
|
10709
|
+
returned: false
|
|
10710
|
+
},
|
|
10711
|
+
organizationId: {
|
|
10712
|
+
type: "string",
|
|
10713
|
+
required: true,
|
|
10714
|
+
index: true
|
|
10715
|
+
},
|
|
10716
|
+
userId: {
|
|
10717
|
+
type: "string",
|
|
10718
|
+
required: true,
|
|
10719
|
+
index: true
|
|
10720
|
+
},
|
|
10721
|
+
memberId: {
|
|
10722
|
+
type: "string",
|
|
10723
|
+
required: true,
|
|
10724
|
+
unique: true
|
|
10725
|
+
},
|
|
10726
|
+
ownership: {
|
|
10727
|
+
type: "string",
|
|
10728
|
+
required: true,
|
|
10729
|
+
returned: false
|
|
10730
|
+
},
|
|
10731
|
+
provisioningDomainId: {
|
|
10732
|
+
type: "string",
|
|
10733
|
+
required: true,
|
|
10734
|
+
index: true
|
|
10735
|
+
},
|
|
10736
|
+
createdAt: {
|
|
10737
|
+
type: "date",
|
|
10738
|
+
required: true
|
|
10739
|
+
},
|
|
10740
|
+
updatedAt: {
|
|
10741
|
+
type: "date",
|
|
10742
|
+
required: true
|
|
10743
|
+
}
|
|
10744
|
+
} }
|
|
10745
|
+
} : {}
|
|
10746
|
+
}
|
|
8936
10747
|
};
|
|
8937
10748
|
};
|
|
8938
10749
|
//#endregion
|