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