@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.
- package/CHANGELOG.md +213 -0
- package/README.md +184 -13
- package/dist/ActiveOrganizationStorage.d.ts +168 -0
- package/dist/ActiveOrganizationStorage.d.ts.map +1 -0
- package/dist/ActiveOrganizationStorage.js +515 -0
- package/dist/AuthContext.d.ts +34 -1
- package/dist/AuthContext.d.ts.map +1 -1
- package/dist/AuthGuard.d.ts +8 -1
- package/dist/AuthGuard.d.ts.map +1 -1
- package/dist/AuthGuard.js +16 -3
- package/dist/AuthProvider.d.ts +1 -1
- package/dist/AuthProvider.d.ts.map +1 -1
- package/dist/AuthProvider.js +359 -32
- package/dist/ForgotPasswordForm.d.ts +1 -1
- package/dist/ForgotPasswordForm.d.ts.map +1 -1
- package/dist/ForgotPasswordForm.js +5 -5
- package/dist/LoginForm.d.ts +1 -1
- package/dist/LoginForm.d.ts.map +1 -1
- package/dist/LoginForm.js +4 -4
- package/dist/PreviewBanner.js +1 -1
- package/dist/RegisterForm.d.ts +1 -1
- package/dist/RegisterForm.d.ts.map +1 -1
- package/dist/RegisterForm.js +3 -3
- package/dist/SocialSignInButtons.js +2 -2
- package/dist/UserMenu.js +2 -2
- package/dist/createAuthClient.d.ts +23 -1
- package/dist/createAuthClient.d.ts.map +1 -1
- package/dist/createAuthClient.js +112 -25
- package/dist/createAuthenticatedFetch.d.ts +9 -9
- package/dist/createAuthenticatedFetch.d.ts.map +1 -1
- package/dist/createAuthenticatedFetch.js +82 -37
- package/dist/index.d.ts +20 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -17
- package/dist/invitation-status.d.ts +59 -0
- package/dist/invitation-status.d.ts.map +1 -0
- package/dist/invitation-status.js +32 -0
- package/dist/types.d.ts +52 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/useAuth.d.ts +1 -1
- package/dist/useAuth.d.ts.map +1 -1
- package/dist/useAuth.js +6 -1
- package/dist/useWorkspaceAdminStatus.d.ts +85 -0
- package/dist/useWorkspaceAdminStatus.d.ts.map +1 -0
- package/dist/useWorkspaceAdminStatus.js +102 -0
- package/package.json +5 -5
- package/dist/useIsWorkspaceAdmin.d.ts +0 -21
- package/dist/useIsWorkspaceAdmin.d.ts.map +0 -1
- 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,
|
|
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"}
|
package/dist/createAuthClient.js
CHANGED
|
@@ -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
|
|
48
|
-
* a localized message instead of surfacing
|
|
49
|
-
* back to the server message, then
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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;
|
|
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
|
-
|
|
8
|
+
import { TokenStorage } from './createAuthClient.js';
|
|
9
|
+
import { authGateEvents, detectAuthGate } from './auth-gate-events.js';
|
|
10
|
+
import { ActiveOrganizationStorage } from './ActiveOrganizationStorage.js';
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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 {
|
|
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 {
|
|
39
|
-
export
|
|
40
|
-
export type {
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|