@reauth-dev/sdk 0.1.0 → 0.3.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/dist/chunk-5LFJ5PXQ.mjs +1042 -0
- package/dist/chunk-EY5LQCDG.mjs +6 -0
- package/dist/index.d.mts +201 -16
- package/dist/index.d.ts +201 -16
- package/dist/index.js +801 -29
- package/dist/index.mjs +2 -1
- package/dist/react/index.d.mts +75 -5
- package/dist/react/index.d.ts +75 -5
- package/dist/react/index.js +965 -44
- package/dist/react/index.mjs +160 -11
- package/dist/server.d.mts +20 -2
- package/dist/server.d.ts +20 -2
- package/dist/server.js +61 -12
- package/dist/server.mjs +57 -11
- package/dist/types-DKUKhCNE.d.mts +349 -0
- package/dist/types-DKUKhCNE.d.ts +349 -0
- package/dist/webhooks.js +7 -3
- package/dist/webhooks.mjs +7 -3
- package/package.json +5 -2
- package/dist/chunk-JX2J36FS.mjs +0 -269
- package/dist/types-D8oOYbeC.d.mts +0 -169
- package/dist/types-D8oOYbeC.d.ts +0 -169
package/dist/react/index.js
CHANGED
|
@@ -23,16 +23,35 @@ __export(react_exports, {
|
|
|
23
23
|
AuthProvider: () => AuthProvider,
|
|
24
24
|
ProtectedRoute: () => ProtectedRoute,
|
|
25
25
|
useAuth: () => useAuth,
|
|
26
|
-
useAuthContext: () => useAuthContext
|
|
26
|
+
useAuthContext: () => useAuthContext,
|
|
27
|
+
useHeadlessAuth: () => useHeadlessAuth
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(react_exports);
|
|
29
30
|
|
|
30
31
|
// src/react/useAuth.ts
|
|
31
32
|
var import_react = require("react");
|
|
32
33
|
|
|
34
|
+
// src/types.ts
|
|
35
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
36
|
+
|
|
33
37
|
// src/client.ts
|
|
38
|
+
function assertHttpsUrl(url) {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = new URL(url);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error("URL must use HTTPS");
|
|
44
|
+
}
|
|
45
|
+
if (parsed.protocol !== "https:") {
|
|
46
|
+
throw new Error("URL must use HTTPS");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
34
49
|
function createReauthClient(config) {
|
|
35
50
|
const { domain } = config;
|
|
51
|
+
if (config.timeout !== void 0 && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
|
|
52
|
+
throw new Error("timeout must be a positive finite number in milliseconds");
|
|
53
|
+
}
|
|
54
|
+
const timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
36
55
|
const baseUrl = `https://reauth.${domain}/api/public`;
|
|
37
56
|
return {
|
|
38
57
|
/**
|
|
@@ -51,35 +70,47 @@ function createReauthClient(config) {
|
|
|
51
70
|
*/
|
|
52
71
|
async getSession() {
|
|
53
72
|
const res = await fetch(`${baseUrl}/auth/session`, {
|
|
54
|
-
credentials: "include"
|
|
73
|
+
credentials: "include",
|
|
74
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
55
75
|
});
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new Error(`Failed to get session: ${res.status}`);
|
|
78
|
+
}
|
|
56
79
|
return res.json();
|
|
57
80
|
},
|
|
58
81
|
/**
|
|
59
82
|
* Refresh the access token using the refresh token.
|
|
60
83
|
* Call this when getSession() returns valid: false but no error_code.
|
|
61
|
-
* @
|
|
84
|
+
* @throws Error on failed refresh (401) or server error
|
|
62
85
|
*/
|
|
63
86
|
async refresh() {
|
|
64
87
|
const res = await fetch(`${baseUrl}/auth/refresh`, {
|
|
65
88
|
method: "POST",
|
|
66
|
-
credentials: "include"
|
|
89
|
+
credentials: "include",
|
|
90
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
67
91
|
});
|
|
68
|
-
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new Error(`Failed to refresh: ${res.status}`);
|
|
94
|
+
}
|
|
69
95
|
},
|
|
70
96
|
/**
|
|
71
97
|
* Get an access token for Bearer authentication.
|
|
72
98
|
* Use this when calling your own API that uses local token verification.
|
|
73
99
|
*
|
|
74
|
-
* @returns TokenResponse with access token, or null if not authenticated
|
|
100
|
+
* @returns TokenResponse with access token, or null if not authenticated or network unreachable
|
|
101
|
+
* @throws Error on server errors (non-401 HTTP status codes) or request timeout
|
|
75
102
|
*
|
|
76
103
|
* @example
|
|
77
104
|
* ```typescript
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
105
|
+
* try {
|
|
106
|
+
* const tokenResponse = await reauth.getToken();
|
|
107
|
+
* if (tokenResponse) {
|
|
108
|
+
* fetch('/api/data', {
|
|
109
|
+
* headers: { Authorization: `Bearer ${tokenResponse.accessToken}` }
|
|
110
|
+
* });
|
|
111
|
+
* }
|
|
112
|
+
* } catch (err) {
|
|
113
|
+
* // Server error or timeout — do not log the user out
|
|
83
114
|
* }
|
|
84
115
|
* ```
|
|
85
116
|
*/
|
|
@@ -87,7 +118,8 @@ function createReauthClient(config) {
|
|
|
87
118
|
try {
|
|
88
119
|
const res = await fetch(`${baseUrl}/auth/token`, {
|
|
89
120
|
method: "GET",
|
|
90
|
-
credentials: "include"
|
|
121
|
+
credentials: "include",
|
|
122
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
91
123
|
});
|
|
92
124
|
if (!res.ok) {
|
|
93
125
|
if (res.status === 401) return null;
|
|
@@ -99,29 +131,158 @@ function createReauthClient(config) {
|
|
|
99
131
|
expiresIn: data.expires_in,
|
|
100
132
|
tokenType: data.token_type
|
|
101
133
|
};
|
|
102
|
-
} catch {
|
|
103
|
-
return null;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err instanceof TypeError) return null;
|
|
136
|
+
throw err;
|
|
104
137
|
}
|
|
105
138
|
},
|
|
106
139
|
/**
|
|
107
140
|
* Log out the user by clearing all session cookies.
|
|
141
|
+
* @throws Error on server error
|
|
108
142
|
*/
|
|
109
143
|
async logout() {
|
|
110
|
-
await fetch(`${baseUrl}/auth/logout`, {
|
|
144
|
+
const res = await fetch(`${baseUrl}/auth/logout`, {
|
|
111
145
|
method: "POST",
|
|
112
|
-
credentials: "include"
|
|
146
|
+
credentials: "include",
|
|
147
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
113
148
|
});
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
throw new Error(`Failed to logout: ${res.status}`);
|
|
151
|
+
}
|
|
114
152
|
},
|
|
115
153
|
/**
|
|
116
154
|
* Delete the user's own account (self-service).
|
|
117
|
-
* @
|
|
155
|
+
* @throws Error on permission denied or server error
|
|
118
156
|
*/
|
|
119
157
|
async deleteAccount() {
|
|
120
158
|
const res = await fetch(`${baseUrl}/auth/account`, {
|
|
121
159
|
method: "DELETE",
|
|
122
|
-
credentials: "include"
|
|
160
|
+
credentials: "include",
|
|
161
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
123
162
|
});
|
|
124
|
-
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
throw new Error(`Failed to delete account: ${res.status}`);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
// ========================================================================
|
|
168
|
+
// Headless Auth Methods
|
|
169
|
+
// ========================================================================
|
|
170
|
+
/**
|
|
171
|
+
* Get the domain's public auth configuration.
|
|
172
|
+
* Returns enabled auth methods and whether headless auth is available.
|
|
173
|
+
*/
|
|
174
|
+
async getConfig() {
|
|
175
|
+
const res = await fetch(`${baseUrl}/config`, {
|
|
176
|
+
credentials: "include",
|
|
177
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
178
|
+
});
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
throw new Error(`Failed to get config: ${res.status}`);
|
|
181
|
+
}
|
|
182
|
+
const data = await res.json();
|
|
183
|
+
return {
|
|
184
|
+
domain: data.domain,
|
|
185
|
+
authMethods: {
|
|
186
|
+
magicLink: data.auth_methods.magic_link,
|
|
187
|
+
googleOauth: data.auth_methods.google_oauth,
|
|
188
|
+
twitterOauth: data.auth_methods.twitter_oauth
|
|
189
|
+
},
|
|
190
|
+
redirectUrl: data.redirect_url,
|
|
191
|
+
headlessEnabled: data.headless_enabled
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Request a magic link email for headless authentication.
|
|
196
|
+
* When callbackUrl is provided, the email link will point to your custom URL
|
|
197
|
+
* with the token as a query parameter.
|
|
198
|
+
* @param opts Options including email and optional callbackUrl
|
|
199
|
+
*/
|
|
200
|
+
async requestMagicLink(opts) {
|
|
201
|
+
const body = { email: opts.email };
|
|
202
|
+
if (opts.callbackUrl) {
|
|
203
|
+
body.callback_url = opts.callbackUrl;
|
|
204
|
+
}
|
|
205
|
+
const res = await fetch(`${baseUrl}/auth/request-magic-link`, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { "Content-Type": "application/json" },
|
|
208
|
+
credentials: "include",
|
|
209
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
210
|
+
body: JSON.stringify(body)
|
|
211
|
+
});
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
const err = await res.json().catch(() => ({}));
|
|
214
|
+
throw new Error(err.message || `Failed to request magic link: ${res.status}`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
/**
|
|
218
|
+
* Verify a magic link token received from the email callback URL.
|
|
219
|
+
* On success, session cookies are set automatically.
|
|
220
|
+
* @param opts Options including the token from the callback URL
|
|
221
|
+
*/
|
|
222
|
+
async verifyMagicLink(opts) {
|
|
223
|
+
const res = await fetch(`${baseUrl}/auth/verify-magic-link`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: { "Content-Type": "application/json" },
|
|
226
|
+
credentials: "include",
|
|
227
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
228
|
+
body: JSON.stringify({ token: opts.token })
|
|
229
|
+
});
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
const err = await res.json().catch(() => ({}));
|
|
232
|
+
throw new Error(err.message || `Failed to verify magic link: ${res.status}`);
|
|
233
|
+
}
|
|
234
|
+
const data = await res.json();
|
|
235
|
+
return {
|
|
236
|
+
success: data.success,
|
|
237
|
+
redirectUrl: data.redirect_url,
|
|
238
|
+
endUserId: data.end_user_id,
|
|
239
|
+
email: data.email,
|
|
240
|
+
waitlistPosition: data.waitlist_position
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
/**
|
|
244
|
+
* Start a Google OAuth flow for headless authentication.
|
|
245
|
+
* Returns the Google authorization URL to redirect the user to.
|
|
246
|
+
* After Google authentication, the portal handles the callback and
|
|
247
|
+
* redirects to the domain's configured redirect_url.
|
|
248
|
+
*/
|
|
249
|
+
async startGoogleOAuth() {
|
|
250
|
+
const res = await fetch(`${baseUrl}/auth/google/start`, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
credentials: "include",
|
|
253
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
254
|
+
});
|
|
255
|
+
if (!res.ok) {
|
|
256
|
+
const err = await res.json().catch(() => ({}));
|
|
257
|
+
throw new Error(err.message || `Failed to start Google OAuth: ${res.status}`);
|
|
258
|
+
}
|
|
259
|
+
const data = await res.json();
|
|
260
|
+
return {
|
|
261
|
+
authUrl: data.auth_url,
|
|
262
|
+
state: data.state
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
/**
|
|
266
|
+
* Start an X (Twitter) OAuth flow for headless authentication.
|
|
267
|
+
* Returns the X authorization URL to redirect the user to.
|
|
268
|
+
* After X authentication, the portal handles the callback and
|
|
269
|
+
* redirects to the domain's configured redirect_url.
|
|
270
|
+
*/
|
|
271
|
+
async startTwitterOAuth() {
|
|
272
|
+
const res = await fetch(`${baseUrl}/auth/twitter/start`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
credentials: "include",
|
|
275
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
276
|
+
});
|
|
277
|
+
if (!res.ok) {
|
|
278
|
+
const err = await res.json().catch(() => ({}));
|
|
279
|
+
throw new Error(err.message || `Failed to start Twitter OAuth: ${res.status}`);
|
|
280
|
+
}
|
|
281
|
+
const data = await res.json();
|
|
282
|
+
return {
|
|
283
|
+
authUrl: data.auth_url,
|
|
284
|
+
state: data.state
|
|
285
|
+
};
|
|
125
286
|
},
|
|
126
287
|
// ========================================================================
|
|
127
288
|
// Billing Methods
|
|
@@ -129,12 +290,16 @@ function createReauthClient(config) {
|
|
|
129
290
|
/**
|
|
130
291
|
* Get available subscription plans for the domain.
|
|
131
292
|
* Only returns public plans sorted by display order.
|
|
293
|
+
* @throws Error on server error
|
|
132
294
|
*/
|
|
133
295
|
async getPlans() {
|
|
134
296
|
const res = await fetch(`${baseUrl}/billing/plans`, {
|
|
135
|
-
credentials: "include"
|
|
297
|
+
credentials: "include",
|
|
298
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
136
299
|
});
|
|
137
|
-
if (!res.ok)
|
|
300
|
+
if (!res.ok) {
|
|
301
|
+
throw new Error(`Failed to get plans: ${res.status}`);
|
|
302
|
+
}
|
|
138
303
|
const data = await res.json();
|
|
139
304
|
return data.map(
|
|
140
305
|
(p) => ({
|
|
@@ -147,8 +312,17 @@ function createReauthClient(config) {
|
|
|
147
312
|
interval: p.interval,
|
|
148
313
|
intervalCount: p.interval_count,
|
|
149
314
|
trialDays: p.trial_days,
|
|
150
|
-
features: p.features
|
|
151
|
-
|
|
315
|
+
features: p.features.map((f) => ({
|
|
316
|
+
code: f.code,
|
|
317
|
+
name: f.name,
|
|
318
|
+
featureType: f.feature_type,
|
|
319
|
+
numericValue: f.numeric_value,
|
|
320
|
+
unitLabel: f.unit_label
|
|
321
|
+
})),
|
|
322
|
+
displayOrder: p.display_order,
|
|
323
|
+
creditsAmount: p.credits_amount,
|
|
324
|
+
planType: p.plan_type,
|
|
325
|
+
contactUrl: p.contact_url
|
|
152
326
|
})
|
|
153
327
|
);
|
|
154
328
|
},
|
|
@@ -157,8 +331,12 @@ function createReauthClient(config) {
|
|
|
157
331
|
*/
|
|
158
332
|
async getSubscription() {
|
|
159
333
|
const res = await fetch(`${baseUrl}/billing/subscription`, {
|
|
160
|
-
credentials: "include"
|
|
334
|
+
credentials: "include",
|
|
335
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
161
336
|
});
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
throw new Error(`Failed to get subscription: ${res.status}`);
|
|
339
|
+
}
|
|
162
340
|
const data = await res.json();
|
|
163
341
|
return {
|
|
164
342
|
id: data.id,
|
|
@@ -182,6 +360,7 @@ function createReauthClient(config) {
|
|
|
182
360
|
method: "POST",
|
|
183
361
|
headers: { "Content-Type": "application/json" },
|
|
184
362
|
credentials: "include",
|
|
363
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
185
364
|
body: JSON.stringify({
|
|
186
365
|
plan_code: planCode,
|
|
187
366
|
success_url: successUrl,
|
|
@@ -210,6 +389,7 @@ function createReauthClient(config) {
|
|
|
210
389
|
currentUrl,
|
|
211
390
|
currentUrl
|
|
212
391
|
);
|
|
392
|
+
assertHttpsUrl(checkoutUrl);
|
|
213
393
|
window.location.href = checkoutUrl;
|
|
214
394
|
},
|
|
215
395
|
/**
|
|
@@ -224,6 +404,7 @@ function createReauthClient(config) {
|
|
|
224
404
|
method: "POST",
|
|
225
405
|
headers: { "Content-Type": "application/json" },
|
|
226
406
|
credentials: "include",
|
|
407
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
227
408
|
body: JSON.stringify({
|
|
228
409
|
return_url: returnUrl || window.location.href
|
|
229
410
|
})
|
|
@@ -232,18 +413,22 @@ function createReauthClient(config) {
|
|
|
232
413
|
throw new Error("Failed to open billing portal");
|
|
233
414
|
}
|
|
234
415
|
const data = await res.json();
|
|
416
|
+
assertHttpsUrl(data.portal_url);
|
|
235
417
|
window.location.href = data.portal_url;
|
|
236
418
|
},
|
|
237
419
|
/**
|
|
238
420
|
* Cancel the user's subscription at period end.
|
|
239
|
-
* @
|
|
421
|
+
* @throws Error on failure or server error
|
|
240
422
|
*/
|
|
241
423
|
async cancelSubscription() {
|
|
242
424
|
const res = await fetch(`${baseUrl}/billing/cancel`, {
|
|
243
425
|
method: "POST",
|
|
244
|
-
credentials: "include"
|
|
426
|
+
credentials: "include",
|
|
427
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
245
428
|
});
|
|
246
|
-
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
throw new Error(`Failed to cancel subscription: ${res.status}`);
|
|
431
|
+
}
|
|
247
432
|
},
|
|
248
433
|
// ========================================================================
|
|
249
434
|
// Balance Methods
|
|
@@ -254,13 +439,15 @@ function createReauthClient(config) {
|
|
|
254
439
|
*/
|
|
255
440
|
async getBalance() {
|
|
256
441
|
const res = await fetch(`${baseUrl}/balance`, {
|
|
257
|
-
credentials: "include"
|
|
442
|
+
credentials: "include",
|
|
443
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
258
444
|
});
|
|
259
445
|
if (!res.ok) {
|
|
260
446
|
if (res.status === 401) throw new Error("Not authenticated");
|
|
261
447
|
throw new Error(`Failed to get balance: ${res.status}`);
|
|
262
448
|
}
|
|
263
|
-
|
|
449
|
+
const data = await res.json();
|
|
450
|
+
return { balance: data.balance };
|
|
264
451
|
},
|
|
265
452
|
/**
|
|
266
453
|
* Get the current user's balance transaction history.
|
|
@@ -274,7 +461,7 @@ function createReauthClient(config) {
|
|
|
274
461
|
const qs = params.toString();
|
|
275
462
|
const res = await fetch(
|
|
276
463
|
`${baseUrl}/balance/transactions${qs ? `?${qs}` : ""}`,
|
|
277
|
-
{ credentials: "include" }
|
|
464
|
+
{ credentials: "include", signal: AbortSignal.timeout(timeoutMs) }
|
|
278
465
|
);
|
|
279
466
|
if (!res.ok) {
|
|
280
467
|
if (res.status === 401) throw new Error("Not authenticated");
|
|
@@ -292,13 +479,602 @@ function createReauthClient(config) {
|
|
|
292
479
|
})
|
|
293
480
|
)
|
|
294
481
|
};
|
|
482
|
+
},
|
|
483
|
+
// ========================================================================
|
|
484
|
+
// Credits Methods
|
|
485
|
+
// ========================================================================
|
|
486
|
+
/**
|
|
487
|
+
* Get the domain's credits configuration.
|
|
488
|
+
* Returns exchange rate, display settings, and purchase limits.
|
|
489
|
+
*/
|
|
490
|
+
async getCreditsConfig() {
|
|
491
|
+
const res = await fetch(`${baseUrl}/billing/credits-config`, {
|
|
492
|
+
credentials: "include",
|
|
493
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
494
|
+
});
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
if (res.status === 401) throw new Error("Not authenticated");
|
|
497
|
+
throw new Error(`Failed to get credits config: ${res.status}`);
|
|
498
|
+
}
|
|
499
|
+
const data = await res.json();
|
|
500
|
+
return {
|
|
501
|
+
creditsEnabled: data.credits_enabled,
|
|
502
|
+
creditsPerDollar: data.credits_per_dollar,
|
|
503
|
+
displayName: data.display_name,
|
|
504
|
+
displaySymbol: data.display_symbol,
|
|
505
|
+
displaySymbolPosition: data.display_symbol_position,
|
|
506
|
+
displayDecimals: data.display_decimals,
|
|
507
|
+
minPurchaseCents: data.min_purchase_cents,
|
|
508
|
+
maxPurchaseCents: data.max_purchase_cents,
|
|
509
|
+
manualTopUpAvailable: data.manual_top_up_available,
|
|
510
|
+
autoTopUpAvailable: data.auto_top_up_available,
|
|
511
|
+
overdrawEnabled: data.overdraw_enabled
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
// ========================================================================
|
|
515
|
+
// Payment Method Methods
|
|
516
|
+
// ========================================================================
|
|
517
|
+
/**
|
|
518
|
+
* List the current user's stored payment methods.
|
|
519
|
+
* Sorted by priority (lower number = higher priority).
|
|
520
|
+
*/
|
|
521
|
+
async getPaymentMethods() {
|
|
522
|
+
const res = await fetch(`${baseUrl}/billing/payment-methods`, {
|
|
523
|
+
credentials: "include",
|
|
524
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
525
|
+
});
|
|
526
|
+
if (!res.ok) {
|
|
527
|
+
if (res.status === 401) throw new Error("Not authenticated");
|
|
528
|
+
throw new Error(`Failed to get payment methods: ${res.status}`);
|
|
529
|
+
}
|
|
530
|
+
const data = await res.json();
|
|
531
|
+
return data.map(
|
|
532
|
+
(pm) => ({
|
|
533
|
+
id: pm.id,
|
|
534
|
+
provider: pm.provider,
|
|
535
|
+
methodType: pm.method_type,
|
|
536
|
+
cardBrand: pm.card_brand,
|
|
537
|
+
cardLast4: pm.card_last4,
|
|
538
|
+
cardExpMonth: pm.card_exp_month,
|
|
539
|
+
cardExpYear: pm.card_exp_year,
|
|
540
|
+
priority: pm.priority,
|
|
541
|
+
createdAt: pm.created_at
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
},
|
|
545
|
+
/**
|
|
546
|
+
* Create a Stripe SetupIntent for adding a new payment method.
|
|
547
|
+
* Use the returned clientSecret with Stripe.js `confirmCardSetup()`.
|
|
548
|
+
*/
|
|
549
|
+
async createSetupIntent() {
|
|
550
|
+
const res = await fetch(`${baseUrl}/billing/setup-intent`, {
|
|
551
|
+
method: "POST",
|
|
552
|
+
credentials: "include",
|
|
553
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
554
|
+
});
|
|
555
|
+
if (!res.ok) {
|
|
556
|
+
if (res.status === 401) throw new Error("Not authenticated");
|
|
557
|
+
throw new Error(`Failed to create setup intent: ${res.status}`);
|
|
558
|
+
}
|
|
559
|
+
const data = await res.json();
|
|
560
|
+
return {
|
|
561
|
+
clientSecret: data.client_secret,
|
|
562
|
+
setupIntentId: data.setup_intent_id
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
/**
|
|
566
|
+
* Delete a stored payment method.
|
|
567
|
+
* @param id UUID of the payment method to delete
|
|
568
|
+
*/
|
|
569
|
+
async deletePaymentMethod(id) {
|
|
570
|
+
const res = await fetch(`${baseUrl}/billing/payment-methods/${id}`, {
|
|
571
|
+
method: "DELETE",
|
|
572
|
+
credentials: "include",
|
|
573
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
574
|
+
});
|
|
575
|
+
if (!res.ok) {
|
|
576
|
+
if (res.status === 401) throw new Error("Not authenticated");
|
|
577
|
+
if (res.status === 404) throw new Error("Payment method not found");
|
|
578
|
+
throw new Error(`Failed to delete payment method: ${res.status}`);
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
/**
|
|
582
|
+
* Reorder payment method priorities.
|
|
583
|
+
* @param paymentMethodIds Array of payment method UUIDs in desired priority order
|
|
584
|
+
*/
|
|
585
|
+
async reorderPaymentMethods(paymentMethodIds) {
|
|
586
|
+
const res = await fetch(`${baseUrl}/billing/payment-methods/reorder`, {
|
|
587
|
+
method: "PUT",
|
|
588
|
+
headers: { "Content-Type": "application/json" },
|
|
589
|
+
credentials: "include",
|
|
590
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
591
|
+
body: JSON.stringify({ payment_method_ids: paymentMethodIds })
|
|
592
|
+
});
|
|
593
|
+
if (!res.ok) {
|
|
594
|
+
if (res.status === 401) throw new Error("Not authenticated");
|
|
595
|
+
throw new Error(`Failed to reorder payment methods: ${res.status}`);
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
/**
|
|
599
|
+
* Purchase credits using a stored payment method.
|
|
600
|
+
* @param opts Purchase options (amount, payment method, idempotency key)
|
|
601
|
+
*/
|
|
602
|
+
async purchaseCredits(opts) {
|
|
603
|
+
const res = await fetch(`${baseUrl}/billing/credits/purchase`, {
|
|
604
|
+
method: "POST",
|
|
605
|
+
headers: { "Content-Type": "application/json" },
|
|
606
|
+
credentials: "include",
|
|
607
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
608
|
+
body: JSON.stringify({
|
|
609
|
+
amount_cents: opts.amountCents,
|
|
610
|
+
payment_method_id: opts.paymentMethodId,
|
|
611
|
+
idempotency_key: opts.idempotencyKey
|
|
612
|
+
})
|
|
613
|
+
});
|
|
614
|
+
if (!res.ok) {
|
|
615
|
+
if (res.status === 401) throw new Error("Not authenticated");
|
|
616
|
+
const err = await res.json().catch(() => ({}));
|
|
617
|
+
throw new Error(err.message || `Failed to purchase credits: ${res.status}`);
|
|
618
|
+
}
|
|
619
|
+
const data = await res.json();
|
|
620
|
+
return {
|
|
621
|
+
creditsPurchased: data.credits_purchased,
|
|
622
|
+
newBalance: data.new_balance,
|
|
623
|
+
paymentIntentId: data.payment_intent_id
|
|
624
|
+
};
|
|
625
|
+
},
|
|
626
|
+
// ========================================================================
|
|
627
|
+
// Auto Top-Up Methods
|
|
628
|
+
// ========================================================================
|
|
629
|
+
/**
|
|
630
|
+
* Get the current user's auto top-up configuration and status.
|
|
631
|
+
*/
|
|
632
|
+
async getAutoTopUpStatus() {
|
|
633
|
+
const res = await fetch(`${baseUrl}/billing/auto-top-up`, {
|
|
634
|
+
credentials: "include",
|
|
635
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
636
|
+
});
|
|
637
|
+
if (!res.ok) {
|
|
638
|
+
if (res.status === 401) throw new Error("Not authenticated");
|
|
639
|
+
throw new Error(`Failed to get auto top-up status: ${res.status}`);
|
|
640
|
+
}
|
|
641
|
+
const data = await res.json();
|
|
642
|
+
return {
|
|
643
|
+
enabled: data.enabled,
|
|
644
|
+
thresholdCents: data.threshold_cents,
|
|
645
|
+
purchaseAmountCents: data.purchase_amount_cents,
|
|
646
|
+
status: data.status,
|
|
647
|
+
lastFailureReason: data.last_failure_reason,
|
|
648
|
+
retriesRemaining: data.retries_remaining,
|
|
649
|
+
nextRetryAt: data.next_retry_at
|
|
650
|
+
};
|
|
651
|
+
},
|
|
652
|
+
/**
|
|
653
|
+
* Update the current user's auto top-up configuration.
|
|
654
|
+
* @param opts New auto top-up settings
|
|
655
|
+
*/
|
|
656
|
+
async updateAutoTopUp(opts) {
|
|
657
|
+
const res = await fetch(`${baseUrl}/billing/auto-top-up`, {
|
|
658
|
+
method: "PUT",
|
|
659
|
+
headers: { "Content-Type": "application/json" },
|
|
660
|
+
credentials: "include",
|
|
661
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
662
|
+
body: JSON.stringify({
|
|
663
|
+
enabled: opts.enabled,
|
|
664
|
+
threshold_cents: opts.thresholdCents,
|
|
665
|
+
purchase_amount_cents: opts.purchaseAmountCents
|
|
666
|
+
})
|
|
667
|
+
});
|
|
668
|
+
if (!res.ok) {
|
|
669
|
+
if (res.status === 401) throw new Error("Not authenticated");
|
|
670
|
+
const err = await res.json().catch(() => ({}));
|
|
671
|
+
throw new Error(err.message || `Failed to update auto top-up: ${res.status}`);
|
|
672
|
+
}
|
|
673
|
+
const data = await res.json();
|
|
674
|
+
return {
|
|
675
|
+
enabled: data.enabled,
|
|
676
|
+
thresholdCents: data.threshold_cents,
|
|
677
|
+
purchaseAmountCents: data.purchase_amount_cents,
|
|
678
|
+
status: data.status,
|
|
679
|
+
lastFailureReason: data.last_failure_reason,
|
|
680
|
+
retriesRemaining: data.retries_remaining,
|
|
681
|
+
nextRetryAt: data.next_retry_at
|
|
682
|
+
};
|
|
683
|
+
},
|
|
684
|
+
// ========================================================================
|
|
685
|
+
// Organization Methods
|
|
686
|
+
// ========================================================================
|
|
687
|
+
/**
|
|
688
|
+
* Create a new organization.
|
|
689
|
+
* @param name The organization name
|
|
690
|
+
* @returns The created organization
|
|
691
|
+
*/
|
|
692
|
+
async createOrg(name) {
|
|
693
|
+
const res = await fetch(`${baseUrl}/auth/orgs`, {
|
|
694
|
+
method: "POST",
|
|
695
|
+
headers: { "Content-Type": "application/json" },
|
|
696
|
+
credentials: "include",
|
|
697
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
698
|
+
body: JSON.stringify({ name })
|
|
699
|
+
});
|
|
700
|
+
if (!res.ok) {
|
|
701
|
+
const err = await res.json().catch(() => ({}));
|
|
702
|
+
throw new Error(err.message || `Failed to create org: ${res.status}`);
|
|
703
|
+
}
|
|
704
|
+
const data = await res.json();
|
|
705
|
+
return transformOrg(data);
|
|
706
|
+
},
|
|
707
|
+
/**
|
|
708
|
+
* List all organizations the current user belongs to.
|
|
709
|
+
*/
|
|
710
|
+
async listOrgs() {
|
|
711
|
+
const res = await fetch(`${baseUrl}/auth/orgs`, {
|
|
712
|
+
credentials: "include",
|
|
713
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
714
|
+
});
|
|
715
|
+
if (!res.ok) {
|
|
716
|
+
throw new Error(`Failed to list orgs: ${res.status}`);
|
|
717
|
+
}
|
|
718
|
+
const data = await res.json();
|
|
719
|
+
return data.map(transformOrg);
|
|
720
|
+
},
|
|
721
|
+
/**
|
|
722
|
+
* Get organization details.
|
|
723
|
+
* @param orgId The organization ID
|
|
724
|
+
*/
|
|
725
|
+
async getOrg(orgId) {
|
|
726
|
+
const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}`, {
|
|
727
|
+
credentials: "include",
|
|
728
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
729
|
+
});
|
|
730
|
+
if (!res.ok) {
|
|
731
|
+
if (res.status === 404) throw new Error("Organization not found");
|
|
732
|
+
throw new Error(`Failed to get org: ${res.status}`);
|
|
733
|
+
}
|
|
734
|
+
const data = await res.json();
|
|
735
|
+
return transformOrg(data);
|
|
736
|
+
},
|
|
737
|
+
/**
|
|
738
|
+
* Update an organization's name. Requires owner role.
|
|
739
|
+
* @param orgId The organization ID
|
|
740
|
+
* @param name The new name
|
|
741
|
+
*/
|
|
742
|
+
async updateOrg(orgId, name) {
|
|
743
|
+
const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}`, {
|
|
744
|
+
method: "PATCH",
|
|
745
|
+
headers: { "Content-Type": "application/json" },
|
|
746
|
+
credentials: "include",
|
|
747
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
748
|
+
body: JSON.stringify({ name })
|
|
749
|
+
});
|
|
750
|
+
if (!res.ok) {
|
|
751
|
+
const err = await res.json().catch(() => ({}));
|
|
752
|
+
throw new Error(err.message || `Failed to update org: ${res.status}`);
|
|
753
|
+
}
|
|
754
|
+
const data = await res.json();
|
|
755
|
+
return transformOrg(data);
|
|
756
|
+
},
|
|
757
|
+
/**
|
|
758
|
+
* Delete an organization. Requires owner role.
|
|
759
|
+
* @param orgId The organization ID
|
|
760
|
+
*/
|
|
761
|
+
async deleteOrg(orgId) {
|
|
762
|
+
const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}`, {
|
|
763
|
+
method: "DELETE",
|
|
764
|
+
credentials: "include",
|
|
765
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
766
|
+
});
|
|
767
|
+
if (!res.ok) {
|
|
768
|
+
throw new Error(`Failed to delete org: ${res.status}`);
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
// ========================================================================
|
|
772
|
+
// Organization Member Methods
|
|
773
|
+
// ========================================================================
|
|
774
|
+
/**
|
|
775
|
+
* List members of an organization. Requires membership.
|
|
776
|
+
* @param orgId The organization ID
|
|
777
|
+
*/
|
|
778
|
+
async listOrgMembers(orgId) {
|
|
779
|
+
const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/members`, {
|
|
780
|
+
credentials: "include",
|
|
781
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
782
|
+
});
|
|
783
|
+
if (!res.ok) {
|
|
784
|
+
throw new Error(`Failed to list org members: ${res.status}`);
|
|
785
|
+
}
|
|
786
|
+
const data = await res.json();
|
|
787
|
+
return data.map(transformOrgMember);
|
|
788
|
+
},
|
|
789
|
+
/**
|
|
790
|
+
* Remove a member from an organization. Requires owner role.
|
|
791
|
+
* @param orgId The organization ID
|
|
792
|
+
* @param userId The user ID to remove
|
|
793
|
+
*/
|
|
794
|
+
async removeOrgMember(orgId, userId) {
|
|
795
|
+
const res = await fetch(
|
|
796
|
+
`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`,
|
|
797
|
+
{
|
|
798
|
+
method: "DELETE",
|
|
799
|
+
credentials: "include",
|
|
800
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
801
|
+
}
|
|
802
|
+
);
|
|
803
|
+
if (!res.ok) {
|
|
804
|
+
throw new Error(`Failed to remove org member: ${res.status}`);
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
/**
|
|
808
|
+
* Update a member's role. Requires owner role.
|
|
809
|
+
* @param orgId The organization ID
|
|
810
|
+
* @param userId The user ID to update
|
|
811
|
+
* @param role The new role
|
|
812
|
+
*/
|
|
813
|
+
async updateMemberRole(orgId, userId, role) {
|
|
814
|
+
const res = await fetch(
|
|
815
|
+
`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`,
|
|
816
|
+
{
|
|
817
|
+
method: "PATCH",
|
|
818
|
+
headers: { "Content-Type": "application/json" },
|
|
819
|
+
credentials: "include",
|
|
820
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
821
|
+
body: JSON.stringify({ role })
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
if (!res.ok) {
|
|
825
|
+
const err = await res.json().catch(() => ({}));
|
|
826
|
+
throw new Error(err.message || `Failed to update member role: ${res.status}`);
|
|
827
|
+
}
|
|
828
|
+
const data = await res.json();
|
|
829
|
+
return transformOrgMember(data);
|
|
830
|
+
},
|
|
831
|
+
// ========================================================================
|
|
832
|
+
// Organization Context Methods
|
|
833
|
+
// ========================================================================
|
|
834
|
+
/**
|
|
835
|
+
* Switch the active organization. Re-issues session tokens with new org context.
|
|
836
|
+
* @param orgId The organization ID to switch to
|
|
837
|
+
* @returns The new active org context with subscription info
|
|
838
|
+
*/
|
|
839
|
+
async switchOrg(orgId) {
|
|
840
|
+
const res = await fetch(`${baseUrl}/auth/switch-org`, {
|
|
841
|
+
method: "POST",
|
|
842
|
+
headers: { "Content-Type": "application/json" },
|
|
843
|
+
credentials: "include",
|
|
844
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
845
|
+
body: JSON.stringify({ org_id: orgId })
|
|
846
|
+
});
|
|
847
|
+
if (!res.ok) {
|
|
848
|
+
const err = await res.json().catch(() => ({}));
|
|
849
|
+
throw new Error(err.message || `Failed to switch org: ${res.status}`);
|
|
850
|
+
}
|
|
851
|
+
const data = await res.json();
|
|
852
|
+
return {
|
|
853
|
+
activeOrgId: data.active_org_id,
|
|
854
|
+
orgRole: data.org_role,
|
|
855
|
+
subscription: {
|
|
856
|
+
status: data.subscription.status,
|
|
857
|
+
planCode: data.subscription.plan_code,
|
|
858
|
+
planName: data.subscription.plan_name,
|
|
859
|
+
currentPeriodEnd: data.subscription.current_period_end,
|
|
860
|
+
cancelAtPeriodEnd: data.subscription.cancel_at_period_end,
|
|
861
|
+
trialEndsAt: data.subscription.trial_ends_at
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
},
|
|
865
|
+
/**
|
|
866
|
+
* Get the active organization from the current session.
|
|
867
|
+
* @returns Active org ID and role, or null if no active org
|
|
868
|
+
*/
|
|
869
|
+
async getActiveOrg() {
|
|
870
|
+
const session = await this.getSession();
|
|
871
|
+
if (!session.valid || !session.active_org_id || !session.org_role) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
id: session.active_org_id,
|
|
876
|
+
role: session.org_role
|
|
877
|
+
};
|
|
878
|
+
},
|
|
879
|
+
// ========================================================================
|
|
880
|
+
// Invitation Methods
|
|
881
|
+
// ========================================================================
|
|
882
|
+
/**
|
|
883
|
+
* Send an email invite to join an organization. Requires owner role.
|
|
884
|
+
* @param orgId The organization ID
|
|
885
|
+
* @param email The email address to invite
|
|
886
|
+
* @param role Optional role (defaults to "member")
|
|
887
|
+
*/
|
|
888
|
+
async inviteToOrg(orgId, email, role) {
|
|
889
|
+
const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invites`, {
|
|
890
|
+
method: "POST",
|
|
891
|
+
headers: { "Content-Type": "application/json" },
|
|
892
|
+
credentials: "include",
|
|
893
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
894
|
+
body: JSON.stringify({ email, role: role ?? "member" })
|
|
895
|
+
});
|
|
896
|
+
if (!res.ok) {
|
|
897
|
+
const err = await res.json().catch(() => ({}));
|
|
898
|
+
throw new Error(err.message || `Failed to send invite: ${res.status}`);
|
|
899
|
+
}
|
|
900
|
+
const data = await res.json();
|
|
901
|
+
return transformInvitation(data);
|
|
902
|
+
},
|
|
903
|
+
/**
|
|
904
|
+
* List pending invites for an organization. Requires owner role.
|
|
905
|
+
* @param orgId The organization ID
|
|
906
|
+
*/
|
|
907
|
+
async listPendingInvites(orgId) {
|
|
908
|
+
const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invites`, {
|
|
909
|
+
credentials: "include",
|
|
910
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
911
|
+
});
|
|
912
|
+
if (!res.ok) {
|
|
913
|
+
throw new Error(`Failed to list invites: ${res.status}`);
|
|
914
|
+
}
|
|
915
|
+
const data = await res.json();
|
|
916
|
+
return data.map(transformInvitation);
|
|
917
|
+
},
|
|
918
|
+
/**
|
|
919
|
+
* Revoke a pending invite. Requires owner role.
|
|
920
|
+
* @param orgId The organization ID
|
|
921
|
+
* @param inviteId The invite ID to revoke
|
|
922
|
+
*/
|
|
923
|
+
async revokeInvite(orgId, inviteId) {
|
|
924
|
+
const res = await fetch(
|
|
925
|
+
`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}`,
|
|
926
|
+
{
|
|
927
|
+
method: "DELETE",
|
|
928
|
+
credentials: "include",
|
|
929
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
if (!res.ok) {
|
|
933
|
+
throw new Error(`Failed to revoke invite: ${res.status}`);
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
/**
|
|
937
|
+
* Accept an email invite using the invite token.
|
|
938
|
+
* @param token The invite token from the email
|
|
939
|
+
* @returns The org ID and role assigned
|
|
940
|
+
*/
|
|
941
|
+
async acceptInvite(token) {
|
|
942
|
+
const res = await fetch(
|
|
943
|
+
`${baseUrl}/auth/orgs/invites/${encodeURIComponent(token)}/accept`,
|
|
944
|
+
{
|
|
945
|
+
method: "POST",
|
|
946
|
+
credentials: "include",
|
|
947
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
if (!res.ok) {
|
|
951
|
+
const err = await res.json().catch(() => ({}));
|
|
952
|
+
throw new Error(err.message || `Failed to accept invite: ${res.status}`);
|
|
953
|
+
}
|
|
954
|
+
const data = await res.json();
|
|
955
|
+
return {
|
|
956
|
+
orgId: data.org_id,
|
|
957
|
+
role: data.role
|
|
958
|
+
};
|
|
959
|
+
},
|
|
960
|
+
/**
|
|
961
|
+
* Create a shareable invite link for an organization. Requires owner role.
|
|
962
|
+
* @param orgId The organization ID
|
|
963
|
+
* @param role Optional role for joiners (defaults to "member")
|
|
964
|
+
*/
|
|
965
|
+
async createInviteLink(orgId, role) {
|
|
966
|
+
const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invite-links`, {
|
|
967
|
+
method: "POST",
|
|
968
|
+
headers: { "Content-Type": "application/json" },
|
|
969
|
+
credentials: "include",
|
|
970
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
971
|
+
body: JSON.stringify({ role: role ?? "member" })
|
|
972
|
+
});
|
|
973
|
+
if (!res.ok) {
|
|
974
|
+
const err = await res.json().catch(() => ({}));
|
|
975
|
+
throw new Error(err.message || `Failed to create invite link: ${res.status}`);
|
|
976
|
+
}
|
|
977
|
+
const data = await res.json();
|
|
978
|
+
return transformInviteLink(data);
|
|
979
|
+
},
|
|
980
|
+
/**
|
|
981
|
+
* List invite links for an organization. Requires owner role.
|
|
982
|
+
* @param orgId The organization ID
|
|
983
|
+
*/
|
|
984
|
+
async listInviteLinks(orgId) {
|
|
985
|
+
const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invite-links`, {
|
|
986
|
+
credentials: "include",
|
|
987
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
988
|
+
});
|
|
989
|
+
if (!res.ok) {
|
|
990
|
+
throw new Error(`Failed to list invite links: ${res.status}`);
|
|
991
|
+
}
|
|
992
|
+
const data = await res.json();
|
|
993
|
+
return data.map(transformInviteLink);
|
|
994
|
+
},
|
|
995
|
+
/**
|
|
996
|
+
* Revoke an invite link. Requires owner role.
|
|
997
|
+
* @param orgId The organization ID
|
|
998
|
+
* @param linkId The invite link ID to revoke
|
|
999
|
+
*/
|
|
1000
|
+
async revokeInviteLink(orgId, linkId) {
|
|
1001
|
+
const res = await fetch(
|
|
1002
|
+
`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invite-links/${encodeURIComponent(linkId)}`,
|
|
1003
|
+
{
|
|
1004
|
+
method: "DELETE",
|
|
1005
|
+
credentials: "include",
|
|
1006
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1007
|
+
}
|
|
1008
|
+
);
|
|
1009
|
+
if (!res.ok) {
|
|
1010
|
+
throw new Error(`Failed to revoke invite link: ${res.status}`);
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
/**
|
|
1014
|
+
* Join an organization via an invite link token.
|
|
1015
|
+
* @param token The invite link token
|
|
1016
|
+
*/
|
|
1017
|
+
async joinViaLink(token) {
|
|
1018
|
+
const res = await fetch(`${baseUrl}/auth/orgs/join/${encodeURIComponent(token)}`, {
|
|
1019
|
+
method: "POST",
|
|
1020
|
+
credentials: "include",
|
|
1021
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1022
|
+
});
|
|
1023
|
+
if (!res.ok) {
|
|
1024
|
+
const err = await res.json().catch(() => ({}));
|
|
1025
|
+
throw new Error(err.message || `Failed to join via link: ${res.status}`);
|
|
1026
|
+
}
|
|
295
1027
|
}
|
|
296
1028
|
};
|
|
297
1029
|
}
|
|
1030
|
+
function transformOrg(data) {
|
|
1031
|
+
return {
|
|
1032
|
+
id: data.id,
|
|
1033
|
+
name: data.name,
|
|
1034
|
+
isPersonal: data.is_personal,
|
|
1035
|
+
createdAt: data.created_at
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function transformOrgMember(data) {
|
|
1039
|
+
return {
|
|
1040
|
+
id: data.id,
|
|
1041
|
+
orgId: data.org_id,
|
|
1042
|
+
endUserId: data.end_user_id,
|
|
1043
|
+
role: data.role,
|
|
1044
|
+
joinedAt: data.joined_at
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function transformInvitation(data) {
|
|
1048
|
+
return {
|
|
1049
|
+
id: data.id,
|
|
1050
|
+
orgId: data.org_id,
|
|
1051
|
+
email: data.email,
|
|
1052
|
+
token: data.token,
|
|
1053
|
+
role: data.role,
|
|
1054
|
+
status: data.status,
|
|
1055
|
+
expiresAt: data.expires_at,
|
|
1056
|
+
acceptedAt: data.accepted_at,
|
|
1057
|
+
createdAt: data.created_at
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function transformInviteLink(data) {
|
|
1061
|
+
return {
|
|
1062
|
+
id: data.id,
|
|
1063
|
+
orgId: data.org_id,
|
|
1064
|
+
token: data.token,
|
|
1065
|
+
role: data.role,
|
|
1066
|
+
isActive: data.is_active,
|
|
1067
|
+
expiresAt: data.expires_at,
|
|
1068
|
+
createdAt: data.created_at
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
298
1071
|
|
|
299
1072
|
// src/react/useAuth.ts
|
|
1073
|
+
var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
300
1074
|
function useAuth(config) {
|
|
301
|
-
const
|
|
1075
|
+
const { refreshInterval = DEFAULT_REFRESH_INTERVAL_MS, ...clientConfig } = config;
|
|
1076
|
+
const client = (0, import_react.useMemo)(() => createReauthClient(clientConfig), [clientConfig.domain]);
|
|
1077
|
+
const isRefreshing = (0, import_react.useRef)(false);
|
|
302
1078
|
const [state, setState] = (0, import_react.useState)({
|
|
303
1079
|
user: null,
|
|
304
1080
|
loading: true,
|
|
@@ -307,12 +1083,15 @@ function useAuth(config) {
|
|
|
307
1083
|
waitlistPosition: null
|
|
308
1084
|
});
|
|
309
1085
|
const checkSession = (0, import_react.useCallback)(async () => {
|
|
1086
|
+
if (isRefreshing.current) return;
|
|
1087
|
+
isRefreshing.current = true;
|
|
310
1088
|
try {
|
|
311
1089
|
let session = await client.getSession();
|
|
312
1090
|
if (!session.valid && !session.error_code && !session.end_user_id) {
|
|
313
|
-
|
|
314
|
-
|
|
1091
|
+
try {
|
|
1092
|
+
await client.refresh();
|
|
315
1093
|
session = await client.getSession();
|
|
1094
|
+
} catch {
|
|
316
1095
|
}
|
|
317
1096
|
}
|
|
318
1097
|
if (session.error_code === "ACCOUNT_SUSPENDED") {
|
|
@@ -330,7 +1109,9 @@ function useAuth(config) {
|
|
|
330
1109
|
user: {
|
|
331
1110
|
id: session.end_user_id,
|
|
332
1111
|
email: session.email,
|
|
333
|
-
roles: session.roles || []
|
|
1112
|
+
roles: session.roles || [],
|
|
1113
|
+
activeOrgId: session.active_org_id || "",
|
|
1114
|
+
orgRole: session.org_role || ""
|
|
334
1115
|
},
|
|
335
1116
|
loading: false,
|
|
336
1117
|
error: null,
|
|
@@ -344,7 +1125,9 @@ function useAuth(config) {
|
|
|
344
1125
|
user: {
|
|
345
1126
|
id: session.end_user_id,
|
|
346
1127
|
email: session.email,
|
|
347
|
-
roles: session.roles || []
|
|
1128
|
+
roles: session.roles || [],
|
|
1129
|
+
activeOrgId: session.active_org_id || "",
|
|
1130
|
+
orgRole: session.org_role || ""
|
|
348
1131
|
},
|
|
349
1132
|
loading: false,
|
|
350
1133
|
error: null,
|
|
@@ -368,11 +1151,20 @@ function useAuth(config) {
|
|
|
368
1151
|
isOnWaitlist: false,
|
|
369
1152
|
waitlistPosition: null
|
|
370
1153
|
});
|
|
1154
|
+
} finally {
|
|
1155
|
+
isRefreshing.current = false;
|
|
371
1156
|
}
|
|
372
1157
|
}, [client]);
|
|
373
1158
|
(0, import_react.useEffect)(() => {
|
|
374
1159
|
checkSession();
|
|
375
1160
|
}, [checkSession]);
|
|
1161
|
+
(0, import_react.useEffect)(() => {
|
|
1162
|
+
if (refreshInterval <= 0) return;
|
|
1163
|
+
const intervalId = setInterval(() => {
|
|
1164
|
+
checkSession();
|
|
1165
|
+
}, refreshInterval);
|
|
1166
|
+
return () => clearInterval(intervalId);
|
|
1167
|
+
}, [checkSession, refreshInterval]);
|
|
376
1168
|
const logout = (0, import_react.useCallback)(async () => {
|
|
377
1169
|
await client.logout();
|
|
378
1170
|
setState({
|
|
@@ -391,16 +1183,141 @@ function useAuth(config) {
|
|
|
391
1183
|
};
|
|
392
1184
|
}
|
|
393
1185
|
|
|
394
|
-
// src/react/
|
|
1186
|
+
// src/react/useHeadlessAuth.ts
|
|
395
1187
|
var import_react2 = require("react");
|
|
1188
|
+
function useHeadlessAuth(options) {
|
|
1189
|
+
const client = (0, import_react2.useMemo)(
|
|
1190
|
+
() => createReauthClient({ domain: options.domain, timeout: options.timeout }),
|
|
1191
|
+
[options.domain, options.timeout]
|
|
1192
|
+
);
|
|
1193
|
+
const [state, setState] = (0, import_react2.useState)({
|
|
1194
|
+
loading: false,
|
|
1195
|
+
error: null,
|
|
1196
|
+
step: "idle",
|
|
1197
|
+
config: null,
|
|
1198
|
+
verifyResult: null
|
|
1199
|
+
});
|
|
1200
|
+
const getConfig = (0, import_react2.useCallback)(async () => {
|
|
1201
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
1202
|
+
try {
|
|
1203
|
+
const config = await client.getConfig();
|
|
1204
|
+
setState((s) => ({ ...s, loading: false, config }));
|
|
1205
|
+
return config;
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
const message = err instanceof Error ? err.message : "Failed to get config";
|
|
1208
|
+
setState((s) => ({ ...s, loading: false, error: message }));
|
|
1209
|
+
throw err;
|
|
1210
|
+
}
|
|
1211
|
+
}, [client]);
|
|
1212
|
+
const requestMagicLink = (0, import_react2.useCallback)(
|
|
1213
|
+
async (email) => {
|
|
1214
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
1215
|
+
try {
|
|
1216
|
+
await client.requestMagicLink({
|
|
1217
|
+
email,
|
|
1218
|
+
callbackUrl: options.callbackUrl
|
|
1219
|
+
});
|
|
1220
|
+
setState((s) => ({
|
|
1221
|
+
...s,
|
|
1222
|
+
loading: false,
|
|
1223
|
+
step: "magic_link_sent"
|
|
1224
|
+
}));
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
const message = err instanceof Error ? err.message : "Failed to send magic link";
|
|
1227
|
+
setState((s) => ({ ...s, loading: false, error: message }));
|
|
1228
|
+
throw err;
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
[client, options.callbackUrl]
|
|
1232
|
+
);
|
|
1233
|
+
const verifyMagicLink = (0, import_react2.useCallback)(
|
|
1234
|
+
async (token) => {
|
|
1235
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
1236
|
+
try {
|
|
1237
|
+
const result = await client.verifyMagicLink({ token });
|
|
1238
|
+
setState((s) => ({
|
|
1239
|
+
...s,
|
|
1240
|
+
loading: false,
|
|
1241
|
+
step: "completed",
|
|
1242
|
+
verifyResult: result
|
|
1243
|
+
}));
|
|
1244
|
+
return result;
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
const message = err instanceof Error ? err.message : "Failed to verify magic link";
|
|
1247
|
+
setState((s) => ({ ...s, loading: false, error: message }));
|
|
1248
|
+
throw err;
|
|
1249
|
+
}
|
|
1250
|
+
},
|
|
1251
|
+
[client]
|
|
1252
|
+
);
|
|
1253
|
+
const startGoogleOAuth = (0, import_react2.useCallback)(async () => {
|
|
1254
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
1255
|
+
try {
|
|
1256
|
+
const result = await client.startGoogleOAuth();
|
|
1257
|
+
setState((s) => ({
|
|
1258
|
+
...s,
|
|
1259
|
+
loading: false,
|
|
1260
|
+
step: "google_started"
|
|
1261
|
+
}));
|
|
1262
|
+
if (typeof window !== "undefined") {
|
|
1263
|
+
window.location.href = result.authUrl;
|
|
1264
|
+
}
|
|
1265
|
+
return result;
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
const message = err instanceof Error ? err.message : "Failed to start Google OAuth";
|
|
1268
|
+
setState((s) => ({ ...s, loading: false, error: message }));
|
|
1269
|
+
throw err;
|
|
1270
|
+
}
|
|
1271
|
+
}, [client]);
|
|
1272
|
+
const startTwitterOAuth = (0, import_react2.useCallback)(async () => {
|
|
1273
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
1274
|
+
try {
|
|
1275
|
+
const result = await client.startTwitterOAuth();
|
|
1276
|
+
setState((s) => ({
|
|
1277
|
+
...s,
|
|
1278
|
+
loading: false,
|
|
1279
|
+
step: "twitter_started"
|
|
1280
|
+
}));
|
|
1281
|
+
if (typeof window !== "undefined") {
|
|
1282
|
+
window.location.href = result.authUrl;
|
|
1283
|
+
}
|
|
1284
|
+
return result;
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
const message = err instanceof Error ? err.message : "Failed to start Twitter OAuth";
|
|
1287
|
+
setState((s) => ({ ...s, loading: false, error: message }));
|
|
1288
|
+
throw err;
|
|
1289
|
+
}
|
|
1290
|
+
}, [client]);
|
|
1291
|
+
const reset = (0, import_react2.useCallback)(() => {
|
|
1292
|
+
setState({
|
|
1293
|
+
loading: false,
|
|
1294
|
+
error: null,
|
|
1295
|
+
step: "idle",
|
|
1296
|
+
config: null,
|
|
1297
|
+
verifyResult: null
|
|
1298
|
+
});
|
|
1299
|
+
}, []);
|
|
1300
|
+
return {
|
|
1301
|
+
...state,
|
|
1302
|
+
getConfig,
|
|
1303
|
+
requestMagicLink,
|
|
1304
|
+
verifyMagicLink,
|
|
1305
|
+
startGoogleOAuth,
|
|
1306
|
+
startTwitterOAuth,
|
|
1307
|
+
reset
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/react/AuthProvider.tsx
|
|
1312
|
+
var import_react3 = require("react");
|
|
396
1313
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
397
|
-
var AuthContext = (0,
|
|
1314
|
+
var AuthContext = (0, import_react3.createContext)(null);
|
|
398
1315
|
function AuthProvider({ config, children }) {
|
|
399
1316
|
const auth = useAuth(config);
|
|
400
1317
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthContext.Provider, { value: auth, children });
|
|
401
1318
|
}
|
|
402
1319
|
function useAuthContext() {
|
|
403
|
-
const context = (0,
|
|
1320
|
+
const context = (0, import_react3.useContext)(AuthContext);
|
|
404
1321
|
if (!context) {
|
|
405
1322
|
throw new Error("useAuthContext must be used within AuthProvider");
|
|
406
1323
|
}
|
|
@@ -408,7 +1325,7 @@ function useAuthContext() {
|
|
|
408
1325
|
}
|
|
409
1326
|
|
|
410
1327
|
// src/react/ProtectedRoute.tsx
|
|
411
|
-
var
|
|
1328
|
+
var import_react4 = require("react");
|
|
412
1329
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
413
1330
|
function ProtectedRoute({
|
|
414
1331
|
children,
|
|
@@ -417,8 +1334,10 @@ function ProtectedRoute({
|
|
|
417
1334
|
onWaitlist
|
|
418
1335
|
}) {
|
|
419
1336
|
const { user, loading, isOnWaitlist, login } = useAuthContext();
|
|
420
|
-
(0,
|
|
421
|
-
|
|
1337
|
+
const hasRedirected = (0, import_react4.useRef)(false);
|
|
1338
|
+
(0, import_react4.useEffect)(() => {
|
|
1339
|
+
if (!loading && !user && !hasRedirected.current) {
|
|
1340
|
+
hasRedirected.current = true;
|
|
422
1341
|
if (onUnauthenticated) {
|
|
423
1342
|
onUnauthenticated();
|
|
424
1343
|
} else {
|
|
@@ -426,8 +1345,9 @@ function ProtectedRoute({
|
|
|
426
1345
|
}
|
|
427
1346
|
}
|
|
428
1347
|
}, [loading, user, login, onUnauthenticated]);
|
|
429
|
-
(0,
|
|
430
|
-
if (!loading && isOnWaitlist && onWaitlist) {
|
|
1348
|
+
(0, import_react4.useEffect)(() => {
|
|
1349
|
+
if (!loading && isOnWaitlist && onWaitlist && !hasRedirected.current) {
|
|
1350
|
+
hasRedirected.current = true;
|
|
431
1351
|
onWaitlist();
|
|
432
1352
|
}
|
|
433
1353
|
}, [loading, isOnWaitlist, onWaitlist]);
|
|
@@ -444,5 +1364,6 @@ function ProtectedRoute({
|
|
|
444
1364
|
AuthProvider,
|
|
445
1365
|
ProtectedRoute,
|
|
446
1366
|
useAuth,
|
|
447
|
-
useAuthContext
|
|
1367
|
+
useAuthContext,
|
|
1368
|
+
useHeadlessAuth
|
|
448
1369
|
});
|