@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2
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/README.md +12 -0
- package/dist/_exports/_internal.d.ts +163 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +332 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/_exports/agent.d.ts +65 -0
- package/dist/_exports/agent.d.ts.map +1 -0
- package/dist/_exports/agent.js +71 -0
- package/dist/_exports/agent.js.map +1 -0
- package/dist/_exports/comlink.d.ts +118 -0
- package/dist/_exports/comlink.d.ts.map +1 -0
- package/dist/_exports/comlink.js +69 -0
- package/dist/_exports/comlink.js.map +1 -0
- package/dist/authStore-CtE9bLHW.js +754 -0
- package/dist/authStore-CtE9bLHW.js.map +1 -0
- package/dist/authStore-DKFX8fn7.d.ts +3179 -0
- package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
- package/dist/clientStore-BtE2-q2i.js +90 -0
- package/dist/clientStore-BtE2-q2i.js.map +1 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
- package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
- package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
- package/dist/getEnv-D5RGkQLu.js +8 -0
- package/dist/getEnv-D5RGkQLu.js.map +1 -0
- package/dist/getNodeState-CR3__B2D.js +67 -0
- package/dist/getNodeState-CR3__B2D.js.map +1 -0
- package/dist/index.d.ts +3 -2332
- package/dist/index.js +4171 -4332
- package/dist/index.js.map +1 -1
- package/dist/logger-DAtJb5RQ.js +173 -0
- package/dist/logger-DAtJb5RQ.js.map +1 -0
- package/dist/object-CxXlSVg7.js +83 -0
- package/dist/object-CxXlSVg7.js.map +1 -0
- package/dist/queryStore-DPf9sN3V.js +1346 -0
- package/dist/queryStore-DPf9sN3V.js.map +1 -0
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
- package/dist/telemetryManager-Dq_qCkeI.js +127 -0
- package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
- package/dist/version-BChjsi4u.js +9 -0
- package/dist/version-BChjsi4u.js.map +1 -0
- package/package.json +47 -52
- package/src/_exports/_internal.ts +27 -0
- package/src/_exports/agent.ts +19 -0
- package/src/_exports/comlink.ts +16 -0
- package/src/_exports/index.ts +125 -64
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +88 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +367 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +58 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/comments/addonDatasetStore.test.ts +215 -0
- package/src/comments/addonDatasetStore.ts +249 -0
- package/src/comments/buildCommentThreads.test.ts +83 -0
- package/src/comments/buildCommentThreads.ts +54 -0
- package/src/comments/commentActions.test.ts +540 -0
- package/src/comments/commentActions.ts +555 -0
- package/src/comments/commentFieldPath.test.ts +40 -0
- package/src/comments/commentFieldPath.ts +44 -0
- package/src/comments/commentsConstants.ts +80 -0
- package/src/comments/commentsStore.test.ts +419 -0
- package/src/comments/commentsStore.ts +483 -0
- package/src/comments/normalizeComment.test.ts +116 -0
- package/src/comments/normalizeComment.ts +45 -0
- package/src/comments/observeComments.test.ts +193 -0
- package/src/comments/observeComments.ts +113 -0
- package/src/comments/reducers.test.ts +277 -0
- package/src/comments/reducers.ts +316 -0
- package/src/comments/types.ts +249 -0
- package/src/comments/weakenReferences.test.ts +47 -0
- package/src/comments/weakenReferences.ts +29 -0
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +40 -82
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +35 -13
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +15 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +753 -273
- package/src/document/documentStore.ts +197 -75
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +76 -2
- package/src/document/patchOperations.ts +60 -1
- package/src/document/permissions.test.ts +107 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +772 -64
- package/src/document/reducers.ts +205 -51
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.test.ts +46 -0
- package/src/document/util.ts +50 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +73 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +206 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +201 -9
- package/src/presence/bifurTransport.ts +98 -14
- package/src/presence/paths.test.ts +70 -0
- package/src/presence/paths.ts +41 -0
- package/src/presence/presenceStore.test.ts +915 -24
- package/src/presence/presenceStore.ts +576 -104
- package/src/presence/types.ts +175 -3
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/types.ts +0 -15
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -9
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +85 -69
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +109 -90
- package/src/store/createActionBinder.ts +36 -28
- package/src/store/createSanityInstance.test.ts +15 -13
- package/src/store/createSanityInstance.ts +14 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +38 -43
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +3 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -2,8 +2,6 @@ import {
|
|
|
2
2
|
distinctUntilChanged,
|
|
3
3
|
exhaustMap,
|
|
4
4
|
filter,
|
|
5
|
-
firstValueFrom,
|
|
6
|
-
from,
|
|
7
5
|
map,
|
|
8
6
|
Observable,
|
|
9
7
|
type Subscription,
|
|
@@ -13,42 +11,12 @@ import {
|
|
|
13
11
|
} from 'rxjs'
|
|
14
12
|
|
|
15
13
|
import {type StoreContext} from '../store/defineStore'
|
|
16
|
-
import {DEFAULT_API_VERSION} from './authConstants'
|
|
14
|
+
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
|
|
15
|
+
import {getAuthLogger} from './authLogger'
|
|
17
16
|
import {AuthStateType} from './authStateType'
|
|
18
17
|
import {type AuthState, type AuthStoreState} from './authStore'
|
|
19
18
|
|
|
20
19
|
const REFRESH_INTERVAL = 12 * 60 * 60 * 1000 // 12 hours in milliseconds
|
|
21
|
-
const LOCK_NAME = 'sanity-token-refresh-lock'
|
|
22
|
-
|
|
23
|
-
/** @internal */
|
|
24
|
-
export function getLastRefreshTime(storageArea: Storage | undefined, storageKey: string): number {
|
|
25
|
-
try {
|
|
26
|
-
const data = storageArea?.getItem(`${storageKey}_last_refresh`)
|
|
27
|
-
const parsed = data ? parseInt(data, 10) : 0
|
|
28
|
-
return isNaN(parsed) ? 0 : parsed
|
|
29
|
-
} catch {
|
|
30
|
-
return 0
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** @internal */
|
|
35
|
-
export function setLastRefreshTime(storageArea: Storage | undefined, storageKey: string): void {
|
|
36
|
-
try {
|
|
37
|
-
storageArea?.setItem(`${storageKey}_last_refresh`, Date.now().toString())
|
|
38
|
-
} catch {
|
|
39
|
-
// Ignore storage errors
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** @internal */
|
|
44
|
-
export function getNextRefreshDelay(storageArea: Storage | undefined, storageKey: string): number {
|
|
45
|
-
const lastRefresh = getLastRefreshTime(storageArea, storageKey)
|
|
46
|
-
if (!lastRefresh) return 0
|
|
47
|
-
|
|
48
|
-
const now = Date.now()
|
|
49
|
-
const nextRefreshTime = lastRefresh + REFRESH_INTERVAL
|
|
50
|
-
return Math.max(0, nextRefreshTime - now)
|
|
51
|
-
}
|
|
52
20
|
|
|
53
21
|
function createTokenRefreshStream(
|
|
54
22
|
token: string,
|
|
@@ -58,7 +26,7 @@ function createTokenRefreshStream(
|
|
|
58
26
|
return new Observable((subscriber) => {
|
|
59
27
|
const client = clientFactory({
|
|
60
28
|
apiVersion: DEFAULT_API_VERSION,
|
|
61
|
-
requestTagPrefix:
|
|
29
|
+
requestTagPrefix: REQUEST_TAG_PREFIX,
|
|
62
30
|
useProjectHostname: false,
|
|
63
31
|
useCdn: false,
|
|
64
32
|
token,
|
|
@@ -70,6 +38,7 @@ function createTokenRefreshStream(
|
|
|
70
38
|
.request<{token: string}>({
|
|
71
39
|
uri: 'auth/refresh-token',
|
|
72
40
|
method: 'POST',
|
|
41
|
+
tag: 'refresh-token',
|
|
73
42
|
body: {
|
|
74
43
|
token,
|
|
75
44
|
},
|
|
@@ -80,57 +49,6 @@ function createTokenRefreshStream(
|
|
|
80
49
|
})
|
|
81
50
|
}
|
|
82
51
|
|
|
83
|
-
async function acquireTokenRefreshLock(
|
|
84
|
-
refreshFn: () => Promise<void>,
|
|
85
|
-
storageArea: Storage | undefined,
|
|
86
|
-
storageKey: string,
|
|
87
|
-
): Promise<boolean> {
|
|
88
|
-
if (!navigator.locks) {
|
|
89
|
-
// If Web Locks API is not supported, perform an immediate, uncoordinated refresh.
|
|
90
|
-
// eslint-disable-next-line no-console
|
|
91
|
-
console.warn('Web Locks API not supported. Proceeding with uncoordinated refresh.')
|
|
92
|
-
await refreshFn()
|
|
93
|
-
setLastRefreshTime(storageArea, storageKey)
|
|
94
|
-
return true // Indicate success to allow stream processing, though it won't loop.
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
// Attempt to acquire an exclusive lock for token refresh coordination.
|
|
99
|
-
// The callback handles the continuous refresh cycle while the lock is held.
|
|
100
|
-
const result = await navigator.locks.request(LOCK_NAME, {mode: 'exclusive'}, async (lock) => {
|
|
101
|
-
if (!lock) return false // Lock not granted
|
|
102
|
-
|
|
103
|
-
// Problematic infinite loop - needs redesign for graceful termination.
|
|
104
|
-
// This loop continuously refreshes the token at REFRESH_INTERVAL.
|
|
105
|
-
while (true) {
|
|
106
|
-
const delay = getNextRefreshDelay(storageArea, storageKey)
|
|
107
|
-
if (delay > 0) {
|
|
108
|
-
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
await refreshFn()
|
|
112
|
-
setLastRefreshTime(storageArea, storageKey)
|
|
113
|
-
} catch (error) {
|
|
114
|
-
// eslint-disable-next-line no-console
|
|
115
|
-
console.error('Token refresh failed within lock:', error)
|
|
116
|
-
// Decide how to handle errors - break, retry, etc.? Currently logs and continues.
|
|
117
|
-
}
|
|
118
|
-
// Wait for the next interval
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve, REFRESH_INTERVAL))
|
|
120
|
-
}
|
|
121
|
-
// Unreachable due to while(true)
|
|
122
|
-
})
|
|
123
|
-
// The promise from navigator.locks.request resolves with the callback's return value,
|
|
124
|
-
// but only if the callback finishes. The infinite loop prevents this.
|
|
125
|
-
return result === true
|
|
126
|
-
} catch (error) {
|
|
127
|
-
// Handle potential errors during the initial lock request itself.
|
|
128
|
-
// eslint-disable-next-line no-console
|
|
129
|
-
console.error('Failed to request token refresh lock:', error)
|
|
130
|
-
return false // Indicate lock acquisition failure.
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
52
|
function shouldRefreshToken(lastRefresh: number | undefined): boolean {
|
|
135
53
|
if (!lastRefresh) return true
|
|
136
54
|
const timeSinceLastRefresh = Date.now() - lastRefresh
|
|
@@ -138,99 +56,46 @@ function shouldRefreshToken(lastRefresh: number | undefined): boolean {
|
|
|
138
56
|
}
|
|
139
57
|
|
|
140
58
|
/**
|
|
59
|
+
* Periodically refreshes stamped auth tokens (those containing `-st`).
|
|
60
|
+
*
|
|
61
|
+
* Two triggers cause a refresh while the user stays logged in:
|
|
62
|
+
* - a 12-hour interval timer (only fires while the tab is visible)
|
|
63
|
+
* - the tab becoming visible again after `lastTokenRefresh` has gone stale
|
|
64
|
+
*
|
|
65
|
+
* Non-stamped tokens (e.g. personal access tokens) are never refreshed.
|
|
66
|
+
*
|
|
141
67
|
* @internal
|
|
142
68
|
*/
|
|
143
|
-
export const refreshStampedToken = ({
|
|
69
|
+
export const refreshStampedToken = ({
|
|
70
|
+
state,
|
|
71
|
+
instance,
|
|
72
|
+
}: StoreContext<AuthStoreState>): Subscription => {
|
|
73
|
+
const logger = getAuthLogger(instance)
|
|
74
|
+
|
|
144
75
|
const {clientFactory, apiHost, storageArea, storageKey} = state.get().options
|
|
145
76
|
|
|
146
77
|
const refreshToken$ = state.observable.pipe(
|
|
147
|
-
map((storeState) =>
|
|
148
|
-
authState: storeState.authState,
|
|
149
|
-
dashboardContext: storeState.dashboardContext,
|
|
150
|
-
})),
|
|
78
|
+
map((storeState) => storeState.authState),
|
|
151
79
|
filter(
|
|
152
|
-
(
|
|
153
|
-
|
|
154
|
-
): storeState is {
|
|
155
|
-
authState: Extract<AuthState, {type: AuthStateType.LOGGED_IN}>
|
|
156
|
-
dashboardContext: AuthStoreState['dashboardContext']
|
|
157
|
-
} => storeState.authState.type === AuthStateType.LOGGED_IN,
|
|
80
|
+
(authState): authState is Extract<AuthState, {type: AuthStateType.LOGGED_IN}> =>
|
|
81
|
+
authState.type === AuthStateType.LOGGED_IN,
|
|
158
82
|
),
|
|
159
|
-
distinctUntilChanged(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const currentToken = currentState.authState.token
|
|
176
|
-
|
|
177
|
-
const response = await firstValueFrom(
|
|
178
|
-
createTokenRefreshStream(currentToken, clientFactory, apiHost),
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
state.set('setRefreshStampedToken', (prev) => ({
|
|
182
|
-
authState:
|
|
183
|
-
prev.authState.type === AuthStateType.LOGGED_IN
|
|
184
|
-
? {...prev.authState, token: response.token}
|
|
185
|
-
: prev.authState,
|
|
186
|
-
}))
|
|
187
|
-
storageArea?.setItem(storageKey, JSON.stringify({token: response.token}))
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (storeState.dashboardContext) {
|
|
191
|
-
return new Observable<{token: string}>((subscriber) => {
|
|
192
|
-
const visibilityHandler = () => {
|
|
193
|
-
const currentState = state.get()
|
|
194
|
-
if (
|
|
195
|
-
document.visibilityState === 'visible' &&
|
|
196
|
-
currentState.authState.type === AuthStateType.LOGGED_IN &&
|
|
197
|
-
shouldRefreshToken(currentState.authState.lastTokenRefresh)
|
|
198
|
-
) {
|
|
199
|
-
createTokenRefreshStream(
|
|
200
|
-
currentState.authState.token,
|
|
201
|
-
clientFactory,
|
|
202
|
-
apiHost,
|
|
203
|
-
).subscribe({
|
|
204
|
-
next: (response) => {
|
|
205
|
-
state.set('setRefreshStampedToken', (prev) => ({
|
|
206
|
-
authState:
|
|
207
|
-
prev.authState.type === AuthStateType.LOGGED_IN
|
|
208
|
-
? {
|
|
209
|
-
...prev.authState,
|
|
210
|
-
token: response.token,
|
|
211
|
-
lastTokenRefresh: Date.now(),
|
|
212
|
-
}
|
|
213
|
-
: prev.authState,
|
|
214
|
-
}))
|
|
215
|
-
subscriber.next(response)
|
|
216
|
-
},
|
|
217
|
-
error: (error) => subscriber.error(error),
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const timerSubscription = timer(REFRESH_INTERVAL, REFRESH_INTERVAL)
|
|
223
|
-
.pipe(
|
|
224
|
-
filter(() => document.visibilityState === 'visible'),
|
|
225
|
-
switchMap(() => {
|
|
226
|
-
const currentState = state.get().authState
|
|
227
|
-
if (currentState.type !== AuthStateType.LOGGED_IN) {
|
|
228
|
-
throw new Error('User logged out before refresh could complete')
|
|
229
|
-
}
|
|
230
|
-
return createTokenRefreshStream(currentState.token, clientFactory, apiHost)
|
|
231
|
-
}),
|
|
232
|
-
)
|
|
233
|
-
.subscribe({
|
|
83
|
+
distinctUntilChanged((prev, curr) => prev.token === curr.token),
|
|
84
|
+
filter((authState) => authState.token.includes('-st')), // Ensure we only try to refresh stamped tokens
|
|
85
|
+
exhaustMap(() =>
|
|
86
|
+
new Observable<{token: string}>((subscriber) => {
|
|
87
|
+
const visibilityHandler = () => {
|
|
88
|
+
const currentState = state.get()
|
|
89
|
+
if (
|
|
90
|
+
document.visibilityState === 'visible' &&
|
|
91
|
+
currentState.authState.type === AuthStateType.LOGGED_IN &&
|
|
92
|
+
shouldRefreshToken(currentState.authState.lastTokenRefresh)
|
|
93
|
+
) {
|
|
94
|
+
createTokenRefreshStream(
|
|
95
|
+
currentState.authState.token,
|
|
96
|
+
clientFactory,
|
|
97
|
+
apiHost,
|
|
98
|
+
).subscribe({
|
|
234
99
|
next: (response) => {
|
|
235
100
|
state.set('setRefreshStampedToken', (prev) => ({
|
|
236
101
|
authState:
|
|
@@ -246,35 +111,53 @@ export const refreshStampedToken = ({state}: StoreContext<AuthStoreState>): Subs
|
|
|
246
111
|
},
|
|
247
112
|
error: (error) => subscriber.error(error),
|
|
248
113
|
})
|
|
249
|
-
|
|
250
|
-
document.addEventListener('visibilitychange', visibilityHandler)
|
|
251
|
-
|
|
252
|
-
return () => {
|
|
253
|
-
document.removeEventListener('visibilitychange', visibilityHandler)
|
|
254
|
-
timerSubscription.unsubscribe()
|
|
255
114
|
}
|
|
256
|
-
}
|
|
257
|
-
takeWhile(() => state.get().authState.type === AuthStateType.LOGGED_IN),
|
|
258
|
-
map((response: {token: string}) => ({token: response.token})),
|
|
259
|
-
)
|
|
260
|
-
}
|
|
115
|
+
}
|
|
261
116
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
117
|
+
const timerSubscription = timer(REFRESH_INTERVAL, REFRESH_INTERVAL)
|
|
118
|
+
.pipe(
|
|
119
|
+
filter(() => document.visibilityState === 'visible'),
|
|
120
|
+
switchMap(() => {
|
|
121
|
+
const currentState = state.get().authState
|
|
122
|
+
if (currentState.type !== AuthStateType.LOGGED_IN) {
|
|
123
|
+
throw new Error('User logged out before refresh could complete')
|
|
124
|
+
}
|
|
125
|
+
return createTokenRefreshStream(currentState.token, clientFactory, apiHost)
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
.subscribe({
|
|
129
|
+
next: (response) => {
|
|
130
|
+
state.set('setRefreshStampedToken', (prev) => ({
|
|
131
|
+
authState:
|
|
132
|
+
prev.authState.type === AuthStateType.LOGGED_IN
|
|
133
|
+
? {
|
|
134
|
+
...prev.authState,
|
|
135
|
+
token: response.token,
|
|
136
|
+
lastTokenRefresh: Date.now(),
|
|
137
|
+
}
|
|
138
|
+
: prev.authState,
|
|
139
|
+
}))
|
|
140
|
+
subscriber.next(response)
|
|
141
|
+
},
|
|
142
|
+
error: (error) => subscriber.error(error),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
document.addEventListener('visibilitychange', visibilityHandler)
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
document.removeEventListener('visibilitychange', visibilityHandler)
|
|
149
|
+
timerSubscription.unsubscribe()
|
|
150
|
+
}
|
|
151
|
+
}).pipe(
|
|
152
|
+
takeWhile(() => state.get().authState.type === AuthStateType.LOGGED_IN),
|
|
153
|
+
map((response: {token: string}) => ({token: response.token})),
|
|
154
|
+
),
|
|
155
|
+
),
|
|
274
156
|
)
|
|
275
157
|
|
|
276
158
|
return refreshToken$.subscribe({
|
|
277
159
|
next: (response: {token: string}) => {
|
|
160
|
+
logger.debug('Token refresh completed, updating state')
|
|
278
161
|
state.set('setRefreshStampedToken', (prev) => ({
|
|
279
162
|
authState:
|
|
280
163
|
prev.authState.type === AuthStateType.LOGGED_IN
|
|
@@ -288,6 +171,7 @@ export const refreshStampedToken = ({state}: StoreContext<AuthStoreState>): Subs
|
|
|
288
171
|
storageArea?.setItem(storageKey, JSON.stringify({token: response.token}))
|
|
289
172
|
},
|
|
290
173
|
error: (error) => {
|
|
174
|
+
logger.error('Token refresh failed', {error})
|
|
291
175
|
state.set('setRefreshStampedTokenError', {authState: {type: AuthStateType.ERROR, error}})
|
|
292
176
|
},
|
|
293
177
|
})
|
package/src/auth/studioAuth.ts
CHANGED
|
@@ -211,7 +211,7 @@ function initializeWithFallback(
|
|
|
211
211
|
: null
|
|
212
212
|
|
|
213
213
|
if (!token) {
|
|
214
|
-
const projectIdValue = instance.config.
|
|
214
|
+
const projectIdValue = instance.config.projectId
|
|
215
215
|
const clientFactory = state.get().options.clientFactory
|
|
216
216
|
checkForCookieAuth(projectIdValue, clientFactory).then((isCookieAuthEnabled) => {
|
|
217
217
|
if (!isCookieAuthEnabled) return
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {type ClientConfig, type SanityClient} from '@sanity/client'
|
|
2
2
|
|
|
3
|
+
import {REQUEST_TAG_PREFIX} from './authConstants'
|
|
3
4
|
import {getTokenFromStorage} from './utils'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -25,7 +26,7 @@ export async function checkForCookieAuth(
|
|
|
25
26
|
const client = clientFactory({
|
|
26
27
|
projectId,
|
|
27
28
|
useCdn: false,
|
|
28
|
-
requestTagPrefix:
|
|
29
|
+
requestTagPrefix: REQUEST_TAG_PREFIX,
|
|
29
30
|
timeout: COOKIE_AUTH_TIMEOUT_MS,
|
|
30
31
|
})
|
|
31
32
|
const user = await client.request({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {type CurrentUser} from '@sanity/types'
|
|
2
2
|
import {delay, of, throwError} from 'rxjs'
|
|
3
|
-
import {beforeEach, describe, it} from 'vitest'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {createSanityInstance} from '../store/createSanityInstance'
|
|
6
6
|
import {createStoreState} from '../store/createStoreState'
|
|
@@ -8,19 +8,32 @@ import {AuthStateType} from './authStateType'
|
|
|
8
8
|
import {authStore} from './authStore'
|
|
9
9
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
10
10
|
|
|
11
|
+
// Mock logger to prevent actual logging during tests
|
|
12
|
+
vi.mock('../utils/logger', async (importOriginal) => {
|
|
13
|
+
const original = await importOriginal<typeof import('../utils/logger')>()
|
|
14
|
+
return {
|
|
15
|
+
...original,
|
|
16
|
+
createLogger: vi.fn(() => ({
|
|
17
|
+
info: vi.fn(),
|
|
18
|
+
debug: vi.fn(),
|
|
19
|
+
warn: vi.fn(),
|
|
20
|
+
error: vi.fn(),
|
|
21
|
+
trace: vi.fn(),
|
|
22
|
+
})),
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
11
26
|
describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
12
27
|
beforeEach(() => {
|
|
13
28
|
vi.clearAllMocks()
|
|
14
29
|
})
|
|
15
30
|
|
|
16
31
|
it('fetches the current user if the is logged in without a current user present', () => {
|
|
17
|
-
const mockUser = {id: 'example-user'} as CurrentUser
|
|
32
|
+
const mockUser = {id: 'example-user', email: 'user@example.com'} as CurrentUser
|
|
18
33
|
const mockRequest = vi.fn().mockReturnValue(of(mockUser))
|
|
19
34
|
const mockClient = {observable: {request: mockRequest}}
|
|
20
35
|
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
21
|
-
const instance = createSanityInstance({
|
|
22
|
-
auth: {clientFactory},
|
|
23
|
-
})
|
|
36
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
24
37
|
|
|
25
38
|
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
26
39
|
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
@@ -42,7 +55,11 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
42
55
|
useProjectHostname: false,
|
|
43
56
|
useCdn: false,
|
|
44
57
|
})
|
|
45
|
-
expect(mockRequest).toHaveBeenCalledWith({
|
|
58
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
59
|
+
method: 'GET',
|
|
60
|
+
uri: '/users/me',
|
|
61
|
+
tag: 'users.get-current',
|
|
62
|
+
})
|
|
46
63
|
|
|
47
64
|
subscription.unsubscribe()
|
|
48
65
|
})
|
|
@@ -52,9 +69,7 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
52
69
|
const mockRequest = vi.fn().mockReturnValue(of(mockUser).pipe(delay(0)))
|
|
53
70
|
const mockClient = {observable: {request: mockRequest}}
|
|
54
71
|
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
55
|
-
const instance = createSanityInstance({
|
|
56
|
-
auth: {clientFactory},
|
|
57
|
-
})
|
|
72
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
58
73
|
|
|
59
74
|
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
60
75
|
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
@@ -80,7 +95,11 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
80
95
|
useProjectHostname: false,
|
|
81
96
|
useCdn: false,
|
|
82
97
|
})
|
|
83
|
-
expect(mockRequest).toHaveBeenCalledWith({
|
|
98
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
99
|
+
method: 'GET',
|
|
100
|
+
uri: '/users/me',
|
|
101
|
+
tag: 'users.get-current',
|
|
102
|
+
})
|
|
84
103
|
|
|
85
104
|
subscription.unsubscribe()
|
|
86
105
|
})
|
|
@@ -90,9 +109,7 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
90
109
|
const mockRequest = vi.fn().mockReturnValue(throwError(() => error))
|
|
91
110
|
const mockClient = {observable: {request: mockRequest}}
|
|
92
111
|
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
93
|
-
const instance = createSanityInstance({
|
|
94
|
-
auth: {clientFactory},
|
|
95
|
-
})
|
|
112
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
96
113
|
|
|
97
114
|
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
98
115
|
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
@@ -110,4 +127,120 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
110
127
|
|
|
111
128
|
subscription.unsubscribe()
|
|
112
129
|
})
|
|
130
|
+
|
|
131
|
+
it('recovers from a fetch error when a new token is set', () => {
|
|
132
|
+
const error = new Error('Unauthorized')
|
|
133
|
+
const mockUser = {id: 'recovered-user'} as CurrentUser
|
|
134
|
+
const mockRequest = vi
|
|
135
|
+
.fn()
|
|
136
|
+
.mockReturnValueOnce(throwError(() => error))
|
|
137
|
+
.mockReturnValueOnce(of(mockUser))
|
|
138
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
139
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
140
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
141
|
+
|
|
142
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
143
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
144
|
+
|
|
145
|
+
// First token causes a 401 — state should transition to ERROR
|
|
146
|
+
state.set('setLoggedIn', {
|
|
147
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'expired-token', currentUser: null},
|
|
148
|
+
})
|
|
149
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error}})
|
|
150
|
+
|
|
151
|
+
// Simulate comlink providing a fresh token (setAuthToken sets LOGGED_IN with new token)
|
|
152
|
+
state.set('setNewToken', {
|
|
153
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'fresh-token', currentUser: null},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Subscription should still be alive — re-fetches /users/me with the new token
|
|
157
|
+
expect(state.get()).toMatchObject({
|
|
158
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'fresh-token', currentUser: mockUser},
|
|
159
|
+
})
|
|
160
|
+
expect(mockRequest).toHaveBeenCalledTimes(2)
|
|
161
|
+
|
|
162
|
+
subscription.unsubscribe()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('recovers from multiple consecutive fetch errors', () => {
|
|
166
|
+
const error1 = new Error('Unauthorized')
|
|
167
|
+
const error2 = new Error('Unauthorized again')
|
|
168
|
+
const mockUser = {id: 'finally-recovered'} as CurrentUser
|
|
169
|
+
const mockRequest = vi
|
|
170
|
+
.fn()
|
|
171
|
+
.mockReturnValueOnce(throwError(() => error1))
|
|
172
|
+
.mockReturnValueOnce(throwError(() => error2))
|
|
173
|
+
.mockReturnValueOnce(of(mockUser))
|
|
174
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
175
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
176
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
177
|
+
|
|
178
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
179
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
180
|
+
|
|
181
|
+
// First attempt fails
|
|
182
|
+
state.set('setLoggedIn', {
|
|
183
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-1', currentUser: null},
|
|
184
|
+
})
|
|
185
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error: error1}})
|
|
186
|
+
|
|
187
|
+
// Second attempt also fails
|
|
188
|
+
state.set('setNewToken', {
|
|
189
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-2', currentUser: null},
|
|
190
|
+
})
|
|
191
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error: error2}})
|
|
192
|
+
|
|
193
|
+
// Third attempt succeeds
|
|
194
|
+
state.set('setNewToken', {
|
|
195
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-3', currentUser: null},
|
|
196
|
+
})
|
|
197
|
+
expect(state.get()).toMatchObject({
|
|
198
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-3', currentUser: mockUser},
|
|
199
|
+
})
|
|
200
|
+
expect(mockRequest).toHaveBeenCalledTimes(3)
|
|
201
|
+
|
|
202
|
+
subscription.unsubscribe()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('does not re-fetch with the same token but recovers with a different token', () => {
|
|
206
|
+
const error = new Error('Unauthorized')
|
|
207
|
+
const mockUser = {id: 'recovered-user'} as CurrentUser
|
|
208
|
+
const mockRequest = vi
|
|
209
|
+
.fn()
|
|
210
|
+
.mockReturnValueOnce(throwError(() => error))
|
|
211
|
+
.mockReturnValueOnce(of(mockUser))
|
|
212
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
213
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
214
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
215
|
+
|
|
216
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
217
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
218
|
+
|
|
219
|
+
// First attempt fails
|
|
220
|
+
state.set('setLoggedIn', {
|
|
221
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'same-token', currentUser: null},
|
|
222
|
+
})
|
|
223
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error}})
|
|
224
|
+
|
|
225
|
+
// Same token should be blocked by distinctUntilChanged — no re-fetch
|
|
226
|
+
state.set('setNewToken', {
|
|
227
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'same-token', currentUser: null},
|
|
228
|
+
})
|
|
229
|
+
expect(mockRequest).toHaveBeenCalledTimes(1)
|
|
230
|
+
|
|
231
|
+
// A different token should pass distinctUntilChanged and trigger recovery
|
|
232
|
+
state.set('setNewToken', {
|
|
233
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'different-token', currentUser: null},
|
|
234
|
+
})
|
|
235
|
+
expect(state.get()).toMatchObject({
|
|
236
|
+
authState: {
|
|
237
|
+
type: AuthStateType.LOGGED_IN,
|
|
238
|
+
token: 'different-token',
|
|
239
|
+
currentUser: mockUser,
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
expect(mockRequest).toHaveBeenCalledTimes(2)
|
|
243
|
+
|
|
244
|
+
subscription.unsubscribe()
|
|
245
|
+
})
|
|
113
246
|
})
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import {type CurrentUser} from '@sanity/types'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
catchError,
|
|
4
|
+
distinctUntilChanged,
|
|
5
|
+
EMPTY,
|
|
6
|
+
filter,
|
|
7
|
+
map,
|
|
8
|
+
type Subscription,
|
|
9
|
+
switchMap,
|
|
10
|
+
} from 'rxjs'
|
|
3
11
|
|
|
4
12
|
import {type StoreContext} from '../store/defineStore'
|
|
5
13
|
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
|
|
14
|
+
import {getAuthLogger} from './authLogger'
|
|
6
15
|
import {isStudioConfig} from './authMode'
|
|
7
16
|
import {AuthStateType} from './authStateType'
|
|
8
17
|
import {type AuthMethodOptions, type AuthState, type AuthStoreState} from './authStore'
|
|
@@ -22,9 +31,11 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
22
31
|
{state, instance}: StoreContext<AuthStoreState>,
|
|
23
32
|
fetchOptions?: {useProjectHostname?: boolean},
|
|
24
33
|
): Subscription => {
|
|
34
|
+
const logger = getAuthLogger(instance)
|
|
35
|
+
|
|
25
36
|
const {clientFactory, apiHost} = state.get().options
|
|
26
37
|
const useProjectHostname = fetchOptions?.useProjectHostname ?? isStudioConfig(instance.config)
|
|
27
|
-
const projectId = instance.config.
|
|
38
|
+
const projectId = instance.config.projectId
|
|
28
39
|
|
|
29
40
|
const currentUser$ = state.observable
|
|
30
41
|
.pipe(
|
|
@@ -60,12 +71,35 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
60
71
|
}),
|
|
61
72
|
),
|
|
62
73
|
switchMap((client) =>
|
|
63
|
-
client.observable
|
|
74
|
+
client.observable
|
|
75
|
+
.request<CurrentUser>({
|
|
76
|
+
uri: '/users/me',
|
|
77
|
+
method: 'GET',
|
|
78
|
+
tag: 'users.get-current',
|
|
79
|
+
})
|
|
80
|
+
.pipe(
|
|
81
|
+
/**
|
|
82
|
+
* Catch inside switchMap so the outer subscription survives.
|
|
83
|
+
* Without this, a 401 terminates the subscription permanently
|
|
84
|
+
* and subsequent token refreshes via comlink never re-fetch /users/me.
|
|
85
|
+
* @see SDK-1409
|
|
86
|
+
*/
|
|
87
|
+
catchError((error) => {
|
|
88
|
+
logger.error('Failed to fetch current user', {error})
|
|
89
|
+
state.set('setError', {authState: {type: AuthStateType.ERROR, error}})
|
|
90
|
+
return EMPTY
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
64
93
|
),
|
|
65
94
|
)
|
|
66
95
|
|
|
67
96
|
return currentUser$.subscribe({
|
|
68
97
|
next: (currentUser) => {
|
|
98
|
+
logger.info('Current user fetched successfully', {
|
|
99
|
+
hasEmail: !!currentUser.email,
|
|
100
|
+
})
|
|
101
|
+
// userId is PII, so only surface it at debug level
|
|
102
|
+
logger.debug('Current user details', {userId: currentUser.id})
|
|
69
103
|
state.set('setCurrentUser', (prev) => ({
|
|
70
104
|
authState:
|
|
71
105
|
prev.authState.type === AuthStateType.LOGGED_IN
|
|
@@ -73,8 +107,5 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
73
107
|
: prev.authState,
|
|
74
108
|
}))
|
|
75
109
|
},
|
|
76
|
-
error: (error) => {
|
|
77
|
-
state.set('setError', {authState: {type: AuthStateType.ERROR, error}})
|
|
78
|
-
},
|
|
79
110
|
})
|
|
80
111
|
}
|