kitcn 0.0.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/bin/intent.js +3 -0
  2. package/dist/aggregate/index.d.ts +388 -0
  3. package/dist/aggregate/index.js +37 -0
  4. package/dist/api-entry-BckXqaLb.js +66 -0
  5. package/dist/auth/client/index.d.ts +37 -0
  6. package/dist/auth/client/index.js +217 -0
  7. package/dist/auth/config/index.d.ts +45 -0
  8. package/dist/auth/config/index.js +24 -0
  9. package/dist/auth/generated/index.d.ts +2 -0
  10. package/dist/auth/generated/index.js +3 -0
  11. package/dist/auth/http/index.d.ts +64 -0
  12. package/dist/auth/http/index.js +461 -0
  13. package/dist/auth/index.d.ts +221 -0
  14. package/dist/auth/index.js +1398 -0
  15. package/dist/auth/nextjs/index.d.ts +50 -0
  16. package/dist/auth/nextjs/index.js +81 -0
  17. package/dist/auth-store-Cljlmdmi.js +197 -0
  18. package/dist/builder-CBdG5W6A.js +1974 -0
  19. package/dist/caller-factory-cTXNvYdz.js +216 -0
  20. package/dist/cli.mjs +13264 -0
  21. package/dist/codegen-lF80HSWu.mjs +3416 -0
  22. package/dist/context-utils-HPC5nXzx.d.ts +17 -0
  23. package/dist/create-schema-odyF4kCy.js +156 -0
  24. package/dist/create-schema-orm-DOyiNDCx.js +246 -0
  25. package/dist/crpc/index.d.ts +105 -0
  26. package/dist/crpc/index.js +169 -0
  27. package/dist/customFunctions-C0voKmtx.js +144 -0
  28. package/dist/error-BZEnI7Sq.js +41 -0
  29. package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
  30. package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
  31. package/dist/http-types-DqJubRPJ.d.ts +292 -0
  32. package/dist/meta-utils-0Pu0Nrap.js +117 -0
  33. package/dist/middleware-BUybuv9n.d.ts +34 -0
  34. package/dist/middleware-C2qTZ3V7.js +84 -0
  35. package/dist/orm/index.d.ts +17 -0
  36. package/dist/orm/index.js +10713 -0
  37. package/dist/plugins/index.d.ts +2 -0
  38. package/dist/plugins/index.js +3 -0
  39. package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
  40. package/dist/procedure-caller-MWcxhQDv.js +349 -0
  41. package/dist/query-context-B8o6-8kC.js +1518 -0
  42. package/dist/query-context-CFZqIvD7.d.ts +42 -0
  43. package/dist/query-options-Dw7cOyXl.js +121 -0
  44. package/dist/ratelimit/index.d.ts +269 -0
  45. package/dist/ratelimit/index.js +856 -0
  46. package/dist/ratelimit/react/index.d.ts +76 -0
  47. package/dist/ratelimit/react/index.js +183 -0
  48. package/dist/react/index.d.ts +1284 -0
  49. package/dist/react/index.js +2526 -0
  50. package/dist/rsc/index.d.ts +276 -0
  51. package/dist/rsc/index.js +233 -0
  52. package/dist/runtime-CtvJPkur.js +2453 -0
  53. package/dist/server/index.d.ts +5 -0
  54. package/dist/server/index.js +6 -0
  55. package/dist/solid/index.d.ts +1221 -0
  56. package/dist/solid/index.js +2940 -0
  57. package/dist/transformer-DtDhR3Lc.js +194 -0
  58. package/dist/types-BTb_4BaU.d.ts +42 -0
  59. package/dist/types-BiJE7qxR.d.ts +4 -0
  60. package/dist/types-DEJpkIhw.d.ts +88 -0
  61. package/dist/types-HhO_R6pd.d.ts +213 -0
  62. package/dist/validators-B7oIJCAp.js +279 -0
  63. package/dist/validators-vzRKjBJC.d.ts +88 -0
  64. package/dist/watcher.mjs +96 -0
  65. package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
  66. package/package.json +107 -34
  67. package/skills/convex/SKILL.md +486 -0
  68. package/skills/convex/references/features/aggregates.md +353 -0
  69. package/skills/convex/references/features/auth-admin.md +446 -0
  70. package/skills/convex/references/features/auth-organizations.md +1141 -0
  71. package/skills/convex/references/features/auth-polar.md +579 -0
  72. package/skills/convex/references/features/auth.md +470 -0
  73. package/skills/convex/references/features/create-plugins.md +153 -0
  74. package/skills/convex/references/features/http.md +676 -0
  75. package/skills/convex/references/features/migrations.md +162 -0
  76. package/skills/convex/references/features/orm.md +1166 -0
  77. package/skills/convex/references/features/react.md +657 -0
  78. package/skills/convex/references/features/scheduling.md +267 -0
  79. package/skills/convex/references/features/testing.md +209 -0
  80. package/skills/convex/references/setup/auth.md +501 -0
  81. package/skills/convex/references/setup/biome.md +190 -0
  82. package/skills/convex/references/setup/doc-guidelines.md +145 -0
  83. package/skills/convex/references/setup/index.md +761 -0
  84. package/skills/convex/references/setup/next.md +116 -0
  85. package/skills/convex/references/setup/react.md +175 -0
  86. package/skills/convex/references/setup/server.md +473 -0
  87. package/skills/convex/references/setup/start.md +67 -0
  88. package/LICENSE +0 -21
  89. package/README.md +0 -0
  90. package/dist/index.d.mts +0 -5
  91. package/dist/index.d.mts.map +0 -1
  92. package/dist/index.mjs +0 -6
  93. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,856 @@
1
+ import { l as requireMutationCtx } from "../api-entry-BckXqaLb.js";
2
+ import { h as CRPCError } from "../builder-CBdG5W6A.js";
3
+ import { t as definePlugin } from "../middleware-C2qTZ3V7.js";
4
+ import { v } from "convex/values";
5
+ import { mutationGeneric, queryGeneric } from "convex/server";
6
+
7
+ //#region src/ratelimit/duration.ts
8
+ const UNIT_TO_MS = {
9
+ ms: 1,
10
+ s: 1e3,
11
+ m: 6e4,
12
+ h: 36e5,
13
+ d: 864e5
14
+ };
15
+ const DURATION_REGEX = /^(\d+(?:\.\d+)?)\s?(ms|s|m|h|d)$/;
16
+ function toMs(duration) {
17
+ if (typeof duration === "number") {
18
+ if (!Number.isFinite(duration) || duration <= 0) throw new Error(`Invalid duration: ${duration}`);
19
+ return duration;
20
+ }
21
+ const match = duration.trim().match(DURATION_REGEX);
22
+ if (!match) throw new Error(`Unable to parse duration: ${duration}`);
23
+ const milliseconds = Number.parseFloat(match[1]) * UNIT_TO_MS[match[2]];
24
+ if (!Number.isFinite(milliseconds) || milliseconds <= 0) throw new Error(`Invalid duration: ${duration}`);
25
+ return milliseconds;
26
+ }
27
+
28
+ //#endregion
29
+ //#region src/ratelimit/core/algorithms.ts
30
+ const DEFAULT_SHARDS = 1;
31
+ function fixedWindow(limit, window, options) {
32
+ validatePositive(limit, "limit");
33
+ const shards = normalizeShards(options?.shards);
34
+ const capacity = options?.capacity ?? limit;
35
+ validatePositive(capacity, "capacity");
36
+ return {
37
+ kind: "fixedWindow",
38
+ limit,
39
+ window: toMs(window),
40
+ capacity,
41
+ maxReserved: options?.maxReserved,
42
+ start: options?.start,
43
+ shards
44
+ };
45
+ }
46
+ function slidingWindow(limit, window, options) {
47
+ validatePositive(limit, "limit");
48
+ return {
49
+ kind: "slidingWindow",
50
+ limit,
51
+ window: toMs(window),
52
+ maxReserved: options?.maxReserved,
53
+ shards: normalizeShards(options?.shards)
54
+ };
55
+ }
56
+ function tokenBucket(refillRate, interval, maxTokens, options) {
57
+ validatePositive(refillRate, "refillRate");
58
+ validatePositive(maxTokens, "maxTokens");
59
+ return {
60
+ kind: "tokenBucket",
61
+ refillRate,
62
+ interval: toMs(interval),
63
+ maxTokens,
64
+ maxReserved: options?.maxReserved,
65
+ shards: normalizeShards(options?.shards)
66
+ };
67
+ }
68
+ function applyDynamicLimit(algorithm, dynamicLimit) {
69
+ if (!dynamicLimit || dynamicLimit <= 0) return algorithm;
70
+ if (algorithm.kind === "tokenBucket") return {
71
+ ...algorithm,
72
+ refillRate: dynamicLimit,
73
+ maxTokens: algorithm.maxTokens === algorithm.refillRate ? dynamicLimit : algorithm.maxTokens
74
+ };
75
+ if (algorithm.kind === "fixedWindow") return {
76
+ ...algorithm,
77
+ limit: dynamicLimit,
78
+ capacity: algorithm.capacity === algorithm.limit ? dynamicLimit : algorithm.capacity
79
+ };
80
+ return {
81
+ ...algorithm,
82
+ limit: dynamicLimit
83
+ };
84
+ }
85
+ function normalizeShards(shards) {
86
+ if (shards === void 0) return DEFAULT_SHARDS;
87
+ const rounded = Math.round(shards);
88
+ if (rounded < 1) throw new Error("shards must be >= 1");
89
+ return rounded;
90
+ }
91
+ function validatePositive(value, field) {
92
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${field} must be a positive number`);
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/ratelimit/core/calculate-rate-limit.ts
97
+ function calculateRatelimit(state, algorithm, now, count) {
98
+ if (algorithm.kind === "fixedWindow") return calculateFixedWindow(state, algorithm, now, count);
99
+ if (algorithm.kind === "tokenBucket") return calculateTokenBucket(state, algorithm, now, count);
100
+ return calculateSlidingWindow(state, algorithm, now, count);
101
+ }
102
+ function calculateTokenBucket(state, config, now, count) {
103
+ const ratePerMs = config.refillRate / config.interval;
104
+ const initial = state ?? {
105
+ value: config.maxTokens,
106
+ ts: now
107
+ };
108
+ const elapsed = Math.max(0, now - initial.ts);
109
+ const nextValue = Math.min(initial.value + elapsed * ratePerMs, config.maxTokens) - count;
110
+ const retryAfter = nextValue < 0 ? Math.ceil(-nextValue / ratePerMs) : void 0;
111
+ return {
112
+ state: {
113
+ value: nextValue,
114
+ ts: now
115
+ },
116
+ retryAfter,
117
+ remaining: Math.max(0, Math.floor(nextValue)),
118
+ reset: retryAfter ? now + retryAfter : now,
119
+ limit: config.maxTokens
120
+ };
121
+ }
122
+ function calculateFixedWindow(state, config, now, count) {
123
+ const windowStart = alignWindowStart(now, config.window, config.start);
124
+ const initial = state ?? {
125
+ value: config.capacity,
126
+ ts: windowStart
127
+ };
128
+ const elapsedWindows = Math.max(0, Math.floor((now - initial.ts) / config.window));
129
+ const replenished = Math.min(initial.value + config.limit * elapsedWindows, config.capacity);
130
+ const ts = initial.ts + elapsedWindows * config.window;
131
+ const nextValue = replenished - count;
132
+ const retryAfter = nextValue < 0 ? ts + config.window * Math.ceil(-nextValue / config.limit) - now : void 0;
133
+ return {
134
+ state: {
135
+ value: nextValue,
136
+ ts
137
+ },
138
+ retryAfter,
139
+ remaining: Math.max(0, Math.floor(nextValue)),
140
+ reset: ts + config.window,
141
+ limit: config.limit
142
+ };
143
+ }
144
+ function calculateSlidingWindow(state, config, now, count) {
145
+ const windowStart = alignWindowStart(now, config.window);
146
+ const previousWindowStart = windowStart - config.window;
147
+ const elapsedInWindow = now - windowStart;
148
+ const previousWeight = Math.max(0, (config.window - elapsedInWindow) / config.window);
149
+ let currentCount = 0;
150
+ let previousCount = 0;
151
+ if (state) {
152
+ if (state.ts === windowStart) {
153
+ currentCount = Math.max(0, state.value);
154
+ if (state.auxTs === previousWindowStart) previousCount = Math.max(0, state.auxValue ?? 0);
155
+ } else if (state.ts === previousWindowStart) previousCount = Math.max(0, state.value);
156
+ }
157
+ const projectedCurrent = currentCount + count;
158
+ const projectedUsed = projectedCurrent + previousCount * previousWeight;
159
+ const remaining = config.limit - projectedUsed;
160
+ const retryAfter = remaining < 0 ? Math.max(1, config.window - elapsedInWindow) : void 0;
161
+ return {
162
+ state: {
163
+ value: projectedCurrent,
164
+ ts: windowStart,
165
+ auxValue: previousCount,
166
+ auxTs: previousWindowStart
167
+ },
168
+ retryAfter,
169
+ remaining: Math.max(0, Math.floor(remaining)),
170
+ reset: windowStart + config.window,
171
+ limit: config.limit
172
+ };
173
+ }
174
+ function alignWindowStart(now, window, start = 0) {
175
+ const offsetNow = now - start;
176
+ return start + Math.floor(offsetNow / window) * window;
177
+ }
178
+
179
+ //#endregion
180
+ //#region src/ratelimit/core/cache.ts
181
+ var EphemeralBlockCache = class {
182
+ constructor(cache) {
183
+ this.cache = cache;
184
+ }
185
+ isBlocked(identifier) {
186
+ const reset = this.cache.get(identifier);
187
+ if (!reset) return {
188
+ blocked: false,
189
+ reset: 0
190
+ };
191
+ if (reset <= Date.now()) {
192
+ this.cache.delete(identifier);
193
+ return {
194
+ blocked: false,
195
+ reset: 0
196
+ };
197
+ }
198
+ return {
199
+ blocked: true,
200
+ reset
201
+ };
202
+ }
203
+ blockUntil(identifier, reset) {
204
+ this.cache.set(identifier, reset);
205
+ }
206
+ clear(identifier) {
207
+ this.cache.delete(identifier);
208
+ }
209
+ size() {
210
+ return this.cache.size;
211
+ }
212
+ };
213
+ function createReadDedupeCache() {
214
+ const cache = /* @__PURE__ */ new Map();
215
+ return {
216
+ get: (key) => cache.get(key),
217
+ set: (key, value) => cache.set(key, value),
218
+ delete: (key) => cache.delete(key),
219
+ clear: () => cache.clear()
220
+ };
221
+ }
222
+
223
+ //#endregion
224
+ //#region src/ratelimit/core/deny-list.ts
225
+ const DEFAULT_BLOCK_MS = 6e4;
226
+ const THRESHOLD_BLOCK_MS = 1440 * 60 * 1e3;
227
+ const protectionState = /* @__PURE__ */ new Map();
228
+ function getState(prefix) {
229
+ let state = protectionState.get(prefix);
230
+ if (!state) {
231
+ state = {
232
+ hits: /* @__PURE__ */ new Map(),
233
+ blockedUntil: /* @__PURE__ */ new Map()
234
+ };
235
+ protectionState.set(prefix, state);
236
+ }
237
+ return state;
238
+ }
239
+ function pickDeniedValue(options) {
240
+ const members = getMembers(options.identifier, options.request);
241
+ const state = getState(options.prefix);
242
+ for (const member of members) {
243
+ const until = state.blockedUntil.get(member.value);
244
+ if (until && until > Date.now()) return member.value;
245
+ if (until && until <= Date.now()) state.blockedUntil.delete(member.value);
246
+ }
247
+ if (!options.lists) return;
248
+ const listMatchers = [
249
+ {
250
+ values: options.lists.identifiers,
251
+ kind: "identifier"
252
+ },
253
+ {
254
+ values: options.lists.ips,
255
+ kind: "ip"
256
+ },
257
+ {
258
+ values: options.lists.userAgents,
259
+ kind: "userAgent"
260
+ },
261
+ {
262
+ values: options.lists.countries,
263
+ kind: "country"
264
+ }
265
+ ];
266
+ for (const matcher of listMatchers) {
267
+ if (!matcher.values || matcher.values.length === 0) continue;
268
+ const valueSet = new Set(matcher.values);
269
+ const hit = members.find((member) => member.kind === matcher.kind && valueSet.has(member.value));
270
+ if (hit) {
271
+ state.blockedUntil.set(hit.value, Date.now() + DEFAULT_BLOCK_MS);
272
+ return hit.value;
273
+ }
274
+ }
275
+ }
276
+ function recordRatelimitFailure(options) {
277
+ const members = getMembers(options.identifier, options.request);
278
+ const state = getState(options.prefix);
279
+ for (const member of members) {
280
+ const next = (state.hits.get(member.value) ?? 0) + 1;
281
+ state.hits.set(member.value, next);
282
+ if (next >= options.threshold) state.blockedUntil.set(member.value, Date.now() + THRESHOLD_BLOCK_MS);
283
+ }
284
+ }
285
+ function clearProtection(prefix, identifier) {
286
+ const state = getState(prefix);
287
+ state.hits.delete(identifier);
288
+ state.blockedUntil.delete(identifier);
289
+ }
290
+ function getMembers(identifier, request) {
291
+ return [
292
+ {
293
+ kind: "identifier",
294
+ value: identifier
295
+ },
296
+ {
297
+ kind: "ip",
298
+ value: request?.ip
299
+ },
300
+ {
301
+ kind: "userAgent",
302
+ value: request?.userAgent
303
+ },
304
+ {
305
+ kind: "country",
306
+ value: request?.country
307
+ }
308
+ ].filter((member) => Boolean(member.value));
309
+ }
310
+
311
+ //#endregion
312
+ //#region src/ratelimit/store/convex-store.ts
313
+ const RATE_LIMIT_STATE_TABLE = "ratelimitState";
314
+ const RATE_LIMIT_DYNAMIC_TABLE = "ratelimitDynamicLimit";
315
+ const RATE_LIMIT_HIT_TABLE = "ratelimitProtectionHit";
316
+ const RATE_LIMIT_TABLE_NAMES = [
317
+ RATE_LIMIT_STATE_TABLE,
318
+ RATE_LIMIT_DYNAMIC_TABLE,
319
+ RATE_LIMIT_HIT_TABLE
320
+ ];
321
+ const missingDbMessage = "Ratelimit requires a Convex db context. Pass `db` in constructor config or use hookAPI().";
322
+ const missingTableGuidance = "Ratelimit tables are missing. Scaffold and register `convex/lib/plugins/ratelimit/schema.ts`, or run `kitcn add ratelimit`.";
323
+ var ConvexRatelimitStore = class ConvexRatelimitStore {
324
+ dedupe = createReadDedupeCache();
325
+ listDedupe = createReadDedupeCache();
326
+ dynamicDedupe = createReadDedupeCache();
327
+ constructor(db) {
328
+ this.db = db;
329
+ }
330
+ withDb(db) {
331
+ return new ConvexRatelimitStore(db);
332
+ }
333
+ async getState(name, key, shard) {
334
+ return this.withSetupGuidance(async () => {
335
+ const db = this.getReader();
336
+ const cacheKey = stateCacheKey(name, key, shard);
337
+ const cached = this.dedupe.get(cacheKey);
338
+ if (cached) return cached;
339
+ const query = db.query(RATE_LIMIT_STATE_TABLE).withIndex("by_name_key_shard", (q) => q.eq("name", name).eq("key", key).eq("shard", shard)).unique().then((row) => row ? row : null);
340
+ this.dedupe.set(cacheKey, query);
341
+ return query;
342
+ });
343
+ }
344
+ async listStates(name, key) {
345
+ return this.withSetupGuidance(async () => {
346
+ const db = this.getReader();
347
+ const cacheKey = listCacheKey(name, key);
348
+ const cached = this.listDedupe.get(cacheKey);
349
+ if (cached) return await cached ?? [];
350
+ const query = db.query(RATE_LIMIT_STATE_TABLE).withIndex("by_name_key", (q) => q.eq("name", name).eq("key", key)).collect().then((rows) => rows);
351
+ this.listDedupe.set(cacheKey, query);
352
+ return query;
353
+ });
354
+ }
355
+ async upsertState(options) {
356
+ await this.withSetupGuidance(async () => {
357
+ const db = this.getWriter();
358
+ const existing = await this.getState(options.name, options.key, options.shard);
359
+ if (existing) await db.patch(existing._id, {
360
+ value: options.state.value,
361
+ ts: options.state.ts,
362
+ auxValue: options.state.auxValue,
363
+ auxTs: options.state.auxTs
364
+ });
365
+ else await db.insert(RATE_LIMIT_STATE_TABLE, {
366
+ name: options.name,
367
+ key: options.key,
368
+ shard: options.shard,
369
+ value: options.state.value,
370
+ ts: options.state.ts,
371
+ auxValue: options.state.auxValue,
372
+ auxTs: options.state.auxTs
373
+ });
374
+ this.invalidate(options.name, options.key, options.shard);
375
+ });
376
+ }
377
+ async deleteStates(name, key) {
378
+ await this.withSetupGuidance(async () => {
379
+ const db = this.getWriter();
380
+ const rows = await this.listStates(name, key);
381
+ for (const row of rows) await db.delete(RATE_LIMIT_STATE_TABLE, row._id);
382
+ this.invalidateAll(name, key);
383
+ });
384
+ }
385
+ async getDynamicLimit(prefix) {
386
+ return this.withSetupGuidance(async () => {
387
+ const db = this.getReader();
388
+ const cacheKey = dynamicCacheKey(prefix);
389
+ const cached = this.dynamicDedupe.get(cacheKey);
390
+ if (cached) {
391
+ const row = await cached;
392
+ return row ? row.limit : null;
393
+ }
394
+ const query = db.query(RATE_LIMIT_DYNAMIC_TABLE).withIndex("by_prefix", (q) => q.eq("prefix", prefix)).unique().then((row) => row ? row : null);
395
+ this.dynamicDedupe.set(cacheKey, query);
396
+ const row = await query;
397
+ return row ? row.limit : null;
398
+ });
399
+ }
400
+ async setDynamicLimit(prefix, limit) {
401
+ await this.withSetupGuidance(async () => {
402
+ const db = this.getWriter();
403
+ const existing = await db.query(RATE_LIMIT_DYNAMIC_TABLE).withIndex("by_prefix", (q) => q.eq("prefix", prefix)).unique();
404
+ if (limit === false) {
405
+ if (existing?._id) await db.delete(RATE_LIMIT_DYNAMIC_TABLE, existing._id);
406
+ this.dynamicDedupe.delete(dynamicCacheKey(prefix));
407
+ return;
408
+ }
409
+ if (existing?._id) await db.patch(existing._id, {
410
+ limit,
411
+ updatedAt: Date.now()
412
+ });
413
+ else await db.insert(RATE_LIMIT_DYNAMIC_TABLE, {
414
+ prefix,
415
+ limit,
416
+ updatedAt: Date.now()
417
+ });
418
+ this.dynamicDedupe.delete(dynamicCacheKey(prefix));
419
+ });
420
+ }
421
+ invalidate(name, key, shard) {
422
+ this.dedupe.delete(stateCacheKey(name, key, shard));
423
+ this.listDedupe.delete(listCacheKey(name, key));
424
+ }
425
+ invalidateAll(name, key) {
426
+ this.listDedupe.delete(listCacheKey(name, key));
427
+ this.dedupe.clear();
428
+ }
429
+ getReader() {
430
+ if (!this.db) throw new Error(missingDbMessage);
431
+ return this.db;
432
+ }
433
+ getWriter() {
434
+ if (!this.db || !("insert" in this.db) || !("patch" in this.db) || !("delete" in this.db)) throw new Error("Ratelimit write operation requires mutation context (db.insert/patch/delete).");
435
+ return this.db;
436
+ }
437
+ async withSetupGuidance(run) {
438
+ try {
439
+ return await run();
440
+ } catch (error) {
441
+ throw withMissingTableGuidance(error);
442
+ }
443
+ }
444
+ };
445
+ function stateCacheKey(name, key, shard) {
446
+ return `state:${name}:${key ?? "__global__"}:${shard}`;
447
+ }
448
+ function listCacheKey(name, key) {
449
+ return `list:${name}:${key ?? "__global__"}`;
450
+ }
451
+ function dynamicCacheKey(prefix) {
452
+ return `dynamic:${prefix}`;
453
+ }
454
+ function withMissingTableGuidance(error) {
455
+ const message = error instanceof Error ? error.message : String(error);
456
+ const lower = message.toLowerCase();
457
+ if (!RATE_LIMIT_TABLE_NAMES.some((tableName) => {
458
+ const normalizedTable = tableName.toLowerCase();
459
+ return lower.includes(normalizedTable) && (lower.includes("table") || lower.includes("does not exist") || lower.includes("not found") || lower.includes("unknown"));
460
+ })) return error instanceof Error ? error : new Error(message);
461
+ return new Error(`${missingTableGuidance} Original error: ${message}`, { cause: error instanceof Error ? error : void 0 });
462
+ }
463
+
464
+ //#endregion
465
+ //#region src/ratelimit/ratelimit.ts
466
+ const DEFAULT_PREFIX = "kitcn/ratelimit";
467
+ const DEFAULT_TIMEOUT_MS = 5e3;
468
+ const DEFAULT_THRESHOLD = 30;
469
+ const MIN_POWER_OF_TWO_CHOICES = 3;
470
+ var Ratelimit = class Ratelimit {
471
+ static fixedWindow = fixedWindow;
472
+ static slidingWindow = slidingWindow;
473
+ static tokenBucket = tokenBucket;
474
+ store;
475
+ prefix;
476
+ timeout;
477
+ dynamicLimits;
478
+ failureMode;
479
+ enableProtection;
480
+ denyListThreshold;
481
+ denyList;
482
+ limiter;
483
+ blockCache;
484
+ blockCacheSource;
485
+ checkCache = createReadDedupeCache();
486
+ constructor(config) {
487
+ this.config = config;
488
+ this.store = new ConvexRatelimitStore(config.db);
489
+ this.prefix = config.prefix ?? DEFAULT_PREFIX;
490
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
491
+ this.dynamicLimits = config.dynamicLimits ?? false;
492
+ this.failureMode = config.failureMode ?? "closed";
493
+ this.enableProtection = config.enableProtection ?? false;
494
+ this.denyListThreshold = config.denyListThreshold ?? DEFAULT_THRESHOLD;
495
+ this.denyList = config.denyList;
496
+ this.limiter = config.limiter;
497
+ if (config.ephemeralCache !== false) {
498
+ this.blockCacheSource = config.ephemeralCache ?? /* @__PURE__ */ new Map();
499
+ this.blockCache = new EphemeralBlockCache(this.blockCacheSource);
500
+ }
501
+ }
502
+ async limit(identifier, request) {
503
+ return this.runWithTimeout(() => this.evaluate(identifier, request, true));
504
+ }
505
+ async check(identifier, request) {
506
+ return this.runWithTimeout(() => this.evaluate(identifier, request, false));
507
+ }
508
+ async blockUntilReady(identifier, timeoutMs) {
509
+ if (timeoutMs <= 0) throw new Error("timeout must be positive");
510
+ const deadline = Date.now() + timeoutMs;
511
+ let latest = this.timeoutResponse(false);
512
+ while (Date.now() <= deadline) {
513
+ latest = await this.limit(identifier);
514
+ if (latest.success) return latest;
515
+ await sleep(Math.max(1, Math.min(latest.reset, deadline) - Date.now()));
516
+ }
517
+ return latest;
518
+ }
519
+ async resetUsedTokens(identifier) {
520
+ await this.store.deleteStates(this.prefix, identifier);
521
+ this.checkCache.clear();
522
+ if (this.blockCache) this.blockCache.clear(identifier);
523
+ clearProtection(this.prefix, identifier);
524
+ }
525
+ async getRemaining(identifier) {
526
+ const value = await this.getValue(identifier, { sampleShards: this.limiter.shards });
527
+ const evaluated = calculateRatelimit({
528
+ value: value.value,
529
+ ts: value.ts
530
+ }, value.config, Date.now(), 0);
531
+ return {
532
+ remaining: Math.max(0, evaluated.remaining),
533
+ reset: evaluated.reset,
534
+ limit: evaluated.limit
535
+ };
536
+ }
537
+ async getValue(identifier, options) {
538
+ const cacheKey = `${identifier}:${options?.sampleShards ?? 0}`;
539
+ const cached = this.checkCache.get(cacheKey);
540
+ if (cached) {
541
+ const snapshot = await cached;
542
+ if (snapshot) return snapshot;
543
+ }
544
+ const algorithm = await this.resolveAlgorithm();
545
+ const sampleShards = Math.max(1, Math.min(options?.sampleShards ?? 1, algorithm.shards));
546
+ const shards = pickSampleShards(algorithm.shards, sampleShards);
547
+ const now = Date.now();
548
+ let best = null;
549
+ for (const shard of shards) {
550
+ const evaluated = calculateRatelimit(normalizeState(await this.store.getState(this.prefix, identifier, shard)), algorithm, now, 0);
551
+ const current = {
552
+ value: algorithm.kind === "slidingWindow" ? evaluated.remaining : evaluated.state.value,
553
+ ts: evaluated.state.ts,
554
+ shard,
555
+ config: algorithm
556
+ };
557
+ if (!best || current.value > best.value) best = current;
558
+ }
559
+ const result = best ?? {
560
+ value: algorithm.kind === "tokenBucket" ? algorithm.maxTokens : algorithm.limit,
561
+ ts: now,
562
+ shard: 0,
563
+ config: algorithm
564
+ };
565
+ this.checkCache.set(cacheKey, Promise.resolve(result));
566
+ return result;
567
+ }
568
+ async setDynamicLimit(options) {
569
+ if (!this.dynamicLimits) throw new Error("dynamicLimits must be enabled in the Ratelimit constructor to use setDynamicLimit()");
570
+ await this.store.setDynamicLimit(this.prefix, options.limit);
571
+ }
572
+ async getDynamicLimit() {
573
+ if (!this.dynamicLimits) throw new Error("dynamicLimits must be enabled in the Ratelimit constructor to use getDynamicLimit()");
574
+ return { dynamicLimit: await this.store.getDynamicLimit(this.prefix) };
575
+ }
576
+ hookAPI(options) {
577
+ return {
578
+ getRatelimit: queryGeneric({
579
+ args: {
580
+ identifier: v.optional(v.string()),
581
+ sampleShards: v.optional(v.number())
582
+ },
583
+ returns: v.object({
584
+ value: v.number(),
585
+ ts: v.number(),
586
+ shard: v.number(),
587
+ config: v.any()
588
+ }),
589
+ handler: async (ctx, args) => {
590
+ const identifier = await resolveIdentifier(options?.identifier, ctx, args.identifier);
591
+ return this.withDb(ctx.db).getValue(identifier, { sampleShards: args.sampleShards ?? options?.sampleShards });
592
+ }
593
+ }),
594
+ getServerTime: mutationGeneric({
595
+ args: {},
596
+ returns: v.number(),
597
+ handler: async () => Date.now()
598
+ })
599
+ };
600
+ }
601
+ withDb(db) {
602
+ return new Ratelimit({
603
+ ...this.config,
604
+ db,
605
+ ephemeralCache: this.blockCacheSource
606
+ });
607
+ }
608
+ async evaluate(identifier, request, consume) {
609
+ const deniedValue = this.enableProtection ? pickDeniedValue({
610
+ prefix: this.prefix,
611
+ identifier,
612
+ request,
613
+ lists: this.denyList
614
+ }) : void 0;
615
+ if (deniedValue) return {
616
+ success: false,
617
+ ok: false,
618
+ limit: this.rawLimit(this.limiter),
619
+ remaining: 0,
620
+ reset: Date.now() + 6e4,
621
+ pending: Promise.resolve(),
622
+ reason: "denyList",
623
+ deniedValue
624
+ };
625
+ const algorithm = await this.resolveAlgorithm();
626
+ const count = consume ? normalizeCount(request) : 0;
627
+ const reserveRequested = consume && Boolean(request?.reserve);
628
+ if (this.blockCache && count > 0) {
629
+ const cacheKey = `${this.prefix}:${identifier}`;
630
+ const blocked = this.blockCache.isBlocked(cacheKey);
631
+ if (blocked.blocked) return {
632
+ success: false,
633
+ ok: false,
634
+ limit: this.rawLimit(algorithm),
635
+ remaining: 0,
636
+ reset: blocked.reset,
637
+ pending: Promise.resolve(),
638
+ reason: "cacheBlock"
639
+ };
640
+ }
641
+ const now = Date.now();
642
+ const candidates = await this.evaluateCandidates(identifier, algorithm, now, count, reserveRequested);
643
+ const successful = candidates.filter((candidate) => candidate.success);
644
+ if (successful.length > 0) {
645
+ const best = successful.sort((a, b) => b.evaluated.remaining - a.evaluated.remaining)[0];
646
+ if (consume && count !== 0) await this.store.upsertState({
647
+ name: this.prefix,
648
+ key: identifier,
649
+ shard: best.shard,
650
+ state: best.evaluated.state
651
+ });
652
+ if (this.blockCache) this.blockCache.clear(`${this.prefix}:${identifier}`);
653
+ clearProtection(this.prefix, identifier);
654
+ this.checkCache.clear();
655
+ return {
656
+ success: true,
657
+ ok: true,
658
+ limit: best.evaluated.limit,
659
+ remaining: best.evaluated.remaining,
660
+ reset: best.evaluated.reset,
661
+ pending: Promise.resolve()
662
+ };
663
+ }
664
+ const failure = candidates.filter((candidate) => candidate.evaluated.retryAfter !== void 0).sort((a, b) => (a.evaluated.retryAfter ?? Number.MAX_SAFE_INTEGER) - (b.evaluated.retryAfter ?? Number.MAX_SAFE_INTEGER))[0] ?? candidates[0];
665
+ const reset = now + (failure.evaluated.retryAfter ?? 1);
666
+ if (consume && this.blockCache && count > 0) this.blockCache.blockUntil(`${this.prefix}:${identifier}`, reset);
667
+ if (consume && this.enableProtection) recordRatelimitFailure({
668
+ prefix: this.prefix,
669
+ identifier,
670
+ request,
671
+ threshold: this.denyListThreshold
672
+ });
673
+ return {
674
+ success: false,
675
+ ok: false,
676
+ limit: failure.evaluated.limit,
677
+ remaining: 0,
678
+ reset,
679
+ pending: Promise.resolve()
680
+ };
681
+ }
682
+ async evaluateCandidates(identifier, algorithm, now, count, reserveRequested) {
683
+ const shards = pickCandidateShards(algorithm.shards);
684
+ const result = [];
685
+ for (const shard of shards) {
686
+ const state = normalizeState(await this.store.getState(this.prefix, identifier, shard));
687
+ const evaluated = calculateRatelimit(state, algorithm, now, count);
688
+ const canReserve = reserveRequested && evaluated.retryAfter !== void 0 && algorithm.kind !== "slidingWindow" && (algorithm.maxReserved === void 0 || Math.abs(evaluated.state.value) <= algorithm.maxReserved);
689
+ const success = evaluated.retryAfter === void 0 || canReserve;
690
+ result.push({
691
+ shard,
692
+ state,
693
+ evaluated,
694
+ success
695
+ });
696
+ }
697
+ return result;
698
+ }
699
+ async resolveAlgorithm() {
700
+ if (!this.dynamicLimits) return this.limiter;
701
+ const dynamicLimit = await this.store.getDynamicLimit(this.prefix);
702
+ return applyDynamicLimit(this.limiter, dynamicLimit);
703
+ }
704
+ rawLimit(algorithm) {
705
+ if (algorithm.kind === "tokenBucket") return algorithm.maxTokens;
706
+ return algorithm.limit;
707
+ }
708
+ async runWithTimeout(operation) {
709
+ if (this.timeout <= 0) return operation();
710
+ const startedAt = Date.now();
711
+ try {
712
+ const result = await operation();
713
+ if (Date.now() - startedAt > this.timeout) return this.timeoutResponse(this.failureMode === "open");
714
+ return result;
715
+ } catch (error) {
716
+ if (Date.now() - startedAt > this.timeout) return this.timeoutResponse(this.failureMode === "open");
717
+ throw error;
718
+ }
719
+ }
720
+ timeoutResponse(success) {
721
+ return {
722
+ success,
723
+ ok: success,
724
+ limit: 0,
725
+ remaining: 0,
726
+ reset: Date.now(),
727
+ pending: Promise.resolve(),
728
+ reason: "timeout"
729
+ };
730
+ }
731
+ };
732
+ function normalizeCount(request) {
733
+ if (!request) return 1;
734
+ const value = request.rate ?? request.count ?? 1;
735
+ if (!Number.isFinite(value)) throw new Error("count/rate must be a finite number");
736
+ return value;
737
+ }
738
+ function normalizeState(row) {
739
+ if (!row) return null;
740
+ return {
741
+ value: row.value,
742
+ ts: row.ts,
743
+ auxValue: row.auxValue,
744
+ auxTs: row.auxTs
745
+ };
746
+ }
747
+ function pickCandidateShards(shards) {
748
+ const first = Math.floor(Math.random() * shards);
749
+ if (shards < MIN_POWER_OF_TWO_CHOICES) return [first];
750
+ return [first, (first + 1 + Math.floor(Math.random() * (shards - 1))) % shards];
751
+ }
752
+ function pickSampleShards(total, sample) {
753
+ const all = Array.from({ length: total }, (_, index) => index);
754
+ const selected = [];
755
+ while (all.length > 0 && selected.length < sample) {
756
+ const randomIndex = Math.floor(Math.random() * all.length);
757
+ const [shard] = all.splice(randomIndex, 1);
758
+ if (shard !== void 0) selected.push(shard);
759
+ }
760
+ return selected.length > 0 ? selected : [0];
761
+ }
762
+ async function sleep(ms) {
763
+ try {
764
+ await new Promise((resolve) => {
765
+ setTimeout(resolve, ms);
766
+ });
767
+ } catch (error) {
768
+ if (isTimerUnsupportedError(error)) throw new Error("blockUntilReady is not supported in Convex queries/mutations. Use an action or non-Convex runtime.");
769
+ throw error;
770
+ }
771
+ }
772
+ function isTimerUnsupportedError(error) {
773
+ if (!(error instanceof Error)) return false;
774
+ const message = error.message.toLowerCase();
775
+ return message.includes("can't use settimeout in queries and mutations") || message.includes("settimeout");
776
+ }
777
+ async function resolveIdentifier(identifierOption, ctx, fromClient) {
778
+ if (!identifierOption) {
779
+ if (!fromClient) throw new Error("hookAPI requires identifier in options or request args");
780
+ return fromClient;
781
+ }
782
+ if (typeof identifierOption === "function") return await identifierOption(ctx, fromClient);
783
+ return identifierOption;
784
+ }
785
+
786
+ //#endregion
787
+ //#region src/ratelimit/plugin.ts
788
+ const DEFAULT_RATELIMIT_MESSAGE = "Rate limit exceeded. Please try again later.";
789
+ function resolveBucketLimiter(options, bucket, tier) {
790
+ const bucketConfig = options.buckets[bucket];
791
+ if (!bucketConfig) throw new Error(`Unknown ratelimit bucket "${bucket}".`);
792
+ const limiter = bucketConfig[tier];
793
+ if (!limiter) throw new Error(`Unknown ratelimit tier "${tier}" for bucket "${bucket}".`);
794
+ return limiter;
795
+ }
796
+ function resolvePrefix(options, args) {
797
+ if (typeof options.prefix === "function") return options.prefix(args);
798
+ return options.prefix ?? `ratelimit:${args.bucket}:${args.tier}`;
799
+ }
800
+ function resolveMessage(options, args) {
801
+ if (typeof options.message === "function") return options.message(args);
802
+ return options.message ?? DEFAULT_RATELIMIT_MESSAGE;
803
+ }
804
+ const RatelimitPlugin = definePlugin("ratelimit", ({ options }) => {
805
+ if (!options) throw new Error("RatelimitPlugin must be configured before use.");
806
+ return options;
807
+ }).extend(({ middleware }) => ({ middleware: () => middleware().pipe(async ({ ctx, meta, next }) => {
808
+ const options = ctx.api.ratelimit;
809
+ const mutationCtx = requireMutationCtx(ctx);
810
+ const bucket = await options.getBucket({
811
+ ctx,
812
+ meta
813
+ });
814
+ const user = await options.getUser({
815
+ ctx,
816
+ meta
817
+ });
818
+ const tier = await options.getTier(user);
819
+ const identifier = await options.getIdentifier({
820
+ ctx,
821
+ meta,
822
+ user,
823
+ bucket
824
+ });
825
+ const args = {
826
+ ctx,
827
+ meta,
828
+ user,
829
+ bucket,
830
+ tier,
831
+ identifier
832
+ };
833
+ if (!(await new Ratelimit({
834
+ db: mutationCtx.db,
835
+ prefix: await resolvePrefix(options, args),
836
+ limiter: resolveBucketLimiter(options, bucket, tier),
837
+ failureMode: options.failureMode,
838
+ enableProtection: options.enableProtection,
839
+ denyListThreshold: options.denyListThreshold
840
+ }).limit(identifier, await options.getSignals(args))).success) throw new CRPCError({
841
+ code: "TOO_MANY_REQUESTS",
842
+ message: await resolveMessage(options, args)
843
+ });
844
+ return next({ ctx });
845
+ }) }));
846
+
847
+ //#endregion
848
+ //#region src/ratelimit/index.ts
849
+ const SECOND = 1e3;
850
+ const MINUTE = 60 * SECOND;
851
+ const HOUR = 60 * MINUTE;
852
+ const DAY = 24 * HOUR;
853
+ const WEEK = 7 * DAY;
854
+
855
+ //#endregion
856
+ export { DAY, HOUR, MINUTE, RATE_LIMIT_DYNAMIC_TABLE, RATE_LIMIT_HIT_TABLE, RATE_LIMIT_STATE_TABLE, Ratelimit, RatelimitPlugin, SECOND, WEEK, applyDynamicLimit, calculateRatelimit, fixedWindow, slidingWindow, toMs, tokenBucket };