kitcn 0.23.0 → 0.25.0
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/dist/aggregate/index.d.ts +11 -1
- package/dist/aggregate/index.js +1 -1
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/generated/index.js +1 -1
- package/dist/auth/http/index.d.ts +1 -1
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/index.js +10 -9
- package/dist/auth/nextjs/index.d.ts +2 -2
- package/dist/auth/nextjs/index.js +3 -3
- package/dist/auth/start/server/index.d.ts +1 -1
- package/dist/auth/start/server/index.js +1 -1
- package/dist/{builder-CsxVc5xC.js → builder-1Vx-tEnS.js} +3 -3
- package/dist/{caller-factory-CWa0ELLD.js → caller-factory-Dd3H7j3V.js} +3 -3
- package/dist/{where-clause-compiler-B_H3oio5.d.ts → capabilities-Bem7xvGK.d.ts} +2958 -4145
- package/dist/cli.mjs +155 -60
- package/dist/{convex-plugin-D8B0oCFq.js → convex-plugin-DfOhBU9g.js} +1 -1
- package/dist/{create-schema-orm-B3f2Kc8O.js → create-schema-orm-Bu2JCiBS.js} +1 -1
- package/dist/crpc/index.d.ts +3 -3
- package/dist/crpc/index.js +4 -4
- package/dist/{customFunctions-DxEEO4Dq.js → customFunctions-BbhgdRGl.js} +2 -2
- package/dist/definitions-D9vhJ6OV.js +215 -0
- package/dist/extensions-DzjCJXYl.js +55 -0
- package/dist/filter-expression-Dydt8wS0.js +402 -0
- package/dist/{generated-contract-disabled-B-nmd7Ne.d.ts → generated-contract-disabled-BEc4d98x.d.ts} +2 -2
- package/dist/{http-types-BoSDAh4Y.d.ts → http-types-90mapMP9.d.ts} +33 -3
- package/dist/id-Bibg34Yb.js +37 -0
- package/dist/{local-env-yFKub75x.mjs → local-env-Du24tdbp.mjs} +2243 -2113
- package/dist/{middleware-Br5zubu7.d.ts → middleware-CE_MC6Gi.d.ts} +1 -1
- package/dist/{middleware-DUd1Sj39.js → middleware-cJDRHTRY.js} +1 -1
- package/dist/orm/aggregate-index/index.d.ts +20 -0
- package/dist/orm/aggregate-index/index.js +2136 -0
- package/dist/orm/index.d.ts +5 -4
- package/dist/orm/index.js +126 -4292
- package/dist/orm/migrations/index.d.ts +3 -0
- package/dist/orm/migrations/index.js +506 -0
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/{procedure-caller-DQxnLBS_.js → procedure-caller-DIzXfX2E.js} +2 -2
- package/dist/{procedure-name-C55TynK3.d.ts → procedure-name-B09-cf1-.d.ts} +3 -3
- package/dist/{query-context-BzihIpnM.js → query-context-DOm5Xm3H.js} +1 -402
- package/dist/{query-options-C_eBSIXG.js → query-options-DUiOj01a.js} +33 -1
- package/dist/ratelimit/index.d.ts +47 -11
- package/dist/ratelimit/index.js +256 -102
- package/dist/react/index.d.ts +9 -6
- package/dist/react/index.js +36 -17
- package/dist/rsc/index.d.ts +6 -9
- package/dist/rsc/index.js +7 -11
- package/dist/runtime-DfGsZtx3.js +1074 -0
- package/dist/schema-Besl-rpv.js +1411 -0
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +4 -4
- package/dist/solid/index.d.ts +9 -6
- package/dist/solid/index.js +36 -17
- package/dist/{runtime-6DJxpDfb.js → table-rmJm5Qio.js} +3 -1095
- package/dist/{token-xlpENMVn.js → token-DcV_0fkF.js} +1 -1
- package/dist/{types-C0Xl7P8K.d.ts → types-In-ZZOsd.d.ts} +1 -1
- package/dist/{validators-C7LelqTN.js → validators-Dmbhsd3U.js} +1 -1
- package/dist/watcher.mjs +1 -1
- package/dist/where-clause-compiler-mohWFiG0.d.ts +2042 -0
- package/package.json +3 -2
- package/skills/kitcn/SKILL.md +19 -18
- package/skills/kitcn/references/features/aggregates.md +21 -0
- package/skills/kitcn/references/features/auth-organizations.md +3 -3
- package/skills/kitcn/references/features/auth-polar.md +2 -1
- package/skills/kitcn/references/features/auth.md +8 -1
- package/skills/kitcn/references/features/http.md +26 -1
- package/skills/kitcn/references/features/ratelimit.md +11 -0
- package/skills/kitcn/references/features/react.md +1 -1
- package/skills/kitcn/references/setup/auth.md +52 -42
- package/skills/kitcn/references/setup/index.md +1 -1
- package/skills/kitcn/references/setup/server.md +37 -6
- /package/dist/{api-entry-N3nBOlI2.js → api-entry-CkDpGYVg.js} +0 -0
- /package/dist/{context-utils-BBUtBqjN.d.ts → context-utils-DwZ3Cam1.d.ts} +0 -0
- /package/dist/{create-schema-BXrKE2YY.js → create-schema-BRFjdgRr.js} +0 -0
- /package/dist/{error-Bvo7YEhk.js → error-CMLeCadS.js} +0 -0
- /package/dist/{generated-contract-disabled-BXaz7JCE.js → generated-contract-disabled-CZa0iyV0.js} +0 -0
- /package/dist/{meta-utils-D9K4fICl.js → meta-utils-D2lqN2Lg.js} +0 -0
- /package/dist/{query-context-CNo9ffvI.d.ts → query-context-DJONf8X5.d.ts} +0 -0
- /package/dist/{token-B9Bjcqug.d.ts → token-kQaqFby4.d.ts} +0 -0
- /package/dist/{transformer-yZuBWo8v.js → transformer-D8wO-kEj.js} +0 -0
- /package/dist/{types-BCl8gfGy.d.ts → types-Bf3XQex5.d.ts} +0 -0
- /package/dist/{types-DF2cg_w0.d.ts → types-MHYXboyy.d.ts} +0 -0
- /package/dist/{types-CnTpHR1F.d.ts → types-eNzQmbqM.d.ts} +0 -0
- /package/dist/{upstream-BR6sBLg3.js → upstream-BCgGZX6q.js} +0 -0
- /package/dist/{validators-BhsByJeg.d.ts → validators-V4eM2-Jg.d.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kitcn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "kitcn - React Query integration and CLI tools for Convex",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"convex",
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"./auth/start/server": "./dist/auth/start/server/index.js",
|
|
32
32
|
"./crpc": "./dist/crpc/index.js",
|
|
33
33
|
"./orm": "./dist/orm/index.js",
|
|
34
|
+
"./orm/aggregate-index": "./dist/orm/aggregate-index/index.js",
|
|
35
|
+
"./orm/migrations": "./dist/orm/migrations/index.js",
|
|
34
36
|
"./plugins": "./dist/plugins/index.js",
|
|
35
37
|
"./ratelimit": "./dist/ratelimit/index.js",
|
|
36
38
|
"./ratelimit/react": "./dist/ratelimit/react/index.js",
|
|
@@ -73,7 +75,6 @@
|
|
|
73
75
|
"jose": "^6.1.3",
|
|
74
76
|
"picocolors": "^1.1.1",
|
|
75
77
|
"remeda": "^2.33.6",
|
|
76
|
-
"svix": "^1.84.1",
|
|
77
78
|
"typescript": "5.9.3",
|
|
78
79
|
"type-fest": "^5.4.4"
|
|
79
80
|
},
|
package/skills/kitcn/SKILL.md
CHANGED
|
@@ -171,10 +171,9 @@ Schema rules that matter:
|
|
|
171
171
|
### 2) Procedure Builders + Middleware
|
|
172
172
|
|
|
173
173
|
```ts
|
|
174
|
-
import {
|
|
174
|
+
import { getSession } from "kitcn/auth";
|
|
175
175
|
import { CRPCError } from "kitcn/server";
|
|
176
|
-
import {
|
|
177
|
-
import { initCRPC } from "../functions/generated/server";
|
|
176
|
+
import { initCRPC, type QueryCtx } from "../functions/generated/server";
|
|
178
177
|
|
|
179
178
|
const c = initCRPC
|
|
180
179
|
.meta<{
|
|
@@ -190,40 +189,42 @@ function requireAuth<T>(user: T | null): T {
|
|
|
190
189
|
}
|
|
191
190
|
return user;
|
|
192
191
|
}
|
|
192
|
+
async function getSessionUser(ctx: QueryCtx) {
|
|
193
|
+
const session = await getSession(ctx);
|
|
194
|
+
if (!session) return null;
|
|
195
|
+
return await ctx.orm.query.user.findFirst({
|
|
196
|
+
where: { id: { eq: session.userId } },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
193
200
|
export const publicQuery = c.query.meta({ auth: "optional" });
|
|
194
201
|
export const authQuery = c.query
|
|
195
202
|
.meta({ auth: "required" })
|
|
196
203
|
.use(async ({ ctx, next }) => {
|
|
197
|
-
const
|
|
198
|
-
const session = await auth.api.getSession({
|
|
199
|
-
headers: await getHeaders(ctx),
|
|
200
|
-
});
|
|
201
|
-
const user = requireAuth(session?.user ?? null);
|
|
204
|
+
const user = requireAuth(await getSessionUser(ctx));
|
|
202
205
|
return next({ ctx: { ...ctx, user, userId: user.id } });
|
|
203
206
|
});
|
|
204
207
|
export const authMutation = c.mutation
|
|
205
208
|
.meta({ auth: "optional" })
|
|
206
209
|
.use(async ({ ctx, next }) => {
|
|
207
|
-
const
|
|
208
|
-
const session = await auth.api.getSession({
|
|
209
|
-
headers: await getHeaders(ctx),
|
|
210
|
-
});
|
|
210
|
+
const user = await getSessionUser(ctx);
|
|
211
211
|
return next({
|
|
212
|
-
ctx: {
|
|
213
|
-
...ctx,
|
|
214
|
-
user: session?.user ?? null,
|
|
215
|
-
userId: session?.user?.id ?? null,
|
|
216
|
-
},
|
|
212
|
+
ctx: { ...ctx, user, userId: user?.id ?? null },
|
|
217
213
|
});
|
|
218
214
|
});
|
|
219
215
|
```
|
|
220
216
|
|
|
221
217
|
Builder rules that matter:
|
|
222
218
|
|
|
223
|
-
1. Build `public`, `optional`, `auth`, and `private` procedure families once in `convex/lib/crpc.ts`.
|
|
219
|
+
1. Build `public`, `optional`, `auth`, and `private` procedure families once in `convex/lib/crpc.ts`. Authenticated action builders live in `convex/lib/crpc-action.ts`, the only builder module that imports `getAuth`.
|
|
224
220
|
2. `.meta(...)` is client-visible via generated API metadata. Never put secrets there.
|
|
225
221
|
3. Middleware receives server-only `procedure` info. When procedures are built from your app `generated/server` helper, standard `export const` queries, mutations, and actions infer `module:function` automatically from file path + export name. Use `.name("module:function")` only to override or cover unusual export shapes.
|
|
226
222
|
4. Resolve session/user once in middleware. Do not re-fetch auth state in every procedure.
|
|
223
|
+
Query/mutation middleware uses `getSession(ctx)` from `kitcn/auth`, which reads the session row
|
|
224
|
+
directly. Keep `getAuth(ctx)` out of `convex/lib/crpc.ts`: it pulls the whole Better Auth
|
|
225
|
+
definition and every auth plugin into the static import closure of every procedure module, and
|
|
226
|
+
Convex has no dynamic `import()` to escape it. Import `getAuth` only in the modules that call
|
|
227
|
+
`auth.api.*` — `convex/lib/crpc-action.ts`, HTTP routes, and organization/admin mutations.
|
|
227
228
|
5. Shared `c.middleware()` chains preserve mutation writer types on mutation procedures. If the middleware itself performs writes, type it as mutation-only with `c.middleware<MutationCtx>(...)`.
|
|
228
229
|
6. Keep deeper auth/runtime edge cases in `references/setup/server.md` and `references/features/auth*.md`.
|
|
229
230
|
|
|
@@ -9,6 +9,27 @@ Canonical runtime rules:
|
|
|
9
9
|
- Use `rankIndex` + `rank()` for rankings, random access, sorted pagination
|
|
10
10
|
- `aggregateIndex` and `rankIndex` backfill automatically via `kitcn dev` — no manual trigger wiring needed
|
|
11
11
|
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
Aggregate and rank indexes are an optional ORM subsystem. Register it where the ORM is created:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createOrm } from "kitcn/orm";
|
|
18
|
+
import { aggregateCapability } from "kitcn/orm/aggregate-index";
|
|
19
|
+
|
|
20
|
+
export const orm = createOrm({ schema, capabilities: [aggregateCapability()] });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`kitcn codegen` writes this into `<functionsDir>/generated/server.ts` for schemas that declare an
|
|
24
|
+
`aggregateIndex` or `rankIndex`, and leaves it out otherwise. Convex bundles everything a module
|
|
25
|
+
statically imports and has no dynamic `import()`, and every procedure module imports
|
|
26
|
+
`generated/server.ts`, so an unused registration would put the aggregate btree in every Convex
|
|
27
|
+
function of the app. Declare the index first, then rerun `kitcn codegen`.
|
|
28
|
+
|
|
29
|
+
Without the capability, `aggregateIndex`/`rankIndex` schemas and filtered `count()` / `aggregate()`
|
|
30
|
+
/ `groupBy()` / `rank()` / relation `_count` throw a setup error. Unfiltered `count()` and
|
|
31
|
+
`aggregate({ _count: true })` are served by the native Convex count syscall and need nothing.
|
|
32
|
+
|
|
12
33
|
## ORM Scalar Metrics
|
|
13
34
|
|
|
14
35
|
### `aggregateIndex` Schema Declaration
|
|
@@ -438,9 +438,9 @@ export const updateOrganization = authMutation
|
|
|
438
438
|
});
|
|
439
439
|
```
|
|
440
440
|
|
|
441
|
-
Use an `authAction` instead of an `authMutation` for
|
|
442
|
-
that can run external plugin work such as Stripe,
|
|
443
|
-
Convex mutations cannot call those SDKs.
|
|
441
|
+
Use an `authAction` (from `../lib/crpc-action`) instead of an `authMutation` for
|
|
442
|
+
any Better Auth endpoint that can run external plugin work such as Stripe,
|
|
443
|
+
Polar, or email delivery. Convex mutations cannot call those SDKs.
|
|
444
444
|
|
|
445
445
|
### Delete Organization
|
|
446
446
|
|
|
@@ -435,7 +435,8 @@ export const updateUserPolarCustomerId = privateMutation
|
|
|
435
435
|
import '../lib/polar-polyfills';
|
|
436
436
|
import { CRPCError } from 'kitcn/server';
|
|
437
437
|
import { z } from 'zod';
|
|
438
|
-
import {
|
|
438
|
+
import { privateMutation, privateQuery } from '../lib/crpc';
|
|
439
|
+
import { authAction } from '../lib/crpc-action';
|
|
439
440
|
import { getPolarClient } from '../lib/polar-client';
|
|
440
441
|
import { createPolarSubscriptionCaller } from './generated/polarSubscription.runtime';
|
|
441
442
|
|
|
@@ -15,7 +15,14 @@ Covers Better Auth integration with Convex: server setup, client hooks, triggers
|
|
|
15
15
|
| Query/Mutation (`ctx.db`) | Direct DB | No `runQuery`/`runMutation` wrapper |
|
|
16
16
|
| Action/HTTP | HTTP adapter | Uses `ctx.run*` APIs |
|
|
17
17
|
|
|
18
|
-
**
|
|
18
|
+
**Entrypoints**:
|
|
19
|
+
|
|
20
|
+
- `getSession(ctx)` from `kitcn/auth` in queries and mutations, including the shared cRPC auth
|
|
21
|
+
middleware. It reads the session row directly and keeps the Better Auth definition out of the
|
|
22
|
+
importing module's static closure.
|
|
23
|
+
- `getAuth(ctx)` only where `auth.api.*` is actually called: `convex/lib/crpc-action.ts`, HTTP
|
|
24
|
+
routes, and organization/admin mutations. Convex has no dynamic `import()`, so every module that
|
|
25
|
+
names `getAuth` carries the whole auth definition and its plugins into its evaluation closure.
|
|
19
26
|
|
|
20
27
|
## Auth Flow
|
|
21
28
|
|
|
@@ -495,8 +495,18 @@ crpc.http.todos.list.queryOptions({
|
|
|
495
495
|
searchParams: { limit: "10" },
|
|
496
496
|
headers: { "X-Custom": "value" },
|
|
497
497
|
});
|
|
498
|
+
|
|
499
|
+
// Override the default 30s staleTime
|
|
500
|
+
crpc.http.todos.list.queryOptions(
|
|
501
|
+
{ searchParams: { limit: "10" } },
|
|
502
|
+
{ staleTime: 0 }
|
|
503
|
+
);
|
|
498
504
|
```
|
|
499
505
|
|
|
506
|
+
`queryOptions` defaults to a 30 second `staleTime` so RSC-prefetched entries hydrate
|
|
507
|
+
without an immediate refetch. `refetchOnMount` keeps TanStack's default, so a route
|
|
508
|
+
invalidated while unmounted still refetches.
|
|
509
|
+
|
|
500
510
|
### One-Time Fetch
|
|
501
511
|
|
|
502
512
|
```ts
|
|
@@ -558,6 +568,20 @@ const updateTodo = useMutation(
|
|
|
558
568
|
);
|
|
559
569
|
```
|
|
560
570
|
|
|
571
|
+
### Cache Keys
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
const queryKey = crpc.http.todos.list.queryKey();
|
|
575
|
+
// => ['httpQuery', 'todos.list', {}]
|
|
576
|
+
queryClient.getQueryData(queryKey);
|
|
577
|
+
|
|
578
|
+
crpc.http.todos.get.queryKey({ params: { id: '123' } });
|
|
579
|
+
// => ['httpQuery', 'todos.get', { params: { id: '123' } }]
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
`queryKey()` is the exact key `queryOptions()` and RSC prefetching store under, so
|
|
583
|
+
`getQueryData`/`setQueryData` hit. Use `queryFilter()` to match every args variant.
|
|
584
|
+
|
|
561
585
|
See [Client Methods](#client-methods) in API Reference.
|
|
562
586
|
|
|
563
587
|
## RSC Prefetching
|
|
@@ -645,4 +669,5 @@ Client args can include `params`, `searchParams`, `form`, custom `fetch`,
|
|
|
645
669
|
### Client Methods
|
|
646
670
|
|
|
647
671
|
Generated clients expose `queryOptions`, `staticQueryOptions`,
|
|
648
|
-
`mutationOptions`, `queryKey
|
|
672
|
+
`mutationOptions`, `queryKey` (exact cache key), and `queryFilter` (matches every
|
|
673
|
+
args variant when called without args).
|
|
@@ -81,6 +81,17 @@ For denied reserved fixed-window and token-bucket requests, `reset` is when the
|
|
|
81
81
|
|
|
82
82
|
A request larger than every shard's capacity plus finite reservation headroom can never succeed. It returns `reason: 'requestTooLarge'` and `reset: 0` without reading shard state. When only smaller shards cannot serve a request, they are excluded from retry calculations. Fresh, full, and partial snapshot projections preserve permanent denial as `retryAfter: Infinity`, and the React hook reports `ok: false` without scheduling a retry timer. Reduce `count`, reduce `shards`, or raise the configured capacity.
|
|
83
83
|
|
|
84
|
+
## Protection / deny list — delta from parity
|
|
85
|
+
|
|
86
|
+
With `enableProtection: true`, failures are counted per identifier, ip, userAgent, and country.
|
|
87
|
+
|
|
88
|
+
- A value's block is cached for up to 24 h once it reaches `denyListThreshold` **within a rolling 10 minute window**. Failures paced wider than that window decay, so a shared NAT/carrier IP is not blocked by unrelated failures accumulated over days.
|
|
89
|
+
- Protection state is a bounded in-memory LRU. Failure histories are evicted before blocks, active blocks refresh on use, and the coldest block can be evicted above 4,096 simultaneous blocked values per prefix. Evicted values fall through to the database-backed limiter.
|
|
90
|
+
- A success clears the **identifier** counter only. ip/userAgent counters are attacker-supplied; clearing them on success would let a caller loop `threshold - 1` failures plus one success indefinitely, or reset a victim's counter by forging their user-agent.
|
|
91
|
+
- State is module-scope memory: at most 4096 tracked values per prefix with least-recently-hit eviction, and values over 128 characters stored truncated. It is per-isolate, so it does not survive a cold start and is not shared across function entries.
|
|
92
|
+
- Increments are not rolled back when Convex retries a mutation after a write conflict, so a value can count slightly more failures than it was served.
|
|
93
|
+
- `pickDeniedValue` runs before any database read, so a blocked value is rejected without touching `ratelimitState`.
|
|
94
|
+
|
|
84
95
|
## Convex constraints
|
|
85
96
|
|
|
86
97
|
- `blockUntilReady()` needs `setTimeout`, so it only runs in actions or non-Convex runtimes. It throws with that guidance inside queries and mutations. `limit()` and `check()` never touch timers.
|
|
@@ -28,7 +28,7 @@ export const { CRPCProvider, useCRPC, useCRPCClient } = createCRPCContext({
|
|
|
28
28
|
|
|
29
29
|
### QueryClient
|
|
30
30
|
|
|
31
|
-
cRPC auto-sets `staleTime: Infinity`, `refetch*: false` per query (Convex pushes via WebSocket — never stale).
|
|
31
|
+
cRPC auto-sets `staleTime: Infinity`, `refetch*: false` per query (Convex pushes via WebSocket — never stale). `crpc.http.*` routes are pull-model instead: `staleTime: 30_000` with `refetchOnMount` left at TanStack's default, so RSC-prefetched data hydrates but an invalidated route still refetches.
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
34
|
// src/lib/convex/query-client.ts
|
|
@@ -331,11 +331,10 @@ Again: printed payload only. You still need to set the env manually.
|
|
|
331
331
|
After non-auth baseline is green, replace `convex/lib/crpc.ts` with this auth-aware variant:
|
|
332
332
|
|
|
333
333
|
```ts
|
|
334
|
-
import {
|
|
334
|
+
import { getSession } from "kitcn/auth";
|
|
335
335
|
import { CRPCError } from "kitcn/server";
|
|
336
336
|
|
|
337
|
-
import {
|
|
338
|
-
import { initCRPC } from "../functions/generated/server";
|
|
337
|
+
import { initCRPC, type QueryCtx } from "../functions/generated/server";
|
|
339
338
|
|
|
340
339
|
const c = initCRPC
|
|
341
340
|
.meta<{
|
|
@@ -366,6 +365,14 @@ function requireAuth<T>(user: T | null): T {
|
|
|
366
365
|
return user;
|
|
367
366
|
}
|
|
368
367
|
|
|
368
|
+
async function getSessionUser(ctx: QueryCtx) {
|
|
369
|
+
const session = await getSession(ctx);
|
|
370
|
+
if (!session) return null;
|
|
371
|
+
return await ctx.orm.query.user.findFirst({
|
|
372
|
+
where: { id: { eq: session.userId } },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
369
376
|
export const publicQuery = c.query.meta({ auth: "optional" });
|
|
370
377
|
export const publicAction = c.action;
|
|
371
378
|
export const publicMutation = c.mutation;
|
|
@@ -377,28 +384,17 @@ export const privateAction = c.action.internal();
|
|
|
377
384
|
export const optionalAuthQuery = c.query
|
|
378
385
|
.meta({ auth: "optional" })
|
|
379
386
|
.use(async ({ ctx, next }) => {
|
|
380
|
-
const
|
|
381
|
-
const session = await auth.api.getSession({
|
|
382
|
-
headers: await getHeaders(ctx),
|
|
383
|
-
});
|
|
387
|
+
const user = await getSessionUser(ctx);
|
|
384
388
|
|
|
385
389
|
return next({
|
|
386
|
-
ctx: {
|
|
387
|
-
...ctx,
|
|
388
|
-
user: session?.user ?? null,
|
|
389
|
-
userId: session?.user?.id ?? null,
|
|
390
|
-
},
|
|
390
|
+
ctx: { ...ctx, user, userId: user?.id ?? null },
|
|
391
391
|
});
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
export const authQuery = c.query
|
|
395
395
|
.meta({ auth: "required" })
|
|
396
396
|
.use(async ({ ctx, next }) => {
|
|
397
|
-
const
|
|
398
|
-
const session = await auth.api.getSession({
|
|
399
|
-
headers: await getHeaders(ctx),
|
|
400
|
-
});
|
|
401
|
-
const user = requireAuth(session?.user ?? null);
|
|
397
|
+
const user = requireAuth(await getSessionUser(ctx));
|
|
402
398
|
return next({ ctx: { ...ctx, user, userId: user.id } });
|
|
403
399
|
})
|
|
404
400
|
.use(roleMiddleware);
|
|
@@ -406,43 +402,21 @@ export const authQuery = c.query
|
|
|
406
402
|
export const optionalAuthMutation = c.mutation
|
|
407
403
|
.meta({ auth: "optional" })
|
|
408
404
|
.use(async ({ ctx, next }) => {
|
|
409
|
-
const
|
|
410
|
-
const session = await auth.api.getSession({
|
|
411
|
-
headers: await getHeaders(ctx),
|
|
412
|
-
});
|
|
405
|
+
const user = await getSessionUser(ctx);
|
|
413
406
|
|
|
414
407
|
return next({
|
|
415
|
-
ctx: {
|
|
416
|
-
...ctx,
|
|
417
|
-
user: session?.user ?? null,
|
|
418
|
-
userId: session?.user?.id ?? null,
|
|
419
|
-
},
|
|
408
|
+
ctx: { ...ctx, user, userId: user?.id ?? null },
|
|
420
409
|
});
|
|
421
410
|
});
|
|
422
411
|
|
|
423
412
|
export const authMutation = c.mutation
|
|
424
413
|
.meta({ auth: "required" })
|
|
425
414
|
.use(async ({ ctx, next }) => {
|
|
426
|
-
const
|
|
427
|
-
const session = await auth.api.getSession({
|
|
428
|
-
headers: await getHeaders(ctx),
|
|
429
|
-
});
|
|
430
|
-
const user = requireAuth(session?.user ?? null);
|
|
415
|
+
const user = requireAuth(await getSessionUser(ctx));
|
|
431
416
|
return next({ ctx: { ...ctx, user, userId: user.id } });
|
|
432
417
|
})
|
|
433
418
|
.use(roleMiddleware);
|
|
434
419
|
|
|
435
|
-
export const authAction = c.action
|
|
436
|
-
.meta({ auth: "required" })
|
|
437
|
-
.use(async ({ ctx, next }) => {
|
|
438
|
-
const auth = getAuth(ctx);
|
|
439
|
-
const session = await auth.api.getSession({
|
|
440
|
-
headers: await getHeaders(ctx),
|
|
441
|
-
});
|
|
442
|
-
const user = requireAuth(session?.user ?? null);
|
|
443
|
-
return next({ ctx: { ...ctx, user, userId: user.id } });
|
|
444
|
-
});
|
|
445
|
-
|
|
446
420
|
export const publicRoute = c.httpAction;
|
|
447
421
|
export const authRoute = c.httpAction.use(async ({ ctx, next }) => {
|
|
448
422
|
const identity = await ctx.auth.getUserIdentity();
|
|
@@ -480,6 +454,42 @@ export const optionalAuthRoute = c.httpAction.use(async ({ ctx, next }) => {
|
|
|
480
454
|
export const router = c.router;
|
|
481
455
|
```
|
|
482
456
|
|
|
457
|
+
### 6.9.1 Authenticated action builders
|
|
458
|
+
|
|
459
|
+
Authenticated actions live in their own module. Actions have no `ctx.db`, so they resolve the
|
|
460
|
+
session through `auth.api.getSession` instead of `getSession(ctx)`. `getAuth` carries the whole
|
|
461
|
+
Better Auth definition and every plugin into the static import closure of whatever imports it, and
|
|
462
|
+
Convex has no dynamic `import()`, so it stays out of `convex/lib/crpc.ts` — the module every
|
|
463
|
+
procedure imports.
|
|
464
|
+
|
|
465
|
+
Skip this file if the app has no authenticated actions.
|
|
466
|
+
|
|
467
|
+
**Create:** `convex/lib/crpc-action.ts`
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { getHeaders } from "kitcn/auth";
|
|
471
|
+
import { CRPCError } from "kitcn/server";
|
|
472
|
+
|
|
473
|
+
import { getAuth } from "../functions/generated/auth";
|
|
474
|
+
import { publicAction } from "./crpc";
|
|
475
|
+
|
|
476
|
+
export const authAction = publicAction
|
|
477
|
+
.meta({ auth: "required" })
|
|
478
|
+
.use(async ({ ctx, next }) => {
|
|
479
|
+
const auth = getAuth(ctx);
|
|
480
|
+
const session = await auth.api.getSession({
|
|
481
|
+
headers: await getHeaders(ctx),
|
|
482
|
+
});
|
|
483
|
+
const user = session?.user;
|
|
484
|
+
if (!user) {
|
|
485
|
+
throw new CRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
|
|
486
|
+
}
|
|
487
|
+
return next({ ctx: { ...ctx, user, userId: user.id } });
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Import `authAction` from `../lib/crpc-action`. Every other builder comes from `../lib/crpc`.
|
|
492
|
+
|
|
483
493
|
### 6.10 Auth sign-in gate (required before Section 7+ and all optional modules/plugins)
|
|
484
494
|
|
|
485
495
|
Do not continue until all checks below pass:
|
|
@@ -755,7 +755,7 @@ domain depth: `orm.md`, `http.md`, `react.md`, `aggregates.md`,
|
|
|
755
755
|
| HTTP calls fail but queries work | `.site` URL missing or wrong | Set `NEXT_PUBLIC_CONVEX_SITE_URL` correctly |
|
|
756
756
|
| Auth works locally but fails in prod | JWKS not pushed | Run `bunx kitcn env push --prod` |
|
|
757
757
|
| Sign-in fails on `/auth` (loop, no session, or immediate sign-out) | Auth route/env/provider wiring mismatch | Recheck Sections 6.5-6.7 (`authMiddleware`, route registration, env push), verify provider credentials/URLs, then rerun Section 11.3 |
|
|
758
|
-
| `UNAUTHORIZED` on protected procedures | auth middleware not attaching `userId` | Ensure
|
|
758
|
+
| `UNAUTHORIZED` on protected procedures | auth middleware not attaching `userId` | Ensure the `getSession(ctx)` session lookup is in middleware |
|
|
759
759
|
| `ctx.orm` missing in handlers | Generated `initCRPC` not used | Use `initCRPC` from `../functions/generated/server` — ORM context is pre-wired |
|
|
760
760
|
| `Property 'insert'/'update' does not exist on type 'OrmReader'` | Using query context for mutations | Ensure mutation handlers use `publicMutation` / `protectedMutation` builders |
|
|
761
761
|
| `useCRPC must be used within CRPCProvider` | Provider chain not mounted around route tree | Wrap app with `AppConvexProvider` and verify `CRPCProvider` is inside QueryClientProvider (Section 7.4 / 8.A.4) |
|
|
@@ -175,7 +175,8 @@ export const router = c.router;
|
|
|
175
175
|
Phase ordering rule:
|
|
176
176
|
|
|
177
177
|
1. Keep this non-auth baseline until Section 11.2 fully passes.
|
|
178
|
-
2. Only then replace this file with the auth-aware variant in Section 6.9
|
|
178
|
+
2. Only then replace this file with the auth-aware variant in Section 6.9, and add
|
|
179
|
+
`convex/lib/crpc-action.ts` from Section 6.9.1 if the app has authenticated actions.
|
|
179
180
|
|
|
180
181
|
### 5.4 Shared API/type helpers (generated)
|
|
181
182
|
|
|
@@ -356,7 +357,7 @@ Rate limiting is opt-in: scaffold the full starter once.
|
|
|
356
357
|
bunx kitcn add ratelimit
|
|
357
358
|
```
|
|
358
359
|
|
|
359
|
-
This creates `convex/lib/plugins/ratelimit/schema.ts`, `convex/lib/plugins/ratelimit/plugin.ts`, and registers `ratelimitExtension()` in `convex/functions/schema.ts`.
|
|
360
|
+
This creates `convex/lib/plugins/ratelimit/schema.ts`, `convex/lib/plugins/ratelimit/plugin.ts`, `convex/functions/plugins/ratelimit.ts`, and registers `ratelimitExtension()` in `convex/functions/schema.ts`.
|
|
360
361
|
|
|
361
362
|
```ts
|
|
362
363
|
import { defineSchema } from "kitcn/orm";
|
|
@@ -365,12 +366,18 @@ import { ratelimitExtension } from "../lib/plugins/ratelimit/schema";
|
|
|
365
366
|
export default defineSchema(tables).extend(ratelimitExtension());
|
|
366
367
|
```
|
|
367
368
|
|
|
368
|
-
Create `convex/lib/plugins/ratelimit/plugin.ts` and call `ratelimit.middleware()` from mutation builders. Use the default bucket for normal writes
|
|
369
|
+
Create `convex/lib/plugins/ratelimit/plugin.ts` and call `ratelimit.middleware()` from mutation builders. Use the default bucket for normal writes; the starter's `interactive` bucket demonstrates a named `.meta({ ratelimit: ... })` override. Rename or remove it to match the application. Runtime accounting (`shards`, `check()`, snapshots, read accuracy) lives in `features/ratelimit.md`.
|
|
369
370
|
|
|
370
371
|
Use `RatelimitPlugin` from `kitcn/ratelimit`:
|
|
371
372
|
|
|
372
373
|
```ts
|
|
373
|
-
import {
|
|
374
|
+
import {
|
|
375
|
+
type LimitRequest,
|
|
376
|
+
MINUTE,
|
|
377
|
+
Ratelimit,
|
|
378
|
+
RatelimitPlugin,
|
|
379
|
+
SECOND,
|
|
380
|
+
} from "kitcn/ratelimit";
|
|
374
381
|
import type { MutationCtx } from "../../../functions/generated/server";
|
|
375
382
|
|
|
376
383
|
const fixed = (rate: number) => Ratelimit.fixedWindow(rate, MINUTE);
|
|
@@ -381,6 +388,11 @@ export const ratelimitBuckets = {
|
|
|
381
388
|
free: fixed(60),
|
|
382
389
|
premium: fixed(200),
|
|
383
390
|
},
|
|
391
|
+
interactive: {
|
|
392
|
+
public: Ratelimit.fixedWindow(3, 30 * SECOND),
|
|
393
|
+
free: Ratelimit.fixedWindow(3, 30 * SECOND),
|
|
394
|
+
premium: Ratelimit.fixedWindow(3, 30 * SECOND),
|
|
395
|
+
},
|
|
384
396
|
} as const;
|
|
385
397
|
|
|
386
398
|
type RatelimitTier = keyof (typeof ratelimitBuckets)["default"];
|
|
@@ -415,14 +427,27 @@ async function getRequestSignals(ctx: RatelimitCtx) {
|
|
|
415
427
|
};
|
|
416
428
|
}
|
|
417
429
|
|
|
430
|
+
function getRequestIdentifier(
|
|
431
|
+
user: RatelimitUser | null,
|
|
432
|
+
signals: LimitRequest | undefined
|
|
433
|
+
) {
|
|
434
|
+
if (user) return user.id;
|
|
435
|
+
return signals?.ip ? `ip:${signals.ip}` : "ip:unknown";
|
|
436
|
+
}
|
|
437
|
+
|
|
418
438
|
export const ratelimit = RatelimitPlugin.configure({
|
|
419
439
|
buckets: ratelimitBuckets,
|
|
420
440
|
getBucket: ({ meta }: { meta: RatelimitMeta }) => meta.ratelimit ?? "default",
|
|
421
441
|
getUser: ({ ctx }: { ctx: RatelimitCtx }) => ctx.user ?? null,
|
|
422
|
-
getIdentifier: ({ user }: { user: RatelimitUser | null }) =>
|
|
423
|
-
user?.id ?? "anonymous",
|
|
424
442
|
getTier: getUserTier,
|
|
425
443
|
getSignals: ({ ctx }: { ctx: RatelimitCtx }) => getRequestSignals(ctx),
|
|
444
|
+
getIdentifier: ({
|
|
445
|
+
user,
|
|
446
|
+
signals,
|
|
447
|
+
}: {
|
|
448
|
+
user: RatelimitUser | null;
|
|
449
|
+
signals: LimitRequest | undefined;
|
|
450
|
+
}) => getRequestIdentifier(user, signals),
|
|
426
451
|
prefix: ({ bucket, tier }) => `ratelimit:${bucket}:${tier}`,
|
|
427
452
|
failureMode: "closed",
|
|
428
453
|
enableProtection: true,
|
|
@@ -432,6 +457,12 @@ export const ratelimit = RatelimitPlugin.configure({
|
|
|
432
457
|
|
|
433
458
|
Use `ctx.meta.getRequestMetadata()` on Convex 1.38.0+ for IP/user-agent signals in mutation rate limits. Convex also exposes this metadata in actions; route action-side enforcement through a mutation when database-backed ratelimit state is required.
|
|
434
459
|
|
|
460
|
+
The identifier is the partition key: every request resolving to the same string shares one budget and one `ratelimitState` document. Never key unauthenticated traffic to a constant — `fixedWindow(30, MINUTE)` under a constant identifier is 30 req/min for the whole deployment, and 30 consecutive denials arm a 24 h deny-list block against that constant. `getSignals` runs once per request, before `getIdentifier`, so keying on `signals.ip` costs no extra syscall.
|
|
461
|
+
|
|
462
|
+
Plan for the two consequences: shared NAT/CGNAT addresses share a budget, and calls without request metadata all land on `ip:unknown`. One `ratelimitState` row exists per identifier per `bucket:tier`; run the scaffolded `plugins/ratelimit:cleanup` private mutation on demand, with `olderThanMs` longer than every configured rate-limit window, and repeat while it returns `hasMore: true`.
|
|
463
|
+
|
|
464
|
+
`RatelimitPlugin.configure` also forwards `failureMode`, `timeout`, `enableProtection`, `denyListThreshold`, `denyList`, `dynamicLimits`, and `ephemeralCache` to the limiter.
|
|
465
|
+
|
|
435
466
|
### 9.5 Scheduling gate
|
|
436
467
|
|
|
437
468
|
Create `convex/functions/crons.ts` with `cronJobs()` and use `caller.schedule.now/after/at` in mutations/actions for delayed procedure jobs (`ctx.scheduler.*` only for raw `internal.*` functions).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/dist/{generated-contract-disabled-BXaz7JCE.js → generated-contract-disabled-CZa0iyV0.js}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|