kitcn 0.33.5 → 0.33.7

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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # kitcn
2
2
 
3
+ ## 0.33.7
4
+
5
+ ### Patch Changes
6
+
7
+ - [#475](https://github.com/udecode/kitcn/pull/475) [`9e513f8`](https://github.com/udecode/kitcn/commit/9e513f85d4742efa041840c45989dfd3e96c883e) Thanks [@EfficiencyCorp](https://github.com/EfficiencyCorp)! - ## Patches
8
+
9
+ - Fix `onTokenIdentityChange` letting a token of another user or session into
10
+ the auth store, Convex, cRPC HTTP headers or the TanStack Start loader: the
11
+ SSR token, restored sessions, sign-in tokens, JWTs without `exp` and
12
+ concurrent first tokens are all held to the page's identity and every
13
+ mounted provider's current `tokenIdentityBaseline` getter, including changes
14
+ made by `onTokenIdentityAdmitted` before publication.
15
+ - Fix an identity guard trip staying local to one provider: every mounted
16
+ provider now hands out no token and publishes unauthenticated, each guarded
17
+ one closes its client and calls `onTokenIdentityChange` once, including a
18
+ guard enabled after the trip, and sign-in
19
+ mutations fail with `TOKEN_IDENTITY_CHANGED` until the page reloads.
20
+ - Fix the `optimisticAuth` window reopening after Convex refused a token: it
21
+ now ends at the Convex client's first auth result.
22
+
23
+ ## 0.33.6
24
+
25
+ ### Patch Changes
26
+
27
+ - [#473](https://github.com/udecode/kitcn/pull/473) [`16e0bb4`](https://github.com/udecode/kitcn/commit/16e0bb46d66a4d7166d577793fcb47f8161efa5d) Thanks [@EfficiencyCorp](https://github.com/EfficiencyCorp)! - - Add opt-in optimistic auth and document identity admission to
28
+ `ConvexAuthProvider`, with the same guarded token source for Convex and cRPC
29
+ HTTP requests.
30
+ - Add Convex-native `optimisticUpdate` support to cRPC `mutationOptions`.
31
+ - Reuse the Convex client's logger for server HTTP clients so construction is
32
+ deterministic during prerendering.
33
+
3
34
  ## 0.33.5
4
35
 
5
36
  ### 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-qH6-NCM9.js";
2
+ import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-DAqhUREe.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";
@@ -398,6 +398,75 @@ type ConvexAuthProviderProps = {
398
398
  queryName: string;
399
399
  }) => void; /** Custom function to detect UNAUTHORIZED errors. Default checks code property. */
400
400
  isUnauthorized?: (error: unknown) => boolean;
401
+ /**
402
+ * Run auth-bound queries as soon as an unexpired JWT is held instead of
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`.
414
+ */
415
+ optimisticAuth?: boolean;
416
+ /**
417
+ * Fix the identity (JWT `sub` and `sessionId`) the document speaks for: the
418
+ * one it holds when it mounts (`initialToken`), or, with none, the first
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.
439
+ */
440
+ onTokenIdentityChange?: () => void;
441
+ /**
442
+ * The identity (`sub|sessionId`, as the guard decodes it from a JWT) the
443
+ * document already speaks for when this provider mounts, for an app that
444
+ * mounts the provider more than once in one document (for example one per
445
+ * route group, over a shared Convex client). The guard starts from it
446
+ * instead of from `initialToken` or the first token obtained, so a remount
447
+ * without a token still refuses another user's or session's token.
448
+ *
449
+ * A fixed value is read on the first render only. A getter is read at
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`.
458
+ */
459
+ tokenIdentityBaseline?: string | null | (() => string | null);
460
+ /**
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
467
+ * `onTokenIdentityChange`.
468
+ */
469
+ onTokenIdentityAdmitted?: (token: string) => void;
401
470
  };
402
471
  /**
403
472
  * Unified auth provider for Convex + Better Auth.
@@ -414,7 +483,11 @@ declare function ConvexAuthProvider({
414
483
  initialToken,
415
484
  onMutationUnauthorized,
416
485
  onQueryUnauthorized,
417
- isUnauthorized
486
+ isUnauthorized,
487
+ optimisticAuth,
488
+ onTokenIdentityChange,
489
+ onTokenIdentityAdmitted,
490
+ tokenIdentityBaseline
418
491
  }: ConvexAuthProviderProps): react_jsx_runtime0.JSX.Element;
419
492
  //#endregion
420
493
  export { type AuthClient, type AuthClientWithPlugins, ConvexAuthProvider, type ConvexAuthProviderClient, ConvexAuthProviderProps, ConvexAuthProviderQueryClient, type PluginsWithCrossDomain, type PluginsWithoutCrossDomain, convexClient };
@@ -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 = () => {
@@ -134,13 +167,61 @@ const clearSessionAtom = (authClient) => {
134
167
  * Structure: AuthProvider wraps ConvexAuthProviderInner so that
135
168
  * useAuthStore() is available when creating fetchAccessToken.
136
169
  */
137
- function ConvexAuthProvider({ children, client, authClient, convexQueryClient, initialToken, onMutationUnauthorized, onQueryUnauthorized, isUnauthorized }) {
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,6 +229,11 @@ function ConvexAuthProvider({ children, client, authClient, convexQueryClient, i
148
229
  authClient,
149
230
  client,
150
231
  convexQueryClient,
232
+ guard,
233
+ inheritedTrip,
234
+ onTokenIdentityChange,
235
+ optimisticAuth,
236
+ refusedInitialToken,
151
237
  children
152
238
  })
153
239
  });
@@ -156,7 +242,7 @@ function ConvexAuthProvider({ children, client, authClient, convexQueryClient, i
156
242
  * Inner provider that has access to AuthStore via useAuthStore().
157
243
  * Creates fetchAccessToken and passes it through context (no race condition).
158
244
  */
159
- function ConvexAuthProviderInner({ children, client, authClient, convexQueryClient }) {
245
+ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClient, guard, inheritedTrip, optimisticAuth, onTokenIdentityChange, refusedInitialToken }) {
160
246
  const authStore = useAuthStore();
161
247
  convexQueryClient?.updateAuthStore(authStore);
162
248
  const { data: session, isPending } = authClient.useSession();
@@ -167,13 +253,14 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
167
253
  const isMountedRef = useRef(false);
168
254
  sessionRef.current = session;
169
255
  isPendingRef.current = isPending;
256
+ const isOpaqueUnderGuard = useCallback((token) => identityGuardInPlay(guard) && !isJwt(token), [guard]);
170
257
  const getCachedJwt = useCallback((minTimeRemainingMs = 0) => {
171
258
  const cachedToken = authStore.get("token");
172
- if (!cachedToken) return null;
259
+ if (!cachedToken || isOpaqueUnderGuard(cachedToken)) return null;
173
260
  const expiresAt = decodeJwtExp(cachedToken);
174
261
  if (expiresAt === null || expiresAt <= Date.now() + minTimeRemainingMs) return null;
175
262
  return cachedToken;
176
- }, [authStore]);
263
+ }, [authStore, isOpaqueUnderGuard]);
177
264
  useEffect(() => {
178
265
  if (hasActiveSessionData(session)) {
179
266
  authStore.set("sessionSyncGraceUntil", null);
@@ -190,6 +277,62 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
190
277
  isPending,
191
278
  authStore
192
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]);
193
336
  useEffect(() => {
194
337
  isMountedRef.current = true;
195
338
  return () => {
@@ -203,9 +346,11 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
203
346
  restoredTokenRef.current = persistedToken;
204
347
  const persistedSessionData = readAuthSessionFallbackData();
205
348
  const graceUntil = Date.now() + AUTH_SESSION_SYNC_GRACE_MS;
206
- authStore.set("token", persistedToken);
207
- authStore.set("expiresAt", decodeJwtExp(persistedToken));
208
- authStore.set("sessionSyncGraceUntil", graceUntil);
349
+ if (!publishToken(authStore, persistedToken, {
350
+ announce: true,
351
+ sessionSyncGraceUntil: graceUntil,
352
+ use: "restore"
353
+ })) return;
209
354
  if (persistedSessionData) syncSessionAtom(authClient, persistedSessionData);
210
355
  const ownsToken = () => authStore.get("token") === persistedToken;
211
356
  const shouldStop = () => !isMountedRef.current || !ownsToken();
@@ -244,17 +389,18 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
244
389
  if (pendingTokenRef.current) return pendingTokenRef.current;
245
390
  const cachedToken = authStore.get("token");
246
391
  const fetchOptions = { throw: false };
247
- if (cachedToken && decodeJwtExp(cachedToken) === null) {
392
+ if (cachedToken && (decodeJwtExp(cachedToken) === null || isOpaqueUnderGuard(cachedToken))) {
248
393
  fetchOptions.credentials = "omit";
249
394
  fetchOptions.headers = { Authorization: `Bearer ${cachedToken}` };
250
395
  }
251
396
  pendingTokenRef.current = authClient.convex.token({ fetchOptions }).then((result) => {
252
- const jwt = result.data?.token || null;
397
+ const jwt = result?.data?.token || null;
253
398
  if (jwt) {
254
- const exp = decodeJwtExp(jwt);
255
- authStore.set("token", jwt);
256
- authStore.set("expiresAt", exp);
257
- authStore.set("sessionSyncGraceUntil", null);
399
+ if (!publishToken(authStore, jwt, {
400
+ announce: true,
401
+ sessionSyncGraceUntil: null
402
+ })) return null;
403
+ announcedTokenRef.current = jwt;
258
404
  return jwt;
259
405
  }
260
406
  const cachedJwt = getCachedJwt();
@@ -304,11 +450,7 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
304
450
  return fetchFreshToken();
305
451
  }
306
452
  const freshToken = await fetchFreshTokenForced();
307
- if (!freshToken && cachedJwt) {
308
- authStore.set("token", cachedJwt);
309
- authStore.set("expiresAt", decodeJwtExp(cachedJwt));
310
- return cachedJwt;
311
- }
453
+ if (!freshToken && cachedJwt) return publishToken(authStore, cachedJwt, { announce: false }) ? cachedJwt : null;
312
454
  return freshToken;
313
455
  }
314
456
  authStore.set("token", null);
@@ -319,15 +461,28 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
319
461
  const cachedToken = authStore.get("token");
320
462
  const expiresAt = authStore.get("expiresAt");
321
463
  const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
322
- if (!forceRefreshToken && cachedToken && expiresAt && timeRemaining >= 6e4) return cachedToken;
464
+ if (!forceRefreshToken && cachedToken && expiresAt && timeRemaining >= 6e4 && !isOpaqueUnderGuard(cachedToken)) return cachedToken;
323
465
  if (!forceRefreshToken && pendingTokenRef.current) return pendingTokenRef.current;
324
466
  if (forceRefreshToken) return fetchFreshTokenForced();
325
467
  return fetchFreshToken();
326
468
  }, [
327
469
  authStore,
328
470
  authClient,
329
- getCachedJwt
471
+ getCachedJwt,
472
+ isOpaqueUnderGuard
330
473
  ]);
474
+ const guardedFetchAccessToken = useCallback(async (args = {}) => {
475
+ if (isDocumentTripped()) return null;
476
+ const token = await fetchAccessToken(args);
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]);
331
486
  const useAuth = useCallback(function useConvexAuthHook() {
332
487
  const token = authStore.get("token");
333
488
  const hasSession = hasActiveSessionData(sessionRef.current);
@@ -335,15 +490,20 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
335
490
  return {
336
491
  isLoading: isPendingRef.current && !token,
337
492
  isAuthenticated: sessionMissing ? false : hasSession || token !== null,
338
- fetchAccessToken
493
+ fetchAccessToken: guardedFetchAccessToken
339
494
  };
340
- }, [fetchAccessToken, authStore]);
495
+ }, [guardedFetchAccessToken, authStore]);
341
496
  return /* @__PURE__ */ jsx(FetchAccessTokenContext.Provider, {
342
- value: fetchAccessToken,
497
+ value: guardedFetchAccessToken,
343
498
  children: /* @__PURE__ */ jsx(ConvexProviderWithAuth$1, {
344
499
  client,
345
500
  useAuth,
346
- children: /* @__PURE__ */ jsx(AuthStateSync, { children })
501
+ children: /* @__PURE__ */ jsx(AuthStateSync, {
502
+ client,
503
+ guardTripped,
504
+ optimisticAuth,
505
+ children
506
+ })
347
507
  })
348
508
  });
349
509
  }
@@ -359,22 +519,68 @@ function ConvexAuthProviderInner({ children, client, authClient, convexQueryClie
359
519
  * 5. Without defensive check, we'd sync { isLoading: false, isAuthenticated: false }
360
520
  * 6. Queries would throw UNAUTHORIZED before token is validated
361
521
  */
362
- function AuthStateSync({ children }) {
522
+ function AuthStateSync({ children, client, guardTripped, optimisticAuth = false }) {
363
523
  const { isLoading: convexIsLoading, isAuthenticated } = useConvexAuth();
364
524
  const authStore = useAuthStore();
365
525
  const token = useAuthValue("token");
526
+ const subscribe = useCallback((listener) => subscribeClientSettlement(client, listener), [client]);
527
+ const getSettled = useCallback(() => isClientSettled(client), [client]);
528
+ const settled = useSyncExternalStore(subscribe, getSettled, getSettled);
366
529
  useEffect(() => {
367
- const isLoading = convexIsLoading || !!token && !isAuthenticated;
368
- authStore.set("isLoading", isLoading);
369
- authStore.set("isAuthenticated", isAuthenticated);
530
+ publishAuthState(authStore, resolveAuthGate({
531
+ admitHeldToken: (held) => admitStoreToken(authStore, held, { use: "hold" }),
532
+ convexIsLoading,
533
+ guardTripped: guardTripped || isDocumentTripped(),
534
+ isAuthenticated,
535
+ optimisticWindow: optimisticAuth && !settled && !isClientSettled(client),
536
+ token: authStore.get("token")
537
+ }));
370
538
  }, [
539
+ client,
371
540
  convexIsLoading,
541
+ guardTripped,
372
542
  isAuthenticated,
543
+ optimisticAuth,
544
+ settled,
373
545
  token,
374
546
  authStore
375
547
  ]);
376
548
  return children;
377
549
  }
550
+ function resolveTokenIdentityBaseline(baseline) {
551
+ return typeof baseline === "function" ? baseline() : baseline ?? null;
552
+ }
553
+ /**
554
+ * The auth state published to the store, which is what every query gate reads.
555
+ *
556
+ * Without `optimisticAuth` this is the confirmed state: queries wait until
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.
564
+ */
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 {
571
+ isAuthenticated: true,
572
+ isLoading: false
573
+ };
574
+ return {
575
+ isAuthenticated,
576
+ isLoading: convexIsLoading || !!token && !isAuthenticated
577
+ };
578
+ }
579
+ function isOptimisticToken(token) {
580
+ if (!isJwt(token)) return false;
581
+ const expiresAt = decodeJwtExp(token);
582
+ return expiresAt !== null && expiresAt > Date.now();
583
+ }
378
584
  /**
379
585
  * Handles cross-domain one-time token (OTT) verification.
380
586
  */
@@ -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-QZXEzUnd.js";
2
2
  export { type AuthRuntime, BetterAuthOptionsWithoutDatabase, type GeneratedAuthDisabledReasonKind, GenericAuthBeforeResult, GenericAuthDefinition, GenericAuthTriggerChange, GenericAuthTriggerHandlers, GenericAuthTriggers, createDisabledAuthRuntime, defineAuth, getGeneratedAuthDisabledReason };
@@ -1,7 +1,7 @@
1
1
  import { a as QueryCtxWithPreferredOrmQueryTable, n as LookupByIdResultByCtx, t as DocByCtx } from "../query-context-BkNjkDCk.js";
2
2
  import { t as GetAuth } from "../types-ex0-J-SC.js";
3
3
  import { t as GenericCtx } from "../context-utils-Cbv4r0AA.js";
4
- import { C as GenericAuthTriggerHandlers, S as GenericAuthTriggerChange, T as defineAuth, _ as updateManyHandler, a as AuthFunctions, b as GenericAuthBeforeResult, c as consumeOneHandler, d as createHandler, f as deleteManyHandler, g as incrementOneHandler, h as findOneHandler, i as getGeneratedAuthDisabledReason, l as countHandler, m as findManyHandler, n as GeneratedAuthDisabledReasonKind, o as Triggers, p as deleteOneHandler, r as createDisabledAuthRuntime, s as createClient, t as AuthRuntime, u as createApi, v as updateOneHandler, w as GenericAuthTriggers, x as GenericAuthDefinition, y as BetterAuthOptionsWithoutDatabase } from "../generated-contract-disabled-PdNGvNYP.js";
4
+ import { C as GenericAuthTriggerHandlers, S as GenericAuthTriggerChange, T as defineAuth, _ as updateManyHandler, a as AuthFunctions, b as GenericAuthBeforeResult, c as consumeOneHandler, d as createHandler, f as deleteManyHandler, g as incrementOneHandler, h as findOneHandler, i as getGeneratedAuthDisabledReason, l as countHandler, m as findManyHandler, n as GeneratedAuthDisabledReasonKind, o as Triggers, p as deleteOneHandler, r as createDisabledAuthRuntime, s as createClient, t as AuthRuntime, u as createApi, v as updateOneHandler, w as GenericAuthTriggers, x as GenericAuthDefinition, y as BetterAuthOptionsWithoutDatabase } from "../generated-contract-disabled-QZXEzUnd.js";
5
5
  import * as convex_values0 from "convex/values";
6
6
  import { Infer } from "convex/values";
7
7
  import { AuthConfig, DocumentByName, GenericDataModel, GenericMutationCtx, GenericQueryCtx, GenericSchema, PaginationOptions, PaginationResult, SchemaDefinition, TableNamesInDataModel } from "convex/server";
@@ -109,31 +109,31 @@ type AdapterPaginationOptions = PaginationOptions & {
109
109
  };
110
110
  declare const adapterWhereValidator: convex_values0.VObject<{
111
111
  mode?: "sensitive" | "insensitive" | undefined;
112
+ operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
112
113
  connector?: "AND" | "OR" | undefined;
113
- operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
114
- field: string;
115
114
  value: string | number | boolean | string[] | number[] | null;
115
+ field: string;
116
116
  }, {
117
117
  connector: convex_values0.VUnion<"AND" | "OR" | undefined, [convex_values0.VLiteral<"AND", "required">, convex_values0.VLiteral<"OR", "required">], "optional", never>;
118
118
  field: convex_values0.VString<string, "required">;
119
119
  mode: convex_values0.VUnion<"sensitive" | "insensitive" | undefined, [convex_values0.VLiteral<"sensitive", "required">, convex_values0.VLiteral<"insensitive", "required">], "optional", never>;
120
- operator: convex_values0.VUnion<"lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
120
+ operator: convex_values0.VUnion<"eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
121
121
  value: convex_values0.VUnion<string | number | boolean | string[] | number[] | null, [convex_values0.VString<string, "required">, convex_values0.VFloat64<number, "required">, convex_values0.VBoolean<boolean, "required">, convex_values0.VArray<string[], convex_values0.VString<string, "required">, "required">, convex_values0.VArray<number[], convex_values0.VFloat64<number, "required">, "required">, convex_values0.VNull<null, "required">], "required", never>;
122
- }, "required", "mode" | "connector" | "field" | "operator" | "value">;
122
+ }, "required", "mode" | "operator" | "value" | "field" | "connector">;
123
123
  declare const adapterArgsValidator: convex_values0.VObject<{
124
+ limit?: number | undefined;
124
125
  where?: {
125
126
  mode?: "sensitive" | "insensitive" | undefined;
127
+ operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
126
128
  connector?: "AND" | "OR" | undefined;
127
- operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
128
- field: string;
129
129
  value: string | number | boolean | string[] | number[] | null;
130
+ field: string;
130
131
  }[] | undefined;
131
- limit?: number | undefined;
132
- offset?: number | undefined;
133
132
  select?: string[] | undefined;
133
+ offset?: number | undefined;
134
134
  sortBy?: {
135
- field: string;
136
135
  direction: "asc" | "desc";
136
+ field: string;
137
137
  } | undefined;
138
138
  model: string;
139
139
  }, {
@@ -142,32 +142,32 @@ declare const adapterArgsValidator: convex_values0.VObject<{
142
142
  offset: convex_values0.VFloat64<number | undefined, "optional">;
143
143
  select: convex_values0.VArray<string[] | undefined, convex_values0.VString<string, "required">, "optional">;
144
144
  sortBy: convex_values0.VObject<{
145
- field: string;
146
145
  direction: "asc" | "desc";
146
+ field: string;
147
147
  } | undefined, {
148
148
  direction: convex_values0.VUnion<"asc" | "desc", [convex_values0.VLiteral<"asc", "required">, convex_values0.VLiteral<"desc", "required">], "required", never>;
149
149
  field: convex_values0.VString<string, "required">;
150
- }, "optional", "field" | "direction">;
150
+ }, "optional", "direction" | "field">;
151
151
  where: convex_values0.VArray<{
152
152
  mode?: "sensitive" | "insensitive" | undefined;
153
+ operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
153
154
  connector?: "AND" | "OR" | undefined;
154
- operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
155
- field: string;
156
155
  value: string | number | boolean | string[] | number[] | null;
156
+ field: string;
157
157
  }[] | undefined, convex_values0.VObject<{
158
158
  mode?: "sensitive" | "insensitive" | undefined;
159
+ operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
159
160
  connector?: "AND" | "OR" | undefined;
160
- operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
161
- field: string;
162
161
  value: string | number | boolean | string[] | number[] | null;
162
+ field: string;
163
163
  }, {
164
164
  connector: convex_values0.VUnion<"AND" | "OR" | undefined, [convex_values0.VLiteral<"AND", "required">, convex_values0.VLiteral<"OR", "required">], "optional", never>;
165
165
  field: convex_values0.VString<string, "required">;
166
166
  mode: convex_values0.VUnion<"sensitive" | "insensitive" | undefined, [convex_values0.VLiteral<"sensitive", "required">, convex_values0.VLiteral<"insensitive", "required">], "optional", never>;
167
- operator: convex_values0.VUnion<"lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
167
+ operator: convex_values0.VUnion<"eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
168
168
  value: convex_values0.VUnion<string | number | boolean | string[] | number[] | null, [convex_values0.VString<string, "required">, convex_values0.VFloat64<number, "required">, convex_values0.VBoolean<boolean, "required">, convex_values0.VArray<string[], convex_values0.VString<string, "required">, "required">, convex_values0.VArray<number[], convex_values0.VFloat64<number, "required">, "required">, convex_values0.VNull<null, "required">], "required", never>;
169
- }, "required", "mode" | "connector" | "field" | "operator" | "value">, "optional">;
170
- }, "required", "model" | "where" | "limit" | "offset" | "select" | "sortBy" | "sortBy.field" | "sortBy.direction">;
169
+ }, "required", "mode" | "operator" | "value" | "field" | "connector">, "optional">;
170
+ }, "required", "limit" | "where" | "model" | "select" | "offset" | "sortBy" | "sortBy.direction" | "sortBy.field">;
171
171
  declare const hasUniqueFields: (betterAuthSchema: BetterAuthDBSchema, model: string, input: Record<string, any>) => boolean;
172
172
  declare const checkUniqueFields: <Schema extends SchemaDefinition<any, any>>(ctx: GenericQueryCtx<GenericDataModel>, schema: Schema, betterAuthSchema: BetterAuthDBSchema, table: string, input: Record<string, any>, doc?: Record<string, any>) => Promise<void>;
173
173
  declare const selectFields: <T extends TableNamesInDataModel<GenericDataModel>, D extends DocumentByName<GenericDataModel, T>>(doc: D | null, select?: string[]) => D | null;