@supabase/auth-js 2.107.0-canary.0 → 2.107.0-canary.2
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/main/GoTrueAdminApi.d.ts +0 -1
- package/dist/main/GoTrueAdminApi.d.ts.map +1 -1
- package/dist/main/GoTrueAdminApi.js +7 -20
- package/dist/main/GoTrueAdminApi.js.map +1 -1
- package/dist/main/GoTrueClient.d.ts +83 -14
- package/dist/main/GoTrueClient.d.ts.map +1 -1
- package/dist/main/GoTrueClient.js +346 -107
- package/dist/main/GoTrueClient.js.map +1 -1
- package/dist/main/lib/errors.d.ts +24 -0
- package/dist/main/lib/errors.d.ts.map +1 -1
- package/dist/main/lib/errors.js +31 -1
- package/dist/main/lib/errors.js.map +1 -1
- package/dist/main/lib/locks.d.ts +28 -34
- package/dist/main/lib/locks.d.ts.map +1 -1
- package/dist/main/lib/locks.js +28 -34
- package/dist/main/lib/locks.js.map +1 -1
- package/dist/main/lib/types.d.ts +16 -27
- package/dist/main/lib/types.d.ts.map +1 -1
- package/dist/main/lib/types.js.map +1 -1
- package/dist/main/lib/version.d.ts +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/module/GoTrueAdminApi.d.ts +0 -1
- package/dist/module/GoTrueAdminApi.d.ts.map +1 -1
- package/dist/module/GoTrueAdminApi.js +8 -21
- package/dist/module/GoTrueAdminApi.js.map +1 -1
- package/dist/module/GoTrueClient.d.ts +83 -14
- package/dist/module/GoTrueClient.d.ts.map +1 -1
- package/dist/module/GoTrueClient.js +348 -109
- package/dist/module/GoTrueClient.js.map +1 -1
- package/dist/module/lib/errors.d.ts +24 -0
- package/dist/module/lib/errors.d.ts.map +1 -1
- package/dist/module/lib/errors.js +28 -0
- package/dist/module/lib/errors.js.map +1 -1
- package/dist/module/lib/locks.d.ts +28 -34
- package/dist/module/lib/locks.d.ts.map +1 -1
- package/dist/module/lib/locks.js +28 -34
- package/dist/module/lib/locks.js.map +1 -1
- package/dist/module/lib/types.d.ts +16 -27
- package/dist/module/lib/types.d.ts.map +1 -1
- package/dist/module/lib/types.js.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.js +1 -1
- package/dist/tsconfig.module.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migrations/lockless-coordination.md +89 -0
- package/package.json +1 -1
- package/src/GoTrueAdminApi.ts +32 -72
- package/src/GoTrueClient.ts +412 -137
- package/src/lib/errors.ts +32 -0
- package/src/lib/locks.ts +29 -34
- package/src/lib/types.ts +16 -27
- package/src/lib/version.ts +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Lockless auth coordination
|
|
2
|
+
|
|
3
|
+
**Since:** v2.X.Y (update at release time)
|
|
4
|
+
**Action required by:** v3.0.0
|
|
5
|
+
|
|
6
|
+
`@supabase/auth-js` now coordinates session refreshes without a shared mutex by default. The legacy `lock` option is still honored when supplied, but it is deprecated and will be removed in v3.
|
|
7
|
+
|
|
8
|
+
## What changed
|
|
9
|
+
|
|
10
|
+
- **Default coordination is lockless.** A client constructed without a `lock` option no longer acquires `navigator.locks` or any in-process lock. In-tab concurrent refreshes are deduplicated via the pre-existing single-flight (`refreshingDeferred`); cross-tab refresh races are resolved by GoTrue's server-side parent-of-active mechanism on the v1 refresh-token path.
|
|
11
|
+
- **Commit guard inside `_callRefreshToken`.** The client snapshots storage before the rotated-token fetch and re-reads after. If a non-null pre-fetch snapshot was cleared between the two reads (typical case: a concurrent `signOut` ran `_removeSession`), the rotated tokens are discarded instead of being written back over the cleared storage. The discarded result resolves with `{ data: null, error: new AuthRefreshDiscardedError() }`.
|
|
12
|
+
- **New `AuthRefreshDiscardedError`** (with `isAuthRefreshDiscardedError` type guard). Surfaces through `refreshSession()` and `getSession()` results when the commit guard fires. Distinct from `AuthRetryableFetchError` (transient network) and `AuthApiError` (server rejection).
|
|
13
|
+
- **New `client.auth.dispose()`.** Tears down the auto-refresh interval, the `visibilitychange` listener, the `BroadcastChannel`, and registered `onAuthStateChange` subscribers. Idempotent. Designed for React Strict Mode and HMR cleanup hooks. In-flight `fetch` calls are not aborted — they run to completion.
|
|
14
|
+
- **`lock` and `lockAcquireTimeout` options.** Accepted and honored when supplied (legacy opt-in path); both are `@deprecated` and will be removed in v3.
|
|
15
|
+
|
|
16
|
+
## Who is affected
|
|
17
|
+
|
|
18
|
+
**Most callers: nothing to do.** If you do not pass a `lock` option to `createClient` / `GoTrueClient`, you are already on the lockless default path and will get the bug fixes and new APIs without any code change.
|
|
19
|
+
|
|
20
|
+
**Callers passing a custom `lock`** (typically React Native `processLock`, Node multi-process setups with shared AsyncStorage, or a custom lock implementation):
|
|
21
|
+
|
|
22
|
+
- v2.x (this release): your custom `lock` is still invoked exactly as before. The legacy `_acquireLock` machinery is preserved on an opt-in path gated by `settings.lock != null`. No code change required.
|
|
23
|
+
- v3.0.0 (planned): the `lock` and `lockAcquireTimeout` options will be removed entirely. Drop them from your client options before upgrading to v3.
|
|
24
|
+
|
|
25
|
+
## New APIs worth knowing
|
|
26
|
+
|
|
27
|
+
### `client.auth.dispose()`
|
|
28
|
+
|
|
29
|
+
Tears down the client's background work in one call. Safe to call repeatedly.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const supabase = createClient(URL, KEY)
|
|
34
|
+
return () => {
|
|
35
|
+
supabase.auth.dispose()
|
|
36
|
+
}
|
|
37
|
+
}, [])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `AuthRefreshDiscardedError`
|
|
41
|
+
|
|
42
|
+
Returned from `refreshSession()` / `getSession()` when the commit guard discards a successfully-rotated session.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { isAuthRefreshDiscardedError } from '@supabase/auth-js'
|
|
46
|
+
|
|
47
|
+
const { data, error } = await supabase.auth.refreshSession()
|
|
48
|
+
if (isAuthRefreshDiscardedError(error)) {
|
|
49
|
+
// A concurrent signOut cleared storage between fetch start and now.
|
|
50
|
+
// The rotated tokens were discarded; the SIGNED_OUT event already fired.
|
|
51
|
+
// Treat as a successful no-op — no need to retry.
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Behavior changes worth flagging
|
|
56
|
+
|
|
57
|
+
- **`_autoRefreshTokenTick` may run concurrently with `signOut` / `setSession` / `getUser`** on the lockless default path. Previously the tick used `_acquireLock(0, ...)` which skipped whenever any auth op held the lock. The lockless equivalent only skips when `refreshingDeferred` is set. The commit guard keeps storage consistent under the new concurrency. The legacy lock opt-in path retains the old skip-on-any-lock behavior.
|
|
58
|
+
- **`onAuthStateChange` async callbacks** that call `getUser`, `setSession`, or read the session from inside the callback are now safe on the default path (previously deadlocked through the lock). One residual hazard remains: calling `refreshSession` (or anything routing through `_callRefreshToken`) from inside a `TOKEN_REFRESHED` handler still deadlocks via `refreshingDeferred`. The `@deprecated` marker on the async overload is kept with its reason updated to point at this specific case.
|
|
59
|
+
- **Subscriber timing on the default path:** subscribers stay awaited; same as before. What changes is that `signOut` no longer waits for an in-flight refresh's HTTP and continuation to finish before its own fetch goes out. Both fetches now run concurrently, and the commit guard keeps storage consistent.
|
|
60
|
+
|
|
61
|
+
## Migration steps
|
|
62
|
+
|
|
63
|
+
### If you do not pass a custom `lock` (most users)
|
|
64
|
+
|
|
65
|
+
No action required for v2. No action required for v3.
|
|
66
|
+
|
|
67
|
+
### If you pass a custom `lock` (e.g., React Native `processLock`)
|
|
68
|
+
|
|
69
|
+
No action required for v2 — your lock continues to work.
|
|
70
|
+
|
|
71
|
+
For v3 readiness:
|
|
72
|
+
|
|
73
|
+
1. Remove `lock` and `lockAcquireTimeout` from your `createClient` / `GoTrueClient` options before upgrading to v3.
|
|
74
|
+
2. If you depended on cross-process serialization (e.g., Node multi-process with shared AsyncStorage), validate that the lockless coordination (in-tab single-flight + server parent-of-active) is sufficient for your runtime. The default is safe for the cases the lock was originally added to handle (cross-tab refresh races), since the server resolves them.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// Before (v2.x, still works):
|
|
78
|
+
const supabase = createClient(URL, KEY, {
|
|
79
|
+
auth: { lock: processLock, lockAcquireTimeout: 5000 },
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// After (v3-ready):
|
|
83
|
+
const supabase = createClient(URL, KEY)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Reference
|
|
87
|
+
|
|
88
|
+
- Server-side parent-of-active mechanism: `internal/tokens/service.go:376-385` in the [supabase/auth](https://github.com/supabase/auth) repo (v1 branch, the `*models.RefreshToken` type assertion). When a request arrives with a revoked refresh token whose child is the currently-active token, the server returns the active token instead of rejecting — both tabs receive the same rotated token under DB row locking.
|
|
89
|
+
- `lib/locks.ts` exports (`navigatorLock`, `processLock`, `LockAcquireTimeoutError`, `NavigatorLockAcquireTimeoutError`, `ProcessLockAcquireTimeoutError`, `internals`) remain available for direct imports, marked `@deprecated`. Direct callers who use these exports outside the `GoTrueClient` constructor option are unaffected.
|
package/package.json
CHANGED
package/src/GoTrueAdminApi.ts
CHANGED
|
@@ -65,14 +65,6 @@ export default class GoTrueAdminApi {
|
|
|
65
65
|
protected headers: {
|
|
66
66
|
[key: string]: string
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
private _encodePathSegment(segment: string): string {
|
|
70
|
-
if (segment === '.' || segment === '..') {
|
|
71
|
-
throw new AuthError('Invalid path segment')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return encodeURIComponent(segment)
|
|
75
|
-
}
|
|
76
68
|
protected fetch: Fetch
|
|
77
69
|
protected experimental: ExperimentalFeatureFlags
|
|
78
70
|
|
|
@@ -990,18 +982,12 @@ export default class GoTrueAdminApi {
|
|
|
990
982
|
*/
|
|
991
983
|
private async _getOAuthClient(clientId: string): Promise<OAuthClientResponse> {
|
|
992
984
|
try {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
headers: this.headers,
|
|
1000
|
-
xform: (client: any) => {
|
|
1001
|
-
return { data: client, error: null }
|
|
1002
|
-
},
|
|
1003
|
-
}
|
|
1004
|
-
)
|
|
985
|
+
return await _request(this.fetch, 'GET', `${this.url}/admin/oauth/clients/${clientId}`, {
|
|
986
|
+
headers: this.headers,
|
|
987
|
+
xform: (client: any) => {
|
|
988
|
+
return { data: client, error: null }
|
|
989
|
+
},
|
|
990
|
+
})
|
|
1005
991
|
} catch (error) {
|
|
1006
992
|
if (isAuthError(error)) {
|
|
1007
993
|
return { data: null, error }
|
|
@@ -1022,19 +1008,13 @@ export default class GoTrueAdminApi {
|
|
|
1022
1008
|
params: UpdateOAuthClientParams
|
|
1023
1009
|
): Promise<OAuthClientResponse> {
|
|
1024
1010
|
try {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
this.
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
headers: this.headers,
|
|
1033
|
-
xform: (client: any) => {
|
|
1034
|
-
return { data: client, error: null }
|
|
1035
|
-
},
|
|
1036
|
-
}
|
|
1037
|
-
)
|
|
1011
|
+
return await _request(this.fetch, 'PUT', `${this.url}/admin/oauth/clients/${clientId}`, {
|
|
1012
|
+
body: params,
|
|
1013
|
+
headers: this.headers,
|
|
1014
|
+
xform: (client: any) => {
|
|
1015
|
+
return { data: client, error: null }
|
|
1016
|
+
},
|
|
1017
|
+
})
|
|
1038
1018
|
} catch (error) {
|
|
1039
1019
|
if (isAuthError(error)) {
|
|
1040
1020
|
return { data: null, error }
|
|
@@ -1054,8 +1034,7 @@ export default class GoTrueAdminApi {
|
|
|
1054
1034
|
clientId: string
|
|
1055
1035
|
): Promise<{ data: null; error: AuthError | null }> {
|
|
1056
1036
|
try {
|
|
1057
|
-
|
|
1058
|
-
await _request(this.fetch, 'DELETE', `${this.url}/admin/oauth/clients/${encodedClientId}`, {
|
|
1037
|
+
await _request(this.fetch, 'DELETE', `${this.url}/admin/oauth/clients/${clientId}`, {
|
|
1059
1038
|
headers: this.headers,
|
|
1060
1039
|
noResolveJson: true,
|
|
1061
1040
|
})
|
|
@@ -1077,11 +1056,10 @@ export default class GoTrueAdminApi {
|
|
|
1077
1056
|
*/
|
|
1078
1057
|
private async _regenerateOAuthClientSecret(clientId: string): Promise<OAuthClientResponse> {
|
|
1079
1058
|
try {
|
|
1080
|
-
const encodedClientId = this._encodePathSegment(clientId)
|
|
1081
1059
|
return await _request(
|
|
1082
1060
|
this.fetch,
|
|
1083
1061
|
'POST',
|
|
1084
|
-
`${this.url}/admin/oauth/clients/${
|
|
1062
|
+
`${this.url}/admin/oauth/clients/${clientId}/regenerate_secret`,
|
|
1085
1063
|
{
|
|
1086
1064
|
headers: this.headers,
|
|
1087
1065
|
xform: (client: any) => {
|
|
@@ -1163,18 +1141,12 @@ export default class GoTrueAdminApi {
|
|
|
1163
1141
|
*/
|
|
1164
1142
|
private async _getCustomProvider(identifier: string): Promise<CustomProviderResponse> {
|
|
1165
1143
|
try {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
headers: this.headers,
|
|
1173
|
-
xform: (provider: any) => {
|
|
1174
|
-
return { data: provider, error: null }
|
|
1175
|
-
},
|
|
1176
|
-
}
|
|
1177
|
-
)
|
|
1144
|
+
return await _request(this.fetch, 'GET', `${this.url}/admin/custom-providers/${identifier}`, {
|
|
1145
|
+
headers: this.headers,
|
|
1146
|
+
xform: (provider: any) => {
|
|
1147
|
+
return { data: provider, error: null }
|
|
1148
|
+
},
|
|
1149
|
+
})
|
|
1178
1150
|
} catch (error) {
|
|
1179
1151
|
if (isAuthError(error)) {
|
|
1180
1152
|
return { data: null, error }
|
|
@@ -1198,19 +1170,13 @@ export default class GoTrueAdminApi {
|
|
|
1198
1170
|
params: UpdateCustomProviderParams
|
|
1199
1171
|
): Promise<CustomProviderResponse> {
|
|
1200
1172
|
try {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
this.
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
headers: this.headers,
|
|
1209
|
-
xform: (provider: any) => {
|
|
1210
|
-
return { data: provider, error: null }
|
|
1211
|
-
},
|
|
1212
|
-
}
|
|
1213
|
-
)
|
|
1173
|
+
return await _request(this.fetch, 'PUT', `${this.url}/admin/custom-providers/${identifier}`, {
|
|
1174
|
+
body: params,
|
|
1175
|
+
headers: this.headers,
|
|
1176
|
+
xform: (provider: any) => {
|
|
1177
|
+
return { data: provider, error: null }
|
|
1178
|
+
},
|
|
1179
|
+
})
|
|
1214
1180
|
} catch (error) {
|
|
1215
1181
|
if (isAuthError(error)) {
|
|
1216
1182
|
return { data: null, error }
|
|
@@ -1228,16 +1194,10 @@ export default class GoTrueAdminApi {
|
|
|
1228
1194
|
identifier: string
|
|
1229
1195
|
): Promise<{ data: null; error: AuthError | null }> {
|
|
1230
1196
|
try {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
`${this.url}/admin/custom-providers/${encodedIdentifier}`,
|
|
1236
|
-
{
|
|
1237
|
-
headers: this.headers,
|
|
1238
|
-
noResolveJson: true,
|
|
1239
|
-
}
|
|
1240
|
-
)
|
|
1197
|
+
await _request(this.fetch, 'DELETE', `${this.url}/admin/custom-providers/${identifier}`, {
|
|
1198
|
+
headers: this.headers,
|
|
1199
|
+
noResolveJson: true,
|
|
1200
|
+
})
|
|
1241
1201
|
return { data: null, error: null }
|
|
1242
1202
|
} catch (error) {
|
|
1243
1203
|
if (isAuthError(error)) {
|