@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.
@@ -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.1.14",
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.12.14",
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 };