@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.
@@ -0,0 +1,1042 @@
1
+ import {
2
+ DEFAULT_TIMEOUT_MS
3
+ } from "./chunk-EY5LQCDG.mjs";
4
+
5
+ // src/client.ts
6
+ function assertHttpsUrl(url) {
7
+ let parsed;
8
+ try {
9
+ parsed = new URL(url);
10
+ } catch {
11
+ throw new Error("URL must use HTTPS");
12
+ }
13
+ if (parsed.protocol !== "https:") {
14
+ throw new Error("URL must use HTTPS");
15
+ }
16
+ }
17
+ function createReauthClient(config) {
18
+ const { domain } = config;
19
+ if (config.timeout !== void 0 && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
20
+ throw new Error("timeout must be a positive finite number in milliseconds");
21
+ }
22
+ const timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
23
+ const baseUrl = `https://reauth.${domain}/api/public`;
24
+ return {
25
+ /**
26
+ * Redirect the user to the reauth.dev login page.
27
+ * After successful login, they'll be redirected back to your configured redirect URL.
28
+ */
29
+ login() {
30
+ if (typeof window === "undefined") {
31
+ throw new Error("login() can only be called in browser");
32
+ }
33
+ window.location.href = `https://reauth.${domain}/`;
34
+ },
35
+ /**
36
+ * Check if the user is authenticated.
37
+ * Returns session info including user ID, email, and roles.
38
+ */
39
+ async getSession() {
40
+ const res = await fetch(`${baseUrl}/auth/session`, {
41
+ credentials: "include",
42
+ signal: AbortSignal.timeout(timeoutMs)
43
+ });
44
+ if (!res.ok) {
45
+ throw new Error(`Failed to get session: ${res.status}`);
46
+ }
47
+ return res.json();
48
+ },
49
+ /**
50
+ * Refresh the access token using the refresh token.
51
+ * Call this when getSession() returns valid: false but no error_code.
52
+ * @throws Error on failed refresh (401) or server error
53
+ */
54
+ async refresh() {
55
+ const res = await fetch(`${baseUrl}/auth/refresh`, {
56
+ method: "POST",
57
+ credentials: "include",
58
+ signal: AbortSignal.timeout(timeoutMs)
59
+ });
60
+ if (!res.ok) {
61
+ throw new Error(`Failed to refresh: ${res.status}`);
62
+ }
63
+ },
64
+ /**
65
+ * Get an access token for Bearer authentication.
66
+ * Use this when calling your own API that uses local token verification.
67
+ *
68
+ * @returns TokenResponse with access token, or null if not authenticated or network unreachable
69
+ * @throws Error on server errors (non-401 HTTP status codes) or request timeout
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * try {
74
+ * const tokenResponse = await reauth.getToken();
75
+ * if (tokenResponse) {
76
+ * fetch('/api/data', {
77
+ * headers: { Authorization: `Bearer ${tokenResponse.accessToken}` }
78
+ * });
79
+ * }
80
+ * } catch (err) {
81
+ * // Server error or timeout — do not log the user out
82
+ * }
83
+ * ```
84
+ */
85
+ async getToken() {
86
+ try {
87
+ const res = await fetch(`${baseUrl}/auth/token`, {
88
+ method: "GET",
89
+ credentials: "include",
90
+ signal: AbortSignal.timeout(timeoutMs)
91
+ });
92
+ if (!res.ok) {
93
+ if (res.status === 401) return null;
94
+ throw new Error(`Failed to get token: ${res.status}`);
95
+ }
96
+ const data = await res.json();
97
+ return {
98
+ accessToken: data.access_token,
99
+ expiresIn: data.expires_in,
100
+ tokenType: data.token_type
101
+ };
102
+ } catch (err) {
103
+ if (err instanceof TypeError) return null;
104
+ throw err;
105
+ }
106
+ },
107
+ /**
108
+ * Log out the user by clearing all session cookies.
109
+ * @throws Error on server error
110
+ */
111
+ async logout() {
112
+ const res = await fetch(`${baseUrl}/auth/logout`, {
113
+ method: "POST",
114
+ credentials: "include",
115
+ signal: AbortSignal.timeout(timeoutMs)
116
+ });
117
+ if (!res.ok) {
118
+ throw new Error(`Failed to logout: ${res.status}`);
119
+ }
120
+ },
121
+ /**
122
+ * Delete the user's own account (self-service).
123
+ * @throws Error on permission denied or server error
124
+ */
125
+ async deleteAccount() {
126
+ const res = await fetch(`${baseUrl}/auth/account`, {
127
+ method: "DELETE",
128
+ credentials: "include",
129
+ signal: AbortSignal.timeout(timeoutMs)
130
+ });
131
+ if (!res.ok) {
132
+ throw new Error(`Failed to delete account: ${res.status}`);
133
+ }
134
+ },
135
+ // ========================================================================
136
+ // Headless Auth Methods
137
+ // ========================================================================
138
+ /**
139
+ * Get the domain's public auth configuration.
140
+ * Returns enabled auth methods and whether headless auth is available.
141
+ */
142
+ async getConfig() {
143
+ const res = await fetch(`${baseUrl}/config`, {
144
+ credentials: "include",
145
+ signal: AbortSignal.timeout(timeoutMs)
146
+ });
147
+ if (!res.ok) {
148
+ throw new Error(`Failed to get config: ${res.status}`);
149
+ }
150
+ const data = await res.json();
151
+ return {
152
+ domain: data.domain,
153
+ authMethods: {
154
+ magicLink: data.auth_methods.magic_link,
155
+ googleOauth: data.auth_methods.google_oauth,
156
+ twitterOauth: data.auth_methods.twitter_oauth
157
+ },
158
+ redirectUrl: data.redirect_url,
159
+ headlessEnabled: data.headless_enabled
160
+ };
161
+ },
162
+ /**
163
+ * Request a magic link email for headless authentication.
164
+ * When callbackUrl is provided, the email link will point to your custom URL
165
+ * with the token as a query parameter.
166
+ * @param opts Options including email and optional callbackUrl
167
+ */
168
+ async requestMagicLink(opts) {
169
+ const body = { email: opts.email };
170
+ if (opts.callbackUrl) {
171
+ body.callback_url = opts.callbackUrl;
172
+ }
173
+ const res = await fetch(`${baseUrl}/auth/request-magic-link`, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ credentials: "include",
177
+ signal: AbortSignal.timeout(timeoutMs),
178
+ body: JSON.stringify(body)
179
+ });
180
+ if (!res.ok) {
181
+ const err = await res.json().catch(() => ({}));
182
+ throw new Error(err.message || `Failed to request magic link: ${res.status}`);
183
+ }
184
+ },
185
+ /**
186
+ * Verify a magic link token received from the email callback URL.
187
+ * On success, session cookies are set automatically.
188
+ * @param opts Options including the token from the callback URL
189
+ */
190
+ async verifyMagicLink(opts) {
191
+ const res = await fetch(`${baseUrl}/auth/verify-magic-link`, {
192
+ method: "POST",
193
+ headers: { "Content-Type": "application/json" },
194
+ credentials: "include",
195
+ signal: AbortSignal.timeout(timeoutMs),
196
+ body: JSON.stringify({ token: opts.token })
197
+ });
198
+ if (!res.ok) {
199
+ const err = await res.json().catch(() => ({}));
200
+ throw new Error(err.message || `Failed to verify magic link: ${res.status}`);
201
+ }
202
+ const data = await res.json();
203
+ return {
204
+ success: data.success,
205
+ redirectUrl: data.redirect_url,
206
+ endUserId: data.end_user_id,
207
+ email: data.email,
208
+ waitlistPosition: data.waitlist_position
209
+ };
210
+ },
211
+ /**
212
+ * Start a Google OAuth flow for headless authentication.
213
+ * Returns the Google authorization URL to redirect the user to.
214
+ * After Google authentication, the portal handles the callback and
215
+ * redirects to the domain's configured redirect_url.
216
+ */
217
+ async startGoogleOAuth() {
218
+ const res = await fetch(`${baseUrl}/auth/google/start`, {
219
+ method: "POST",
220
+ credentials: "include",
221
+ signal: AbortSignal.timeout(timeoutMs)
222
+ });
223
+ if (!res.ok) {
224
+ const err = await res.json().catch(() => ({}));
225
+ throw new Error(err.message || `Failed to start Google OAuth: ${res.status}`);
226
+ }
227
+ const data = await res.json();
228
+ return {
229
+ authUrl: data.auth_url,
230
+ state: data.state
231
+ };
232
+ },
233
+ /**
234
+ * Start an X (Twitter) OAuth flow for headless authentication.
235
+ * Returns the X authorization URL to redirect the user to.
236
+ * After X authentication, the portal handles the callback and
237
+ * redirects to the domain's configured redirect_url.
238
+ */
239
+ async startTwitterOAuth() {
240
+ const res = await fetch(`${baseUrl}/auth/twitter/start`, {
241
+ method: "POST",
242
+ credentials: "include",
243
+ signal: AbortSignal.timeout(timeoutMs)
244
+ });
245
+ if (!res.ok) {
246
+ const err = await res.json().catch(() => ({}));
247
+ throw new Error(err.message || `Failed to start Twitter OAuth: ${res.status}`);
248
+ }
249
+ const data = await res.json();
250
+ return {
251
+ authUrl: data.auth_url,
252
+ state: data.state
253
+ };
254
+ },
255
+ // ========================================================================
256
+ // Billing Methods
257
+ // ========================================================================
258
+ /**
259
+ * Get available subscription plans for the domain.
260
+ * Only returns public plans sorted by display order.
261
+ * @throws Error on server error
262
+ */
263
+ async getPlans() {
264
+ const res = await fetch(`${baseUrl}/billing/plans`, {
265
+ credentials: "include",
266
+ signal: AbortSignal.timeout(timeoutMs)
267
+ });
268
+ if (!res.ok) {
269
+ throw new Error(`Failed to get plans: ${res.status}`);
270
+ }
271
+ const data = await res.json();
272
+ return data.map(
273
+ (p) => ({
274
+ id: p.id,
275
+ code: p.code,
276
+ name: p.name,
277
+ description: p.description,
278
+ priceCents: p.price_cents,
279
+ currency: p.currency,
280
+ interval: p.interval,
281
+ intervalCount: p.interval_count,
282
+ trialDays: p.trial_days,
283
+ features: p.features.map((f) => ({
284
+ code: f.code,
285
+ name: f.name,
286
+ featureType: f.feature_type,
287
+ numericValue: f.numeric_value,
288
+ unitLabel: f.unit_label
289
+ })),
290
+ displayOrder: p.display_order,
291
+ creditsAmount: p.credits_amount,
292
+ planType: p.plan_type,
293
+ contactUrl: p.contact_url
294
+ })
295
+ );
296
+ },
297
+ /**
298
+ * Get the current user's subscription status.
299
+ */
300
+ async getSubscription() {
301
+ const res = await fetch(`${baseUrl}/billing/subscription`, {
302
+ credentials: "include",
303
+ signal: AbortSignal.timeout(timeoutMs)
304
+ });
305
+ if (!res.ok) {
306
+ throw new Error(`Failed to get subscription: ${res.status}`);
307
+ }
308
+ const data = await res.json();
309
+ return {
310
+ id: data.id,
311
+ planCode: data.plan_code,
312
+ planName: data.plan_name,
313
+ status: data.status,
314
+ currentPeriodEnd: data.current_period_end,
315
+ trialEnd: data.trial_end,
316
+ cancelAtPeriodEnd: data.cancel_at_period_end
317
+ };
318
+ },
319
+ /**
320
+ * Create a Stripe checkout session to subscribe to a plan.
321
+ * @param planCode The plan code to subscribe to
322
+ * @param successUrl URL to redirect to after successful payment
323
+ * @param cancelUrl URL to redirect to if checkout is canceled
324
+ * @returns Checkout session with URL to redirect the user to
325
+ */
326
+ async createCheckout(planCode, successUrl, cancelUrl) {
327
+ const res = await fetch(`${baseUrl}/billing/checkout`, {
328
+ method: "POST",
329
+ headers: { "Content-Type": "application/json" },
330
+ credentials: "include",
331
+ signal: AbortSignal.timeout(timeoutMs),
332
+ body: JSON.stringify({
333
+ plan_code: planCode,
334
+ success_url: successUrl,
335
+ cancel_url: cancelUrl
336
+ })
337
+ });
338
+ if (!res.ok) {
339
+ const err = await res.json().catch(() => ({}));
340
+ throw new Error(err.message || "Failed to create checkout session");
341
+ }
342
+ const data = await res.json();
343
+ return { checkoutUrl: data.checkout_url };
344
+ },
345
+ /**
346
+ * Redirect user to subscribe to a plan.
347
+ * Creates a checkout session and redirects to Stripe.
348
+ * @param planCode The plan code to subscribe to
349
+ */
350
+ async subscribe(planCode) {
351
+ if (typeof window === "undefined") {
352
+ throw new Error("subscribe() can only be called in browser");
353
+ }
354
+ const currentUrl = window.location.href;
355
+ const { checkoutUrl } = await this.createCheckout(
356
+ planCode,
357
+ currentUrl,
358
+ currentUrl
359
+ );
360
+ assertHttpsUrl(checkoutUrl);
361
+ window.location.href = checkoutUrl;
362
+ },
363
+ /**
364
+ * Open the Stripe customer portal for managing subscription.
365
+ * @param returnUrl URL to return to after leaving the portal
366
+ */
367
+ async openBillingPortal(returnUrl) {
368
+ if (typeof window === "undefined") {
369
+ throw new Error("openBillingPortal() can only be called in browser");
370
+ }
371
+ const res = await fetch(`${baseUrl}/billing/portal`, {
372
+ method: "POST",
373
+ headers: { "Content-Type": "application/json" },
374
+ credentials: "include",
375
+ signal: AbortSignal.timeout(timeoutMs),
376
+ body: JSON.stringify({
377
+ return_url: returnUrl || window.location.href
378
+ })
379
+ });
380
+ if (!res.ok) {
381
+ throw new Error("Failed to open billing portal");
382
+ }
383
+ const data = await res.json();
384
+ assertHttpsUrl(data.portal_url);
385
+ window.location.href = data.portal_url;
386
+ },
387
+ /**
388
+ * Cancel the user's subscription at period end.
389
+ * @throws Error on failure or server error
390
+ */
391
+ async cancelSubscription() {
392
+ const res = await fetch(`${baseUrl}/billing/cancel`, {
393
+ method: "POST",
394
+ credentials: "include",
395
+ signal: AbortSignal.timeout(timeoutMs)
396
+ });
397
+ if (!res.ok) {
398
+ throw new Error(`Failed to cancel subscription: ${res.status}`);
399
+ }
400
+ },
401
+ // ========================================================================
402
+ // Balance Methods
403
+ // ========================================================================
404
+ /**
405
+ * Get the current user's balance.
406
+ * @returns Object with the current balance
407
+ */
408
+ async getBalance() {
409
+ const res = await fetch(`${baseUrl}/balance`, {
410
+ credentials: "include",
411
+ signal: AbortSignal.timeout(timeoutMs)
412
+ });
413
+ if (!res.ok) {
414
+ if (res.status === 401) throw new Error("Not authenticated");
415
+ throw new Error(`Failed to get balance: ${res.status}`);
416
+ }
417
+ const data = await res.json();
418
+ return { balance: data.balance };
419
+ },
420
+ /**
421
+ * Get the current user's balance transaction history.
422
+ * @param opts - Optional pagination (limit, offset)
423
+ * @returns Object with array of transactions (newest first)
424
+ */
425
+ async getTransactions(opts) {
426
+ const params = new URLSearchParams();
427
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
428
+ if (opts?.offset !== void 0) params.set("offset", String(opts.offset));
429
+ const qs = params.toString();
430
+ const res = await fetch(
431
+ `${baseUrl}/balance/transactions${qs ? `?${qs}` : ""}`,
432
+ { credentials: "include", signal: AbortSignal.timeout(timeoutMs) }
433
+ );
434
+ if (!res.ok) {
435
+ if (res.status === 401) throw new Error("Not authenticated");
436
+ throw new Error(`Failed to get transactions: ${res.status}`);
437
+ }
438
+ const data = await res.json();
439
+ return {
440
+ transactions: data.transactions.map(
441
+ (t) => ({
442
+ id: t.id,
443
+ amountDelta: t.amount_delta,
444
+ reason: t.reason,
445
+ balanceAfter: t.balance_after,
446
+ createdAt: t.created_at
447
+ })
448
+ )
449
+ };
450
+ },
451
+ // ========================================================================
452
+ // Credits Methods
453
+ // ========================================================================
454
+ /**
455
+ * Get the domain's credits configuration.
456
+ * Returns exchange rate, display settings, and purchase limits.
457
+ */
458
+ async getCreditsConfig() {
459
+ const res = await fetch(`${baseUrl}/billing/credits-config`, {
460
+ credentials: "include",
461
+ signal: AbortSignal.timeout(timeoutMs)
462
+ });
463
+ if (!res.ok) {
464
+ if (res.status === 401) throw new Error("Not authenticated");
465
+ throw new Error(`Failed to get credits config: ${res.status}`);
466
+ }
467
+ const data = await res.json();
468
+ return {
469
+ creditsEnabled: data.credits_enabled,
470
+ creditsPerDollar: data.credits_per_dollar,
471
+ displayName: data.display_name,
472
+ displaySymbol: data.display_symbol,
473
+ displaySymbolPosition: data.display_symbol_position,
474
+ displayDecimals: data.display_decimals,
475
+ minPurchaseCents: data.min_purchase_cents,
476
+ maxPurchaseCents: data.max_purchase_cents,
477
+ manualTopUpAvailable: data.manual_top_up_available,
478
+ autoTopUpAvailable: data.auto_top_up_available,
479
+ overdrawEnabled: data.overdraw_enabled
480
+ };
481
+ },
482
+ // ========================================================================
483
+ // Payment Method Methods
484
+ // ========================================================================
485
+ /**
486
+ * List the current user's stored payment methods.
487
+ * Sorted by priority (lower number = higher priority).
488
+ */
489
+ async getPaymentMethods() {
490
+ const res = await fetch(`${baseUrl}/billing/payment-methods`, {
491
+ credentials: "include",
492
+ signal: AbortSignal.timeout(timeoutMs)
493
+ });
494
+ if (!res.ok) {
495
+ if (res.status === 401) throw new Error("Not authenticated");
496
+ throw new Error(`Failed to get payment methods: ${res.status}`);
497
+ }
498
+ const data = await res.json();
499
+ return data.map(
500
+ (pm) => ({
501
+ id: pm.id,
502
+ provider: pm.provider,
503
+ methodType: pm.method_type,
504
+ cardBrand: pm.card_brand,
505
+ cardLast4: pm.card_last4,
506
+ cardExpMonth: pm.card_exp_month,
507
+ cardExpYear: pm.card_exp_year,
508
+ priority: pm.priority,
509
+ createdAt: pm.created_at
510
+ })
511
+ );
512
+ },
513
+ /**
514
+ * Create a Stripe SetupIntent for adding a new payment method.
515
+ * Use the returned clientSecret with Stripe.js `confirmCardSetup()`.
516
+ */
517
+ async createSetupIntent() {
518
+ const res = await fetch(`${baseUrl}/billing/setup-intent`, {
519
+ method: "POST",
520
+ credentials: "include",
521
+ signal: AbortSignal.timeout(timeoutMs)
522
+ });
523
+ if (!res.ok) {
524
+ if (res.status === 401) throw new Error("Not authenticated");
525
+ throw new Error(`Failed to create setup intent: ${res.status}`);
526
+ }
527
+ const data = await res.json();
528
+ return {
529
+ clientSecret: data.client_secret,
530
+ setupIntentId: data.setup_intent_id
531
+ };
532
+ },
533
+ /**
534
+ * Delete a stored payment method.
535
+ * @param id UUID of the payment method to delete
536
+ */
537
+ async deletePaymentMethod(id) {
538
+ const res = await fetch(`${baseUrl}/billing/payment-methods/${id}`, {
539
+ method: "DELETE",
540
+ credentials: "include",
541
+ signal: AbortSignal.timeout(timeoutMs)
542
+ });
543
+ if (!res.ok) {
544
+ if (res.status === 401) throw new Error("Not authenticated");
545
+ if (res.status === 404) throw new Error("Payment method not found");
546
+ throw new Error(`Failed to delete payment method: ${res.status}`);
547
+ }
548
+ },
549
+ /**
550
+ * Reorder payment method priorities.
551
+ * @param paymentMethodIds Array of payment method UUIDs in desired priority order
552
+ */
553
+ async reorderPaymentMethods(paymentMethodIds) {
554
+ const res = await fetch(`${baseUrl}/billing/payment-methods/reorder`, {
555
+ method: "PUT",
556
+ headers: { "Content-Type": "application/json" },
557
+ credentials: "include",
558
+ signal: AbortSignal.timeout(timeoutMs),
559
+ body: JSON.stringify({ payment_method_ids: paymentMethodIds })
560
+ });
561
+ if (!res.ok) {
562
+ if (res.status === 401) throw new Error("Not authenticated");
563
+ throw new Error(`Failed to reorder payment methods: ${res.status}`);
564
+ }
565
+ },
566
+ /**
567
+ * Purchase credits using a stored payment method.
568
+ * @param opts Purchase options (amount, payment method, idempotency key)
569
+ */
570
+ async purchaseCredits(opts) {
571
+ const res = await fetch(`${baseUrl}/billing/credits/purchase`, {
572
+ method: "POST",
573
+ headers: { "Content-Type": "application/json" },
574
+ credentials: "include",
575
+ signal: AbortSignal.timeout(timeoutMs),
576
+ body: JSON.stringify({
577
+ amount_cents: opts.amountCents,
578
+ payment_method_id: opts.paymentMethodId,
579
+ idempotency_key: opts.idempotencyKey
580
+ })
581
+ });
582
+ if (!res.ok) {
583
+ if (res.status === 401) throw new Error("Not authenticated");
584
+ const err = await res.json().catch(() => ({}));
585
+ throw new Error(err.message || `Failed to purchase credits: ${res.status}`);
586
+ }
587
+ const data = await res.json();
588
+ return {
589
+ creditsPurchased: data.credits_purchased,
590
+ newBalance: data.new_balance,
591
+ paymentIntentId: data.payment_intent_id
592
+ };
593
+ },
594
+ // ========================================================================
595
+ // Auto Top-Up Methods
596
+ // ========================================================================
597
+ /**
598
+ * Get the current user's auto top-up configuration and status.
599
+ */
600
+ async getAutoTopUpStatus() {
601
+ const res = await fetch(`${baseUrl}/billing/auto-top-up`, {
602
+ credentials: "include",
603
+ signal: AbortSignal.timeout(timeoutMs)
604
+ });
605
+ if (!res.ok) {
606
+ if (res.status === 401) throw new Error("Not authenticated");
607
+ throw new Error(`Failed to get auto top-up status: ${res.status}`);
608
+ }
609
+ const data = await res.json();
610
+ return {
611
+ enabled: data.enabled,
612
+ thresholdCents: data.threshold_cents,
613
+ purchaseAmountCents: data.purchase_amount_cents,
614
+ status: data.status,
615
+ lastFailureReason: data.last_failure_reason,
616
+ retriesRemaining: data.retries_remaining,
617
+ nextRetryAt: data.next_retry_at
618
+ };
619
+ },
620
+ /**
621
+ * Update the current user's auto top-up configuration.
622
+ * @param opts New auto top-up settings
623
+ */
624
+ async updateAutoTopUp(opts) {
625
+ const res = await fetch(`${baseUrl}/billing/auto-top-up`, {
626
+ method: "PUT",
627
+ headers: { "Content-Type": "application/json" },
628
+ credentials: "include",
629
+ signal: AbortSignal.timeout(timeoutMs),
630
+ body: JSON.stringify({
631
+ enabled: opts.enabled,
632
+ threshold_cents: opts.thresholdCents,
633
+ purchase_amount_cents: opts.purchaseAmountCents
634
+ })
635
+ });
636
+ if (!res.ok) {
637
+ if (res.status === 401) throw new Error("Not authenticated");
638
+ const err = await res.json().catch(() => ({}));
639
+ throw new Error(err.message || `Failed to update auto top-up: ${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
+ // Organization Methods
654
+ // ========================================================================
655
+ /**
656
+ * Create a new organization.
657
+ * @param name The organization name
658
+ * @returns The created organization
659
+ */
660
+ async createOrg(name) {
661
+ const res = await fetch(`${baseUrl}/auth/orgs`, {
662
+ method: "POST",
663
+ headers: { "Content-Type": "application/json" },
664
+ credentials: "include",
665
+ signal: AbortSignal.timeout(timeoutMs),
666
+ body: JSON.stringify({ name })
667
+ });
668
+ if (!res.ok) {
669
+ const err = await res.json().catch(() => ({}));
670
+ throw new Error(err.message || `Failed to create org: ${res.status}`);
671
+ }
672
+ const data = await res.json();
673
+ return transformOrg(data);
674
+ },
675
+ /**
676
+ * List all organizations the current user belongs to.
677
+ */
678
+ async listOrgs() {
679
+ const res = await fetch(`${baseUrl}/auth/orgs`, {
680
+ credentials: "include",
681
+ signal: AbortSignal.timeout(timeoutMs)
682
+ });
683
+ if (!res.ok) {
684
+ throw new Error(`Failed to list orgs: ${res.status}`);
685
+ }
686
+ const data = await res.json();
687
+ return data.map(transformOrg);
688
+ },
689
+ /**
690
+ * Get organization details.
691
+ * @param orgId The organization ID
692
+ */
693
+ async getOrg(orgId) {
694
+ const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}`, {
695
+ credentials: "include",
696
+ signal: AbortSignal.timeout(timeoutMs)
697
+ });
698
+ if (!res.ok) {
699
+ if (res.status === 404) throw new Error("Organization not found");
700
+ throw new Error(`Failed to get org: ${res.status}`);
701
+ }
702
+ const data = await res.json();
703
+ return transformOrg(data);
704
+ },
705
+ /**
706
+ * Update an organization's name. Requires owner role.
707
+ * @param orgId The organization ID
708
+ * @param name The new name
709
+ */
710
+ async updateOrg(orgId, name) {
711
+ const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}`, {
712
+ method: "PATCH",
713
+ headers: { "Content-Type": "application/json" },
714
+ credentials: "include",
715
+ signal: AbortSignal.timeout(timeoutMs),
716
+ body: JSON.stringify({ name })
717
+ });
718
+ if (!res.ok) {
719
+ const err = await res.json().catch(() => ({}));
720
+ throw new Error(err.message || `Failed to update org: ${res.status}`);
721
+ }
722
+ const data = await res.json();
723
+ return transformOrg(data);
724
+ },
725
+ /**
726
+ * Delete an organization. Requires owner role.
727
+ * @param orgId The organization ID
728
+ */
729
+ async deleteOrg(orgId) {
730
+ const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}`, {
731
+ method: "DELETE",
732
+ credentials: "include",
733
+ signal: AbortSignal.timeout(timeoutMs)
734
+ });
735
+ if (!res.ok) {
736
+ throw new Error(`Failed to delete org: ${res.status}`);
737
+ }
738
+ },
739
+ // ========================================================================
740
+ // Organization Member Methods
741
+ // ========================================================================
742
+ /**
743
+ * List members of an organization. Requires membership.
744
+ * @param orgId The organization ID
745
+ */
746
+ async listOrgMembers(orgId) {
747
+ const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/members`, {
748
+ credentials: "include",
749
+ signal: AbortSignal.timeout(timeoutMs)
750
+ });
751
+ if (!res.ok) {
752
+ throw new Error(`Failed to list org members: ${res.status}`);
753
+ }
754
+ const data = await res.json();
755
+ return data.map(transformOrgMember);
756
+ },
757
+ /**
758
+ * Remove a member from an organization. Requires owner role.
759
+ * @param orgId The organization ID
760
+ * @param userId The user ID to remove
761
+ */
762
+ async removeOrgMember(orgId, userId) {
763
+ const res = await fetch(
764
+ `${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`,
765
+ {
766
+ method: "DELETE",
767
+ credentials: "include",
768
+ signal: AbortSignal.timeout(timeoutMs)
769
+ }
770
+ );
771
+ if (!res.ok) {
772
+ throw new Error(`Failed to remove org member: ${res.status}`);
773
+ }
774
+ },
775
+ /**
776
+ * Update a member's role. Requires owner role.
777
+ * @param orgId The organization ID
778
+ * @param userId The user ID to update
779
+ * @param role The new role
780
+ */
781
+ async updateMemberRole(orgId, userId, role) {
782
+ const res = await fetch(
783
+ `${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`,
784
+ {
785
+ method: "PATCH",
786
+ headers: { "Content-Type": "application/json" },
787
+ credentials: "include",
788
+ signal: AbortSignal.timeout(timeoutMs),
789
+ body: JSON.stringify({ role })
790
+ }
791
+ );
792
+ if (!res.ok) {
793
+ const err = await res.json().catch(() => ({}));
794
+ throw new Error(err.message || `Failed to update member role: ${res.status}`);
795
+ }
796
+ const data = await res.json();
797
+ return transformOrgMember(data);
798
+ },
799
+ // ========================================================================
800
+ // Organization Context Methods
801
+ // ========================================================================
802
+ /**
803
+ * Switch the active organization. Re-issues session tokens with new org context.
804
+ * @param orgId The organization ID to switch to
805
+ * @returns The new active org context with subscription info
806
+ */
807
+ async switchOrg(orgId) {
808
+ const res = await fetch(`${baseUrl}/auth/switch-org`, {
809
+ method: "POST",
810
+ headers: { "Content-Type": "application/json" },
811
+ credentials: "include",
812
+ signal: AbortSignal.timeout(timeoutMs),
813
+ body: JSON.stringify({ org_id: orgId })
814
+ });
815
+ if (!res.ok) {
816
+ const err = await res.json().catch(() => ({}));
817
+ throw new Error(err.message || `Failed to switch org: ${res.status}`);
818
+ }
819
+ const data = await res.json();
820
+ return {
821
+ activeOrgId: data.active_org_id,
822
+ orgRole: data.org_role,
823
+ subscription: {
824
+ status: data.subscription.status,
825
+ planCode: data.subscription.plan_code,
826
+ planName: data.subscription.plan_name,
827
+ currentPeriodEnd: data.subscription.current_period_end,
828
+ cancelAtPeriodEnd: data.subscription.cancel_at_period_end,
829
+ trialEndsAt: data.subscription.trial_ends_at
830
+ }
831
+ };
832
+ },
833
+ /**
834
+ * Get the active organization from the current session.
835
+ * @returns Active org ID and role, or null if no active org
836
+ */
837
+ async getActiveOrg() {
838
+ const session = await this.getSession();
839
+ if (!session.valid || !session.active_org_id || !session.org_role) {
840
+ return null;
841
+ }
842
+ return {
843
+ id: session.active_org_id,
844
+ role: session.org_role
845
+ };
846
+ },
847
+ // ========================================================================
848
+ // Invitation Methods
849
+ // ========================================================================
850
+ /**
851
+ * Send an email invite to join an organization. Requires owner role.
852
+ * @param orgId The organization ID
853
+ * @param email The email address to invite
854
+ * @param role Optional role (defaults to "member")
855
+ */
856
+ async inviteToOrg(orgId, email, role) {
857
+ const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invites`, {
858
+ method: "POST",
859
+ headers: { "Content-Type": "application/json" },
860
+ credentials: "include",
861
+ signal: AbortSignal.timeout(timeoutMs),
862
+ body: JSON.stringify({ email, role: role ?? "member" })
863
+ });
864
+ if (!res.ok) {
865
+ const err = await res.json().catch(() => ({}));
866
+ throw new Error(err.message || `Failed to send invite: ${res.status}`);
867
+ }
868
+ const data = await res.json();
869
+ return transformInvitation(data);
870
+ },
871
+ /**
872
+ * List pending invites for an organization. Requires owner role.
873
+ * @param orgId The organization ID
874
+ */
875
+ async listPendingInvites(orgId) {
876
+ const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invites`, {
877
+ credentials: "include",
878
+ signal: AbortSignal.timeout(timeoutMs)
879
+ });
880
+ if (!res.ok) {
881
+ throw new Error(`Failed to list invites: ${res.status}`);
882
+ }
883
+ const data = await res.json();
884
+ return data.map(transformInvitation);
885
+ },
886
+ /**
887
+ * Revoke a pending invite. Requires owner role.
888
+ * @param orgId The organization ID
889
+ * @param inviteId The invite ID to revoke
890
+ */
891
+ async revokeInvite(orgId, inviteId) {
892
+ const res = await fetch(
893
+ `${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}`,
894
+ {
895
+ method: "DELETE",
896
+ credentials: "include",
897
+ signal: AbortSignal.timeout(timeoutMs)
898
+ }
899
+ );
900
+ if (!res.ok) {
901
+ throw new Error(`Failed to revoke invite: ${res.status}`);
902
+ }
903
+ },
904
+ /**
905
+ * Accept an email invite using the invite token.
906
+ * @param token The invite token from the email
907
+ * @returns The org ID and role assigned
908
+ */
909
+ async acceptInvite(token) {
910
+ const res = await fetch(
911
+ `${baseUrl}/auth/orgs/invites/${encodeURIComponent(token)}/accept`,
912
+ {
913
+ method: "POST",
914
+ credentials: "include",
915
+ signal: AbortSignal.timeout(timeoutMs)
916
+ }
917
+ );
918
+ if (!res.ok) {
919
+ const err = await res.json().catch(() => ({}));
920
+ throw new Error(err.message || `Failed to accept invite: ${res.status}`);
921
+ }
922
+ const data = await res.json();
923
+ return {
924
+ orgId: data.org_id,
925
+ role: data.role
926
+ };
927
+ },
928
+ /**
929
+ * Create a shareable invite link for an organization. Requires owner role.
930
+ * @param orgId The organization ID
931
+ * @param role Optional role for joiners (defaults to "member")
932
+ */
933
+ async createInviteLink(orgId, role) {
934
+ const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invite-links`, {
935
+ method: "POST",
936
+ headers: { "Content-Type": "application/json" },
937
+ credentials: "include",
938
+ signal: AbortSignal.timeout(timeoutMs),
939
+ body: JSON.stringify({ role: role ?? "member" })
940
+ });
941
+ if (!res.ok) {
942
+ const err = await res.json().catch(() => ({}));
943
+ throw new Error(err.message || `Failed to create invite link: ${res.status}`);
944
+ }
945
+ const data = await res.json();
946
+ return transformInviteLink(data);
947
+ },
948
+ /**
949
+ * List invite links for an organization. Requires owner role.
950
+ * @param orgId The organization ID
951
+ */
952
+ async listInviteLinks(orgId) {
953
+ const res = await fetch(`${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invite-links`, {
954
+ credentials: "include",
955
+ signal: AbortSignal.timeout(timeoutMs)
956
+ });
957
+ if (!res.ok) {
958
+ throw new Error(`Failed to list invite links: ${res.status}`);
959
+ }
960
+ const data = await res.json();
961
+ return data.map(transformInviteLink);
962
+ },
963
+ /**
964
+ * Revoke an invite link. Requires owner role.
965
+ * @param orgId The organization ID
966
+ * @param linkId The invite link ID to revoke
967
+ */
968
+ async revokeInviteLink(orgId, linkId) {
969
+ const res = await fetch(
970
+ `${baseUrl}/auth/orgs/${encodeURIComponent(orgId)}/invite-links/${encodeURIComponent(linkId)}`,
971
+ {
972
+ method: "DELETE",
973
+ credentials: "include",
974
+ signal: AbortSignal.timeout(timeoutMs)
975
+ }
976
+ );
977
+ if (!res.ok) {
978
+ throw new Error(`Failed to revoke invite link: ${res.status}`);
979
+ }
980
+ },
981
+ /**
982
+ * Join an organization via an invite link token.
983
+ * @param token The invite link token
984
+ */
985
+ async joinViaLink(token) {
986
+ const res = await fetch(`${baseUrl}/auth/orgs/join/${encodeURIComponent(token)}`, {
987
+ method: "POST",
988
+ credentials: "include",
989
+ signal: AbortSignal.timeout(timeoutMs)
990
+ });
991
+ if (!res.ok) {
992
+ const err = await res.json().catch(() => ({}));
993
+ throw new Error(err.message || `Failed to join via link: ${res.status}`);
994
+ }
995
+ }
996
+ };
997
+ }
998
+ function transformOrg(data) {
999
+ return {
1000
+ id: data.id,
1001
+ name: data.name,
1002
+ isPersonal: data.is_personal,
1003
+ createdAt: data.created_at
1004
+ };
1005
+ }
1006
+ function transformOrgMember(data) {
1007
+ return {
1008
+ id: data.id,
1009
+ orgId: data.org_id,
1010
+ endUserId: data.end_user_id,
1011
+ role: data.role,
1012
+ joinedAt: data.joined_at
1013
+ };
1014
+ }
1015
+ function transformInvitation(data) {
1016
+ return {
1017
+ id: data.id,
1018
+ orgId: data.org_id,
1019
+ email: data.email,
1020
+ token: data.token,
1021
+ role: data.role,
1022
+ status: data.status,
1023
+ expiresAt: data.expires_at,
1024
+ acceptedAt: data.accepted_at,
1025
+ createdAt: data.created_at
1026
+ };
1027
+ }
1028
+ function transformInviteLink(data) {
1029
+ return {
1030
+ id: data.id,
1031
+ orgId: data.org_id,
1032
+ token: data.token,
1033
+ role: data.role,
1034
+ isActive: data.is_active,
1035
+ expiresAt: data.expires_at,
1036
+ createdAt: data.created_at
1037
+ };
1038
+ }
1039
+
1040
+ export {
1041
+ createReauthClient
1042
+ };