@sanity/sdk 3.1.0 → 3.3.0-rc.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 (86) 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-BGoEGxOq.d.ts} +531 -63
  15. package/dist/authStore-BGoEGxOq.d.ts.map +1 -0
  16. package/dist/{clientStore-gUALQ0qW.js → clientStore-QDrJNTM4.js} +128 -7
  17. package/dist/clientStore-QDrJNTM4.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 +371 -54
  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-DXvuH9h4.d.ts +435 -0
  33. package/dist/topicStore-DXvuH9h4.d.ts.map +1 -0
  34. package/dist/{version-C90rNWaX.js → version-CIhX5ppJ.js} +6 -6
  35. package/dist/{version-C90rNWaX.js.map → version-CIhX5ppJ.js.map} +1 -1
  36. package/package.json +20 -19
  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.test.ts +18 -0
  45. package/src/auth/authStore.ts +15 -0
  46. package/src/auth/authStrategy.ts +2 -0
  47. package/src/auth/handleAuthCallback.test.ts +33 -0
  48. package/src/auth/handleAuthCallback.ts +11 -1
  49. package/src/auth/logout.test.ts +53 -0
  50. package/src/auth/logout.ts +19 -1
  51. package/src/auth/oauth/oauthActions.test.ts +593 -0
  52. package/src/auth/oauth/oauthActions.ts +438 -0
  53. package/src/auth/oauth/oauthAuth.test.ts +229 -0
  54. package/src/auth/oauth/oauthAuth.ts +178 -0
  55. package/src/auth/oauth/pkce.test.ts +44 -0
  56. package/src/auth/oauth/pkce.ts +60 -0
  57. package/src/auth/oauth/types.ts +12 -0
  58. package/src/config/authConfig.ts +18 -0
  59. package/src/dashboard/messageBus/__fixtures__/test-topics.ts +18 -0
  60. package/src/dashboard/messageBus/bus.test.ts +1033 -0
  61. package/src/dashboard/messageBus/bus.ts +1246 -0
  62. package/src/dashboard/messageBus/store.test.ts +142 -0
  63. package/src/dashboard/messageBus/store.ts +74 -0
  64. package/src/dashboard/messageBus/topicStore.test.ts +172 -0
  65. package/src/dashboard/messageBus/topicStore.ts +184 -0
  66. package/src/dashboard/messageBus/topics.test.ts +71 -0
  67. package/src/dashboard/messageBus/topics.ts +260 -0
  68. package/src/document/actions.ts +3 -3
  69. package/src/document/applyDocumentActions.ts +3 -3
  70. package/src/document/documentStore.ts +12 -12
  71. package/src/document/processMutations.ts +58 -22
  72. package/src/installations/installations.test-d.ts +3 -3
  73. package/src/installations/installations.ts +2 -19
  74. package/src/projection/getProjectionState.ts +2 -2
  75. package/src/projection/resolveProjection.ts +2 -2
  76. package/src/query/queryStore.ts +3 -3
  77. package/src/typegen/defineProjection.test.ts +17 -0
  78. package/src/typegen/defineProjection.ts +19 -0
  79. package/src/typegen/groqCompat.ts +122 -0
  80. package/src/typegen/resolve.ts +37 -0
  81. package/src/utils/createGroqSearchFilter.test.ts +8 -0
  82. package/src/utils/createGroqSearchFilter.ts +6 -3
  83. package/dist/authStore-C-OEkGaM.d.ts.map +0 -1
  84. package/dist/clientStore-gUALQ0qW.js.map +0 -1
  85. package/dist/createSanityInstance-DCePpBYW.d.ts.map +0 -1
  86. package/dist/object-BwynGPRZ.js.map +0 -1
@@ -0,0 +1,178 @@
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, including an `expiresAt` that does
23
+ * not parse to a valid date (an `Invalid Date` would otherwise read as never
24
+ * expiring, since `NaN <= now` is always `false`).
25
+ *
26
+ * @internal
27
+ */
28
+ export function deserializeTokens(raw: string | null): OAuthTokens | null {
29
+ if (!raw) return null
30
+ try {
31
+ const parsed: unknown = JSON.parse(raw)
32
+ if (
33
+ typeof parsed !== 'object' ||
34
+ parsed === null ||
35
+ !('accessToken' in parsed) ||
36
+ typeof (parsed as SerializedOAuthTokens).accessToken !== 'string' ||
37
+ !('expiresAt' in parsed) ||
38
+ typeof (parsed as SerializedOAuthTokens).expiresAt !== 'string'
39
+ ) {
40
+ return null
41
+ }
42
+ const value = parsed as SerializedOAuthTokens
43
+ const expiresAt = new Date(value.expiresAt)
44
+ if (Number.isNaN(expiresAt.getTime())) return null
45
+ return {
46
+ accessToken: value.accessToken,
47
+ tokenType: 'bearer',
48
+ expiresIn: value.expiresIn,
49
+ expiresAt,
50
+ ...(value.refreshToken !== undefined && {refreshToken: value.refreshToken}),
51
+ }
52
+ } catch {
53
+ return null
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Resolves the initial auth state for OAuth mode.
59
+ *
60
+ * State discovery order:
61
+ * 1. Persisted tokens in `__sanity_oauth_tokens` → `LOGGED_IN`
62
+ * 2. Callback URL (contains `code` or `error` and matches `redirectUri`)
63
+ * → `LOGGING_IN`
64
+ * 3. Otherwise → `LOGGED_OUT`
65
+ *
66
+ * @internal
67
+ */
68
+ export function getOauthInitialState(options: AuthStrategyOptions): AuthStrategyResult {
69
+ const {authConfig, initialLocationHref} = options
70
+ const storageKey = OAUTH_TOKENS_KEY
71
+ const storageArea = authConfig.storageArea ?? getDefaultStorage()
72
+ const redirectUri = authConfig.oauth?.redirectUri
73
+
74
+ // Persisted tokens win
75
+ const rawTokens = storageArea?.getItem(OAUTH_TOKENS_KEY) ?? null
76
+ const tokens = deserializeTokens(rawTokens)
77
+ if (rawTokens && !tokens) {
78
+ // Corrupt entry: drop it so it cannot keep shadowing a fresh login
79
+ storageArea?.removeItem(OAUTH_TOKENS_KEY)
80
+ }
81
+ if (tokens) {
82
+ return {
83
+ authState: createLoggedInAuthState(tokens.accessToken, null),
84
+ storageKey,
85
+ storageArea,
86
+ authMethod: 'localstorage',
87
+ dashboardContext: {},
88
+ oauthTokens: tokens,
89
+ }
90
+ }
91
+
92
+ // Callback URL with code/error whose origin + pathname match our redirect
93
+ // URI (query and hash are ignored). `state` alone is not a callback: the
94
+ // server always sends `code` or `error`, and handleOAuthCallback would
95
+ // otherwise leave the app stuck in LOGGING_IN.
96
+ const {searchParams} = new URL(initialLocationHref, DEFAULT_BASE)
97
+ const isCallback = searchParams.has('code') || searchParams.has('error')
98
+ if (redirectUri && isCallback) {
99
+ const loc = new URL(initialLocationHref, DEFAULT_BASE)
100
+ const redirect = new URL(redirectUri, DEFAULT_BASE)
101
+ if (loc.origin === redirect.origin && loc.pathname === redirect.pathname) {
102
+ return {
103
+ authState: {type: AuthStateType.LOGGING_IN, isExchangingToken: false},
104
+ storageKey,
105
+ storageArea,
106
+ authMethod: undefined,
107
+ dashboardContext: {},
108
+ }
109
+ }
110
+ }
111
+
112
+ // No tokens, not a callback
113
+ return {
114
+ authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
115
+ storageKey,
116
+ storageArea,
117
+ authMethod: undefined,
118
+ dashboardContext: {},
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Subscribes to cross-tab `storage` events for the OAuth tokens key, syncing
124
+ * this tab's auth state when tokens change in another tab.
125
+ *
126
+ * @internal
127
+ */
128
+ export function subscribeToOAuthStorageEvents({state}: StoreContext<AuthStoreState>): Subscription {
129
+ const {storageArea} = state.get().options
130
+
131
+ const tokens$ = defer(getStorageEvents).pipe(
132
+ filter((e) => e.storageArea === storageArea && e.key === OAUTH_TOKENS_KEY),
133
+ map(() => deserializeTokens(storageArea?.getItem(OAUTH_TOKENS_KEY) ?? null)),
134
+ distinctUntilChanged((a, b) => a?.accessToken === b?.accessToken),
135
+ )
136
+
137
+ return tokens$.subscribe((tokens) => {
138
+ state.set('updateOAuthTokensFromStorageEvent', {
139
+ authState: tokens
140
+ ? createLoggedInAuthState(tokens.accessToken, null)
141
+ : {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
142
+ oauthTokens: tokens ?? undefined,
143
+ })
144
+ })
145
+ }
146
+
147
+ /**
148
+ * Initialize OAuth auth subscriptions:
149
+ * - Subscribe to state changes and fetch current user
150
+ * - Subscribe to cross-tab storage events for the OAuth tokens key
151
+ *
152
+ * OAuth tokens are refreshed via the `refresh_token` grant (see
153
+ * `refreshOAuthTokens`), so the stamped-token refresher is not started here.
154
+ *
155
+ * @internal
156
+ */
157
+ export function initializeOauthAuth(context: StoreContext<AuthStoreState>): {
158
+ dispose: () => void
159
+ tokenRefresherStarted: boolean
160
+ } {
161
+ const subscriptions: Subscription[] = []
162
+
163
+ subscriptions.push(subscribeToStateAndFetchCurrentUser(context, {useProjectHostname: false}))
164
+
165
+ const storageArea = context.state.get().options?.storageArea
166
+ if (storageArea) {
167
+ subscriptions.push(subscribeToOAuthStorageEvents(context))
168
+ }
169
+
170
+ return {
171
+ dispose: () => {
172
+ for (const subscription of subscriptions) {
173
+ subscription.unsubscribe()
174
+ }
175
+ },
176
+ tokenRefresherStarted: false,
177
+ }
178
+ }
@@ -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
+ }