@object-ui/auth 17.3.0 → 17.6.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +184 -13
  3. package/dist/ActiveOrganizationStorage.d.ts +168 -0
  4. package/dist/ActiveOrganizationStorage.d.ts.map +1 -0
  5. package/dist/ActiveOrganizationStorage.js +515 -0
  6. package/dist/AuthContext.d.ts +34 -1
  7. package/dist/AuthContext.d.ts.map +1 -1
  8. package/dist/AuthGuard.d.ts +8 -1
  9. package/dist/AuthGuard.d.ts.map +1 -1
  10. package/dist/AuthGuard.js +16 -3
  11. package/dist/AuthProvider.d.ts +1 -1
  12. package/dist/AuthProvider.d.ts.map +1 -1
  13. package/dist/AuthProvider.js +359 -32
  14. package/dist/ForgotPasswordForm.d.ts +1 -1
  15. package/dist/ForgotPasswordForm.d.ts.map +1 -1
  16. package/dist/ForgotPasswordForm.js +5 -5
  17. package/dist/LoginForm.d.ts +1 -1
  18. package/dist/LoginForm.d.ts.map +1 -1
  19. package/dist/LoginForm.js +4 -4
  20. package/dist/PreviewBanner.js +1 -1
  21. package/dist/RegisterForm.d.ts +1 -1
  22. package/dist/RegisterForm.d.ts.map +1 -1
  23. package/dist/RegisterForm.js +3 -3
  24. package/dist/SocialSignInButtons.js +2 -2
  25. package/dist/UserMenu.js +2 -2
  26. package/dist/createAuthClient.d.ts +23 -1
  27. package/dist/createAuthClient.d.ts.map +1 -1
  28. package/dist/createAuthClient.js +112 -25
  29. package/dist/createAuthenticatedFetch.d.ts +9 -9
  30. package/dist/createAuthenticatedFetch.d.ts.map +1 -1
  31. package/dist/createAuthenticatedFetch.js +82 -37
  32. package/dist/index.d.ts +20 -19
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +28 -17
  35. package/dist/invitation-status.d.ts +59 -0
  36. package/dist/invitation-status.d.ts.map +1 -0
  37. package/dist/invitation-status.js +32 -0
  38. package/dist/types.d.ts +52 -4
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/useAuth.d.ts +1 -1
  41. package/dist/useAuth.d.ts.map +1 -1
  42. package/dist/useAuth.js +6 -1
  43. package/dist/useWorkspaceAdminStatus.d.ts +85 -0
  44. package/dist/useWorkspaceAdminStatus.d.ts.map +1 -0
  45. package/dist/useWorkspaceAdminStatus.js +102 -0
  46. package/package.json +5 -5
  47. package/dist/useIsWorkspaceAdmin.d.ts +0 -21
  48. package/dist/useIsWorkspaceAdmin.d.ts.map +0 -1
  49. package/dist/useIsWorkspaceAdmin.js +0 -47
@@ -7,8 +7,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  */
9
9
  import { useEffect, useState } from 'react';
10
- import { useAuth } from './useAuth';
11
- import { AuthSpinner } from './authStyles';
10
+ import { useAuth } from './useAuth.js';
11
+ import { AuthSpinner } from './authStyles.js';
12
12
  // Brand name overrides for providers whose `name` from the server may be unset
13
13
  // or where we want to canonicalize casing.
14
14
  const PROVIDER_LABEL = {
package/dist/UserMenu.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useAuth } from './useAuth';
3
- import { getUserInitials } from './types';
2
+ import { useAuth } from './useAuth.js';
3
+ import { getUserInitials } from './types.js';
4
4
  /**
5
5
  * User menu component displaying the authenticated user's info.
6
6
  * Shows avatar, name, email, and common actions (profile, settings, sign out).
@@ -5,16 +5,38 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import type { AuthClient, AuthClientConfig } from './types';
8
+ import type { AuthClient, AuthClientConfig } from './types.js';
9
9
  /**
10
10
  * Simple token storage backed by localStorage.
11
11
  * Falls back to in-memory storage when localStorage is unavailable (SSR, tests).
12
+ *
13
+ * ## Rotation notifications (objectui#4467)
14
+ *
15
+ * `subscribeRotation` fires when a token WE ALREADY HELD is replaced by a
16
+ * different one — i.e. the server rotated the session under a request nobody
17
+ * was watching. That is the only case with no owner: sign-in, sign-up and
18
+ * sign-out all set/clear this storage from `AuthProvider`, which updates its
19
+ * own identity state in the same breath, so those transitions deliberately
20
+ * stay silent (first store: no notify; `clear()`: no notify; re-storing the
21
+ * same value, as `getSession` does on every boot: no notify).
22
+ *
23
+ * A rotation with no owner is exactly what impersonation produces — the
24
+ * console POSTs `/auth/admin/impersonate-user` through a generic metadata
25
+ * action, the server hands back a NEW session token in `set-auth-token`, and
26
+ * nothing in that code path knows the identity just changed. See
27
+ * `AuthProvider`'s subscription and `__tests__/impersonation-lane-4467.test.tsx`.
12
28
  */
13
29
  export declare const TokenStorage: {
14
30
  _memoryToken: string | null;
31
+ _rotationListeners: Set<() => void>;
15
32
  get(): string | null;
16
33
  set(token: string): void;
17
34
  clear(): void;
35
+ /**
36
+ * Observe session-token ROTATIONS (see the header for what does and does not
37
+ * count as one). Returns an unsubscribe function.
38
+ */
39
+ subscribeRotation(listener: () => void): () => void;
18
40
  };
19
41
  /**
20
42
  * Create an auth client instance backed by the official better-auth client.
@@ -1 +1 @@
1
- {"version":3,"file":"createAuthClient.d.ts","sourceRoot":"","sources":["../src/createAuthClient.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EACV,UAAU,EAAE,gBAAgB,EAE7B,MAAM,SAAS,CAAC;AAIjB;;;GAGG;AACH,eAAO,MAAM,YAAY;kBACD,MAAM,GAAG,IAAI;WAE5B,MAAM,GAAG,IAAI;eAST,MAAM,GAAG,IAAI;aASf,IAAI;CAQd,CAAC;AAmHF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAqrBrE"}
1
+ {"version":3,"file":"createAuthClient.d.ts","sourceRoot":"","sources":["../src/createAuthClient.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EACV,UAAU,EAAE,gBAAgB,EAE7B,MAAM,YAAY,CAAC;AAKpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY;kBACD,MAAM,GAAG,IAAI;kCACD,IAAI;WAE/B,MAAM,GAAG,IAAI;eAST,MAAM,GAAG,IAAI;aAkBf,IAAI;IASb;;;OAGG;gCACyB,MAAM,IAAI,GAAG,MAAM,IAAI;CAMpD,CAAC;AA2KF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAqrBrE"}
@@ -7,13 +7,31 @@
7
7
  */
8
8
  import { createAuthClient as createBetterAuthClient } from 'better-auth/client';
9
9
  import { organizationClient, twoFactorClient } from 'better-auth/client/plugins';
10
+ import { AUTH_INVITATION_STATUSES, isAuthInvitationStatus } from './invitation-status.js';
10
11
  const TOKEN_STORAGE_KEY = 'auth-session-token';
11
12
  /**
12
13
  * Simple token storage backed by localStorage.
13
14
  * Falls back to in-memory storage when localStorage is unavailable (SSR, tests).
15
+ *
16
+ * ## Rotation notifications (objectui#4467)
17
+ *
18
+ * `subscribeRotation` fires when a token WE ALREADY HELD is replaced by a
19
+ * different one — i.e. the server rotated the session under a request nobody
20
+ * was watching. That is the only case with no owner: sign-in, sign-up and
21
+ * sign-out all set/clear this storage from `AuthProvider`, which updates its
22
+ * own identity state in the same breath, so those transitions deliberately
23
+ * stay silent (first store: no notify; `clear()`: no notify; re-storing the
24
+ * same value, as `getSession` does on every boot: no notify).
25
+ *
26
+ * A rotation with no owner is exactly what impersonation produces — the
27
+ * console POSTs `/auth/admin/impersonate-user` through a generic metadata
28
+ * action, the server hands back a NEW session token in `set-auth-token`, and
29
+ * nothing in that code path knows the identity just changed. See
30
+ * `AuthProvider`'s subscription and `__tests__/impersonation-lane-4467.test.tsx`.
14
31
  */
15
32
  export const TokenStorage = {
16
33
  _memoryToken: null,
34
+ _rotationListeners: new Set(),
17
35
  get() {
18
36
  try {
19
37
  if (typeof localStorage !== 'undefined') {
@@ -24,6 +42,7 @@ export const TokenStorage = {
24
42
  return this._memoryToken;
25
43
  },
26
44
  set(token) {
45
+ const previous = this.get();
27
46
  this._memoryToken = token;
28
47
  try {
29
48
  if (typeof localStorage !== 'undefined') {
@@ -31,6 +50,15 @@ export const TokenStorage = {
31
50
  }
32
51
  }
33
52
  catch { /* SSR / test */ }
53
+ // Rotation = a token we were already using got replaced. See the header.
54
+ if (previous && previous !== token) {
55
+ for (const listener of [...this._rotationListeners]) {
56
+ try {
57
+ listener();
58
+ }
59
+ catch { /* a listener must never break the write that triggered it */ }
60
+ }
61
+ }
34
62
  },
35
63
  clear() {
36
64
  this._memoryToken = null;
@@ -41,19 +69,78 @@ export const TokenStorage = {
41
69
  }
42
70
  catch { /* SSR / test */ }
43
71
  },
72
+ /**
73
+ * Observe session-token ROTATIONS (see the header for what does and does not
74
+ * count as one). Returns an unsubscribe function.
75
+ */
76
+ subscribeRotation(listener) {
77
+ this._rotationListeners.add(listener);
78
+ return () => {
79
+ this._rotationListeners.delete(listener);
80
+ };
81
+ },
44
82
  };
45
83
  /**
46
84
  * Build an `Error` from a better-auth client error, preserving the machine
47
- * `code` on the thrown Error so callers (LoginForm/RegisterForm) can map it to
48
- * a localized message instead of surfacing the raw English server text. Falls
49
- * back to the server message, then the HTTP status.
85
+ * `code` on the thrown Error so callers (LoginForm/RegisterForm, and every
86
+ * organization screen) can map it to a localized message instead of surfacing
87
+ * the raw English server text. Falls back to the server message, then
88
+ * `fallbackMessage`, then the HTTP status.
89
+ *
90
+ * ## Why every organization route now comes through here (objectui#4474)
91
+ *
92
+ * This helper existed for sign-in/sign-up only. Every `organization.*` method
93
+ * below threw `new Error(error.message ?? '…')` directly and therefore **dropped
94
+ * `code` at the boundary** — so a console screen holding the rejection had the
95
+ * English sentence and nothing else to key on. That is what forced the
96
+ * organization UI to echo better-auth's English verbatim in a zh session: the
97
+ * only remaining way to localize it would have been matching the English text,
98
+ * which binds the console to a third party's copy-editing and breaks silently
99
+ * the day they reword a sentence.
100
+ *
101
+ * The repair belongs HERE rather than at the console call sites: the code is
102
+ * produced at this boundary, and a consumer cannot recover information the
103
+ * producer discarded. `message` is unchanged for every caller — this only stops
104
+ * throwing away the half of the pair that was already being computed.
105
+ * `packages/auth/src/__tests__/org-error-code-4474.test.ts` pins both halves.
50
106
  */
51
- function toAuthError(error) {
52
- const err = new Error(error.message ?? `Auth request failed with status ${error.status}`);
107
+ function toAuthError(error, fallbackMessage) {
108
+ const err = new Error(error.message ?? fallbackMessage ?? `Auth request failed with status ${error.status}`);
53
109
  if (error.code)
54
110
  err.code = error.code;
55
111
  return err;
56
112
  }
113
+ /**
114
+ * The wire boundary for invitations (objectui#3879).
115
+ *
116
+ * better-auth's client hands these routes back as `any`, so `as AuthInvitation`
117
+ * was a claim nothing checked — and with `status` narrowed to a closed union,
118
+ * an unchecked cast would make that union a comment again the day a backend
119
+ * stored a fifth value. This is the ONE place the claim is verified.
120
+ *
121
+ * It fails LOUDLY, naming the offending value, rather than degrading: a status
122
+ * outside better-auth's own `InvitationStatus` is a producer defect, and the
123
+ * alternatives all hide it — a neutral badge label renders it as UI copy in ten
124
+ * packs (the objectui#3879 symptom), and dropping the row deletes an invitation
125
+ * from an administrative ledger without saying so. Both call paths already
126
+ * render a rejection with a retry (`InvitationsPage`, `AcceptInvitationPage`),
127
+ * so the throw lands on a designed surface; `resolveOrgErrorMessage` shows the
128
+ * sentence verbatim, which is that module's own stated preference over
129
+ * swallowing an error.
130
+ */
131
+ function asAuthInvitation(raw, method) {
132
+ const status = raw?.status;
133
+ if (!isAuthInvitationStatus(status)) {
134
+ throw new Error(`${method}: unexpected invitation status ${JSON.stringify(status)} — ` +
135
+ `expected one of ${AUTH_INVITATION_STATUSES.join(' | ')}`);
136
+ }
137
+ return raw;
138
+ }
139
+ /** Same boundary, per row, for the list-returning invitation routes. */
140
+ function asAuthInvitations(raw, method) {
141
+ const rows = Array.isArray(raw) ? raw : [];
142
+ return rows.map((row) => asAuthInvitation(row, method));
143
+ }
57
144
  /**
58
145
  * Resolve a baseURL (which may be relative or absolute) into the
59
146
  * `{ origin, basePath }` pair required by the better-auth client.
@@ -630,7 +717,7 @@ export function createAuthClient(config) {
630
717
  async listOrganizations() {
631
718
  const { data, error } = await betterAuth.organization.list();
632
719
  if (error)
633
- throw new Error(error.message ?? 'Failed to list organizations');
720
+ throw toAuthError(error, 'Failed to list organizations');
634
721
  return (data ?? []);
635
722
  },
636
723
  async createOrganization(orgData) {
@@ -640,7 +727,7 @@ export function createAuthClient(config) {
640
727
  logo: orgData.logo,
641
728
  });
642
729
  if (error)
643
- throw new Error(error.message ?? 'Failed to create organization');
730
+ throw toAuthError(error, 'Failed to create organization');
644
731
  return data;
645
732
  },
646
733
  async setActiveOrganization(orgId) {
@@ -648,7 +735,7 @@ export function createAuthClient(config) {
648
735
  organizationId: orgId,
649
736
  });
650
737
  if (error)
651
- throw new Error(error.message ?? 'Failed to set active organization');
738
+ throw toAuthError(error, 'Failed to set active organization');
652
739
  return (data ?? null);
653
740
  },
654
741
  async getActiveOrganization() {
@@ -677,7 +764,7 @@ export function createAuthClient(config) {
677
764
  query: { organizationId: orgId },
678
765
  });
679
766
  if (error)
680
- throw new Error(error.message ?? 'Failed to get members');
767
+ throw toAuthError(error, 'Failed to get members');
681
768
  const result = data;
682
769
  if (Array.isArray(result))
683
770
  return result;
@@ -697,8 +784,8 @@ export function createAuthClient(config) {
697
784
  ...(inviteData.positions?.length ? { positions: inviteData.positions } : {}),
698
785
  });
699
786
  if (error)
700
- throw new Error(error.message ?? 'Failed to invite member');
701
- return data;
787
+ throw toAuthError(error, 'Failed to invite member');
788
+ return asAuthInvitation(data, 'inviteMember');
702
789
  },
703
790
  async removeMember(removeData) {
704
791
  const { error } = await betterAuth.organization.removeMember({
@@ -706,7 +793,7 @@ export function createAuthClient(config) {
706
793
  memberIdOrUserId: removeData.memberIdOrUserId,
707
794
  });
708
795
  if (error)
709
- throw new Error(error.message ?? 'Failed to remove member');
796
+ throw toAuthError(error, 'Failed to remove member');
710
797
  },
711
798
  async updateMemberRole(payload) {
712
799
  const { error } = await betterAuth.organization.updateMemberRole({
@@ -715,7 +802,7 @@ export function createAuthClient(config) {
715
802
  role: payload.role,
716
803
  });
717
804
  if (error)
718
- throw new Error(error.message ?? 'Failed to update member role');
805
+ throw toAuthError(error, 'Failed to update member role');
719
806
  },
720
807
  async updateOrganization(orgId, orgData) {
721
808
  const { data, error } = await betterAuth.organization.update({
@@ -723,7 +810,7 @@ export function createAuthClient(config) {
723
810
  data: orgData,
724
811
  });
725
812
  if (error)
726
- throw new Error(error.message ?? 'Failed to update organization');
813
+ throw toAuthError(error, 'Failed to update organization');
727
814
  return data;
728
815
  },
729
816
  async deleteOrganization(orgId) {
@@ -731,14 +818,14 @@ export function createAuthClient(config) {
731
818
  organizationId: orgId,
732
819
  });
733
820
  if (error)
734
- throw new Error(error.message ?? 'Failed to delete organization');
821
+ throw toAuthError(error, 'Failed to delete organization');
735
822
  },
736
823
  async leaveOrganization(orgId) {
737
824
  const { error } = await betterAuth.organization.leave({
738
825
  organizationId: orgId,
739
826
  });
740
827
  if (error)
741
- throw new Error(error.message ?? 'Failed to leave organization');
828
+ throw toAuthError(error, 'Failed to leave organization');
742
829
  },
743
830
  // --- Invitation methods ---
744
831
  async listInvitations(orgId) {
@@ -746,37 +833,37 @@ export function createAuthClient(config) {
746
833
  query: { organizationId: orgId },
747
834
  });
748
835
  if (error)
749
- throw new Error(error.message ?? 'Failed to list invitations');
750
- return (data ?? []);
836
+ throw toAuthError(error, 'Failed to list invitations');
837
+ return asAuthInvitations(data, 'listInvitations');
751
838
  },
752
839
  async cancelInvitation(invitationId) {
753
840
  const { error } = await betterAuth.organization.cancelInvitation({ invitationId });
754
841
  if (error)
755
- throw new Error(error.message ?? 'Failed to cancel invitation');
842
+ throw toAuthError(error, 'Failed to cancel invitation');
756
843
  },
757
844
  async getInvitation(invitationId) {
758
845
  const { data, error } = await betterAuth.organization.getInvitation({
759
846
  query: { id: invitationId },
760
847
  });
761
848
  if (error)
762
- throw new Error(error.message ?? 'Failed to load invitation');
763
- return data;
849
+ throw toAuthError(error, 'Failed to load invitation');
850
+ return asAuthInvitation(data, 'getInvitation');
764
851
  },
765
852
  async acceptInvitation(invitationId) {
766
853
  const { error } = await betterAuth.organization.acceptInvitation({ invitationId });
767
854
  if (error)
768
- throw new Error(error.message ?? 'Failed to accept invitation');
855
+ throw toAuthError(error, 'Failed to accept invitation');
769
856
  },
770
857
  async rejectInvitation(invitationId) {
771
858
  const { error } = await betterAuth.organization.rejectInvitation({ invitationId });
772
859
  if (error)
773
- throw new Error(error.message ?? 'Failed to reject invitation');
860
+ throw toAuthError(error, 'Failed to reject invitation');
774
861
  },
775
862
  async listUserInvitations() {
776
863
  const { data, error } = await betterAuth.organization.listUserInvitations();
777
864
  if (error)
778
- throw new Error(error.message ?? 'Failed to list invitations');
779
- return (data ?? []);
865
+ throw toAuthError(error, 'Failed to list invitations');
866
+ return asAuthInvitations(data, 'listUserInvitations');
780
867
  },
781
868
  };
782
869
  }
@@ -15,15 +15,13 @@ export interface AuthenticatedAdapterOptions {
15
15
  [key: string]: unknown;
16
16
  }
17
17
  /**
18
- * Get/set the active organization ID for tenant-scoped API requests.
19
- * Used by createAuthenticatedFetch to inject X-Tenant-ID header.
18
+ * Re-exported from its own module so this file stays the WIRE lane and the
19
+ * storage lane has one home. `ActiveOrganizationStorage` moved out when it
20
+ * gained per-user scoping (objectui#5664); the export identity is unchanged,
21
+ * so `@object-ui/auth`'s barrel, this package's tests and every consumer
22
+ * import the same symbol from the same place they did before.
20
23
  */
21
- export declare const ActiveOrganizationStorage: {
22
- _memoryValue: string | null;
23
- get(): string | null;
24
- set(orgId: string): void;
25
- clear(): void;
26
- };
24
+ export { ActiveOrganizationStorage, SessionUserScope, purgePreviousUserClientState } from './ActiveOrganizationStorage.js';
27
25
  export interface CreateAuthenticatedFetchOptions {
28
26
  /**
29
27
  * When true, requests whose URL resolves to a different origin than the
@@ -39,7 +37,9 @@ export interface CreateAuthenticatedFetchOptions {
39
37
  /**
40
38
  * Creates an authenticated fetch wrapper that injects the Bearer token
41
39
  * from localStorage into every request to the ObjectStack API.
42
- * Also injects X-Tenant-ID header when an active organization is set.
40
+ * Also injects X-Tenant-ID header when an active organization is set — see the
41
+ * "The `X-Tenant-ID` edge contract" section of this package's README for what
42
+ * that header means, who reads it, and the window in which it is not sent.
43
43
  *
44
44
  * @example
45
45
  * ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"createAuthenticatedFetch.d.ts","sourceRoot":"","sources":["../src/createAuthenticatedFetch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID;;;GAGG;AACH,eAAO,MAAM,yBAAyB;kBACd,MAAM,GAAG,IAAI;WAE5B,MAAM,GAAG,IAAI;eAST,MAAM,GAAG,IAAI;aASf,IAAI;CAQd,CAAC;AAEF,MAAM,WAAW,+BAA+B;IAC9C;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAYD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,CAAC,EAAE,+BAA+B,GACxC,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CA0CrE"}
1
+ {"version":3,"file":"createAuthenticatedFetch.d.ts","sourceRoot":"","sources":["../src/createAuthenticatedFetch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,OAAO,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAE3H,MAAM,WAAW,+BAA+B;IAC9C;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAYD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,CAAC,EAAE,+BAA+B,GACxC,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CA+GrE"}
@@ -5,43 +5,17 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import { TokenStorage } from './createAuthClient';
9
- import { authGateEvents, detectAuthGate } from './auth-gate-events';
10
- const ACTIVE_ORG_STORAGE_KEY = 'auth-active-organization-id';
8
+ import { TokenStorage } from './createAuthClient.js';
9
+ import { authGateEvents, detectAuthGate } from './auth-gate-events.js';
10
+ import { ActiveOrganizationStorage } from './ActiveOrganizationStorage.js';
11
11
  /**
12
- * Get/set the active organization ID for tenant-scoped API requests.
13
- * Used by createAuthenticatedFetch to inject X-Tenant-ID header.
12
+ * Re-exported from its own module so this file stays the WIRE lane and the
13
+ * storage lane has one home. `ActiveOrganizationStorage` moved out when it
14
+ * gained per-user scoping (objectui#5664); the export identity is unchanged,
15
+ * so `@object-ui/auth`'s barrel, this package's tests and every consumer
16
+ * import the same symbol from the same place they did before.
14
17
  */
15
- export const ActiveOrganizationStorage = {
16
- _memoryValue: null,
17
- get() {
18
- try {
19
- if (typeof localStorage !== 'undefined') {
20
- return localStorage.getItem(ACTIVE_ORG_STORAGE_KEY);
21
- }
22
- }
23
- catch { /* SSR / test */ }
24
- return this._memoryValue;
25
- },
26
- set(orgId) {
27
- this._memoryValue = orgId;
28
- try {
29
- if (typeof localStorage !== 'undefined') {
30
- localStorage.setItem(ACTIVE_ORG_STORAGE_KEY, orgId);
31
- }
32
- }
33
- catch { /* SSR / test */ }
34
- },
35
- clear() {
36
- this._memoryValue = null;
37
- try {
38
- if (typeof localStorage !== 'undefined') {
39
- localStorage.removeItem(ACTIVE_ORG_STORAGE_KEY);
40
- }
41
- }
42
- catch { /* SSR / test */ }
43
- },
44
- };
18
+ export { ActiveOrganizationStorage, SessionUserScope, purgePreviousUserClientState } from './ActiveOrganizationStorage.js';
45
19
  /** True when `url` resolves to a different origin than the current page. */
46
20
  function isCrossOrigin(url) {
47
21
  if (typeof window === 'undefined' || !window.location)
@@ -56,7 +30,9 @@ function isCrossOrigin(url) {
56
30
  /**
57
31
  * Creates an authenticated fetch wrapper that injects the Bearer token
58
32
  * from localStorage into every request to the ObjectStack API.
59
- * Also injects X-Tenant-ID header when an active organization is set.
33
+ * Also injects X-Tenant-ID header when an active organization is set — see the
34
+ * "The `X-Tenant-ID` edge contract" section of this package's README for what
35
+ * that header means, who reads it, and the window in which it is not sent.
60
36
  *
61
37
  * @example
62
38
  * ```ts
@@ -83,7 +59,43 @@ export function createAuthenticatedFetch(options) {
83
59
  if (token && isApiCall) {
84
60
  headers.set('Authorization', `Bearer ${token}`);
85
61
  }
86
- // Inject tenant header for multi-tenant routing
62
+ // ── `X-Tenant-ID` — the edge routing contract (objectui#5279) ────────
63
+ //
64
+ // WHAT IT MEANS. A routing hint for the hosting edge: "this request belongs
65
+ // to tenant <id>". The value is the better-auth `activeOrganizationId` the
66
+ // session already carries. It is NOT an identity claim, NOT an
67
+ // authorization input, and NOT what scopes rows.
68
+ //
69
+ // DO NOT DELETE THIS ON THE STRENGTH OF A GREP. The framework
70
+ // (`objectstack`) does not read it: `resolveAuthzContext` takes `tenantId`
71
+ // from the API-key principal or `session.activeOrganizationId` and never
72
+ // from a header, and environment routing reads the hostname and
73
+ // `X-Environment-Id`. So a search confined to this repo plus the framework
74
+ // finds zero consumers and reads as "dead stamp" — which is the false
75
+ // premise objectui#5279 was filed on. The consumer is in the CLOUD repo,
76
+ // which neither checkout contains: `service-tenant`'s `tenant-context.ts`
77
+ // resolves the header, and `tenant-router`'s `turso-multi-tenant.zod.ts`
78
+ // configures that resolution. The configuration contract IS readable from
79
+ // here — `TenantRoutingConfigSchema` in `@objectstack/spec/cloud` defaults
80
+ // `tenantHeaderName` to `X-Tenant-ID` and ranks `header` SECOND of six
81
+ // identification sources, behind `subdomain`.
82
+ //
83
+ // THE UNSTAMPED-FIRST-REQUEST GAP. `ActiveOrganizationStorage` is filled
84
+ // only after AuthProvider's async organization chain resolves, so early
85
+ // requests go out with the header ABSENT — never present-and-empty. A
86
+ // reader must fall through to its next identification source rather than
87
+ // fail closed. Nothing about row visibility rides on this: the framework
88
+ // scopes from the session, so a response computed inside the window is
89
+ // still computed for the right tenant.
90
+ //
91
+ // Not gated on `isApiCall`, unlike `Authorization` and `Accept-Language`
92
+ // above — recorded as the behaviour that ships, not endorsed; the
93
+ // asymmetry is reported on objectui#5279 for triage to route. A wrapper
94
+ // built with `sameOriginOnly` short-circuits every cross-origin request
95
+ // before this line.
96
+ //
97
+ // Full contract, including what a reader may and may not assume: this
98
+ // package's README, "The `X-Tenant-ID` edge contract".
87
99
  const activeOrgId = ActiveOrganizationStorage.get();
88
100
  if (activeOrgId) {
89
101
  headers.set('X-Tenant-ID', activeOrgId);
@@ -103,6 +115,39 @@ export function createAuthenticatedFetch(options) {
103
115
  }
104
116
  }
105
117
  const response = await fetch(input, { ...init, headers });
118
+ // Adopt a session rotation the server declares on the response, exactly as
119
+ // the auth lane already does (`createBearerFetch` in createAuthClient.ts).
120
+ //
121
+ // ## Why this lane needs it too (objectui#4467)
122
+ //
123
+ // The console injects the same localStorage bearer from two places: the
124
+ // AUTH lane (sign-in / get-session / the auth endpoints) and THIS one — the
125
+ // adapter, `provider: 'api'` data sources, and every metadata `type: 'api'`
126
+ // action (`useConsoleActionRuntime`'s `apiHandler`). better-auth's
127
+ // server-side bearer plugin hands a rotated session token back in
128
+ // `set-auth-token` on whichever lane the call arrived over. Only the auth
129
+ // lane read it, so a rotation issued to a data-lane call was dropped on the
130
+ // floor and the browser kept sending the OLD token.
131
+ //
132
+ // Impersonation is exactly that call: `POST /auth/admin/impersonate-user`
133
+ // runs as an ordinary metadata action, so the impersonated session token
134
+ // arrived here and was discarded — while the server's bearer plugin kept
135
+ // overwriting the impersonation cookie with the admin bearer we kept
136
+ // sending. Impersonation was a complete no-op in the console, not merely an
137
+ // invisible one.
138
+ //
139
+ // The rule is the server's declared contract and carries no knowledge of
140
+ // which endpoint rotated: one contract, one answer, on both lanes. Gated on
141
+ // `isApiCall` — the same condition that decided we authenticated this
142
+ // request at all — so a response we never sent the bearer to cannot rotate
143
+ // the session. Untrusted targets are the `sameOriginOnly` option's job (it
144
+ // short-circuits above, before any header work).
145
+ if (isApiCall) {
146
+ const rotatedToken = response.headers.get('set-auth-token');
147
+ if (rotatedToken) {
148
+ TokenStorage.set(rotatedToken);
149
+ }
150
+ }
106
151
  // ADR-0069 — surface an auth-policy gate (expired password / required MFA)
107
152
  // to the remediation overlay. Clone so the caller still reads the body.
108
153
  if (isApiCall && response.status === 403) {
package/dist/index.d.ts CHANGED
@@ -19,23 +19,24 @@
19
19
  *
20
20
  * @packageDocumentation
21
21
  */
22
- export { AuthProvider, type AuthProviderProps } from './AuthProvider';
23
- export { useAuth } from './useAuth';
24
- export { useIsWorkspaceAdmin } from './useIsWorkspaceAdmin';
25
- export { AuthGuard, type AuthGuardProps } from './AuthGuard';
26
- export { AuthShell, type AuthShellProps, type AuthShellBrandPanel } from './AuthShell';
27
- export { LoginForm, type LoginFormProps, type LoginFormLabels } from './LoginForm';
28
- export { RegisterForm, type RegisterFormProps, type RegisterFormLabels } from './RegisterForm';
29
- export { ForgotPasswordForm, type ForgotPasswordFormProps, type ForgotPasswordFormLabels } from './ForgotPasswordForm';
30
- export { SocialSignInButtons, type SocialSignInButtonsProps } from './SocialSignInButtons';
31
- export { UserMenu, type UserMenuProps } from './UserMenu';
32
- export { PreviewBanner, type PreviewBannerProps } from './PreviewBanner';
33
- export { createAuthClient, TokenStorage } from './createAuthClient';
34
- export { normalizePhoneIdentifier, looksLikePhoneIdentifier } from './phone-identifier';
35
- export { createAuthenticatedFetch, ActiveOrganizationStorage, type AuthenticatedAdapterOptions } from './createAuthenticatedFetch';
36
- export { getUserInitials } from './types';
37
- export { ORG_ROLE_OWNER, ORG_ROLE_ADMIN, ORG_ROLE_DELEGATED_ADMIN, ORG_ROLE_MEMBER, ORG_ROLES, ORG_ROLE_LABELS, orgRoleGrade, invitableOrgRoles, assignableOrgRoles, type OrgRole, } from './org-roles';
38
- export { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthAlertIcon, AuthCheckIcon, AuthDivider, AuthErrorBanner, AuthFormHeader, AuthMailIcon, AuthSpinner, } from './authStyles';
39
- export type { AuthUser, AuthClientSession, AuthState, AuthClient, AuthClientConfig, AuthLinkComponentProps, AuthProviderOptions, PreviewModeOptions, SignInCredentials, SignUpData, AuthOrganization, AuthOrganizationMember, AuthInvitation, AuthSocialProvider, AuthPublicConfig, SignInWithProviderOptions, TenancyPosture, DelegableScope, } from './types';
40
- export type { AuthContextValue } from './AuthContext';
22
+ export { AuthProvider, type AuthProviderProps } from './AuthProvider.js';
23
+ export { useAuth } from './useAuth.js';
24
+ export { useWorkspaceAdminStatus, type WorkspaceAdminStatus } from './useWorkspaceAdminStatus.js';
25
+ export { AuthGuard, type AuthGuardProps } from './AuthGuard.js';
26
+ export { AuthShell, type AuthShellProps, type AuthShellBrandPanel } from './AuthShell.js';
27
+ export { LoginForm, type LoginFormProps, type LoginFormLabels } from './LoginForm.js';
28
+ export { RegisterForm, type RegisterFormProps, type RegisterFormLabels } from './RegisterForm.js';
29
+ export { ForgotPasswordForm, type ForgotPasswordFormProps, type ForgotPasswordFormLabels } from './ForgotPasswordForm.js';
30
+ export { SocialSignInButtons, type SocialSignInButtonsProps } from './SocialSignInButtons.js';
31
+ export { UserMenu, type UserMenuProps } from './UserMenu.js';
32
+ export { PreviewBanner, type PreviewBannerProps } from './PreviewBanner.js';
33
+ export { createAuthClient, TokenStorage } from './createAuthClient.js';
34
+ export { normalizePhoneIdentifier, looksLikePhoneIdentifier } from './phone-identifier.js';
35
+ export { createAuthenticatedFetch, ActiveOrganizationStorage, type AuthenticatedAdapterOptions } from './createAuthenticatedFetch.js';
36
+ export { getUserInitials } from './types.js';
37
+ export { ORG_ROLE_OWNER, ORG_ROLE_ADMIN, ORG_ROLE_DELEGATED_ADMIN, ORG_ROLE_MEMBER, ORG_ROLES, ORG_ROLE_LABELS, orgRoleGrade, invitableOrgRoles, assignableOrgRoles, type OrgRole, } from './org-roles.js';
38
+ export { AUTH_INVITATION_STATUSES, isAuthInvitationStatus, type AuthInvitationStatus, } from './invitation-status.js';
39
+ export { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthAlertIcon, AuthCheckIcon, AuthDivider, AuthErrorBanner, AuthFormHeader, AuthMailIcon, AuthSpinner, } from './authStyles.js';
40
+ export type { AuthUser, AuthClientSession, AuthState, AuthClient, AuthClientConfig, AuthLinkComponentProps, AuthProviderOptions, PreviewModeOptions, SignInCredentials, SignUpData, AuthOrganization, AuthOrganizationMember, AuthInvitation, AuthSocialProvider, AuthPublicConfig, SignInWithProviderOptions, TenancyPosture, DelegableScope, } from './types.js';
41
+ export type { AuthContextValue } from './AuthContext.js';
41
42
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC/F,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACvH,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AACxF,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,KAAK,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACnI,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAO1C,OAAO,EACL,cAAc,EACd,cAAc,EACd,wBAAwB,EACxB,eAAe,EACf,SAAS,EACT,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,OAAO,GACb,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,cAAc,EACd,YAAY,EACZ,WAAW,GACZ,MAAM,cAAc,CAAC;AAGtB,YAAY,EACV,QAAQ,EACR,iBAAiB,EACjB,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,cAAc,GACf,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAOvC,OAAO,EAAE,uBAAuB,EAAE,KAAK,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAClG,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAC1H,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,KAAK,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AACtI,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAO7C,OAAO,EACL,cAAc,EACd,cAAc,EACd,wBAAwB,EACxB,eAAe,EACf,SAAS,EACT,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,OAAO,GACb,MAAM,gBAAgB,CAAC;AAMxB,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,KAAK,oBAAoB,GAC1B,MAAM,wBAAwB,CAAC;AAIhC,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,cAAc,EACd,YAAY,EACZ,WAAW,GACZ,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EACV,QAAQ,EACR,iBAAiB,EACjB,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}