@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "1.0.0",
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.0.0",
46
- "@sanity/comlink": "^3.0.2",
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": {
@@ -47,6 +47,7 @@ export {
47
47
  type DocumentTypeHandle,
48
48
  type PerspectiveHandle,
49
49
  type ProjectHandle,
50
+ type ReleasePerspective,
50
51
  type SanityConfig,
51
52
  } from '../config/sanityConfig'
52
53
  export {getDatasetsState, resolveDatasets} from '../datasets/datasets'
@@ -80,7 +80,7 @@ describe('processPreviewQuery', () => {
80
80
  expect(val?.data).toEqual({
81
81
  title: 'John',
82
82
  media: null,
83
- status: {lastEditedPublishedAt: '2021-01-01'},
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
- status: {
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
- status: {lastEditedPublishedAt: '2023-12-15T12:00:00Z'},
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 status: PreviewValue['status'] = {
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, status}, isPending: false}]
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
@@ -48,7 +48,7 @@ export interface PreviewValue {
48
48
  /**
49
49
  * The status of the document.
50
50
  */
51
- status?: {
51
+ _status?: {
52
52
  /** The date of the last published edit */
53
53
  lastEditedPublishedAt?: string
54
54
  /** The date of the last draft edit */
@@ -96,7 +96,7 @@ describe('processProjectionQuery', () => {
96
96
  data: {
97
97
  title: 'Hello',
98
98
  description: 'World',
99
- status: {
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
- status: {
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
- status: {
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
- status: {
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
- status: {
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
- status: {
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 status = {
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, status},
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
- status: {
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
- status: {
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
- const activeReleases = [release1, release2]
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()[state == "active"]'
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
- state.set('setActiveReleases', {activeReleases: sortReleases(releases ?? [])})
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
  }
@@ -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.