@reauth-dev/sdk 0.2.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.
@@ -23,7 +23,8 @@ __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
 
@@ -164,6 +165,126 @@ function createReauthClient(config) {
164
165
  }
165
166
  },
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
+ };
286
+ },
287
+ // ========================================================================
167
288
  // Billing Methods
168
289
  // ========================================================================
169
290
  /**
@@ -358,9 +479,595 @@ function createReauthClient(config) {
358
479
  })
359
480
  )
360
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
+ }
361
1027
  }
362
1028
  };
363
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
+ }
364
1071
 
365
1072
  // src/react/useAuth.ts
366
1073
  var DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
@@ -402,7 +1109,9 @@ function useAuth(config) {
402
1109
  user: {
403
1110
  id: session.end_user_id,
404
1111
  email: session.email,
405
- roles: session.roles || []
1112
+ roles: session.roles || [],
1113
+ activeOrgId: session.active_org_id || "",
1114
+ orgRole: session.org_role || ""
406
1115
  },
407
1116
  loading: false,
408
1117
  error: null,
@@ -416,7 +1125,9 @@ function useAuth(config) {
416
1125
  user: {
417
1126
  id: session.end_user_id,
418
1127
  email: session.email,
419
- roles: session.roles || []
1128
+ roles: session.roles || [],
1129
+ activeOrgId: session.active_org_id || "",
1130
+ orgRole: session.org_role || ""
420
1131
  },
421
1132
  loading: false,
422
1133
  error: null,
@@ -472,16 +1183,141 @@ function useAuth(config) {
472
1183
  };
473
1184
  }
474
1185
 
475
- // src/react/AuthProvider.tsx
1186
+ // src/react/useHeadlessAuth.ts
476
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");
477
1313
  var import_jsx_runtime = require("react/jsx-runtime");
478
- var AuthContext = (0, import_react2.createContext)(null);
1314
+ var AuthContext = (0, import_react3.createContext)(null);
479
1315
  function AuthProvider({ config, children }) {
480
1316
  const auth = useAuth(config);
481
1317
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthContext.Provider, { value: auth, children });
482
1318
  }
483
1319
  function useAuthContext() {
484
- const context = (0, import_react2.useContext)(AuthContext);
1320
+ const context = (0, import_react3.useContext)(AuthContext);
485
1321
  if (!context) {
486
1322
  throw new Error("useAuthContext must be used within AuthProvider");
487
1323
  }
@@ -489,7 +1325,7 @@ function useAuthContext() {
489
1325
  }
490
1326
 
491
1327
  // src/react/ProtectedRoute.tsx
492
- var import_react3 = require("react");
1328
+ var import_react4 = require("react");
493
1329
  var import_jsx_runtime2 = require("react/jsx-runtime");
494
1330
  function ProtectedRoute({
495
1331
  children,
@@ -498,8 +1334,8 @@ function ProtectedRoute({
498
1334
  onWaitlist
499
1335
  }) {
500
1336
  const { user, loading, isOnWaitlist, login } = useAuthContext();
501
- const hasRedirected = (0, import_react3.useRef)(false);
502
- (0, import_react3.useEffect)(() => {
1337
+ const hasRedirected = (0, import_react4.useRef)(false);
1338
+ (0, import_react4.useEffect)(() => {
503
1339
  if (!loading && !user && !hasRedirected.current) {
504
1340
  hasRedirected.current = true;
505
1341
  if (onUnauthenticated) {
@@ -509,7 +1345,7 @@ function ProtectedRoute({
509
1345
  }
510
1346
  }
511
1347
  }, [loading, user, login, onUnauthenticated]);
512
- (0, import_react3.useEffect)(() => {
1348
+ (0, import_react4.useEffect)(() => {
513
1349
  if (!loading && isOnWaitlist && onWaitlist && !hasRedirected.current) {
514
1350
  hasRedirected.current = true;
515
1351
  onWaitlist();
@@ -528,5 +1364,6 @@ function ProtectedRoute({
528
1364
  AuthProvider,
529
1365
  ProtectedRoute,
530
1366
  useAuth,
531
- useAuthContext
1367
+ useAuthContext,
1368
+ useHeadlessAuth
532
1369
  });