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