@sanity/sdk 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -2
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/_exports/index.ts +1 -0
- package/src/preview/previewQuery.test.ts +3 -3
- package/src/preview/previewQuery.ts +2 -2
- package/src/preview/previewStore.ts +1 -1
- package/src/projection/projectionQuery.test.ts +6 -6
- package/src/projection/projectionQuery.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +2 -2
- package/src/releases/getPerspectiveState.test.ts +2 -1
- package/src/releases/getPerspectiveState.ts +2 -1
- package/src/releases/releasesStore.test.ts +2 -2
- package/src/releases/releasesStore.ts +12 -2
- package/src/auth/Auth Guide.md +0 -165
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"browserslist": "extends @sanity/browserslist-config",
|
|
43
43
|
"prettier": "@sanity/prettier-config",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@sanity/client": "^7.
|
|
46
|
-
"@sanity/comlink": "^3.0.
|
|
45
|
+
"@sanity/client": "^7.2.1",
|
|
46
|
+
"@sanity/comlink": "^3.0.4",
|
|
47
47
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
48
48
|
"@sanity/message-protocol": "^0.12.0",
|
|
49
49
|
"@sanity/mutate": "^0.12.4",
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"vite": "^6.3.4",
|
|
69
69
|
"vitest": "^3.1.2",
|
|
70
70
|
"@repo/config-eslint": "0.0.0",
|
|
71
|
+
"@repo/config-test": "0.0.1",
|
|
71
72
|
"@repo/package.bundle": "3.82.0",
|
|
72
73
|
"@repo/package.config": "0.0.1",
|
|
73
|
-
"@repo/config-test": "0.0.1",
|
|
74
74
|
"@repo/tsconfig": "0.0.1"
|
|
75
75
|
},
|
|
76
76
|
"engines": {
|
package/src/_exports/index.ts
CHANGED
|
@@ -80,7 +80,7 @@ describe('processPreviewQuery', () => {
|
|
|
80
80
|
expect(val?.data).toEqual({
|
|
81
81
|
title: 'John',
|
|
82
82
|
media: null,
|
|
83
|
-
|
|
83
|
+
_status: {lastEditedPublishedAt: '2021-01-01'},
|
|
84
84
|
})
|
|
85
85
|
expect(val?.isPending).toBe(false)
|
|
86
86
|
})
|
|
@@ -126,7 +126,7 @@ describe('processPreviewQuery', () => {
|
|
|
126
126
|
expect(val?.data).toEqual({
|
|
127
127
|
title: 'Draft Title',
|
|
128
128
|
media: null,
|
|
129
|
-
|
|
129
|
+
_status: {
|
|
130
130
|
lastEditedDraftAt: '2023-12-16T12:00:00Z',
|
|
131
131
|
lastEditedPublishedAt: '2023-12-15T12:00:00Z',
|
|
132
132
|
},
|
|
@@ -176,7 +176,7 @@ describe('processPreviewQuery', () => {
|
|
|
176
176
|
title: titleCandidates[TITLE_CANDIDATES[0] as keyof typeof titleCandidates],
|
|
177
177
|
subtitle: subtitleCandidates[SUBTITLE_CANDIDATES[0] as keyof typeof subtitleCandidates],
|
|
178
178
|
media: null,
|
|
179
|
-
|
|
179
|
+
_status: {lastEditedPublishedAt: '2023-12-15T12:00:00Z'},
|
|
180
180
|
})
|
|
181
181
|
expect(val?.isPending).toBe(false)
|
|
182
182
|
})
|
|
@@ -118,12 +118,12 @@ export function processPreviewQuery({
|
|
|
118
118
|
media: normalizeMedia(result.media, projectId, dataset),
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
const
|
|
121
|
+
const _status: PreviewValue['_status'] = {
|
|
122
122
|
...(draftResult?._updatedAt && {lastEditedDraftAt: draftResult._updatedAt}),
|
|
123
123
|
...(publishedResult?._updatedAt && {lastEditedPublishedAt: publishedResult._updatedAt}),
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
return [id, {data: {...preview,
|
|
126
|
+
return [id, {data: {...preview, _status}, isPending: false}]
|
|
127
127
|
} catch (e) {
|
|
128
128
|
// TODO: replace this with bubbling the error
|
|
129
129
|
// eslint-disable-next-line no-console
|
|
@@ -96,7 +96,7 @@ describe('processProjectionQuery', () => {
|
|
|
96
96
|
data: {
|
|
97
97
|
title: 'Hello',
|
|
98
98
|
description: 'World',
|
|
99
|
-
|
|
99
|
+
_status: {
|
|
100
100
|
lastEditedPublishedAt: '2021-01-01',
|
|
101
101
|
},
|
|
102
102
|
},
|
|
@@ -127,7 +127,7 @@ describe('processProjectionQuery', () => {
|
|
|
127
127
|
data: {
|
|
128
128
|
title: 'Hello',
|
|
129
129
|
description: 'World',
|
|
130
|
-
|
|
130
|
+
_status: {
|
|
131
131
|
lastEditedPublishedAt: '2021-01-01',
|
|
132
132
|
},
|
|
133
133
|
},
|
|
@@ -163,7 +163,7 @@ describe('processProjectionQuery', () => {
|
|
|
163
163
|
expect(processed['doc1']?.[testProjectionHash]).toEqual({
|
|
164
164
|
data: {
|
|
165
165
|
title: 'Draft',
|
|
166
|
-
|
|
166
|
+
_status: {
|
|
167
167
|
lastEditedDraftAt: '2021-01-02',
|
|
168
168
|
lastEditedPublishedAt: '2021-01-01',
|
|
169
169
|
},
|
|
@@ -193,7 +193,7 @@ describe('processProjectionQuery', () => {
|
|
|
193
193
|
expect(processed['doc1']?.[testProjectionHash]).toEqual({
|
|
194
194
|
data: {
|
|
195
195
|
title: 'Published',
|
|
196
|
-
|
|
196
|
+
_status: {
|
|
197
197
|
lastEditedPublishedAt: '2021-01-01',
|
|
198
198
|
},
|
|
199
199
|
},
|
|
@@ -231,7 +231,7 @@ describe('processProjectionQuery', () => {
|
|
|
231
231
|
expect(processed['doc1']?.[hash1]).toEqual({
|
|
232
232
|
data: {
|
|
233
233
|
title: 'Published Title',
|
|
234
|
-
|
|
234
|
+
_status: {
|
|
235
235
|
lastEditedPublishedAt: '2021-01-01',
|
|
236
236
|
},
|
|
237
237
|
},
|
|
@@ -240,7 +240,7 @@ describe('processProjectionQuery', () => {
|
|
|
240
240
|
expect(processed['doc1']?.[hash2]).toEqual({
|
|
241
241
|
data: {
|
|
242
242
|
description: 'Published Desc',
|
|
243
|
-
|
|
243
|
+
_status: {
|
|
244
244
|
lastEditedPublishedAt: '2021-01-01',
|
|
245
245
|
},
|
|
246
246
|
},
|
|
@@ -123,13 +123,13 @@ export function processProjectionQuery({ids, results}: ProcessProjectionQueryOpt
|
|
|
123
123
|
continue
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const
|
|
126
|
+
const _status = {
|
|
127
127
|
...(draft?._updatedAt && {lastEditedDraftAt: draft._updatedAt}),
|
|
128
128
|
...(published?._updatedAt && {lastEditedPublishedAt: published._updatedAt}),
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
finalValues[originalId][hash] = {
|
|
132
|
-
data: {...projectionResultData,
|
|
132
|
+
data: {...projectionResultData, _status},
|
|
133
133
|
isPending: false,
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -141,7 +141,7 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
141
141
|
isPending: false,
|
|
142
142
|
data: {
|
|
143
143
|
title: 'resolved',
|
|
144
|
-
|
|
144
|
+
_status: {
|
|
145
145
|
lastEditedDraftAt: timestamp,
|
|
146
146
|
lastEditedPublishedAt: timestamp,
|
|
147
147
|
},
|
|
@@ -299,7 +299,7 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
299
299
|
data: expect.objectContaining({
|
|
300
300
|
title: 'Test Document',
|
|
301
301
|
description: 'Test Description',
|
|
302
|
-
|
|
302
|
+
_status: {
|
|
303
303
|
lastEditedPublishedAt: timestamp,
|
|
304
304
|
},
|
|
305
305
|
}),
|
|
@@ -38,7 +38,8 @@ describe('getPerspectiveState', () => {
|
|
|
38
38
|
metadata: {title: 'Release 2', releaseType: 'asap'},
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// the release store is reversed in getActiveReleases to match UI elsewhere
|
|
42
|
+
const activeReleases = [release2, release1]
|
|
42
43
|
|
|
43
44
|
beforeEach(() => {
|
|
44
45
|
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
@@ -4,6 +4,7 @@ import {type PerspectiveHandle, type ReleasePerspective} from '../config/sanityC
|
|
|
4
4
|
import {bindActionByDataset} from '../store/createActionBinder'
|
|
5
5
|
import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
|
|
6
6
|
import {releasesStore, type ReleasesStoreState} from './releasesStore'
|
|
7
|
+
import {sortReleases} from './utils/sortReleases'
|
|
7
8
|
|
|
8
9
|
function isReleasePerspective(
|
|
9
10
|
perspective: PerspectiveHandle['perspective'],
|
|
@@ -75,7 +76,7 @@ export const getPerspectiveState = bindActionByDataset(
|
|
|
75
76
|
// if there are no active releases we can't compute the release perspective
|
|
76
77
|
if (!activeReleases || activeReleases.length === 0) return undefined
|
|
77
78
|
|
|
78
|
-
const releaseNames = activeReleases.map((release) => release.name)
|
|
79
|
+
const releaseNames = sortReleases(activeReleases).map((release) => release.name)
|
|
79
80
|
const index = releaseNames.findIndex((name) => name === perspective.releaseName)
|
|
80
81
|
|
|
81
82
|
if (index < 0) {
|
|
@@ -61,7 +61,7 @@ describe('releasesStore', () => {
|
|
|
61
61
|
|
|
62
62
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
63
63
|
|
|
64
|
-
expect(state.getCurrent()).toEqual(mockReleases)
|
|
64
|
+
expect(state.getCurrent()).toEqual(mockReleases.reverse())
|
|
65
65
|
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
|
66
66
|
})
|
|
67
67
|
|
|
@@ -105,7 +105,7 @@ describe('releasesStore', () => {
|
|
|
105
105
|
releasesSubject.next(updatedReleases)
|
|
106
106
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
107
107
|
|
|
108
|
-
expect(state.getCurrent()).toEqual(updatedReleases)
|
|
108
|
+
expect(state.getCurrent()).toEqual(updatedReleases.reverse())
|
|
109
109
|
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
|
110
110
|
})
|
|
111
111
|
|
|
@@ -9,6 +9,8 @@ import {defineStore, type StoreContext} from '../store/defineStore'
|
|
|
9
9
|
import {listenQuery} from '../utils/listenQuery'
|
|
10
10
|
import {sortReleases} from './utils/sortReleases'
|
|
11
11
|
|
|
12
|
+
const ARCHIVED_RELEASE_STATES = ['archived', 'published']
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Represents a document in a Sanity dataset that represents release options.
|
|
14
16
|
* @internal
|
|
@@ -16,10 +18,12 @@ import {sortReleases} from './utils/sortReleases'
|
|
|
16
18
|
export type ReleaseDocument = SanityDocument & {
|
|
17
19
|
name: string
|
|
18
20
|
publishAt?: string
|
|
21
|
+
state: 'active' | 'scheduled'
|
|
19
22
|
metadata: {
|
|
20
23
|
title: string
|
|
21
24
|
releaseType: 'asap' | 'scheduled' | 'undecided'
|
|
22
25
|
intendedPublishAt?: string
|
|
26
|
+
description?: string
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -50,7 +54,7 @@ export const getActiveReleasesState = bindActionByDataset(
|
|
|
50
54
|
}),
|
|
51
55
|
)
|
|
52
56
|
|
|
53
|
-
const RELEASES_QUERY = 'releases::all()
|
|
57
|
+
const RELEASES_QUERY = 'releases::all()'
|
|
54
58
|
const QUERY_PARAMS = {}
|
|
55
59
|
|
|
56
60
|
const subscribeToReleases = ({instance, state}: StoreContext<ReleasesStoreState>) => {
|
|
@@ -83,7 +87,13 @@ const subscribeToReleases = ({instance, state}: StoreContext<ReleasesStoreState>
|
|
|
83
87
|
)
|
|
84
88
|
.subscribe({
|
|
85
89
|
next: (releases) => {
|
|
86
|
-
|
|
90
|
+
// logic here mirrors that of studio:
|
|
91
|
+
// https://github.com/sanity-io/sanity/blob/156e8fa482703d99219f08da7bacb384517f1513/packages/sanity/src/core/releases/store/useActiveReleases.ts#L29
|
|
92
|
+
state.set('setActiveReleases', {
|
|
93
|
+
activeReleases: sortReleases(releases ?? [])
|
|
94
|
+
.filter((release) => !ARCHIVED_RELEASE_STATES.includes(release.state))
|
|
95
|
+
.reverse(),
|
|
96
|
+
})
|
|
87
97
|
},
|
|
88
98
|
})
|
|
89
99
|
}
|
package/src/auth/Auth Guide.md
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
# SDK Authentication Guide
|
|
2
|
-
|
|
3
|
-
This document outlines the various authentication mechanisms supported by the Sanity SDK, catering to different usage contexts like embedded dashboard apps, Studio integrations, and standalone applications.
|
|
4
|
-
|
|
5
|
-
## ✨ High-Level Overview
|
|
6
|
-
|
|
7
|
-
Authentication in the SDK is primarily managed by the `authStore`, which tracks the user's authentication state (`LoggedIn`, `LoggedOut`, `LoggingIn`, `Error`). It determines the initial state based on the environment (Dashboard iframe, Studio mode, presence of a provided token, or auth callback URL parameters).
|
|
8
|
-
|
|
9
|
-
API client instances, managed by `clientStore`, automatically utilize the current authentication token from `authStore` for making requests. The `clientStore` also handles differentiating between clients configured for 'global' endpoints (like `api.sanity.io`) and 'default' (project-specific) endpoints (like `<projectId>.api.sanity.io`).
|
|
10
|
-
|
|
11
|
-
The primary interactive authentication flow involves redirecting the user to `sanity.io/login` (or `sanity.work/login` for staging environments) and handling the callback, which returns an `authCode` (`sid`) exchanged for a session token.
|
|
12
|
-
|
|
13
|
-
### Authentication Channels Overview
|
|
14
|
-
|
|
15
|
-
- **Dashboard (Primary):** Relies on the host environment providing
|
|
16
|
-
authentication context (`sid`) via URL parameters. Seamless for the end-user.
|
|
17
|
-
This mechanism applies to both third-party Sanity Apps and internal Sanity
|
|
18
|
-
applications (e.g., Canvas, Media Library).
|
|
19
|
-
|
|
20
|
-
- **Studio Mode:** Leverages Studio's own auth context (token or cookie)
|
|
21
|
-
when the SDK is used within the Studio application.
|
|
22
|
-
|
|
23
|
-
- **Standalone:** Supports manually provided tokens (secure for backends,
|
|
24
|
-
requires care for frontends). The built-in web login flow (`sanity.io/login`) has
|
|
25
|
-
significant limitations for apps on custom domains due to origin restrictions.
|
|
26
|
-
|
|
27
|
-
## 🚦 Authentication Channels
|
|
28
|
-
|
|
29
|
-
### 1. Dashboard Apps (Running inside Sanity Dashboard iframe)
|
|
30
|
-
|
|
31
|
-
- 🎯 **Use Case:** This is the **primary intended use case** for the SDK — that is, applications built to run embedded within an `iframe` in the Sanity Dashboard environment. This includes both customer-built Sanity Apps and internally developed applications like Canvas and Media Library.
|
|
32
|
-
|
|
33
|
-
- ⚙️ **Mechanism:**
|
|
34
|
-
|
|
35
|
-
- **Host-Provided Auth:** Authentication is managed **by the host Dashboard environment**. The Dashboard loads the app's iframe with specific URL parameters. The SDK's `getAuthCode` function looks for the session identifier (`sid`) in the following order: first in the **URL hash** (`#sid=...`), then in the **URL search parameters** (`?sid=...`), and finally as a fallback within the `_context` query parameter (`?_context={"sid":"..."}`). The `_context` parameter is a URL-encoded JSON object that may also contain other context (`orgId`, `mode`, etc.). It looks for these in these places because a custom built studio-like authentication will use the query parameter in a callback, the Dashboard will use a hash in the iframe URL, and the Dashboard will use the `_context` parameter which can also contain the `sid`. Additionally, the SDK will look for a `token` hash in the URL which can be used to pass a token into the App.
|
|
36
|
-
|
|
37
|
-
- **Automatic Handling:** On load, the SDK's `authStore` and `handleAuthCallback` detect the `sid` using the logic described above. If an `sid` (auth code) is present, `handleAuthCallback` automatically attempts to exchange it for an authentication token without user interaction or redirects.
|
|
38
|
-
|
|
39
|
-
- **No SDK Login Flow:** The SDK's standard login flow (redirecting to `sanity.io/login`) is **not used** in this context. The user is expected to be already authenticated within the host Dashboard.
|
|
40
|
-
|
|
41
|
-
- **Communication:** The initial authentication relies on the URL parameters from the host Dashboard.
|
|
42
|
-
|
|
43
|
-
- **Client Configuration:** API clients operate using the token obtained from the `sid` exchange, effectively using the user's active Dashboard session. The token will be a global, stamped token that is refreshed every 12 hours.
|
|
44
|
-
|
|
45
|
-
- 🚧 **Limitations:** Tightly coupled to the Sanity Dashboard environment. Requires the app to be loaded within the dashboard iframe by the host.
|
|
46
|
-
|
|
47
|
-
- 🛠️ **Technical Details:**
|
|
48
|
-
|
|
49
|
-
- `authStore.ts` and `handleAuthCallback` parse `initialLocationHref` for `_context` and `sid` parameters.
|
|
50
|
-
|
|
51
|
-
- `handleAuthCallback` triggers the `sid`-for-token exchange via `/auth/fetch`.
|
|
52
|
-
|
|
53
|
-
- `AuthBoundary.tsx` detects iframe context (`isInIframe()`) and prevents the SDK's own redirect-based login flow when embedded.
|
|
54
|
-
|
|
55
|
-
### 2. Sanity Studio Mode (👷 Under construction)
|
|
56
|
-
|
|
57
|
-
- 🎯 **Use Case:** Using the SDK _within_ the Sanity Studio V3 codebase itself (not running in the dashboard iframe). For instance: in custom input components, tools, or plugins integrated directly into the Studio application.
|
|
58
|
-
|
|
59
|
-
- ⚙️ **Mechanism:** Enabled by setting `studioMode.enabled: true` in the SDK configuration.
|
|
60
|
-
|
|
61
|
-
- **Studio Token (localStorage):** The primary method. `authStore` looks for a token specific to the Studio session stored in `localStorage` under the key `__sanity_auth_token_${projectId}`. This token is project-specific.
|
|
62
|
-
|
|
63
|
-
- **Studio Cookie Auth:** As a fallback, if the `localStorage` token is not found, `checkForCookieAuth` is called. This function attempts a request (`withCredentials: true`) to a Studio backend endpoint to verify if a valid HTTP-only session cookie exists. If so, subsequent API requests managed by the SDK client will rely on this cookie for authentication.
|
|
64
|
-
|
|
65
|
-
- 🚧 **Limitations:**
|
|
66
|
-
|
|
67
|
-
- Provides only project-level access (cannot use global tokens/endpoints). Calls to global endpoints will fail.
|
|
68
|
-
|
|
69
|
-
- Relies entirely on the authentication context established by the Studio itself.
|
|
70
|
-
|
|
71
|
-
- 🛠️ **Technical Details:**
|
|
72
|
-
|
|
73
|
-
- `authStore` contains specific logic gated by `instance.config.studioMode?.enabled`.
|
|
74
|
-
|
|
75
|
-
- `getStudioTokenFromLocalStorage` retrieves the project-specific token.
|
|
76
|
-
|
|
77
|
-
- `checkForCookieAuth` initiates the cookie check flow.
|
|
78
|
-
|
|
79
|
-
- `clientStore` will configure clients based on the available token or implicitly rely on cookies if `withCredentials` is set appropriately. Clients are only configured for the project-specific endpoint.
|
|
80
|
-
|
|
81
|
-
### 3. Standalone Applications
|
|
82
|
-
|
|
83
|
-
- 🎯 **Use Case:** External web applications using the SDK that operate independently of the Sanity Dashboard or Studio (e.g., SDK Explorer, custom internal dashboards). These applications are **not** running inside the Dashboard iframe or as part of the Studio build and they are not hosted on Sanity's domains.
|
|
84
|
-
|
|
85
|
-
- ⚙️ **Mechanism:**
|
|
86
|
-
|
|
87
|
-
- **No Auth:** For accessing only public datasets, `ResourceProvider` can be used without any specific auth configuration. Clients will operate without authentication.
|
|
88
|
-
|
|
89
|
-
- **Provided Token:** Developers can manually provide a `token` (either a PAT or a project-scoped token) within the `SanityConfig` when initializing the SDK instance (`createSanityInstance` or via `ResourceProvider`). `authStore` detects and uses this `providedToken`.
|
|
90
|
-
|
|
91
|
-
- **Important:** The automatic token refresh mechanism is designed for _stamped_ tokens (those containing `-st`, obtained via the interactive login flow). It will **not** attempt to refresh standard Personal Access Tokens (PATs) or other manually provided, non-stamped tokens. Applications using `providedToken` must be prepared for the token to be refreshed by the SDK if a stamped token is passed in.
|
|
92
|
-
|
|
93
|
-
- **Auth Code Flow (Limited Use):** The standard redirect flow (`sanity.io/login` -> callback -> `handleAuthCallback`) can be technically implemented using components like `LoginCallback.tsx` and `AuthBoundary.tsx`. This flow is primarily relevant **only** for this standalone context. However, the `sanity.io/login` endpoint has a strict allowlist for the `origin` parameter. While `localhost` and Sanity domains (`*.sanity.dev`, `*.sanity.work`, `*.sanity.io`) are typically allowed, deploying a standalone app to an arbitrary domain (e.g., `myapp.vercel.app`) will fail this origin check, preventing the flow from completing. However during development of a standalone app, the origin check will pass because localhost is allowed, so the developer can be mistaken to think that a standalone app will continue to work once deployed.
|
|
94
|
-
|
|
95
|
-
- 🚧 **Limitations:**
|
|
96
|
-
|
|
97
|
-
- No straightforward, built-in authentication flow for web applications deployed to arbitrary domains due to the `sanity.io/login` origin restrictions.
|
|
98
|
-
|
|
99
|
-
- Frontend applications using `providedToken` need careful consideration regarding token type (project or global and stamped or non-stamped) and security.
|
|
100
|
-
|
|
101
|
-
- 🛠️ **Technical Details:**
|
|
102
|
-
|
|
103
|
-
- `authStore` checks `instance.config.auth.token` for a `providedToken`.
|
|
104
|
-
|
|
105
|
-
- `handleAuthCallback` implements the exchange of the `authCode` (`sid`) from the callback URL for a token by calling the `/auth/fetch` endpoint.
|
|
106
|
-
|
|
107
|
-
- The `origin` restriction on `sanity.io/login` is the main blocker for a universal web auth solution.
|
|
108
|
-
|
|
109
|
-
## 🔑 Key Concepts & Technical Details
|
|
110
|
-
|
|
111
|
-
- **Tokens:**
|
|
112
|
-
|
|
113
|
-
- **Types:**
|
|
114
|
-
|
|
115
|
-
- **Global Tokens:** Tokens not tied to a specific project, but instead to a Sanity User. It includes access to all of the user's orgs and projects. Required for accessing global Sanity APIs (e.g., project management). Used when `clientStore` configures a client with `scope: 'global'` or without a `projectId`.
|
|
116
|
-
|
|
117
|
-
- **Project Tokens:** Scoped to a single project (any of that project's datasets). Used by Studio integration or can be provided manually. Can only be used for project-specific endpoints (`<projectId>.api.sanity.io`).
|
|
118
|
-
|
|
119
|
-
- **Stamped Tokens:** Tokens obtained via the `sanity.io/login` flow and from the Dashboard are "stamped" (`type=stampedToken`). Refresh is handled via `refreshStampedToken.ts`. Non-stamped tokens will not be refreshed by the SDK.
|
|
120
|
-
|
|
121
|
-
- **Storage:** Primarily `localStorage` (`storageKey` in `authStore` defaults to `__sanity_auth_token`, or `__sanity_auth_token_${projectId}` in Studio mode).
|
|
122
|
-
|
|
123
|
-
- **Dashboard Context:** When running within the **Dashboard iframe context (section 1 above)**, the SDK does **not** store the obtained token in its own `localStorage`. Authentication relies on the initial `sid` exchange and potential host-managed sessions. The resulting token will be refreshed by the SDK's internal refresh mechanism.
|
|
124
|
-
|
|
125
|
-
- **Login Flow (`sanity.io/login`)**
|
|
126
|
-
|
|
127
|
-
- Constructed in `authStore.ts`.
|
|
128
|
-
|
|
129
|
-
- Requires specific parameters: `origin` (must be allowlisted), `type=stampedToken`, `withSid=true`.
|
|
130
|
-
|
|
131
|
-
- Redirects back to the specified `callbackUrl` (or inferred location) with an `authCode` (as `sid` parameter, potentially others like `_context`).
|
|
132
|
-
|
|
133
|
-
- `handleAuthCallback.ts` extracts the `authCode` and exchanges it for a `{token}` using the `/auth/fetch?sid=<authCode>` endpoint.
|
|
134
|
-
|
|
135
|
-
- **API Clients (`clientStore`)**
|
|
136
|
-
|
|
137
|
-
- Creates and caches `SanityClient` instances based on configuration.
|
|
138
|
-
|
|
139
|
-
- Listens to `getTokenState` changes (`listenToToken`). When the token updates, it clears the client cache (`clients: {}`), forcing regeneration of clients with the new token upon next request.
|
|
140
|
-
|
|
141
|
-
- **Client Scope:**
|
|
142
|
-
|
|
143
|
-
- `scope: 'global'` or missing `projectId` results in a client configured with `useProjectHostname: false`, targeting global APIs (e.g., `api.sanity.io`). Requires a global token.
|
|
144
|
-
|
|
145
|
-
- Default behavior (with `projectId` and `dataset`) uses project-specific hostnames (e.g., `<projectId>.api.sanity.io`).
|
|
146
|
-
|
|
147
|
-
- **CORS & Endpoints:**
|
|
148
|
-
|
|
149
|
-
- **Global Endpoints (e.g., `api.sanity.io`):** Generally have stricter Cross-Origin Resource Sharing (CORS) policies. They primarily allow requests from Sanity's own domains (`*.sanity.io`, `*.sanity.work`), associated development domains (`*.sanity.dev`), and `localhost`. Accessing these from arbitrary external domains is usually blocked by browser CORS rules. They also require global tokens.
|
|
150
|
-
|
|
151
|
-
- **Project Endpoints (e.g., `<projectId>.api.sanity.io`):** CORS rules are configured per-project in `manage.sanity.io`. You can add specific origins (like your deployed application's domain) to the allowlist. These endpoints work with project-specific tokens (or cookies in Studio mode).
|
|
152
|
-
|
|
153
|
-
- **Token Refresh**
|
|
154
|
-
|
|
155
|
-
- Runs periodically **only** for stamped tokens identified by containing a `-st` suffix.
|
|
156
|
-
|
|
157
|
-
- Calls the `/auth/refresh-token` endpoint using the current stamped token to extend the token's validity or get a new one based on the active session, preventing the user from being logged out unexpectedly in long-lived browser sessions.
|
|
158
|
-
|
|
159
|
-
- Uses the Web Locks API (`navigator.locks`) for coordination when running
|
|
160
|
-
outside the dashboard context to prevent multiple tabs from attempting to
|
|
161
|
-
refresh simultaneously. Falls back to uncoordinated refresh if Locks API is
|
|
162
|
-
unavailable.
|
|
163
|
-
|
|
164
|
-
- When running inside the dashboard context, it uses a simpler timer mechanism
|
|
165
|
-
as coordination is not needed because each tab/host will have its own `sid` and therefore its own stamped token.
|