kitcn 0.33.6 → 0.33.8
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/CHANGELOG.md +28 -0
- package/dist/aggregate/index.d.ts +1 -1
- package/dist/auth/client/index.d.ts +45 -21
- package/dist/auth/client/index.js +203 -111
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/index.d.ts +16 -16
- package/dist/auth/nextjs/index.d.ts +3 -3
- package/dist/auth/nextjs/index.js +3 -2
- package/dist/auth/start/index.js +134 -2
- package/dist/auth/start/server/index.d.ts +1 -1
- package/dist/auth/start/server/index.js +1 -1
- package/dist/crpc/index.d.ts +2 -2
- package/dist/crpc/index.js +2 -2
- package/dist/{generated-contract-disabled-PdNGvNYP.d.ts → generated-contract-disabled-CFLQf8Wg.d.ts} +25 -25
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/migrations/index.d.ts +1 -1
- package/dist/{query-options-DGG93--5.js → query-options-YkFvau71.js} +6 -2
- package/dist/react/index.js +149 -140
- package/dist/rsc/index.d.ts +1 -1
- package/dist/rsc/index.js +1 -1
- package/dist/solid/index.js +4 -1
- package/dist/{token-tlbNKQS0.d.ts → token-COZD00dG.d.ts} +2 -1
- package/dist/{token-CgIvcEZX.js → token-CZbuEcTL.js} +13 -4
- package/dist/{auth-store-x3_wvhTR.js → token-gate-CEzbXArg.js} +178 -1
- package/dist/{types-C7JkpfeY.d.ts → types-Bb0kwteA.d.ts} +5 -1
- package/dist/{where-clause-compiler-BzAJnWt4.d.ts → where-clause-compiler-CTblNxLo.d.ts} +24 -24
- package/package.json +1 -1
- package/skills/kitcn/references/features/auth.md +26 -8
- package/skills/kitcn/references/features/react.md +5 -1
- package/skills/kitcn/references/setup/next.md +48 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# kitcn
|
|
2
2
|
|
|
3
|
+
## 0.33.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#478](https://github.com/udecode/kitcn/pull/478) [`03b1ad1`](https://github.com/udecode/kitcn/commit/03b1ad1bd72333ce3867f79d8f079193cf77c170) Thanks [@zbeyens](https://github.com/zbeyens)! - ## Patches
|
|
8
|
+
|
|
9
|
+
- Support app-owned synchronous or asynchronous JWT cache clocks for Next.js partial prefetching and private-cache rendering.
|
|
10
|
+
|
|
11
|
+
## 0.33.7
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#475](https://github.com/udecode/kitcn/pull/475) [`9e513f8`](https://github.com/udecode/kitcn/commit/9e513f85d4742efa041840c45989dfd3e96c883e) Thanks [@EfficiencyCorp](https://github.com/EfficiencyCorp)! - ## Patches
|
|
16
|
+
|
|
17
|
+
- Fix `onTokenIdentityChange` letting a token of another user or session into
|
|
18
|
+
the auth store, Convex, cRPC HTTP headers or the TanStack Start loader: the
|
|
19
|
+
SSR token, restored sessions, sign-in tokens, JWTs without `exp` and
|
|
20
|
+
concurrent first tokens are all held to the page's identity and every
|
|
21
|
+
mounted provider's current `tokenIdentityBaseline` getter, including changes
|
|
22
|
+
made by `onTokenIdentityAdmitted` before publication.
|
|
23
|
+
- Fix an identity guard trip staying local to one provider: every mounted
|
|
24
|
+
provider now hands out no token and publishes unauthenticated, each guarded
|
|
25
|
+
one closes its client and calls `onTokenIdentityChange` once, including a
|
|
26
|
+
guard enabled after the trip, and sign-in
|
|
27
|
+
mutations fail with `TOKEN_IDENTITY_CHANGED` until the page reloads.
|
|
28
|
+
- Fix the `optimisticAuth` window reopening after Convex refused a token: it
|
|
29
|
+
now ends at the Convex client's first auth result.
|
|
30
|
+
|
|
3
31
|
## 0.33.6
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Vn as ConvexTextBuilderInitial, Zt as ConvexTableWithColumns } from "../capabilities-Bxzoofl4.js";
|
|
2
|
-
import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-
|
|
2
|
+
import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-CTblNxLo.js";
|
|
3
3
|
import * as convex_values0 from "convex/values";
|
|
4
4
|
import { GenericId, Infer, Value } from "convex/values";
|
|
5
5
|
import { DocumentByName, GenericDataModel, GenericDatabaseReader, GenericDatabaseWriter, TableNamesInDataModel } from "convex/server";
|
|
@@ -400,21 +400,42 @@ type ConvexAuthProviderProps = {
|
|
|
400
400
|
isUnauthorized?: (error: unknown) => boolean;
|
|
401
401
|
/**
|
|
402
402
|
* Run auth-bound queries as soon as an unexpired JWT is held instead of
|
|
403
|
-
* after Convex confirms it
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
403
|
+
* after Convex confirms it, until the Convex client reports its first auth
|
|
404
|
+
* result (confirmed or refused); after that the gate follows Convex's
|
|
405
|
+
* confirmed state for the client's lifetime, remounts included. Convex
|
|
406
|
+
* sends them after Authenticate on the same socket and evaluates none of
|
|
407
|
+
* them if the token is refused; a refused token sets `isAuthenticated` back
|
|
408
|
+
* to false, which resets auth-bound queries. With the identity guard, only
|
|
409
|
+
* a token the guard admits opens it. No effect over a client the TanStack
|
|
410
|
+
* Start loader already authenticated. Every provider over one Convex client
|
|
411
|
+
* must use the same setting: mixing optimistic and non-optimistic providers
|
|
412
|
+
* over one client is unsupported, because results the client reports
|
|
413
|
+
* before an optimistic provider mounts are not seen. Default `false`.
|
|
407
414
|
*/
|
|
408
415
|
optimisticAuth?: boolean;
|
|
409
416
|
/**
|
|
410
417
|
* Fix the identity (JWT `sub` and `sessionId`) the document speaks for: the
|
|
411
418
|
* one it holds when it mounts (`initialToken`), or, with none, the first
|
|
412
|
-
* token
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
419
|
+
* token that carries one. Guarantees: (1) a token of another user or
|
|
420
|
+
* session (or a JWT without an identity once one is established) is never
|
|
421
|
+
* cached, published, or handed to Convex, cRPC HTTP or the Start loader;
|
|
422
|
+
* (2) when a trip happens (this or another provider, or the Start loader,
|
|
423
|
+
* refuses one), this provider stops handing out tokens (null), publishes
|
|
424
|
+
* unauthenticated, calls `client.close()` (Convex's close semantics govern
|
|
425
|
+
* its queued work) and then this, once, where the app should reload the
|
|
426
|
+
* page. A provider that mounts (or shows again) on a page that is already
|
|
427
|
+
* tripped starts tripped and does the same, once, so a trip that happened
|
|
428
|
+
* while no guarded provider was mounted still reaches the app.
|
|
429
|
+
* The trip is page-wide in the browser (never on the server); sign-in
|
|
430
|
+
* mutations fail with `AuthMutationError` code `TOKEN_IDENTITY_CHANGED`
|
|
431
|
+
* until the reload. It governs the token kitcn supplies, not an
|
|
432
|
+
* `Authorization` header the app sets itself. Same-session refreshes pass
|
|
433
|
+
* through. Off when not set: a page that never enables it is unchanged.
|
|
434
|
+
* Once a guarded provider establishes the page identity, it persists until
|
|
435
|
+
* the reload, binding every provider (with or without this) and the Start
|
|
436
|
+
* loader, even after that provider unmounts. Two kitcn versions or
|
|
437
|
+
* revisions on one page (dev HMR across revisions included) are
|
|
438
|
+
* unsupported: they share no page identity until the reload.
|
|
418
439
|
*/
|
|
419
440
|
onTokenIdentityChange?: () => void;
|
|
420
441
|
/**
|
|
@@ -426,20 +447,23 @@ type ConvexAuthProviderProps = {
|
|
|
426
447
|
* without a token still refuses another user's or session's token.
|
|
427
448
|
*
|
|
428
449
|
* A fixed value is read on the first render only. A getter is read at
|
|
429
|
-
* every admission, cached tokens included, and
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
450
|
+
* every admission, cached tokens included, and while this provider is
|
|
451
|
+
* mounted it binds every provider's admissions. A token must match all of:
|
|
452
|
+
* the page identity (the first one a provider knew), this provider's
|
|
453
|
+
* baseline and admitted identity, and every mounted getter's current
|
|
454
|
+
* answer; any mismatch trips the page. So a provider kept mounted but
|
|
455
|
+
* hidden (React `<Activity>`) cannot resume under an identity the document
|
|
456
|
+
* has since moved away from. A getter returning null does not constrain.
|
|
457
|
+
* Needs `onTokenIdentityChange`.
|
|
435
458
|
*/
|
|
436
459
|
tokenIdentityBaseline?: string | null | (() => string | null);
|
|
437
460
|
/**
|
|
438
|
-
* Called synchronously with every token the identity guard admits
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
461
|
+
* Called synchronously with every token the identity guard admits, before
|
|
462
|
+
* Convex or HTTP headers receive it: a fresh token when it is admitted and
|
|
463
|
+
* cached, a cached token each time it is handed out. The document can claim
|
|
464
|
+
* the identity at that moment (for example the first token a document
|
|
465
|
+
* without one obtains). Never called for a refused token. Read from a ref,
|
|
466
|
+
* so passing a new function does not re-run effects. Needs
|
|
443
467
|
* `onTokenIdentityChange`.
|
|
444
468
|
*/
|
|
445
469
|
onTokenIdentityAdmitted?: (token: string) => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { C as FetchAccessTokenContext, D as isSessionSyncGraceActive, H as readAuthSessionFallbackData, K as CRPCClientError, M as useAuthValue, S as ConvexProviderWithAuth$1, U as readAuthSessionFallbackToken, V as clearAuthSessionFallback, W as writeAuthSessionFallbackData, _ as AUTH_SESSION_SYNC_GRACE_MS, a as admitToken, c as identityGuardInPlay, d as isTokenAdmissible, f as joinPage, g as tripDocument, h as subscribeDocumentTrip, i as publishToken, j as useAuthStore, l as isDocumentTripped, m as pageRegistry, n as publishAuthState, o as attachStoreGuard, p as mountGuard, q as defaultIsUnauthorized, s as decodeTokenSubjectSessionIdentity, t as admitStoreToken, u as isJwt, v as AuthProvider, z as decodeJwtExp } from "../../token-gate-CEzbXArg.js";
|
|
3
3
|
import { useConvexAuth } from "convex/react";
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
|
|
7
7
|
//#region src/auth/internal/convex-client.ts
|
|
@@ -12,6 +12,39 @@ const convexClient = () => {
|
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/auth-client/client-settlement.ts
|
|
17
|
+
function watchClientSettlement(client) {
|
|
18
|
+
const registry = pageRegistry();
|
|
19
|
+
if (!registry || registry.watchedClients.has(client)) return;
|
|
20
|
+
registry.watchedClients.add(client);
|
|
21
|
+
const setAuth = client.setAuth.bind(client);
|
|
22
|
+
client.setAuth = (fetchToken, onChange, onRefreshChange) => setAuth(fetchToken, (isAuthenticated) => {
|
|
23
|
+
settleClient(client);
|
|
24
|
+
onChange?.(isAuthenticated);
|
|
25
|
+
}, onRefreshChange);
|
|
26
|
+
}
|
|
27
|
+
function settleClient(client) {
|
|
28
|
+
const registry = pageRegistry();
|
|
29
|
+
if (!registry || registry.settledClients.has(client)) return;
|
|
30
|
+
registry.settledClients.add(client);
|
|
31
|
+
for (const listener of [...registry.settlementListeners.get(client) ?? []]) listener();
|
|
32
|
+
}
|
|
33
|
+
const isClientSettled = (client) => pageRegistry()?.settledClients.has(client) ?? false;
|
|
34
|
+
const subscribeClientSettlement = (client, listener) => {
|
|
35
|
+
const registry = pageRegistry();
|
|
36
|
+
if (!registry) return () => {};
|
|
37
|
+
let clientListeners = registry.settlementListeners.get(client);
|
|
38
|
+
if (!clientListeners) {
|
|
39
|
+
clientListeners = /* @__PURE__ */ new Set();
|
|
40
|
+
registry.settlementListeners.set(client, clientListeners);
|
|
41
|
+
}
|
|
42
|
+
clientListeners.add(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
clientListeners.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
15
48
|
//#endregion
|
|
16
49
|
//#region src/auth-client/convex-auth-provider.tsx
|
|
17
50
|
const defaultMutationHandler = () => {
|
|
@@ -136,11 +169,59 @@ const clearSessionAtom = (authClient) => {
|
|
|
136
169
|
*/
|
|
137
170
|
function ConvexAuthProvider({ children, client, authClient, convexQueryClient, initialToken, onMutationUnauthorized, onQueryUnauthorized, isUnauthorized, optimisticAuth = false, onTokenIdentityChange, onTokenIdentityAdmitted, tokenIdentityBaseline }) {
|
|
138
171
|
useOTTHandler(authClient);
|
|
172
|
+
useMemo(() => {
|
|
173
|
+
if (optimisticAuth) watchClientSettlement(client);
|
|
174
|
+
}, [client, optimisticAuth]);
|
|
175
|
+
const baselineRef = useRef(tokenIdentityBaseline);
|
|
176
|
+
baselineRef.current = tokenIdentityBaseline;
|
|
177
|
+
const onAdmittedRef = useRef(onTokenIdentityAdmitted);
|
|
178
|
+
onAdmittedRef.current = onTokenIdentityAdmitted;
|
|
179
|
+
const [{ guard, inheritedTrip, refusedInitialToken }] = useState(() => {
|
|
180
|
+
const guarded = onTokenIdentityChange !== void 0;
|
|
181
|
+
const getter = typeof tokenIdentityBaseline === "function";
|
|
182
|
+
const fixed = getter ? null : tokenIdentityBaseline ?? null;
|
|
183
|
+
const created = {
|
|
184
|
+
baseline: () => {
|
|
185
|
+
if (!created.guarded) return null;
|
|
186
|
+
return created.hasGetter ? resolveTokenIdentityBaseline(baselineRef.current) : fixed;
|
|
187
|
+
},
|
|
188
|
+
guarded,
|
|
189
|
+
hasGetter: guarded && getter,
|
|
190
|
+
heldToken: () => null,
|
|
191
|
+
identity: guarded ? fixed : null,
|
|
192
|
+
onAdmitted: (token) => onAdmittedRef.current?.(token),
|
|
193
|
+
tripped: isDocumentTripped()
|
|
194
|
+
};
|
|
195
|
+
const refused = !created.tripped && !!initialToken && !isTokenAdmissible(initialToken, created, "hold");
|
|
196
|
+
if (refused) created.tripped = true;
|
|
197
|
+
else if (guarded) created.identity ??= decodeTokenSubjectSessionIdentity(initialToken ?? null);
|
|
198
|
+
return {
|
|
199
|
+
guard: created,
|
|
200
|
+
inheritedTrip: isDocumentTripped(),
|
|
201
|
+
refusedInitialToken: refused
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
useLayoutEffect(() => {
|
|
205
|
+
const guarded = onTokenIdentityChange !== void 0;
|
|
206
|
+
guard.hasGetter = guarded && typeof tokenIdentityBaseline === "function";
|
|
207
|
+
if (guarded === guard.guarded) return;
|
|
208
|
+
guard.guarded = guarded;
|
|
209
|
+
if (!guarded) return;
|
|
210
|
+
guard.identity ??= (guard.hasGetter ? null : guard.baseline()) ?? decodeTokenSubjectSessionIdentity(guard.heldToken());
|
|
211
|
+
joinPage(guard);
|
|
212
|
+
});
|
|
139
213
|
return /* @__PURE__ */ jsx(AuthProvider, {
|
|
140
|
-
initialValues: useMemo(() =>
|
|
214
|
+
initialValues: useMemo(() => refusedInitialToken || inheritedTrip ? {
|
|
215
|
+
expiresAt: null,
|
|
216
|
+
token: null
|
|
217
|
+
} : {
|
|
141
218
|
expiresAt: initialToken ? decodeJwtExp(initialToken) : null,
|
|
142
219
|
token: initialToken ?? null
|
|
143
|
-
}
|
|
220
|
+
}, [
|
|
221
|
+
initialToken,
|
|
222
|
+
inheritedTrip,
|
|
223
|
+
refusedInitialToken
|
|
224
|
+
]),
|
|
144
225
|
isUnauthorized: isUnauthorized ?? defaultIsUnauthorized,
|
|
145
226
|
onMutationUnauthorized: onMutationUnauthorized ?? defaultMutationHandler,
|
|
146
227
|
onQueryUnauthorized: onQueryUnauthorized ?? (() => {}),
|
|
@@ -148,10 +229,11 @@ function ConvexAuthProvider({ children, client, authClient, convexQueryClient, i
|
|
|
148
229
|
authClient,
|
|
149
230
|
client,
|
|
150
231
|
convexQueryClient,
|
|
151
|
-
|
|
232
|
+
guard,
|
|
233
|
+
inheritedTrip,
|
|
152
234
|
onTokenIdentityChange,
|
|
153
235
|
optimisticAuth,
|
|
154
|
-
|
|
236
|
+
refusedInitialToken,
|
|
155
237
|
children
|
|
156
238
|
})
|
|
157
239
|
});
|
|
@@ -160,7 +242,7 @@ function ConvexAuthProvider({ children, client, authClient, convexQueryClient, i
|
|
|
160
242
|
* Inner provider that has access to AuthStore via useAuthStore().
|
|
161
243
|
* Creates fetchAccessToken and passes it through context (no race condition).
|
|
162
244
|
*/
|
|
163
|
-
function ConvexAuthProviderInner({ children, client, authClient, convexQueryClient, optimisticAuth, onTokenIdentityChange,
|
|
245
|
+
function ConvexAuthProviderInner({ children, client, authClient, convexQueryClient, guard, inheritedTrip, optimisticAuth, onTokenIdentityChange, refusedInitialToken }) {
|
|
164
246
|
const authStore = useAuthStore();
|
|
165
247
|
convexQueryClient?.updateAuthStore(authStore);
|
|
166
248
|
const { data: session, isPending } = authClient.useSession();
|
|
@@ -171,13 +253,14 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
171
253
|
const isMountedRef = useRef(false);
|
|
172
254
|
sessionRef.current = session;
|
|
173
255
|
isPendingRef.current = isPending;
|
|
256
|
+
const isOpaqueUnderGuard = useCallback((token) => identityGuardInPlay(guard) && !isJwt(token), [guard]);
|
|
174
257
|
const getCachedJwt = useCallback((minTimeRemainingMs = 0) => {
|
|
175
258
|
const cachedToken = authStore.get("token");
|
|
176
|
-
if (!cachedToken) return null;
|
|
259
|
+
if (!cachedToken || isOpaqueUnderGuard(cachedToken)) return null;
|
|
177
260
|
const expiresAt = decodeJwtExp(cachedToken);
|
|
178
261
|
if (expiresAt === null || expiresAt <= Date.now() + minTimeRemainingMs) return null;
|
|
179
262
|
return cachedToken;
|
|
180
|
-
}, [authStore]);
|
|
263
|
+
}, [authStore, isOpaqueUnderGuard]);
|
|
181
264
|
useEffect(() => {
|
|
182
265
|
if (hasActiveSessionData(session)) {
|
|
183
266
|
authStore.set("sessionSyncGraceUntil", null);
|
|
@@ -194,6 +277,62 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
194
277
|
isPending,
|
|
195
278
|
authStore
|
|
196
279
|
]);
|
|
280
|
+
const onTokenIdentityChangeRef = useRef(onTokenIdentityChange);
|
|
281
|
+
onTokenIdentityChangeRef.current = onTokenIdentityChange;
|
|
282
|
+
attachStoreGuard(authStore.store, guard);
|
|
283
|
+
const [guardTripped, setGuardTripped] = useState(refusedInitialToken || inheritedTrip);
|
|
284
|
+
const quarantinedRef = useRef(false);
|
|
285
|
+
const tripNotifiedRef = useRef(false);
|
|
286
|
+
const announcedTokenRef = useRef(null);
|
|
287
|
+
const quarantine = useCallback(() => {
|
|
288
|
+
guard.tripped = true;
|
|
289
|
+
authStore.set("token", null);
|
|
290
|
+
authStore.set("expiresAt", null);
|
|
291
|
+
authStore.set("sessionSyncGraceUntil", null);
|
|
292
|
+
authStore.set("isAuthenticated", false);
|
|
293
|
+
authStore.set("isLoading", false);
|
|
294
|
+
setGuardTripped(true);
|
|
295
|
+
}, [authStore, guard]);
|
|
296
|
+
const tripGuard = useCallback(() => {
|
|
297
|
+
guard.tripped = true;
|
|
298
|
+
if (!quarantinedRef.current) {
|
|
299
|
+
quarantinedRef.current = true;
|
|
300
|
+
quarantine();
|
|
301
|
+
}
|
|
302
|
+
tripDocument();
|
|
303
|
+
if (tripNotifiedRef.current || !onTokenIdentityChangeRef.current) return;
|
|
304
|
+
tripNotifiedRef.current = true;
|
|
305
|
+
try {
|
|
306
|
+
Promise.resolve(client.close()).catch(() => {});
|
|
307
|
+
} catch {}
|
|
308
|
+
try {
|
|
309
|
+
onTokenIdentityChangeRef.current?.();
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error("[ConvexAuthProvider] onTokenIdentityChange threw", error);
|
|
312
|
+
}
|
|
313
|
+
}, [
|
|
314
|
+
client,
|
|
315
|
+
guard,
|
|
316
|
+
quarantine
|
|
317
|
+
]);
|
|
318
|
+
useLayoutEffect(() => {
|
|
319
|
+
guard.heldToken = () => authStore.get("token");
|
|
320
|
+
const unsubscribe = subscribeDocumentTrip(tripGuard);
|
|
321
|
+
const unmount = mountGuard(guard);
|
|
322
|
+
if (refusedInitialToken || isDocumentTripped()) tripGuard();
|
|
323
|
+
return () => {
|
|
324
|
+
unmount();
|
|
325
|
+
unsubscribe();
|
|
326
|
+
};
|
|
327
|
+
}, [
|
|
328
|
+
authStore,
|
|
329
|
+
guard,
|
|
330
|
+
refusedInitialToken,
|
|
331
|
+
tripGuard
|
|
332
|
+
]);
|
|
333
|
+
useLayoutEffect(() => {
|
|
334
|
+
if (onTokenIdentityChange && isDocumentTripped()) tripGuard();
|
|
335
|
+
}, [onTokenIdentityChange, tripGuard]);
|
|
197
336
|
useEffect(() => {
|
|
198
337
|
isMountedRef.current = true;
|
|
199
338
|
return () => {
|
|
@@ -207,9 +346,11 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
207
346
|
restoredTokenRef.current = persistedToken;
|
|
208
347
|
const persistedSessionData = readAuthSessionFallbackData();
|
|
209
348
|
const graceUntil = Date.now() + AUTH_SESSION_SYNC_GRACE_MS;
|
|
210
|
-
authStore
|
|
211
|
-
|
|
212
|
-
|
|
349
|
+
if (!publishToken(authStore, persistedToken, {
|
|
350
|
+
announce: true,
|
|
351
|
+
sessionSyncGraceUntil: graceUntil,
|
|
352
|
+
use: "restore"
|
|
353
|
+
})) return;
|
|
213
354
|
if (persistedSessionData) syncSessionAtom(authClient, persistedSessionData);
|
|
214
355
|
const ownsToken = () => authStore.get("token") === persistedToken;
|
|
215
356
|
const shouldStop = () => !isMountedRef.current || !ownsToken();
|
|
@@ -243,42 +384,23 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
243
384
|
authStore,
|
|
244
385
|
authClient
|
|
245
386
|
]);
|
|
246
|
-
const onTokenIdentityChangeRef = useRef(onTokenIdentityChange);
|
|
247
|
-
onTokenIdentityChangeRef.current = onTokenIdentityChange;
|
|
248
|
-
const onTokenIdentityAdmittedRef = useRef(onTokenIdentityAdmitted);
|
|
249
|
-
onTokenIdentityAdmittedRef.current = onTokenIdentityAdmitted;
|
|
250
|
-
const tokenIdentityBaselineRef = useRef(tokenIdentityBaseline);
|
|
251
|
-
tokenIdentityBaselineRef.current = tokenIdentityBaseline;
|
|
252
|
-
const identityGuardRef = useRef(null);
|
|
253
|
-
identityGuardRef.current ??= {
|
|
254
|
-
identity: (typeof tokenIdentityBaseline === "function" ? null : resolveTokenIdentityBaseline(tokenIdentityBaseline)) ?? decodeTokenSubjectSessionIdentity(authStore.get("token")),
|
|
255
|
-
currentDocumentIdentity: typeof tokenIdentityBaseline === "function" ? () => resolveTokenIdentityBaseline(tokenIdentityBaselineRef.current) : null,
|
|
256
|
-
tripped: false
|
|
257
|
-
};
|
|
258
|
-
const admitToken = useCallback((token, onAdmitted) => guardTokenIdentity({
|
|
259
|
-
client,
|
|
260
|
-
guard: identityGuardRef.current,
|
|
261
|
-
onAdmitted,
|
|
262
|
-
onIdentityChange: onTokenIdentityChangeRef.current,
|
|
263
|
-
token
|
|
264
|
-
}), [client]);
|
|
265
387
|
const fetchAccessToken = useCallback(async ({ forceRefreshToken = false } = {}) => {
|
|
266
388
|
const fetchFreshToken = () => {
|
|
267
389
|
if (pendingTokenRef.current) return pendingTokenRef.current;
|
|
268
390
|
const cachedToken = authStore.get("token");
|
|
269
391
|
const fetchOptions = { throw: false };
|
|
270
|
-
if (cachedToken && decodeJwtExp(cachedToken) === null) {
|
|
392
|
+
if (cachedToken && (decodeJwtExp(cachedToken) === null || isOpaqueUnderGuard(cachedToken))) {
|
|
271
393
|
fetchOptions.credentials = "omit";
|
|
272
394
|
fetchOptions.headers = { Authorization: `Bearer ${cachedToken}` };
|
|
273
395
|
}
|
|
274
396
|
pendingTokenRef.current = authClient.convex.token({ fetchOptions }).then((result) => {
|
|
275
|
-
const jwt = result
|
|
276
|
-
if (jwt && !admitToken(jwt)) return null;
|
|
397
|
+
const jwt = result?.data?.token || null;
|
|
277
398
|
if (jwt) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
399
|
+
if (!publishToken(authStore, jwt, {
|
|
400
|
+
announce: true,
|
|
401
|
+
sessionSyncGraceUntil: null
|
|
402
|
+
})) return null;
|
|
403
|
+
announcedTokenRef.current = jwt;
|
|
282
404
|
return jwt;
|
|
283
405
|
}
|
|
284
406
|
const cachedJwt = getCachedJwt();
|
|
@@ -328,11 +450,7 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
328
450
|
return fetchFreshToken();
|
|
329
451
|
}
|
|
330
452
|
const freshToken = await fetchFreshTokenForced();
|
|
331
|
-
if (!freshToken && cachedJwt) {
|
|
332
|
-
authStore.set("token", cachedJwt);
|
|
333
|
-
authStore.set("expiresAt", decodeJwtExp(cachedJwt));
|
|
334
|
-
return cachedJwt;
|
|
335
|
-
}
|
|
453
|
+
if (!freshToken && cachedJwt) return publishToken(authStore, cachedJwt, { announce: false }) ? cachedJwt : null;
|
|
336
454
|
return freshToken;
|
|
337
455
|
}
|
|
338
456
|
authStore.set("token", null);
|
|
@@ -343,7 +461,7 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
343
461
|
const cachedToken = authStore.get("token");
|
|
344
462
|
const expiresAt = authStore.get("expiresAt");
|
|
345
463
|
const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
|
|
346
|
-
if (!forceRefreshToken && cachedToken && expiresAt && timeRemaining >= 6e4) return cachedToken;
|
|
464
|
+
if (!forceRefreshToken && cachedToken && expiresAt && timeRemaining >= 6e4 && !isOpaqueUnderGuard(cachedToken)) return cachedToken;
|
|
347
465
|
if (!forceRefreshToken && pendingTokenRef.current) return pendingTokenRef.current;
|
|
348
466
|
if (forceRefreshToken) return fetchFreshTokenForced();
|
|
349
467
|
return fetchFreshToken();
|
|
@@ -351,14 +469,20 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
351
469
|
authStore,
|
|
352
470
|
authClient,
|
|
353
471
|
getCachedJwt,
|
|
354
|
-
|
|
472
|
+
isOpaqueUnderGuard
|
|
355
473
|
]);
|
|
356
474
|
const guardedFetchAccessToken = useCallback(async (args = {}) => {
|
|
357
|
-
if (
|
|
475
|
+
if (isDocumentTripped()) return null;
|
|
358
476
|
const token = await fetchAccessToken(args);
|
|
359
|
-
if (!token
|
|
360
|
-
|
|
361
|
-
|
|
477
|
+
if (!token) return null;
|
|
478
|
+
const announced = announcedTokenRef.current === token;
|
|
479
|
+
if (announced) announcedTokenRef.current = null;
|
|
480
|
+
return admitToken(token, {
|
|
481
|
+
announce: !announced,
|
|
482
|
+
guard,
|
|
483
|
+
use: "handout"
|
|
484
|
+
}) ? token : null;
|
|
485
|
+
}, [fetchAccessToken, guard]);
|
|
362
486
|
const useAuth = useCallback(function useConvexAuthHook() {
|
|
363
487
|
const token = authStore.get("token");
|
|
364
488
|
const hasSession = hasActiveSessionData(sessionRef.current);
|
|
@@ -375,6 +499,8 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
375
499
|
client,
|
|
376
500
|
useAuth,
|
|
377
501
|
children: /* @__PURE__ */ jsx(AuthStateSync, {
|
|
502
|
+
client,
|
|
503
|
+
guardTripped,
|
|
378
504
|
optimisticAuth,
|
|
379
505
|
children
|
|
380
506
|
})
|
|
@@ -393,89 +519,55 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
|
|
|
393
519
|
* 5. Without defensive check, we'd sync { isLoading: false, isAuthenticated: false }
|
|
394
520
|
* 6. Queries would throw UNAUTHORIZED before token is validated
|
|
395
521
|
*/
|
|
396
|
-
function AuthStateSync({ children, optimisticAuth = false }) {
|
|
522
|
+
function AuthStateSync({ children, client, guardTripped, optimisticAuth = false }) {
|
|
397
523
|
const { isLoading: convexIsLoading, isAuthenticated } = useConvexAuth();
|
|
398
524
|
const authStore = useAuthStore();
|
|
399
525
|
const token = useAuthValue("token");
|
|
400
|
-
const
|
|
526
|
+
const subscribe = useCallback((listener) => subscribeClientSettlement(client, listener), [client]);
|
|
527
|
+
const getSettled = useCallback(() => isClientSettled(client), [client]);
|
|
528
|
+
const settled = useSyncExternalStore(subscribe, getSettled, getSettled);
|
|
401
529
|
useEffect(() => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
isAuthenticated,
|
|
405
|
-
optimisticAuth,
|
|
406
|
-
rejectedToken: rejectedTokenRef.current,
|
|
407
|
-
token
|
|
408
|
-
});
|
|
409
|
-
if (optimisticAuth && isTokenRejectedByConvex({
|
|
530
|
+
publishAuthState(authStore, resolveAuthGate({
|
|
531
|
+
admitHeldToken: (held) => admitStoreToken(authStore, held, { use: "hold" }),
|
|
410
532
|
convexIsLoading,
|
|
533
|
+
guardTripped: guardTripped || isDocumentTripped(),
|
|
411
534
|
isAuthenticated,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
authStore.set("isAuthenticated", gate.isAuthenticated);
|
|
535
|
+
optimisticWindow: optimisticAuth && !settled && !isClientSettled(client),
|
|
536
|
+
token: authStore.get("token")
|
|
537
|
+
}));
|
|
416
538
|
}, [
|
|
539
|
+
client,
|
|
417
540
|
convexIsLoading,
|
|
541
|
+
guardTripped,
|
|
418
542
|
isAuthenticated,
|
|
419
543
|
optimisticAuth,
|
|
544
|
+
settled,
|
|
420
545
|
token,
|
|
421
546
|
authStore
|
|
422
547
|
]);
|
|
423
548
|
return children;
|
|
424
549
|
}
|
|
425
|
-
function decodeTokenSubjectSessionIdentity(token) {
|
|
426
|
-
if (!token) return null;
|
|
427
|
-
try {
|
|
428
|
-
const segment = token.split(".")[1];
|
|
429
|
-
if (!segment) return null;
|
|
430
|
-
const payload = JSON.parse(atob(segment.replaceAll("-", "+").replaceAll("_", "/")));
|
|
431
|
-
if (typeof payload !== "object" || payload === null) return null;
|
|
432
|
-
const sub = "sub" in payload ? payload.sub : null;
|
|
433
|
-
const sessionId = "sessionId" in payload ? payload.sessionId : null;
|
|
434
|
-
if (typeof sub !== "string") return null;
|
|
435
|
-
return `${sub}|${typeof sessionId === "string" ? sessionId : ""}`;
|
|
436
|
-
} catch {
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
550
|
function resolveTokenIdentityBaseline(baseline) {
|
|
441
551
|
return typeof baseline === "function" ? baseline() : baseline ?? null;
|
|
442
552
|
}
|
|
443
|
-
function guardTokenIdentity({ client, guard, onAdmitted, onIdentityChange, token }) {
|
|
444
|
-
if (!onIdentityChange) return true;
|
|
445
|
-
if (guard.tripped) return false;
|
|
446
|
-
const identity = decodeTokenSubjectSessionIdentity(token);
|
|
447
|
-
if (identity === null) return true;
|
|
448
|
-
const current = guard.currentDocumentIdentity?.() ?? null;
|
|
449
|
-
if ((guard.identity === null || guard.identity === identity) && (current === null || current === identity)) {
|
|
450
|
-
guard.identity = identity;
|
|
451
|
-
onAdmitted?.(token);
|
|
452
|
-
return true;
|
|
453
|
-
}
|
|
454
|
-
guard.tripped = true;
|
|
455
|
-
client.close();
|
|
456
|
-
onIdentityChange();
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Convex has settled on "not authenticated" while a token is still held: the
|
|
461
|
-
* server refused it (or refused its refresh).
|
|
462
|
-
*/
|
|
463
|
-
function isTokenRejectedByConvex({ convexIsLoading, isAuthenticated, token }) {
|
|
464
|
-
return !!token && !convexIsLoading && !isAuthenticated;
|
|
465
|
-
}
|
|
466
553
|
/**
|
|
467
554
|
* The auth state published to the store, which is what every query gate reads.
|
|
468
555
|
*
|
|
469
556
|
* Without `optimisticAuth` this is the confirmed state: queries wait until
|
|
470
|
-
* Convex has confirmed the token. With it,
|
|
471
|
-
*
|
|
472
|
-
* auth-bound queries subscribe at once; Convex sends
|
|
473
|
-
* the same socket, and a refused token closes the
|
|
474
|
-
* evaluated. When Convex refuses the token,
|
|
475
|
-
* false, which resets auth-bound queries (see
|
|
557
|
+
* Convex has confirmed the token. With it, until the Convex client reports its
|
|
558
|
+
* first auth result, a held, unexpired JWT counts as authenticated while
|
|
559
|
+
* Convex confirms it, so auth-bound queries subscribe at once; Convex sends
|
|
560
|
+
* them after Authenticate on the same socket, and a refused token closes the
|
|
561
|
+
* socket before any of them is evaluated. When Convex refuses the token,
|
|
562
|
+
* `isAuthenticated` falls back to false, which resets auth-bound queries (see
|
|
563
|
+
* `CRPCProviderInner`), and the window is over: no token reopens it.
|
|
476
564
|
*/
|
|
477
|
-
function resolveAuthGate({ convexIsLoading,
|
|
478
|
-
if (
|
|
565
|
+
function resolveAuthGate({ admitHeldToken, convexIsLoading, guardTripped, isAuthenticated, optimisticWindow, token }) {
|
|
566
|
+
if (guardTripped || token !== null && !admitHeldToken(token)) return {
|
|
567
|
+
isAuthenticated: false,
|
|
568
|
+
isLoading: false
|
|
569
|
+
};
|
|
570
|
+
if (optimisticWindow && convexIsLoading && token !== null && isOptimisticToken(token)) return {
|
|
479
571
|
isAuthenticated: true,
|
|
480
572
|
isLoading: false
|
|
481
573
|
};
|
|
@@ -484,8 +576,8 @@ function resolveAuthGate({ convexIsLoading, isAuthenticated, optimisticAuth, rej
|
|
|
484
576
|
isLoading: convexIsLoading || !!token && !isAuthenticated
|
|
485
577
|
};
|
|
486
578
|
}
|
|
487
|
-
function isOptimisticToken(token
|
|
488
|
-
if (!token
|
|
579
|
+
function isOptimisticToken(token) {
|
|
580
|
+
if (!isJwt(token)) return false;
|
|
489
581
|
const expiresAt = decodeJwtExp(token);
|
|
490
582
|
return expiresAt !== null && expiresAt > Date.now();
|
|
491
583
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as GenericAuthTriggerHandlers, S as GenericAuthTriggerChange, T as defineAuth, b as GenericAuthBeforeResult, i as getGeneratedAuthDisabledReason, n as GeneratedAuthDisabledReasonKind, r as createDisabledAuthRuntime, t as AuthRuntime, w as GenericAuthTriggers, x as GenericAuthDefinition, y as BetterAuthOptionsWithoutDatabase } from "../../generated-contract-disabled-
|
|
1
|
+
import { C as GenericAuthTriggerHandlers, S as GenericAuthTriggerChange, T as defineAuth, b as GenericAuthBeforeResult, i as getGeneratedAuthDisabledReason, n as GeneratedAuthDisabledReasonKind, r as createDisabledAuthRuntime, t as AuthRuntime, w as GenericAuthTriggers, x as GenericAuthDefinition, y as BetterAuthOptionsWithoutDatabase } from "../../generated-contract-disabled-CFLQf8Wg.js";
|
|
2
2
|
export { type AuthRuntime, BetterAuthOptionsWithoutDatabase, type GeneratedAuthDisabledReasonKind, GenericAuthBeforeResult, GenericAuthDefinition, GenericAuthTriggerChange, GenericAuthTriggerHandlers, GenericAuthTriggers, createDisabledAuthRuntime, defineAuth, getGeneratedAuthDisabledReason };
|