@concavejs/cli 0.0.1-alpha.6 → 0.0.1-alpha.7

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.
@@ -10081,6 +10081,8 @@ function decodeJwtClaimsToken(token) {
10081
10081
  return null;
10082
10082
  }
10083
10083
  }
10084
+ var DEFAULT_JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
10085
+ var MAX_JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
10084
10086
  var JWKS_CACHE = new Map;
10085
10087
  function decodeJwtUnsafe(token) {
10086
10088
  if (!token)
@@ -11137,6 +11139,9 @@ class FsBlobStore {
11137
11139
  return false;
11138
11140
  }
11139
11141
  }
11142
+ isNotFoundError(error) {
11143
+ return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
11144
+ }
11140
11145
  generateStorageId() {
11141
11146
  return crypto.randomUUID();
11142
11147
  }
@@ -11177,31 +11182,25 @@ class FsBlobStore {
11177
11182
  async get(storageId) {
11178
11183
  const objectPath = this.getObjectPath(storageId);
11179
11184
  try {
11180
- const fileExists = await this.pathExists(objectPath);
11181
- if (!fileExists) {
11182
- return null;
11183
- }
11184
11185
  const data = await readFile(objectPath);
11185
11186
  const metadata = await this.getMetadata(storageId);
11186
11187
  const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
11187
11188
  return new Blob([arrayBuffer], { type: metadata?.contentType || "application/octet-stream" });
11188
11189
  } catch (error) {
11190
+ if (this.isNotFoundError(error)) {
11191
+ return null;
11192
+ }
11189
11193
  console.error("Error reading object from filesystem:", error);
11190
- return null;
11194
+ throw error;
11191
11195
  }
11192
11196
  }
11193
11197
  async delete(storageId) {
11194
11198
  const objectPath = this.getObjectPath(storageId);
11195
11199
  const metadataPath = this.getMetadataPath(storageId);
11196
- try {
11197
- await Promise.all([
11198
- unlink(objectPath).catch(() => {}),
11199
- unlink(metadataPath).catch(() => {})
11200
- ]);
11201
- } catch (error) {
11202
- console.error("Error deleting object from filesystem:", error);
11203
- throw error;
11204
- }
11200
+ await Promise.all([
11201
+ this.unlinkIfExists(objectPath),
11202
+ this.unlinkIfExists(metadataPath)
11203
+ ]);
11205
11204
  }
11206
11205
  async getUrl(storageId) {
11207
11206
  const objectPath = this.getObjectPath(storageId);
@@ -11215,15 +11214,25 @@ class FsBlobStore {
11215
11214
  async getMetadata(storageId) {
11216
11215
  const metadataPath = this.getMetadataPath(storageId);
11217
11216
  try {
11218
- const fileExists = await this.pathExists(metadataPath);
11219
- if (!fileExists) {
11220
- return null;
11221
- }
11222
11217
  const data = await readFile(metadataPath, "utf-8");
11223
11218
  return JSON.parse(data);
11224
11219
  } catch (error) {
11220
+ if (this.isNotFoundError(error)) {
11221
+ return null;
11222
+ }
11225
11223
  console.error("Error reading metadata from filesystem:", error);
11226
- return null;
11224
+ throw error;
11225
+ }
11226
+ }
11227
+ async unlinkIfExists(path) {
11228
+ try {
11229
+ await unlink(path);
11230
+ } catch (error) {
11231
+ if (this.isNotFoundError(error)) {
11232
+ return;
11233
+ }
11234
+ console.error("Error deleting object from filesystem:", error);
11235
+ throw error;
11227
11236
  }
11228
11237
  }
11229
11238
  }
@@ -17076,6 +17085,8 @@ class SystemAuthError extends Error {
17076
17085
  }
17077
17086
  }
17078
17087
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 60;
17088
+ var DEFAULT_JWKS_CACHE_TTL_MS2 = 5 * 60 * 1000;
17089
+ var MAX_JWKS_CACHE_TTL_MS2 = 24 * 60 * 60 * 1000;
17079
17090
  var JWKS_CACHE2 = new Map;
17080
17091
  var defaultValidationConfig;
17081
17092
  var adminAuthConfig;
@@ -17171,7 +17182,8 @@ function resolveJwtValidationConfigFromEnv(env) {
17171
17182
  const secret = getEnvValue("AUTH_SECRET", env) ?? getEnvValue("CONCAVE_JWT_SECRET", env) ?? getEnvValue("JWT_SECRET", env);
17172
17183
  const skipVerification = parseBoolean(getEnvValue("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean(getEnvValue("CONCAVE_JWT_SKIP_VERIFICATION", env));
17173
17184
  const clockTolerance = parseNumber(getEnvValue("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber(getEnvValue("CONCAVE_JWT_CLOCK_TOLERANCE", env));
17174
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
17185
+ const jwksCacheTtlMs = parseNumber(getEnvValue("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber(getEnvValue("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
17186
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
17175
17187
  return;
17176
17188
  }
17177
17189
  return {
@@ -17180,7 +17192,8 @@ function resolveJwtValidationConfigFromEnv(env) {
17180
17192
  audience,
17181
17193
  secret,
17182
17194
  skipVerification,
17183
- clockTolerance
17195
+ clockTolerance,
17196
+ jwksCacheTtlMs
17184
17197
  };
17185
17198
  }
17186
17199
  function normalizeList(value) {
@@ -17224,15 +17237,33 @@ function validateClaims(claims, config) {
17224
17237
  throw new JWTValidationError("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
17225
17238
  }
17226
17239
  }
17227
- function getRemoteJwks(jwksUrl) {
17240
+ function getRemoteJwks(jwksUrl, config) {
17241
+ const now = Date.now();
17228
17242
  const cached = JWKS_CACHE2.get(jwksUrl);
17243
+ if (cached && cached.expiresAtMs > now) {
17244
+ return cached.resolver;
17245
+ }
17229
17246
  if (cached) {
17230
- return cached;
17247
+ JWKS_CACHE2.delete(jwksUrl);
17231
17248
  }
17232
17249
  const jwks = createRemoteJWKSet(new URL(jwksUrl));
17233
- JWKS_CACHE2.set(jwksUrl, jwks);
17250
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig?.jwksCacheTtlMs;
17251
+ const ttlMs = resolveJwksCacheTtlMs(configuredTtl);
17252
+ JWKS_CACHE2.set(jwksUrl, {
17253
+ resolver: jwks,
17254
+ expiresAtMs: now + ttlMs
17255
+ });
17234
17256
  return jwks;
17235
17257
  }
17258
+ function resolveJwksCacheTtlMs(configuredTtl) {
17259
+ if (configuredTtl === undefined) {
17260
+ return DEFAULT_JWKS_CACHE_TTL_MS2;
17261
+ }
17262
+ if (!Number.isFinite(configuredTtl)) {
17263
+ return DEFAULT_JWKS_CACHE_TTL_MS2;
17264
+ }
17265
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS2, Math.floor(configuredTtl)));
17266
+ }
17236
17267
  function decodeJwtUnsafe2(token) {
17237
17268
  if (!token)
17238
17269
  return null;
@@ -17265,7 +17296,7 @@ async function verifyJwt(token, config) {
17265
17296
  const key = new TextEncoder().encode(effectiveConfig.secret);
17266
17297
  ({ payload } = await jwtVerify(token, key, options));
17267
17298
  } else {
17268
- ({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl), options));
17299
+ ({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl, effectiveConfig), options));
17269
17300
  }
17270
17301
  const claims = payload;
17271
17302
  validateClaims(claims, effectiveConfig);
@@ -17320,7 +17351,7 @@ class UdfExecutionAdapter {
17320
17351
  this.executor = executor;
17321
17352
  this.callType = callType;
17322
17353
  }
17323
- async executeUdf(path, jsonArgs, type, auth, componentPath, requestId) {
17354
+ async executeUdf(path, jsonArgs, type, auth, componentPath, requestId, snapshotTimestamp) {
17324
17355
  const convexArgs = convertClientArgs(jsonArgs);
17325
17356
  const target = normalizeExecutionTarget(path, componentPath);
17326
17357
  let authContext2;
@@ -17358,7 +17389,7 @@ class UdfExecutionAdapter {
17358
17389
  return runWithAuth(userIdentity, async () => {
17359
17390
  const executeWithContext = this.callType === "client" ? runAsClientCall : runAsServerCall2;
17360
17391
  return executeWithContext(async () => {
17361
- return await this.executor.execute(target.path, convexArgs, type, authContext2 ?? userIdentity, normalizeComponentPath3(target.componentPath), requestId);
17392
+ return await this.executor.execute(target.path, convexArgs, type, authContext2 ?? userIdentity, normalizeComponentPath3(target.componentPath), requestId, snapshotTimestamp);
17362
17393
  });
17363
17394
  });
17364
17395
  }
@@ -19632,6 +19663,11 @@ var WEBSOCKET_READY_STATE_OPEN = 1;
19632
19663
  var BACKPRESSURE_HIGH_WATER_MARK = 100;
19633
19664
  var BACKPRESSURE_BUFFER_LIMIT = 1024 * 1024;
19634
19665
  var SLOW_CLIENT_TIMEOUT_MS = 30000;
19666
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 5000;
19667
+ var DEFAULT_MAX_MESSAGES_PER_WINDOW = 1000;
19668
+ var DEFAULT_OPERATION_TIMEOUT_MS = 15000;
19669
+ var DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION = 1000;
19670
+ var RATE_LIMIT_HARD_MULTIPLIER = 5;
19635
19671
 
19636
19672
  class SyncSession {
19637
19673
  websocket;
@@ -19660,15 +19696,25 @@ class SyncSession {
19660
19696
  class SyncProtocolHandler {
19661
19697
  udfExecutor;
19662
19698
  sessions = new Map;
19699
+ rateLimitStates = new Map;
19663
19700
  subscriptionManager;
19664
19701
  instanceName;
19665
19702
  backpressureController;
19666
19703
  heartbeatController;
19667
19704
  isDev;
19705
+ maxMessagesPerWindow;
19706
+ rateLimitWindowMs;
19707
+ operationTimeoutMs;
19708
+ maxActiveQueriesPerSession;
19668
19709
  constructor(instanceName, udfExecutor, options) {
19669
19710
  this.udfExecutor = udfExecutor;
19670
19711
  this.instanceName = instanceName;
19671
19712
  this.isDev = options?.isDev ?? true;
19713
+ this.maxMessagesPerWindow = Math.max(1, options?.maxMessagesPerWindow ?? DEFAULT_MAX_MESSAGES_PER_WINDOW);
19714
+ this.rateLimitWindowMs = Math.max(1, options?.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS);
19715
+ const configuredOperationTimeout = options?.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
19716
+ this.operationTimeoutMs = Number.isFinite(configuredOperationTimeout) ? Math.max(0, Math.floor(configuredOperationTimeout)) : DEFAULT_OPERATION_TIMEOUT_MS;
19717
+ this.maxActiveQueriesPerSession = Math.max(1, options?.maxActiveQueriesPerSession ?? DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION);
19672
19718
  this.subscriptionManager = new SubscriptionManager;
19673
19719
  this.backpressureController = new SessionBackpressureController({
19674
19720
  websocketReadyStateOpen: WEBSOCKET_READY_STATE_OPEN,
@@ -19686,6 +19732,10 @@ class SyncProtocolHandler {
19686
19732
  createSession(sessionId, websocket) {
19687
19733
  const session = new SyncSession(websocket);
19688
19734
  this.sessions.set(sessionId, session);
19735
+ this.rateLimitStates.set(sessionId, {
19736
+ windowStartedAt: Date.now(),
19737
+ messagesInWindow: 0
19738
+ });
19689
19739
  return session;
19690
19740
  }
19691
19741
  getSession(sessionId) {
@@ -19700,6 +19750,7 @@ class SyncProtocolHandler {
19700
19750
  session.isDraining = false;
19701
19751
  this.subscriptionManager.unsubscribeAll(sessionId);
19702
19752
  this.sessions.delete(sessionId);
19753
+ this.rateLimitStates.delete(sessionId);
19703
19754
  }
19704
19755
  }
19705
19756
  updateSessionId(oldSessionId, newSessionId) {
@@ -19707,6 +19758,11 @@ class SyncProtocolHandler {
19707
19758
  if (session) {
19708
19759
  this.sessions.delete(oldSessionId);
19709
19760
  this.sessions.set(newSessionId, session);
19761
+ const rateLimitState = this.rateLimitStates.get(oldSessionId);
19762
+ if (rateLimitState) {
19763
+ this.rateLimitStates.delete(oldSessionId);
19764
+ this.rateLimitStates.set(newSessionId, rateLimitState);
19765
+ }
19710
19766
  this.subscriptionManager.updateSessionId(oldSessionId, newSessionId);
19711
19767
  }
19712
19768
  }
@@ -19715,6 +19771,29 @@ class SyncProtocolHandler {
19715
19771
  if (!session && message2.type !== "Connect") {
19716
19772
  throw new Error("Session not found");
19717
19773
  }
19774
+ if (session) {
19775
+ const rateLimitDecision = this.consumeRateLimit(sessionId);
19776
+ if (rateLimitDecision === "reject") {
19777
+ return [
19778
+ {
19779
+ type: "FatalError",
19780
+ error: "Rate limit exceeded, retry shortly"
19781
+ }
19782
+ ];
19783
+ }
19784
+ if (rateLimitDecision === "close") {
19785
+ try {
19786
+ session.websocket.close(1013, "Rate limit exceeded");
19787
+ } catch {}
19788
+ this.destroySession(sessionId);
19789
+ return [
19790
+ {
19791
+ type: "FatalError",
19792
+ error: "Rate limit exceeded"
19793
+ }
19794
+ ];
19795
+ }
19796
+ }
19718
19797
  switch (message2.type) {
19719
19798
  case "Connect":
19720
19799
  return this.handleConnect(sessionId, message2);
@@ -19771,6 +19850,15 @@ class SyncProtocolHandler {
19771
19850
  return [fatalError];
19772
19851
  }
19773
19852
  const startVersion = makeStateVersion(session.querySetVersion, session.identityVersion, session.timestamp);
19853
+ const projectedActiveQueryCount = this.computeProjectedActiveQueryCount(session, message2);
19854
+ if (projectedActiveQueryCount > this.maxActiveQueriesPerSession) {
19855
+ return [
19856
+ {
19857
+ type: "FatalError",
19858
+ error: `Too many active queries: ${projectedActiveQueryCount} exceeds limit ${this.maxActiveQueriesPerSession}`
19859
+ }
19860
+ ];
19861
+ }
19774
19862
  session.querySetVersion = message2.newVersion;
19775
19863
  const modifications = [];
19776
19864
  for (const mod of message2.modifications) {
@@ -20094,7 +20182,54 @@ class SyncProtocolHandler {
20094
20182
  }, () => {
20095
20183
  return;
20096
20184
  });
20097
- return run;
20185
+ return this.withOperationTimeout(run);
20186
+ }
20187
+ withOperationTimeout(promise) {
20188
+ if (this.operationTimeoutMs <= 0) {
20189
+ return promise;
20190
+ }
20191
+ let timeoutHandle;
20192
+ const timeoutPromise = new Promise((_, reject) => {
20193
+ timeoutHandle = setTimeout(() => {
20194
+ reject(new Error(`Sync operation timed out after ${this.operationTimeoutMs}ms`));
20195
+ }, this.operationTimeoutMs);
20196
+ });
20197
+ return Promise.race([promise, timeoutPromise]).finally(() => {
20198
+ if (timeoutHandle) {
20199
+ clearTimeout(timeoutHandle);
20200
+ }
20201
+ });
20202
+ }
20203
+ consumeRateLimit(sessionId) {
20204
+ const state = this.rateLimitStates.get(sessionId);
20205
+ if (!state) {
20206
+ return "allow";
20207
+ }
20208
+ const now = Date.now();
20209
+ if (now - state.windowStartedAt >= this.rateLimitWindowMs) {
20210
+ state.windowStartedAt = now;
20211
+ state.messagesInWindow = 0;
20212
+ }
20213
+ state.messagesInWindow += 1;
20214
+ if (state.messagesInWindow <= this.maxMessagesPerWindow) {
20215
+ return "allow";
20216
+ }
20217
+ const hardLimit = Math.max(this.maxMessagesPerWindow + 1, this.maxMessagesPerWindow * RATE_LIMIT_HARD_MULTIPLIER);
20218
+ if (state.messagesInWindow >= hardLimit) {
20219
+ return "close";
20220
+ }
20221
+ return "reject";
20222
+ }
20223
+ computeProjectedActiveQueryCount(session, message2) {
20224
+ const projected = new Set(session.activeQueries.keys());
20225
+ for (const mod of message2.modifications) {
20226
+ if (mod.type === "Add") {
20227
+ projected.add(mod.queryId);
20228
+ } else if (mod.type === "Remove") {
20229
+ projected.delete(mod.queryId);
20230
+ }
20231
+ }
20232
+ return projected.size;
20098
20233
  }
20099
20234
  sendPing(session) {
20100
20235
  if (!session.websocket || session.websocket.readyState !== WEBSOCKET_READY_STATE_OPEN) {
@@ -21977,6 +22112,8 @@ class SystemAuthError2 extends Error {
21977
22112
  }
21978
22113
  }
21979
22114
  var DEFAULT_CLOCK_TOLERANCE_SECONDS2 = 60;
22115
+ var DEFAULT_JWKS_CACHE_TTL_MS3 = 5 * 60 * 1000;
22116
+ var MAX_JWKS_CACHE_TTL_MS3 = 24 * 60 * 60 * 1000;
21980
22117
  var JWKS_CACHE3 = new Map;
21981
22118
  var defaultValidationConfig2;
21982
22119
  var adminAuthConfig2;
@@ -22092,7 +22229,8 @@ function resolveJwtValidationConfigFromEnv2(env) {
22092
22229
  const secret = getEnvValue2("AUTH_SECRET", env) ?? getEnvValue2("CONCAVE_JWT_SECRET", env) ?? getEnvValue2("JWT_SECRET", env);
22093
22230
  const skipVerification = parseBoolean2(getEnvValue2("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean2(getEnvValue2("CONCAVE_JWT_SKIP_VERIFICATION", env));
22094
22231
  const clockTolerance = parseNumber2(getEnvValue2("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber2(getEnvValue2("CONCAVE_JWT_CLOCK_TOLERANCE", env));
22095
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
22232
+ const jwksCacheTtlMs = parseNumber2(getEnvValue2("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber2(getEnvValue2("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
22233
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
22096
22234
  return;
22097
22235
  }
22098
22236
  return {
@@ -22101,7 +22239,8 @@ function resolveJwtValidationConfigFromEnv2(env) {
22101
22239
  audience,
22102
22240
  secret,
22103
22241
  skipVerification,
22104
- clockTolerance
22242
+ clockTolerance,
22243
+ jwksCacheTtlMs
22105
22244
  };
22106
22245
  }
22107
22246
  function normalizeList2(value) {
@@ -22145,15 +22284,33 @@ function validateClaims2(claims, config) {
22145
22284
  throw new JWTValidationError2("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
22146
22285
  }
22147
22286
  }
22148
- function getRemoteJwks2(jwksUrl) {
22287
+ function getRemoteJwks2(jwksUrl, config) {
22288
+ const now = Date.now();
22149
22289
  const cached = JWKS_CACHE3.get(jwksUrl);
22290
+ if (cached && cached.expiresAtMs > now) {
22291
+ return cached.resolver;
22292
+ }
22150
22293
  if (cached) {
22151
- return cached;
22294
+ JWKS_CACHE3.delete(jwksUrl);
22152
22295
  }
22153
22296
  const jwks = createRemoteJWKSet2(new URL(jwksUrl));
22154
- JWKS_CACHE3.set(jwksUrl, jwks);
22297
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig2?.jwksCacheTtlMs;
22298
+ const ttlMs = resolveJwksCacheTtlMs2(configuredTtl);
22299
+ JWKS_CACHE3.set(jwksUrl, {
22300
+ resolver: jwks,
22301
+ expiresAtMs: now + ttlMs
22302
+ });
22155
22303
  return jwks;
22156
22304
  }
22305
+ function resolveJwksCacheTtlMs2(configuredTtl) {
22306
+ if (configuredTtl === undefined) {
22307
+ return DEFAULT_JWKS_CACHE_TTL_MS3;
22308
+ }
22309
+ if (!Number.isFinite(configuredTtl)) {
22310
+ return DEFAULT_JWKS_CACHE_TTL_MS3;
22311
+ }
22312
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS3, Math.floor(configuredTtl)));
22313
+ }
22157
22314
  function decodeJwtUnsafe3(token) {
22158
22315
  if (!token)
22159
22316
  return null;
@@ -22186,7 +22343,7 @@ async function verifyJwt2(token, config) {
22186
22343
  const key = new TextEncoder().encode(effectiveConfig.secret);
22187
22344
  ({ payload } = await jwtVerify2(token, key, options));
22188
22345
  } else {
22189
- ({ payload } = await jwtVerify2(token, getRemoteJwks2(effectiveConfig.jwksUrl), options));
22346
+ ({ payload } = await jwtVerify2(token, getRemoteJwks2(effectiveConfig.jwksUrl, effectiveConfig), options));
22190
22347
  }
22191
22348
  const claims = payload;
22192
22349
  validateClaims2(claims, effectiveConfig);
@@ -22629,6 +22786,39 @@ async function resolveAuthContext(bodyAuth, headerToken, headerIdentity) {
22629
22786
  }
22630
22787
  return bodyAuth;
22631
22788
  }
22789
+ function parseTimestampInput(value) {
22790
+ if (value === undefined || value === null) {
22791
+ return;
22792
+ }
22793
+ if (typeof value === "bigint") {
22794
+ return value >= 0n ? value : undefined;
22795
+ }
22796
+ if (typeof value === "number") {
22797
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
22798
+ return;
22799
+ }
22800
+ return BigInt(value);
22801
+ }
22802
+ if (typeof value === "string") {
22803
+ const trimmed = value.trim();
22804
+ if (!/^\d+$/.test(trimmed)) {
22805
+ return;
22806
+ }
22807
+ try {
22808
+ return BigInt(trimmed);
22809
+ } catch {
22810
+ return;
22811
+ }
22812
+ }
22813
+ return;
22814
+ }
22815
+ async function resolveSnapshotTimestamp(options, request) {
22816
+ const fromCallback = options.getSnapshotTimestamp ? await options.getSnapshotTimestamp(request) : undefined;
22817
+ if (typeof fromCallback === "bigint") {
22818
+ return fromCallback;
22819
+ }
22820
+ return BigInt(Date.now());
22821
+ }
22632
22822
  async function handleCoreHttpApiRequest(request, options) {
22633
22823
  const url = new URL(request.url);
22634
22824
  const segments = url.pathname.split("/").filter(Boolean);
@@ -22668,6 +22858,19 @@ async function handleCoreHttpApiRequest(request, options) {
22668
22858
  throw error;
22669
22859
  }
22670
22860
  const route = routeSegments[0];
22861
+ if (route === "query_ts") {
22862
+ if (request.method !== "POST") {
22863
+ return {
22864
+ handled: true,
22865
+ response: apply(Response.json({ error: "Method not allowed" }, { status: 405 }))
22866
+ };
22867
+ }
22868
+ const snapshotTimestamp = await resolveSnapshotTimestamp(options, request);
22869
+ return {
22870
+ handled: true,
22871
+ response: apply(Response.json({ ts: snapshotTimestamp.toString() }))
22872
+ };
22873
+ }
22671
22874
  if (route === "storage") {
22672
22875
  if (!options.storage) {
22673
22876
  return {
@@ -22725,7 +22928,7 @@ async function handleCoreHttpApiRequest(request, options) {
22725
22928
  }
22726
22929
  }
22727
22930
  }
22728
- if (route === "query" || route === "mutation" || route === "action") {
22931
+ if (route === "query" || route === "mutation" || route === "action" || route === "query_at_ts") {
22729
22932
  if (request.method !== "POST") {
22730
22933
  return {
22731
22934
  handled: true,
@@ -22748,7 +22951,7 @@ async function handleCoreHttpApiRequest(request, options) {
22748
22951
  response: apply(Response.json({ error: "Invalid request body" }, { status: 400 }))
22749
22952
  };
22750
22953
  }
22751
- const { path, args, format, auth: bodyAuth, componentPath } = body;
22954
+ const { path, args, format, auth: bodyAuth, componentPath, ts } = body;
22752
22955
  if (!path || typeof path !== "string") {
22753
22956
  return {
22754
22957
  handled: true,
@@ -22777,6 +22980,14 @@ async function handleCoreHttpApiRequest(request, options) {
22777
22980
  };
22778
22981
  }
22779
22982
  const jsonArgs = rawArgs ?? {};
22983
+ const executionType = route === "query_at_ts" ? "query" : route;
22984
+ const snapshotTimestamp = route === "query_at_ts" ? parseTimestampInput(ts) : undefined;
22985
+ if (route === "query_at_ts" && snapshotTimestamp === undefined) {
22986
+ return {
22987
+ handled: true,
22988
+ response: apply(Response.json({ error: "Invalid or missing ts" }, { status: 400 }))
22989
+ };
22990
+ }
22780
22991
  let authForExecution;
22781
22992
  try {
22782
22993
  authForExecution = await resolveAuthContext(bodyAuth, headerToken, headerIdentity);
@@ -22790,11 +23001,12 @@ async function handleCoreHttpApiRequest(request, options) {
22790
23001
  throw error;
22791
23002
  }
22792
23003
  const executionParams = {
22793
- type: route,
23004
+ type: executionType,
22794
23005
  path,
22795
23006
  args: jsonArgs,
22796
23007
  auth: authForExecution,
22797
23008
  componentPath,
23009
+ snapshotTimestamp,
22798
23010
  request
22799
23011
  };
22800
23012
  try {
@@ -22809,7 +23021,7 @@ async function handleCoreHttpApiRequest(request, options) {
22809
23021
  throw validationError;
22810
23022
  }
22811
23023
  const result = await options.executeFunction(executionParams);
22812
- if (options.notifyWrites && (route === "mutation" || route === "action") && (result.writtenRanges?.length || result.writtenTables?.length)) {
23024
+ if (options.notifyWrites && (executionType === "mutation" || executionType === "action") && (result.writtenRanges?.length || result.writtenTables?.length)) {
22813
23025
  await options.notifyWrites(result.writtenRanges, result.writtenTables ?? writtenTablesFromRanges2(result.writtenRanges));
22814
23026
  }
22815
23027
  return {
@@ -25883,7 +26095,7 @@ class ForbiddenInQueriesOrMutations extends Error {
25883
26095
  this.name = "ForbiddenInQueriesOrMutations";
25884
26096
  }
25885
26097
  }
25886
- async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, deterministicSeed, mutationTransaction, udfExecutor, componentPath) {
26098
+ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, deterministicSeed, mutationTransaction, udfExecutor, componentPath, snapshotOverride) {
25887
26099
  const ambientIdentity = getAuthContext();
25888
26100
  let effectiveAuth;
25889
26101
  if (auth && typeof auth === "object" && "tokenType" in auth) {
@@ -25898,7 +26110,7 @@ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, dete
25898
26110
  const inheritedSnapshot = snapshotContext3.getStore() ?? null;
25899
26111
  const existingIdGenerator = idGeneratorContext3.getStore() ?? undefined;
25900
26112
  const idGenerator = existingIdGenerator ?? (deterministicSeed ? createDeterministicIdGenerator(deterministicSeed) : undefined);
25901
- const convex = new UdfKernel(docstore, effectiveAuth, storage2, inheritedSnapshot, mutationTransaction, udfExecutor, componentPath, idGenerator);
26113
+ const convex = new UdfKernel(docstore, effectiveAuth, storage2, snapshotOverride ?? inheritedSnapshot, mutationTransaction, udfExecutor, componentPath, idGenerator);
25902
26114
  convex.clearAccessLogs();
25903
26115
  const logLines = [];
25904
26116
  const logger = (level) => {
@@ -25954,7 +26166,7 @@ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, dete
25954
26166
  };
25955
26167
  } finally {}
25956
26168
  }
25957
- function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath) {
26169
+ function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath, snapshotOverride) {
25958
26170
  const tnow = Date.now();
25959
26171
  const seed = resolveSeed("query", requestId, tnow);
25960
26172
  const rng = udfRng(seed);
@@ -25966,7 +26178,7 @@ function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, compo
25966
26178
  fetch: forbiddenAsyncOp("fetch"),
25967
26179
  setInterval: forbiddenAsyncOp("setInterval"),
25968
26180
  setTimeout: forbiddenAsyncOp("setTimeout")
25969
- }, auth, "query", storage2, seed, undefined, udfExecutor, componentPath);
26181
+ }, auth, "query", storage2, seed, undefined, udfExecutor, componentPath, snapshotOverride);
25970
26182
  }
25971
26183
  function runUdfMutation(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath) {
25972
26184
  const tnow = Date.now();
@@ -26091,7 +26303,7 @@ class InlineUdfExecutor {
26091
26303
  this.moduleRegistry = options.moduleRegistry;
26092
26304
  this.logSink = options.logSink;
26093
26305
  }
26094
- async execute(functionPath, args, udfType, auth, componentPath, requestId) {
26306
+ async execute(functionPath, args, udfType, auth, componentPath, requestId, snapshotTimestamp) {
26095
26307
  const [moduleName, functionName4] = this.parseUdfPath(functionPath);
26096
26308
  const finalRequestId = requestId ?? this.requestIdFactory(udfType, functionPath);
26097
26309
  const isSystemFunction = moduleName === "_system" || functionPath.startsWith("_system:");
@@ -26116,7 +26328,7 @@ class InlineUdfExecutor {
26116
26328
  const runWithType = () => {
26117
26329
  switch (udfType) {
26118
26330
  case "query":
26119
- return runUdfQuery(this.docstore, runUdf4, auth, this.blobstore, finalRequestId, this, componentPath);
26331
+ return runUdfQuery(this.docstore, runUdf4, auth, this.blobstore, finalRequestId, this, componentPath, snapshotTimestamp);
26120
26332
  case "mutation":
26121
26333
  return runUdfMutation(this.docstore, runUdf4, auth, this.blobstore, finalRequestId, this, componentPath);
26122
26334
  case "action":
@@ -26170,7 +26382,7 @@ class InlineUdfExecutor {
26170
26382
  async executeHttp(request, auth, requestId) {
26171
26383
  const url = new URL(request.url);
26172
26384
  const runHttpUdf = async () => {
26173
- const httpModule = await this.loadModule("http", request.url);
26385
+ const httpModule = await this.loadModule("http");
26174
26386
  const router = httpModule?.default;
26175
26387
  if (!router?.isRouter || typeof router.lookup !== "function") {
26176
26388
  throw new Error("convex/http.ts must export a default httpRouter()");
@@ -26304,7 +26516,7 @@ class UdfExecutionAdapter2 {
26304
26516
  this.executor = executor;
26305
26517
  this.callType = callType;
26306
26518
  }
26307
- async executeUdf(path, jsonArgs, type, auth, componentPath, requestId) {
26519
+ async executeUdf(path, jsonArgs, type, auth, componentPath, requestId, snapshotTimestamp) {
26308
26520
  const convexArgs = convertClientArgs2(jsonArgs);
26309
26521
  const target = normalizeExecutionTarget2(path, componentPath);
26310
26522
  let authContext3;
@@ -26342,7 +26554,7 @@ class UdfExecutionAdapter2 {
26342
26554
  return runWithAuth2(userIdentity, async () => {
26343
26555
  const executeWithContext = this.callType === "client" ? runAsClientCall2 : runAsServerCall3;
26344
26556
  return executeWithContext(async () => {
26345
- return await this.executor.execute(target.path, convexArgs, type, authContext3 ?? userIdentity, normalizeComponentPath6(target.componentPath), requestId);
26557
+ return await this.executor.execute(target.path, convexArgs, type, authContext3 ?? userIdentity, normalizeComponentPath6(target.componentPath), requestId, snapshotTimestamp);
26346
26558
  });
26347
26559
  });
26348
26560
  }
@@ -26391,7 +26603,7 @@ function stripApiVersionPrefix(pathname) {
26391
26603
  }
26392
26604
  function isReservedApiPath(pathname) {
26393
26605
  const normalizedPath = stripApiVersionPrefix(pathname);
26394
- if (normalizedPath === "/api/execute" || normalizedPath === "/api/sync" || normalizedPath === "/api/reset-test-state" || normalizedPath === "/api/query" || normalizedPath === "/api/mutation" || normalizedPath === "/api/action") {
26606
+ if (normalizedPath === "/api/execute" || normalizedPath === "/api/sync" || normalizedPath === "/api/reset-test-state" || normalizedPath === "/api/query" || normalizedPath === "/api/query_ts" || normalizedPath === "/api/query_at_ts" || normalizedPath === "/api/mutation" || normalizedPath === "/api/action") {
26395
26607
  return true;
26396
26608
  }
26397
26609
  if (normalizedPath === "/api/storage" || normalizedPath.startsWith("/api/storage/")) {
@@ -26462,7 +26674,7 @@ class HttpHandler {
26462
26674
  }
26463
26675
  };
26464
26676
  const coreResult = await handleCoreHttpApiRequest(request, {
26465
- executeFunction: async ({ type, path, args, auth, componentPath }) => this.adapter.executeUdf(path, args, type, auth, componentPath),
26677
+ executeFunction: async ({ type, path, args, auth, componentPath, snapshotTimestamp }) => this.adapter.executeUdf(path, args, type, auth, componentPath, undefined, snapshotTimestamp),
26466
26678
  notifyWrites,
26467
26679
  storage: this.docstore && this.blobstore ? {
26468
26680
  store: async (blob) => {
@@ -26481,7 +26693,16 @@ class HttpHandler {
26481
26693
  return { blob: blob ?? null };
26482
26694
  }
26483
26695
  } : undefined,
26484
- corsHeaders
26696
+ corsHeaders,
26697
+ getSnapshotTimestamp: () => {
26698
+ const oracle = this.docstore?.timestampOracle;
26699
+ const oracleTimestamp = typeof oracle?.beginSnapshot === "function" ? oracle.beginSnapshot() : typeof oracle?.getCurrentTimestamp === "function" ? oracle.getCurrentTimestamp() : undefined;
26700
+ const wallClock = BigInt(Date.now());
26701
+ if (typeof oracleTimestamp === "bigint" && oracleTimestamp > wallClock) {
26702
+ return oracleTimestamp;
26703
+ }
26704
+ return wallClock;
26705
+ }
26485
26706
  });
26486
26707
  if (coreResult?.handled) {
26487
26708
  return coreResult.response;
@@ -32654,6 +32875,8 @@ function decodeJwtClaimsToken4(token) {
32654
32875
  return null;
32655
32876
  }
32656
32877
  }
32878
+ var DEFAULT_JWKS_CACHE_TTL_MS4 = 5 * 60 * 1000;
32879
+ var MAX_JWKS_CACHE_TTL_MS4 = 24 * 60 * 60 * 1000;
32657
32880
  var JWKS_CACHE4 = new Map;
32658
32881
  function decodeJwtUnsafe4(token) {
32659
32882
  if (!token)
@@ -33471,6 +33694,8 @@ class ScheduledFunctionExecutor {
33471
33694
  logger;
33472
33695
  runMutationInTransaction;
33473
33696
  tableName;
33697
+ maxConcurrentJobs;
33698
+ scanPageSize;
33474
33699
  constructor(options) {
33475
33700
  this.docstore = options.docstore;
33476
33701
  this.udfExecutor = options.udfExecutor;
@@ -33480,46 +33705,68 @@ class ScheduledFunctionExecutor {
33480
33705
  this.logger = options.logger ?? console;
33481
33706
  this.runMutationInTransaction = options.runMutationInTransaction;
33482
33707
  this.tableName = options.tableName ?? SCHEDULED_FUNCTIONS_TABLE;
33708
+ this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
33709
+ this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
33483
33710
  }
33484
33711
  async runDueJobs() {
33485
33712
  const tableId = stringToHex4(this.tableName);
33486
- const allJobs = await this.docstore.scan(tableId);
33487
33713
  const now = this.now();
33488
- const pendingJobs = allJobs.filter((doc) => {
33489
- const value = doc.value?.value;
33490
- if (!value || typeof value !== "object") {
33491
- return false;
33714
+ let executed = 0;
33715
+ const inFlight = new Set;
33716
+ const schedule = async (jobValue) => {
33717
+ const run = this.executeJob(jobValue, tableId).then(() => {
33718
+ executed += 1;
33719
+ }).finally(() => {
33720
+ inFlight.delete(run);
33721
+ });
33722
+ inFlight.add(run);
33723
+ if (inFlight.size >= this.maxConcurrentJobs) {
33724
+ await Promise.race(inFlight);
33492
33725
  }
33493
- const state = value.state;
33494
- const scheduledTime = value.scheduledTime;
33495
- return state?.kind === "pending" && typeof scheduledTime === "number" && scheduledTime <= now;
33496
- });
33497
- if (pendingJobs.length === 0) {
33498
- return {
33499
- executed: 0,
33500
- nextScheduledTime: this.computeNextScheduledTime(allJobs)
33501
- };
33502
- }
33503
- await Promise.all(pendingJobs.map((job) => {
33504
- const jobValue = job.value?.value;
33505
- if (!jobValue) {
33506
- throw new Error("Job value unexpectedly missing after filter");
33507
- }
33508
- return this.executeJob(jobValue, tableId);
33509
- }));
33510
- return {
33511
- executed: pendingJobs.length,
33512
- nextScheduledTime: this.computeNextScheduledTime(await this.docstore.scan(tableId))
33513
33726
  };
33727
+ await this.forEachScheduledJob(async (jobValue) => {
33728
+ const state = jobValue.state;
33729
+ const scheduledTime = jobValue.scheduledTime;
33730
+ if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
33731
+ return;
33732
+ }
33733
+ if (scheduledTime <= now) {
33734
+ await schedule(jobValue);
33735
+ }
33736
+ });
33737
+ await Promise.all(inFlight);
33738
+ return { executed, nextScheduledTime: await this.getNextScheduledTime() };
33514
33739
  }
33515
33740
  async getNextScheduledTime() {
33516
- const tableId = stringToHex4(this.tableName);
33517
- const allJobs = await this.docstore.scan(tableId);
33518
- return this.computeNextScheduledTime(allJobs);
33741
+ let nextScheduledTime = null;
33742
+ await this.forEachScheduledJob(async (jobValue) => {
33743
+ const state = jobValue.state;
33744
+ const scheduledTime = jobValue.scheduledTime;
33745
+ if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
33746
+ return;
33747
+ }
33748
+ if (nextScheduledTime === null || scheduledTime < nextScheduledTime) {
33749
+ nextScheduledTime = scheduledTime;
33750
+ }
33751
+ });
33752
+ return nextScheduledTime;
33519
33753
  }
33520
- computeNextScheduledTime(allJobs) {
33521
- const pending = allJobs.map((doc) => doc.value?.value).filter((value) => !!value && value.state?.kind === "pending").map((value) => value.scheduledTime).filter((time) => typeof time === "number").sort((a, b) => a - b);
33522
- return pending.length > 0 ? pending[0] : null;
33754
+ async forEachScheduledJob(visitor) {
33755
+ const tableId = stringToHex4(this.tableName);
33756
+ let cursor2 = null;
33757
+ while (true) {
33758
+ const page = await this.docstore.scanPaginated(tableId, cursor2, this.scanPageSize, Order4.Asc);
33759
+ for (const doc of page.documents) {
33760
+ const value = doc.value?.value;
33761
+ if (value && typeof value === "object") {
33762
+ await visitor(value);
33763
+ }
33764
+ }
33765
+ if (!page.hasMore || !page.nextCursor) {
33766
+ break;
33767
+ }
33768
+ cursor2 = page.nextCursor;
33769
+ }
33523
33770
  }
33524
33771
  async executeJob(jobValue, tableId) {
33525
33772
  const jobId = jobValue?._id;
@@ -33651,6 +33898,8 @@ class CronExecutor {
33651
33898
  allocateTimestamp;
33652
33899
  now;
33653
33900
  logger;
33901
+ maxConcurrentJobs;
33902
+ scanPageSize;
33654
33903
  constructor(options) {
33655
33904
  this.docstore = options.docstore;
33656
33905
  this.udfExecutor = options.udfExecutor;
@@ -33658,6 +33907,8 @@ class CronExecutor {
33658
33907
  this.allocateTimestamp = options.allocateTimestamp;
33659
33908
  this.now = options.now ?? (() => Date.now());
33660
33909
  this.logger = options.logger ?? console;
33910
+ this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
33911
+ this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
33661
33912
  }
33662
33913
  async syncCronSpecs(cronSpecs) {
33663
33914
  const tableId = stringToHex4(CRONS_TABLE);
@@ -33743,33 +33994,58 @@ class CronExecutor {
33743
33994
  }
33744
33995
  async runDueJobs() {
33745
33996
  const tableId = stringToHex4(CRONS_TABLE);
33746
- const allJobs = await this.docstore.scan(tableId);
33747
33997
  const now = this.now();
33748
- const dueJobs = allJobs.filter((doc) => {
33749
- const value = doc.value?.value;
33750
- return value && value.nextRun <= now;
33751
- });
33752
- if (dueJobs.length === 0) {
33753
- return {
33754
- executed: 0,
33755
- nextScheduledTime: this.computeNextScheduledTime(allJobs)
33756
- };
33757
- }
33758
- await Promise.all(dueJobs.map((job) => this.executeJob(job, tableId)));
33759
- const updatedJobs = await this.docstore.scan(tableId);
33760
- return {
33761
- executed: dueJobs.length,
33762
- nextScheduledTime: this.computeNextScheduledTime(updatedJobs)
33998
+ let executed = 0;
33999
+ const inFlight = new Set;
34000
+ const schedule = async (job) => {
34001
+ const run = this.executeJob(job, tableId).then(() => {
34002
+ executed += 1;
34003
+ }).finally(() => {
34004
+ inFlight.delete(run);
34005
+ });
34006
+ inFlight.add(run);
34007
+ if (inFlight.size >= this.maxConcurrentJobs) {
34008
+ await Promise.race(inFlight);
34009
+ }
33763
34010
  };
34011
+ await this.forEachCronJob(async (job) => {
34012
+ const value = job.value?.value;
34013
+ if (!value || typeof value.nextRun !== "number") {
34014
+ return;
34015
+ }
34016
+ if (value.nextRun <= now) {
34017
+ await schedule(job);
34018
+ }
34019
+ });
34020
+ await Promise.all(inFlight);
34021
+ return { executed, nextScheduledTime: await this.getNextScheduledTime() };
33764
34022
  }
33765
34023
  async getNextScheduledTime() {
33766
- const tableId = stringToHex4(CRONS_TABLE);
33767
- const allJobs = await this.docstore.scan(tableId);
33768
- return this.computeNextScheduledTime(allJobs);
34024
+ let nextScheduledTime = null;
34025
+ await this.forEachCronJob(async (job) => {
34026
+ const nextRun = job.value?.value?.nextRun;
34027
+ if (typeof nextRun !== "number") {
34028
+ return;
34029
+ }
34030
+ if (nextScheduledTime === null || nextRun < nextScheduledTime) {
34031
+ nextScheduledTime = nextRun;
34032
+ }
34033
+ });
34034
+ return nextScheduledTime;
33769
34035
  }
33770
- computeNextScheduledTime(allJobs) {
33771
- const nextTimes = allJobs.map((doc) => doc.value?.value?.nextRun).filter((time) => typeof time === "number").sort((a, b) => a - b);
33772
- return nextTimes.length > 0 ? nextTimes[0] : null;
34036
+ async forEachCronJob(visitor) {
34037
+ const tableId = stringToHex4(CRONS_TABLE);
34038
+ let cursor2 = null;
34039
+ while (true) {
34040
+ const page = await this.docstore.scanPaginated(tableId, cursor2, this.scanPageSize, Order4.Asc);
34041
+ for (const job of page.documents) {
34042
+ await visitor(job);
34043
+ }
34044
+ if (!page.hasMore || !page.nextCursor) {
34045
+ break;
34046
+ }
34047
+ cursor2 = page.nextCursor;
34048
+ }
33773
34049
  }
33774
34050
  async executeJob(job, tableId) {
33775
34051
  const value = job.value?.value;
@@ -41377,6 +41653,8 @@ class SystemAuthError3 extends Error {
41377
41653
  }
41378
41654
  }
41379
41655
  var DEFAULT_CLOCK_TOLERANCE_SECONDS3 = 60;
41656
+ var DEFAULT_JWKS_CACHE_TTL_MS5 = 5 * 60 * 1000;
41657
+ var MAX_JWKS_CACHE_TTL_MS5 = 24 * 60 * 60 * 1000;
41380
41658
  var JWKS_CACHE5 = new Map;
41381
41659
  var defaultValidationConfig3;
41382
41660
  var adminAuthConfig3;
@@ -41472,7 +41750,8 @@ function resolveJwtValidationConfigFromEnv3(env) {
41472
41750
  const secret = getEnvValue3("AUTH_SECRET", env) ?? getEnvValue3("CONCAVE_JWT_SECRET", env) ?? getEnvValue3("JWT_SECRET", env);
41473
41751
  const skipVerification = parseBoolean3(getEnvValue3("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean3(getEnvValue3("CONCAVE_JWT_SKIP_VERIFICATION", env));
41474
41752
  const clockTolerance = parseNumber3(getEnvValue3("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber3(getEnvValue3("CONCAVE_JWT_CLOCK_TOLERANCE", env));
41475
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
41753
+ const jwksCacheTtlMs = parseNumber3(getEnvValue3("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber3(getEnvValue3("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
41754
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
41476
41755
  return;
41477
41756
  }
41478
41757
  return {
@@ -41481,7 +41760,8 @@ function resolveJwtValidationConfigFromEnv3(env) {
41481
41760
  audience,
41482
41761
  secret,
41483
41762
  skipVerification,
41484
- clockTolerance
41763
+ clockTolerance,
41764
+ jwksCacheTtlMs
41485
41765
  };
41486
41766
  }
41487
41767
  function normalizeList3(value) {
@@ -41525,15 +41805,33 @@ function validateClaims3(claims, config) {
41525
41805
  throw new JWTValidationError3("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
41526
41806
  }
41527
41807
  }
41528
- function getRemoteJwks3(jwksUrl) {
41808
+ function getRemoteJwks3(jwksUrl, config) {
41809
+ const now = Date.now();
41529
41810
  const cached = JWKS_CACHE5.get(jwksUrl);
41811
+ if (cached && cached.expiresAtMs > now) {
41812
+ return cached.resolver;
41813
+ }
41530
41814
  if (cached) {
41531
- return cached;
41815
+ JWKS_CACHE5.delete(jwksUrl);
41532
41816
  }
41533
41817
  const jwks = createRemoteJWKSet3(new URL(jwksUrl));
41534
- JWKS_CACHE5.set(jwksUrl, jwks);
41818
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig3?.jwksCacheTtlMs;
41819
+ const ttlMs = resolveJwksCacheTtlMs3(configuredTtl);
41820
+ JWKS_CACHE5.set(jwksUrl, {
41821
+ resolver: jwks,
41822
+ expiresAtMs: now + ttlMs
41823
+ });
41535
41824
  return jwks;
41536
41825
  }
41826
+ function resolveJwksCacheTtlMs3(configuredTtl) {
41827
+ if (configuredTtl === undefined) {
41828
+ return DEFAULT_JWKS_CACHE_TTL_MS5;
41829
+ }
41830
+ if (!Number.isFinite(configuredTtl)) {
41831
+ return DEFAULT_JWKS_CACHE_TTL_MS5;
41832
+ }
41833
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS5, Math.floor(configuredTtl)));
41834
+ }
41537
41835
  function decodeJwtUnsafe5(token) {
41538
41836
  if (!token)
41539
41837
  return null;
@@ -41566,7 +41864,7 @@ async function verifyJwt3(token, config) {
41566
41864
  const key = new TextEncoder().encode(effectiveConfig.secret);
41567
41865
  ({ payload } = await jwtVerify3(token, key, options));
41568
41866
  } else {
41569
- ({ payload } = await jwtVerify3(token, getRemoteJwks3(effectiveConfig.jwksUrl), options));
41867
+ ({ payload } = await jwtVerify3(token, getRemoteJwks3(effectiveConfig.jwksUrl, effectiveConfig), options));
41570
41868
  }
41571
41869
  const claims = payload;
41572
41870
  validateClaims3(claims, effectiveConfig);
@@ -42709,6 +43007,11 @@ var WEBSOCKET_READY_STATE_OPEN2 = 1;
42709
43007
  var BACKPRESSURE_HIGH_WATER_MARK2 = 100;
42710
43008
  var BACKPRESSURE_BUFFER_LIMIT2 = 1024 * 1024;
42711
43009
  var SLOW_CLIENT_TIMEOUT_MS2 = 30000;
43010
+ var DEFAULT_RATE_LIMIT_WINDOW_MS2 = 5000;
43011
+ var DEFAULT_MAX_MESSAGES_PER_WINDOW2 = 1000;
43012
+ var DEFAULT_OPERATION_TIMEOUT_MS2 = 15000;
43013
+ var DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION2 = 1000;
43014
+ var RATE_LIMIT_HARD_MULTIPLIER2 = 5;
42712
43015
 
42713
43016
  class SyncSession2 {
42714
43017
  websocket;
@@ -42737,15 +43040,25 @@ class SyncSession2 {
42737
43040
  class SyncProtocolHandler2 {
42738
43041
  udfExecutor;
42739
43042
  sessions = new Map;
43043
+ rateLimitStates = new Map;
42740
43044
  subscriptionManager;
42741
43045
  instanceName;
42742
43046
  backpressureController;
42743
43047
  heartbeatController;
42744
43048
  isDev;
43049
+ maxMessagesPerWindow;
43050
+ rateLimitWindowMs;
43051
+ operationTimeoutMs;
43052
+ maxActiveQueriesPerSession;
42745
43053
  constructor(instanceName, udfExecutor, options) {
42746
43054
  this.udfExecutor = udfExecutor;
42747
43055
  this.instanceName = instanceName;
42748
43056
  this.isDev = options?.isDev ?? true;
43057
+ this.maxMessagesPerWindow = Math.max(1, options?.maxMessagesPerWindow ?? DEFAULT_MAX_MESSAGES_PER_WINDOW2);
43058
+ this.rateLimitWindowMs = Math.max(1, options?.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS2);
43059
+ const configuredOperationTimeout = options?.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS2;
43060
+ this.operationTimeoutMs = Number.isFinite(configuredOperationTimeout) ? Math.max(0, Math.floor(configuredOperationTimeout)) : DEFAULT_OPERATION_TIMEOUT_MS2;
43061
+ this.maxActiveQueriesPerSession = Math.max(1, options?.maxActiveQueriesPerSession ?? DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION2);
42749
43062
  this.subscriptionManager = new SubscriptionManager3;
42750
43063
  this.backpressureController = new SessionBackpressureController2({
42751
43064
  websocketReadyStateOpen: WEBSOCKET_READY_STATE_OPEN2,
@@ -42763,6 +43076,10 @@ class SyncProtocolHandler2 {
42763
43076
  createSession(sessionId, websocket) {
42764
43077
  const session = new SyncSession2(websocket);
42765
43078
  this.sessions.set(sessionId, session);
43079
+ this.rateLimitStates.set(sessionId, {
43080
+ windowStartedAt: Date.now(),
43081
+ messagesInWindow: 0
43082
+ });
42766
43083
  return session;
42767
43084
  }
42768
43085
  getSession(sessionId) {
@@ -42777,6 +43094,7 @@ class SyncProtocolHandler2 {
42777
43094
  session.isDraining = false;
42778
43095
  this.subscriptionManager.unsubscribeAll(sessionId);
42779
43096
  this.sessions.delete(sessionId);
43097
+ this.rateLimitStates.delete(sessionId);
42780
43098
  }
42781
43099
  }
42782
43100
  updateSessionId(oldSessionId, newSessionId) {
@@ -42784,6 +43102,11 @@ class SyncProtocolHandler2 {
42784
43102
  if (session) {
42785
43103
  this.sessions.delete(oldSessionId);
42786
43104
  this.sessions.set(newSessionId, session);
43105
+ const rateLimitState = this.rateLimitStates.get(oldSessionId);
43106
+ if (rateLimitState) {
43107
+ this.rateLimitStates.delete(oldSessionId);
43108
+ this.rateLimitStates.set(newSessionId, rateLimitState);
43109
+ }
42787
43110
  this.subscriptionManager.updateSessionId(oldSessionId, newSessionId);
42788
43111
  }
42789
43112
  }
@@ -42792,6 +43115,29 @@ class SyncProtocolHandler2 {
42792
43115
  if (!session && message22.type !== "Connect") {
42793
43116
  throw new Error("Session not found");
42794
43117
  }
43118
+ if (session) {
43119
+ const rateLimitDecision = this.consumeRateLimit(sessionId);
43120
+ if (rateLimitDecision === "reject") {
43121
+ return [
43122
+ {
43123
+ type: "FatalError",
43124
+ error: "Rate limit exceeded, retry shortly"
43125
+ }
43126
+ ];
43127
+ }
43128
+ if (rateLimitDecision === "close") {
43129
+ try {
43130
+ session.websocket.close(1013, "Rate limit exceeded");
43131
+ } catch {}
43132
+ this.destroySession(sessionId);
43133
+ return [
43134
+ {
43135
+ type: "FatalError",
43136
+ error: "Rate limit exceeded"
43137
+ }
43138
+ ];
43139
+ }
43140
+ }
42795
43141
  switch (message22.type) {
42796
43142
  case "Connect":
42797
43143
  return this.handleConnect(sessionId, message22);
@@ -42848,6 +43194,15 @@ class SyncProtocolHandler2 {
42848
43194
  return [fatalError];
42849
43195
  }
42850
43196
  const startVersion = makeStateVersion2(session.querySetVersion, session.identityVersion, session.timestamp);
43197
+ const projectedActiveQueryCount = this.computeProjectedActiveQueryCount(session, message22);
43198
+ if (projectedActiveQueryCount > this.maxActiveQueriesPerSession) {
43199
+ return [
43200
+ {
43201
+ type: "FatalError",
43202
+ error: `Too many active queries: ${projectedActiveQueryCount} exceeds limit ${this.maxActiveQueriesPerSession}`
43203
+ }
43204
+ ];
43205
+ }
42851
43206
  session.querySetVersion = message22.newVersion;
42852
43207
  const modifications = [];
42853
43208
  for (const mod of message22.modifications) {
@@ -43171,7 +43526,54 @@ class SyncProtocolHandler2 {
43171
43526
  }, () => {
43172
43527
  return;
43173
43528
  });
43174
- return run;
43529
+ return this.withOperationTimeout(run);
43530
+ }
43531
+ withOperationTimeout(promise) {
43532
+ if (this.operationTimeoutMs <= 0) {
43533
+ return promise;
43534
+ }
43535
+ let timeoutHandle;
43536
+ const timeoutPromise = new Promise((_, reject) => {
43537
+ timeoutHandle = setTimeout(() => {
43538
+ reject(new Error(`Sync operation timed out after ${this.operationTimeoutMs}ms`));
43539
+ }, this.operationTimeoutMs);
43540
+ });
43541
+ return Promise.race([promise, timeoutPromise]).finally(() => {
43542
+ if (timeoutHandle) {
43543
+ clearTimeout(timeoutHandle);
43544
+ }
43545
+ });
43546
+ }
43547
+ consumeRateLimit(sessionId) {
43548
+ const state = this.rateLimitStates.get(sessionId);
43549
+ if (!state) {
43550
+ return "allow";
43551
+ }
43552
+ const now = Date.now();
43553
+ if (now - state.windowStartedAt >= this.rateLimitWindowMs) {
43554
+ state.windowStartedAt = now;
43555
+ state.messagesInWindow = 0;
43556
+ }
43557
+ state.messagesInWindow += 1;
43558
+ if (state.messagesInWindow <= this.maxMessagesPerWindow) {
43559
+ return "allow";
43560
+ }
43561
+ const hardLimit = Math.max(this.maxMessagesPerWindow + 1, this.maxMessagesPerWindow * RATE_LIMIT_HARD_MULTIPLIER2);
43562
+ if (state.messagesInWindow >= hardLimit) {
43563
+ return "close";
43564
+ }
43565
+ return "reject";
43566
+ }
43567
+ computeProjectedActiveQueryCount(session, message22) {
43568
+ const projected = new Set(session.activeQueries.keys());
43569
+ for (const mod of message22.modifications) {
43570
+ if (mod.type === "Add") {
43571
+ projected.add(mod.queryId);
43572
+ } else if (mod.type === "Remove") {
43573
+ projected.delete(mod.queryId);
43574
+ }
43575
+ }
43576
+ return projected.size;
43175
43577
  }
43176
43578
  sendPing(session) {
43177
43579
  if (!session.websocket || session.websocket.readyState !== WEBSOCKET_READY_STATE_OPEN2) {