@reauth-dev/sdk 0.1.0 → 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.
@@ -30,9 +30,27 @@ module.exports = __toCommonJS(react_exports);
30
30
  // src/react/useAuth.ts
31
31
  var import_react = require("react");
32
32
 
33
+ // src/types.ts
34
+ var DEFAULT_TIMEOUT_MS = 1e4;
35
+
33
36
  // src/client.ts
37
+ function assertHttpsUrl(url) {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(url);
41
+ } catch {
42
+ throw new Error("URL must use HTTPS");
43
+ }
44
+ if (parsed.protocol !== "https:") {
45
+ throw new Error("URL must use HTTPS");
46
+ }
47
+ }
34
48
  function createReauthClient(config) {
35
49
  const { domain } = config;
50
+ if (config.timeout !== void 0 && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
51
+ throw new Error("timeout must be a positive finite number in milliseconds");
52
+ }
53
+ const timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
36
54
  const baseUrl = `https://reauth.${domain}/api/public`;
37
55
  return {
38
56
  /**
@@ -51,35 +69,47 @@ function createReauthClient(config) {
51
69
  */
52
70
  async getSession() {
53
71
  const res = await fetch(`${baseUrl}/auth/session`, {
54
- credentials: "include"
72
+ credentials: "include",
73
+ signal: AbortSignal.timeout(timeoutMs)
55
74
  });
75
+ if (!res.ok) {
76
+ throw new Error(`Failed to get session: ${res.status}`);
77
+ }
56
78
  return res.json();
57
79
  },
58
80
  /**
59
81
  * Refresh the access token using the refresh token.
60
82
  * Call this when getSession() returns valid: false but no error_code.
61
- * @returns true if refresh succeeded, false otherwise
83
+ * @throws Error on failed refresh (401) or server error
62
84
  */
63
85
  async refresh() {
64
86
  const res = await fetch(`${baseUrl}/auth/refresh`, {
65
87
  method: "POST",
66
- credentials: "include"
88
+ credentials: "include",
89
+ signal: AbortSignal.timeout(timeoutMs)
67
90
  });
68
- return res.ok;
91
+ if (!res.ok) {
92
+ throw new Error(`Failed to refresh: ${res.status}`);
93
+ }
69
94
  },
70
95
  /**
71
96
  * Get an access token for Bearer authentication.
72
97
  * Use this when calling your own API that uses local token verification.
73
98
  *
74
- * @returns TokenResponse with access token, or null if not authenticated
99
+ * @returns TokenResponse with access token, or null if not authenticated or network unreachable
100
+ * @throws Error on server errors (non-401 HTTP status codes) or request timeout
75
101
  *
76
102
  * @example
77
103
  * ```typescript
78
- * const tokenResponse = await reauth.getToken();
79
- * if (tokenResponse) {
80
- * fetch('/api/data', {
81
- * headers: { Authorization: `Bearer ${tokenResponse.accessToken}` }
82
- * });
104
+ * try {
105
+ * const tokenResponse = await reauth.getToken();
106
+ * if (tokenResponse) {
107
+ * fetch('/api/data', {
108
+ * headers: { Authorization: `Bearer ${tokenResponse.accessToken}` }
109
+ * });
110
+ * }
111
+ * } catch (err) {
112
+ * // Server error or timeout — do not log the user out
83
113
  * }
84
114
  * ```
85
115
  */
@@ -87,7 +117,8 @@ function createReauthClient(config) {
87
117
  try {
88
118
  const res = await fetch(`${baseUrl}/auth/token`, {
89
119
  method: "GET",
90
- credentials: "include"
120
+ credentials: "include",
121
+ signal: AbortSignal.timeout(timeoutMs)
91
122
  });
92
123
  if (!res.ok) {
93
124
  if (res.status === 401) return null;
@@ -99,29 +130,38 @@ function createReauthClient(config) {
99
130
  expiresIn: data.expires_in,
100
131
  tokenType: data.token_type
101
132
  };
102
- } catch {
103
- return null;
133
+ } catch (err) {
134
+ if (err instanceof TypeError) return null;
135
+ throw err;
104
136
  }
105
137
  },
106
138
  /**
107
139
  * Log out the user by clearing all session cookies.
140
+ * @throws Error on server error
108
141
  */
109
142
  async logout() {
110
- await fetch(`${baseUrl}/auth/logout`, {
143
+ const res = await fetch(`${baseUrl}/auth/logout`, {
111
144
  method: "POST",
112
- credentials: "include"
145
+ credentials: "include",
146
+ signal: AbortSignal.timeout(timeoutMs)
113
147
  });
148
+ if (!res.ok) {
149
+ throw new Error(`Failed to logout: ${res.status}`);
150
+ }
114
151
  },
115
152
  /**
116
153
  * Delete the user's own account (self-service).
117
- * @returns true if deletion succeeded, false otherwise
154
+ * @throws Error on permission denied or server error
118
155
  */
119
156
  async deleteAccount() {
120
157
  const res = await fetch(`${baseUrl}/auth/account`, {
121
158
  method: "DELETE",
122
- credentials: "include"
159
+ credentials: "include",
160
+ signal: AbortSignal.timeout(timeoutMs)
123
161
  });
124
- return res.ok;
162
+ if (!res.ok) {
163
+ throw new Error(`Failed to delete account: ${res.status}`);
164
+ }
125
165
  },
126
166
  // ========================================================================
127
167
  // Billing Methods
@@ -129,12 +169,16 @@ function createReauthClient(config) {
129
169
  /**
130
170
  * Get available subscription plans for the domain.
131
171
  * Only returns public plans sorted by display order.
172
+ * @throws Error on server error
132
173
  */
133
174
  async getPlans() {
134
175
  const res = await fetch(`${baseUrl}/billing/plans`, {
135
- credentials: "include"
176
+ credentials: "include",
177
+ signal: AbortSignal.timeout(timeoutMs)
136
178
  });
137
- if (!res.ok) return [];
179
+ if (!res.ok) {
180
+ throw new Error(`Failed to get plans: ${res.status}`);
181
+ }
138
182
  const data = await res.json();
139
183
  return data.map(
140
184
  (p) => ({
@@ -147,8 +191,17 @@ function createReauthClient(config) {
147
191
  interval: p.interval,
148
192
  intervalCount: p.interval_count,
149
193
  trialDays: p.trial_days,
150
- features: p.features,
151
- displayOrder: p.display_order
194
+ features: p.features.map((f) => ({
195
+ code: f.code,
196
+ name: f.name,
197
+ featureType: f.feature_type,
198
+ numericValue: f.numeric_value,
199
+ unitLabel: f.unit_label
200
+ })),
201
+ displayOrder: p.display_order,
202
+ creditsAmount: p.credits_amount,
203
+ planType: p.plan_type,
204
+ contactUrl: p.contact_url
152
205
  })
153
206
  );
154
207
  },
@@ -157,8 +210,12 @@ function createReauthClient(config) {
157
210
  */
158
211
  async getSubscription() {
159
212
  const res = await fetch(`${baseUrl}/billing/subscription`, {
160
- credentials: "include"
213
+ credentials: "include",
214
+ signal: AbortSignal.timeout(timeoutMs)
161
215
  });
216
+ if (!res.ok) {
217
+ throw new Error(`Failed to get subscription: ${res.status}`);
218
+ }
162
219
  const data = await res.json();
163
220
  return {
164
221
  id: data.id,
@@ -182,6 +239,7 @@ function createReauthClient(config) {
182
239
  method: "POST",
183
240
  headers: { "Content-Type": "application/json" },
184
241
  credentials: "include",
242
+ signal: AbortSignal.timeout(timeoutMs),
185
243
  body: JSON.stringify({
186
244
  plan_code: planCode,
187
245
  success_url: successUrl,
@@ -210,6 +268,7 @@ function createReauthClient(config) {
210
268
  currentUrl,
211
269
  currentUrl
212
270
  );
271
+ assertHttpsUrl(checkoutUrl);
213
272
  window.location.href = checkoutUrl;
214
273
  },
215
274
  /**
@@ -224,6 +283,7 @@ function createReauthClient(config) {
224
283
  method: "POST",
225
284
  headers: { "Content-Type": "application/json" },
226
285
  credentials: "include",
286
+ signal: AbortSignal.timeout(timeoutMs),
227
287
  body: JSON.stringify({
228
288
  return_url: returnUrl || window.location.href
229
289
  })
@@ -232,18 +292,22 @@ function createReauthClient(config) {
232
292
  throw new Error("Failed to open billing portal");
233
293
  }
234
294
  const data = await res.json();
295
+ assertHttpsUrl(data.portal_url);
235
296
  window.location.href = data.portal_url;
236
297
  },
237
298
  /**
238
299
  * Cancel the user's subscription at period end.
239
- * @returns true if cancellation succeeded
300
+ * @throws Error on failure or server error
240
301
  */
241
302
  async cancelSubscription() {
242
303
  const res = await fetch(`${baseUrl}/billing/cancel`, {
243
304
  method: "POST",
244
- credentials: "include"
305
+ credentials: "include",
306
+ signal: AbortSignal.timeout(timeoutMs)
245
307
  });
246
- return res.ok;
308
+ if (!res.ok) {
309
+ throw new Error(`Failed to cancel subscription: ${res.status}`);
310
+ }
247
311
  },
248
312
  // ========================================================================
249
313
  // Balance Methods
@@ -254,13 +318,15 @@ function createReauthClient(config) {
254
318
  */
255
319
  async getBalance() {
256
320
  const res = await fetch(`${baseUrl}/balance`, {
257
- credentials: "include"
321
+ credentials: "include",
322
+ signal: AbortSignal.timeout(timeoutMs)
258
323
  });
259
324
  if (!res.ok) {
260
325
  if (res.status === 401) throw new Error("Not authenticated");
261
326
  throw new Error(`Failed to get balance: ${res.status}`);
262
327
  }
263
- return res.json();
328
+ const data = await res.json();
329
+ return { balance: data.balance };
264
330
  },
265
331
  /**
266
332
  * Get the current user's balance transaction history.
@@ -274,7 +340,7 @@ function createReauthClient(config) {
274
340
  const qs = params.toString();
275
341
  const res = await fetch(
276
342
  `${baseUrl}/balance/transactions${qs ? `?${qs}` : ""}`,
277
- { credentials: "include" }
343
+ { credentials: "include", signal: AbortSignal.timeout(timeoutMs) }
278
344
  );
279
345
  if (!res.ok) {
280
346
  if (res.status === 401) throw new Error("Not authenticated");
@@ -297,8 +363,11 @@ function createReauthClient(config) {
297
363
  }
298
364
 
299
365
  // src/react/useAuth.ts
366
+ var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
300
367
  function useAuth(config) {
301
- const client = (0, import_react.useMemo)(() => createReauthClient(config), [config.domain]);
368
+ const { refreshInterval = DEFAULT_REFRESH_INTERVAL_MS, ...clientConfig } = config;
369
+ const client = (0, import_react.useMemo)(() => createReauthClient(clientConfig), [clientConfig.domain]);
370
+ const isRefreshing = (0, import_react.useRef)(false);
302
371
  const [state, setState] = (0, import_react.useState)({
303
372
  user: null,
304
373
  loading: true,
@@ -307,12 +376,15 @@ function useAuth(config) {
307
376
  waitlistPosition: null
308
377
  });
309
378
  const checkSession = (0, import_react.useCallback)(async () => {
379
+ if (isRefreshing.current) return;
380
+ isRefreshing.current = true;
310
381
  try {
311
382
  let session = await client.getSession();
312
383
  if (!session.valid && !session.error_code && !session.end_user_id) {
313
- const refreshed = await client.refresh();
314
- if (refreshed) {
384
+ try {
385
+ await client.refresh();
315
386
  session = await client.getSession();
387
+ } catch {
316
388
  }
317
389
  }
318
390
  if (session.error_code === "ACCOUNT_SUSPENDED") {
@@ -368,11 +440,20 @@ function useAuth(config) {
368
440
  isOnWaitlist: false,
369
441
  waitlistPosition: null
370
442
  });
443
+ } finally {
444
+ isRefreshing.current = false;
371
445
  }
372
446
  }, [client]);
373
447
  (0, import_react.useEffect)(() => {
374
448
  checkSession();
375
449
  }, [checkSession]);
450
+ (0, import_react.useEffect)(() => {
451
+ if (refreshInterval <= 0) return;
452
+ const intervalId = setInterval(() => {
453
+ checkSession();
454
+ }, refreshInterval);
455
+ return () => clearInterval(intervalId);
456
+ }, [checkSession, refreshInterval]);
376
457
  const logout = (0, import_react.useCallback)(async () => {
377
458
  await client.logout();
378
459
  setState({
@@ -417,8 +498,10 @@ function ProtectedRoute({
417
498
  onWaitlist
418
499
  }) {
419
500
  const { user, loading, isOnWaitlist, login } = useAuthContext();
501
+ const hasRedirected = (0, import_react3.useRef)(false);
420
502
  (0, import_react3.useEffect)(() => {
421
- if (!loading && !user) {
503
+ if (!loading && !user && !hasRedirected.current) {
504
+ hasRedirected.current = true;
422
505
  if (onUnauthenticated) {
423
506
  onUnauthenticated();
424
507
  } else {
@@ -427,7 +510,8 @@ function ProtectedRoute({
427
510
  }
428
511
  }, [loading, user, login, onUnauthenticated]);
429
512
  (0, import_react3.useEffect)(() => {
430
- if (!loading && isOnWaitlist && onWaitlist) {
513
+ if (!loading && isOnWaitlist && onWaitlist && !hasRedirected.current) {
514
+ hasRedirected.current = true;
431
515
  onWaitlist();
432
516
  }
433
517
  }, [loading, isOnWaitlist, onWaitlist]);
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  createReauthClient
3
- } from "../chunk-JX2J36FS.mjs";
3
+ } from "../chunk-DMNMTW2C.mjs";
4
+ import "../chunk-EY5LQCDG.mjs";
4
5
 
5
6
  // src/react/useAuth.ts
6
- import { useState, useEffect, useCallback, useMemo } from "react";
7
+ import { useState, useEffect, useCallback, useMemo, useRef } from "react";
8
+ var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
7
9
  function useAuth(config) {
8
- const client = useMemo(() => createReauthClient(config), [config.domain]);
10
+ const { refreshInterval = DEFAULT_REFRESH_INTERVAL_MS, ...clientConfig } = config;
11
+ const client = useMemo(() => createReauthClient(clientConfig), [clientConfig.domain]);
12
+ const isRefreshing = useRef(false);
9
13
  const [state, setState] = useState({
10
14
  user: null,
11
15
  loading: true,
@@ -14,12 +18,15 @@ function useAuth(config) {
14
18
  waitlistPosition: null
15
19
  });
16
20
  const checkSession = useCallback(async () => {
21
+ if (isRefreshing.current) return;
22
+ isRefreshing.current = true;
17
23
  try {
18
24
  let session = await client.getSession();
19
25
  if (!session.valid && !session.error_code && !session.end_user_id) {
20
- const refreshed = await client.refresh();
21
- if (refreshed) {
26
+ try {
27
+ await client.refresh();
22
28
  session = await client.getSession();
29
+ } catch {
23
30
  }
24
31
  }
25
32
  if (session.error_code === "ACCOUNT_SUSPENDED") {
@@ -75,11 +82,20 @@ function useAuth(config) {
75
82
  isOnWaitlist: false,
76
83
  waitlistPosition: null
77
84
  });
85
+ } finally {
86
+ isRefreshing.current = false;
78
87
  }
79
88
  }, [client]);
80
89
  useEffect(() => {
81
90
  checkSession();
82
91
  }, [checkSession]);
92
+ useEffect(() => {
93
+ if (refreshInterval <= 0) return;
94
+ const intervalId = setInterval(() => {
95
+ checkSession();
96
+ }, refreshInterval);
97
+ return () => clearInterval(intervalId);
98
+ }, [checkSession, refreshInterval]);
83
99
  const logout = useCallback(async () => {
84
100
  await client.logout();
85
101
  setState({
@@ -115,7 +131,7 @@ function useAuthContext() {
115
131
  }
116
132
 
117
133
  // src/react/ProtectedRoute.tsx
118
- import { useEffect as useEffect2 } from "react";
134
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
119
135
  import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
120
136
  function ProtectedRoute({
121
137
  children,
@@ -124,8 +140,10 @@ function ProtectedRoute({
124
140
  onWaitlist
125
141
  }) {
126
142
  const { user, loading, isOnWaitlist, login } = useAuthContext();
143
+ const hasRedirected = useRef2(false);
127
144
  useEffect2(() => {
128
- if (!loading && !user) {
145
+ if (!loading && !user && !hasRedirected.current) {
146
+ hasRedirected.current = true;
129
147
  if (onUnauthenticated) {
130
148
  onUnauthenticated();
131
149
  } else {
@@ -134,7 +152,8 @@ function ProtectedRoute({
134
152
  }
135
153
  }, [loading, user, login, onUnauthenticated]);
136
154
  useEffect2(() => {
137
- if (!loading && isOnWaitlist && onWaitlist) {
155
+ if (!loading && isOnWaitlist && onWaitlist && !hasRedirected.current) {
156
+ hasRedirected.current = true;
138
157
  onWaitlist();
139
158
  }
140
159
  }, [loading, isOnWaitlist, onWaitlist]);
package/dist/server.d.mts CHANGED
@@ -1,5 +1,23 @@
1
- import { e as ReauthServerConfig, A as AuthResult, f as RequestLike, d as UserDetails, h as ChargeOptions, i as DepositOptions, b as TransactionsPaginationOptions, B as BalanceTransaction } from './types-D8oOYbeC.mjs';
1
+ import { e as ReauthServerConfig, A as AuthResult, f as RequestLike, d as UserDetails, h as ChargeOptions, i as DepositOptions, b as TransactionsPaginationOptions, B as BalanceTransaction } from './types-BqZzje-y.mjs';
2
2
 
3
+ /**
4
+ * Derives a JWT signing secret from an API key using HKDF-SHA256.
5
+ * This must match the Rust backend implementation exactly.
6
+ *
7
+ * IMPORTANT: Returns hex-encoded string (64 chars), NOT raw bytes.
8
+ * The Rust API uses the ASCII bytes of the hex string as the JWT secret,
9
+ * so we must do the same for compatibility.
10
+ *
11
+ * @param apiKey - The raw API key (e.g., "sk_live_...")
12
+ * @param domainId - UUID of the domain (used as salt for domain isolation)
13
+ * @returns Promise<string> - 64-char hex string (to be used as ASCII bytes for JWT signing)
14
+ */
15
+ declare function deriveJwtSecret(apiKey: string, domainId: string): Promise<string>;
16
+ /**
17
+ * Parse cookies from a cookie header string.
18
+ * Handles URL encoding and multiple cookies properly.
19
+ */
20
+ declare function parseCookies(cookieHeader: string): Record<string, string>;
3
21
  /**
4
22
  * Create a reauth client for server-side authentication.
5
23
  * Uses local JWT verification for fast, reliable auth checks.
@@ -185,4 +203,4 @@ declare function createServerClient(config: ReauthServerConfig): {
185
203
  };
186
204
  type ServerClient = ReturnType<typeof createServerClient>;
187
205
 
188
- export { type ServerClient, createServerClient };
206
+ export { type ServerClient, createServerClient, deriveJwtSecret, parseCookies };
package/dist/server.d.ts CHANGED
@@ -1,5 +1,23 @@
1
- import { e as ReauthServerConfig, A as AuthResult, f as RequestLike, d as UserDetails, h as ChargeOptions, i as DepositOptions, b as TransactionsPaginationOptions, B as BalanceTransaction } from './types-D8oOYbeC.js';
1
+ import { e as ReauthServerConfig, A as AuthResult, f as RequestLike, d as UserDetails, h as ChargeOptions, i as DepositOptions, b as TransactionsPaginationOptions, B as BalanceTransaction } from './types-BqZzje-y.js';
2
2
 
3
+ /**
4
+ * Derives a JWT signing secret from an API key using HKDF-SHA256.
5
+ * This must match the Rust backend implementation exactly.
6
+ *
7
+ * IMPORTANT: Returns hex-encoded string (64 chars), NOT raw bytes.
8
+ * The Rust API uses the ASCII bytes of the hex string as the JWT secret,
9
+ * so we must do the same for compatibility.
10
+ *
11
+ * @param apiKey - The raw API key (e.g., "sk_live_...")
12
+ * @param domainId - UUID of the domain (used as salt for domain isolation)
13
+ * @returns Promise<string> - 64-char hex string (to be used as ASCII bytes for JWT signing)
14
+ */
15
+ declare function deriveJwtSecret(apiKey: string, domainId: string): Promise<string>;
16
+ /**
17
+ * Parse cookies from a cookie header string.
18
+ * Handles URL encoding and multiple cookies properly.
19
+ */
20
+ declare function parseCookies(cookieHeader: string): Record<string, string>;
3
21
  /**
4
22
  * Create a reauth client for server-side authentication.
5
23
  * Uses local JWT verification for fast, reliable auth checks.
@@ -185,4 +203,4 @@ declare function createServerClient(config: ReauthServerConfig): {
185
203
  };
186
204
  type ServerClient = ReturnType<typeof createServerClient>;
187
205
 
188
- export { type ServerClient, createServerClient };
206
+ export { type ServerClient, createServerClient, deriveJwtSecret, parseCookies };
package/dist/server.js CHANGED
@@ -30,11 +30,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/server.ts
31
31
  var server_exports = {};
32
32
  __export(server_exports, {
33
- createServerClient: () => createServerClient
33
+ createServerClient: () => createServerClient,
34
+ deriveJwtSecret: () => deriveJwtSecret,
35
+ parseCookies: () => parseCookies
34
36
  });
35
37
  module.exports = __toCommonJS(server_exports);
36
38
  var import_crypto = require("crypto");
37
39
  var jose = __toESM(require("jose"));
40
+
41
+ // src/types.ts
42
+ var DEFAULT_TIMEOUT_MS = 1e4;
43
+
44
+ // src/server.ts
38
45
  async function deriveJwtSecret(apiKey, domainId) {
39
46
  const salt = Buffer.from(domainId.replace(/-/g, ""), "hex");
40
47
  const info = Buffer.from("reauth-jwt-v1");
@@ -45,6 +52,28 @@ async function deriveJwtSecret(apiKey, domainId) {
45
52
  });
46
53
  });
47
54
  }
55
+ var VALID_SUBSCRIPTION_STATUSES = /* @__PURE__ */ new Set([
56
+ "active",
57
+ "past_due",
58
+ "canceled",
59
+ "trialing",
60
+ "incomplete",
61
+ "incomplete_expired",
62
+ "unpaid",
63
+ "paused",
64
+ "none"
65
+ ]);
66
+ function isValidClaims(payload) {
67
+ if (typeof payload.sub !== "string") return false;
68
+ if (typeof payload.domain_id !== "string") return false;
69
+ if (typeof payload.domain !== "string") return false;
70
+ if (!Array.isArray(payload.roles) || !payload.roles.every((r) => typeof r === "string")) return false;
71
+ const subscription = payload.subscription;
72
+ if (typeof subscription !== "object" || subscription === null || Array.isArray(subscription)) return false;
73
+ const status = subscription.status;
74
+ if (typeof status !== "string" || !VALID_SUBSCRIPTION_STATUSES.has(status)) return false;
75
+ return true;
76
+ }
48
77
  function transformSubscription(sub) {
49
78
  return {
50
79
  status: sub.status,
@@ -71,6 +100,10 @@ function parseCookies(cookieHeader) {
71
100
  }
72
101
  function createServerClient(config) {
73
102
  const { domain, apiKey } = config;
103
+ if (config.timeout !== void 0 && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
104
+ throw new Error("timeout must be a positive finite number in milliseconds");
105
+ }
106
+ const timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
74
107
  if (!apiKey) {
75
108
  throw new Error(
76
109
  "apiKey is required for createServerClient. Get one from the Reauth dashboard."
@@ -118,7 +151,11 @@ function createServerClient(config) {
118
151
  // 60 seconds clock skew tolerance
119
152
  }
120
153
  );
121
- const claims = payload;
154
+ const payloadRecord = payload;
155
+ if (!isValidClaims(payloadRecord)) {
156
+ return { valid: false, user: null, claims: null, error: "Invalid token claims: missing required fields" };
157
+ }
158
+ const claims = payloadRecord;
122
159
  if (claims.domain !== domain) {
123
160
  return { valid: false, user: null, claims: null, error: "Domain mismatch" };
124
161
  }
@@ -241,10 +278,11 @@ function createServerClient(config) {
241
278
  * ```
242
279
  */
243
280
  async getUserById(userId) {
244
- const res = await fetch(`${developerBaseUrl}/users/${userId}`, {
281
+ const res = await fetch(`${developerBaseUrl}/users/${encodeURIComponent(userId)}`, {
245
282
  headers: {
246
283
  Authorization: `Bearer ${apiKey}`
247
- }
284
+ },
285
+ signal: AbortSignal.timeout(timeoutMs)
248
286
  });
249
287
  if (!res.ok) {
250
288
  if (res.status === 401) {
@@ -277,16 +315,18 @@ function createServerClient(config) {
277
315
  * @returns Object with the current balance
278
316
  */
279
317
  async getBalance(userId) {
280
- const res = await fetch(`${developerBaseUrl}/users/${userId}/balance`, {
318
+ const res = await fetch(`${developerBaseUrl}/users/${encodeURIComponent(userId)}/balance`, {
281
319
  headers: {
282
320
  Authorization: `Bearer ${apiKey}`
283
- }
321
+ },
322
+ signal: AbortSignal.timeout(timeoutMs)
284
323
  });
285
324
  if (!res.ok) {
286
325
  if (res.status === 401) throw new Error("Invalid API key");
287
326
  throw new Error(`Failed to get balance: ${res.status}`);
288
327
  }
289
- return res.json();
328
+ const data = await res.json();
329
+ return { balance: data.balance };
290
330
  },
291
331
  /**
292
332
  * Charge (deduct) credits from a user's balance.
@@ -297,12 +337,13 @@ function createServerClient(config) {
297
337
  * @throws Error with status 402 if insufficient balance, 400 if invalid amount
298
338
  */
299
339
  async charge(userId, opts) {
300
- const res = await fetch(`${developerBaseUrl}/users/${userId}/charge`, {
340
+ const res = await fetch(`${developerBaseUrl}/users/${encodeURIComponent(userId)}/charge`, {
301
341
  method: "POST",
302
342
  headers: {
303
343
  "Content-Type": "application/json",
304
344
  Authorization: `Bearer ${apiKey}`
305
345
  },
346
+ signal: AbortSignal.timeout(timeoutMs),
306
347
  body: JSON.stringify({
307
348
  amount: opts.amount,
308
349
  request_uuid: opts.requestUuid,
@@ -326,12 +367,13 @@ function createServerClient(config) {
326
367
  * @returns Object with the new balance after deposit
327
368
  */
328
369
  async deposit(userId, opts) {
329
- const res = await fetch(`${developerBaseUrl}/users/${userId}/deposit`, {
370
+ const res = await fetch(`${developerBaseUrl}/users/${encodeURIComponent(userId)}/deposit`, {
330
371
  method: "POST",
331
372
  headers: {
332
373
  "Content-Type": "application/json",
333
374
  Authorization: `Bearer ${apiKey}`
334
375
  },
376
+ signal: AbortSignal.timeout(timeoutMs),
335
377
  body: JSON.stringify({
336
378
  amount: opts.amount,
337
379
  request_uuid: opts.requestUuid,
@@ -359,11 +401,12 @@ function createServerClient(config) {
359
401
  if (opts?.offset !== void 0) params.set("offset", String(opts.offset));
360
402
  const qs = params.toString();
361
403
  const res = await fetch(
362
- `${developerBaseUrl}/users/${userId}/balance/transactions${qs ? `?${qs}` : ""}`,
404
+ `${developerBaseUrl}/users/${encodeURIComponent(userId)}/balance/transactions${qs ? `?${qs}` : ""}`,
363
405
  {
364
406
  headers: {
365
407
  Authorization: `Bearer ${apiKey}`
366
- }
408
+ },
409
+ signal: AbortSignal.timeout(timeoutMs)
367
410
  }
368
411
  );
369
412
  if (!res.ok) {
@@ -387,5 +430,7 @@ function createServerClient(config) {
387
430
  }
388
431
  // Annotate the CommonJS export names for ESM import in node:
389
432
  0 && (module.exports = {
390
- createServerClient
433
+ createServerClient,
434
+ deriveJwtSecret,
435
+ parseCookies
391
436
  });