kitcn 0.33.6 → 0.33.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/aggregate/index.d.ts +1 -1
- package/dist/auth/client/index.d.ts +45 -21
- package/dist/auth/client/index.js +203 -111
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/index.d.ts +16 -16
- package/dist/auth/nextjs/index.d.ts +3 -3
- package/dist/auth/nextjs/index.js +3 -2
- package/dist/auth/start/index.js +134 -2
- package/dist/auth/start/server/index.d.ts +1 -1
- package/dist/auth/start/server/index.js +1 -1
- package/dist/crpc/index.d.ts +2 -2
- package/dist/crpc/index.js +2 -2
- package/dist/{generated-contract-disabled-PdNGvNYP.d.ts → generated-contract-disabled-CFLQf8Wg.d.ts} +25 -25
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/migrations/index.d.ts +1 -1
- package/dist/{query-options-DGG93--5.js → query-options-YkFvau71.js} +6 -2
- package/dist/react/index.js +149 -140
- package/dist/rsc/index.d.ts +1 -1
- package/dist/rsc/index.js +1 -1
- package/dist/solid/index.js +4 -1
- package/dist/{token-tlbNKQS0.d.ts → token-COZD00dG.d.ts} +2 -1
- package/dist/{token-CgIvcEZX.js → token-CZbuEcTL.js} +13 -4
- package/dist/{auth-store-x3_wvhTR.js → token-gate-CEzbXArg.js} +178 -1
- package/dist/{types-C7JkpfeY.d.ts → types-Bb0kwteA.d.ts} +5 -1
- package/dist/{where-clause-compiler-BzAJnWt4.d.ts → where-clause-compiler-CTblNxLo.d.ts} +24 -24
- package/package.json +1 -1
- package/skills/kitcn/references/features/auth.md +26 -8
- package/skills/kitcn/references/features/react.md +5 -1
- package/skills/kitcn/references/setup/next.md +48 -1
package/dist/react/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { A as
|
|
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:
|
|
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) => {
|
|
@@ -1498,24 +1502,16 @@ function createCRPCContext(options) {
|
|
|
1498
1502
|
convexSiteUrl: httpOptions.convexSiteUrl,
|
|
1499
1503
|
routes: meta._http,
|
|
1500
1504
|
headers: async () => {
|
|
1501
|
-
if (fetchAccessToken) {
|
|
1502
|
-
const expiresAt = authStore.get("expiresAt");
|
|
1503
|
-
const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
|
|
1504
|
-
const guardedToken = await fetchAccessToken({ forceRefreshToken: !!expiresAt && timeRemaining < 6e4 });
|
|
1505
|
-
const userHeaders = typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers;
|
|
1506
|
-
return guardedToken ? {
|
|
1507
|
-
...userHeaders,
|
|
1508
|
-
Authorization: `Bearer ${guardedToken}`
|
|
1509
|
-
} : { ...userHeaders };
|
|
1510
|
-
}
|
|
1511
|
-
const token = authStore.get("token");
|
|
1512
1505
|
const expiresAt = authStore.get("expiresAt");
|
|
1513
1506
|
const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
|
|
1514
|
-
|
|
1515
|
-
|
|
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,
|
|
1516
1513
|
Authorization: `Bearer ${token}`
|
|
1517
|
-
};
|
|
1518
|
-
return { ...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers };
|
|
1514
|
+
}, admit) : { ...userHeaders };
|
|
1519
1515
|
},
|
|
1520
1516
|
fetch: httpOptions.fetch,
|
|
1521
1517
|
onError: httpOptions.onError,
|
|
@@ -1648,10 +1644,19 @@ function isAuthMutationError(error) {
|
|
|
1648
1644
|
|
|
1649
1645
|
//#endregion
|
|
1650
1646
|
//#region src/react/auth-mutations.ts
|
|
1651
|
-
|
|
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
|
+
};
|
|
1652
1656
|
const waitForAuth = async (store, timeout = 5e3) => {
|
|
1653
1657
|
const start = Date.now();
|
|
1654
1658
|
while (Date.now() - start < timeout) {
|
|
1659
|
+
assertDocumentNotTripped();
|
|
1655
1660
|
if (store.get("token")) return true;
|
|
1656
1661
|
await new Promise((r) => setTimeout(r, 50));
|
|
1657
1662
|
}
|
|
@@ -1676,9 +1681,10 @@ const readReturnedToken = (value) => {
|
|
|
1676
1681
|
const seedReturnedToken = (store, value) => {
|
|
1677
1682
|
const token = readReturnedToken(value);
|
|
1678
1683
|
if (!token) return;
|
|
1679
|
-
store
|
|
1680
|
-
|
|
1681
|
-
|
|
1684
|
+
if (!publishToken(store, token, {
|
|
1685
|
+
announce: true,
|
|
1686
|
+
sessionSyncGraceUntil: Date.now() + AUTH_SESSION_SYNC_GRACE_MS
|
|
1687
|
+
})) throw tokenIdentityChangedError();
|
|
1682
1688
|
if (decodeJwtExp(token) === null) writeAuthSessionFallbackToken(token);
|
|
1683
1689
|
};
|
|
1684
1690
|
const toAuthMutationError = (error) => new AuthMutationError({
|
|
@@ -1753,12 +1759,13 @@ function createAuthMutations(authClient) {
|
|
|
1753
1759
|
mutationFn: async (args) => {
|
|
1754
1760
|
const signInSocial = authClient.signIn?.social;
|
|
1755
1761
|
if (typeof signInSocial !== "function") throw new Error("Auth client does not expose signIn.social");
|
|
1762
|
+
assertDocumentNotTripped();
|
|
1756
1763
|
const res = await callAuthMethod(signInSocial, withDisabledSessionSignal(args));
|
|
1757
1764
|
if (res?.error) throw toAuthMutationError(res.error);
|
|
1758
1765
|
seedReturnedToken(authStoreApi, res);
|
|
1759
1766
|
await hydrateReturnedSession(authClient, res);
|
|
1760
1767
|
await ensureAuth(authStoreApi);
|
|
1761
|
-
authStoreApi
|
|
1768
|
+
if (!publishAuthenticated(authStoreApi)) throw tokenIdentityChangedError();
|
|
1762
1769
|
return res;
|
|
1763
1770
|
}
|
|
1764
1771
|
};
|
|
@@ -1771,12 +1778,13 @@ function createAuthMutations(authClient) {
|
|
|
1771
1778
|
mutationFn: async (args) => {
|
|
1772
1779
|
const signIn = authClient.signIn?.[signInMethod];
|
|
1773
1780
|
if (typeof signIn !== "function") throw new Error(`Auth client does not expose signIn.${signInMethod}`);
|
|
1781
|
+
assertDocumentNotTripped();
|
|
1774
1782
|
const res = await callAuthMethod(signIn, withDisabledSessionSignal(args));
|
|
1775
1783
|
if (res?.error) throw toAuthMutationError(res.error);
|
|
1776
1784
|
seedReturnedToken(authStoreApi, res);
|
|
1777
1785
|
await hydrateReturnedSession(authClient, res);
|
|
1778
1786
|
await ensureAuth(authStoreApi);
|
|
1779
|
-
authStoreApi
|
|
1787
|
+
if (!publishAuthenticated(authStoreApi)) throw tokenIdentityChangedError();
|
|
1780
1788
|
return res;
|
|
1781
1789
|
}
|
|
1782
1790
|
};
|
|
@@ -1788,12 +1796,13 @@ function createAuthMutations(authClient) {
|
|
|
1788
1796
|
mutationFn: async (args) => {
|
|
1789
1797
|
const signUpEmail = authClient.signUp?.email;
|
|
1790
1798
|
if (typeof signUpEmail !== "function") throw new Error("Auth client does not expose signUp.email");
|
|
1799
|
+
assertDocumentNotTripped();
|
|
1791
1800
|
const res = await callAuthMethod(signUpEmail, withDisabledSessionSignal(args));
|
|
1792
1801
|
if (res?.error) throw toAuthMutationError(res.error);
|
|
1793
1802
|
seedReturnedToken(authStoreApi, res);
|
|
1794
1803
|
await hydrateReturnedSession(authClient, res);
|
|
1795
1804
|
await ensureAuth(authStoreApi);
|
|
1796
|
-
authStoreApi
|
|
1805
|
+
if (!publishAuthenticated(authStoreApi)) throw tokenIdentityChangedError();
|
|
1797
1806
|
return res;
|
|
1798
1807
|
}
|
|
1799
1808
|
};
|
package/dist/rsc/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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-
|
|
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
5
|
import { Value } from "convex/values";
|
|
6
6
|
import { FunctionArgs, FunctionReference, FunctionReturnType } from "convex/server";
|
|
7
7
|
import { OptimisticUpdate } from "convex/browser";
|
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 {
|
|
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";
|
package/dist/solid/index.js
CHANGED
|
@@ -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:
|
|
580
|
+
headers: sendHeaders,
|
|
578
581
|
...mergedClientOpts.init
|
|
579
582
|
});
|
|
580
583
|
if (!response.ok) {
|
|
@@ -6,7 +6,8 @@ type GetTokenOptions = {
|
|
|
6
6
|
jwtCache?: {
|
|
7
7
|
enabled: boolean;
|
|
8
8
|
expirationToleranceSeconds?: number;
|
|
9
|
-
isAuthError: (error: unknown) => boolean;
|
|
9
|
+
isAuthError: (error: unknown) => boolean; /** Current Unix time in seconds. Defaults to the system clock. */
|
|
10
|
+
now?: () => number | Promise<number>;
|
|
10
11
|
};
|
|
11
12
|
};
|
|
12
13
|
//#endregion
|
|
@@ -32,14 +32,23 @@ const getToken = async (siteUrl, headers, opts) => {
|
|
|
32
32
|
cookiePrefix: opts?.cookiePrefix
|
|
33
33
|
});
|
|
34
34
|
if (!token) return await fetchToken();
|
|
35
|
+
let claims;
|
|
35
36
|
try {
|
|
36
|
-
|
|
37
|
-
isFresh: false,
|
|
38
|
-
token
|
|
39
|
-
};
|
|
37
|
+
claims = jose.decodeJwt(token);
|
|
40
38
|
} catch (error) {
|
|
41
39
|
console.error("Error decoding JWT", error);
|
|
40
|
+
return await fetchToken();
|
|
41
|
+
}
|
|
42
|
+
if (!claims.exp) return await fetchToken();
|
|
43
|
+
let now;
|
|
44
|
+
if (opts.jwtCache.now) {
|
|
45
|
+
now = await opts.jwtCache.now();
|
|
46
|
+
if (!Number.isFinite(now)) throw new RangeError("JWT cache now must return finite Unix time in seconds");
|
|
42
47
|
}
|
|
48
|
+
if (!isTokenExpired(claims.exp, opts.jwtCache.expirationToleranceSeconds ?? 60, now)) return {
|
|
49
|
+
isFresh: false,
|
|
50
|
+
token
|
|
51
|
+
};
|
|
43
52
|
return await fetchToken();
|
|
44
53
|
};
|
|
45
54
|
|
|
@@ -470,4 +470,181 @@ function Unauthenticated({ children }) {
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
//#endregion
|
|
473
|
-
|
|
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 };
|