@better-auth/infra 0.1.14 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/client.d.mts +2 -70
- package/dist/client.mjs +1 -87
- package/dist/dash-client-hJHp7l_X.d.mts +72 -0
- package/dist/index.d.mts +285 -308
- package/dist/index.mjs +199 -16
- package/dist/native.d.mts +18 -0
- package/dist/native.mjs +292 -0
- package/dist/pow-BUuN_EKw.mjs +131 -0
- package/package.json +28 -3
- package/dist/identification-DF2nvmng.mjs +0 -178
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { n as INFRA_KV_URL, r as KV_TIMEOUT_MS, t as INFRA_API_URL } from "./constants-DdWGfvz1.mjs";
|
|
2
|
-
import { n as createIdentificationMiddleware, t as IDENTIFICATION_COOKIE_NAME } from "./identification-DF2nvmng.mjs";
|
|
3
2
|
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
4
3
|
import { APIError, generateId, getAuthTables, logger, parseState } from "better-auth";
|
|
5
4
|
import { env } from "@better-auth/core/env";
|
|
@@ -10,7 +9,6 @@ import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-j
|
|
|
10
9
|
import { createLocalJWKSet, jwtVerify } from "jose";
|
|
11
10
|
import z$1, { z } from "zod";
|
|
12
11
|
import { setSessionCookie } from "better-auth/cookies";
|
|
13
|
-
import { DEFAULT_MAX_SAML_METADATA_SIZE, DigestAlgorithm, DiscoveryError, SignatureAlgorithm, discoverOIDCConfig } from "@better-auth/sso";
|
|
14
12
|
//#region src/options.ts
|
|
15
13
|
function resolveConnectionOptions(options) {
|
|
16
14
|
return {
|
|
@@ -853,6 +851,152 @@ const initTrackEvents = (options) => {
|
|
|
853
851
|
return { tracker: { trackEvent } };
|
|
854
852
|
};
|
|
855
853
|
//#endregion
|
|
854
|
+
//#region src/identification.ts
|
|
855
|
+
/**
|
|
856
|
+
* Identification Service
|
|
857
|
+
*
|
|
858
|
+
* Fetches identification data from the durable-kv service
|
|
859
|
+
* when a request includes an X-Request-Id header.
|
|
860
|
+
*/
|
|
861
|
+
const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
|
|
862
|
+
const identificationCache = /* @__PURE__ */ new Map();
|
|
863
|
+
const CACHE_TTL_MS = 6e4;
|
|
864
|
+
const CACHE_MAX_SIZE = 1e3;
|
|
865
|
+
let lastCleanup = Date.now();
|
|
866
|
+
function cleanupCache() {
|
|
867
|
+
const now = Date.now();
|
|
868
|
+
for (const [key, value] of identificationCache.entries()) if (now - value.timestamp > CACHE_TTL_MS) identificationCache.delete(key);
|
|
869
|
+
lastCleanup = now;
|
|
870
|
+
}
|
|
871
|
+
function maybeCleanup() {
|
|
872
|
+
if (Date.now() - lastCleanup > CACHE_TTL_MS || identificationCache.size > CACHE_MAX_SIZE) cleanupCache();
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Fetch identification data from durable-kv by requestId
|
|
876
|
+
*/
|
|
877
|
+
async function getIdentification(requestId, apiKey, kvUrl) {
|
|
878
|
+
maybeCleanup();
|
|
879
|
+
const cached = identificationCache.get(requestId);
|
|
880
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
|
|
881
|
+
const baseUrl = kvUrl || INFRA_KV_URL;
|
|
882
|
+
const maxRetries = 3;
|
|
883
|
+
const retryDelays = [
|
|
884
|
+
50,
|
|
885
|
+
100,
|
|
886
|
+
200
|
|
887
|
+
];
|
|
888
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
889
|
+
const response = await fetch(`${baseUrl}/identify/${requestId}`, {
|
|
890
|
+
method: "GET",
|
|
891
|
+
headers: { "x-api-key": apiKey },
|
|
892
|
+
signal: AbortSignal.timeout(KV_TIMEOUT_MS)
|
|
893
|
+
});
|
|
894
|
+
if (response.ok) {
|
|
895
|
+
const data = await response.json();
|
|
896
|
+
identificationCache.set(requestId, {
|
|
897
|
+
data,
|
|
898
|
+
timestamp: Date.now()
|
|
899
|
+
});
|
|
900
|
+
return data;
|
|
901
|
+
}
|
|
902
|
+
if (response.status === 404 && attempt < maxRetries) {
|
|
903
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (response.status !== 404) identificationCache.set(requestId, {
|
|
907
|
+
data: null,
|
|
908
|
+
timestamp: Date.now()
|
|
909
|
+
});
|
|
910
|
+
return null;
|
|
911
|
+
} catch (error) {
|
|
912
|
+
if (attempt === maxRetries) {
|
|
913
|
+
logger.error("[Dash] Failed to fetch identification:", error);
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt] || 50));
|
|
917
|
+
}
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Extract identification headers from a request
|
|
922
|
+
*/
|
|
923
|
+
function extractIdentificationHeaders(request) {
|
|
924
|
+
if (!request) return {
|
|
925
|
+
visitorId: null,
|
|
926
|
+
requestId: null
|
|
927
|
+
};
|
|
928
|
+
return {
|
|
929
|
+
visitorId: request.headers.get("X-Visitor-Id"),
|
|
930
|
+
requestId: request.headers.get("X-Request-Id")
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Early middleware that loads identification data
|
|
935
|
+
*/
|
|
936
|
+
function createIdentificationMiddleware(options) {
|
|
937
|
+
return createAuthMiddleware(async (ctx) => {
|
|
938
|
+
const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
|
|
939
|
+
const requestId = headerRequestId ?? ctx.getCookie("__infra-rid") ?? null;
|
|
940
|
+
ctx.context.visitorId = visitorId;
|
|
941
|
+
ctx.context.requestId = requestId;
|
|
942
|
+
if (requestId) ctx.context.identification = ctx.context.identification ?? await getIdentification(requestId, options.apiKey, options.kvUrl) ?? null;
|
|
943
|
+
else ctx.context.identification = null;
|
|
944
|
+
const ipConfig = ctx.context.options?.advanced?.ipAddress;
|
|
945
|
+
if (ipConfig?.disableIpTracking === true) {
|
|
946
|
+
ctx.context.location = void 0;
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const identification = ctx.context.identification;
|
|
950
|
+
if (requestId && identification) {
|
|
951
|
+
const loc = getLocation(identification);
|
|
952
|
+
ctx.context.location = {
|
|
953
|
+
ipAddress: identification.ip || void 0,
|
|
954
|
+
city: loc?.city || void 0,
|
|
955
|
+
country: loc?.country?.name || void 0,
|
|
956
|
+
countryCode: loc?.country?.code || void 0
|
|
957
|
+
};
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
|
|
961
|
+
const countryCode = getCountryCodeFromRequest(ctx.request);
|
|
962
|
+
if (ipAddress || countryCode) {
|
|
963
|
+
ctx.context.location = {
|
|
964
|
+
ipAddress,
|
|
965
|
+
countryCode
|
|
966
|
+
};
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
ctx.context.location = void 0;
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Get the visitor's location
|
|
974
|
+
*/
|
|
975
|
+
function getLocation(identification) {
|
|
976
|
+
if (!identification) return null;
|
|
977
|
+
return identification.location;
|
|
978
|
+
}
|
|
979
|
+
function getClientIpFromRequest(request, ipAddressHeaders) {
|
|
980
|
+
if (!request) return void 0;
|
|
981
|
+
const headers = ipAddressHeaders?.length ? ipAddressHeaders : [
|
|
982
|
+
"cf-connecting-ip",
|
|
983
|
+
"x-forwarded-for",
|
|
984
|
+
"x-real-ip",
|
|
985
|
+
"x-vercel-forwarded-for"
|
|
986
|
+
];
|
|
987
|
+
for (const headerName of headers) {
|
|
988
|
+
const value = request.headers.get(headerName);
|
|
989
|
+
if (!value) continue;
|
|
990
|
+
const ip = value.split(",")[0]?.trim();
|
|
991
|
+
if (ip) return ip;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function getCountryCodeFromRequest(request) {
|
|
995
|
+
if (!request) return void 0;
|
|
996
|
+
const cc = request.headers.get("cf-ipcountry") ?? request.headers.get("x-vercel-ip-country");
|
|
997
|
+
return cc ? cc.toUpperCase() : void 0;
|
|
998
|
+
}
|
|
999
|
+
//#endregion
|
|
856
1000
|
//#region src/validation/matchers.ts
|
|
857
1001
|
const paths = [
|
|
858
1002
|
"/sign-up/email",
|
|
@@ -2602,7 +2746,7 @@ const jwtValidateMiddleware = (options) => createAuthMiddleware(async (ctx) => {
|
|
|
2602
2746
|
});
|
|
2603
2747
|
//#endregion
|
|
2604
2748
|
//#region src/version.ts
|
|
2605
|
-
const PLUGIN_VERSION = "0.
|
|
2749
|
+
const PLUGIN_VERSION = "0.2.0";
|
|
2606
2750
|
//#endregion
|
|
2607
2751
|
//#region src/routes/auth/config.ts
|
|
2608
2752
|
const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
@@ -2655,7 +2799,7 @@ const getConfig = (options) => {
|
|
|
2655
2799
|
version: plugin.version,
|
|
2656
2800
|
options: sanitizePluginOptions(plugin.id, plugin.options)
|
|
2657
2801
|
};
|
|
2658
|
-
if (plugin.id === "dash") return {
|
|
2802
|
+
if (plugin.id === "dash" && !plugin.version) return {
|
|
2659
2803
|
...base,
|
|
2660
2804
|
version: PLUGIN_VERSION
|
|
2661
2805
|
};
|
|
@@ -4089,11 +4233,13 @@ const listOrganizations = (options) => {
|
|
|
4089
4233
|
field: "name",
|
|
4090
4234
|
value: searchTerm,
|
|
4091
4235
|
operator: "starts_with",
|
|
4236
|
+
mode: "insensitive",
|
|
4092
4237
|
connector: "OR"
|
|
4093
4238
|
}, {
|
|
4094
4239
|
field: "slug",
|
|
4095
4240
|
value: searchTerm,
|
|
4096
4241
|
operator: "starts_with",
|
|
4242
|
+
mode: "insensitive",
|
|
4097
4243
|
connector: "OR"
|
|
4098
4244
|
});
|
|
4099
4245
|
}
|
|
@@ -4109,7 +4255,7 @@ const listOrganizations = (options) => {
|
|
|
4109
4255
|
});
|
|
4110
4256
|
const needsInMemoryProcessing = sortBy === "members" || !!filterMembers;
|
|
4111
4257
|
const dbSortBy = sortBy === "members" ? "createdAt" : sortBy;
|
|
4112
|
-
const fetchLimit = needsInMemoryProcessing ?
|
|
4258
|
+
const fetchLimit = needsInMemoryProcessing ? 2500 : limit;
|
|
4113
4259
|
const fetchOffset = needsInMemoryProcessing ? 0 : offset;
|
|
4114
4260
|
const [organizations, initialTotal] = await Promise.all([ctx.context.adapter.findMany({
|
|
4115
4261
|
model: "organization",
|
|
@@ -5254,8 +5400,16 @@ function getSSOPlugin(ctx) {
|
|
|
5254
5400
|
}
|
|
5255
5401
|
//#endregion
|
|
5256
5402
|
//#region src/routes/sso-validation.ts
|
|
5257
|
-
|
|
5258
|
-
|
|
5403
|
+
/**
|
|
5404
|
+
* SAML metadata limits and algorithm URIs aligned with @better-auth/sso
|
|
5405
|
+
* (see packages/sso dist constants). Inlined so consumers (e.g. Metro) never
|
|
5406
|
+
* statically pull @better-auth/sso for validation-only code paths.
|
|
5407
|
+
*/
|
|
5408
|
+
const DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024;
|
|
5409
|
+
const RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
|
|
5410
|
+
const SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1";
|
|
5411
|
+
const DEPRECATED_SIGNATURE_ALGORITHMS = [RSA_SHA1];
|
|
5412
|
+
const DEPRECATED_DIGEST_ALGORITHMS = [SHA1];
|
|
5259
5413
|
function validateSAMLMetadataSize(metadataXml, maxSize = DEFAULT_MAX_SAML_METADATA_SIZE) {
|
|
5260
5414
|
if (new TextEncoder().encode(metadataXml).byteLength > maxSize) throw new Error(`IdP metadata exceeds maximum allowed size (${Math.round(maxSize / 1024)}KB)`);
|
|
5261
5415
|
}
|
|
@@ -5283,6 +5437,10 @@ function validateSAMLMetadataAlgorithms(metadataXml) {
|
|
|
5283
5437
|
}
|
|
5284
5438
|
//#endregion
|
|
5285
5439
|
//#region src/routes/sso/index.ts
|
|
5440
|
+
let ssoRuntimeModule;
|
|
5441
|
+
function loadSsoRuntime() {
|
|
5442
|
+
return ssoRuntimeModule ??= import("@better-auth/sso");
|
|
5443
|
+
}
|
|
5286
5444
|
function requireOrganizationAccess(ctx) {
|
|
5287
5445
|
const orgIdFromUrl = tryDecode(ctx.params.id);
|
|
5288
5446
|
const orgIdFromToken = ctx.context.payload?.organizationId;
|
|
@@ -5355,6 +5513,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
5355
5513
|
}] } : {},
|
|
5356
5514
|
...samlConfig.cert ? { cert: samlConfig.cert } : {}
|
|
5357
5515
|
};
|
|
5516
|
+
const m = samlConfig.mapping;
|
|
5358
5517
|
return {
|
|
5359
5518
|
config: {
|
|
5360
5519
|
issuer: samlConfig.entityId ?? `${baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`,
|
|
@@ -5363,7 +5522,15 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
5363
5522
|
spMetadata: {},
|
|
5364
5523
|
entryPoint: samlConfig.entryPoint ?? "",
|
|
5365
5524
|
cert: samlConfig.cert ?? "",
|
|
5366
|
-
...
|
|
5525
|
+
...m ? { mapping: {
|
|
5526
|
+
id: m.id ?? "nameID",
|
|
5527
|
+
email: m.email ?? "email",
|
|
5528
|
+
name: m.name ?? "name",
|
|
5529
|
+
emailVerified: m.emailVerified,
|
|
5530
|
+
firstName: m.firstName,
|
|
5531
|
+
lastName: m.lastName,
|
|
5532
|
+
extraFields: m.extraFields
|
|
5533
|
+
} } : {}
|
|
5367
5534
|
},
|
|
5368
5535
|
...metadataAlgorithmWarnings.length > 0 ? { warnings: metadataAlgorithmWarnings } : {}
|
|
5369
5536
|
};
|
|
@@ -5377,8 +5544,9 @@ async function resolveOIDCConfig(oidcConfig, domain, ctx) {
|
|
|
5377
5544
|
}
|
|
5378
5545
|
const issuerHint = oidcConfig.issuer || `https://${normalizedDomain}`;
|
|
5379
5546
|
const issuer = issuerHint.startsWith("http") ? issuerHint : `https://${issuerHint}`;
|
|
5547
|
+
const sso = await loadSsoRuntime();
|
|
5380
5548
|
try {
|
|
5381
|
-
const hydratedConfig = await discoverOIDCConfig({
|
|
5549
|
+
const hydratedConfig = await sso.discoverOIDCConfig({
|
|
5382
5550
|
issuer,
|
|
5383
5551
|
discoveryEndpoint: oidcConfig.discoveryUrl,
|
|
5384
5552
|
isTrustedOrigin: (url) => {
|
|
@@ -5390,6 +5558,7 @@ async function resolveOIDCConfig(oidcConfig, domain, ctx) {
|
|
|
5390
5558
|
}
|
|
5391
5559
|
}
|
|
5392
5560
|
});
|
|
5561
|
+
const om = oidcConfig.mapping;
|
|
5393
5562
|
return {
|
|
5394
5563
|
config: {
|
|
5395
5564
|
clientId: oidcConfig.clientId,
|
|
@@ -5402,12 +5571,19 @@ async function resolveOIDCConfig(oidcConfig, domain, ctx) {
|
|
|
5402
5571
|
userInfoEndpoint: hydratedConfig.userInfoEndpoint,
|
|
5403
5572
|
tokenEndpointAuthentication: hydratedConfig.tokenEndpointAuthentication,
|
|
5404
5573
|
pkce: true,
|
|
5405
|
-
...
|
|
5574
|
+
...om ? { mapping: {
|
|
5575
|
+
id: om.id ?? "sub",
|
|
5576
|
+
email: om.email ?? "email",
|
|
5577
|
+
name: om.name ?? "name",
|
|
5578
|
+
emailVerified: om.emailVerified,
|
|
5579
|
+
image: om.image,
|
|
5580
|
+
extraFields: om.extraFields
|
|
5581
|
+
} } : {}
|
|
5406
5582
|
},
|
|
5407
5583
|
issuer: hydratedConfig.issuer
|
|
5408
5584
|
};
|
|
5409
5585
|
} catch (e) {
|
|
5410
|
-
if (e instanceof DiscoveryError) {
|
|
5586
|
+
if (e instanceof sso.DiscoveryError) {
|
|
5411
5587
|
ctx.context.logger.error("[Dash] OIDC discovery failed:", e);
|
|
5412
5588
|
throw ctx.error("BAD_REQUEST", {
|
|
5413
5589
|
message: `OIDC discovery failed: ${e.message}`,
|
|
@@ -5494,6 +5670,8 @@ const createSsoProvider = (options) => {
|
|
|
5494
5670
|
...buildSessionContext(userId)
|
|
5495
5671
|
}
|
|
5496
5672
|
});
|
|
5673
|
+
let verificationToken = null;
|
|
5674
|
+
if ("domainVerificationToken" in result && typeof result.domainVerificationToken === "string") verificationToken = result.domainVerificationToken;
|
|
5497
5675
|
return {
|
|
5498
5676
|
success: true,
|
|
5499
5677
|
provider: {
|
|
@@ -5503,7 +5681,7 @@ const createSsoProvider = (options) => {
|
|
|
5503
5681
|
},
|
|
5504
5682
|
domainVerification: {
|
|
5505
5683
|
txtRecordName: `better-auth-token-${providerId}`,
|
|
5506
|
-
verificationToken
|
|
5684
|
+
verificationToken
|
|
5507
5685
|
}
|
|
5508
5686
|
};
|
|
5509
5687
|
} catch (e) {
|
|
@@ -5604,7 +5782,9 @@ const requestSsoVerificationToken = (options) => {
|
|
|
5604
5782
|
requireOrganizationPlugin(ctx);
|
|
5605
5783
|
requireOrganizationAccess(ctx);
|
|
5606
5784
|
const ssoPlugin = getSSOPlugin(ctx);
|
|
5607
|
-
if (!ssoPlugin
|
|
5785
|
+
if (!ssoPlugin || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
|
|
5786
|
+
const endpoints = ssoPlugin.endpoints;
|
|
5787
|
+
if (typeof endpoints.requestDomainVerification !== "function") throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
|
|
5608
5788
|
const organizationId = tryDecode(ctx.params.id);
|
|
5609
5789
|
const { providerId } = ctx.body;
|
|
5610
5790
|
const provider = await ctx.context.adapter.findOne({
|
|
@@ -5620,7 +5800,7 @@ const requestSsoVerificationToken = (options) => {
|
|
|
5620
5800
|
if (!provider) throw ctx.error("NOT_FOUND", { message: "SSO provider not found" });
|
|
5621
5801
|
const txtRecordName = `${ssoPlugin.options?.domainVerification?.tokenPrefix || "better-auth-token"}-${provider.providerId}`;
|
|
5622
5802
|
try {
|
|
5623
|
-
const result = await
|
|
5803
|
+
const result = await endpoints.requestDomainVerification({
|
|
5624
5804
|
body: { providerId },
|
|
5625
5805
|
context: {
|
|
5626
5806
|
...ctx.context,
|
|
@@ -5650,7 +5830,9 @@ const verifySsoProviderDomain = (options) => {
|
|
|
5650
5830
|
requireOrganizationPlugin(ctx);
|
|
5651
5831
|
requireOrganizationAccess(ctx);
|
|
5652
5832
|
const ssoPlugin = getSSOPlugin(ctx);
|
|
5653
|
-
if (!ssoPlugin
|
|
5833
|
+
if (!ssoPlugin || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
|
|
5834
|
+
const dvEndpoints = ssoPlugin.endpoints;
|
|
5835
|
+
if (typeof dvEndpoints.verifyDomain !== "function") throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
|
|
5654
5836
|
const organizationId = tryDecode(ctx.params.id);
|
|
5655
5837
|
const { providerId } = ctx.body;
|
|
5656
5838
|
const provider = await ctx.context.adapter.findOne({
|
|
@@ -5665,7 +5847,7 @@ const verifySsoProviderDomain = (options) => {
|
|
|
5665
5847
|
});
|
|
5666
5848
|
if (!provider) throw ctx.error("NOT_FOUND", { message: "SSO provider not found" });
|
|
5667
5849
|
try {
|
|
5668
|
-
await
|
|
5850
|
+
await dvEndpoints.verifyDomain({
|
|
5669
5851
|
body: { providerId },
|
|
5670
5852
|
context: {
|
|
5671
5853
|
...ctx.context,
|
|
@@ -7188,6 +7370,7 @@ const dash = (options) => {
|
|
|
7188
7370
|
return {
|
|
7189
7371
|
id: "dash",
|
|
7190
7372
|
options: opts,
|
|
7373
|
+
version: PLUGIN_VERSION,
|
|
7191
7374
|
init(ctx) {
|
|
7192
7375
|
const organizationPlugin = ctx.getPlugin("organization");
|
|
7193
7376
|
if (organizationPlugin) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { a as dashClient, i as DashGetAuditLogsInput, n as DashAuditLogsResponse, r as DashClientOptions, t as DashAuditLog } from "./dash-client-hJHp7l_X.mjs";
|
|
2
|
+
import { BetterAuthClientPlugin } from "better-auth";
|
|
3
|
+
|
|
4
|
+
//#region src/sentinel/native/client.d.ts
|
|
5
|
+
interface SentinelNativeClientOptions {
|
|
6
|
+
identifyUrl?: string;
|
|
7
|
+
autoSolveChallenge?: boolean;
|
|
8
|
+
onChallengeReceived?: (reason: string) => void;
|
|
9
|
+
onChallengeSolved?: (solveTimeMs: number) => void;
|
|
10
|
+
onChallengeFailed?: (error: Error) => void;
|
|
11
|
+
storage?: {
|
|
12
|
+
getItem: (key: string) => Promise<string | null>;
|
|
13
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
declare const sentinelNativeClient: (options?: SentinelNativeClientOptions) => BetterAuthClientPlugin;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { type DashAuditLog, type DashAuditLogsResponse, type DashClientOptions, type DashGetAuditLogsInput, type SentinelNativeClientOptions, dashClient, sentinelNativeClient };
|
package/dist/native.mjs
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { a as identify, c as dashClient, n as encodePoWSolution, o as bytesToHex, r as solvePoWChallenge, t as decodePoWChallenge } from "./pow-BUuN_EKw.mjs";
|
|
2
|
+
import { env } from "@better-auth/core/env";
|
|
3
|
+
import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
|
|
4
|
+
//#region src/sentinel/native/components.ts
|
|
5
|
+
/**
|
|
6
|
+
* Default first-party component payload for React Native / Expo.
|
|
7
|
+
* Avoids browser-style entropy; sets `clientRuntime` for durable-kv bot scoring.
|
|
8
|
+
*/
|
|
9
|
+
async function defaultCollectNativeComponents() {
|
|
10
|
+
const windowDims = Dimensions.get("window");
|
|
11
|
+
const scale = PixelRatio.get();
|
|
12
|
+
let locale = "en";
|
|
13
|
+
let locales = ["en"];
|
|
14
|
+
try {
|
|
15
|
+
locale = Intl.DateTimeFormat().resolvedOptions().locale || locale;
|
|
16
|
+
locales = [locale];
|
|
17
|
+
} catch {}
|
|
18
|
+
const nav = globalThis.navigator;
|
|
19
|
+
if (nav?.languages?.length) locales = [...nav.languages];
|
|
20
|
+
let timezone = "";
|
|
21
|
+
let timezoneOffset = 0;
|
|
22
|
+
try {
|
|
23
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
|
|
24
|
+
timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
25
|
+
} catch {
|
|
26
|
+
timezone = "unknown";
|
|
27
|
+
}
|
|
28
|
+
const components = {
|
|
29
|
+
clientRuntime: "react-native",
|
|
30
|
+
platform: Platform.OS,
|
|
31
|
+
screenResolution: `${Math.round(windowDims.width)}x${Math.round(windowDims.height)}`,
|
|
32
|
+
pixelRatio: scale,
|
|
33
|
+
fontScale: windowDims.fontScale,
|
|
34
|
+
language: locales[0] ?? locale,
|
|
35
|
+
languages: locales,
|
|
36
|
+
locale,
|
|
37
|
+
timezone,
|
|
38
|
+
timezoneOffset,
|
|
39
|
+
touchSupport: true,
|
|
40
|
+
maxTouchPoints: Platform.OS === "ios" ? 5 : 10
|
|
41
|
+
};
|
|
42
|
+
if (nav?.userAgent) components.userAgent = nav.userAgent;
|
|
43
|
+
await applyOptionalExpoMetadata(components);
|
|
44
|
+
return components;
|
|
45
|
+
}
|
|
46
|
+
async function applyOptionalExpoMetadata(components) {
|
|
47
|
+
try {
|
|
48
|
+
const c = (await import("expo-constants")).default;
|
|
49
|
+
if (c?.expoConfig?.version) components.appVersion = c.expoConfig.version;
|
|
50
|
+
else if (c?.nativeAppVersion) components.appVersion = c.nativeAppVersion;
|
|
51
|
+
if (c?.expoConfig?.slug) components.expoSlug = c.expoConfig.slug;
|
|
52
|
+
} catch {}
|
|
53
|
+
try {
|
|
54
|
+
const d = (await import("expo-device")).default;
|
|
55
|
+
if (d?.modelName) components.deviceModel = d.modelName;
|
|
56
|
+
if (d?.osVersion) components.osVersion = d.osVersion;
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/sentinel/native/crypto.ts
|
|
61
|
+
/**
|
|
62
|
+
* Cryptographically secure random bytes for React Native, Node (e.g. Expo API routes),
|
|
63
|
+
* and other runtimes
|
|
64
|
+
*/
|
|
65
|
+
async function randomBytesAsync(length) {
|
|
66
|
+
const c = globalThis.crypto;
|
|
67
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
68
|
+
const bytes = new Uint8Array(length);
|
|
69
|
+
c.getRandomValues(bytes);
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const { getRandomBytesAsync } = await import("expo-crypto");
|
|
74
|
+
return await getRandomBytesAsync(length);
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error("[@better-auth/infra] No secure RNG available: use Web Crypto / Node, add `import \"react-native-get-random-values\"` at app entry (before other imports), or install `expo-crypto` with a native dev build.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/sentinel/native/fingerprint.ts
|
|
81
|
+
async function randomVisitorId() {
|
|
82
|
+
return bytesToHex(await randomBytesAsync(10)).slice(0, 20);
|
|
83
|
+
}
|
|
84
|
+
async function generateRequestId() {
|
|
85
|
+
const h = bytesToHex(await randomBytesAsync(16));
|
|
86
|
+
return [
|
|
87
|
+
h.slice(0, 8),
|
|
88
|
+
h.slice(8, 12),
|
|
89
|
+
h.slice(12, 16),
|
|
90
|
+
h.slice(16, 20),
|
|
91
|
+
h.slice(20, 32)
|
|
92
|
+
].join("-");
|
|
93
|
+
}
|
|
94
|
+
/** Conservative confidence for first-party native payloads (not web-grade signals). */
|
|
95
|
+
const NATIVE_IDENTIFY_CONFIDENCE = .52;
|
|
96
|
+
/** Logical URL recorded with native identify payloads. */
|
|
97
|
+
const NATIVE_IDENTIFY_CONTEXT_URL = "react-native://identify";
|
|
98
|
+
function createNativeFingerprintRuntime(deps) {
|
|
99
|
+
let cached = null;
|
|
100
|
+
let pending = null;
|
|
101
|
+
let identifySent = false;
|
|
102
|
+
let identifyComplete = null;
|
|
103
|
+
let identifyCompleteResolve = null;
|
|
104
|
+
async function getFingerprint() {
|
|
105
|
+
if (cached != null) return cached;
|
|
106
|
+
if (pending != null) return pending;
|
|
107
|
+
pending = (async () => {
|
|
108
|
+
try {
|
|
109
|
+
const visitorId = await deps.getOrCreateVisitorId();
|
|
110
|
+
const components = await defaultCollectNativeComponents();
|
|
111
|
+
const result = {
|
|
112
|
+
visitorId,
|
|
113
|
+
requestId: await generateRequestId(),
|
|
114
|
+
confidence: NATIVE_IDENTIFY_CONFIDENCE,
|
|
115
|
+
components
|
|
116
|
+
};
|
|
117
|
+
cached = result;
|
|
118
|
+
return result;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn("[Sentinel native] Fingerprint generation failed:", error);
|
|
121
|
+
pending = null;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
return pending;
|
|
126
|
+
}
|
|
127
|
+
async function sendIdentify() {
|
|
128
|
+
if (identifySent) return;
|
|
129
|
+
const fingerprint = await getFingerprint();
|
|
130
|
+
if (!fingerprint) return;
|
|
131
|
+
identifySent = true;
|
|
132
|
+
identifyComplete = new Promise((resolve) => {
|
|
133
|
+
identifyCompleteResolve = resolve;
|
|
134
|
+
});
|
|
135
|
+
const payload = {
|
|
136
|
+
visitorId: fingerprint.visitorId,
|
|
137
|
+
requestId: fingerprint.requestId,
|
|
138
|
+
confidence: fingerprint.confidence,
|
|
139
|
+
components: fingerprint.components,
|
|
140
|
+
url: NATIVE_IDENTIFY_CONTEXT_URL,
|
|
141
|
+
incognito: false
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
await identify(deps.identifyUrl, payload);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn("[Sentinel native] Identify request failed:", error);
|
|
147
|
+
} finally {
|
|
148
|
+
identifyCompleteResolve?.();
|
|
149
|
+
identifyCompleteResolve = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function waitForIdentify(timeoutMs = 500) {
|
|
153
|
+
if (!identifyComplete) return;
|
|
154
|
+
await Promise.race([identifyComplete, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
getFingerprint,
|
|
158
|
+
sendIdentify,
|
|
159
|
+
waitForIdentify
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const VISITOR_STORAGE_KEY = "better-auth.infra.sentinel.visitorId";
|
|
163
|
+
let memoryVisitorId = null;
|
|
164
|
+
/**
|
|
165
|
+
* Stable per-install visitor id. Prefer passing `storage` (e.g. secure storage).
|
|
166
|
+
* Otherwise tries `@react-native-async-storage/async-storage`, then falls back to an in-memory id (session only).
|
|
167
|
+
*/
|
|
168
|
+
async function getOrCreateVisitorId(storage) {
|
|
169
|
+
if (storage) {
|
|
170
|
+
const existing = await storage.getItem(VISITOR_STORAGE_KEY);
|
|
171
|
+
if (existing) return existing;
|
|
172
|
+
const id = await randomVisitorId();
|
|
173
|
+
await storage.setItem(VISITOR_STORAGE_KEY, id);
|
|
174
|
+
return id;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const { default: AsyncStorage } = await import("@react-native-async-storage/async-storage");
|
|
178
|
+
const existing = await AsyncStorage.getItem(VISITOR_STORAGE_KEY);
|
|
179
|
+
if (existing) return existing;
|
|
180
|
+
const id = await randomVisitorId();
|
|
181
|
+
await AsyncStorage.setItem(VISITOR_STORAGE_KEY, id);
|
|
182
|
+
return id;
|
|
183
|
+
} catch {
|
|
184
|
+
memoryVisitorId ??= await randomVisitorId();
|
|
185
|
+
return memoryVisitorId;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/sentinel/native/client.ts
|
|
190
|
+
const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
|
|
191
|
+
function scheduleIdentify(send) {
|
|
192
|
+
const run = () => {
|
|
193
|
+
try {
|
|
194
|
+
send();
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.warn("[Sentinel native] identify schedule error:", e);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
const im = InteractionManager;
|
|
201
|
+
if (im && typeof im.runAfterInteractions === "function") {
|
|
202
|
+
im.runAfterInteractions(run);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
} catch {}
|
|
206
|
+
if (typeof queueMicrotask === "function") queueMicrotask(run);
|
|
207
|
+
else setTimeout(run, 0);
|
|
208
|
+
}
|
|
209
|
+
const sentinelNativeClient = (options) => {
|
|
210
|
+
const autoSolve = options?.autoSolveChallenge !== false;
|
|
211
|
+
const runtime = createNativeFingerprintRuntime({
|
|
212
|
+
identifyUrl: options?.identifyUrl ?? env.BETTER_AUTH_KV_URL ?? DEFAULT_IDENTIFY_URL,
|
|
213
|
+
getOrCreateVisitorId: () => getOrCreateVisitorId(options?.storage)
|
|
214
|
+
});
|
|
215
|
+
scheduleIdentify(() => {
|
|
216
|
+
runtime.sendIdentify();
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
id: "sentinel",
|
|
220
|
+
fetchPlugins: [{
|
|
221
|
+
id: "sentinel-fingerprint",
|
|
222
|
+
name: "sentinel-fingerprint",
|
|
223
|
+
hooks: { async onRequest(context) {
|
|
224
|
+
await runtime.waitForIdentify(500);
|
|
225
|
+
const fingerprint = await runtime.getFingerprint();
|
|
226
|
+
if (!fingerprint) return context;
|
|
227
|
+
const headers = context.headers || new Headers();
|
|
228
|
+
if (headers instanceof Headers) {
|
|
229
|
+
headers.set("X-Visitor-Id", fingerprint.visitorId);
|
|
230
|
+
headers.set("X-Request-Id", fingerprint.requestId);
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
...context,
|
|
234
|
+
headers
|
|
235
|
+
};
|
|
236
|
+
} }
|
|
237
|
+
}, {
|
|
238
|
+
id: "sentinel-pow-solver",
|
|
239
|
+
name: "sentinel-pow-solver",
|
|
240
|
+
hooks: {
|
|
241
|
+
async onResponse(context) {
|
|
242
|
+
if (context.response.status !== 423 || !autoSolve) return context;
|
|
243
|
+
const challengeHeader = context.response.headers.get("X-PoW-Challenge");
|
|
244
|
+
const reason = context.response.headers.get("X-PoW-Reason") || "";
|
|
245
|
+
if (!challengeHeader) return context;
|
|
246
|
+
options?.onChallengeReceived?.(reason);
|
|
247
|
+
const challenge = decodePoWChallenge(challengeHeader);
|
|
248
|
+
if (!challenge) return context;
|
|
249
|
+
try {
|
|
250
|
+
const startTime = Date.now();
|
|
251
|
+
const solution = await solvePoWChallenge(challenge);
|
|
252
|
+
const solveTime = Date.now() - startTime;
|
|
253
|
+
options?.onChallengeSolved?.(solveTime);
|
|
254
|
+
const originalUrl = context.response.url;
|
|
255
|
+
const fingerprint = await runtime.getFingerprint();
|
|
256
|
+
const retryHeaders = new Headers();
|
|
257
|
+
retryHeaders.set("X-PoW-Solution", encodePoWSolution(solution));
|
|
258
|
+
if (fingerprint) {
|
|
259
|
+
retryHeaders.set("X-Visitor-Id", fingerprint.visitorId);
|
|
260
|
+
retryHeaders.set("X-Request-Id", fingerprint.requestId);
|
|
261
|
+
}
|
|
262
|
+
retryHeaders.set("Content-Type", "application/json");
|
|
263
|
+
let body;
|
|
264
|
+
if (context.request?.body) body = context.request._originalBody;
|
|
265
|
+
const retryResponse = await fetch(originalUrl, {
|
|
266
|
+
method: context.request?.method || "POST",
|
|
267
|
+
headers: retryHeaders,
|
|
268
|
+
body
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
...context,
|
|
272
|
+
response: retryResponse
|
|
273
|
+
};
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.error("[Sentinel native] Failed to solve PoW challenge:", err);
|
|
276
|
+
options?.onChallengeFailed?.(err instanceof Error ? err : new Error(String(err)));
|
|
277
|
+
return context;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
async onRequest(context) {
|
|
281
|
+
if (context.body) {
|
|
282
|
+
const ctx = context;
|
|
283
|
+
ctx._originalBody = context.body;
|
|
284
|
+
}
|
|
285
|
+
return context;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}]
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
//#endregion
|
|
292
|
+
export { dashClient, sentinelNativeClient };
|