@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import "./constants-DdWGfvz1.mjs";
|
|
2
|
+
//#region src/dash-client.ts
|
|
3
|
+
function resolveDashUserId(input, options) {
|
|
4
|
+
return input.userId || options?.resolveUserId?.({
|
|
5
|
+
userId: input.userId,
|
|
6
|
+
user: input.user,
|
|
7
|
+
session: input.session
|
|
8
|
+
}) || input.user?.id || input.session?.user?.id || void 0;
|
|
9
|
+
}
|
|
10
|
+
const dashClient = (options) => {
|
|
11
|
+
return {
|
|
12
|
+
id: "dash",
|
|
13
|
+
getActions: ($fetch) => ({ dash: { getAuditLogs: async (input = {}) => {
|
|
14
|
+
const userId = resolveDashUserId(input, options);
|
|
15
|
+
return $fetch("/events/audit-logs", {
|
|
16
|
+
method: "GET",
|
|
17
|
+
query: {
|
|
18
|
+
limit: input.limit,
|
|
19
|
+
offset: input.offset,
|
|
20
|
+
organizationId: input.organizationId,
|
|
21
|
+
identifier: input.identifier,
|
|
22
|
+
eventType: input.eventType,
|
|
23
|
+
userId
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
} } }),
|
|
27
|
+
pathMethods: { "/events/audit-logs": "GET" }
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/crypto.ts
|
|
32
|
+
function randomBytes(length) {
|
|
33
|
+
const bytes = new Uint8Array(length);
|
|
34
|
+
crypto.getRandomValues(bytes);
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
function bytesToHex(bytes) {
|
|
38
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
39
|
+
}
|
|
40
|
+
async function hash(message) {
|
|
41
|
+
const msgBuffer = new TextEncoder().encode(message);
|
|
42
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
|
|
43
|
+
return bytesToHex(new Uint8Array(hashBuffer));
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/abort-signal.ts
|
|
47
|
+
/**
|
|
48
|
+
* Returns an AbortSignal that aborts after `ms`.
|
|
49
|
+
* Uses `AbortSignal.timeout` when present; otherwise `AbortController` + `setTimeout`
|
|
50
|
+
* for React Native / older runtimes where `.timeout` is missing.
|
|
51
|
+
*/
|
|
52
|
+
function timeout(ms) {
|
|
53
|
+
if (typeof AbortSignal.timeout === "function") return AbortSignal.timeout(ms);
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
setTimeout(() => controller.abort(), ms);
|
|
56
|
+
return controller.signal;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/identification-client.ts
|
|
60
|
+
function generateRequestId() {
|
|
61
|
+
const hex = bytesToHex(randomBytes(16));
|
|
62
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
63
|
+
}
|
|
64
|
+
async function identify(baseURL, payload, signal) {
|
|
65
|
+
const base = baseURL.replace(/\/$/, "");
|
|
66
|
+
await fetch(`${base}/identify`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify(payload),
|
|
70
|
+
signal: signal ?? timeout(5e3)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/sentinel/pow.ts
|
|
75
|
+
function hasLeadingZeroBits(hash, bits) {
|
|
76
|
+
const fullHexChars = Math.floor(bits / 4);
|
|
77
|
+
const remainingBits = bits % 4;
|
|
78
|
+
for (let i = 0; i < fullHexChars; i++) if (hash[i] !== "0") return false;
|
|
79
|
+
if (remainingBits > 0 && fullHexChars < hash.length) {
|
|
80
|
+
if (parseInt(hash[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
async function solvePoWChallenge(challenge) {
|
|
85
|
+
const { nonce, difficulty } = challenge;
|
|
86
|
+
let counter = 0;
|
|
87
|
+
while (true) {
|
|
88
|
+
if (hasLeadingZeroBits(await hash(`${nonce}:${counter}`), difficulty)) return {
|
|
89
|
+
nonce,
|
|
90
|
+
counter
|
|
91
|
+
};
|
|
92
|
+
counter++;
|
|
93
|
+
if (counter % 1e3 === 0) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
94
|
+
if (counter > 1e8) throw new Error("PoW challenge took too long to solve");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function decodeBase64ToUtf8(encoded) {
|
|
98
|
+
if (typeof globalThis.atob === "function") return globalThis.atob(encoded);
|
|
99
|
+
throw new Error("[Sentinel] Base64 decode requires atob (browser, Hermes, or Bun)");
|
|
100
|
+
}
|
|
101
|
+
function encodeUtf8ToBase64(str) {
|
|
102
|
+
if (typeof globalThis.btoa === "function") return globalThis.btoa(str);
|
|
103
|
+
const bytes = new TextEncoder().encode(str);
|
|
104
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
105
|
+
let out = "";
|
|
106
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
107
|
+
const b0 = bytes[i];
|
|
108
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
109
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
110
|
+
const triple = b0 << 16 | b1 << 8 | b2;
|
|
111
|
+
const pad = i + 2 >= bytes.length ? i + 1 >= bytes.length ? 2 : 1 : 0;
|
|
112
|
+
out += chars[triple >> 18 & 63];
|
|
113
|
+
out += chars[triple >> 12 & 63];
|
|
114
|
+
out += pad < 2 ? chars[triple >> 6 & 63] : "=";
|
|
115
|
+
out += pad < 1 ? chars[triple & 63] : "=";
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function decodePoWChallenge(encoded) {
|
|
120
|
+
try {
|
|
121
|
+
const decoded = decodeBase64ToUtf8(encoded);
|
|
122
|
+
return JSON.parse(decoded);
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function encodePoWSolution(solution) {
|
|
128
|
+
return encodeUtf8ToBase64(JSON.stringify(solution));
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { identify as a, dashClient as c, generateRequestId as i, encodePoWSolution as n, bytesToHex as o, solvePoWChallenge as r, hash as s, decodePoWChallenge as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/infra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Dashboard and analytics plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
"types": "./dist/email.d.mts",
|
|
31
31
|
"import": "./dist/email.mjs",
|
|
32
32
|
"default": "./dist/email.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./native": {
|
|
35
|
+
"types": "./dist/native.d.mts",
|
|
36
|
+
"import": "./dist/native.mjs",
|
|
37
|
+
"default": "./dist/native.mjs"
|
|
33
38
|
}
|
|
34
39
|
},
|
|
35
40
|
"files": [
|
|
@@ -62,8 +67,9 @@
|
|
|
62
67
|
"@types/bun": "latest",
|
|
63
68
|
"@types/node": "catalog:",
|
|
64
69
|
"better-auth": "catalog:",
|
|
70
|
+
"expo-crypto": "^14.0.2",
|
|
65
71
|
"happy-dom": "^20.8.9",
|
|
66
|
-
"msw": "^2.
|
|
72
|
+
"msw": "^2.13.0",
|
|
67
73
|
"tsdown": "^0.21.1",
|
|
68
74
|
"typescript": "catalog:",
|
|
69
75
|
"zod": "catalog:"
|
|
@@ -78,6 +84,25 @@
|
|
|
78
84
|
"better-auth": ">=1.4.0",
|
|
79
85
|
"zod": ">=4.1.12",
|
|
80
86
|
"@better-auth/core": ">=1.4.0",
|
|
81
|
-
"@better-auth/sso": ">=1.4.0"
|
|
87
|
+
"@better-auth/sso": ">=1.4.0",
|
|
88
|
+
"react-native": ">=0.74.0",
|
|
89
|
+
"@react-native-async-storage/async-storage": ">=1.21.0",
|
|
90
|
+
"expo-constants": ">=16.0.0",
|
|
91
|
+
"expo-crypto": ">=13.0.0",
|
|
92
|
+
"expo-device": ">=6.0.0"
|
|
93
|
+
},
|
|
94
|
+
"peerDependenciesMeta": {
|
|
95
|
+
"@react-native-async-storage/async-storage": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"expo-constants": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"expo-crypto": {
|
|
102
|
+
"optional": true
|
|
103
|
+
},
|
|
104
|
+
"expo-device": {
|
|
105
|
+
"optional": true
|
|
106
|
+
}
|
|
82
107
|
}
|
|
83
108
|
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { n as INFRA_KV_URL, r as KV_TIMEOUT_MS } from "./constants-DdWGfvz1.mjs";
|
|
2
|
-
import { logger } from "better-auth";
|
|
3
|
-
import { createAuthMiddleware } from "better-auth/api";
|
|
4
|
-
//#region src/crypto.ts
|
|
5
|
-
function randomBytes(length) {
|
|
6
|
-
const bytes = new Uint8Array(length);
|
|
7
|
-
crypto.getRandomValues(bytes);
|
|
8
|
-
return bytes;
|
|
9
|
-
}
|
|
10
|
-
function bytesToHex(bytes) {
|
|
11
|
-
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
12
|
-
}
|
|
13
|
-
async function hash(message) {
|
|
14
|
-
const msgBuffer = new TextEncoder().encode(message);
|
|
15
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
|
|
16
|
-
return bytesToHex(new Uint8Array(hashBuffer));
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/identification.ts
|
|
20
|
-
/**
|
|
21
|
-
* Identification Service
|
|
22
|
-
*
|
|
23
|
-
* Fetches identification data from the durable-kv service
|
|
24
|
-
* when a request includes an X-Request-Id header.
|
|
25
|
-
*/
|
|
26
|
-
const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
|
|
27
|
-
const identificationCache = /* @__PURE__ */ new Map();
|
|
28
|
-
const CACHE_TTL_MS = 6e4;
|
|
29
|
-
const CACHE_MAX_SIZE = 1e3;
|
|
30
|
-
let lastCleanup = Date.now();
|
|
31
|
-
function cleanupCache() {
|
|
32
|
-
const now = Date.now();
|
|
33
|
-
for (const [key, value] of identificationCache.entries()) if (now - value.timestamp > CACHE_TTL_MS) identificationCache.delete(key);
|
|
34
|
-
lastCleanup = now;
|
|
35
|
-
}
|
|
36
|
-
function maybeCleanup() {
|
|
37
|
-
if (Date.now() - lastCleanup > CACHE_TTL_MS || identificationCache.size > CACHE_MAX_SIZE) cleanupCache();
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Fetch identification data from durable-kv by requestId
|
|
41
|
-
*/
|
|
42
|
-
async function getIdentification(requestId, apiKey, kvUrl) {
|
|
43
|
-
maybeCleanup();
|
|
44
|
-
const cached = identificationCache.get(requestId);
|
|
45
|
-
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
|
|
46
|
-
const baseUrl = kvUrl || INFRA_KV_URL;
|
|
47
|
-
const maxRetries = 3;
|
|
48
|
-
const retryDelays = [
|
|
49
|
-
50,
|
|
50
|
-
100,
|
|
51
|
-
200
|
|
52
|
-
];
|
|
53
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
54
|
-
const response = await fetch(`${baseUrl}/identify/${requestId}`, {
|
|
55
|
-
method: "GET",
|
|
56
|
-
headers: { "x-api-key": apiKey },
|
|
57
|
-
signal: AbortSignal.timeout(KV_TIMEOUT_MS)
|
|
58
|
-
});
|
|
59
|
-
if (response.ok) {
|
|
60
|
-
const data = await response.json();
|
|
61
|
-
identificationCache.set(requestId, {
|
|
62
|
-
data,
|
|
63
|
-
timestamp: Date.now()
|
|
64
|
-
});
|
|
65
|
-
return data;
|
|
66
|
-
}
|
|
67
|
-
if (response.status === 404 && attempt < maxRetries) {
|
|
68
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (response.status !== 404) identificationCache.set(requestId, {
|
|
72
|
-
data: null,
|
|
73
|
-
timestamp: Date.now()
|
|
74
|
-
});
|
|
75
|
-
return null;
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if (attempt === maxRetries) {
|
|
78
|
-
logger.error("[Dash] Failed to fetch identification:", error);
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt] || 50));
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
function generateRequestId() {
|
|
86
|
-
const hex = bytesToHex(randomBytes(16));
|
|
87
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
88
|
-
}
|
|
89
|
-
async function identify(baseURL, payload, signal) {
|
|
90
|
-
const base = baseURL.replace(/\/$/, "");
|
|
91
|
-
await fetch(`${base}/identify`, {
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers: { "Content-Type": "application/json" },
|
|
94
|
-
body: JSON.stringify(payload),
|
|
95
|
-
signal: signal ?? AbortSignal.timeout(5e3)
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Extract identification headers from a request
|
|
100
|
-
*/
|
|
101
|
-
function extractIdentificationHeaders(request) {
|
|
102
|
-
if (!request) return {
|
|
103
|
-
visitorId: null,
|
|
104
|
-
requestId: null
|
|
105
|
-
};
|
|
106
|
-
return {
|
|
107
|
-
visitorId: request.headers.get("X-Visitor-Id"),
|
|
108
|
-
requestId: request.headers.get("X-Request-Id")
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Early middleware that loads identification data
|
|
113
|
-
*/
|
|
114
|
-
function createIdentificationMiddleware(options) {
|
|
115
|
-
return createAuthMiddleware(async (ctx) => {
|
|
116
|
-
const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
|
|
117
|
-
const requestId = headerRequestId ?? ctx.getCookie("__infra-rid") ?? null;
|
|
118
|
-
ctx.context.visitorId = visitorId;
|
|
119
|
-
ctx.context.requestId = requestId;
|
|
120
|
-
if (requestId) ctx.context.identification = ctx.context.identification ?? await getIdentification(requestId, options.apiKey, options.kvUrl) ?? null;
|
|
121
|
-
else ctx.context.identification = null;
|
|
122
|
-
const ipConfig = ctx.context.options?.advanced?.ipAddress;
|
|
123
|
-
if (ipConfig?.disableIpTracking === true) {
|
|
124
|
-
ctx.context.location = void 0;
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const identification = ctx.context.identification;
|
|
128
|
-
if (requestId && identification) {
|
|
129
|
-
const loc = getLocation(identification);
|
|
130
|
-
ctx.context.location = {
|
|
131
|
-
ipAddress: identification.ip || void 0,
|
|
132
|
-
city: loc?.city || void 0,
|
|
133
|
-
country: loc?.country?.name || void 0,
|
|
134
|
-
countryCode: loc?.country?.code || void 0
|
|
135
|
-
};
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
|
|
139
|
-
const countryCode = getCountryCodeFromRequest(ctx.request);
|
|
140
|
-
if (ipAddress || countryCode) {
|
|
141
|
-
ctx.context.location = {
|
|
142
|
-
ipAddress,
|
|
143
|
-
countryCode
|
|
144
|
-
};
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
ctx.context.location = void 0;
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Get the visitor's location
|
|
152
|
-
*/
|
|
153
|
-
function getLocation(identification) {
|
|
154
|
-
if (!identification) return null;
|
|
155
|
-
return identification.location;
|
|
156
|
-
}
|
|
157
|
-
function getClientIpFromRequest(request, ipAddressHeaders) {
|
|
158
|
-
if (!request) return void 0;
|
|
159
|
-
const headers = ipAddressHeaders?.length ? ipAddressHeaders : [
|
|
160
|
-
"cf-connecting-ip",
|
|
161
|
-
"x-forwarded-for",
|
|
162
|
-
"x-real-ip",
|
|
163
|
-
"x-vercel-forwarded-for"
|
|
164
|
-
];
|
|
165
|
-
for (const headerName of headers) {
|
|
166
|
-
const value = request.headers.get(headerName);
|
|
167
|
-
if (!value) continue;
|
|
168
|
-
const ip = value.split(",")[0]?.trim();
|
|
169
|
-
if (ip) return ip;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
function getCountryCodeFromRequest(request) {
|
|
173
|
-
if (!request) return void 0;
|
|
174
|
-
const cc = request.headers.get("cf-ipcountry") ?? request.headers.get("x-vercel-ip-country");
|
|
175
|
-
return cc ? cc.toUpperCase() : void 0;
|
|
176
|
-
}
|
|
177
|
-
//#endregion
|
|
178
|
-
export { hash as a, identify as i, createIdentificationMiddleware as n, generateRequestId as r, IDENTIFICATION_COOKIE_NAME as t };
|