@sanity/sdk 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/_exports/_internal.d.ts +25 -3
  2. package/dist/_exports/_internal.d.ts.map +1 -1
  3. package/dist/_exports/_internal.js +127 -9
  4. package/dist/_exports/_internal.js.map +1 -1
  5. package/dist/_exports/agent.d.ts +1 -1
  6. package/dist/_exports/agent.js +1 -1
  7. package/dist/_exports/comlink.d.ts +1 -1
  8. package/dist/_exports/comlink.js +2 -3
  9. package/dist/_exports/comlink.js.map +1 -1
  10. package/dist/{authStore-C0o1NOxW.d.ts → authStore-C-OEkGaM.d.ts} +2 -2
  11. package/dist/{authStore-C0o1NOxW.d.ts.map → authStore-C-OEkGaM.d.ts.map} +1 -1
  12. package/dist/{authStore-CtE9bLHW.js → clientStore-gUALQ0qW.js} +269 -11
  13. package/dist/{authStore-CtE9bLHW.js.map → clientStore-gUALQ0qW.js.map} +1 -1
  14. package/dist/{createSanityInstance-DUI1bGIg.d.ts → createSanityInstance-DCePpBYW.d.ts} +2 -2
  15. package/dist/{createSanityInstance-DUI1bGIg.d.ts.map → createSanityInstance-DCePpBYW.d.ts.map} +1 -1
  16. package/dist/{getNodeState-CR3__B2D.js → getNodeState-E0hjqFOo.js} +2 -3
  17. package/dist/{getNodeState-CR3__B2D.js.map → getNodeState-E0hjqFOo.js.map} +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +40 -44
  20. package/dist/index.js.map +1 -1
  21. package/dist/{createStateSourceAction-BOKtqFNm.js → object-BwynGPRZ.js} +87 -3
  22. package/dist/object-BwynGPRZ.js.map +1 -0
  23. package/dist/{queryStore-C7yMHPUI.js → version-C90rNWaX.js} +47 -15
  24. package/dist/version-C90rNWaX.js.map +1 -0
  25. package/package.json +5 -5
  26. package/src/_exports/_internal.ts +1 -0
  27. package/src/access/checkPermissions.test.ts +1 -1
  28. package/src/access/checkPermissions.ts +1 -1
  29. package/src/applications/applications.test.ts +4 -4
  30. package/src/applications/applications.ts +4 -4
  31. package/src/auth/handleAuthCallback.test.ts +2 -2
  32. package/src/auth/handleAuthCallback.ts +1 -1
  33. package/src/auth/logout.test.ts +1 -1
  34. package/src/auth/logout.ts +1 -1
  35. package/src/auth/refreshStampedToken.ts +1 -1
  36. package/src/auth/studioModeAuth.test.ts +1 -1
  37. package/src/auth/studioModeAuth.ts +1 -1
  38. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -2
  39. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
  40. package/src/comments/addonDatasetStore.test.ts +2 -2
  41. package/src/comments/addonDatasetStore.ts +2 -2
  42. package/src/comments/commentActions.ts +6 -5
  43. package/src/comments/commentsStore.ts +4 -4
  44. package/src/document/applyDocumentActions.ts +2 -6
  45. package/src/document/documentStore.concurrency.test.ts +2 -1
  46. package/src/document/documentStore.test.ts +19 -18
  47. package/src/document/documentStore.ts +5 -5
  48. package/src/document/permissions.ts +2 -1
  49. package/src/document/processActions.test.ts +2 -1
  50. package/src/document/processMutations.ts +3 -2
  51. package/src/document/reducers.ts +2 -2
  52. package/src/installations/installations.test.ts +2 -2
  53. package/src/installations/installations.ts +2 -2
  54. package/src/organization/organization.test.ts +2 -2
  55. package/src/organization/organization.ts +1 -1
  56. package/src/organizations/organizations.test.ts +2 -2
  57. package/src/organizations/organizations.ts +1 -1
  58. package/src/presence/presenceStore.test.ts +15 -22
  59. package/src/presence/presenceStore.ts +3 -2
  60. package/src/project/project.test.ts +3 -3
  61. package/src/project/project.ts +1 -1
  62. package/src/projection/getProjectionState.test.ts +6 -4
  63. package/src/projection/getProjectionState.ts +2 -2
  64. package/src/projects/projects.test.ts +2 -2
  65. package/src/projects/projects.ts +1 -1
  66. package/src/query/queryStore.ts +3 -3
  67. package/src/store/createSanityInstance.test.ts +1 -1
  68. package/src/store/createSanityInstance.ts +4 -4
  69. package/src/store/createStoreInstance.test.ts +0 -5
  70. package/src/telemetry/telemetryManager.test.ts +1 -1
  71. package/src/telemetry/telemetryManager.ts +2 -2
  72. package/src/users/usersStore.test.ts +1 -1
  73. package/src/users/usersStore.ts +5 -5
  74. package/src/utils/ids.test.ts +40 -15
  75. package/src/utils/ids.ts +47 -2
  76. package/dist/clientStore-BtE2-q2i.js +0 -90
  77. package/dist/clientStore-BtE2-q2i.js.map +0 -1
  78. package/dist/createStateSourceAction-BOKtqFNm.js.map +0 -1
  79. package/dist/getEnv-D5RGkQLu.js +0 -8
  80. package/dist/getEnv-D5RGkQLu.js.map +0 -1
  81. package/dist/logger-DAtJb5RQ.js +0 -173
  82. package/dist/logger-DAtJb5RQ.js.map +0 -1
  83. package/dist/object-CxXlSVg7.js +0 -83
  84. package/dist/object-CxXlSVg7.js.map +0 -1
  85. package/dist/queryStore-C7yMHPUI.js.map +0 -1
  86. package/dist/rolldown-runtime-vyAXikos.js +0 -9
  87. package/dist/telemetryManager-UIEiMtC0.js +0 -127
  88. package/dist/telemetryManager-UIEiMtC0.js.map +0 -1
  89. package/dist/version-D8RP_Q59.js +0 -9
  90. package/dist/version-D8RP_Q59.js.map +0 -1
@@ -38,6 +38,7 @@ import {
38
38
  import {defineStore, type StoreContext} from '../store/defineStore'
39
39
  import {type SanityUser} from '../users/types'
40
40
  import {getUserState} from '../users/usersStore'
41
+ import {randomId} from '../utils/ids'
41
42
  import {createLogger} from '../utils/logger'
42
43
  import {createBifurTransport} from './bifurTransport'
43
44
  import {startsWithPath} from './paths'
@@ -198,7 +199,7 @@ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
198
199
  // storage (`window.open`, or a same-origin iframe), making two live clients
199
200
  // filter each other out as self. Stale sessions are handled by the
200
201
  // disconnect on unload, with the expiry sweep above as the backstop.
201
- const sessionId = crypto.randomUUID()
202
+ const sessionId = randomId(16)
202
203
 
203
204
  // Dataset resources must use the project hostname so the socket URL is project-specific.
204
205
  // Canvas resources use the global API endpoint via the resource config.
@@ -411,7 +412,7 @@ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
411
412
  subscription.add(
412
413
  globalClient.observable
413
414
  .request<{organizationId: string}>({
414
- uri: `/canvases/${resource.canvasId}`,
415
+ url: `/canvases/${resource.canvasId}`,
415
416
  tag: 'canvases.get',
416
417
  })
417
418
  .subscribe({
@@ -36,7 +36,7 @@ describe('project', () => {
36
36
  const result = await project.resolveState(instance, {projectId: 'a'})
37
37
  expect(result).toEqual(detail)
38
38
  expect(request).toHaveBeenCalledWith({
39
- uri: '/projects/a',
39
+ url: '/projects/a',
40
40
  query: {includeMembers: 'true', includeFeatures: 'true'},
41
41
  tag: 'projects.get',
42
42
  })
@@ -59,7 +59,7 @@ describe('project', () => {
59
59
  })
60
60
 
61
61
  expect(request).toHaveBeenCalledWith({
62
- uri: '/projects/a',
62
+ url: '/projects/a',
63
63
  query: {
64
64
  includeMembers: 'false',
65
65
  includeFeatures: 'false',
@@ -80,7 +80,7 @@ describe('project', () => {
80
80
 
81
81
  await project.resolveState(instance)
82
82
  expect(request).toHaveBeenCalledWith({
83
- uri: '/projects/p',
83
+ url: '/projects/p',
84
84
  query: {includeMembers: 'true', includeFeatures: 'true'},
85
85
  tag: 'projects.get',
86
86
  })
@@ -130,7 +130,7 @@ export const project = defineFetcher<
130
130
  getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
131
131
  switchMap((client) =>
132
132
  client.observable.request<Project<boolean, boolean>>({
133
- uri: `/projects/${resolveProjectId(instance, options)}`,
133
+ url: `/projects/${resolveProjectId(instance, options)}`,
134
134
  query: buildQuery(normalizeProjectOptions(options)),
135
135
  tag: 'projects.get',
136
136
  }),
@@ -4,7 +4,7 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
4
4
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
5
5
  import {type StoreState} from '../store/createStoreState'
6
6
  import {hashString} from '../utils/hashString'
7
- import {insecureRandomId} from '../utils/ids'
7
+ import {randomId} from '../utils/ids'
8
8
  import {getProjectionState} from './getProjectionState'
9
9
  import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
10
10
  import {type ProjectionStoreState} from './types'
@@ -19,7 +19,7 @@ vi.mock('../utils/ids', async (importOriginal) => {
19
19
  // Mock implementation uses the external counter
20
20
  return {
21
21
  ...util,
22
- insecureRandomId: vi.fn(() => {
22
+ randomId: vi.fn(() => {
23
23
  const id = `testSubId_${++mockIdCounter}`
24
24
  return id
25
25
  }),
@@ -39,8 +39,7 @@ describe('getProjectionState', () => {
39
39
  let state: StoreState<ProjectionStoreState & {extra?: unknown}>
40
40
 
41
41
  beforeEach(() => {
42
- mockIdCounter = 0
43
- vi.mocked(insecureRandomId).mockClear()
42
+ vi.mocked(randomId).mockClear()
44
43
 
45
44
  // Capture state
46
45
  vi.mocked(subscribeToStateAndFetchBatches).mockImplementation((context) => {
@@ -49,6 +48,9 @@ describe('getProjectionState', () => {
49
48
  })
50
49
 
51
50
  instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
51
+ // Reset after instance creation: the instanceId also comes from randomId,
52
+ // but the test ids should only count subscription-related calls
53
+ mockIdCounter = 0
52
54
  vi.useFakeTimers() // Enable fake timers for each test
53
55
  })
54
56
 
@@ -10,7 +10,7 @@ import {
10
10
  type StateSource,
11
11
  } from '../store/createStateSourceAction'
12
12
  import {hashString} from '../utils/hashString'
13
- import {insecureRandomId} from '../utils/ids'
13
+ import {randomId} from '../utils/ids'
14
14
  import {omitProperty} from '../utils/object'
15
15
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
16
16
  import {projectionStore} from './projectionStore'
@@ -85,7 +85,7 @@ export const _getProjectionState = bindActionByResourceAndPerspective(
85
85
  },
86
86
  onSubscribe: ({state}, options: ProjectionOptions<string, string, string, string>) => {
87
87
  const {projection, ...docHandle} = options
88
- const subscriptionId = insecureRandomId()
88
+ const subscriptionId = randomId(16)
89
89
  const documentId = getPublishedId(DocumentId(docHandle.documentId))
90
90
  const validProjection = validateProjection(projection)
91
91
  const projectionHash = hashString(validProjection)
@@ -35,7 +35,7 @@ describe('projects', () => {
35
35
  const result = await projects.resolveState(instance)
36
36
  expect(result).toEqual(list)
37
37
  expect(request).toHaveBeenCalledWith({
38
- uri: '/projects',
38
+ url: '/projects',
39
39
  query: {includeMembers: 'false', includeFeatures: 'true', onlyExplicitMembership: 'false'},
40
40
  tag: 'projects.list',
41
41
  })
@@ -58,7 +58,7 @@ describe('projects', () => {
58
58
  })
59
59
 
60
60
  expect(request).toHaveBeenCalledWith({
61
- uri: '/projects',
61
+ url: '/projects',
62
62
  query: {
63
63
  organizationId: 'org123',
64
64
  includeMembers: 'true',
@@ -58,7 +58,7 @@ export const projects = defineFetcher<
58
58
  getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
59
59
  switchMap((client) =>
60
60
  client.observable.request<Project<boolean, boolean>[]>({
61
- uri: '/projects',
61
+ url: '/projects',
62
62
  query: buildQuery(normalizeProjectsOptions(options)),
63
63
  tag: 'projects.list',
64
64
  }),
@@ -36,7 +36,7 @@ import {
36
36
  } from '../store/createStateSourceAction'
37
37
  import {type StoreState} from '../store/createStoreState'
38
38
  import {defineStore, type StoreContext} from '../store/defineStore'
39
- import {insecureRandomId} from '../utils/ids'
39
+ import {randomId} from '../utils/ids'
40
40
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
41
41
  import {
42
42
  QUERY_STATE_CLEAR_DELAY,
@@ -309,7 +309,7 @@ const _getQueryState = bindActionByResource(
309
309
  return queryState?.result
310
310
  },
311
311
  onSubscribe: ({state, instance}, options: QueryOptions) => {
312
- const subscriptionId = insecureRandomId()
312
+ const subscriptionId = randomId(16)
313
313
  const key = getQueryKey(instance, options)
314
314
 
315
315
  state.set('addSubscriber', addSubscriber(key, subscriptionId))
@@ -370,7 +370,7 @@ const _resolveQuery = bindActionByResource(
370
370
  // while a component is suspended leaves a subscriber-less key behind — the
371
371
  // component never commits, so no other subscriber exists — and its stored
372
372
  // error would be rethrown on every future mount without ever refetching.
373
- const subscriptionId = insecureRandomId()
373
+ const subscriptionId = randomId(16)
374
374
  state.set('addSubscriber', addSubscriber(key, subscriptionId))
375
375
 
376
376
  const aborted$ = signal
@@ -6,7 +6,7 @@ import {createSanityInstance} from './createSanityInstance'
6
6
  describe('createSanityInstance', () => {
7
7
  it('should create an instance with a unique instanceId and given config', () => {
8
8
  const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
9
- expect(typeof instance.instanceId).toBe('string')
9
+ expect(instance.instanceId).toMatch(/^[a-zA-Z0-9]{16}$/)
10
10
  expect(instance.config).toEqual({projectId: 'proj1', dataset: 'ds1'})
11
11
  expect(instance.isDisposed()).toBe(false)
12
12
  })
@@ -1,5 +1,5 @@
1
1
  import {type SanityConfig} from '../config/sanityConfig'
2
- import {insecureRandomId} from '../utils/ids'
2
+ import {randomId} from '../utils/ids'
3
3
  import {createLogger, type InstanceContext} from '../utils/logger'
4
4
 
5
5
  /**
@@ -10,7 +10,7 @@ import {createLogger, type InstanceContext} from '../utils/logger'
10
10
  export interface SanityInstance {
11
11
  /**
12
12
  * Unique identifier for this instance
13
- * @remarks Generated using crypto.randomUUID()
13
+ * @remarks Generated as a random 16-character alphanumeric ID
14
14
  */
15
15
  readonly instanceId: string
16
16
 
@@ -47,7 +47,7 @@ export interface SanityInstance {
47
47
  * @public
48
48
  */
49
49
  export function createSanityInstance(config: SanityConfig = {}): SanityInstance {
50
- const instanceId = crypto.randomUUID()
50
+ const instanceId = randomId(16)
51
51
  const disposeListeners = new Map<string, () => void>()
52
52
  const disposed = {current: false}
53
53
 
@@ -99,7 +99,7 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
99
99
  logger.info('Instance disposed')
100
100
  },
101
101
  onDispose: (cb) => {
102
- const listenerId = insecureRandomId()
102
+ const listenerId = randomId(16)
103
103
  disposeListeners.set(listenerId, cb)
104
104
  return () => {
105
105
  disposeListeners.delete(listenerId)
@@ -8,11 +8,6 @@ describe('createStoreInstance', () => {
8
8
  let instance: ReturnType<typeof createSanityInstance>
9
9
 
10
10
  beforeEach(() => {
11
- // Mock crypto for predictable instance IDs
12
- vi.stubGlobal('crypto', {
13
- randomUUID: () => 'test-uuid-1234',
14
- })
15
-
16
11
  instance = createSanityInstance({projectId: 'test', dataset: 'test'})
17
12
  })
18
13
 
@@ -280,7 +280,7 @@ describe('createTelemetryManager', () => {
280
280
  const result = await manager.checkConsent()
281
281
  expect(result).toBe(true)
282
282
  expect(mockClient.request).toHaveBeenCalledWith(
283
- expect.objectContaining({uri: '/intake/telemetry-status'}),
283
+ expect.objectContaining({url: '/intake/telemetry-status'}),
284
284
  )
285
285
  })
286
286
 
@@ -86,7 +86,7 @@ export function createTelemetryManager(options: TelemetryManagerOptions): Teleme
86
86
  try {
87
87
  const client = getClient()
88
88
  const result = await client.request<{status: ConsentStatus}>({
89
- uri: '/intake/telemetry-status',
89
+ url: '/intake/telemetry-status',
90
90
  tag: CONSENT_TAG,
91
91
  })
92
92
  cachedConsent = result
@@ -120,7 +120,7 @@ export function createTelemetryManager(options: TelemetryManagerOptions): Teleme
120
120
  const client = getClient()
121
121
  log.debug('sending event batch', {batchSize: batch.length, environment})
122
122
  return client.request({
123
- uri: '/intake/batch',
123
+ url: '/intake/batch',
124
124
  method: 'POST',
125
125
  body: {projectId, batch: enrichBatch(batch)},
126
126
  tag: BATCH_TAG,
@@ -450,7 +450,7 @@ describe('usersStore', () => {
450
450
  )
451
451
  expect(specificRequest).toHaveBeenCalledWith({
452
452
  method: 'GET',
453
- uri: `/users/${projectUserId}`,
453
+ url: `/users/${projectUserId}`,
454
454
  tag: 'users.get',
455
455
  })
456
456
 
@@ -30,7 +30,7 @@ import {bindActionGlobally} from '../store/createActionBinder'
30
30
  import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
31
31
  import {type StoreState} from '../store/createStoreState'
32
32
  import {defineStore, type StoreContext} from '../store/defineStore'
33
- import {insecureRandomId} from '../utils/ids'
33
+ import {randomId} from '../utils/ids'
34
34
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
35
35
  import {
36
36
  addSubscription,
@@ -130,7 +130,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
130
130
  return client.observable
131
131
  .request<PatchedSanityUserFromClient>({
132
132
  method: 'GET',
133
- uri: `/users/${userId}`,
133
+ url: `/users/${userId}`,
134
134
  tag: 'users.get',
135
135
  })
136
136
  .pipe(
@@ -184,7 +184,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
184
184
  return client.observable
185
185
  .request<SanityUser | SanityUserResponse>({
186
186
  method: 'GET',
187
- uri: `access/${resourceType}/${resourceId}/users/${userId}`,
187
+ url: `access/${resourceType}/${resourceId}/users/${userId}`,
188
188
  tag: 'users.get',
189
189
  })
190
190
  .pipe(
@@ -253,7 +253,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
253
253
  switchMap(([[resource, client], cursor]) =>
254
254
  client.observable.request<SanityUserResponse>({
255
255
  method: 'GET',
256
- uri: `access/${resource.type}/${resource.id}/users`,
256
+ url: `access/${resource.type}/${resource.id}/users`,
257
257
  tag: 'users.list',
258
258
  query: cursor
259
259
  ? {nextCursor: cursor, limit: batchSize.toString()}
@@ -310,7 +310,7 @@ export const getUsersState = bindActionGlobally(
310
310
  },
311
311
  ),
312
312
  onSubscribe: ({instance, state}, options?: GetUsersOptions) => {
313
- const subscriptionId = insecureRandomId()
313
+ const subscriptionId = randomId(16)
314
314
  const key = getUsersKey(instance, options)
315
315
  state.set('addSubscription', addSubscription(subscriptionId, key))
316
316
  return () => {
@@ -1,26 +1,51 @@
1
- import {describe, expect, it} from 'vitest'
1
+ import {afterEach, describe, expect, it, vi} from 'vitest'
2
2
 
3
- import {insecureRandomId} from './ids'
3
+ import {randomId, randomUuid} from './ids'
4
4
 
5
- describe('insecureRandomId', () => {
6
- it('should generate 16-character string', () => {
7
- expect(insecureRandomId()).toHaveLength(16)
5
+ describe('randomId', () => {
6
+ it('should generate an 8-character base62 id by default', () => {
7
+ expect(randomId()).toMatch(/^[a-zA-Z0-9]{8}$/)
8
8
  })
9
9
 
10
- it('should generate hex string', () => {
11
- expect(insecureRandomId()).toMatch(/^[0-9a-f]{16}$/)
10
+ it('should generate an id of the given length', () => {
11
+ expect(randomId(16)).toMatch(/^[a-zA-Z0-9]{16}$/)
12
12
  })
13
13
 
14
14
  it('should generate different ids on each call', () => {
15
- const id1 = insecureRandomId()
16
- const id2 = insecureRandomId()
17
- expect(id1).not.toBe(id2)
15
+ expect(randomId()).not.toBe(randomId())
16
+ })
17
+ })
18
+
19
+ describe('randomUuid', () => {
20
+ const uuidV4Pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
21
+
22
+ afterEach(() => {
23
+ vi.unstubAllGlobals()
24
+ })
25
+
26
+ it('should generate a v4 UUID', () => {
27
+ expect(randomUuid()).toMatch(uuidV4Pattern)
28
+ })
29
+
30
+ it('should use crypto.randomUUID when available', () => {
31
+ vi.stubGlobal('crypto', {randomUUID: () => 'native-uuid'})
32
+ expect(randomUuid()).toBe('native-uuid')
33
+ })
34
+
35
+ it('should generate a v4 UUID when crypto.randomUUID is unavailable (insecure contexts)', () => {
36
+ const originalCrypto = globalThis.crypto
37
+ vi.stubGlobal('crypto', {
38
+ getRandomValues: (array: Uint8Array<ArrayBuffer>) => originalCrypto.getRandomValues(array),
39
+ })
40
+ expect(randomUuid()).toMatch(uuidV4Pattern)
18
41
  })
19
42
 
20
- it('should generate properly formatted strings multiple times', () => {
21
- for (let i = 0; i < 100; i++) {
22
- const id = insecureRandomId()
23
- expect(id).toMatch(/^[0-9a-f]{16}$/)
24
- }
43
+ it('should generate unique ids when crypto.randomUUID is unavailable', () => {
44
+ const originalCrypto = globalThis.crypto
45
+ vi.stubGlobal('crypto', {
46
+ getRandomValues: (array: Uint8Array<ArrayBuffer>) => originalCrypto.getRandomValues(array),
47
+ })
48
+ const ids = new Set(Array.from({length: 100}, () => randomUuid()))
49
+ expect(ids.size).toBe(100)
25
50
  })
26
51
  })
package/src/utils/ids.ts CHANGED
@@ -1,3 +1,48 @@
1
- export function insecureRandomId(): string {
2
- return Array.from({length: 16}, () => Math.floor(Math.random() * 16).toString(16)).join('')
1
+ const ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
2
+
3
+ /**
4
+ * Generates a random ID of the given length from the alphabet `[a-zA-Z0-9]`, the same shape
5
+ * `@sanity/client` and Sanity Studio use for short IDs such as release IDs.
6
+ *
7
+ * Ported from `@sanity/client`'s `generateReleaseId`, which uses rejection sampling to keep
8
+ * the output unbiased. Uses `crypto.getRandomValues`, which unlike `crypto.randomUUID` is
9
+ * also available on insecure origins (plain http).
10
+ *
11
+ * @internal
12
+ */
13
+ export function randomId(length: number = 8): string {
14
+ let id = ''
15
+ while (id.length < length) {
16
+ const bytes = crypto.getRandomValues(new Uint8Array(length - id.length))
17
+ for (const byte of bytes) {
18
+ const index = byte & 63
19
+ if (index < ID_ALPHABET.length) id += ID_ALPHABET.charAt(index)
20
+ }
21
+ }
22
+ return id
23
+ }
24
+
25
+ /**
26
+ * Generates a random v4 UUID.
27
+ *
28
+ * Prefers the native `crypto.randomUUID`, which is only available in secure contexts
29
+ * (https, localhost). Falls back to deriving the UUID from `crypto.getRandomValues`,
30
+ * which has no such restriction, so ID generation also works when an app is served over
31
+ * plain http, e.g. when testing from a phone on a local network.
32
+ *
33
+ * @internal
34
+ */
35
+ export function randomUuid(): string {
36
+ // eslint-disable-next-line no-restricted-properties -- this is the sanctioned wrapper around crypto.randomUUID
37
+ if (typeof crypto.randomUUID === 'function') return crypto.randomUUID()
38
+
39
+ const bytes = crypto.getRandomValues(new Uint8Array(16))
40
+ const hex = Array.from(bytes, (byte, index) => {
41
+ // Set the version (4) and variant (RFC 4122) bits
42
+ if (index === 6) byte = (byte & 0x0f) | 0x40
43
+ if (index === 8) byte = (byte & 0x3f) | 0x80
44
+ return byte.toString(16).padStart(2, '0')
45
+ }).join('')
46
+
47
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
3
48
  }
@@ -1,90 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-vyAXikos.js";
2
- import { a as defineStore, c as isDatasetResource, i as bindActionGlobally, l as isMediaLibraryResource, s as isCanvasResource, t as createStateSourceAction } from "./createStateSourceAction-BOKtqFNm.js";
3
- import { T as getStagingApiHost, l as getTokenState, r as getAuthMethodState } from "./authStore-CtE9bLHW.js";
4
- import { i as pickProperties } from "./object-CxXlSVg7.js";
5
- import { createClient } from "@sanity/client";
6
- var clientStore_exports = /* @__PURE__ */ __exportAll({
7
- getClient: () => getClient,
8
- getClientState: () => getClientState
9
- });
10
- const allowedKeys = Object.keys({
11
- apiHost: null,
12
- useCdn: null,
13
- token: null,
14
- perspective: null,
15
- proxy: null,
16
- withCredentials: null,
17
- timeout: null,
18
- maxRetries: null,
19
- dataset: null,
20
- projectId: null,
21
- scope: null,
22
- apiVersion: null,
23
- requestTagPrefix: null,
24
- useProjectHostname: null,
25
- resource: null
26
- }), DEFAULT_CLIENT_CONFIG = {
27
- apiVersion: "2024-11-12",
28
- useCdn: !1,
29
- ignoreBrowserTokenWarning: !0,
30
- allowReconfigure: !1,
31
- requestTagPrefix: "sanity.sdk"
32
- }, clientStore = defineStore({
33
- name: "clientStore",
34
- getInitialState: (instance) => ({
35
- clients: {},
36
- token: getTokenState(instance).getCurrent()
37
- }),
38
- initialize(context) {
39
- let subscription = listenToToken(context), authMethodSubscription = listenToAuthMethod(context);
40
- return () => {
41
- subscription.unsubscribe(), authMethodSubscription.unsubscribe();
42
- };
43
- }
44
- }), listenToToken = ({ instance, state }) => getTokenState(instance).observable.subscribe((token) => {
45
- state.set("setTokenAndResetClients", {
46
- token,
47
- clients: {}
48
- });
49
- }), listenToAuthMethod = ({ instance, state }) => getAuthMethodState(instance).observable.subscribe((authMethod) => {
50
- state.set("setAuthMethod", { authMethod });
51
- }), getClientConfigKey = (options) => JSON.stringify(pickProperties(options, allowedKeys)), getClient = bindActionGlobally(clientStore, ({ state, instance }, options) => {
52
- if (!options || typeof options != "object") throw Error("getClient() requires a configuration object with at least an \"apiVersion\" property. Example: getClient(instance, { apiVersion: \"2024-11-12\" })");
53
- let disallowedKeys = Object.keys(options).filter((key) => !allowedKeys.includes(key));
54
- if (disallowedKeys.length > 0) {
55
- let listFormatter = new Intl.ListFormat("en", {
56
- style: "long",
57
- type: "conjunction"
58
- });
59
- throw Error(`The client options provided contains unsupported properties: ${listFormatter.format(disallowedKeys)}. Allowed keys are: ${listFormatter.format(allowedKeys)}.`);
60
- }
61
- let tokenFromState = state.get().token, { clients, authMethod } = state.get(), projectId = options.projectId ?? instance.config.projectId, dataset = options.dataset ?? instance.config.dataset, resource;
62
- options.resource && (isMediaLibraryResource(options.resource) ? resource = {
63
- type: "media-library",
64
- id: options.resource.mediaLibraryId
65
- } : isCanvasResource(options.resource) ? resource = {
66
- type: "canvas",
67
- id: options.resource.canvasId
68
- } : isDatasetResource(options.resource) && (projectId = options.resource.projectId, dataset = options.resource.dataset));
69
- let apiHost = options.apiHost ?? instance.config.auth?.apiHost ?? getStagingApiHost(), effectiveOptions = {
70
- ...DEFAULT_CLIENT_CONFIG,
71
- ...(options.scope === "global" || !projectId || resource) && { useProjectHostname: !1 },
72
- token: authMethod === "cookie" ? void 0 : tokenFromState ?? void 0,
73
- ...options,
74
- ...projectId && { projectId },
75
- ...dataset && { dataset },
76
- ...resource ? { resource } : { resource: void 0 },
77
- ...apiHost && { apiHost }
78
- };
79
- resource && (delete effectiveOptions.projectId, delete effectiveOptions.dataset), effectiveOptions.token === null || effectiveOptions.token === void 0 ? (delete effectiveOptions.token, authMethod === "cookie" && (effectiveOptions.withCredentials = !0)) : delete effectiveOptions.withCredentials;
80
- let key = getClientConfigKey(effectiveOptions);
81
- if (clients[key]) return clients[key];
82
- let client = createClient(effectiveOptions);
83
- return state.set("addClient", (prev) => ({ clients: {
84
- ...prev.clients,
85
- [key]: client
86
- } })), client;
87
- }), getClientState = bindActionGlobally(clientStore, createStateSourceAction(({ instance }, options) => getClient(instance, options)));
88
- export { getClient as n, getClientState as r, clientStore_exports as t };
89
-
90
- //# sourceMappingURL=clientStore-BtE2-q2i.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"clientStore-BtE2-q2i.js","names":[],"sources":["../src/client/clientStore.ts"],"sourcesContent":["import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'\n\nimport {getAuthMethodState, getTokenState} from '../auth/authStore'\nimport {\n type DocumentResource,\n isCanvasResource,\n isDatasetResource,\n isMediaLibraryResource,\n} from '../config/sanityConfig'\nimport {bindActionGlobally} from '../store/createActionBinder'\nimport {createStateSourceAction} from '../store/createStateSourceAction'\nimport {defineStore, type StoreContext} from '../store/defineStore'\nimport {getStagingApiHost} from '../utils/getStagingApiHost'\nimport {pickProperties} from '../utils/object'\n\nconst DEFAULT_API_VERSION = '2024-11-12'\nconst DEFAULT_REQUEST_TAG_PREFIX = 'sanity.sdk'\n\ntype AllowedClientConfigKey =\n | 'useCdn'\n | 'token'\n | 'perspective'\n | 'apiHost'\n | 'proxy'\n | 'withCredentials'\n | 'timeout'\n | 'maxRetries'\n | 'dataset'\n | 'projectId'\n | 'requestTagPrefix'\n | 'useProjectHostname'\n\nconst allowedKeys = Object.keys({\n apiHost: null,\n useCdn: null,\n token: null,\n perspective: null,\n proxy: null,\n withCredentials: null,\n timeout: null,\n maxRetries: null,\n dataset: null,\n projectId: null,\n scope: null,\n apiVersion: null,\n requestTagPrefix: null,\n useProjectHostname: null,\n resource: null,\n} satisfies Record<keyof ClientOptions, null>) as (keyof ClientOptions)[]\n\nconst DEFAULT_CLIENT_CONFIG: ClientConfig = {\n apiVersion: DEFAULT_API_VERSION,\n useCdn: false,\n ignoreBrowserTokenWarning: true,\n allowReconfigure: false,\n requestTagPrefix: DEFAULT_REQUEST_TAG_PREFIX,\n}\n\n/**\n * States tracked by the client store\n * @public\n */\nexport interface ClientStoreState {\n token: string | null\n clients: {[TKey in string]?: SanityClient}\n authMethod?: 'localstorage' | 'cookie'\n}\n\n/**\n * Options used when retrieving a client instance from the client store.\n *\n * This interface extends the base {@link ClientConfig} and adds:\n *\n * - **apiVersion:** A required string indicating the API version for the client.\n * - **scope:** An optional flag to choose between the project-specific client\n * ('project') and the global client ('global'). When set to `'global'`, the\n * global client is used.\n *\n * These options are utilized by `getClient` and `getClientState` to configure and\n * return appropriate client instances that automatically handle authentication\n * updates and configuration changes.\n *\n * @public\n */\nexport interface ClientOptions extends Pick<ClientConfig, AllowedClientConfigKey> {\n /**\n * An optional flag to choose between the default client (typically project-level)\n * and the global client ('global'). When set to `'global'`, the global client\n * is used.\n */\n scope?: 'default' | 'global'\n /**\n * A required string indicating the API version for the client.\n */\n apiVersion: string\n\n /**\n * @internal\n * The SDK resource to use for the client -- this will get transformed into a ClientConfig resource.\n */\n resource?: DocumentResource\n}\n\nconst clientStore = defineStore<ClientStoreState>({\n name: 'clientStore',\n\n getInitialState: (instance) => ({\n clients: {},\n token: getTokenState(instance).getCurrent(),\n }),\n\n initialize(context) {\n const subscription = listenToToken(context)\n const authMethodSubscription = listenToAuthMethod(context)\n return () => {\n subscription.unsubscribe()\n authMethodSubscription.unsubscribe()\n }\n },\n})\n\n/**\n * Updates the client store state when a token is received.\n * @internal\n */\nconst listenToToken = ({instance, state}: StoreContext<ClientStoreState>) => {\n return getTokenState(instance).observable.subscribe((token) => {\n state.set('setTokenAndResetClients', {token, clients: {}})\n })\n}\n\nconst listenToAuthMethod = ({instance, state}: StoreContext<ClientStoreState>) => {\n return getAuthMethodState(instance).observable.subscribe((authMethod) => {\n state.set('setAuthMethod', {authMethod})\n })\n}\n\ntype ClientInstanceCacheKeyInput = ClientConfig &\n Partial<Pick<ClientOptions, 'scope'>> & {\n apiVersion: string\n }\n\nconst getClientConfigKey = (options: ClientInstanceCacheKeyInput) =>\n JSON.stringify(pickProperties(options, allowedKeys))\n\n/**\n * Retrieves a Sanity client instance configured with the provided options.\n *\n * This function returns a client instance configured for the project or as a\n * global client based on the options provided. It ensures efficient reuse of\n * client instances by returning the same instance for the same options.\n * For automatic handling of authentication token updates, consider using\n * `getClientState`.\n *\n * @public\n */\nexport const getClient = bindActionGlobally(\n clientStore,\n ({state, instance}, options: ClientOptions) => {\n if (!options || typeof options !== 'object') {\n throw new Error(\n 'getClient() requires a configuration object with at least an \"apiVersion\" property. ' +\n 'Example: getClient(instance, { apiVersion: \"2024-11-12\" })',\n )\n }\n\n // Check for disallowed keys\n const providedKeys = Object.keys(options) as (keyof ClientOptions)[]\n const disallowedKeys = providedKeys.filter((key) => !allowedKeys.includes(key))\n\n if (disallowedKeys.length > 0) {\n const listFormatter = new Intl.ListFormat('en', {style: 'long', type: 'conjunction'})\n throw new Error(\n `The client options provided contains unsupported properties: ${listFormatter.format(disallowedKeys)}. ` +\n `Allowed keys are: ${listFormatter.format(allowedKeys)}.`,\n )\n }\n\n const tokenFromState = state.get().token\n const {clients, authMethod} = state.get()\n let projectId = options.projectId ?? instance.config.projectId\n let dataset = options.dataset ?? instance.config.dataset\n\n let resource: ClientConfig['resource'] | undefined\n\n if (options.resource) {\n if (isMediaLibraryResource(options.resource)) {\n resource = {type: 'media-library', id: options.resource.mediaLibraryId}\n } else if (isCanvasResource(options.resource)) {\n resource = {type: 'canvas', id: options.resource.canvasId}\n } else if (isDatasetResource(options.resource)) {\n // use project-based routes for datasets to avoid existing CORS and Studio auth cookie issues\n projectId = options.resource.projectId\n dataset = options.resource.dataset\n }\n }\n\n const apiHost = options.apiHost ?? instance.config.auth?.apiHost ?? getStagingApiHost()\n\n const effectiveOptions: ClientConfig & {apiVersion: string} = {\n ...DEFAULT_CLIENT_CONFIG,\n ...((options.scope === 'global' || !projectId || resource) && {useProjectHostname: false}),\n token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),\n ...options,\n ...(projectId && {projectId}),\n ...(dataset && {dataset}),\n ...(resource ? {resource} : {resource: undefined}),\n ...(apiHost && {apiHost}),\n }\n\n // When a resource is provided, don't use projectId/dataset - the client should be \"projectless\"\n // The client code itself will ignore the non-resource config, so we do this to prevent confusing the user.\n // (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)\n if (resource) {\n delete effectiveOptions.projectId\n delete effectiveOptions.dataset\n }\n\n if (effectiveOptions.token === null || typeof effectiveOptions.token === 'undefined') {\n delete effectiveOptions.token\n if (authMethod === 'cookie') {\n effectiveOptions.withCredentials = true\n }\n } else {\n delete effectiveOptions.withCredentials\n }\n\n const key = getClientConfigKey(effectiveOptions)\n\n if (clients[key]) return clients[key]\n\n const client = createClient(effectiveOptions)\n state.set('addClient', (prev) => ({clients: {...prev.clients, [key]: client}}))\n\n return client\n },\n)\n\n/**\n * Returns a state source for the Sanity client instance.\n *\n * This function provides a subscribable state source that emits updated client\n * instances whenever relevant configurations change (such as authentication tokens).\n * Use this when you need to react to client configuration changes in your application.\n *\n * @public\n */\nexport const getClientState = bindActionGlobally(\n clientStore,\n createStateSourceAction(({instance}, options: ClientOptions) => getClient(instance, options)),\n)\n"],"mappings":"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"}