kitcn 0.22.1 → 0.24.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.
Files changed (33) hide show
  1. package/dist/aggregate/index.d.ts +1 -1
  2. package/dist/auth/nextjs/index.d.ts +1 -1
  3. package/dist/auth/nextjs/index.js +1 -1
  4. package/dist/{builder-Dwy6D2QA.js → builder-CsxVc5xC.js} +110 -57
  5. package/dist/{caller-factory-DHywSoGZ.js → caller-factory-CWa0ELLD.js} +1 -1
  6. package/dist/cli.mjs +96 -20
  7. package/dist/crpc/index.d.ts +3 -3
  8. package/dist/crpc/index.js +3 -3
  9. package/dist/{http-types-zsMHb_QN.d.ts → http-types-DXOgaerG.d.ts} +46 -1
  10. package/dist/{middleware-qzHEHaDy.js → middleware-DUd1Sj39.js} +1 -1
  11. package/dist/orm/index.d.ts +1 -1
  12. package/dist/plugins/index.js +1 -1
  13. package/dist/{procedure-caller-JB9kjYsy.js → procedure-caller-DQxnLBS_.js} +2 -2
  14. package/dist/{procedure-name-exVcmr_p.d.ts → procedure-name-C20pFZnk.d.ts} +21 -7
  15. package/dist/{query-options-C_eBSIXG.js → query-options-CzRV4G4N.js} +33 -1
  16. package/dist/ratelimit/index.d.ts +45 -9
  17. package/dist/ratelimit/index.js +255 -101
  18. package/dist/react/index.d.ts +9 -6
  19. package/dist/react/index.js +81 -18
  20. package/dist/rsc/index.d.ts +4 -7
  21. package/dist/rsc/index.js +5 -9
  22. package/dist/server/index.d.ts +2 -2
  23. package/dist/server/index.js +3 -3
  24. package/dist/solid/index.d.ts +9 -6
  25. package/dist/solid/index.js +81 -18
  26. package/dist/{transformer-C6pGVHqx.js → transformer-yZuBWo8v.js} +45 -1
  27. package/dist/{types-jNTcza_a.d.ts → types-BHR7ZKKD.d.ts} +1 -1
  28. package/dist/{where-clause-compiler-B_H3oio5.d.ts → where-clause-compiler-DW6jy2er.d.ts} +59 -59
  29. package/package.json +1 -1
  30. package/skills/kitcn/references/features/http.md +26 -1
  31. package/skills/kitcn/references/features/ratelimit.md +11 -0
  32. package/skills/kitcn/references/features/react.md +1 -1
  33. package/skills/kitcn/references/setup/server.md +35 -5
@@ -1,6 +1,6 @@
1
1
  import { u as requireMutationCtx } from "../api-entry-N3nBOlI2.js";
2
- import { _ as CRPCError } from "../builder-Dwy6D2QA.js";
3
- import { t as definePlugin } from "../middleware-qzHEHaDy.js";
2
+ import { _ as CRPCError } from "../builder-CsxVc5xC.js";
3
+ import { t as definePlugin } from "../middleware-DUd1Sj39.js";
4
4
  import { v } from "convex/values";
5
5
  import { mutationGeneric, queryGeneric } from "convex/server";
6
6
 
@@ -422,94 +422,6 @@ function createReadDedupeCache() {
422
422
  };
423
423
  }
424
424
 
425
- //#endregion
426
- //#region src/ratelimit/core/deny-list.ts
427
- const DEFAULT_BLOCK_MS = 6e4;
428
- const THRESHOLD_BLOCK_MS = 1440 * 60 * 1e3;
429
- const protectionState = /* @__PURE__ */ new Map();
430
- function getState(prefix) {
431
- let state = protectionState.get(prefix);
432
- if (!state) {
433
- state = {
434
- hits: /* @__PURE__ */ new Map(),
435
- blockedUntil: /* @__PURE__ */ new Map()
436
- };
437
- protectionState.set(prefix, state);
438
- }
439
- return state;
440
- }
441
- function pickDeniedValue(options) {
442
- const members = getMembers(options.identifier, options.request);
443
- const state = getState(options.prefix);
444
- for (const member of members) {
445
- const until = state.blockedUntil.get(member.value);
446
- if (until && until > Date.now()) return member.value;
447
- if (until && until <= Date.now()) state.blockedUntil.delete(member.value);
448
- }
449
- if (!options.lists) return;
450
- const listMatchers = [
451
- {
452
- values: options.lists.identifiers,
453
- kind: "identifier"
454
- },
455
- {
456
- values: options.lists.ips,
457
- kind: "ip"
458
- },
459
- {
460
- values: options.lists.userAgents,
461
- kind: "userAgent"
462
- },
463
- {
464
- values: options.lists.countries,
465
- kind: "country"
466
- }
467
- ];
468
- for (const matcher of listMatchers) {
469
- if (!matcher.values || matcher.values.length === 0) continue;
470
- const valueSet = new Set(matcher.values);
471
- const hit = members.find((member) => member.kind === matcher.kind && valueSet.has(member.value));
472
- if (hit) {
473
- state.blockedUntil.set(hit.value, Date.now() + DEFAULT_BLOCK_MS);
474
- return hit.value;
475
- }
476
- }
477
- }
478
- function recordRatelimitFailure(options) {
479
- const members = getMembers(options.identifier, options.request);
480
- const state = getState(options.prefix);
481
- for (const member of members) {
482
- const next = (state.hits.get(member.value) ?? 0) + 1;
483
- state.hits.set(member.value, next);
484
- if (next >= options.threshold) state.blockedUntil.set(member.value, Date.now() + THRESHOLD_BLOCK_MS);
485
- }
486
- }
487
- function clearProtection(prefix, identifier) {
488
- const state = getState(prefix);
489
- state.hits.delete(identifier);
490
- state.blockedUntil.delete(identifier);
491
- }
492
- function getMembers(identifier, request) {
493
- return [
494
- {
495
- kind: "identifier",
496
- value: identifier
497
- },
498
- {
499
- kind: "ip",
500
- value: request?.ip
501
- },
502
- {
503
- kind: "userAgent",
504
- value: request?.userAgent
505
- },
506
- {
507
- kind: "country",
508
- value: request?.country
509
- }
510
- ].filter((member) => Boolean(member.value));
511
- }
512
-
513
425
  //#endregion
514
426
  //#region src/ratelimit/store/convex-store.ts
515
427
  const RATE_LIMIT_STATE_TABLE = "ratelimitState";
@@ -584,6 +496,20 @@ var ConvexRatelimitStore = class ConvexRatelimitStore {
584
496
  this.invalidateAll(name, key);
585
497
  });
586
498
  }
499
+ async deleteStatesBefore(before, limit) {
500
+ return this.withSetupGuidance(async () => {
501
+ const db = this.getWriter();
502
+ const rows = await db.query(RATE_LIMIT_STATE_TABLE).withIndex("by_ts", (q) => q.lt("ts", before)).take(limit + 1);
503
+ const selected = rows.slice(0, limit);
504
+ for (const row of selected) await db.delete(RATE_LIMIT_STATE_TABLE, row._id);
505
+ this.dedupe.clear();
506
+ this.listDedupe.clear();
507
+ return {
508
+ deleted: selected.length,
509
+ hasMore: rows.length > limit
510
+ };
511
+ });
512
+ }
587
513
  async getDynamicLimit(prefix) {
588
514
  return this.withSetupGuidance(async () => {
589
515
  const db = this.getReader();
@@ -663,6 +589,227 @@ function withMissingTableGuidance(error) {
663
589
  return new Error(`${missingTableGuidance} Original error: ${message}`, { cause: error instanceof Error ? error : void 0 });
664
590
  }
665
591
 
592
+ //#endregion
593
+ //#region src/ratelimit/maintenance.ts
594
+ const DEFAULT_CLEANUP_LIMIT = 100;
595
+ const MAX_CLEANUP_LIMIT = 1e3;
596
+ /** Deletes one on-demand batch of state older than the caller-owned cutoff. */
597
+ async function cleanupRatelimitState(db, options) {
598
+ if (!Number.isFinite(options.before)) throw new Error("before must be a finite timestamp");
599
+ const limit = options.limit ?? DEFAULT_CLEANUP_LIMIT;
600
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_CLEANUP_LIMIT) throw new Error(`limit must be an integer from 1 to ${MAX_CLEANUP_LIMIT}`);
601
+ return new ConvexRatelimitStore(db).deleteStatesBefore(options.before, limit);
602
+ }
603
+
604
+ //#endregion
605
+ //#region src/ratelimit/core/deny-list.ts
606
+ const DEFAULT_BLOCK_MS = 6e4;
607
+ const THRESHOLD_BLOCK_MS = 1440 * 60 * 1e3;
608
+ /**
609
+ * Only failures inside this rolling window count toward the threshold. Without
610
+ * decay `hits` is an all-time counter, so any shared
611
+ * NAT/carrier IP eventually accumulates `denyListThreshold` lifetime failures
612
+ * and blocks every user behind it.
613
+ */
614
+ const HITS_WINDOW_MS = 600 * 1e3;
615
+ /**
616
+ * Hard ceiling on tracked members per prefix. `userAgent` is an
617
+ * attacker-forgeable header, so without a cap one source can plant an unbounded
618
+ * number of permanent entries in this module-scope map.
619
+ */
620
+ const MAX_TRACKED_MEMBERS = 4096;
621
+ /** Bound timestamp storage even when applications configure a high threshold. */
622
+ const MAX_TRACKED_HITS = 65536;
623
+ /** Ceiling on a stored key so attacker-chosen value length cannot inflate cost. */
624
+ const MAX_MEMBER_KEY_LENGTH = 128;
625
+ /**
626
+ * Sweeping expired entries is O(tracked members), so it runs on an interval
627
+ * rather than per denial — the hot path under a flood is the denial path.
628
+ * Correctness does not depend on it: an expired hit already reads as absent.
629
+ */
630
+ const PRUNE_INTERVAL_MS = 3e4;
631
+ const protectionState = /* @__PURE__ */ new Map();
632
+ function getState(prefix) {
633
+ let state = protectionState.get(prefix);
634
+ if (!state) {
635
+ state = {
636
+ hitCount: 0,
637
+ members: /* @__PURE__ */ new Map(),
638
+ nextPruneAt: 0
639
+ };
640
+ protectionState.set(prefix, state);
641
+ }
642
+ return state;
643
+ }
644
+ /**
645
+ * Storage key for a member value. Overlong values are truncated and tagged with
646
+ * their length and hash, so a long forged header cannot inflate retained state
647
+ * or share protection state with another value that has the same prefix.
648
+ */
649
+ function memberKey(value) {
650
+ if (value.length <= MAX_MEMBER_KEY_LENGTH) return value;
651
+ return `${value.slice(0, MAX_MEMBER_KEY_LENGTH)}#${value.length}:${hashMember(value)}`;
652
+ }
653
+ function hashMember(value) {
654
+ let hash = 14695981039346656037n;
655
+ for (let index = 0; index < value.length; index++) {
656
+ hash ^= BigInt(value.charCodeAt(index));
657
+ hash = BigInt.asUintN(64, hash * 1099511628211n);
658
+ }
659
+ return hash.toString(16).padStart(16, "0");
660
+ }
661
+ function pruneExpired(state, now) {
662
+ const cutoff = now - HITS_WINDOW_MS;
663
+ for (const [key, member] of state.members) {
664
+ const retainedHits = member.hits.filter((hitAt) => hitAt > cutoff);
665
+ state.hitCount -= member.hits.length - retainedHits.length;
666
+ member.hits = retainedHits;
667
+ if (member.blockedUntil !== void 0 && member.blockedUntil <= now) member.blockedUntil = void 0;
668
+ if (member.hits.length === 0 && member.blockedUntil === void 0) state.members.delete(key);
669
+ }
670
+ }
671
+ function evictLeastRecentHitOnly(state) {
672
+ for (const [key, member] of state.members) {
673
+ if (member.blockedUntil !== void 0) continue;
674
+ state.hitCount -= member.hits.length;
675
+ member.hits = [];
676
+ state.members.delete(key);
677
+ return true;
678
+ }
679
+ return false;
680
+ }
681
+ function evictLeastRecentMember(state) {
682
+ for (const [key, member] of state.members) {
683
+ state.hitCount -= member.hits.length;
684
+ member.hits = [];
685
+ state.members.delete(key);
686
+ return;
687
+ }
688
+ }
689
+ function getMember(state, key) {
690
+ const existing = state.members.get(key);
691
+ if (existing) {
692
+ state.members.delete(key);
693
+ state.members.set(key, existing);
694
+ return existing;
695
+ }
696
+ if (state.members.size >= MAX_TRACKED_MEMBERS && !evictLeastRecentHitOnly(state)) evictLeastRecentMember(state);
697
+ const member = { hits: [] };
698
+ state.members.set(key, member);
699
+ return member;
700
+ }
701
+ function recordHit(state, key, threshold, now) {
702
+ const member = getMember(state, key);
703
+ const cutoff = now - HITS_WINDOW_MS;
704
+ const retainedHits = member.hits.filter((hitAt) => hitAt > cutoff);
705
+ state.hitCount -= member.hits.length - retainedHits.length;
706
+ member.hits = retainedHits;
707
+ while (state.hitCount >= MAX_TRACKED_HITS && evictLeastRecentHitOnly(state));
708
+ if (!state.members.has(key)) state.members.set(key, member);
709
+ member.hits.push(now);
710
+ state.hitCount += 1;
711
+ if (member.hits.length >= threshold) {
712
+ member.blockedUntil = now + THRESHOLD_BLOCK_MS;
713
+ state.hitCount -= member.hits.length;
714
+ member.hits = [];
715
+ }
716
+ }
717
+ function pickDeniedValue(options) {
718
+ const members = getMembers(options.identifier, options.request);
719
+ const state = getState(options.prefix);
720
+ const now = Date.now();
721
+ if (now >= state.nextPruneAt) {
722
+ pruneExpired(state, now);
723
+ state.nextPruneAt = now + PRUNE_INTERVAL_MS;
724
+ }
725
+ for (const member of members) {
726
+ const key = memberKey(member.value);
727
+ const tracked = state.members.get(key);
728
+ if (tracked?.blockedUntil && tracked.blockedUntil > now) {
729
+ state.members.delete(key);
730
+ state.members.set(key, tracked);
731
+ return member.value;
732
+ }
733
+ if (tracked?.blockedUntil && tracked.blockedUntil <= now) {
734
+ tracked.blockedUntil = void 0;
735
+ if (tracked.hits.length === 0) state.members.delete(key);
736
+ }
737
+ }
738
+ if (!options.lists) return;
739
+ const listMatchers = [
740
+ {
741
+ values: options.lists.identifiers,
742
+ kind: "identifier"
743
+ },
744
+ {
745
+ values: options.lists.ips,
746
+ kind: "ip"
747
+ },
748
+ {
749
+ values: options.lists.userAgents,
750
+ kind: "userAgent"
751
+ },
752
+ {
753
+ values: options.lists.countries,
754
+ kind: "country"
755
+ }
756
+ ];
757
+ for (const matcher of listMatchers) {
758
+ if (!matcher.values || matcher.values.length === 0) continue;
759
+ const valueSet = new Set(matcher.values);
760
+ const hit = members.find((member) => member.kind === matcher.kind && valueSet.has(member.value));
761
+ if (hit) {
762
+ const tracked = getMember(state, memberKey(hit.value));
763
+ tracked.blockedUntil = now + DEFAULT_BLOCK_MS;
764
+ state.hitCount -= tracked.hits.length;
765
+ tracked.hits = [];
766
+ return hit.value;
767
+ }
768
+ }
769
+ }
770
+ function recordRatelimitFailure(options) {
771
+ const members = getMembers(options.identifier, options.request);
772
+ const state = getState(options.prefix);
773
+ const now = Date.now();
774
+ if (now >= state.nextPruneAt) {
775
+ pruneExpired(state, now);
776
+ state.nextPruneAt = now + PRUNE_INTERVAL_MS;
777
+ }
778
+ for (const member of members) recordHit(state, memberKey(member.value), options.threshold, now);
779
+ }
780
+ /**
781
+ * Clears the identifier only. Widening this to every request member would let
782
+ * an attacker reset their own ip/userAgent counters with a single success, or
783
+ * clear a victim's counter by forging their user-agent.
784
+ */
785
+ function clearProtection(prefix, identifier) {
786
+ const state = getState(prefix);
787
+ const key = memberKey(identifier);
788
+ const member = state.members.get(key);
789
+ state.hitCount -= member?.hits.length ?? 0;
790
+ state.members.delete(key);
791
+ }
792
+ function getMembers(identifier, request) {
793
+ return [
794
+ {
795
+ kind: "identifier",
796
+ value: identifier
797
+ },
798
+ {
799
+ kind: "ip",
800
+ value: request?.ip
801
+ },
802
+ {
803
+ kind: "userAgent",
804
+ value: request?.userAgent
805
+ },
806
+ {
807
+ kind: "country",
808
+ value: request?.country
809
+ }
810
+ ].filter((member) => Boolean(member.value));
811
+ }
812
+
666
813
  //#endregion
667
814
  //#region src/ratelimit/ratelimit.ts
668
815
  const DEFAULT_PREFIX = "kitcn/ratelimit";
@@ -859,7 +1006,7 @@ var Ratelimit = class Ratelimit {
859
1006
  return new Ratelimit({
860
1007
  ...this.config,
861
1008
  db,
862
- ephemeralCache: this.blockCacheSource
1009
+ ephemeralCache: this.blockCacheSource ?? false
863
1010
  });
864
1011
  }
865
1012
  async evaluate(identifier, request, consume) {
@@ -1143,18 +1290,21 @@ const RatelimitPlugin = definePlugin("ratelimit", ({ options }) => {
1143
1290
  meta
1144
1291
  });
1145
1292
  const tier = await options.getTier(user);
1146
- const identifier = await options.getIdentifier({
1293
+ const requestArgs = {
1147
1294
  ctx,
1148
1295
  meta,
1149
1296
  user,
1150
- bucket
1297
+ bucket,
1298
+ tier
1299
+ };
1300
+ const signals = await options.getSignals(requestArgs);
1301
+ const identifier = await options.getIdentifier({
1302
+ ...requestArgs,
1303
+ signals
1151
1304
  });
1152
1305
  const args = {
1153
- ctx,
1154
- meta,
1155
- user,
1156
- bucket,
1157
- tier,
1306
+ ...requestArgs,
1307
+ signals,
1158
1308
  identifier
1159
1309
  };
1160
1310
  if (!(await new Ratelimit({
@@ -1163,8 +1313,12 @@ const RatelimitPlugin = definePlugin("ratelimit", ({ options }) => {
1163
1313
  limiter: resolveBucketLimiter(options, bucket, tier),
1164
1314
  failureMode: options.failureMode,
1165
1315
  enableProtection: options.enableProtection,
1166
- denyListThreshold: options.denyListThreshold
1167
- }).limit(identifier, await options.getSignals(args))).success) throw new CRPCError({
1316
+ denyListThreshold: options.denyListThreshold,
1317
+ denyList: options.denyList,
1318
+ dynamicLimits: options.dynamicLimits,
1319
+ timeout: options.timeout,
1320
+ ephemeralCache: options.ephemeralCache
1321
+ }).limit(identifier, signals)).success) throw new CRPCError({
1168
1322
  code: "TOO_MANY_REQUESTS",
1169
1323
  message: await resolveMessage(options, args)
1170
1324
  });
@@ -1180,4 +1334,4 @@ const DAY = 24 * HOUR;
1180
1334
  const WEEK = 7 * DAY;
1181
1335
 
1182
1336
  //#endregion
1183
- export { DAY, HOUR, MINUTE, RATE_LIMIT_DYNAMIC_TABLE, RATE_LIMIT_HIT_TABLE, RATE_LIMIT_STATE_TABLE, Ratelimit, RatelimitPlugin, SECOND, WEEK, applyDynamicLimit, calculateRatelimit, fixedWindow, slidingWindow, snapshotToState, toMs, tokenBucket };
1337
+ export { DAY, HOUR, MINUTE, RATE_LIMIT_DYNAMIC_TABLE, RATE_LIMIT_HIT_TABLE, RATE_LIMIT_STATE_TABLE, Ratelimit, RatelimitPlugin, SECOND, WEEK, applyDynamicLimit, calculateRatelimit, cleanupRatelimitState, fixedWindow, slidingWindow, snapshotToState, toMs, tokenBucket };
@@ -416,6 +416,12 @@ interface CRPCHttpRouter<TRecord extends HttpRouterRecord> {
416
416
  }
417
417
  //#endregion
418
418
  //#region src/crpc/http-types.d.ts
419
+ /** Exact cache key for one route + args, as stored in the QueryClient */
420
+ type HttpQueryKey = readonly ['httpQuery', string, unknown];
421
+ /** Route-wide prefix key that matches every args variant of one route */
422
+ type HttpQueryPrefixKey = readonly ['httpQuery', string];
423
+ /** Mutation key for HTTP endpoints */
424
+ type HttpMutationKey = readonly ['httpMutation', string];
419
425
  /** Error codes that can be returned from HTTP endpoints */
420
426
  type HttpErrorCode = 'BAD_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'METHOD_NOT_SUPPORTED' | 'CONFLICT' | 'UNPROCESSABLE_CONTENT' | 'TOO_MANY_REQUESTS' | 'INTERNAL_SERVER_ERROR' | 'UNKNOWN';
421
427
  /** HTTP client error */
@@ -692,9 +698,6 @@ type InferHttpClientArgs<T> = T extends HttpProcedure<infer TInput, infer _TOutp
692
698
  }> : HttpInputArgs;
693
699
  /** Infer output type from HttpProcedure */
694
700
  type InferHttpOutput<T> = T extends HttpProcedure<infer _TInput, infer TOutput, infer _TParams, infer _TQuery> ? TOutput extends UnsetMarker ? unknown : TOutput extends z.ZodTypeAny ? z.infer<TOutput> : unknown : unknown;
695
- /** Query key with args (3-element) or prefix key without args (2-element) for invalidation */
696
- type HttpQueryKey = readonly ['httpQuery', string, unknown] | readonly ['httpQuery', string];
697
- type HttpMutationKey = readonly ['httpMutation', string];
698
701
  type ReservedQueryOptions$1 = 'queryKey' | 'queryFn';
699
702
  type ReservedMutationOptions$1 = 'mutationFn';
700
703
  /** Query options for GET HTTP endpoints - compatible with both useQuery and useSuspenseQuery */
@@ -713,8 +716,8 @@ type HttpMutationOptions<T extends HttpProcedure> = DistributiveOmit<HttpMutatio
713
716
  * - mutationOptions: For one-time actions like exports (useMutation)
714
717
  */
715
718
  type DecorateHttpQuery<T extends HttpProcedure> = {
716
- queryOptions: keyof InferHttpInput<T> extends never ? (args?: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T> : object extends InferHttpInput<T> ? (args?: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T> : (args: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T>; /** Get query key for QueryClient methods (with args = exact match, without = prefix) */
717
- queryKey: (args?: InferHttpClientArgs<T>) => HttpQueryKey; /** Get query filter for QueryClient methods (e.g., invalidateQueries) */
719
+ queryOptions: keyof InferHttpInput<T> extends never ? (args?: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T> : object extends InferHttpInput<T> ? (args?: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T> : (args: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T>; /** Get the exact cache key these args are stored under (getQueryData/setQueryData) */
720
+ queryKey: (args?: InferHttpClientArgs<T>) => HttpQueryKey; /** Get query filter matching every args variant (e.g., invalidateQueries) */
718
721
  queryFilter: (args?: InferHttpClientArgs<T>, filters?: DistributiveOmit<QueryFilters, 'queryKey'>) => QueryFilters; /** Mutation options for GET endpoints (exports, downloads - no caching) */
719
722
  mutationOptions: (opts?: HttpMutationOptions<T>) => HttpMutationOptsReturn<T>; /** Get mutation key for QueryClient methods */
720
723
  mutationKey: () => HttpMutationKey;
@@ -1151,4 +1154,4 @@ declare function useUploadMutationOptions<TGenerateUrlMutation extends FunctionR
1151
1154
  */
1152
1155
  declare function createVanillaCRPCProxy<TApi extends Record<string, unknown>>(api: TApi, meta: CallerMeta, convexClient: ConvexReactClient$1, transformer?: DataTransformerOptions): VanillaCRPCClient<TApi>;
1153
1156
  //#endregion
1154
- export { AUTH_SESSION_SYNC_GRACE_MS, AuthMutationError, AuthProvider, AuthStore, AuthStoreState, Authenticated, CRPCHttpOptions, ConvexAuthBridge, ConvexAuthRecovery, ConvexAuthRecoveryError, ConvexAuthRecoveryErrorCode, ConvexAuthRecoveryOptions, ConvexAuthRecoveryStatus, ConvexProvider, ConvexProviderWithAuth, ConvexQueryClient, ConvexQueryClientOptions, ConvexQueryClientSingletonOptions, ConvexReactClient, CreateCRPCContextOptions, FetchAccessTokenContext, FetchAccessTokenFn, HttpCRPCClient, HttpCRPCClientFromRouter, type HttpClientOptions, type HttpFormValue, type HttpInputArgs, HttpMutationKey, HttpProxyOptions, HttpQueryKey, HttpRouteInfo, HttpRouteMap, MaybeAuthenticated, MaybeUnauthenticated, PaginationState, PaginationStatus, Unauthenticated, UseInfiniteQueryResult, VanillaHttpCRPCClient, VanillaHttpCRPCClientFromRouter, createAuthMutations, createCRPCContext, createCRPCOptionsProxy, createHttpProxy, createVanillaCRPCProxy, decodeJwtExp, getConvexQueryClientSingleton, getQueryClientSingleton, isAuthMutationError, isSessionSyncGraceActive, useAuth, useAuthGuard, useAuthState, useAuthStore, useAuthValue, useConvex, useConvexActionOptions, useConvexActionQueryOptions, useSafeConvexAuth as useConvexAuth, useSafeConvexAuth, useConvexAuthBridge, useConvexAuthRecovery, useConvexInfiniteQueryOptions, useConvexMutationOptions, useConvexQueryClient, useConvexQueryOptions, useFetchAccessToken, useFnMeta, useInfiniteQuery, useIsAuth, useMaybeAuth, useMeta, useUploadMutationOptions };
1157
+ export { AUTH_SESSION_SYNC_GRACE_MS, AuthMutationError, AuthProvider, AuthStore, AuthStoreState, Authenticated, CRPCHttpOptions, ConvexAuthBridge, ConvexAuthRecovery, ConvexAuthRecoveryError, ConvexAuthRecoveryErrorCode, ConvexAuthRecoveryOptions, ConvexAuthRecoveryStatus, ConvexProvider, ConvexProviderWithAuth, ConvexQueryClient, ConvexQueryClientOptions, ConvexQueryClientSingletonOptions, ConvexReactClient, CreateCRPCContextOptions, FetchAccessTokenContext, FetchAccessTokenFn, HttpCRPCClient, HttpCRPCClientFromRouter, type HttpClientOptions, type HttpFormValue, type HttpInputArgs, type HttpMutationKey, HttpProxyOptions, type HttpQueryKey, type HttpQueryPrefixKey, HttpRouteInfo, HttpRouteMap, MaybeAuthenticated, MaybeUnauthenticated, PaginationState, PaginationStatus, Unauthenticated, UseInfiniteQueryResult, VanillaHttpCRPCClient, VanillaHttpCRPCClientFromRouter, createAuthMutations, createCRPCContext, createCRPCOptionsProxy, createHttpProxy, createVanillaCRPCProxy, decodeJwtExp, getConvexQueryClientSingleton, getQueryClientSingleton, isAuthMutationError, isSessionSyncGraceActive, useAuth, useAuthGuard, useAuthState, useAuthStore, useAuthValue, useConvex, useConvexActionOptions, useConvexActionQueryOptions, useSafeConvexAuth as useConvexAuth, useSafeConvexAuth, useConvexAuthBridge, useConvexAuthRecovery, useConvexInfiniteQueryOptions, useConvexMutationOptions, useConvexQueryClient, useConvexQueryOptions, useFetchAccessToken, useFnMeta, useInfiniteQuery, useIsAuth, useMaybeAuth, useMeta, useUploadMutationOptions };
@@ -126,6 +126,38 @@ function getFunctionMeta(path, source) {
126
126
 
127
127
  //#endregion
128
128
  //#region src/crpc/http-types.ts
129
+ /**
130
+ * Build the exact cache key for an HTTP route.
131
+ *
132
+ * Missing args normalize to `{}` so the RSC prefetch, the browser observer, and
133
+ * a hand-written `getQueryData` call all hash to the same key. Every producer
134
+ * of an `httpQuery` cache key goes through here.
135
+ */
136
+ function buildHttpQueryKey(routeKey, args) {
137
+ return [
138
+ "httpQuery",
139
+ routeKey,
140
+ args ?? {}
141
+ ];
142
+ }
143
+ /**
144
+ * Build the route-wide prefix key.
145
+ *
146
+ * Filters match on prefix, so this matches every args variant of the route.
147
+ * Not a cache key: nothing is ever stored under it.
148
+ */
149
+ function buildHttpQueryPrefixKey(routeKey) {
150
+ return ["httpQuery", routeKey];
151
+ }
152
+ /**
153
+ * Freshness window for `crpc.http.*` routes, in milliseconds.
154
+ *
155
+ * HTTP routes are a pull model with no push channel, so hydrated data needs a
156
+ * real freshness window or the browser refetches it on mount. The RSC
157
+ * QueryClient reads the same constant, so the server dedupe window and the
158
+ * client freshness window cannot drift apart.
159
+ */
160
+ const HTTP_DEFAULT_STALE_TIME = 3e4;
129
161
  /** HTTP client error */
130
162
  var HttpClientError = class extends Error {
131
163
  name = "HttpClientError";
@@ -278,6 +310,7 @@ const DATE_CODEC_TAG = "$date";
278
310
  */
279
311
  const dateWireCodec = {
280
312
  tag: DATE_CODEC_TAG,
313
+ objectsOnly: true,
281
314
  isType: (value) => value instanceof Date,
282
315
  encode: (value) => value.getTime(),
283
316
  decode: (value) => {
@@ -286,6 +319,40 @@ const dateWireCodec = {
286
319
  }
287
320
  };
288
321
  /**
322
+ * One value per primitive `typeof` result, plus the values the object fast path
323
+ * would otherwise skip.
324
+ */
325
+ const PRIMITIVE_PROBES = [
326
+ void 0,
327
+ null,
328
+ "",
329
+ 0,
330
+ NaN,
331
+ false,
332
+ 0n,
333
+ Symbol("kitcn.codec.probe"),
334
+ () => void 0
335
+ ];
336
+ /**
337
+ * Falsify an `objectsOnly` declaration against representative primitives.
338
+ *
339
+ * Sampling cannot prove a predicate object-only, so it never *infers* the
340
+ * capability - it only rejects the misdeclarations it can catch
341
+ * (`typeof value === 'bigint'`, `value === null`, ...) before they silently
342
+ * drop a value's wire encoding. A codec that throws on a probe owns that.
343
+ */
344
+ const assertObjectsOnly = (codec) => {
345
+ for (const probe of PRIMITIVE_PROBES) {
346
+ let claimed = false;
347
+ try {
348
+ claimed = codec.isType(probe);
349
+ } catch {
350
+ continue;
351
+ }
352
+ if (claimed) throw new Error(`Wire codec '${codec.tag}' declares objectsOnly, but isType() claims ${probe === null ? "null" : typeof probe}. Drop objectsOnly so the codec keeps receiving non-object values.`);
353
+ }
354
+ };
355
+ /**
289
356
  * Build a recursive tagged transformer from codecs.
290
357
  */
291
358
  const createTaggedTransformer = (codecs) => {
@@ -293,9 +360,12 @@ const createTaggedTransformer = (codecs) => {
293
360
  for (const codec of codecs) {
294
361
  if (!codec.tag.startsWith("$")) throw new Error(`Invalid wire codec tag '${codec.tag}'. Tags must start with '$'.`);
295
362
  if (codecByTag.has(codec.tag)) throw new Error(`Duplicate wire codec tag '${codec.tag}'.`);
363
+ if (codec.objectsOnly) assertObjectsOnly(codec);
296
364
  codecByTag.set(codec.tag, codec);
297
365
  }
366
+ const skipNonObjects = codecs.every((codec) => codec.objectsOnly === true);
298
367
  const serialize = (value) => {
368
+ if (skipNonObjects && (value === null || typeof value !== "object")) return value;
299
369
  for (const codec of codecs) if (codec.isType(value)) return {
300
370
  [CODEC_MARKER_KEY]: CODEC_MARKER_VALUE,
301
371
  [CODEC_TAG_KEY]: codec.tag,
@@ -329,6 +399,7 @@ const createTaggedTransformer = (codecs) => {
329
399
  return value;
330
400
  };
331
401
  const deserialize = (value) => {
402
+ if (value === null || typeof value !== "object") return value;
332
403
  if (Array.isArray(value)) {
333
404
  let result;
334
405
  for (let index = 0; index < value.length; index += 1) {
@@ -394,16 +465,20 @@ const normalizeCustomTransformer = (transformer) => {
394
465
  * - deserialize: default(Date) -> user
395
466
  */
396
467
  const composeWithDefault = (transformer) => {
397
- if (!transformer) return defaultCRPCTransformer;
468
+ if (!transformer || transformer === defaultCRPCTransformer) return defaultCRPCTransformer;
398
469
  return {
399
470
  serialize: (value) => defaultCRPCTransformer.serialize(transformer.serialize(value)),
400
471
  deserialize: (value) => transformer.deserialize(defaultCRPCTransformer.deserialize(value))
401
472
  };
402
473
  };
403
474
  const transformerCache = /* @__PURE__ */ new WeakMap();
475
+ transformerCache.set(DEFAULT_COMBINED_TRANSFORMER, DEFAULT_COMBINED_TRANSFORMER);
404
476
  /**
405
477
  * Normalize transformer config to split input/output shape.
406
478
  * User transformers are additive and always composed with default Date handling.
479
+ *
480
+ * Idempotent: passing a transformer this function already resolved returns it
481
+ * unchanged.
407
482
  */
408
483
  const getTransformer = (transformer) => {
409
484
  if (!transformer) return DEFAULT_COMBINED_TRANSFORMER;
@@ -416,6 +491,7 @@ const getTransformer = (transformer) => {
416
491
  output: composeWithDefault(custom?.output)
417
492
  };
418
493
  transformerCache.set(cacheKey, resolved);
494
+ transformerCache.set(resolved, resolved);
419
495
  return resolved;
420
496
  };
421
497
  /**
@@ -480,12 +556,9 @@ function createRecursiveHttpProxy(opts, path = []) {
480
556
  if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
481
557
  if (route.method !== "GET") throw new Error(`queryOptions is only available for GET endpoints, got ${route.method} for ${routeKey}`);
482
558
  return (args, queryOpts) => ({
559
+ staleTime: HTTP_DEFAULT_STALE_TIME,
483
560
  ...queryOpts,
484
- queryKey: [
485
- "httpQuery",
486
- routeKey,
487
- args
488
- ],
561
+ queryKey: buildHttpQueryKey(routeKey, args),
489
562
  queryFn: async () => {
490
563
  try {
491
564
  return await executeHttpRequest({
@@ -504,22 +577,12 @@ function createRecursiveHttpProxy(opts, path = []) {
504
577
  }
505
578
  });
506
579
  }
507
- if (prop === "queryKey") return (args) => {
508
- return args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0) ? [
509
- "httpQuery",
510
- routeKey,
511
- args
512
- ] : ["httpQuery", routeKey];
513
- };
580
+ if (prop === "queryKey") return (args) => buildHttpQueryKey(routeKey, args);
514
581
  if (prop === "queryFilter") return (args, filters) => {
515
582
  const hasArgs = args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0);
516
583
  return {
517
584
  ...filters,
518
- queryKey: hasArgs ? [
519
- "httpQuery",
520
- routeKey,
521
- args
522
- ] : ["httpQuery", routeKey]
585
+ queryKey: hasArgs ? buildHttpQueryKey(routeKey, args) : buildHttpQueryPrefixKey(routeKey)
523
586
  };
524
587
  };
525
588
  if (prop === "mutationOptions") {
@@ -1,7 +1,7 @@
1
1
  import { n as DeepPartial, o as Simplify, r as DistributiveOmit } from "../types-DF2cg_w0.js";
2
- import { C as HttpProcedure, d as CRPCHttpRouter, j as DataTransformerOptions, p as HttpRouterRecord } from "../http-types-zsMHb_QN.js";
2
+ import { L as DataTransformerOptions, _ as CRPCHttpRouter, c as HttpQueryKey, k as HttpProcedure, o as HttpMutationKey, y as HttpRouterRecord } from "../http-types-DXOgaerG.js";
3
3
  import { g as UnsetMarker } from "../types-CnTpHR1F.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-jNTcza_a.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-BHR7ZKKD.js";
5
5
  import { FunctionArgs, FunctionReference, FunctionReturnType } from "convex/server";
6
6
  import { z } from "zod";
7
7
  import { DefaultError, QueryFilters, SkipToken, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
@@ -137,9 +137,6 @@ type InferHttpClientArgs<T> = T extends HttpProcedure<infer TInput, infer _TOutp
137
137
  }> : HttpInputArgs;
138
138
  /** Infer output type from HttpProcedure */
139
139
  type InferHttpOutput<T> = T extends HttpProcedure<infer _TInput, infer TOutput, infer _TParams, infer _TQuery> ? TOutput extends UnsetMarker ? unknown : TOutput extends z.ZodTypeAny ? z.infer<TOutput> : unknown : unknown;
140
- /** Query key with args (3-element) or prefix key without args (2-element) for invalidation */
141
- type HttpQueryKey = readonly ['httpQuery', string, unknown] | readonly ['httpQuery', string];
142
- type HttpMutationKey = readonly ['httpMutation', string];
143
140
  type ReservedQueryOptions = 'queryKey' | 'queryFn';
144
141
  type ReservedMutationOptions = 'mutationFn';
145
142
  /** Query options for GET HTTP endpoints - compatible with both useQuery and useSuspenseQuery */
@@ -158,8 +155,8 @@ type HttpMutationOptions<T extends HttpProcedure> = DistributiveOmit<HttpMutatio
158
155
  * - mutationOptions: For one-time actions like exports (useMutation)
159
156
  */
160
157
  type DecorateHttpQuery<T extends HttpProcedure> = {
161
- queryOptions: keyof InferHttpInput<T> extends never ? (args?: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T> : object extends InferHttpInput<T> ? (args?: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T> : (args: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T>; /** Get query key for QueryClient methods (with args = exact match, without = prefix) */
162
- queryKey: (args?: InferHttpClientArgs<T>) => HttpQueryKey; /** Get query filter for QueryClient methods (e.g., invalidateQueries) */
158
+ queryOptions: keyof InferHttpInput<T> extends never ? (args?: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T> : object extends InferHttpInput<T> ? (args?: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T> : (args: InferHttpClientArgs<T>, opts?: HttpQueryOptions<T>) => HttpQueryOptsReturn<T>; /** Get the exact cache key these args are stored under (getQueryData/setQueryData) */
159
+ queryKey: (args?: InferHttpClientArgs<T>) => HttpQueryKey; /** Get query filter matching every args variant (e.g., invalidateQueries) */
163
160
  queryFilter: (args?: InferHttpClientArgs<T>, filters?: DistributiveOmit<QueryFilters, 'queryKey'>) => QueryFilters; /** Mutation options for GET endpoints (exports, downloads - no caching) */
164
161
  mutationOptions: (opts?: HttpMutationOptions<T>) => HttpMutationOptsReturn<T>; /** Get mutation key for QueryClient methods */
165
162
  mutationKey: () => HttpMutationKey;