@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.
- package/dist/_exports/_internal.d.ts +52 -21
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +10 -9
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/_exports/agent.d.ts +7 -7
- package/dist/_exports/agent.d.ts.map +1 -1
- package/dist/_exports/agent.js +1 -1
- package/dist/_exports/comlink.d.ts +9 -9
- package/dist/_exports/comlink.d.ts.map +1 -1
- package/dist/_exports/comlink.js +3 -2
- package/dist/_exports/comlink.js.map +1 -1
- package/dist/_exports/dashboard.d.ts +2 -0
- package/dist/_exports/dashboard.js +2 -0
- package/dist/{authStore-C-OEkGaM.d.ts → authStore-BO2vch3I.d.ts} +516 -63
- package/dist/authStore-BO2vch3I.d.ts.map +1 -0
- package/dist/{clientStore-gUALQ0qW.js → clientStore-BI4D8HhJ.js} +124 -7
- package/dist/clientStore-BI4D8HhJ.js.map +1 -0
- package/dist/{createSanityInstance-DCePpBYW.d.ts → createSanityInstance-BQIqyGLX.d.ts} +16 -1
- package/dist/createSanityInstance-BQIqyGLX.d.ts.map +1 -0
- package/dist/{object-BwynGPRZ.js → createStateSourceAction-DdAhEfnZ.js} +2 -82
- package/dist/createStateSourceAction-DdAhEfnZ.js.map +1 -0
- package/dist/defineProperty-Bho9Ngsi.js +31 -0
- package/dist/{getNodeState-E0hjqFOo.js → getNodeState-B4nG0sjm.js} +3 -2
- package/dist/{getNodeState-E0hjqFOo.js.map → getNodeState-B4nG0sjm.js.map} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +335 -51
- package/dist/index.js.map +1 -1
- package/dist/object-CxXlSVg7.js +83 -0
- package/dist/object-CxXlSVg7.js.map +1 -0
- package/dist/topicStore-Ct7IFNtY.js +565 -0
- package/dist/topicStore-Ct7IFNtY.js.map +1 -0
- package/dist/topicStore-D0WqhQGF.d.ts +435 -0
- package/dist/topicStore-D0WqhQGF.d.ts.map +1 -0
- package/dist/{version-C90rNWaX.js → version-BcRmCiYi.js} +6 -6
- package/dist/{version-C90rNWaX.js.map → version-BcRmCiYi.js.map} +1 -1
- package/package.json +19 -18
- package/src/_exports/_internal.ts +12 -0
- package/src/_exports/dashboard.ts +36 -0
- package/src/_exports/index.ts +17 -1
- package/src/applications/applications.test-d.ts +28 -4
- package/src/applications/applications.ts +39 -14
- package/src/auth/authMode.test.ts +24 -0
- package/src/auth/authMode.ts +8 -3
- package/src/auth/authStore.ts +12 -0
- package/src/auth/authStrategy.ts +2 -0
- package/src/auth/logout.test.ts +45 -0
- package/src/auth/logout.ts +1 -0
- package/src/auth/oauth/oauthActions.test.ts +457 -0
- package/src/auth/oauth/oauthActions.ts +389 -0
- package/src/auth/oauth/oauthAuth.test.ts +206 -0
- package/src/auth/oauth/oauthAuth.ts +168 -0
- package/src/auth/oauth/pkce.test.ts +44 -0
- package/src/auth/oauth/pkce.ts +60 -0
- package/src/auth/oauth/types.ts +12 -0
- package/src/config/authConfig.ts +18 -0
- package/src/dashboard/messageBus/__fixtures__/test-topics.ts +18 -0
- package/src/dashboard/messageBus/bus.test.ts +1033 -0
- package/src/dashboard/messageBus/bus.ts +1246 -0
- package/src/dashboard/messageBus/store.test.ts +142 -0
- package/src/dashboard/messageBus/store.ts +74 -0
- package/src/dashboard/messageBus/topicStore.test.ts +172 -0
- package/src/dashboard/messageBus/topicStore.ts +184 -0
- package/src/dashboard/messageBus/topics.test.ts +71 -0
- package/src/dashboard/messageBus/topics.ts +260 -0
- package/src/document/actions.ts +3 -3
- package/src/document/applyDocumentActions.ts +3 -3
- package/src/document/documentStore.ts +12 -12
- package/src/document/processMutations.ts +58 -22
- package/src/installations/installations.test-d.ts +3 -3
- package/src/installations/installations.ts +2 -19
- package/src/projection/getProjectionState.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/query/queryStore.ts +3 -3
- package/src/typegen/defineProjection.test.ts +17 -0
- package/src/typegen/defineProjection.ts +19 -0
- package/src/typegen/groqCompat.ts +122 -0
- package/src/typegen/resolve.ts +37 -0
- package/src/utils/createGroqSearchFilter.test.ts +8 -0
- package/src/utils/createGroqSearchFilter.ts +6 -3
- package/dist/authStore-C-OEkGaM.d.ts.map +0 -1
- package/dist/clientStore-gUALQ0qW.js.map +0 -1
- package/dist/createSanityInstance-DCePpBYW.d.ts.map +0 -1
- 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
|
+
}
|
package/src/config/authConfig.ts
CHANGED
|
@@ -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
|
+
}
|