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.
Files changed (30) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/aggregate/index.d.ts +1 -1
  3. package/dist/auth/client/index.d.ts +45 -21
  4. package/dist/auth/client/index.js +203 -111
  5. package/dist/auth/generated/index.d.ts +1 -1
  6. package/dist/auth/index.d.ts +16 -16
  7. package/dist/auth/nextjs/index.d.ts +3 -3
  8. package/dist/auth/nextjs/index.js +3 -2
  9. package/dist/auth/start/index.js +134 -2
  10. package/dist/auth/start/server/index.d.ts +1 -1
  11. package/dist/auth/start/server/index.js +1 -1
  12. package/dist/crpc/index.d.ts +2 -2
  13. package/dist/crpc/index.js +2 -2
  14. package/dist/{generated-contract-disabled-PdNGvNYP.d.ts → generated-contract-disabled-CFLQf8Wg.d.ts} +25 -25
  15. package/dist/orm/index.d.ts +1 -1
  16. package/dist/orm/migrations/index.d.ts +1 -1
  17. package/dist/{query-options-DGG93--5.js → query-options-YkFvau71.js} +6 -2
  18. package/dist/react/index.js +149 -140
  19. package/dist/rsc/index.d.ts +1 -1
  20. package/dist/rsc/index.js +1 -1
  21. package/dist/solid/index.js +4 -1
  22. package/dist/{token-tlbNKQS0.d.ts → token-COZD00dG.d.ts} +2 -1
  23. package/dist/{token-CgIvcEZX.js → token-CZbuEcTL.js} +13 -4
  24. package/dist/{auth-store-x3_wvhTR.js → token-gate-CEzbXArg.js} +178 -1
  25. package/dist/{types-C7JkpfeY.d.ts → types-Bb0kwteA.d.ts} +5 -1
  26. package/dist/{where-clause-compiler-BzAJnWt4.d.ts → where-clause-compiler-CTblNxLo.d.ts} +24 -24
  27. package/package.json +1 -1
  28. package/skills/kitcn/references/features/auth.md +26 -8
  29. package/skills/kitcn/references/features/react.md +5 -1
  30. 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-BzAJnWt4.js";
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. Convex sends them after Authenticate on the same
404
- * socket and evaluates none of them if the token is refused; a refused token
405
- * sets `isAuthenticated` back to false, which resets auth-bound queries, and
406
- * never opens the gate again. Default `false`.
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 it obtains. A token for another user or session is refused before
413
- * it is cached, so neither Convex, the optimistic gate nor HTTP headers ever
414
- * see it: the Convex client is closed (dropping queued requests and
415
- * optimistic updates), every later token request answers null, and this is
416
- * called, typically to reload the page. Same-session refreshes pass
417
- * through. Off when not set.
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 a token must match both the
430
- * identity it returns and the one this guard already admitted; any
431
- * mismatch trips the guard. So a provider kept mounted but hidden (React
432
- * `<Activity>`) cannot resume under an identity the document has since
433
- * moved away from. A getter returning null does not constrain. Needs
434
- * `onTokenIdentityChange`.
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 as it is
439
- * handed to Convex or HTTP headers, cached tokens included, so the document
440
- * can claim the identity at that moment (for example the first token a
441
- * document without one obtains). Never called for a refused token. Read
442
- * from a ref, so passing a new function does not re-run effects. Needs
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 { A as CRPCClientError, C as decodeJwtExp, D as readAuthSessionFallbackToken, E as readAuthSessionFallbackData, O as writeAuthSessionFallbackData, T as clearAuthSessionFallback, d as isSessionSyncGraceActive, g as useAuthValue, h as useAuthStore, j as defaultIsUnauthorized, n as AuthProvider, o as ConvexProviderWithAuth$1, s as FetchAccessTokenContext, t as AUTH_SESSION_SYNC_GRACE_MS } from "../../auth-store-x3_wvhTR.js";
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
- }), [initialToken]),
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
- onTokenIdentityAdmitted,
232
+ guard,
233
+ inheritedTrip,
152
234
  onTokenIdentityChange,
153
235
  optimisticAuth,
154
- tokenIdentityBaseline,
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, onTokenIdentityAdmitted, tokenIdentityBaseline }) {
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.set("token", persistedToken);
211
- authStore.set("expiresAt", decodeJwtExp(persistedToken));
212
- authStore.set("sessionSyncGraceUntil", graceUntil);
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.data?.token || null;
276
- if (jwt && !admitToken(jwt)) return null;
397
+ const jwt = result?.data?.token || null;
277
398
  if (jwt) {
278
- const exp = decodeJwtExp(jwt);
279
- authStore.set("token", jwt);
280
- authStore.set("expiresAt", exp);
281
- authStore.set("sessionSyncGraceUntil", null);
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
- admitToken
472
+ isOpaqueUnderGuard
355
473
  ]);
356
474
  const guardedFetchAccessToken = useCallback(async (args = {}) => {
357
- if (identityGuardRef.current?.tripped) return null;
475
+ if (isDocumentTripped()) return null;
358
476
  const token = await fetchAccessToken(args);
359
- if (!token || !admitToken(token, onTokenIdentityAdmittedRef.current)) return null;
360
- return token;
361
- }, [fetchAccessToken, admitToken]);
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 rejectedTokenRef = useRef(null);
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
- const gate = resolveAuthGate({
403
- convexIsLoading,
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
- token
413
- })) rejectedTokenRef.current = token;
414
- authStore.set("isLoading", gate.isLoading);
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, a held, unexpired JWT that Convex
471
- * has not refused yet counts as authenticated while Convex confirms it, so
472
- * auth-bound queries subscribe at once; Convex sends them after Authenticate on
473
- * the same socket, and a refused token closes the socket before any of them is
474
- * evaluated. When Convex refuses the token, `isAuthenticated` falls back to
475
- * false, which resets auth-bound queries (see `CRPCProviderInner`).
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, isAuthenticated, optimisticAuth, rejectedToken, token }) {
478
- if (optimisticAuth && convexIsLoading && isOptimisticToken(token, rejectedToken)) return {
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, rejectedToken) {
488
- if (!token || token === rejectedToken) return false;
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-PdNGvNYP.js";
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 };