kitcn 0.23.0 → 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.
@@ -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";
@@ -524,12 +556,9 @@ function createRecursiveHttpProxy(opts, path = []) {
524
556
  if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
525
557
  if (route.method !== "GET") throw new Error(`queryOptions is only available for GET endpoints, got ${route.method} for ${routeKey}`);
526
558
  return (args, queryOpts) => ({
559
+ staleTime: HTTP_DEFAULT_STALE_TIME,
527
560
  ...queryOpts,
528
- queryKey: [
529
- "httpQuery",
530
- routeKey,
531
- args
532
- ],
561
+ queryKey: buildHttpQueryKey(routeKey, args),
533
562
  queryFn: async () => {
534
563
  try {
535
564
  return await executeHttpRequest({
@@ -548,22 +577,12 @@ function createRecursiveHttpProxy(opts, path = []) {
548
577
  }
549
578
  });
550
579
  }
551
- if (prop === "queryKey") return (args) => {
552
- return args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0) ? [
553
- "httpQuery",
554
- routeKey,
555
- args
556
- ] : ["httpQuery", routeKey];
557
- };
580
+ if (prop === "queryKey") return (args) => buildHttpQueryKey(routeKey, args);
558
581
  if (prop === "queryFilter") return (args, filters) => {
559
582
  const hasArgs = args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0);
560
583
  return {
561
584
  ...filters,
562
- queryKey: hasArgs ? [
563
- "httpQuery",
564
- routeKey,
565
- args
566
- ] : ["httpQuery", routeKey]
585
+ queryKey: hasArgs ? buildHttpQueryKey(routeKey, args) : buildHttpQueryPrefixKey(routeKey)
567
586
  };
568
587
  };
569
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-BoSDAh4Y.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-C0Xl7P8K.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;
package/dist/rsc/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as defaultIsUnauthorized } from "../error-Bvo7YEhk.js";
2
2
  import { n as getFuncRef, r as getFunctionMeta, t as buildMetaIndex } from "../meta-utils-D9K4fICl.js";
3
3
  import { o as encodeWire, s as getTransformer } from "../transformer-yZuBWo8v.js";
4
- import { n as convexInfiniteQueryOptions, o as executeHttpRequest, r as convexQuery } from "../query-options-C_eBSIXG.js";
4
+ import { c as HTTP_DEFAULT_STALE_TIME, n as convexInfiniteQueryOptions, o as executeHttpRequest, r as convexQuery, u as buildHttpQueryKey } from "../query-options-CzRV4G4N.js";
5
5
  import { convexToJson } from "convex/values";
6
6
  import { getFunctionName } from "convex/server";
7
7
  import { fetchAction, fetchQuery } from "convex/nextjs";
@@ -14,11 +14,7 @@ import { hashKey } from "@tanstack/query-core";
14
14
  */
15
15
  function buildHttpQueryOptions(route, routeKey, args) {
16
16
  return {
17
- queryKey: [
18
- "httpQuery",
19
- routeKey,
20
- args
21
- ],
17
+ queryKey: buildHttpQueryKey(routeKey, args),
22
18
  meta: {
23
19
  path: route.path,
24
20
  method: route.method
@@ -59,7 +55,7 @@ function createRecursiveProxy(api, path, meta) {
59
55
  const routeKey = path.slice(1).join(".");
60
56
  const route = meta._http?.[routeKey];
61
57
  if (!route) throw new Error(`HTTP route not found: ${routeKey}`);
62
- return (args = {}) => buildHttpQueryOptions(route, routeKey, args);
58
+ return (args) => buildHttpQueryOptions(route, routeKey, args);
63
59
  }
64
60
  if (prop === "queryOptions") return (args = {}, opts) => {
65
61
  return convexQuery(getFuncRef(api, path), args, meta, opts);
@@ -189,7 +185,7 @@ function createHashFn(fallback = hashKey) {
189
185
  function getServerQueryClientOptions({ getToken, convexSiteUrl, transformer: transformerOptions } = {}) {
190
186
  const transformer = getTransformer(transformerOptions);
191
187
  return { queries: {
192
- staleTime: 3e4,
188
+ staleTime: HTTP_DEFAULT_STALE_TIME,
193
189
  queryFn: async ({ queryKey, meta }) => {
194
190
  const [type, ...rest] = queryKey;
195
191
  const token = await getToken?.();
@@ -1,5 +1,5 @@
1
- import { $ as ActionProcedureBuilder, A as CRPCError, B as RuntimeEnv, C as createProcedureHandlerFactory, Ct as zCustomQuery, D as WithHttpRouter, Dt as zodToConvex, E as typedProcedureResolver, Et as zodOutputToConvexFields, F as getCRPCErrorFromUnknown, G as createLazyCaller, H as ConvexContext, I as getHTTPStatusCodeFromError, J as ServerCaller, K as CallerMeta, L as isCRPCError, M as CRPCErrorData, N as CRPC_ERROR_CODES_BY_KEY, O as inferApiInputs, Ot as zodToConvexFields, P as CRPC_ERROR_CODE_TO_HTTP, Q as getGeneratedValue, R as toCRPCError, S as createProcedureCallerFactory, St as zCustomMutation, T as getGeneratedFunctionReference, Tt as zodOutputToConvex, U as createCallerFactory, V as createEnv, W as LazyCaller, X as createApiLeaf, Y as createServerCaller, Z as createGeneratedFunctionReference, _ as ProcedureSchedulableCallerFromRegistry, _t as ZodValidatorFromConvex, a as CreateProcedureCallerFactoryOptions, at as initCRPC, b as createGenericCallerFactory, bt as withSystemFields, c as GeneratedRegistryCallerFactory, ct as extractPathParams, d as GeneratedRegistryHandlerForContext, dt as ConvexValidatorFromZod, et as CRPCFunctionTypeHint, f as ProcedureActionCallerFromRegistry, ft as ConvexValidatorFromZodOutput, g as ProcedureFromFunctionReference, gt as ZodFromValidatorBase, h as ProcedureDefinition, ht as Zid, i as registerProcedureNameLookup, it as createMiddlewareFactory, j as CRPCErrorCode, k as inferApiOutputs, l as GeneratedRegistryCallerForContext, lt as handleHttpError, m as ProcedureCallerFromRegistry, mt as ZCustomCtx, n as ProcedureNameLookup, nt as ProcedureBuilder, o as GeneratedProcedureRegistry, ot as HttpProcedureBuilder, p as ProcedureCaller, pt as CustomBuilder, q as CallerOpts, r as inferProcedureNameFromCallsite, rt as QueryProcedureBuilder, s as GeneratedProcedureRegistryEntry, st as createHttpProcedureBuilder, t as ProcedureNameEntry, tt as MutationProcedureBuilder, u as GeneratedRegistryHandlerFactory, ut as matchPathParams, v as ProcedureScheduleCallerFromRegistry, vt as convexToZod, w as defineProcedure, wt as zid, x as createGenericHandlerFactory, xt as zCustomAction, y as createGeneratedRegistryRuntime, yt as convexToZodFields, z as CreateEnvOptions } from "../procedure-name-C55TynK3.js";
2
- import { C as HttpProcedure, D as ProcedureMeta, E as InferHttpInput, S as HttpMethod, T as HttpRouteDefinition, _ as extractRouteMap, b as HttpActionHandler, d as CRPCHttpRouter, f as HttpRouterDef, g as createHttpRouterFactory, h as createHttpRouter, m as HttpRouterWithHono, p as HttpRouterRecord, v as CRPCHonoHandler, w as HttpProcedureBuilderDef, x as HttpHandlerOpts, y as HttpActionConstructor } from "../http-types-BoSDAh4Y.js";
1
+ import { $ as ActionProcedureBuilder, A as CRPCError, B as RuntimeEnv, C as createProcedureHandlerFactory, Ct as zCustomQuery, D as WithHttpRouter, Dt as zodToConvex, E as typedProcedureResolver, Et as zodOutputToConvexFields, F as getCRPCErrorFromUnknown, G as createLazyCaller, H as ConvexContext, I as getHTTPStatusCodeFromError, J as ServerCaller, K as CallerMeta, L as isCRPCError, M as CRPCErrorData, N as CRPC_ERROR_CODES_BY_KEY, O as inferApiInputs, Ot as zodToConvexFields, P as CRPC_ERROR_CODE_TO_HTTP, Q as getGeneratedValue, R as toCRPCError, S as createProcedureCallerFactory, St as zCustomMutation, T as getGeneratedFunctionReference, Tt as zodOutputToConvex, U as createCallerFactory, V as createEnv, W as LazyCaller, X as createApiLeaf, Y as createServerCaller, Z as createGeneratedFunctionReference, _ as ProcedureSchedulableCallerFromRegistry, _t as ZodValidatorFromConvex, a as CreateProcedureCallerFactoryOptions, at as initCRPC, b as createGenericCallerFactory, bt as withSystemFields, c as GeneratedRegistryCallerFactory, ct as extractPathParams, d as GeneratedRegistryHandlerForContext, dt as ConvexValidatorFromZod, et as CRPCFunctionTypeHint, f as ProcedureActionCallerFromRegistry, ft as ConvexValidatorFromZodOutput, g as ProcedureFromFunctionReference, gt as ZodFromValidatorBase, h as ProcedureDefinition, ht as Zid, i as registerProcedureNameLookup, it as createMiddlewareFactory, j as CRPCErrorCode, k as inferApiOutputs, l as GeneratedRegistryCallerForContext, lt as handleHttpError, m as ProcedureCallerFromRegistry, mt as ZCustomCtx, n as ProcedureNameLookup, nt as ProcedureBuilder, o as GeneratedProcedureRegistry, ot as HttpProcedureBuilder, p as ProcedureCaller, pt as CustomBuilder, q as CallerOpts, r as inferProcedureNameFromCallsite, rt as QueryProcedureBuilder, s as GeneratedProcedureRegistryEntry, st as createHttpProcedureBuilder, t as ProcedureNameEntry, tt as MutationProcedureBuilder, u as GeneratedRegistryHandlerFactory, ut as matchPathParams, v as ProcedureScheduleCallerFromRegistry, vt as convexToZod, w as defineProcedure, wt as zid, x as createGenericHandlerFactory, xt as zCustomAction, y as createGeneratedRegistryRuntime, yt as convexToZodFields, z as CreateEnvOptions } from "../procedure-name-C20pFZnk.js";
2
+ import { A as HttpProcedureBuilderDef, C as extractRouteMap, D as HttpHandlerOpts, E as HttpActionHandler, M as InferHttpInput, N as ProcedureMeta, O as HttpMethod, S as createHttpRouterFactory, T as HttpActionConstructor, _ as CRPCHttpRouter, b as HttpRouterWithHono, j as HttpRouteDefinition, k as HttpProcedure, v as HttpRouterDef, w as CRPCHonoHandler, x as createHttpRouter, y as HttpRouterRecord } from "../http-types-DXOgaerG.js";
3
3
  import { a as MergeZodObjects, c as MiddlewareMarker, d as MiddlewareProcedureType, f as MiddlewareResult, g as UnsetMarker, h as Simplify, i as IntersectIfDefined, l as MiddlewareNext, m as ResolveIfSet, n as AnyMiddlewareBuilder, o as MiddlewareBuilder, p as Overwrite, r as GetRawInputFn, s as MiddlewareFunction, t as AnyMiddleware, u as MiddlewareProcedureInfo } from "../types-CnTpHR1F.js";
4
4
  import { a as isMutationCtx, c as isSchedulerCtx, d as requireQueryCtx, f as requireRunMutationCtx, i as isActionCtx, l as requireActionCtx, n as RunMutationCtx, o as isQueryCtx, p as requireSchedulerCtx, r as SchedulerCtx, s as isRunMutationCtx, t as GenericCtx, u as requireMutationCtx } from "../context-utils-BBUtBqjN.js";
5
5
  export { ActionProcedureBuilder, AnyMiddleware, AnyMiddlewareBuilder, CRPCError, CRPCErrorCode, CRPCErrorData, CRPCFunctionTypeHint, CRPCHonoHandler, CRPCHttpRouter, CRPC_ERROR_CODES_BY_KEY, CRPC_ERROR_CODE_TO_HTTP, CallerMeta, CallerOpts, ConvexContext, ConvexValidatorFromZod, ConvexValidatorFromZodOutput, CreateEnvOptions, CreateProcedureCallerFactoryOptions, CustomBuilder, GeneratedProcedureRegistry, GeneratedProcedureRegistryEntry, GeneratedRegistryCallerFactory, GeneratedRegistryCallerForContext, GeneratedRegistryHandlerFactory, GeneratedRegistryHandlerForContext, GenericCtx, GetRawInputFn, HttpActionConstructor, HttpActionHandler, HttpHandlerOpts, HttpMethod, HttpProcedure, HttpProcedureBuilder, HttpProcedureBuilderDef, HttpRouteDefinition, HttpRouterDef, HttpRouterRecord, HttpRouterWithHono, InferHttpInput, IntersectIfDefined, LazyCaller, MergeZodObjects, MiddlewareBuilder, MiddlewareFunction, MiddlewareMarker, MiddlewareNext, MiddlewareProcedureInfo, MiddlewareProcedureType, MiddlewareResult, MutationProcedureBuilder, Overwrite, ProcedureActionCallerFromRegistry, ProcedureBuilder, ProcedureCaller, ProcedureCallerFromRegistry, ProcedureDefinition, ProcedureFromFunctionReference, ProcedureMeta, ProcedureNameEntry, ProcedureNameLookup, ProcedureSchedulableCallerFromRegistry, ProcedureScheduleCallerFromRegistry, QueryProcedureBuilder, ResolveIfSet, RunMutationCtx, RuntimeEnv, SchedulerCtx, ServerCaller, Simplify, UnsetMarker, WithHttpRouter, ZCustomCtx, Zid, ZodFromValidatorBase, ZodValidatorFromConvex, convexToZod, convexToZodFields, createApiLeaf, createCallerFactory, createEnv, createGeneratedFunctionReference, createGeneratedRegistryRuntime, createGenericCallerFactory, createGenericHandlerFactory, createHttpProcedureBuilder, createHttpRouter, createHttpRouterFactory, createLazyCaller, createMiddlewareFactory, createProcedureCallerFactory, createProcedureHandlerFactory, createServerCaller, defineProcedure, extractPathParams, extractRouteMap, getCRPCErrorFromUnknown, getGeneratedFunctionReference, getGeneratedValue, getHTTPStatusCodeFromError, handleHttpError, inferApiInputs, inferApiOutputs, inferProcedureNameFromCallsite, initCRPC, isActionCtx, isCRPCError, isMutationCtx, isQueryCtx, isRunMutationCtx, isSchedulerCtx, matchPathParams, registerProcedureNameLookup, requireActionCtx, requireMutationCtx, requireQueryCtx, requireRunMutationCtx, requireSchedulerCtx, toCRPCError, typedProcedureResolver, withSystemFields, zCustomAction, zCustomMutation, zCustomQuery, zid, zodOutputToConvex, zodOutputToConvexFields, zodToConvex, zodToConvexFields };