@sanity/sdk 3.1.0 → 3.2.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 (83) hide show
  1. package/dist/_exports/_internal.d.ts +52 -21
  2. package/dist/_exports/_internal.d.ts.map +1 -1
  3. package/dist/_exports/_internal.js +10 -9
  4. package/dist/_exports/_internal.js.map +1 -1
  5. package/dist/_exports/agent.d.ts +7 -7
  6. package/dist/_exports/agent.d.ts.map +1 -1
  7. package/dist/_exports/agent.js +1 -1
  8. package/dist/_exports/comlink.d.ts +9 -9
  9. package/dist/_exports/comlink.d.ts.map +1 -1
  10. package/dist/_exports/comlink.js +3 -2
  11. package/dist/_exports/comlink.js.map +1 -1
  12. package/dist/_exports/dashboard.d.ts +2 -0
  13. package/dist/_exports/dashboard.js +2 -0
  14. package/dist/{authStore-C-OEkGaM.d.ts → authStore-BO2vch3I.d.ts} +516 -63
  15. package/dist/authStore-BO2vch3I.d.ts.map +1 -0
  16. package/dist/{clientStore-gUALQ0qW.js → clientStore-BI4D8HhJ.js} +124 -7
  17. package/dist/clientStore-BI4D8HhJ.js.map +1 -0
  18. package/dist/{createSanityInstance-DCePpBYW.d.ts → createSanityInstance-BQIqyGLX.d.ts} +16 -1
  19. package/dist/createSanityInstance-BQIqyGLX.d.ts.map +1 -0
  20. package/dist/{object-BwynGPRZ.js → createStateSourceAction-DdAhEfnZ.js} +2 -82
  21. package/dist/createStateSourceAction-DdAhEfnZ.js.map +1 -0
  22. package/dist/defineProperty-Bho9Ngsi.js +31 -0
  23. package/dist/{getNodeState-E0hjqFOo.js → getNodeState-B4nG0sjm.js} +3 -2
  24. package/dist/{getNodeState-E0hjqFOo.js.map → getNodeState-B4nG0sjm.js.map} +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +335 -51
  27. package/dist/index.js.map +1 -1
  28. package/dist/object-CxXlSVg7.js +83 -0
  29. package/dist/object-CxXlSVg7.js.map +1 -0
  30. package/dist/topicStore-Ct7IFNtY.js +565 -0
  31. package/dist/topicStore-Ct7IFNtY.js.map +1 -0
  32. package/dist/topicStore-D0WqhQGF.d.ts +435 -0
  33. package/dist/topicStore-D0WqhQGF.d.ts.map +1 -0
  34. package/dist/{version-C90rNWaX.js → version-BcRmCiYi.js} +6 -6
  35. package/dist/{version-C90rNWaX.js.map → version-BcRmCiYi.js.map} +1 -1
  36. package/package.json +19 -18
  37. package/src/_exports/_internal.ts +12 -0
  38. package/src/_exports/dashboard.ts +36 -0
  39. package/src/_exports/index.ts +17 -1
  40. package/src/applications/applications.test-d.ts +28 -4
  41. package/src/applications/applications.ts +39 -14
  42. package/src/auth/authMode.test.ts +24 -0
  43. package/src/auth/authMode.ts +8 -3
  44. package/src/auth/authStore.ts +12 -0
  45. package/src/auth/authStrategy.ts +2 -0
  46. package/src/auth/logout.test.ts +45 -0
  47. package/src/auth/logout.ts +1 -0
  48. package/src/auth/oauth/oauthActions.test.ts +457 -0
  49. package/src/auth/oauth/oauthActions.ts +389 -0
  50. package/src/auth/oauth/oauthAuth.test.ts +206 -0
  51. package/src/auth/oauth/oauthAuth.ts +168 -0
  52. package/src/auth/oauth/pkce.test.ts +44 -0
  53. package/src/auth/oauth/pkce.ts +60 -0
  54. package/src/auth/oauth/types.ts +12 -0
  55. package/src/config/authConfig.ts +18 -0
  56. package/src/dashboard/messageBus/__fixtures__/test-topics.ts +18 -0
  57. package/src/dashboard/messageBus/bus.test.ts +1033 -0
  58. package/src/dashboard/messageBus/bus.ts +1246 -0
  59. package/src/dashboard/messageBus/store.test.ts +142 -0
  60. package/src/dashboard/messageBus/store.ts +74 -0
  61. package/src/dashboard/messageBus/topicStore.test.ts +172 -0
  62. package/src/dashboard/messageBus/topicStore.ts +184 -0
  63. package/src/dashboard/messageBus/topics.test.ts +71 -0
  64. package/src/dashboard/messageBus/topics.ts +260 -0
  65. package/src/document/actions.ts +3 -3
  66. package/src/document/applyDocumentActions.ts +3 -3
  67. package/src/document/documentStore.ts +12 -12
  68. package/src/document/processMutations.ts +58 -22
  69. package/src/installations/installations.test-d.ts +3 -3
  70. package/src/installations/installations.ts +2 -19
  71. package/src/projection/getProjectionState.ts +2 -2
  72. package/src/projection/resolveProjection.ts +2 -2
  73. package/src/query/queryStore.ts +3 -3
  74. package/src/typegen/defineProjection.test.ts +17 -0
  75. package/src/typegen/defineProjection.ts +19 -0
  76. package/src/typegen/groqCompat.ts +122 -0
  77. package/src/typegen/resolve.ts +37 -0
  78. package/src/utils/createGroqSearchFilter.test.ts +8 -0
  79. package/src/utils/createGroqSearchFilter.ts +6 -3
  80. package/dist/authStore-C-OEkGaM.d.ts.map +0 -1
  81. package/dist/clientStore-gUALQ0qW.js.map +0 -1
  82. package/dist/createSanityInstance-DCePpBYW.d.ts.map +0 -1
  83. package/dist/object-BwynGPRZ.js.map +0 -1
@@ -0,0 +1,168 @@
1
+ import {defer, distinctUntilChanged, filter, map, type Subscription} from 'rxjs'
2
+
3
+ import {type StoreContext} from '../../store/defineStore'
4
+ import {DEFAULT_BASE} from '../authConstants'
5
+ import {AuthStateType} from '../authStateType'
6
+ import {type AuthStoreState} from '../authStore'
7
+ import {type AuthStrategyOptions, type AuthStrategyResult} from '../authStrategy'
8
+ import {subscribeToStateAndFetchCurrentUser} from '../subscribeToStateAndFetchCurrentUser'
9
+ import {createLoggedInAuthState, getDefaultStorage, getStorageEvents} from '../utils'
10
+ import {type OAuthTokens} from './types'
11
+
12
+ /** localStorage (or configured `storageArea`) key for persisted OAuth tokens. */
13
+ export const OAUTH_TOKENS_KEY = '__sanity_oauth_tokens'
14
+
15
+ /** The persisted JSON shape of {@link OAuthTokens}, `expiresAt` as an ISO string. */
16
+ interface SerializedOAuthTokens extends Omit<OAuthTokens, 'expiresAt'> {
17
+ expiresAt: string
18
+ }
19
+
20
+ /**
21
+ * Parses persisted token JSON back into {@link OAuthTokens}. Returns `null`
22
+ * when the value is missing or malformed.
23
+ *
24
+ * @internal
25
+ */
26
+ export function deserializeTokens(raw: string | null): OAuthTokens | null {
27
+ if (!raw) return null
28
+ try {
29
+ const parsed: unknown = JSON.parse(raw)
30
+ if (
31
+ typeof parsed !== 'object' ||
32
+ parsed === null ||
33
+ !('accessToken' in parsed) ||
34
+ typeof (parsed as SerializedOAuthTokens).accessToken !== 'string' ||
35
+ !('expiresAt' in parsed) ||
36
+ typeof (parsed as SerializedOAuthTokens).expiresAt !== 'string'
37
+ ) {
38
+ return null
39
+ }
40
+ const value = parsed as SerializedOAuthTokens
41
+ return {
42
+ accessToken: value.accessToken,
43
+ tokenType: 'bearer',
44
+ expiresIn: value.expiresIn,
45
+ expiresAt: new Date(value.expiresAt),
46
+ ...(value.refreshToken !== undefined && {refreshToken: value.refreshToken}),
47
+ }
48
+ } catch {
49
+ return null
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Resolves the initial auth state for OAuth mode.
55
+ *
56
+ * State discovery order:
57
+ * 1. Persisted tokens in `__sanity_oauth_tokens` → `LOGGED_IN`
58
+ * 2. Callback URL (contains `code`/`state`/`error` and matches `redirectUri`)
59
+ * → `LOGGING_IN`
60
+ * 3. Otherwise → `LOGGED_OUT`
61
+ *
62
+ * @internal
63
+ */
64
+ export function getOauthInitialState(options: AuthStrategyOptions): AuthStrategyResult {
65
+ const {authConfig, initialLocationHref} = options
66
+ const storageKey = OAUTH_TOKENS_KEY
67
+ const storageArea = authConfig.storageArea ?? getDefaultStorage()
68
+ const redirectUri = authConfig.oauth?.redirectUri
69
+
70
+ // Persisted tokens win
71
+ const tokens = deserializeTokens(storageArea?.getItem(OAUTH_TOKENS_KEY) ?? null)
72
+ if (tokens) {
73
+ return {
74
+ authState: createLoggedInAuthState(tokens.accessToken, null),
75
+ storageKey,
76
+ storageArea,
77
+ authMethod: 'localstorage',
78
+ dashboardContext: {},
79
+ oauthTokens: tokens,
80
+ }
81
+ }
82
+
83
+ // Callback URL with code/state/error whose origin + pathname match our
84
+ // redirect URI (query and hash are ignored).
85
+ const {searchParams} = new URL(initialLocationHref, DEFAULT_BASE)
86
+ const isCallback =
87
+ searchParams.has('code') || searchParams.has('state') || searchParams.has('error')
88
+ if (redirectUri && isCallback) {
89
+ const loc = new URL(initialLocationHref, DEFAULT_BASE)
90
+ const redirect = new URL(redirectUri, DEFAULT_BASE)
91
+ if (loc.origin === redirect.origin && loc.pathname === redirect.pathname) {
92
+ return {
93
+ authState: {type: AuthStateType.LOGGING_IN, isExchangingToken: false},
94
+ storageKey,
95
+ storageArea,
96
+ authMethod: undefined,
97
+ dashboardContext: {},
98
+ }
99
+ }
100
+ }
101
+
102
+ // No tokens, not a callback
103
+ return {
104
+ authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
105
+ storageKey,
106
+ storageArea,
107
+ authMethod: undefined,
108
+ dashboardContext: {},
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Subscribes to cross-tab `storage` events for the OAuth tokens key, syncing
114
+ * this tab's auth state when tokens change in another tab.
115
+ *
116
+ * @internal
117
+ */
118
+ export function subscribeToOAuthStorageEvents({state}: StoreContext<AuthStoreState>): Subscription {
119
+ const {storageArea} = state.get().options
120
+
121
+ const tokens$ = defer(getStorageEvents).pipe(
122
+ filter((e) => e.storageArea === storageArea && e.key === OAUTH_TOKENS_KEY),
123
+ map(() => deserializeTokens(storageArea?.getItem(OAUTH_TOKENS_KEY) ?? null)),
124
+ distinctUntilChanged((a, b) => a?.accessToken === b?.accessToken),
125
+ )
126
+
127
+ return tokens$.subscribe((tokens) => {
128
+ state.set('updateOAuthTokensFromStorageEvent', {
129
+ authState: tokens
130
+ ? createLoggedInAuthState(tokens.accessToken, null)
131
+ : {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
132
+ oauthTokens: tokens ?? undefined,
133
+ })
134
+ })
135
+ }
136
+
137
+ /**
138
+ * Initialize OAuth auth subscriptions:
139
+ * - Subscribe to state changes and fetch current user
140
+ * - Subscribe to cross-tab storage events for the OAuth tokens key
141
+ *
142
+ * OAuth tokens are refreshed via the `refresh_token` grant (see
143
+ * `refreshOAuthTokens`), so the stamped-token refresher is not started here.
144
+ *
145
+ * @internal
146
+ */
147
+ export function initializeOauthAuth(context: StoreContext<AuthStoreState>): {
148
+ dispose: () => void
149
+ tokenRefresherStarted: boolean
150
+ } {
151
+ const subscriptions: Subscription[] = []
152
+
153
+ subscriptions.push(subscribeToStateAndFetchCurrentUser(context, {useProjectHostname: false}))
154
+
155
+ const storageArea = context.state.get().options?.storageArea
156
+ if (storageArea) {
157
+ subscriptions.push(subscribeToOAuthStorageEvents(context))
158
+ }
159
+
160
+ return {
161
+ dispose: () => {
162
+ for (const subscription of subscriptions) {
163
+ subscription.unsubscribe()
164
+ }
165
+ },
166
+ tokenRefresherStarted: false,
167
+ }
168
+ }
@@ -0,0 +1,44 @@
1
+ import {createHash} from 'node:crypto'
2
+
3
+ import {describe, expect, it} from 'vitest'
4
+
5
+ import {base64UrlEncode, generateCodeChallenge, generateCodeVerifier, generateState} from './pkce'
6
+
7
+ describe('base64UrlEncode', () => {
8
+ it('encodes bytes as base64url without padding', () => {
9
+ // 0xff 0xef 0xbf → "/++/" in base64, "_--_" in base64url (minus padding)
10
+ const encoded = base64UrlEncode(new Uint8Array([0xff, 0xef, 0xbf]))
11
+ expect(encoded).not.toContain('+')
12
+ expect(encoded).not.toContain('/')
13
+ expect(encoded).not.toContain('=')
14
+ expect(encoded).toBe('_--_')
15
+ })
16
+ })
17
+
18
+ describe('generateCodeVerifier / generateState', () => {
19
+ it('generates unique, URL-safe strings', () => {
20
+ const a = generateCodeVerifier()
21
+ const b = generateCodeVerifier()
22
+ const s = generateState()
23
+ expect(a).not.toBe(b)
24
+ expect(a).toMatch(/^[A-Za-z0-9_-]+$/)
25
+ expect(s).toMatch(/^[A-Za-z0-9_-]+$/)
26
+ expect(a.length).toBeGreaterThanOrEqual(43)
27
+ })
28
+ })
29
+
30
+ describe('generateCodeChallenge', () => {
31
+ it('produces base64url(sha256(verifier))', async () => {
32
+ const verifier = 'test-verifier'
33
+ const challenge = await generateCodeChallenge(verifier)
34
+
35
+ const expected = createHash('sha256')
36
+ .update(verifier)
37
+ .digest('base64')
38
+ .replace(/\+/g, '-')
39
+ .replace(/\//g, '_')
40
+ .replace(/=+$/, '')
41
+
42
+ expect(challenge).toBe(expected)
43
+ })
44
+ })
@@ -0,0 +1,60 @@
1
+ /**
2
+ * PKCE (Proof Key for Code Exchange, RFC 7636) helpers for the OAuth
3
+ * authorization-code flow.
4
+ *
5
+ * @internal
6
+ */
7
+
8
+ /** Number of random bytes used for the `code_verifier` and `state`. */
9
+ const RANDOM_BYTE_LENGTH = 32
10
+
11
+ /**
12
+ * Encodes a byte array as a base64url string (RFC 4648 §5) with no padding,
13
+ * as required for PKCE `code_verifier`/`code_challenge` values.
14
+ *
15
+ * @internal
16
+ */
17
+ export function base64UrlEncode(bytes: Uint8Array): string {
18
+ let binary = ''
19
+ for (const byte of bytes) {
20
+ binary += String.fromCharCode(byte)
21
+ }
22
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
23
+ }
24
+
25
+ function randomString(): string {
26
+ const bytes = new Uint8Array(RANDOM_BYTE_LENGTH)
27
+ crypto.getRandomValues(bytes)
28
+ return base64UrlEncode(bytes)
29
+ }
30
+
31
+ /**
32
+ * Generates a PKCE `code_verifier`
33
+ *
34
+ * @internal
35
+ */
36
+ export function generateCodeVerifier(): string {
37
+ return randomString()
38
+ }
39
+
40
+ /**
41
+ * Generates the `state` parameter used to defend against CSRF on the OAuth
42
+ * callback.
43
+ *
44
+ * @internal
45
+ */
46
+ export function generateState(): string {
47
+ return randomString()
48
+ }
49
+
50
+ /**
51
+ * Derives the PKCE `code_challenge` from a `code_verifier` using
52
+ * `base64url(sha256(verifier))` (the `S256` method).
53
+ *
54
+ * @internal
55
+ */
56
+ export async function generateCodeChallenge(codeVerifier: string): Promise<string> {
57
+ const data = new TextEncoder().encode(codeVerifier)
58
+ const digest = await crypto.subtle.digest('SHA-256', data)
59
+ return base64UrlEncode(new Uint8Array(digest))
60
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * OAuth tokens issued by Sanity's OAuth token endpoint.
3
+ *
4
+ * @public
5
+ */
6
+ export interface OAuthTokens {
7
+ accessToken: string
8
+ tokenType: 'bearer'
9
+ expiresIn: number
10
+ expiresAt: Date
11
+ refreshToken?: string
12
+ }
@@ -76,4 +76,22 @@ export interface AuthConfig {
76
76
  * ignoring any storage or callback handling.
77
77
  */
78
78
  token?: string
79
+
80
+ oauth?: {
81
+ /**
82
+ * Opaque OAuth `client_id`.
83
+ */
84
+ clientId: string
85
+
86
+ /**
87
+ * The redirect URI registered for this client. The OAuth provider
88
+ * redirects back here with the authorization `code` and `state`.
89
+ */
90
+ redirectUri: string
91
+
92
+ /**
93
+ * Organisation id used for RFC 8707 resource scoping.
94
+ */
95
+ organizationId: string
96
+ }
79
97
  }
@@ -0,0 +1,18 @@
1
+ import {type EventTopicDef, type StateTopicDef} from '../topics'
2
+
3
+ declare module '../topics' {
4
+ interface Topics {
5
+ 'test.count': StateTopicDef<number>
6
+ 'test.token': StateTopicDef<string | null>
7
+ 'test.scheme': StateTopicDef<'light' | 'dark'>
8
+ 'test.suspending': StateTopicDef<string>
9
+ 'test.ping': EventTopicDef<{n: number}>
10
+ 'test.echo': EventTopicDef<{n: number}, {n: number}>
11
+ 'test.mint': EventTopicDef<void, string>
12
+ 'test.profile': StateTopicDef<{
13
+ fullName: string
14
+ tags: readonly string[]
15
+ }>
16
+ 'test.greet': EventTopicDef<{fullName: string}, {salutation: string; language: string}>
17
+ }
18
+ }