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.
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { A as CRPCClientError, C as decodeJwtExp, M as isCRPCClientError, O as writeAuthSessionFallbackData, S as useSafeConvexAuth, T as clearAuthSessionFallback, _ as useConvexAuthBridge, a as ConvexAuthRecoveryError, b as useIsAuth, c as MaybeAuthenticated, d as isSessionSyncGraceActive, f as useAuth, g as useAuthValue, h as useAuthStore, i as ConvexAuthBridge, j as defaultIsUnauthorized, k as writeAuthSessionFallbackToken, l as MaybeUnauthenticated, m as useAuthState, n as AuthProvider, o as ConvexProviderWithAuth, p as useAuthGuard, r as Authenticated, s as FetchAccessTokenContext, t as AUTH_SESSION_SYNC_GRACE_MS, u as Unauthenticated, v as useConvexAuthRecovery, w as decodeJwtIdentity, x as useMaybeAuth, y as useFetchAccessToken } from "../auth-store-x3_wvhTR.js";
2
+ import { A as useAuthState, B as decodeJwtIdentity, C as FetchAccessTokenContext, D as isSessionSyncGraceActive, E as Unauthenticated, F as useFetchAccessToken, G as writeAuthSessionFallbackToken, I as useIsAuth, J as isCRPCClientError, K as CRPCClientError, L as useMaybeAuth, M as useAuthValue, N as useConvexAuthBridge, O as useAuth, P as useConvexAuthRecovery, R as useSafeConvexAuth, S as ConvexProviderWithAuth, T as MaybeUnauthenticated, V as clearAuthSessionFallback, W as writeAuthSessionFallbackData, _ as AUTH_SESSION_SYNC_GRACE_MS, b as ConvexAuthBridge, i as publishToken, j as useAuthStore, k as useAuthGuard, l as isDocumentTripped, q as defaultIsUnauthorized, r as publishAuthenticated, t as admitStoreToken, v as AuthProvider, w as MaybeAuthenticated, x as ConvexAuthRecoveryError, y as Authenticated, z as decodeJwtExp } from "../token-gate-CEzbXArg.js";
3
3
  import { ConvexProvider, ConvexReactClient, ConvexReactClient as ConvexReactClient$1, useAction, useConvex, useMutation } from "convex/react";
4
4
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
@@ -9,122 +9,6 @@ import { hashKey } from "@tanstack/query-core";
9
9
  import { convexToJson } from "convex/values";
10
10
  import { ConvexHttpClient } from "convex/browser";
11
11
 
12
- //#region src/shared/meta-utils.ts
13
- const metaCache = /* @__PURE__ */ new WeakMap();
14
- const nonMetaLeafKeys = new Set(["functionRef", "ref"]);
15
- function isRecord$1(value) {
16
- return typeof value === "object" && value !== null;
17
- }
18
- function isFunctionType(value) {
19
- return value === "query" || value === "mutation" || value === "action";
20
- }
21
- function isMetaScalar(value) {
22
- return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
23
- }
24
- function extractLeafMeta(value) {
25
- const type = value.type;
26
- if (!isFunctionType(type)) return;
27
- const result = { type };
28
- for (const [key, entry] of Object.entries(value)) {
29
- if (key === "type" || nonMetaLeafKeys.has(key) || key.startsWith("_")) continue;
30
- if (entry === void 0) continue;
31
- if (isMetaScalar(entry)) result[key] = entry;
32
- }
33
- return result;
34
- }
35
- function getHttpRoutes(api) {
36
- const routes = api._http;
37
- if (!isRecord$1(routes)) return;
38
- const normalized = {};
39
- for (const [routeKey, routeValue] of Object.entries(routes)) {
40
- if (!isRecord$1(routeValue)) continue;
41
- const routePath = routeValue.path;
42
- const routeMethod = routeValue.method;
43
- if (typeof routePath === "string" && typeof routeMethod === "string") normalized[routeKey] = {
44
- path: routePath,
45
- method: routeMethod
46
- };
47
- }
48
- return normalized;
49
- }
50
- /**
51
- * Build a metadata index from merged API leaves.
52
- * Supports both generated `api` objects and plain metadata fixtures.
53
- */
54
- function buildMetaIndex(api) {
55
- const cached = metaCache.get(api);
56
- if (cached) return cached;
57
- const meta = {};
58
- const httpRoutes = getHttpRoutes(api);
59
- if (httpRoutes) meta._http = httpRoutes;
60
- const walk = (node, path) => {
61
- for (const [key, value] of Object.entries(node)) {
62
- if (key.startsWith("_")) continue;
63
- if (!isRecord$1(value)) continue;
64
- const leafMeta = extractLeafMeta(value);
65
- if (leafMeta) {
66
- if (path.length === 0) continue;
67
- const namespace = path.join("/");
68
- meta[namespace] ??= {};
69
- meta[namespace][key] = leafMeta;
70
- continue;
71
- }
72
- walk(value, [...path, key]);
73
- }
74
- };
75
- walk(api, []);
76
- metaCache.set(api, meta);
77
- return meta;
78
- }
79
- /**
80
- * Get a function reference from the API object by traversing the path.
81
- */
82
- function getFuncRef(api, path) {
83
- let current = api;
84
- for (const key of path) if (current && typeof current === "object") {
85
- const next = current[key];
86
- if (next === void 0) throw new Error(`Invalid path: ${path.join(".")}`);
87
- current = next;
88
- } else throw new Error(`Invalid path: ${path.join(".")}`);
89
- if (current && typeof current === "object") {
90
- const maybeFunctionRef = current.functionRef;
91
- if (maybeFunctionRef && typeof maybeFunctionRef === "object") return maybeFunctionRef;
92
- }
93
- if (!current || typeof current !== "object") throw new Error(`Invalid function reference at path: ${path.join(".")}`);
94
- return current;
95
- }
96
- /**
97
- * Get function type from meta using path.
98
- * Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
99
- *
100
- * @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
101
- * @param meta - The meta object from codegen
102
- * @returns Function type or 'query' as default
103
- */
104
- function getFunctionType(path, source) {
105
- if (path.length < 2) return "query";
106
- const meta = buildMetaIndex(source);
107
- const fnName = path.at(-1);
108
- const fnType = meta[path.slice(0, -1).join("/")]?.[fnName]?.type;
109
- if (fnType === "query" || fnType === "mutation" || fnType === "action") return fnType;
110
- return "query";
111
- }
112
- /**
113
- * Get function metadata from meta using path.
114
- * Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
115
- *
116
- * @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
117
- * @param meta - The meta object from codegen
118
- * @returns Function metadata or undefined
119
- */
120
- function getFunctionMeta(path, source) {
121
- if (path.length < 2) return;
122
- const meta = buildMetaIndex(source);
123
- const fnName = path.at(-1);
124
- return meta[path.slice(0, -1).join("/")]?.[fnName];
125
- }
126
-
127
- //#endregion
128
12
  //#region src/crpc/http-types.ts
129
13
  /**
130
14
  * Build the exact cache key for an HTTP route.
@@ -209,6 +93,8 @@ function buildSearchParams(query) {
209
93
  else if (value !== void 0 && value !== null) params.append(key, value);
210
94
  return params;
211
95
  }
96
+ const RECHECK_AUTHORIZATION = Symbol.for("kitcn.http.recheckAuthorization");
97
+ const withAuthorizationRecheck = (headers, recheck) => Object.defineProperty(headers, RECHECK_AUTHORIZATION, { value: recheck });
212
98
  /**
213
99
  * Hono-style HTTP request executor.
214
100
  * Processes args in the same way as Hono's ClientRequestImpl.fetch().
@@ -247,6 +133,8 @@ async function executeHttpRequest(opts) {
247
133
  for (const [key, value] of Object.entries(resolvedBaseHeaders)) if (value !== void 0) finalHeaders[key] = value;
248
134
  }
249
135
  Object.assign(finalHeaders, headerValues);
136
+ const recheck = resolvedBaseHeaders?.[RECHECK_AUTHORIZATION];
137
+ const sendHeaders = recheck && finalHeaders.Authorization === resolvedBaseHeaders?.Authorization && !recheck() ? Object.fromEntries(Object.entries(finalHeaders).filter(([key]) => key !== "Authorization")) : finalHeaders;
250
138
  let url = opts.convexSiteUrl + path;
251
139
  if (args.params) url = opts.convexSiteUrl + replaceUrlParam(path, args.params);
252
140
  if (args.searchParams) {
@@ -258,7 +146,7 @@ async function executeHttpRequest(opts) {
258
146
  const response = await (mergedClientOpts.fetch ?? opts.baseFetch ?? globalThis.fetch)(url, {
259
147
  body: setBody ? rBody : void 0,
260
148
  method: methodUpperCase,
261
- headers: finalHeaders,
149
+ headers: sendHeaders,
262
150
  ...mergedClientOpts.init
263
151
  });
264
152
  if (!response.ok) {
@@ -280,6 +168,122 @@ async function executeHttpRequest(opts) {
280
168
  return response.text();
281
169
  }
282
170
 
171
+ //#endregion
172
+ //#region src/shared/meta-utils.ts
173
+ const metaCache = /* @__PURE__ */ new WeakMap();
174
+ const nonMetaLeafKeys = new Set(["functionRef", "ref"]);
175
+ function isRecord$1(value) {
176
+ return typeof value === "object" && value !== null;
177
+ }
178
+ function isFunctionType(value) {
179
+ return value === "query" || value === "mutation" || value === "action";
180
+ }
181
+ function isMetaScalar(value) {
182
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
183
+ }
184
+ function extractLeafMeta(value) {
185
+ const type = value.type;
186
+ if (!isFunctionType(type)) return;
187
+ const result = { type };
188
+ for (const [key, entry] of Object.entries(value)) {
189
+ if (key === "type" || nonMetaLeafKeys.has(key) || key.startsWith("_")) continue;
190
+ if (entry === void 0) continue;
191
+ if (isMetaScalar(entry)) result[key] = entry;
192
+ }
193
+ return result;
194
+ }
195
+ function getHttpRoutes(api) {
196
+ const routes = api._http;
197
+ if (!isRecord$1(routes)) return;
198
+ const normalized = {};
199
+ for (const [routeKey, routeValue] of Object.entries(routes)) {
200
+ if (!isRecord$1(routeValue)) continue;
201
+ const routePath = routeValue.path;
202
+ const routeMethod = routeValue.method;
203
+ if (typeof routePath === "string" && typeof routeMethod === "string") normalized[routeKey] = {
204
+ path: routePath,
205
+ method: routeMethod
206
+ };
207
+ }
208
+ return normalized;
209
+ }
210
+ /**
211
+ * Build a metadata index from merged API leaves.
212
+ * Supports both generated `api` objects and plain metadata fixtures.
213
+ */
214
+ function buildMetaIndex(api) {
215
+ const cached = metaCache.get(api);
216
+ if (cached) return cached;
217
+ const meta = {};
218
+ const httpRoutes = getHttpRoutes(api);
219
+ if (httpRoutes) meta._http = httpRoutes;
220
+ const walk = (node, path) => {
221
+ for (const [key, value] of Object.entries(node)) {
222
+ if (key.startsWith("_")) continue;
223
+ if (!isRecord$1(value)) continue;
224
+ const leafMeta = extractLeafMeta(value);
225
+ if (leafMeta) {
226
+ if (path.length === 0) continue;
227
+ const namespace = path.join("/");
228
+ meta[namespace] ??= {};
229
+ meta[namespace][key] = leafMeta;
230
+ continue;
231
+ }
232
+ walk(value, [...path, key]);
233
+ }
234
+ };
235
+ walk(api, []);
236
+ metaCache.set(api, meta);
237
+ return meta;
238
+ }
239
+ /**
240
+ * Get a function reference from the API object by traversing the path.
241
+ */
242
+ function getFuncRef(api, path) {
243
+ let current = api;
244
+ for (const key of path) if (current && typeof current === "object") {
245
+ const next = current[key];
246
+ if (next === void 0) throw new Error(`Invalid path: ${path.join(".")}`);
247
+ current = next;
248
+ } else throw new Error(`Invalid path: ${path.join(".")}`);
249
+ if (current && typeof current === "object") {
250
+ const maybeFunctionRef = current.functionRef;
251
+ if (maybeFunctionRef && typeof maybeFunctionRef === "object") return maybeFunctionRef;
252
+ }
253
+ if (!current || typeof current !== "object") throw new Error(`Invalid function reference at path: ${path.join(".")}`);
254
+ return current;
255
+ }
256
+ /**
257
+ * Get function type from meta using path.
258
+ * Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
259
+ *
260
+ * @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
261
+ * @param meta - The meta object from codegen
262
+ * @returns Function type or 'query' as default
263
+ */
264
+ function getFunctionType(path, source) {
265
+ if (path.length < 2) return "query";
266
+ const meta = buildMetaIndex(source);
267
+ const fnName = path.at(-1);
268
+ const fnType = meta[path.slice(0, -1).join("/")]?.[fnName]?.type;
269
+ if (fnType === "query" || fnType === "mutation" || fnType === "action") return fnType;
270
+ return "query";
271
+ }
272
+ /**
273
+ * Get function metadata from meta using path.
274
+ * Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
275
+ *
276
+ * @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
277
+ * @param meta - The meta object from codegen
278
+ * @returns Function metadata or undefined
279
+ */
280
+ function getFunctionMeta(path, source) {
281
+ if (path.length < 2) return;
282
+ const meta = buildMetaIndex(source);
283
+ const fnName = path.at(-1);
284
+ return meta[path.slice(0, -1).join("/")]?.[fnName];
285
+ }
286
+
283
287
  //#endregion
284
288
  //#region src/crpc/transformer.ts
285
289
  const isPlainObject = (value) => {
@@ -1155,10 +1159,12 @@ function useConvexMutationOptions(mutation, options, transformer) {
1155
1159
  const name = getFunctionName(mutation);
1156
1160
  const [namespace, fnName] = name.split(":");
1157
1161
  const authType = getMeta(namespace, fnName)?.auth;
1158
- const convexMutation = useMutation(mutation);
1162
+ const { optimisticUpdate, ...mutationOptions } = options ?? {};
1163
+ const reactMutation = useMutation(mutation);
1164
+ const convexMutation = optimisticUpdate ? reactMutation.withOptimisticUpdate(optimisticUpdate) : reactMutation;
1159
1165
  const resolvedTransformer = getTransformer(transformer);
1160
1166
  return {
1161
- ...options,
1167
+ ...mutationOptions,
1162
1168
  mutationFn: async (args) => {
1163
1169
  if (authType === "required" && guard()) throw new CRPCClientError({
1164
1170
  code: "UNAUTHORIZED",
@@ -1496,21 +1502,16 @@ function createCRPCContext(options) {
1496
1502
  convexSiteUrl: httpOptions.convexSiteUrl,
1497
1503
  routes: meta._http,
1498
1504
  headers: async () => {
1499
- const token = authStore.get("token");
1500
1505
  const expiresAt = authStore.get("expiresAt");
1501
1506
  const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
1502
- if (token && expiresAt && timeRemaining >= 6e4) return {
1503
- ...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
1507
+ const heldToken = authStore.get("token");
1508
+ const token = fetchAccessToken ? await fetchAccessToken({ forceRefreshToken: !!expiresAt && timeRemaining < 6e4 }) : heldToken && expiresAt && timeRemaining >= 6e4 ? heldToken : null;
1509
+ const userHeaders = typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers;
1510
+ const admit = () => !!token && admitStoreToken(authStore, token, { use: "handout" });
1511
+ return admit() ? withAuthorizationRecheck({
1512
+ ...userHeaders,
1504
1513
  Authorization: `Bearer ${token}`
1505
- };
1506
- if (fetchAccessToken) {
1507
- const newToken = await fetchAccessToken({ forceRefreshToken: !!expiresAt });
1508
- if (newToken) return {
1509
- ...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
1510
- Authorization: `Bearer ${newToken}`
1511
- };
1512
- }
1513
- return { ...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers };
1514
+ }, admit) : { ...userHeaders };
1514
1515
  },
1515
1516
  fetch: httpOptions.fetch,
1516
1517
  onError: httpOptions.onError,
@@ -1643,10 +1644,19 @@ function isAuthMutationError(error) {
1643
1644
 
1644
1645
  //#endregion
1645
1646
  //#region src/react/auth-mutations.ts
1646
- /** Poll until JWT token exists (auth complete) (max 5s) */
1647
+ const tokenIdentityChangedError = () => new AuthMutationError({
1648
+ code: "TOKEN_IDENTITY_CHANGED",
1649
+ message: "This page switched accounts. Reload it to sign in.",
1650
+ status: 401,
1651
+ statusText: "UNAUTHORIZED"
1652
+ });
1653
+ const assertDocumentNotTripped = () => {
1654
+ if (isDocumentTripped()) throw tokenIdentityChangedError();
1655
+ };
1647
1656
  const waitForAuth = async (store, timeout = 5e3) => {
1648
1657
  const start = Date.now();
1649
1658
  while (Date.now() - start < timeout) {
1659
+ assertDocumentNotTripped();
1650
1660
  if (store.get("token")) return true;
1651
1661
  await new Promise((r) => setTimeout(r, 50));
1652
1662
  }
@@ -1671,9 +1681,10 @@ const readReturnedToken = (value) => {
1671
1681
  const seedReturnedToken = (store, value) => {
1672
1682
  const token = readReturnedToken(value);
1673
1683
  if (!token) return;
1674
- store.set("token", token);
1675
- store.set("expiresAt", decodeJwtExp(token));
1676
- store.set("sessionSyncGraceUntil", Date.now() + AUTH_SESSION_SYNC_GRACE_MS);
1684
+ if (!publishToken(store, token, {
1685
+ announce: true,
1686
+ sessionSyncGraceUntil: Date.now() + AUTH_SESSION_SYNC_GRACE_MS
1687
+ })) throw tokenIdentityChangedError();
1677
1688
  if (decodeJwtExp(token) === null) writeAuthSessionFallbackToken(token);
1678
1689
  };
1679
1690
  const toAuthMutationError = (error) => new AuthMutationError({
@@ -1748,12 +1759,13 @@ function createAuthMutations(authClient) {
1748
1759
  mutationFn: async (args) => {
1749
1760
  const signInSocial = authClient.signIn?.social;
1750
1761
  if (typeof signInSocial !== "function") throw new Error("Auth client does not expose signIn.social");
1762
+ assertDocumentNotTripped();
1751
1763
  const res = await callAuthMethod(signInSocial, withDisabledSessionSignal(args));
1752
1764
  if (res?.error) throw toAuthMutationError(res.error);
1753
1765
  seedReturnedToken(authStoreApi, res);
1754
1766
  await hydrateReturnedSession(authClient, res);
1755
1767
  await ensureAuth(authStoreApi);
1756
- authStoreApi.set("isAuthenticated", true);
1768
+ if (!publishAuthenticated(authStoreApi)) throw tokenIdentityChangedError();
1757
1769
  return res;
1758
1770
  }
1759
1771
  };
@@ -1766,12 +1778,13 @@ function createAuthMutations(authClient) {
1766
1778
  mutationFn: async (args) => {
1767
1779
  const signIn = authClient.signIn?.[signInMethod];
1768
1780
  if (typeof signIn !== "function") throw new Error(`Auth client does not expose signIn.${signInMethod}`);
1781
+ assertDocumentNotTripped();
1769
1782
  const res = await callAuthMethod(signIn, withDisabledSessionSignal(args));
1770
1783
  if (res?.error) throw toAuthMutationError(res.error);
1771
1784
  seedReturnedToken(authStoreApi, res);
1772
1785
  await hydrateReturnedSession(authClient, res);
1773
1786
  await ensureAuth(authStoreApi);
1774
- authStoreApi.set("isAuthenticated", true);
1787
+ if (!publishAuthenticated(authStoreApi)) throw tokenIdentityChangedError();
1775
1788
  return res;
1776
1789
  }
1777
1790
  };
@@ -1783,12 +1796,13 @@ function createAuthMutations(authClient) {
1783
1796
  mutationFn: async (args) => {
1784
1797
  const signUpEmail = authClient.signUp?.email;
1785
1798
  if (typeof signUpEmail !== "function") throw new Error("Auth client does not expose signUp.email");
1799
+ assertDocumentNotTripped();
1786
1800
  const res = await callAuthMethod(signUpEmail, withDisabledSessionSignal(args));
1787
1801
  if (res?.error) throw toAuthMutationError(res.error);
1788
1802
  seedReturnedToken(authStoreApi, res);
1789
1803
  await hydrateReturnedSession(authClient, res);
1790
1804
  await ensureAuth(authStoreApi);
1791
- authStoreApi.set("isAuthenticated", true);
1805
+ if (!publishAuthenticated(authStoreApi)) throw tokenIdentityChangedError();
1792
1806
  return res;
1793
1807
  }
1794
1808
  };
@@ -2105,7 +2119,10 @@ var ConvexQueryClient = class {
2105
2119
  this._queryClient = options.queryClient;
2106
2120
  this.unsubscribe = this.subscribeInner(options.queryClient.getQueryCache());
2107
2121
  }
2108
- if (isServer) this.serverHttpClient = new ConvexHttpClient(this.convexClient.url, { fetch: options.serverFetch });
2122
+ if (isServer) this.serverHttpClient = new ConvexHttpClient(this.convexClient.url, {
2123
+ fetch: options.serverFetch,
2124
+ logger: this.convexClient.logger
2125
+ });
2109
2126
  }
2110
2127
  /**
2111
2128
  * Connect to TanStack QueryClient.
@@ -1,8 +1,10 @@
1
1
  import { n as DeepPartial, o as Simplify, r as DistributiveOmit } from "../types-Bqi_ivSd.js";
2
2
  import { L as DataTransformerOptions, _ as CRPCHttpRouter, c as HttpQueryKey, k as HttpProcedure, o as HttpMutationKey, y as HttpRouterRecord } from "../http-types-BhhPZtwa.js";
3
3
  import { g as UnsetMarker } from "../types-Bkeicpo6.js";
4
- import { A as HttpInputArgs, C as ReservedMutationOptions$1, S as ReservedInfiniteQueryOptions, T as StaticQueryOptsParam, _ as IsPaginated, b as PaginatedFnMeta, c as ConvexMutationKey, d as ConvexQueryMeta, f as EmptyObject, g as InfiniteQueryInput, l as ConvexQueryHookOptions, m as FUNC_REF_SYMBOL, o as ConvexActionKey, p as ExtractPaginatedItem, s as ConvexInfiniteQueryMeta, u as ConvexQueryKey, w as ReservedQueryOptions$1, y as MutationVariables } from "../types-C7JkpfeY.js";
4
+ import { A as HttpInputArgs, C as ReservedMutationOptions$1, S as ReservedInfiniteQueryOptions, T as StaticQueryOptsParam, _ as IsPaginated, b as PaginatedFnMeta, c as ConvexMutationKey, d as ConvexQueryMeta, f as EmptyObject, g as InfiniteQueryInput, l as ConvexQueryHookOptions, m as FUNC_REF_SYMBOL, o as ConvexActionKey, p as ExtractPaginatedItem, s as ConvexInfiniteQueryMeta, u as ConvexQueryKey, w as ReservedQueryOptions$1, y as MutationVariables } from "../types-Bb0kwteA.js";
5
+ import { Value } from "convex/values";
5
6
  import { FunctionArgs, FunctionReference, FunctionReturnType } from "convex/server";
7
+ import { OptimisticUpdate } from "convex/browser";
6
8
  import { z } from "zod";
7
9
  import { DefaultError, QueryFilters, SkipToken, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
8
10
 
@@ -70,11 +72,19 @@ type DecorateInfiniteQuery<T extends FunctionReference<'query'>> = {
70
72
  infiniteQueryKey: (args?: DeepPartial<InfiniteQueryInput<FunctionArgs<T>>>) => ConvexQueryKey<T>; /** Function metadata from server (auth, limit, rateLimit, role, type) */
71
73
  meta: PaginatedFnMeta;
72
74
  };
75
+ /**
76
+ * Convex-native optimistic update for a cRPC mutation. It edits Convex's local
77
+ * query store, which the query client reads, so Convex rolls it back when the
78
+ * mutation fails. Args and query values are in their wire shape.
79
+ */
80
+ type ConvexOptimisticUpdateOption<Args extends Record<string, Value>> = {
81
+ optimisticUpdate?: OptimisticUpdate<Args>;
82
+ };
73
83
  /**
74
84
  * Decorated mutation procedure with mutationOptions and mutationKey methods.
75
85
  */
76
86
  type DecorateMutation<T extends FunctionReference<'mutation'>> = {
77
- mutationOptions: (opts?: DistributiveOmit<UseMutationOptions<FunctionReturnType<T>, DefaultError, MutationVariables<T>>, ReservedMutationOptions$1>) => UseMutationOptions<FunctionReturnType<T>, DefaultError, MutationVariables<T>>; /** Get mutation key for QueryClient methods */
87
+ mutationOptions: (opts?: DistributiveOmit<UseMutationOptions<FunctionReturnType<T>, DefaultError, MutationVariables<T>>, ReservedMutationOptions$1> & ConvexOptimisticUpdateOption<FunctionArgs<T>>) => UseMutationOptions<FunctionReturnType<T>, DefaultError, MutationVariables<T>>; /** Get mutation key for QueryClient methods */
78
88
  mutationKey: () => ConvexMutationKey;
79
89
  };
80
90
  /**
package/dist/rsc/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as defaultIsUnauthorized } from "../error-CLgc9khF.js";
2
2
  import { n as getFuncRef, r as getFunctionMeta, t as buildMetaIndex } from "../meta-utils-Cc7neQOB.js";
3
3
  import { o as encodeWire, s as getTransformer } from "../transformer-C8lVwHNT.js";
4
- import { c as HTTP_DEFAULT_STALE_TIME, n as convexInfiniteQueryOptions, o as executeHttpRequest, r as convexQuery, u as buildHttpQueryKey } from "../query-options-DGG93--5.js";
4
+ import { f as buildHttpQueryKey, n as convexInfiniteQueryOptions, r as convexQuery, s as executeHttpRequest, u as HTTP_DEFAULT_STALE_TIME } from "../query-options-YkFvau71.js";
5
5
  import { convexToJson } from "convex/values";
6
6
  import { getFunctionName } from "convex/server";
7
7
  import { fetchAction, fetchQuery } from "convex/nextjs";
@@ -525,6 +525,7 @@ function buildSearchParams(query) {
525
525
  else if (value !== void 0 && value !== null) params.append(key, value);
526
526
  return params;
527
527
  }
528
+ const RECHECK_AUTHORIZATION = Symbol.for("kitcn.http.recheckAuthorization");
528
529
  /**
529
530
  * Hono-style HTTP request executor.
530
531
  * Processes args in the same way as Hono's ClientRequestImpl.fetch().
@@ -563,6 +564,8 @@ async function executeHttpRequest(opts) {
563
564
  for (const [key, value] of Object.entries(resolvedBaseHeaders)) if (value !== void 0) finalHeaders[key] = value;
564
565
  }
565
566
  Object.assign(finalHeaders, headerValues);
567
+ const recheck = resolvedBaseHeaders?.[RECHECK_AUTHORIZATION];
568
+ const sendHeaders = recheck && finalHeaders.Authorization === resolvedBaseHeaders?.Authorization && !recheck() ? Object.fromEntries(Object.entries(finalHeaders).filter(([key]) => key !== "Authorization")) : finalHeaders;
566
569
  let url = opts.convexSiteUrl + path;
567
570
  if (args.params) url = opts.convexSiteUrl + replaceUrlParam(path, args.params);
568
571
  if (args.searchParams) {
@@ -574,7 +577,7 @@ async function executeHttpRequest(opts) {
574
577
  const response = await (mergedClientOpts.fetch ?? opts.baseFetch ?? globalThis.fetch)(url, {
575
578
  body: setBody ? rBody : void 0,
576
579
  method: methodUpperCase,
577
- headers: finalHeaders,
580
+ headers: sendHeaders,
578
581
  ...mergedClientOpts.init
579
582
  });
580
583
  if (!response.ok) {
@@ -470,4 +470,181 @@ function Unauthenticated({ children }) {
470
470
  }
471
471
 
472
472
  //#endregion
473
- export { CRPCClientError as A, decodeJwtExp as C, readAuthSessionFallbackToken as D, readAuthSessionFallbackData as E, isCRPCClientError as M, writeAuthSessionFallbackData as O, useSafeConvexAuth as S, clearAuthSessionFallback as T, useConvexAuthBridge as _, ConvexAuthRecoveryError as a, useIsAuth as b, MaybeAuthenticated as c, isSessionSyncGraceActive as d, useAuth as f, useAuthValue as g, useAuthStore as h, ConvexAuthBridge as i, defaultIsUnauthorized as j, writeAuthSessionFallbackToken as k, MaybeUnauthenticated as l, useAuthState as m, AuthProvider as n, ConvexProviderWithAuth$1 as o, useAuthGuard as p, Authenticated as r, FetchAccessTokenContext as s, AUTH_SESSION_SYNC_GRACE_MS as t, Unauthenticated as u, useConvexAuthRecovery as v, decodeJwtIdentity as w, useMaybeAuth as x, useFetchAccessToken as y };
473
+ //#region src/react/identity-guard-registry.ts
474
+ const REGISTRY_KEY = Symbol.for("kitcn.identityGuard.v2");
475
+ const createRegistry = () => ({
476
+ documentIdentity: null,
477
+ guards: /* @__PURE__ */ new Set(),
478
+ settledClients: /* @__PURE__ */ new WeakSet(),
479
+ settlementListeners: /* @__PURE__ */ new WeakMap(),
480
+ storeGuards: /* @__PURE__ */ new WeakMap(),
481
+ tripListeners: /* @__PURE__ */ new Set(),
482
+ tripped: false,
483
+ watchedClients: /* @__PURE__ */ new WeakSet()
484
+ });
485
+ const isRegistry = (value) => {
486
+ const candidate = value;
487
+ return typeof candidate === "object" && candidate !== null && typeof candidate.tripped === "boolean" && (candidate.documentIdentity === null || typeof candidate.documentIdentity === "string") && candidate.tripListeners instanceof Set && candidate.guards instanceof Set && candidate.storeGuards instanceof WeakMap && candidate.settledClients instanceof WeakSet && candidate.settlementListeners instanceof WeakMap && candidate.watchedClients instanceof WeakSet;
488
+ };
489
+ let incompatibleStandIn;
490
+ const pageRegistry = () => {
491
+ if (typeof window === "undefined") return null;
492
+ const scope = globalThis;
493
+ scope[REGISTRY_KEY] ??= createRegistry();
494
+ const found = scope[REGISTRY_KEY];
495
+ if (isRegistry(found)) return found;
496
+ if (!incompatibleStandIn) {
497
+ console.warn("[kitcn] identity guard state of another kitcn revision is on this page; guarded providers hand out no token until the page reloads.");
498
+ incompatibleStandIn = {
499
+ ...createRegistry(),
500
+ incompatible: true
501
+ };
502
+ }
503
+ return incompatibleStandIn;
504
+ };
505
+ const isDocumentTripped = () => pageRegistry()?.tripped ?? false;
506
+ const tripDocument = () => {
507
+ const registry = pageRegistry();
508
+ if (!registry || registry.tripped) return;
509
+ registry.tripped = true;
510
+ for (const listener of [...registry.tripListeners]) try {
511
+ listener();
512
+ } catch (error) {
513
+ console.error("[kitcn] identity guard trip listener threw", error);
514
+ }
515
+ };
516
+ const subscribeDocumentTrip = (listener) => {
517
+ const registry = pageRegistry();
518
+ if (!registry) return () => {};
519
+ registry.tripListeners.add(listener);
520
+ return () => {
521
+ registry.tripListeners.delete(listener);
522
+ };
523
+ };
524
+ const attachStoreGuard = (store, guard) => {
525
+ if (store) pageRegistry()?.storeGuards.set(store, guard);
526
+ };
527
+ const storeGuard = (store) => store ? pageRegistry()?.storeGuards.get(store) : void 0;
528
+ const mountGuard = (guard) => {
529
+ const registry = pageRegistry();
530
+ if (!registry) return () => {};
531
+ registry.guards.add(guard);
532
+ joinPage(guard);
533
+ return () => {
534
+ registry.guards.delete(guard);
535
+ };
536
+ };
537
+ const joinPage = (guard) => {
538
+ const registry = pageRegistry();
539
+ if (!registry) return;
540
+ if (guard.guarded && guard.identity) registry.documentIdentity ??= guard.identity;
541
+ for (const mounted of [...registry.guards]) {
542
+ const held = mounted.heldToken();
543
+ if (held && isJwt(held) && !isTokenAdmissible(held, mounted, "hold")) {
544
+ tripDocument();
545
+ break;
546
+ }
547
+ }
548
+ };
549
+ const guardedProviderMounted = () => [...pageRegistry()?.guards ?? []].some((mounted) => mounted.guarded);
550
+ const boundIdentities = (guard) => {
551
+ const registry = pageRegistry();
552
+ const bound = [registry?.documentIdentity ?? null];
553
+ if (guard?.guarded) bound.push(guard.identity, guard.baseline());
554
+ for (const mounted of registry?.guards ?? []) if (mounted.hasGetter) bound.push(mounted.baseline());
555
+ return bound.filter((identity) => identity !== null);
556
+ };
557
+ const identityGuardInPlay = (guard) => !!guard?.guarded || boundIdentities(guard).length > 0 || guardedProviderMounted();
558
+ const isTokenAdmissible = (token, guard, use) => {
559
+ if (isDocumentTripped() || guard?.tripped) return false;
560
+ if (guard?.guarded && pageRegistry()?.incompatible) return false;
561
+ const bound = boundIdentities(guard);
562
+ if (!isJwt(token)) {
563
+ if (use === "hold") return true;
564
+ if (use === "restore") return bound.length === 0;
565
+ return !identityGuardInPlay(guard);
566
+ }
567
+ const identity = decodeTokenSubjectSessionIdentity(token);
568
+ return identity === null ? bound.length === 0 : bound.every((known) => known === identity);
569
+ };
570
+ const admitToken = (token, { announce = false, guard, use }) => {
571
+ if (!isTokenAdmissible(token, guard, use)) {
572
+ if (isJwt(token)) {
573
+ if (guard) guard.tripped = true;
574
+ tripDocument();
575
+ }
576
+ return false;
577
+ }
578
+ const identity = isJwt(token) ? decodeTokenSubjectSessionIdentity(token) : null;
579
+ if (identity !== null && (guard?.guarded || guardedProviderMounted())) {
580
+ if (guard?.guarded) guard.identity = identity;
581
+ const registry = pageRegistry();
582
+ if (registry) registry.documentIdentity ??= identity;
583
+ }
584
+ if (announce && guard?.guarded && isJwt(token)) {
585
+ guard.onAdmitted(token);
586
+ return admitToken(token, {
587
+ guard,
588
+ use
589
+ });
590
+ }
591
+ return !isDocumentTripped() && !guard?.tripped;
592
+ };
593
+ function decodeJwtClaims(token) {
594
+ const segments = token.split(".");
595
+ if (segments.length !== 3 || !segments[1]) return null;
596
+ try {
597
+ const payload = JSON.parse(atob(segments[1].replaceAll("-", "+").replaceAll("_", "/")));
598
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : null;
599
+ } catch {
600
+ return null;
601
+ }
602
+ }
603
+ const isJwt = (token) => decodeJwtClaims(token) !== null;
604
+ /**
605
+ * The user and session a Better Auth Convex JWT speaks for (`sub` and
606
+ * `sessionId`), or null. Other claims (name, email, updatedAt) may change
607
+ * within one session and do not count.
608
+ */
609
+ function decodeTokenSubjectSessionIdentity(token) {
610
+ const claims = token ? decodeJwtClaims(token) : null;
611
+ if (!claims || typeof claims.sub !== "string") return null;
612
+ const sessionId = typeof claims.sessionId === "string" ? claims.sessionId : "";
613
+ return `${claims.sub}|${sessionId}`;
614
+ }
615
+
616
+ //#endregion
617
+ //#region src/react/token-gate.ts
618
+ const admitStoreToken = (authStore, token, options) => admitToken(token, {
619
+ ...options,
620
+ guard: storeGuard(authStore.store)
621
+ });
622
+ const publishToken = (authStore, token, { announce, sessionSyncGraceUntil, use = "hold" }) => {
623
+ if (!admitStoreToken(authStore, token, {
624
+ announce,
625
+ use
626
+ })) return false;
627
+ authStore.set("token", token);
628
+ authStore.set("expiresAt", decodeJwtExp(token));
629
+ if (sessionSyncGraceUntil !== void 0) authStore.set("sessionSyncGraceUntil", sessionSyncGraceUntil);
630
+ return true;
631
+ };
632
+ const publishAuthenticated = (authStore) => {
633
+ const token = authStore.get("token");
634
+ if (isDocumentTripped() || token && !admitStoreToken(authStore, token, { use: "hold" })) return false;
635
+ authStore.set("isAuthenticated", true);
636
+ return true;
637
+ };
638
+ const publishAuthState = (authStore, state) => {
639
+ const tripped = isDocumentTripped();
640
+ authStore.set("isLoading", tripped ? false : state.isLoading);
641
+ if (!state.isAuthenticated || tripped) {
642
+ authStore.set("isAuthenticated", false);
643
+ return;
644
+ }
645
+ const token = authStore.get("token");
646
+ authStore.set("isAuthenticated", !isDocumentTripped() && !(token && !admitStoreToken(authStore, token, { use: "hold" })));
647
+ };
648
+
649
+ //#endregion
650
+ export { useAuthState as A, decodeJwtIdentity as B, FetchAccessTokenContext as C, isSessionSyncGraceActive as D, Unauthenticated as E, useFetchAccessToken as F, writeAuthSessionFallbackToken as G, readAuthSessionFallbackData as H, useIsAuth as I, isCRPCClientError as J, CRPCClientError as K, useMaybeAuth as L, useAuthValue as M, useConvexAuthBridge as N, useAuth as O, useConvexAuthRecovery as P, useSafeConvexAuth as R, ConvexProviderWithAuth$1 as S, MaybeUnauthenticated as T, readAuthSessionFallbackToken as U, clearAuthSessionFallback as V, writeAuthSessionFallbackData as W, AUTH_SESSION_SYNC_GRACE_MS as _, admitToken as a, ConvexAuthBridge as b, identityGuardInPlay as c, isTokenAdmissible as d, joinPage as f, tripDocument as g, subscribeDocumentTrip as h, publishToken as i, useAuthStore as j, useAuthGuard as k, isDocumentTripped as l, pageRegistry as m, publishAuthState as n, attachStoreGuard as o, mountGuard as p, defaultIsUnauthorized as q, publishAuthenticated as r, decodeTokenSubjectSessionIdentity as s, admitStoreToken as t, isJwt as u, AuthProvider as v, MaybeAuthenticated as w, ConvexAuthRecoveryError as x, Authenticated as y, decodeJwtExp as z };