@better-auth/infra 0.4.1 → 0.4.2
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 +11 -0
- package/dist/client.mjs +2 -2
- package/dist/{constants-CtvGRrlI.mjs → constants-CDhjfr8S.mjs} +1 -1
- package/dist/{crypto-CV91nSbp.mjs → crypto-D3yZH0Cy.mjs} +1 -1
- package/dist/email.mjs +1 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.mjs +474 -119
- package/dist/native.mjs +2 -2
- package/dist/{pow-retry-7sPi32Ow.mjs → pow-retry-jWGI__oh.mjs} +1 -1
- package/package.json +4 -3
- package/dist/saml-policy-DQR0MBLk.mjs +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to `@better-auth/infra` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.2] - 2026-08-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Identification on OAuth callbacks** — Sentinel and dash run identification on GET OAuth callback routes and persist `X-Request-Id` as the `__infra-rid` cookie in an after hook, so visitor identity resolves from KV across redirect-based social/OAuth sign-in (where identify headers are absent).
|
|
13
|
+
- **Issuer-scoped account lookups** — OAuth callback user resolution honors `account.identityStrategy` (`issuer` vs `provider-id`) when deriving account keys, matching Better Auth 1.7 issuer-scoped identity and the provider-scoped compatibility mode.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **No runtime dependency on SCIM / SSO packages** — Directory-sync SCIM user-link acquisition and SAML SP policy checks are implemented in-package (aligned with `@better-auth/scim` / `@better-auth/sso`). Those packages remain type-only deps and are excluded from the published bundle.
|
|
18
|
+
|
|
8
19
|
## [0.4.1] - 2026-08-19
|
|
9
20
|
|
|
10
21
|
### Fixed
|
package/dist/client.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as hash, o as createKV } from "./crypto-
|
|
2
|
-
import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, s as generateRequestId, t as createPowRetryTimeout } from "./pow-retry-
|
|
1
|
+
import { n as hash, o as createKV } from "./crypto-D3yZH0Cy.mjs";
|
|
2
|
+
import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, s as generateRequestId, t as createPowRetryTimeout } from "./pow-retry-jWGI__oh.mjs";
|
|
3
3
|
import { env } from "@better-auth/core/env";
|
|
4
4
|
//#region src/sentinel/fingerprint.ts
|
|
5
5
|
function murmurhash3(str, seed = 0) {
|
package/dist/email.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-
|
|
1
|
+
import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-CDhjfr8S.mjs";
|
|
2
2
|
import { logger } from "better-auth";
|
|
3
3
|
import { env } from "@better-auth/core/env";
|
|
4
4
|
import { createFetch } from "@better-fetch/fetch";
|
package/dist/index.d.mts
CHANGED
|
@@ -249,7 +249,22 @@ type DashSCIMManagedConnectionEventType = "connection.created" | "credential.iss
|
|
|
249
249
|
/** Lifecycle state for one Infrastructure-owned directory-sync alias. */
|
|
250
250
|
type DirectorySyncConnectionStatus = "active" | "decommissioning" | "decommissioned";
|
|
251
251
|
type DirectorySyncMode = "legacy" | "managed" | "unavailable";
|
|
252
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Structural stand-in for the SCIM plugin instance.
|
|
254
|
+
* Avoids `import("@better-auth/scim")` on the public type surface so consumers
|
|
255
|
+
* without that package can still typecheck the root entry.
|
|
256
|
+
*/
|
|
257
|
+
type SCIMPlugin = {
|
|
258
|
+
id?: string;
|
|
259
|
+
options?: {
|
|
260
|
+
providerOwnership?: {
|
|
261
|
+
enabled?: boolean;
|
|
262
|
+
};
|
|
263
|
+
managedConnections?: unknown;
|
|
264
|
+
[key: string]: unknown;
|
|
265
|
+
};
|
|
266
|
+
endpoints: Record<string, unknown>;
|
|
267
|
+
};
|
|
253
268
|
/** Public credential metadata. Raw bearer tokens are never included here. */
|
|
254
269
|
interface DashDirectoryCredential {
|
|
255
270
|
credentialId: string;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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, n as hash$1, 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-CDhjfr8S.mjs";
|
|
2
|
+
import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-D3yZH0Cy.mjs";
|
|
3
3
|
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
4
4
|
import { getCurrentAdapter, getCurrentAuthContext, getCurrentDBAdapterAsyncLocalStorage, runWithTransaction } from "@better-auth/core/context";
|
|
5
5
|
import { APIError, generateId, getAuthTables, logger } from "better-auth";
|
|
@@ -7,8 +7,11 @@ import { env } from "@better-auth/core/env";
|
|
|
7
7
|
import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
|
|
8
8
|
import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
|
|
9
9
|
import { createFetch } from "@better-fetch/fetch";
|
|
10
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
10
11
|
import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js";
|
|
11
12
|
import z, { z as z$1 } from "zod";
|
|
13
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
14
|
+
import { base64Url } from "@better-auth/utils/base64";
|
|
12
15
|
import { createLocalJWKSet, jwtVerify } from "jose";
|
|
13
16
|
import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
|
|
14
17
|
import { createOTP } from "@better-auth/utils/otp";
|
|
@@ -374,6 +377,33 @@ function createLocalAccountIssuer(providerId) {
|
|
|
374
377
|
function createOAuthAccountIssuer(providerId) {
|
|
375
378
|
return `local:oauth:${encodeURIComponent(providerId)}`;
|
|
376
379
|
}
|
|
380
|
+
/** Configured strategy when set; `null` when omitted (pre-option or default). */
|
|
381
|
+
function getConfiguredAccountIdentityStrategy(account) {
|
|
382
|
+
const strategy = account?.identityStrategy;
|
|
383
|
+
if (strategy === "provider-id" || strategy === "issuer") return strategy;
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Effective strategy for account-key derivation.
|
|
388
|
+
* Matches better-auth: omit/`"issuer"` → issuer-scoped; `"provider-id"` → provider-scoped.
|
|
389
|
+
*/
|
|
390
|
+
function resolveAccountIdentityStrategy(account) {
|
|
391
|
+
return getConfiguredAccountIdentityStrategy(account) ?? "issuer";
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Resolves the OAuth account issuer the same way better-auth does under
|
|
395
|
+
* `account.identityStrategy`, including provider-scoped compatibility mode.
|
|
396
|
+
*/
|
|
397
|
+
async function resolveOAuthAccountIssuer(params) {
|
|
398
|
+
if (params.identityStrategy === "provider-id") return createOAuthAccountIssuer(params.providerId);
|
|
399
|
+
const { accountIssuer } = params;
|
|
400
|
+
if (typeof accountIssuer === "function") {
|
|
401
|
+
const issuer = await accountIssuer(params.context);
|
|
402
|
+
return typeof issuer === "string" && issuer.trim().length > 0 ? issuer : void 0;
|
|
403
|
+
}
|
|
404
|
+
if (typeof accountIssuer === "string" && accountIssuer.trim().length > 0) return accountIssuer;
|
|
405
|
+
return createOAuthAccountIssuer(params.providerId);
|
|
406
|
+
}
|
|
377
407
|
/** True when the installed better-auth build scopes accounts by issuer. */
|
|
378
408
|
function supportsIssuerScopedAccounts(adapter) {
|
|
379
409
|
return typeof adapter.findAccountByKey === "function";
|
|
@@ -408,7 +438,7 @@ function normalizeProviderSubject(subject) {
|
|
|
408
438
|
if (!subject || subject === "undefined" || subject === "null") return;
|
|
409
439
|
return subject;
|
|
410
440
|
}
|
|
411
|
-
async function resolveAccountKey(provider, tokens, profile) {
|
|
441
|
+
async function resolveAccountKey(provider, tokens, profile, identityStrategy) {
|
|
412
442
|
const keyedProvider = provider;
|
|
413
443
|
const context = {
|
|
414
444
|
tokens,
|
|
@@ -424,7 +454,12 @@ async function resolveAccountKey(provider, tokens, profile) {
|
|
|
424
454
|
}
|
|
425
455
|
const accountId = rawSubject ? normalizeProviderSubject(rawSubject) : void 0;
|
|
426
456
|
if (!accountId) return void 0;
|
|
427
|
-
const issuer =
|
|
457
|
+
const issuer = await resolveOAuthAccountIssuer({
|
|
458
|
+
providerId: provider.id,
|
|
459
|
+
identityStrategy,
|
|
460
|
+
accountIssuer: keyedProvider.accountIssuer,
|
|
461
|
+
context
|
|
462
|
+
});
|
|
428
463
|
if (!issuer) return void 0;
|
|
429
464
|
return {
|
|
430
465
|
issuer,
|
|
@@ -447,7 +482,8 @@ function instrumentSocialProviders(providers) {
|
|
|
447
482
|
if (user) try {
|
|
448
483
|
const endpointCtx = await getCurrentAuthContext();
|
|
449
484
|
const adapter = endpointCtx.context.internalAdapter;
|
|
450
|
-
const
|
|
485
|
+
const identityStrategy = resolveAccountIdentityStrategy(endpointCtx.context.options.account);
|
|
486
|
+
const accountKey = result.data && typeof adapter.findAccountOwnerByKey === "function" ? await resolveAccountKey(provider, token, result.data, identityStrategy) : void 0;
|
|
451
487
|
endpointCtx.context[OAUTH_CALLBACK_USER] = {
|
|
452
488
|
user,
|
|
453
489
|
accountKey
|
|
@@ -1053,6 +1089,49 @@ function resolveClientIpFromHeaders(headers, ipAddressHeaders) {
|
|
|
1053
1089
|
* when a request includes an X-Request-Id header.
|
|
1054
1090
|
*/
|
|
1055
1091
|
const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
|
|
1092
|
+
/** GET routes that need identification (OAuth callbacks, verify links, etc.). */
|
|
1093
|
+
const IDENTIFICATION_GET_ROUTES = [
|
|
1094
|
+
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
1095
|
+
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
1096
|
+
routes.DASH_IMPERSONATE_USER,
|
|
1097
|
+
routes.VERIFY_EMAIL,
|
|
1098
|
+
routes.MAGIC_LINK_VERIFY,
|
|
1099
|
+
routes.DASH_ACCEPT_INVITATION,
|
|
1100
|
+
routes.DASH_COMPLETE_INVITATION_SOCIAL
|
|
1101
|
+
];
|
|
1102
|
+
/** GET OAuth callbacks where user creation can run without identify headers. */
|
|
1103
|
+
const IDENTIFICATION_OAUTH_CALLBACK_GET_ROUTES = [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK];
|
|
1104
|
+
/**
|
|
1105
|
+
* Whether identification middleware should run for this request.
|
|
1106
|
+
* Non-GET always runs; GET only for routes that need cookie/KV resolution.
|
|
1107
|
+
*/
|
|
1108
|
+
function shouldRunIdentification(ctx, getRoutes = IDENTIFICATION_GET_ROUTES) {
|
|
1109
|
+
if (ctx.request?.method !== "GET") return true;
|
|
1110
|
+
return matchesAnyRoute(ctx.path, [...getRoutes]);
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Persist `X-Request-Id` as `__infra-rid` so redirect-flow OAuth callbacks
|
|
1114
|
+
* (GET, no identify headers) can resolve visitor identification from KV.
|
|
1115
|
+
* Must run in an after hook so Set-Cookie reaches the response.
|
|
1116
|
+
*/
|
|
1117
|
+
function createIdentificationCookieAfterMiddleware() {
|
|
1118
|
+
return createAuthMiddleware(async (ctx) => {
|
|
1119
|
+
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
1120
|
+
if (headerRequestId) {
|
|
1121
|
+
ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
1122
|
+
maxAge: 600,
|
|
1123
|
+
sameSite: "lax",
|
|
1124
|
+
httpOnly: true,
|
|
1125
|
+
path: "/"
|
|
1126
|
+
});
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (ctx.context.requestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, "", {
|
|
1130
|
+
maxAge: 0,
|
|
1131
|
+
path: "/"
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1056
1135
|
const identificationCache = /* @__PURE__ */ new Map();
|
|
1057
1136
|
const CACHE_TTL_MS = 6e4;
|
|
1058
1137
|
const CACHE_MAX_SIZE = 1e3;
|
|
@@ -2465,7 +2544,7 @@ const sentinel = (options) => {
|
|
|
2465
2544
|
hooks: {
|
|
2466
2545
|
before: [
|
|
2467
2546
|
{
|
|
2468
|
-
matcher: (ctx) => ctx
|
|
2547
|
+
matcher: (ctx) => shouldRunIdentification(ctx, IDENTIFICATION_OAUTH_CALLBACK_GET_ROUTES),
|
|
2469
2548
|
handler: createIdentificationMiddleware($kv, {
|
|
2470
2549
|
skipIdentification: (ctx) => isDashRoute(ctx.path),
|
|
2471
2550
|
retry: opts.kvOptions.retry
|
|
@@ -2567,74 +2646,81 @@ const sentinel = (options) => {
|
|
|
2567
2646
|
})
|
|
2568
2647
|
}
|
|
2569
2648
|
],
|
|
2570
|
-
after: [
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2649
|
+
after: [
|
|
2650
|
+
{
|
|
2651
|
+
matcher: (ctx) => shouldRunIdentification(ctx, IDENTIFICATION_OAUTH_CALLBACK_GET_ROUTES),
|
|
2652
|
+
handler: createIdentificationCookieAfterMiddleware()
|
|
2653
|
+
},
|
|
2654
|
+
{
|
|
2655
|
+
matcher: (ctx) => !!opts.security?.staleUsers?.enabled && !isDashRoute(ctx.path),
|
|
2656
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
2657
|
+
if (ctx.context.returned instanceof Error) return;
|
|
2658
|
+
const created = ctx.context.newSession;
|
|
2659
|
+
const userId = created?.user?.id ?? created?.session?.userId;
|
|
2660
|
+
const sessionToken = created?.session?.token;
|
|
2661
|
+
if (!userId || !sessionToken) return;
|
|
2662
|
+
let user = created?.user ?? null;
|
|
2663
|
+
try {
|
|
2664
|
+
user = await getUserById(userId, ctx, { includeLastActiveAt: activityTrackingEnabled }) ?? user;
|
|
2665
|
+
} catch (error) {
|
|
2666
|
+
logger.warn("[Sentinel] Failed to fetch user for stale-account check:", error);
|
|
2667
|
+
if (!user) return;
|
|
2668
|
+
}
|
|
2583
2669
|
if (!user) return;
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2670
|
+
recordCheck(ctx, "stale_users");
|
|
2671
|
+
const staleCheck = await securityService.checkStaleUser(userId, activityTrackingEnabled ? user.lastActiveAt ?? null : null);
|
|
2672
|
+
if (!staleCheck.isStale) return;
|
|
2673
|
+
const identification = ctx.context.identification;
|
|
2674
|
+
const staleOpts = opts.security?.staleUsers;
|
|
2675
|
+
const notificationPromises = [];
|
|
2676
|
+
if (staleCheck.notifyUser && user.email) notificationPromises.push(securityService.notifyStaleAccountUser(user.email, user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2677
|
+
if (staleCheck.notifyAdmin && staleOpts?.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2678
|
+
if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
|
|
2679
|
+
logger.error("[Sentinel] Failed to send stale account notifications:", error);
|
|
2680
|
+
});
|
|
2681
|
+
if (staleCheck.action !== "block") return;
|
|
2682
|
+
setOutcome(ctx, "blocked", "stale_users", {
|
|
2683
|
+
userId,
|
|
2684
|
+
daysSinceLastActive: staleCheck.daysSinceLastActive,
|
|
2685
|
+
staleDays: staleCheck.staleDays,
|
|
2686
|
+
lastActiveAt: staleCheck.lastActiveAt,
|
|
2687
|
+
notifyUser: staleCheck.notifyUser,
|
|
2688
|
+
notifyAdmin: staleCheck.notifyAdmin
|
|
2689
|
+
});
|
|
2690
|
+
emitEvaluation(ctx, trackEvent);
|
|
2691
|
+
try {
|
|
2692
|
+
await ctx.context.internalAdapter.deleteSession(sessionToken);
|
|
2693
|
+
} catch (error) {
|
|
2694
|
+
logger.warn("[Sentinel] Failed to delete stale-blocked session:", error);
|
|
2695
|
+
}
|
|
2696
|
+
deleteSessionCookie(ctx);
|
|
2697
|
+
ctx.context.setNewSession(null);
|
|
2698
|
+
throw new APIError("FORBIDDEN", STALE_ACCOUNT_BLOCK_ERROR);
|
|
2699
|
+
})
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
|
|
2703
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
2704
|
+
const untrustedVisitorId = ctx.context.untrustedVisitorId;
|
|
2705
|
+
const ip = ctx.context.ip;
|
|
2706
|
+
const body = ctx.body;
|
|
2707
|
+
const loginId = matchesAnyRoute(ctx.path, [routes.SIGN_IN_USERNAME]) ? body?.username : body?.email;
|
|
2708
|
+
const isPasswordSignInRoute = matchesAnyRoute(ctx.path, [
|
|
2709
|
+
routes.SIGN_IN_EMAIL,
|
|
2710
|
+
routes.SIGN_IN_USERNAME,
|
|
2711
|
+
routes.SIGN_IN_EMAIL_OTP
|
|
2712
|
+
]);
|
|
2713
|
+
emitEvaluation(ctx, trackEvent, {
|
|
2714
|
+
identifier: loginId,
|
|
2715
|
+
userAgent: ctx.headers?.get?.("user-agent") || ""
|
|
2716
|
+
});
|
|
2717
|
+
const returned = ctx.context.returned;
|
|
2718
|
+
const staleBlocked = isStaleAccountError(returned);
|
|
2719
|
+
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));
|
|
2720
|
+
if (isPasswordSignInRoute && (!(returned instanceof Error) || staleBlocked) && loginId) await ctx.context.runInBackgroundOrAwait(securityService.clearFailedAttempts(loginId));
|
|
2721
|
+
})
|
|
2722
|
+
}
|
|
2723
|
+
]
|
|
2638
2724
|
}
|
|
2639
2725
|
};
|
|
2640
2726
|
};
|
|
@@ -2821,12 +2907,98 @@ function setCredentialResponseSecurityHeaders(ctx) {
|
|
|
2821
2907
|
ctx.setHeader("Referrer-Policy", "no-referrer");
|
|
2822
2908
|
}
|
|
2823
2909
|
//#endregion
|
|
2824
|
-
//#region src/directory-sync/
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2910
|
+
//#region src/directory-sync/saml-policy.ts
|
|
2911
|
+
/**
|
|
2912
|
+
* Aligned with `@better-auth/sso` SAML metadata constants / helpers.
|
|
2913
|
+
* @see better-auth packages/sso parseSAMLServiceProviderMetadata
|
|
2914
|
+
*/
|
|
2915
|
+
const SAML_METADATA_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:metadata";
|
|
2916
|
+
const HTTP_POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
|
|
2917
|
+
/**
|
|
2918
|
+
* Whether a SAML provider config requires signed assertions.
|
|
2919
|
+
*
|
|
2920
|
+
* Matches `@better-auth/sso` `deriveSAMLServiceProviderPolicy(...).wantAssertionsSigned`.
|
|
2921
|
+
* Invalid or unusable custom SP metadata throws (pairing maps this to BAD_REQUEST).
|
|
2922
|
+
*/
|
|
2923
|
+
function requiresSignedSAMLAssertions(config) {
|
|
2924
|
+
const metadata = config.spMetadata?.metadata;
|
|
2925
|
+
if (!metadata) return config.wantAssertionsSigned === true;
|
|
2926
|
+
const parsedMetadata = parseSAMLServiceProviderMetadata(metadata);
|
|
2927
|
+
if (!parsedMetadata.postAssertionConsumerServiceUrls.length) throw new Error("Unusable SAML service provider metadata");
|
|
2928
|
+
return parsedMetadata.wantAssertionsSigned;
|
|
2829
2929
|
}
|
|
2930
|
+
/**
|
|
2931
|
+
* Parses the security- and identity-relevant parts of SAML service-provider
|
|
2932
|
+
* metadata without relying on local-name-only XML matching.
|
|
2933
|
+
*
|
|
2934
|
+
* Port of `@better-auth/sso` `parseSAMLServiceProviderMetadata`.
|
|
2935
|
+
*/
|
|
2936
|
+
function parseSAMLServiceProviderMetadata(metadata) {
|
|
2937
|
+
const document = new DOMParser({ onError: (_level, message) => {
|
|
2938
|
+
throw new Error(message);
|
|
2939
|
+
} }).parseFromString(metadata, "text/xml");
|
|
2940
|
+
const entityDescriptor = document.documentElement;
|
|
2941
|
+
if (!entityDescriptor || entityDescriptor.localName !== "EntityDescriptor" || entityDescriptor.namespaceURI !== SAML_METADATA_NAMESPACE) throw new Error("Invalid SAML EntityDescriptor");
|
|
2942
|
+
const entityID = entityDescriptor.getAttribute("entityID")?.trim();
|
|
2943
|
+
if (!entityID) throw new Error("Missing SAML entityID");
|
|
2944
|
+
const serviceProviderDescriptors = directMetadataChildren(entityDescriptor, "SPSSODescriptor");
|
|
2945
|
+
if (serviceProviderDescriptors.length === 0) throw new Error("SAML metadata must contain an SPSSODescriptor");
|
|
2946
|
+
const acceptedServiceProviderDescriptors = new Set(serviceProviderDescriptors);
|
|
2947
|
+
for (const element of Array.from(document.getElementsByTagName("*"))) {
|
|
2948
|
+
if (element.localName === "EntityDescriptor" && element !== entityDescriptor) throw new Error("Invalid nested SAML EntityDescriptor");
|
|
2949
|
+
if (element.localName === "SPSSODescriptor" && !acceptedServiceProviderDescriptors.has(element)) throw new Error("Invalid SAML SPSSODescriptor namespace or position");
|
|
2950
|
+
if ((element.localName === "AssertionConsumerService" || element.localName === "NameIDFormat") && (element.namespaceURI !== SAML_METADATA_NAMESPACE || !element.parentNode || !isElement(element.parentNode) || !acceptedServiceProviderDescriptors.has(element.parentNode))) throw new Error(`Invalid SAML ${element.localName} namespace or position`);
|
|
2951
|
+
}
|
|
2952
|
+
const postAssertionConsumerServiceUrls = [];
|
|
2953
|
+
const nameIDFormats = [];
|
|
2954
|
+
let wantAssertionsSigned = false;
|
|
2955
|
+
for (const descriptor of serviceProviderDescriptors) {
|
|
2956
|
+
wantAssertionsSigned = parseXMLSchemaBoolean(descriptor.getAttribute("WantAssertionsSigned")) || wantAssertionsSigned;
|
|
2957
|
+
for (const nameIDFormat of directMetadataChildren(descriptor, "NameIDFormat")) {
|
|
2958
|
+
const value = nameIDFormat.textContent?.trim();
|
|
2959
|
+
if (value) nameIDFormats.push(value);
|
|
2960
|
+
}
|
|
2961
|
+
for (const service of directMetadataChildren(descriptor, "AssertionConsumerService")) {
|
|
2962
|
+
if (service.getAttribute("Binding") !== HTTP_POST_BINDING) continue;
|
|
2963
|
+
const location = service.getAttribute("Location")?.trim();
|
|
2964
|
+
if (!location || !isAbsoluteHttpUrl(location)) throw new Error("Invalid SAML POST AssertionConsumerService");
|
|
2965
|
+
postAssertionConsumerServiceUrls.push(location);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
return {
|
|
2969
|
+
entityID,
|
|
2970
|
+
nameIDFormats: [...new Set(nameIDFormats)],
|
|
2971
|
+
postAssertionConsumerServiceUrls: [...new Set(postAssertionConsumerServiceUrls)],
|
|
2972
|
+
wantAssertionsSigned
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
function isElement(node) {
|
|
2976
|
+
return node.nodeType === 1;
|
|
2977
|
+
}
|
|
2978
|
+
function directMetadataChildren(element, localName) {
|
|
2979
|
+
return Array.from(element.childNodes).filter((node) => isElement(node) && node.localName === localName && node.namespaceURI === SAML_METADATA_NAMESPACE);
|
|
2980
|
+
}
|
|
2981
|
+
function parseXMLSchemaBoolean(value) {
|
|
2982
|
+
if (value === null) return false;
|
|
2983
|
+
switch (value.trim()) {
|
|
2984
|
+
case "true":
|
|
2985
|
+
case "1": return true;
|
|
2986
|
+
case "false":
|
|
2987
|
+
case "0": return false;
|
|
2988
|
+
default: throw new Error("Invalid XML Schema boolean");
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
function isAbsoluteHttpUrl(value) {
|
|
2992
|
+
if (value.includes("#")) return false;
|
|
2993
|
+
try {
|
|
2994
|
+
const url = new URL(value);
|
|
2995
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
2996
|
+
} catch {
|
|
2997
|
+
return false;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
//#endregion
|
|
3001
|
+
//#region src/directory-sync/pairing.ts
|
|
2830
3002
|
function isRecord$1(value) {
|
|
2831
3003
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2832
3004
|
}
|
|
@@ -2879,10 +3051,9 @@ async function resolveDirectorySyncSSOPairing(ctx, organizationId, pairing) {
|
|
|
2879
3051
|
if (!oidcConfiguration || samlConfiguration) throw ctx.error("BAD_REQUEST", { message: "The selected SSO provider is not an OIDC provider" });
|
|
2880
3052
|
} else {
|
|
2881
3053
|
if (!samlConfiguration || oidcConfiguration) throw ctx.error("BAD_REQUEST", { message: "The selected SSO provider is not a SAML provider" });
|
|
2882
|
-
const samlPolicy = await loadSAMLPolicy();
|
|
2883
3054
|
let wantAssertionsSigned = false;
|
|
2884
3055
|
try {
|
|
2885
|
-
wantAssertionsSigned =
|
|
3056
|
+
wantAssertionsSigned = requiresSignedSAMLAssertions({
|
|
2886
3057
|
spMetadata: isRecord$1(samlConfiguration.spMetadata) ? { metadata: typeof samlConfiguration.spMetadata.metadata === "string" ? samlConfiguration.spMetadata.metadata : void 0 } : void 0,
|
|
2887
3058
|
wantAssertionsSigned: samlConfiguration.wantAssertionsSigned === true
|
|
2888
3059
|
});
|
|
@@ -2929,17 +3100,228 @@ async function guardDirectorySyncSSOProviderMutation(input, context) {
|
|
|
2929
3100
|
}
|
|
2930
3101
|
}
|
|
2931
3102
|
//#endregion
|
|
3103
|
+
//#region src/directory-sync/scim-user-link.ts
|
|
3104
|
+
function createScopedKey(parts) {
|
|
3105
|
+
return base64Url.encode(sha256(new TextEncoder().encode(JSON.stringify(parts))), { padding: false });
|
|
3106
|
+
}
|
|
3107
|
+
function createSCIMUserExternalIdKey(connectionId, externalId) {
|
|
3108
|
+
return createScopedKey([
|
|
3109
|
+
"scim-user-external-id",
|
|
3110
|
+
connectionId,
|
|
3111
|
+
externalId
|
|
3112
|
+
]);
|
|
3113
|
+
}
|
|
3114
|
+
function createSCIMConnectionKey(connectionId) {
|
|
3115
|
+
return createScopedKey(["scim-connection", connectionId]);
|
|
3116
|
+
}
|
|
3117
|
+
var SCIMIdentityMutationConflict = class extends Error {
|
|
3118
|
+
constructor() {
|
|
3119
|
+
super("The SCIM identity changed concurrently; retry the request");
|
|
3120
|
+
this.name = "SCIMIdentityMutationConflict";
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
function concurrentIdentityMutation() {
|
|
3124
|
+
throw new SCIMIdentityMutationConflict();
|
|
3125
|
+
}
|
|
3126
|
+
function hasIncrementOne(database) {
|
|
3127
|
+
return typeof database.incrementOne === "function";
|
|
3128
|
+
}
|
|
3129
|
+
async function tryFenceActiveSCIMConnection(database, connectionId) {
|
|
3130
|
+
return database.incrementOne({
|
|
3131
|
+
model: "scimConnectionBinding",
|
|
3132
|
+
where: [
|
|
3133
|
+
{
|
|
3134
|
+
field: "connectionKey",
|
|
3135
|
+
value: createSCIMConnectionKey(connectionId)
|
|
3136
|
+
},
|
|
3137
|
+
{
|
|
3138
|
+
field: "connectionId",
|
|
3139
|
+
value: connectionId
|
|
3140
|
+
},
|
|
3141
|
+
{
|
|
3142
|
+
field: "decommissionStatus",
|
|
3143
|
+
value: "active"
|
|
3144
|
+
}
|
|
3145
|
+
],
|
|
3146
|
+
increment: { decommissionRevision: 1 }
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Acquires an active provisioned User link inside the caller's transaction.
|
|
3151
|
+
* Returns null when no active link exists. Throws on concurrent identity mutation.
|
|
3152
|
+
*/
|
|
3153
|
+
async function acquireActiveSCIMUserLink(reference, context) {
|
|
3154
|
+
if (!hasIncrementOne(context.database)) return null;
|
|
3155
|
+
const database = context.database;
|
|
3156
|
+
const externalIdKey = createSCIMUserExternalIdKey(reference.connectionId, reference.externalId);
|
|
3157
|
+
const source = await database.findOne({
|
|
3158
|
+
model: "scimUser",
|
|
3159
|
+
where: [
|
|
3160
|
+
{
|
|
3161
|
+
field: "connectionId",
|
|
3162
|
+
value: reference.connectionId
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
field: "externalIdKey",
|
|
3166
|
+
value: externalIdKey
|
|
3167
|
+
},
|
|
3168
|
+
{
|
|
3169
|
+
field: "externalId",
|
|
3170
|
+
value: reference.externalId
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
field: "active",
|
|
3174
|
+
value: true
|
|
3175
|
+
}
|
|
3176
|
+
]
|
|
3177
|
+
});
|
|
3178
|
+
if (!source) return null;
|
|
3179
|
+
const binding = await database.findOne({
|
|
3180
|
+
model: "scimConnectionBinding",
|
|
3181
|
+
where: [
|
|
3182
|
+
{
|
|
3183
|
+
field: "connectionKey",
|
|
3184
|
+
value: createSCIMConnectionKey(reference.connectionId)
|
|
3185
|
+
},
|
|
3186
|
+
{
|
|
3187
|
+
field: "connectionId",
|
|
3188
|
+
value: reference.connectionId
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
field: "decommissionStatus",
|
|
3192
|
+
value: "active"
|
|
3193
|
+
}
|
|
3194
|
+
]
|
|
3195
|
+
});
|
|
3196
|
+
if (!binding || binding.provisioningDomainId !== source.provisioningDomainId) return null;
|
|
3197
|
+
if (await database.findOne({
|
|
3198
|
+
model: "scimIdentityTombstone",
|
|
3199
|
+
where: [
|
|
3200
|
+
{
|
|
3201
|
+
field: "connectionId",
|
|
3202
|
+
value: reference.connectionId
|
|
3203
|
+
},
|
|
3204
|
+
{
|
|
3205
|
+
field: "externalIdKey",
|
|
3206
|
+
value: externalIdKey
|
|
3207
|
+
},
|
|
3208
|
+
{
|
|
3209
|
+
field: "externalId",
|
|
3210
|
+
value: reference.externalId
|
|
3211
|
+
}
|
|
3212
|
+
]
|
|
3213
|
+
})) return null;
|
|
3214
|
+
const subject = await database.findOne({
|
|
3215
|
+
model: "scimSubject",
|
|
3216
|
+
where: [{
|
|
3217
|
+
field: "userId",
|
|
3218
|
+
value: source.userId
|
|
3219
|
+
}]
|
|
3220
|
+
});
|
|
3221
|
+
if (!subject) return null;
|
|
3222
|
+
if (!await database.findOne({
|
|
3223
|
+
model: "user",
|
|
3224
|
+
where: [{
|
|
3225
|
+
field: "id",
|
|
3226
|
+
value: source.userId
|
|
3227
|
+
}]
|
|
3228
|
+
})) return null;
|
|
3229
|
+
const acquiredSubject = await database.incrementOne({
|
|
3230
|
+
model: "scimSubject",
|
|
3231
|
+
where: [
|
|
3232
|
+
{
|
|
3233
|
+
field: "id",
|
|
3234
|
+
value: subject.id
|
|
3235
|
+
},
|
|
3236
|
+
{
|
|
3237
|
+
field: "userId",
|
|
3238
|
+
value: source.userId
|
|
3239
|
+
},
|
|
3240
|
+
{
|
|
3241
|
+
field: "revision",
|
|
3242
|
+
value: subject.revision
|
|
3243
|
+
}
|
|
3244
|
+
],
|
|
3245
|
+
increment: { revision: 1 },
|
|
3246
|
+
set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
3247
|
+
});
|
|
3248
|
+
if (!acquiredSubject) concurrentIdentityMutation();
|
|
3249
|
+
const acquiredSource = await database.findOne({
|
|
3250
|
+
model: "scimUser",
|
|
3251
|
+
where: [
|
|
3252
|
+
{
|
|
3253
|
+
field: "id",
|
|
3254
|
+
value: source.id
|
|
3255
|
+
},
|
|
3256
|
+
{
|
|
3257
|
+
field: "connectionId",
|
|
3258
|
+
value: reference.connectionId
|
|
3259
|
+
},
|
|
3260
|
+
{
|
|
3261
|
+
field: "provisioningDomainId",
|
|
3262
|
+
value: binding.provisioningDomainId
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
field: "userId",
|
|
3266
|
+
value: source.userId
|
|
3267
|
+
},
|
|
3268
|
+
{
|
|
3269
|
+
field: "connectionUserKey",
|
|
3270
|
+
value: source.connectionUserKey
|
|
3271
|
+
},
|
|
3272
|
+
{
|
|
3273
|
+
field: "externalIdKey",
|
|
3274
|
+
value: externalIdKey
|
|
3275
|
+
},
|
|
3276
|
+
{
|
|
3277
|
+
field: "externalId",
|
|
3278
|
+
value: reference.externalId
|
|
3279
|
+
},
|
|
3280
|
+
{
|
|
3281
|
+
field: "active",
|
|
3282
|
+
value: true
|
|
3283
|
+
}
|
|
3284
|
+
]
|
|
3285
|
+
});
|
|
3286
|
+
if (!acquiredSource || !acquiredSource.active || acquiredSource.userId !== acquiredSubject.userId) concurrentIdentityMutation();
|
|
3287
|
+
if (!await database.findOne({
|
|
3288
|
+
model: "user",
|
|
3289
|
+
where: [{
|
|
3290
|
+
field: "id",
|
|
3291
|
+
value: acquiredSource.userId
|
|
3292
|
+
}]
|
|
3293
|
+
})) concurrentIdentityMutation();
|
|
3294
|
+
if (await database.findOne({
|
|
3295
|
+
model: "scimIdentityTombstone",
|
|
3296
|
+
where: [
|
|
3297
|
+
{
|
|
3298
|
+
field: "connectionId",
|
|
3299
|
+
value: reference.connectionId
|
|
3300
|
+
},
|
|
3301
|
+
{
|
|
3302
|
+
field: "externalIdKey",
|
|
3303
|
+
value: externalIdKey
|
|
3304
|
+
},
|
|
3305
|
+
{
|
|
3306
|
+
field: "externalId",
|
|
3307
|
+
value: reference.externalId
|
|
3308
|
+
}
|
|
3309
|
+
]
|
|
3310
|
+
})) concurrentIdentityMutation();
|
|
3311
|
+
const acquiredBinding = await tryFenceActiveSCIMConnection(database, reference.connectionId);
|
|
3312
|
+
if (!acquiredBinding || acquiredBinding.id !== binding.id || acquiredBinding.provisioningDomainId !== acquiredSource.provisioningDomainId) concurrentIdentityMutation();
|
|
3313
|
+
return {
|
|
3314
|
+
scimUserId: source.id,
|
|
3315
|
+
userId: source.userId
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
//#endregion
|
|
2932
3319
|
//#region src/directory-sync/sso-user-resolution.ts
|
|
2933
3320
|
const GENERIC_REJECTION = {
|
|
2934
3321
|
action: "reject",
|
|
2935
3322
|
code: "DIRECTORY_SYNC_AUTHENTICATION_FAILED",
|
|
2936
3323
|
message: "Unable to sign in with this SSO connection"
|
|
2937
3324
|
};
|
|
2938
|
-
let scimCatalogModule;
|
|
2939
|
-
function loadSCIMCatalog() {
|
|
2940
|
-
scimCatalogModule ??= import("@better-auth/scim");
|
|
2941
|
-
return scimCatalogModule;
|
|
2942
|
-
}
|
|
2943
3325
|
function readStringExternalId(value) {
|
|
2944
3326
|
if (typeof value === "string" && value.length > 0) return value;
|
|
2945
3327
|
return null;
|
|
@@ -2985,9 +3367,7 @@ async function resolveOrganizationDirectorySyncUser(input, context) {
|
|
|
2985
3367
|
const externalId = readExternalId(input, pairing);
|
|
2986
3368
|
if (!externalId) return GENERIC_REJECTION;
|
|
2987
3369
|
try {
|
|
2988
|
-
const
|
|
2989
|
-
if (typeof catalog.acquireActiveSCIMUserLink !== "function") return GENERIC_REJECTION;
|
|
2990
|
-
const link = await catalog.acquireActiveSCIMUserLink({
|
|
3370
|
+
const link = await acquireActiveSCIMUserLink({
|
|
2991
3371
|
connectionId: directory.connectionId,
|
|
2992
3372
|
externalId
|
|
2993
3373
|
}, { database: context.database });
|
|
@@ -3911,6 +4291,7 @@ const getConfig = (options) => {
|
|
|
3911
4291
|
disableCSRFCheck: ctx.context.options.advanced?.disableCSRFCheck || false,
|
|
3912
4292
|
disableOriginCheck: ctx.context.options.advanced?.disableOriginCheck || false,
|
|
3913
4293
|
allowDifferentEmails: ctx.context.options.account?.accountLinking?.enabled && ctx.context.options.account?.accountLinking?.allowDifferentEmails || false,
|
|
4294
|
+
identityStrategy: getConfiguredAccountIdentityStrategy(ctx.context.options.account),
|
|
3914
4295
|
skipStateCookieCheck: ctx.context.options.account?.skipStateCookieCheck || false,
|
|
3915
4296
|
storeStateCookieStrategy: ctx.context.options.account?.storeStateStrategy || null,
|
|
3916
4297
|
cookieCache: {
|
|
@@ -4336,14 +4717,6 @@ async function recoverManagedDirectoryConnection(ctx, row, input, policy) {
|
|
|
4336
4717
|
async function createManagedDirectoryConnection(ctx, input, policy) {
|
|
4337
4718
|
await assertManagedDirectoryTransactionsConfigured(ctx);
|
|
4338
4719
|
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
4720
|
const aliasKey = await createAliasKey(input.organizationId, input.providerId);
|
|
4348
4721
|
const provisioningDomainId = await createProvisioningDomainId(input.organizationId, input.providerId);
|
|
4349
4722
|
const activeOrganizationKey = await createActiveOrganizationKey(input.organizationId);
|
|
@@ -10488,18 +10861,7 @@ const dash = (options) => {
|
|
|
10488
10861
|
},
|
|
10489
10862
|
hooks: {
|
|
10490
10863
|
before: [{
|
|
10491
|
-
matcher: (ctx) =>
|
|
10492
|
-
if (ctx.request?.method !== "GET") return true;
|
|
10493
|
-
return matchesAnyRoute(ctx.path, [
|
|
10494
|
-
routes.SIGN_IN_SOCIAL_CALLBACK,
|
|
10495
|
-
routes.SIGN_IN_OAUTH_CALLBACK,
|
|
10496
|
-
routes.DASH_IMPERSONATE_USER,
|
|
10497
|
-
routes.VERIFY_EMAIL,
|
|
10498
|
-
routes.MAGIC_LINK_VERIFY,
|
|
10499
|
-
routes.DASH_ACCEPT_INVITATION,
|
|
10500
|
-
routes.DASH_COMPLETE_INVITATION_SOCIAL
|
|
10501
|
-
]);
|
|
10502
|
-
},
|
|
10864
|
+
matcher: (ctx) => shouldRunIdentification(ctx, IDENTIFICATION_GET_ROUTES),
|
|
10503
10865
|
handler: createIdentificationMiddleware($kv, {
|
|
10504
10866
|
skipIdentification: (ctx) => isDashRoute(ctx.path),
|
|
10505
10867
|
retry: opts.kvOptions.retry
|
|
@@ -10532,19 +10894,12 @@ const dash = (options) => {
|
|
|
10532
10894
|
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
10895
|
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
10896
|
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
10897
|
})
|
|
10547
10898
|
},
|
|
10899
|
+
{
|
|
10900
|
+
matcher: (ctx) => shouldRunIdentification(ctx, IDENTIFICATION_GET_ROUTES),
|
|
10901
|
+
handler: createIdentificationCookieAfterMiddleware()
|
|
10902
|
+
},
|
|
10548
10903
|
{
|
|
10549
10904
|
handler: createAuthMiddleware(async (ctx) => {
|
|
10550
10905
|
if (!opts.activityTracking?.enabled) return;
|
package/dist/native.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as createKV, t as bytesToHex } from "./crypto-
|
|
2
|
-
import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-
|
|
1
|
+
import { o as createKV, t as bytesToHex } from "./crypto-D3yZH0Cy.mjs";
|
|
2
|
+
import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-jWGI__oh.mjs";
|
|
3
3
|
import { env } from "@better-auth/core/env";
|
|
4
4
|
import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
|
|
5
5
|
//#region src/sentinel/native/components.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-
|
|
1
|
+
import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-D3yZH0Cy.mjs";
|
|
2
2
|
//#region src/dash-client.ts
|
|
3
3
|
function resolveDashUserId(input, options) {
|
|
4
4
|
return input.userId || options?.resolveUserId?.({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/infra",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Dashboard and analytics plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@types/bun": "latest",
|
|
71
71
|
"@types/node": "^24.12.0",
|
|
72
72
|
"better-auth": "1.7.0-rc.1",
|
|
73
|
-
"expo-crypto": "^
|
|
73
|
+
"expo-crypto": "^57.0.1",
|
|
74
74
|
"happy-dom": "^20.9.0",
|
|
75
75
|
"msw": "^2.14.6",
|
|
76
76
|
"tsdown": "^0.22.14",
|
|
@@ -81,9 +81,10 @@
|
|
|
81
81
|
"@better-auth/utils": "^0.4.2",
|
|
82
82
|
"@better-fetch/fetch": "1.3.1",
|
|
83
83
|
"@noble/hashes": "^2.2.0",
|
|
84
|
+
"@xmldom/xmldom": "^0.9.10",
|
|
84
85
|
"better-call": "1.3.7",
|
|
85
86
|
"jose": "^6.2.4",
|
|
86
|
-
"libphonenumber-js": "^1.13.
|
|
87
|
+
"libphonenumber-js": "^1.13.11"
|
|
87
88
|
},
|
|
88
89
|
"peerDependencies": {
|
|
89
90
|
"better-auth": ">=1.4.0",
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
//#region src/directory-sync/saml-policy.ts
|
|
2
|
-
/**
|
|
3
|
-
* `deriveSAMLServiceProviderPolicy` ships in `@better-auth/sso` 1.7.0+.
|
|
4
|
-
* Load it at runtime so typecheck against older matrix versions still succeeds.
|
|
5
|
-
*/
|
|
6
|
-
async function requiresSignedSAMLAssertions(config) {
|
|
7
|
-
const sso = await import("@better-auth/sso");
|
|
8
|
-
if (typeof sso.deriveSAMLServiceProviderPolicy === "function") return sso.deriveSAMLServiceProviderPolicy(config).wantAssertionsSigned;
|
|
9
|
-
return config.wantAssertionsSigned === true;
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
export { requiresSignedSAMLAssertions };
|