@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.
@@ -12456,6 +12456,30 @@ class BaseSqliteDocStore {
12456
12456
  return scored.slice(0, limit);
12457
12457
  }
12458
12458
  }
12459
+ function createSerializedTransactionRunner(hooks) {
12460
+ let queue = Promise.resolve();
12461
+ return async function runInTransaction(fn) {
12462
+ const run = queue.then(async () => {
12463
+ await hooks.begin();
12464
+ try {
12465
+ const result = await fn();
12466
+ await hooks.commit();
12467
+ return result;
12468
+ } catch (error) {
12469
+ try {
12470
+ await hooks.rollback();
12471
+ } catch {}
12472
+ throw error;
12473
+ }
12474
+ });
12475
+ queue = run.then(() => {
12476
+ return;
12477
+ }, () => {
12478
+ return;
12479
+ });
12480
+ return run;
12481
+ };
12482
+ }
12459
12483
 
12460
12484
  class Long22 {
12461
12485
  low;
@@ -12608,8 +12632,14 @@ class NodePreparedStatement {
12608
12632
 
12609
12633
  class NodeSqliteAdapter {
12610
12634
  db;
12635
+ runSerializedTransaction;
12611
12636
  constructor(db) {
12612
12637
  this.db = db;
12638
+ this.runSerializedTransaction = createSerializedTransactionRunner({
12639
+ begin: () => this.db.exec("BEGIN TRANSACTION"),
12640
+ commit: () => this.db.exec("COMMIT"),
12641
+ rollback: () => this.db.exec("ROLLBACK")
12642
+ });
12613
12643
  }
12614
12644
  exec(sql) {
12615
12645
  this.db.exec(sql);
@@ -12618,15 +12648,7 @@ class NodeSqliteAdapter {
12618
12648
  return new NodePreparedStatement(this.db.prepare(sql));
12619
12649
  }
12620
12650
  async transaction(fn) {
12621
- try {
12622
- this.db.exec("BEGIN TRANSACTION");
12623
- const result = await fn();
12624
- this.db.exec("COMMIT");
12625
- return result;
12626
- } catch (error) {
12627
- this.db.exec("ROLLBACK");
12628
- throw error;
12629
- }
12651
+ return this.runSerializedTransaction(fn);
12630
12652
  }
12631
12653
  hexToBuffer(hex) {
12632
12654
  return Buffer.from(hexToArrayBuffer32(hex));
@@ -12730,7 +12752,7 @@ var __defProp26, __export5 = (target, all) => {
12730
12752
  } catch {
12731
12753
  return;
12732
12754
  }
12733
- }, AsyncLocalStorageCtor5, snapshotContext5, transactionContext5, idGeneratorContext5, CALL_CONTEXT_SYMBOL5, globalCallContext5, callContext5, JWKS_CACHE5, debug5 = () => {}, Convex24, UZERO22, TWO_PWR_16_DBL22, TWO_PWR_32_DBL22, TWO_PWR_64_DBL22, MAX_UNSIGNED_VALUE22, SqliteDocStore;
12755
+ }, AsyncLocalStorageCtor5, snapshotContext5, transactionContext5, idGeneratorContext5, CALL_CONTEXT_SYMBOL5, globalCallContext5, callContext5, DEFAULT_JWKS_CACHE_TTL_MS5, MAX_JWKS_CACHE_TTL_MS5, JWKS_CACHE5, debug5 = () => {}, Convex24, UZERO22, TWO_PWR_16_DBL22, TWO_PWR_32_DBL22, TWO_PWR_64_DBL22, MAX_UNSIGNED_VALUE22, SqliteDocStore;
12734
12756
  var init_dist = __esm(() => {
12735
12757
  __defProp26 = Object.defineProperty;
12736
12758
  init_base645 = __esm5(() => {
@@ -13828,6 +13850,8 @@ var init_dist = __esm(() => {
13828
13850
  init_values5();
13829
13851
  init_index_manager5();
13830
13852
  init_interface6();
13853
+ DEFAULT_JWKS_CACHE_TTL_MS5 = 5 * 60 * 1000;
13854
+ MAX_JWKS_CACHE_TTL_MS5 = 24 * 60 * 60 * 1000;
13831
13855
  JWKS_CACHE5 = new Map;
13832
13856
  init_values5();
13833
13857
  init_schema_service5();
@@ -13883,6 +13907,9 @@ class FsBlobStore {
13883
13907
  return false;
13884
13908
  }
13885
13909
  }
13910
+ isNotFoundError(error) {
13911
+ return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
13912
+ }
13886
13913
  generateStorageId() {
13887
13914
  return crypto.randomUUID();
13888
13915
  }
@@ -13923,31 +13950,25 @@ class FsBlobStore {
13923
13950
  async get(storageId) {
13924
13951
  const objectPath = this.getObjectPath(storageId);
13925
13952
  try {
13926
- const fileExists = await this.pathExists(objectPath);
13927
- if (!fileExists) {
13928
- return null;
13929
- }
13930
13953
  const data = await readFile(objectPath);
13931
13954
  const metadata = await this.getMetadata(storageId);
13932
13955
  const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
13933
13956
  return new Blob([arrayBuffer], { type: metadata?.contentType || "application/octet-stream" });
13934
13957
  } catch (error) {
13958
+ if (this.isNotFoundError(error)) {
13959
+ return null;
13960
+ }
13935
13961
  console.error("Error reading object from filesystem:", error);
13936
- return null;
13962
+ throw error;
13937
13963
  }
13938
13964
  }
13939
13965
  async delete(storageId) {
13940
13966
  const objectPath = this.getObjectPath(storageId);
13941
13967
  const metadataPath = this.getMetadataPath(storageId);
13942
- try {
13943
- await Promise.all([
13944
- unlink(objectPath).catch(() => {}),
13945
- unlink(metadataPath).catch(() => {})
13946
- ]);
13947
- } catch (error) {
13948
- console.error("Error deleting object from filesystem:", error);
13949
- throw error;
13950
- }
13968
+ await Promise.all([
13969
+ this.unlinkIfExists(objectPath),
13970
+ this.unlinkIfExists(metadataPath)
13971
+ ]);
13951
13972
  }
13952
13973
  async getUrl(storageId) {
13953
13974
  const objectPath = this.getObjectPath(storageId);
@@ -13961,15 +13982,25 @@ class FsBlobStore {
13961
13982
  async getMetadata(storageId) {
13962
13983
  const metadataPath = this.getMetadataPath(storageId);
13963
13984
  try {
13964
- const fileExists = await this.pathExists(metadataPath);
13965
- if (!fileExists) {
13966
- return null;
13967
- }
13968
13985
  const data = await readFile(metadataPath, "utf-8");
13969
13986
  return JSON.parse(data);
13970
13987
  } catch (error) {
13988
+ if (this.isNotFoundError(error)) {
13989
+ return null;
13990
+ }
13971
13991
  console.error("Error reading metadata from filesystem:", error);
13972
- return null;
13992
+ throw error;
13993
+ }
13994
+ }
13995
+ async unlinkIfExists(path2) {
13996
+ try {
13997
+ await unlink(path2);
13998
+ } catch (error) {
13999
+ if (this.isNotFoundError(error)) {
14000
+ return;
14001
+ }
14002
+ console.error("Error deleting object from filesystem:", error);
14003
+ throw error;
13973
14004
  }
13974
14005
  }
13975
14006
  }
@@ -19853,6 +19884,8 @@ class SystemAuthError extends Error {
19853
19884
  }
19854
19885
  }
19855
19886
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 60;
19887
+ var DEFAULT_JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
19888
+ var MAX_JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
19856
19889
  var JWKS_CACHE = new Map;
19857
19890
  var defaultValidationConfig;
19858
19891
  var adminAuthConfig;
@@ -19948,7 +19981,8 @@ function resolveJwtValidationConfigFromEnv(env) {
19948
19981
  const secret = getEnvValue("AUTH_SECRET", env) ?? getEnvValue("CONCAVE_JWT_SECRET", env) ?? getEnvValue("JWT_SECRET", env);
19949
19982
  const skipVerification = parseBoolean(getEnvValue("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean(getEnvValue("CONCAVE_JWT_SKIP_VERIFICATION", env));
19950
19983
  const clockTolerance = parseNumber(getEnvValue("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber(getEnvValue("CONCAVE_JWT_CLOCK_TOLERANCE", env));
19951
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
19984
+ const jwksCacheTtlMs = parseNumber(getEnvValue("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber(getEnvValue("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
19985
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
19952
19986
  return;
19953
19987
  }
19954
19988
  return {
@@ -19957,7 +19991,8 @@ function resolveJwtValidationConfigFromEnv(env) {
19957
19991
  audience,
19958
19992
  secret,
19959
19993
  skipVerification,
19960
- clockTolerance
19994
+ clockTolerance,
19995
+ jwksCacheTtlMs
19961
19996
  };
19962
19997
  }
19963
19998
  function normalizeList(value) {
@@ -20001,15 +20036,33 @@ function validateClaims(claims, config) {
20001
20036
  throw new JWTValidationError("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
20002
20037
  }
20003
20038
  }
20004
- function getRemoteJwks(jwksUrl) {
20039
+ function getRemoteJwks(jwksUrl, config) {
20040
+ const now = Date.now();
20005
20041
  const cached = JWKS_CACHE.get(jwksUrl);
20042
+ if (cached && cached.expiresAtMs > now) {
20043
+ return cached.resolver;
20044
+ }
20006
20045
  if (cached) {
20007
- return cached;
20046
+ JWKS_CACHE.delete(jwksUrl);
20008
20047
  }
20009
20048
  const jwks = createRemoteJWKSet(new URL(jwksUrl));
20010
- JWKS_CACHE.set(jwksUrl, jwks);
20049
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig?.jwksCacheTtlMs;
20050
+ const ttlMs = resolveJwksCacheTtlMs(configuredTtl);
20051
+ JWKS_CACHE.set(jwksUrl, {
20052
+ resolver: jwks,
20053
+ expiresAtMs: now + ttlMs
20054
+ });
20011
20055
  return jwks;
20012
20056
  }
20057
+ function resolveJwksCacheTtlMs(configuredTtl) {
20058
+ if (configuredTtl === undefined) {
20059
+ return DEFAULT_JWKS_CACHE_TTL_MS;
20060
+ }
20061
+ if (!Number.isFinite(configuredTtl)) {
20062
+ return DEFAULT_JWKS_CACHE_TTL_MS;
20063
+ }
20064
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS, Math.floor(configuredTtl)));
20065
+ }
20013
20066
  function decodeJwtUnsafe(token) {
20014
20067
  if (!token)
20015
20068
  return null;
@@ -20042,7 +20095,7 @@ async function verifyJwt(token, config) {
20042
20095
  const key = new TextEncoder().encode(effectiveConfig.secret);
20043
20096
  ({ payload } = await jwtVerify(token, key, options));
20044
20097
  } else {
20045
- ({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl), options));
20098
+ ({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl, effectiveConfig), options));
20046
20099
  }
20047
20100
  const claims = payload;
20048
20101
  validateClaims(claims, effectiveConfig);
@@ -20097,7 +20150,7 @@ class UdfExecutionAdapter {
20097
20150
  this.executor = executor;
20098
20151
  this.callType = callType;
20099
20152
  }
20100
- async executeUdf(path, jsonArgs, type, auth, componentPath, requestId) {
20153
+ async executeUdf(path, jsonArgs, type, auth, componentPath, requestId, snapshotTimestamp) {
20101
20154
  const convexArgs = convertClientArgs(jsonArgs);
20102
20155
  const target = normalizeExecutionTarget(path, componentPath);
20103
20156
  let authContext2;
@@ -20135,7 +20188,7 @@ class UdfExecutionAdapter {
20135
20188
  return runWithAuth(userIdentity, async () => {
20136
20189
  const executeWithContext = this.callType === "client" ? runAsClientCall : runAsServerCall;
20137
20190
  return executeWithContext(async () => {
20138
- return await this.executor.execute(target.path, convexArgs, type, authContext2 ?? userIdentity, normalizeComponentPath(target.componentPath), requestId);
20191
+ return await this.executor.execute(target.path, convexArgs, type, authContext2 ?? userIdentity, normalizeComponentPath(target.componentPath), requestId, snapshotTimestamp);
20139
20192
  });
20140
20193
  });
20141
20194
  }
@@ -22409,6 +22462,11 @@ var WEBSOCKET_READY_STATE_OPEN = 1;
22409
22462
  var BACKPRESSURE_HIGH_WATER_MARK = 100;
22410
22463
  var BACKPRESSURE_BUFFER_LIMIT = 1024 * 1024;
22411
22464
  var SLOW_CLIENT_TIMEOUT_MS = 30000;
22465
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 5000;
22466
+ var DEFAULT_MAX_MESSAGES_PER_WINDOW = 1000;
22467
+ var DEFAULT_OPERATION_TIMEOUT_MS = 15000;
22468
+ var DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION = 1000;
22469
+ var RATE_LIMIT_HARD_MULTIPLIER = 5;
22412
22470
 
22413
22471
  class SyncSession {
22414
22472
  websocket;
@@ -22437,15 +22495,25 @@ class SyncSession {
22437
22495
  class SyncProtocolHandler {
22438
22496
  udfExecutor;
22439
22497
  sessions = new Map;
22498
+ rateLimitStates = new Map;
22440
22499
  subscriptionManager;
22441
22500
  instanceName;
22442
22501
  backpressureController;
22443
22502
  heartbeatController;
22444
22503
  isDev;
22504
+ maxMessagesPerWindow;
22505
+ rateLimitWindowMs;
22506
+ operationTimeoutMs;
22507
+ maxActiveQueriesPerSession;
22445
22508
  constructor(instanceName, udfExecutor, options) {
22446
22509
  this.udfExecutor = udfExecutor;
22447
22510
  this.instanceName = instanceName;
22448
22511
  this.isDev = options?.isDev ?? true;
22512
+ this.maxMessagesPerWindow = Math.max(1, options?.maxMessagesPerWindow ?? DEFAULT_MAX_MESSAGES_PER_WINDOW);
22513
+ this.rateLimitWindowMs = Math.max(1, options?.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS);
22514
+ const configuredOperationTimeout = options?.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
22515
+ this.operationTimeoutMs = Number.isFinite(configuredOperationTimeout) ? Math.max(0, Math.floor(configuredOperationTimeout)) : DEFAULT_OPERATION_TIMEOUT_MS;
22516
+ this.maxActiveQueriesPerSession = Math.max(1, options?.maxActiveQueriesPerSession ?? DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION);
22449
22517
  this.subscriptionManager = new SubscriptionManager;
22450
22518
  this.backpressureController = new SessionBackpressureController({
22451
22519
  websocketReadyStateOpen: WEBSOCKET_READY_STATE_OPEN,
@@ -22463,6 +22531,10 @@ class SyncProtocolHandler {
22463
22531
  createSession(sessionId, websocket) {
22464
22532
  const session = new SyncSession(websocket);
22465
22533
  this.sessions.set(sessionId, session);
22534
+ this.rateLimitStates.set(sessionId, {
22535
+ windowStartedAt: Date.now(),
22536
+ messagesInWindow: 0
22537
+ });
22466
22538
  return session;
22467
22539
  }
22468
22540
  getSession(sessionId) {
@@ -22477,6 +22549,7 @@ class SyncProtocolHandler {
22477
22549
  session.isDraining = false;
22478
22550
  this.subscriptionManager.unsubscribeAll(sessionId);
22479
22551
  this.sessions.delete(sessionId);
22552
+ this.rateLimitStates.delete(sessionId);
22480
22553
  }
22481
22554
  }
22482
22555
  updateSessionId(oldSessionId, newSessionId) {
@@ -22484,6 +22557,11 @@ class SyncProtocolHandler {
22484
22557
  if (session) {
22485
22558
  this.sessions.delete(oldSessionId);
22486
22559
  this.sessions.set(newSessionId, session);
22560
+ const rateLimitState = this.rateLimitStates.get(oldSessionId);
22561
+ if (rateLimitState) {
22562
+ this.rateLimitStates.delete(oldSessionId);
22563
+ this.rateLimitStates.set(newSessionId, rateLimitState);
22564
+ }
22487
22565
  this.subscriptionManager.updateSessionId(oldSessionId, newSessionId);
22488
22566
  }
22489
22567
  }
@@ -22492,6 +22570,29 @@ class SyncProtocolHandler {
22492
22570
  if (!session && message2.type !== "Connect") {
22493
22571
  throw new Error("Session not found");
22494
22572
  }
22573
+ if (session) {
22574
+ const rateLimitDecision = this.consumeRateLimit(sessionId);
22575
+ if (rateLimitDecision === "reject") {
22576
+ return [
22577
+ {
22578
+ type: "FatalError",
22579
+ error: "Rate limit exceeded, retry shortly"
22580
+ }
22581
+ ];
22582
+ }
22583
+ if (rateLimitDecision === "close") {
22584
+ try {
22585
+ session.websocket.close(1013, "Rate limit exceeded");
22586
+ } catch {}
22587
+ this.destroySession(sessionId);
22588
+ return [
22589
+ {
22590
+ type: "FatalError",
22591
+ error: "Rate limit exceeded"
22592
+ }
22593
+ ];
22594
+ }
22595
+ }
22495
22596
  switch (message2.type) {
22496
22597
  case "Connect":
22497
22598
  return this.handleConnect(sessionId, message2);
@@ -22548,6 +22649,15 @@ class SyncProtocolHandler {
22548
22649
  return [fatalError];
22549
22650
  }
22550
22651
  const startVersion = makeStateVersion(session.querySetVersion, session.identityVersion, session.timestamp);
22652
+ const projectedActiveQueryCount = this.computeProjectedActiveQueryCount(session, message2);
22653
+ if (projectedActiveQueryCount > this.maxActiveQueriesPerSession) {
22654
+ return [
22655
+ {
22656
+ type: "FatalError",
22657
+ error: `Too many active queries: ${projectedActiveQueryCount} exceeds limit ${this.maxActiveQueriesPerSession}`
22658
+ }
22659
+ ];
22660
+ }
22551
22661
  session.querySetVersion = message2.newVersion;
22552
22662
  const modifications = [];
22553
22663
  for (const mod of message2.modifications) {
@@ -22871,7 +22981,54 @@ class SyncProtocolHandler {
22871
22981
  }, () => {
22872
22982
  return;
22873
22983
  });
22874
- return run;
22984
+ return this.withOperationTimeout(run);
22985
+ }
22986
+ withOperationTimeout(promise) {
22987
+ if (this.operationTimeoutMs <= 0) {
22988
+ return promise;
22989
+ }
22990
+ let timeoutHandle;
22991
+ const timeoutPromise = new Promise((_, reject) => {
22992
+ timeoutHandle = setTimeout(() => {
22993
+ reject(new Error(`Sync operation timed out after ${this.operationTimeoutMs}ms`));
22994
+ }, this.operationTimeoutMs);
22995
+ });
22996
+ return Promise.race([promise, timeoutPromise]).finally(() => {
22997
+ if (timeoutHandle) {
22998
+ clearTimeout(timeoutHandle);
22999
+ }
23000
+ });
23001
+ }
23002
+ consumeRateLimit(sessionId) {
23003
+ const state = this.rateLimitStates.get(sessionId);
23004
+ if (!state) {
23005
+ return "allow";
23006
+ }
23007
+ const now = Date.now();
23008
+ if (now - state.windowStartedAt >= this.rateLimitWindowMs) {
23009
+ state.windowStartedAt = now;
23010
+ state.messagesInWindow = 0;
23011
+ }
23012
+ state.messagesInWindow += 1;
23013
+ if (state.messagesInWindow <= this.maxMessagesPerWindow) {
23014
+ return "allow";
23015
+ }
23016
+ const hardLimit = Math.max(this.maxMessagesPerWindow + 1, this.maxMessagesPerWindow * RATE_LIMIT_HARD_MULTIPLIER);
23017
+ if (state.messagesInWindow >= hardLimit) {
23018
+ return "close";
23019
+ }
23020
+ return "reject";
23021
+ }
23022
+ computeProjectedActiveQueryCount(session, message2) {
23023
+ const projected = new Set(session.activeQueries.keys());
23024
+ for (const mod of message2.modifications) {
23025
+ if (mod.type === "Add") {
23026
+ projected.add(mod.queryId);
23027
+ } else if (mod.type === "Remove") {
23028
+ projected.delete(mod.queryId);
23029
+ }
23030
+ }
23031
+ return projected.size;
22875
23032
  }
22876
23033
  sendPing(session) {
22877
23034
  if (!session.websocket || session.websocket.readyState !== WEBSOCKET_READY_STATE_OPEN) {
@@ -24748,6 +24905,8 @@ class SystemAuthError2 extends Error {
24748
24905
  }
24749
24906
  }
24750
24907
  var DEFAULT_CLOCK_TOLERANCE_SECONDS2 = 60;
24908
+ var DEFAULT_JWKS_CACHE_TTL_MS2 = 5 * 60 * 1000;
24909
+ var MAX_JWKS_CACHE_TTL_MS2 = 24 * 60 * 60 * 1000;
24751
24910
  var JWKS_CACHE2 = new Map;
24752
24911
  var defaultValidationConfig2;
24753
24912
  var adminAuthConfig2;
@@ -24863,7 +25022,8 @@ function resolveJwtValidationConfigFromEnv2(env) {
24863
25022
  const secret = getEnvValue2("AUTH_SECRET", env) ?? getEnvValue2("CONCAVE_JWT_SECRET", env) ?? getEnvValue2("JWT_SECRET", env);
24864
25023
  const skipVerification = parseBoolean2(getEnvValue2("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean2(getEnvValue2("CONCAVE_JWT_SKIP_VERIFICATION", env));
24865
25024
  const clockTolerance = parseNumber2(getEnvValue2("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber2(getEnvValue2("CONCAVE_JWT_CLOCK_TOLERANCE", env));
24866
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
25025
+ const jwksCacheTtlMs = parseNumber2(getEnvValue2("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber2(getEnvValue2("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
25026
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
24867
25027
  return;
24868
25028
  }
24869
25029
  return {
@@ -24872,7 +25032,8 @@ function resolveJwtValidationConfigFromEnv2(env) {
24872
25032
  audience,
24873
25033
  secret,
24874
25034
  skipVerification,
24875
- clockTolerance
25035
+ clockTolerance,
25036
+ jwksCacheTtlMs
24876
25037
  };
24877
25038
  }
24878
25039
  function normalizeList2(value) {
@@ -24916,15 +25077,33 @@ function validateClaims2(claims, config) {
24916
25077
  throw new JWTValidationError2("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
24917
25078
  }
24918
25079
  }
24919
- function getRemoteJwks2(jwksUrl) {
25080
+ function getRemoteJwks2(jwksUrl, config) {
25081
+ const now = Date.now();
24920
25082
  const cached = JWKS_CACHE2.get(jwksUrl);
25083
+ if (cached && cached.expiresAtMs > now) {
25084
+ return cached.resolver;
25085
+ }
24921
25086
  if (cached) {
24922
- return cached;
25087
+ JWKS_CACHE2.delete(jwksUrl);
24923
25088
  }
24924
25089
  const jwks = createRemoteJWKSet2(new URL(jwksUrl));
24925
- JWKS_CACHE2.set(jwksUrl, jwks);
25090
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig2?.jwksCacheTtlMs;
25091
+ const ttlMs = resolveJwksCacheTtlMs2(configuredTtl);
25092
+ JWKS_CACHE2.set(jwksUrl, {
25093
+ resolver: jwks,
25094
+ expiresAtMs: now + ttlMs
25095
+ });
24926
25096
  return jwks;
24927
25097
  }
25098
+ function resolveJwksCacheTtlMs2(configuredTtl) {
25099
+ if (configuredTtl === undefined) {
25100
+ return DEFAULT_JWKS_CACHE_TTL_MS2;
25101
+ }
25102
+ if (!Number.isFinite(configuredTtl)) {
25103
+ return DEFAULT_JWKS_CACHE_TTL_MS2;
25104
+ }
25105
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS2, Math.floor(configuredTtl)));
25106
+ }
24928
25107
  function decodeJwtUnsafe2(token) {
24929
25108
  if (!token)
24930
25109
  return null;
@@ -24957,7 +25136,7 @@ async function verifyJwt2(token, config) {
24957
25136
  const key = new TextEncoder().encode(effectiveConfig.secret);
24958
25137
  ({ payload } = await jwtVerify2(token, key, options));
24959
25138
  } else {
24960
- ({ payload } = await jwtVerify2(token, getRemoteJwks2(effectiveConfig.jwksUrl), options));
25139
+ ({ payload } = await jwtVerify2(token, getRemoteJwks2(effectiveConfig.jwksUrl, effectiveConfig), options));
24961
25140
  }
24962
25141
  const claims = payload;
24963
25142
  validateClaims2(claims, effectiveConfig);
@@ -25400,6 +25579,39 @@ async function resolveAuthContext(bodyAuth, headerToken, headerIdentity) {
25400
25579
  }
25401
25580
  return bodyAuth;
25402
25581
  }
25582
+ function parseTimestampInput(value) {
25583
+ if (value === undefined || value === null) {
25584
+ return;
25585
+ }
25586
+ if (typeof value === "bigint") {
25587
+ return value >= 0n ? value : undefined;
25588
+ }
25589
+ if (typeof value === "number") {
25590
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
25591
+ return;
25592
+ }
25593
+ return BigInt(value);
25594
+ }
25595
+ if (typeof value === "string") {
25596
+ const trimmed = value.trim();
25597
+ if (!/^\d+$/.test(trimmed)) {
25598
+ return;
25599
+ }
25600
+ try {
25601
+ return BigInt(trimmed);
25602
+ } catch {
25603
+ return;
25604
+ }
25605
+ }
25606
+ return;
25607
+ }
25608
+ async function resolveSnapshotTimestamp(options, request) {
25609
+ const fromCallback = options.getSnapshotTimestamp ? await options.getSnapshotTimestamp(request) : undefined;
25610
+ if (typeof fromCallback === "bigint") {
25611
+ return fromCallback;
25612
+ }
25613
+ return BigInt(Date.now());
25614
+ }
25403
25615
  async function handleCoreHttpApiRequest(request, options) {
25404
25616
  const url = new URL(request.url);
25405
25617
  const segments = url.pathname.split("/").filter(Boolean);
@@ -25439,6 +25651,19 @@ async function handleCoreHttpApiRequest(request, options) {
25439
25651
  throw error;
25440
25652
  }
25441
25653
  const route = routeSegments[0];
25654
+ if (route === "query_ts") {
25655
+ if (request.method !== "POST") {
25656
+ return {
25657
+ handled: true,
25658
+ response: apply(Response.json({ error: "Method not allowed" }, { status: 405 }))
25659
+ };
25660
+ }
25661
+ const snapshotTimestamp = await resolveSnapshotTimestamp(options, request);
25662
+ return {
25663
+ handled: true,
25664
+ response: apply(Response.json({ ts: snapshotTimestamp.toString() }))
25665
+ };
25666
+ }
25442
25667
  if (route === "storage") {
25443
25668
  if (!options.storage) {
25444
25669
  return {
@@ -25496,7 +25721,7 @@ async function handleCoreHttpApiRequest(request, options) {
25496
25721
  }
25497
25722
  }
25498
25723
  }
25499
- if (route === "query" || route === "mutation" || route === "action") {
25724
+ if (route === "query" || route === "mutation" || route === "action" || route === "query_at_ts") {
25500
25725
  if (request.method !== "POST") {
25501
25726
  return {
25502
25727
  handled: true,
@@ -25519,7 +25744,7 @@ async function handleCoreHttpApiRequest(request, options) {
25519
25744
  response: apply(Response.json({ error: "Invalid request body" }, { status: 400 }))
25520
25745
  };
25521
25746
  }
25522
- const { path, args, format, auth: bodyAuth, componentPath } = body;
25747
+ const { path, args, format, auth: bodyAuth, componentPath, ts } = body;
25523
25748
  if (!path || typeof path !== "string") {
25524
25749
  return {
25525
25750
  handled: true,
@@ -25548,6 +25773,14 @@ async function handleCoreHttpApiRequest(request, options) {
25548
25773
  };
25549
25774
  }
25550
25775
  const jsonArgs = rawArgs ?? {};
25776
+ const executionType = route === "query_at_ts" ? "query" : route;
25777
+ const snapshotTimestamp = route === "query_at_ts" ? parseTimestampInput(ts) : undefined;
25778
+ if (route === "query_at_ts" && snapshotTimestamp === undefined) {
25779
+ return {
25780
+ handled: true,
25781
+ response: apply(Response.json({ error: "Invalid or missing ts" }, { status: 400 }))
25782
+ };
25783
+ }
25551
25784
  let authForExecution;
25552
25785
  try {
25553
25786
  authForExecution = await resolveAuthContext(bodyAuth, headerToken, headerIdentity);
@@ -25561,11 +25794,12 @@ async function handleCoreHttpApiRequest(request, options) {
25561
25794
  throw error;
25562
25795
  }
25563
25796
  const executionParams = {
25564
- type: route,
25797
+ type: executionType,
25565
25798
  path,
25566
25799
  args: jsonArgs,
25567
25800
  auth: authForExecution,
25568
25801
  componentPath,
25802
+ snapshotTimestamp,
25569
25803
  request
25570
25804
  };
25571
25805
  try {
@@ -25580,7 +25814,7 @@ async function handleCoreHttpApiRequest(request, options) {
25580
25814
  throw validationError;
25581
25815
  }
25582
25816
  const result = await options.executeFunction(executionParams);
25583
- if (options.notifyWrites && (route === "mutation" || route === "action") && (result.writtenRanges?.length || result.writtenTables?.length)) {
25817
+ if (options.notifyWrites && (executionType === "mutation" || executionType === "action") && (result.writtenRanges?.length || result.writtenTables?.length)) {
25584
25818
  await options.notifyWrites(result.writtenRanges, result.writtenTables ?? writtenTablesFromRanges2(result.writtenRanges));
25585
25819
  }
25586
25820
  return {
@@ -28654,7 +28888,7 @@ class ForbiddenInQueriesOrMutations extends Error {
28654
28888
  this.name = "ForbiddenInQueriesOrMutations";
28655
28889
  }
28656
28890
  }
28657
- async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, deterministicSeed, mutationTransaction, udfExecutor, componentPath) {
28891
+ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, deterministicSeed, mutationTransaction, udfExecutor, componentPath, snapshotOverride) {
28658
28892
  const ambientIdentity = getAuthContext();
28659
28893
  let effectiveAuth;
28660
28894
  if (auth && typeof auth === "object" && "tokenType" in auth) {
@@ -28669,7 +28903,7 @@ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, dete
28669
28903
  const inheritedSnapshot = snapshotContext2.getStore() ?? null;
28670
28904
  const existingIdGenerator = idGeneratorContext2.getStore() ?? undefined;
28671
28905
  const idGenerator = existingIdGenerator ?? (deterministicSeed ? createDeterministicIdGenerator(deterministicSeed) : undefined);
28672
- const convex = new UdfKernel(docstore, effectiveAuth, storage2, inheritedSnapshot, mutationTransaction, udfExecutor, componentPath, idGenerator);
28906
+ const convex = new UdfKernel(docstore, effectiveAuth, storage2, snapshotOverride ?? inheritedSnapshot, mutationTransaction, udfExecutor, componentPath, idGenerator);
28673
28907
  convex.clearAccessLogs();
28674
28908
  const logLines = [];
28675
28909
  const logger = (level) => {
@@ -28725,7 +28959,7 @@ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, dete
28725
28959
  };
28726
28960
  } finally {}
28727
28961
  }
28728
- function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath) {
28962
+ function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath, snapshotOverride) {
28729
28963
  const tnow = Date.now();
28730
28964
  const seed = resolveSeed("query", requestId, tnow);
28731
28965
  const rng = udfRng(seed);
@@ -28737,7 +28971,7 @@ function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, compo
28737
28971
  fetch: forbiddenAsyncOp("fetch"),
28738
28972
  setInterval: forbiddenAsyncOp("setInterval"),
28739
28973
  setTimeout: forbiddenAsyncOp("setTimeout")
28740
- }, auth, "query", storage2, seed, undefined, udfExecutor, componentPath);
28974
+ }, auth, "query", storage2, seed, undefined, udfExecutor, componentPath, snapshotOverride);
28741
28975
  }
28742
28976
  function runUdfMutation(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath) {
28743
28977
  const tnow = Date.now();
@@ -28862,7 +29096,7 @@ class InlineUdfExecutor {
28862
29096
  this.moduleRegistry = options.moduleRegistry;
28863
29097
  this.logSink = options.logSink;
28864
29098
  }
28865
- async execute(functionPath, args, udfType, auth, componentPath, requestId) {
29099
+ async execute(functionPath, args, udfType, auth, componentPath, requestId, snapshotTimestamp) {
28866
29100
  const [moduleName, functionName3] = this.parseUdfPath(functionPath);
28867
29101
  const finalRequestId = requestId ?? this.requestIdFactory(udfType, functionPath);
28868
29102
  const isSystemFunction = moduleName === "_system" || functionPath.startsWith("_system:");
@@ -28887,7 +29121,7 @@ class InlineUdfExecutor {
28887
29121
  const runWithType = () => {
28888
29122
  switch (udfType) {
28889
29123
  case "query":
28890
- return runUdfQuery(this.docstore, runUdf3, auth, this.blobstore, finalRequestId, this, componentPath);
29124
+ return runUdfQuery(this.docstore, runUdf3, auth, this.blobstore, finalRequestId, this, componentPath, snapshotTimestamp);
28891
29125
  case "mutation":
28892
29126
  return runUdfMutation(this.docstore, runUdf3, auth, this.blobstore, finalRequestId, this, componentPath);
28893
29127
  case "action":
@@ -28941,7 +29175,7 @@ class InlineUdfExecutor {
28941
29175
  async executeHttp(request, auth, requestId) {
28942
29176
  const url = new URL(request.url);
28943
29177
  const runHttpUdf = async () => {
28944
- const httpModule = await this.loadModule("http", request.url);
29178
+ const httpModule = await this.loadModule("http");
28945
29179
  const router = httpModule?.default;
28946
29180
  if (!router?.isRouter || typeof router.lookup !== "function") {
28947
29181
  throw new Error("convex/http.ts must export a default httpRouter()");
@@ -29075,7 +29309,7 @@ class UdfExecutionAdapter2 {
29075
29309
  this.executor = executor;
29076
29310
  this.callType = callType;
29077
29311
  }
29078
- async executeUdf(path, jsonArgs, type, auth, componentPath, requestId) {
29312
+ async executeUdf(path, jsonArgs, type, auth, componentPath, requestId, snapshotTimestamp) {
29079
29313
  const convexArgs = convertClientArgs2(jsonArgs);
29080
29314
  const target = normalizeExecutionTarget2(path, componentPath);
29081
29315
  let authContext3;
@@ -29113,7 +29347,7 @@ class UdfExecutionAdapter2 {
29113
29347
  return runWithAuth2(userIdentity, async () => {
29114
29348
  const executeWithContext = this.callType === "client" ? runAsClientCall2 : runAsServerCall2;
29115
29349
  return executeWithContext(async () => {
29116
- return await this.executor.execute(target.path, convexArgs, type, authContext3 ?? userIdentity, normalizeComponentPath5(target.componentPath), requestId);
29350
+ return await this.executor.execute(target.path, convexArgs, type, authContext3 ?? userIdentity, normalizeComponentPath5(target.componentPath), requestId, snapshotTimestamp);
29117
29351
  });
29118
29352
  });
29119
29353
  }
@@ -29162,7 +29396,7 @@ function stripApiVersionPrefix(pathname) {
29162
29396
  }
29163
29397
  function isReservedApiPath(pathname) {
29164
29398
  const normalizedPath = stripApiVersionPrefix(pathname);
29165
- if (normalizedPath === "/api/execute" || normalizedPath === "/api/sync" || normalizedPath === "/api/reset-test-state" || normalizedPath === "/api/query" || normalizedPath === "/api/mutation" || normalizedPath === "/api/action") {
29399
+ 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") {
29166
29400
  return true;
29167
29401
  }
29168
29402
  if (normalizedPath === "/api/storage" || normalizedPath.startsWith("/api/storage/")) {
@@ -29233,7 +29467,7 @@ class HttpHandler {
29233
29467
  }
29234
29468
  };
29235
29469
  const coreResult = await handleCoreHttpApiRequest(request, {
29236
- executeFunction: async ({ type, path, args, auth, componentPath }) => this.adapter.executeUdf(path, args, type, auth, componentPath),
29470
+ executeFunction: async ({ type, path, args, auth, componentPath, snapshotTimestamp }) => this.adapter.executeUdf(path, args, type, auth, componentPath, undefined, snapshotTimestamp),
29237
29471
  notifyWrites,
29238
29472
  storage: this.docstore && this.blobstore ? {
29239
29473
  store: async (blob) => {
@@ -29252,7 +29486,16 @@ class HttpHandler {
29252
29486
  return { blob: blob ?? null };
29253
29487
  }
29254
29488
  } : undefined,
29255
- corsHeaders
29489
+ corsHeaders,
29490
+ getSnapshotTimestamp: () => {
29491
+ const oracle = this.docstore?.timestampOracle;
29492
+ const oracleTimestamp = typeof oracle?.beginSnapshot === "function" ? oracle.beginSnapshot() : typeof oracle?.getCurrentTimestamp === "function" ? oracle.getCurrentTimestamp() : undefined;
29493
+ const wallClock = BigInt(Date.now());
29494
+ if (typeof oracleTimestamp === "bigint" && oracleTimestamp > wallClock) {
29495
+ return oracleTimestamp;
29496
+ }
29497
+ return wallClock;
29498
+ }
29256
29499
  });
29257
29500
  if (coreResult?.handled) {
29258
29501
  return coreResult.response;
@@ -35403,6 +35646,8 @@ function decodeJwtClaimsToken3(token) {
35403
35646
  return null;
35404
35647
  }
35405
35648
  }
35649
+ var DEFAULT_JWKS_CACHE_TTL_MS3 = 5 * 60 * 1000;
35650
+ var MAX_JWKS_CACHE_TTL_MS3 = 24 * 60 * 60 * 1000;
35406
35651
  var JWKS_CACHE3 = new Map;
35407
35652
  function decodeJwtUnsafe3(token) {
35408
35653
  if (!token)
@@ -36220,6 +36465,8 @@ class ScheduledFunctionExecutor {
36220
36465
  logger;
36221
36466
  runMutationInTransaction;
36222
36467
  tableName;
36468
+ maxConcurrentJobs;
36469
+ scanPageSize;
36223
36470
  constructor(options) {
36224
36471
  this.docstore = options.docstore;
36225
36472
  this.udfExecutor = options.udfExecutor;
@@ -36229,46 +36476,68 @@ class ScheduledFunctionExecutor {
36229
36476
  this.logger = options.logger ?? console;
36230
36477
  this.runMutationInTransaction = options.runMutationInTransaction;
36231
36478
  this.tableName = options.tableName ?? SCHEDULED_FUNCTIONS_TABLE;
36479
+ this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
36480
+ this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
36232
36481
  }
36233
36482
  async runDueJobs() {
36234
36483
  const tableId = stringToHex3(this.tableName);
36235
- const allJobs = await this.docstore.scan(tableId);
36236
36484
  const now = this.now();
36237
- const pendingJobs = allJobs.filter((doc) => {
36238
- const value = doc.value?.value;
36239
- if (!value || typeof value !== "object") {
36240
- return false;
36241
- }
36242
- const state = value.state;
36243
- const scheduledTime = value.scheduledTime;
36244
- return state?.kind === "pending" && typeof scheduledTime === "number" && scheduledTime <= now;
36245
- });
36246
- if (pendingJobs.length === 0) {
36247
- return {
36248
- executed: 0,
36249
- nextScheduledTime: this.computeNextScheduledTime(allJobs)
36250
- };
36251
- }
36252
- await Promise.all(pendingJobs.map((job) => {
36253
- const jobValue = job.value?.value;
36254
- if (!jobValue) {
36255
- throw new Error("Job value unexpectedly missing after filter");
36485
+ let executed = 0;
36486
+ const inFlight = new Set;
36487
+ const schedule = async (jobValue) => {
36488
+ const run = this.executeJob(jobValue, tableId).then(() => {
36489
+ executed += 1;
36490
+ }).finally(() => {
36491
+ inFlight.delete(run);
36492
+ });
36493
+ inFlight.add(run);
36494
+ if (inFlight.size >= this.maxConcurrentJobs) {
36495
+ await Promise.race(inFlight);
36256
36496
  }
36257
- return this.executeJob(jobValue, tableId);
36258
- }));
36259
- return {
36260
- executed: pendingJobs.length,
36261
- nextScheduledTime: this.computeNextScheduledTime(await this.docstore.scan(tableId))
36262
36497
  };
36498
+ await this.forEachScheduledJob(async (jobValue) => {
36499
+ const state = jobValue.state;
36500
+ const scheduledTime = jobValue.scheduledTime;
36501
+ if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
36502
+ return;
36503
+ }
36504
+ if (scheduledTime <= now) {
36505
+ await schedule(jobValue);
36506
+ }
36507
+ });
36508
+ await Promise.all(inFlight);
36509
+ return { executed, nextScheduledTime: await this.getNextScheduledTime() };
36263
36510
  }
36264
36511
  async getNextScheduledTime() {
36265
- const tableId = stringToHex3(this.tableName);
36266
- const allJobs = await this.docstore.scan(tableId);
36267
- return this.computeNextScheduledTime(allJobs);
36512
+ let nextScheduledTime = null;
36513
+ await this.forEachScheduledJob(async (jobValue) => {
36514
+ const state = jobValue.state;
36515
+ const scheduledTime = jobValue.scheduledTime;
36516
+ if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
36517
+ return;
36518
+ }
36519
+ if (nextScheduledTime === null || scheduledTime < nextScheduledTime) {
36520
+ nextScheduledTime = scheduledTime;
36521
+ }
36522
+ });
36523
+ return nextScheduledTime;
36268
36524
  }
36269
- computeNextScheduledTime(allJobs) {
36270
- 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);
36271
- return pending.length > 0 ? pending[0] : null;
36525
+ async forEachScheduledJob(visitor) {
36526
+ const tableId = stringToHex3(this.tableName);
36527
+ let cursor2 = null;
36528
+ while (true) {
36529
+ const page = await this.docstore.scanPaginated(tableId, cursor2, this.scanPageSize, Order3.Asc);
36530
+ for (const doc of page.documents) {
36531
+ const value = doc.value?.value;
36532
+ if (value && typeof value === "object") {
36533
+ await visitor(value);
36534
+ }
36535
+ }
36536
+ if (!page.hasMore || !page.nextCursor) {
36537
+ break;
36538
+ }
36539
+ cursor2 = page.nextCursor;
36540
+ }
36272
36541
  }
36273
36542
  async executeJob(jobValue, tableId) {
36274
36543
  const jobId = jobValue?._id;
@@ -36400,6 +36669,8 @@ class CronExecutor {
36400
36669
  allocateTimestamp;
36401
36670
  now;
36402
36671
  logger;
36672
+ maxConcurrentJobs;
36673
+ scanPageSize;
36403
36674
  constructor(options) {
36404
36675
  this.docstore = options.docstore;
36405
36676
  this.udfExecutor = options.udfExecutor;
@@ -36407,6 +36678,8 @@ class CronExecutor {
36407
36678
  this.allocateTimestamp = options.allocateTimestamp;
36408
36679
  this.now = options.now ?? (() => Date.now());
36409
36680
  this.logger = options.logger ?? console;
36681
+ this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
36682
+ this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
36410
36683
  }
36411
36684
  async syncCronSpecs(cronSpecs) {
36412
36685
  const tableId = stringToHex3(CRONS_TABLE);
@@ -36492,33 +36765,58 @@ class CronExecutor {
36492
36765
  }
36493
36766
  async runDueJobs() {
36494
36767
  const tableId = stringToHex3(CRONS_TABLE);
36495
- const allJobs = await this.docstore.scan(tableId);
36496
36768
  const now = this.now();
36497
- const dueJobs = allJobs.filter((doc) => {
36498
- const value = doc.value?.value;
36499
- return value && value.nextRun <= now;
36500
- });
36501
- if (dueJobs.length === 0) {
36502
- return {
36503
- executed: 0,
36504
- nextScheduledTime: this.computeNextScheduledTime(allJobs)
36505
- };
36506
- }
36507
- await Promise.all(dueJobs.map((job) => this.executeJob(job, tableId)));
36508
- const updatedJobs = await this.docstore.scan(tableId);
36509
- return {
36510
- executed: dueJobs.length,
36511
- nextScheduledTime: this.computeNextScheduledTime(updatedJobs)
36769
+ let executed = 0;
36770
+ const inFlight = new Set;
36771
+ const schedule = async (job) => {
36772
+ const run = this.executeJob(job, tableId).then(() => {
36773
+ executed += 1;
36774
+ }).finally(() => {
36775
+ inFlight.delete(run);
36776
+ });
36777
+ inFlight.add(run);
36778
+ if (inFlight.size >= this.maxConcurrentJobs) {
36779
+ await Promise.race(inFlight);
36780
+ }
36512
36781
  };
36782
+ await this.forEachCronJob(async (job) => {
36783
+ const value = job.value?.value;
36784
+ if (!value || typeof value.nextRun !== "number") {
36785
+ return;
36786
+ }
36787
+ if (value.nextRun <= now) {
36788
+ await schedule(job);
36789
+ }
36790
+ });
36791
+ await Promise.all(inFlight);
36792
+ return { executed, nextScheduledTime: await this.getNextScheduledTime() };
36513
36793
  }
36514
36794
  async getNextScheduledTime() {
36515
- const tableId = stringToHex3(CRONS_TABLE);
36516
- const allJobs = await this.docstore.scan(tableId);
36517
- return this.computeNextScheduledTime(allJobs);
36795
+ let nextScheduledTime = null;
36796
+ await this.forEachCronJob(async (job) => {
36797
+ const nextRun = job.value?.value?.nextRun;
36798
+ if (typeof nextRun !== "number") {
36799
+ return;
36800
+ }
36801
+ if (nextScheduledTime === null || nextRun < nextScheduledTime) {
36802
+ nextScheduledTime = nextRun;
36803
+ }
36804
+ });
36805
+ return nextScheduledTime;
36518
36806
  }
36519
- computeNextScheduledTime(allJobs) {
36520
- const nextTimes = allJobs.map((doc) => doc.value?.value?.nextRun).filter((time) => typeof time === "number").sort((a, b) => a - b);
36521
- return nextTimes.length > 0 ? nextTimes[0] : null;
36807
+ async forEachCronJob(visitor) {
36808
+ const tableId = stringToHex3(CRONS_TABLE);
36809
+ let cursor2 = null;
36810
+ while (true) {
36811
+ const page = await this.docstore.scanPaginated(tableId, cursor2, this.scanPageSize, Order3.Asc);
36812
+ for (const job of page.documents) {
36813
+ await visitor(job);
36814
+ }
36815
+ if (!page.hasMore || !page.nextCursor) {
36816
+ break;
36817
+ }
36818
+ cursor2 = page.nextCursor;
36819
+ }
36522
36820
  }
36523
36821
  async executeJob(job, tableId) {
36524
36822
  const value = job.value?.value;
@@ -44126,6 +44424,8 @@ class SystemAuthError3 extends Error {
44126
44424
  }
44127
44425
  }
44128
44426
  var DEFAULT_CLOCK_TOLERANCE_SECONDS3 = 60;
44427
+ var DEFAULT_JWKS_CACHE_TTL_MS4 = 5 * 60 * 1000;
44428
+ var MAX_JWKS_CACHE_TTL_MS4 = 24 * 60 * 60 * 1000;
44129
44429
  var JWKS_CACHE4 = new Map;
44130
44430
  var defaultValidationConfig3;
44131
44431
  var adminAuthConfig3;
@@ -44221,7 +44521,8 @@ function resolveJwtValidationConfigFromEnv3(env) {
44221
44521
  const secret = getEnvValue3("AUTH_SECRET", env) ?? getEnvValue3("CONCAVE_JWT_SECRET", env) ?? getEnvValue3("JWT_SECRET", env);
44222
44522
  const skipVerification = parseBoolean3(getEnvValue3("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean3(getEnvValue3("CONCAVE_JWT_SKIP_VERIFICATION", env));
44223
44523
  const clockTolerance = parseNumber3(getEnvValue3("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber3(getEnvValue3("CONCAVE_JWT_CLOCK_TOLERANCE", env));
44224
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
44524
+ const jwksCacheTtlMs = parseNumber3(getEnvValue3("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber3(getEnvValue3("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
44525
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
44225
44526
  return;
44226
44527
  }
44227
44528
  return {
@@ -44230,7 +44531,8 @@ function resolveJwtValidationConfigFromEnv3(env) {
44230
44531
  audience,
44231
44532
  secret,
44232
44533
  skipVerification,
44233
- clockTolerance
44534
+ clockTolerance,
44535
+ jwksCacheTtlMs
44234
44536
  };
44235
44537
  }
44236
44538
  function normalizeList3(value) {
@@ -44274,15 +44576,33 @@ function validateClaims3(claims, config) {
44274
44576
  throw new JWTValidationError3("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
44275
44577
  }
44276
44578
  }
44277
- function getRemoteJwks3(jwksUrl) {
44579
+ function getRemoteJwks3(jwksUrl, config) {
44580
+ const now = Date.now();
44278
44581
  const cached = JWKS_CACHE4.get(jwksUrl);
44582
+ if (cached && cached.expiresAtMs > now) {
44583
+ return cached.resolver;
44584
+ }
44279
44585
  if (cached) {
44280
- return cached;
44586
+ JWKS_CACHE4.delete(jwksUrl);
44281
44587
  }
44282
44588
  const jwks = createRemoteJWKSet3(new URL(jwksUrl));
44283
- JWKS_CACHE4.set(jwksUrl, jwks);
44589
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig3?.jwksCacheTtlMs;
44590
+ const ttlMs = resolveJwksCacheTtlMs3(configuredTtl);
44591
+ JWKS_CACHE4.set(jwksUrl, {
44592
+ resolver: jwks,
44593
+ expiresAtMs: now + ttlMs
44594
+ });
44284
44595
  return jwks;
44285
44596
  }
44597
+ function resolveJwksCacheTtlMs3(configuredTtl) {
44598
+ if (configuredTtl === undefined) {
44599
+ return DEFAULT_JWKS_CACHE_TTL_MS4;
44600
+ }
44601
+ if (!Number.isFinite(configuredTtl)) {
44602
+ return DEFAULT_JWKS_CACHE_TTL_MS4;
44603
+ }
44604
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS4, Math.floor(configuredTtl)));
44605
+ }
44286
44606
  function decodeJwtUnsafe4(token) {
44287
44607
  if (!token)
44288
44608
  return null;
@@ -44315,7 +44635,7 @@ async function verifyJwt3(token, config) {
44315
44635
  const key = new TextEncoder().encode(effectiveConfig.secret);
44316
44636
  ({ payload } = await jwtVerify3(token, key, options));
44317
44637
  } else {
44318
- ({ payload } = await jwtVerify3(token, getRemoteJwks3(effectiveConfig.jwksUrl), options));
44638
+ ({ payload } = await jwtVerify3(token, getRemoteJwks3(effectiveConfig.jwksUrl, effectiveConfig), options));
44319
44639
  }
44320
44640
  const claims = payload;
44321
44641
  validateClaims3(claims, effectiveConfig);
@@ -45458,6 +45778,11 @@ var WEBSOCKET_READY_STATE_OPEN2 = 1;
45458
45778
  var BACKPRESSURE_HIGH_WATER_MARK2 = 100;
45459
45779
  var BACKPRESSURE_BUFFER_LIMIT2 = 1024 * 1024;
45460
45780
  var SLOW_CLIENT_TIMEOUT_MS2 = 30000;
45781
+ var DEFAULT_RATE_LIMIT_WINDOW_MS2 = 5000;
45782
+ var DEFAULT_MAX_MESSAGES_PER_WINDOW2 = 1000;
45783
+ var DEFAULT_OPERATION_TIMEOUT_MS2 = 15000;
45784
+ var DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION2 = 1000;
45785
+ var RATE_LIMIT_HARD_MULTIPLIER2 = 5;
45461
45786
 
45462
45787
  class SyncSession2 {
45463
45788
  websocket;
@@ -45486,15 +45811,25 @@ class SyncSession2 {
45486
45811
  class SyncProtocolHandler2 {
45487
45812
  udfExecutor;
45488
45813
  sessions = new Map;
45814
+ rateLimitStates = new Map;
45489
45815
  subscriptionManager;
45490
45816
  instanceName;
45491
45817
  backpressureController;
45492
45818
  heartbeatController;
45493
45819
  isDev;
45820
+ maxMessagesPerWindow;
45821
+ rateLimitWindowMs;
45822
+ operationTimeoutMs;
45823
+ maxActiveQueriesPerSession;
45494
45824
  constructor(instanceName, udfExecutor, options) {
45495
45825
  this.udfExecutor = udfExecutor;
45496
45826
  this.instanceName = instanceName;
45497
45827
  this.isDev = options?.isDev ?? true;
45828
+ this.maxMessagesPerWindow = Math.max(1, options?.maxMessagesPerWindow ?? DEFAULT_MAX_MESSAGES_PER_WINDOW2);
45829
+ this.rateLimitWindowMs = Math.max(1, options?.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS2);
45830
+ const configuredOperationTimeout = options?.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS2;
45831
+ this.operationTimeoutMs = Number.isFinite(configuredOperationTimeout) ? Math.max(0, Math.floor(configuredOperationTimeout)) : DEFAULT_OPERATION_TIMEOUT_MS2;
45832
+ this.maxActiveQueriesPerSession = Math.max(1, options?.maxActiveQueriesPerSession ?? DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION2);
45498
45833
  this.subscriptionManager = new SubscriptionManager3;
45499
45834
  this.backpressureController = new SessionBackpressureController2({
45500
45835
  websocketReadyStateOpen: WEBSOCKET_READY_STATE_OPEN2,
@@ -45512,6 +45847,10 @@ class SyncProtocolHandler2 {
45512
45847
  createSession(sessionId, websocket) {
45513
45848
  const session = new SyncSession2(websocket);
45514
45849
  this.sessions.set(sessionId, session);
45850
+ this.rateLimitStates.set(sessionId, {
45851
+ windowStartedAt: Date.now(),
45852
+ messagesInWindow: 0
45853
+ });
45515
45854
  return session;
45516
45855
  }
45517
45856
  getSession(sessionId) {
@@ -45526,6 +45865,7 @@ class SyncProtocolHandler2 {
45526
45865
  session.isDraining = false;
45527
45866
  this.subscriptionManager.unsubscribeAll(sessionId);
45528
45867
  this.sessions.delete(sessionId);
45868
+ this.rateLimitStates.delete(sessionId);
45529
45869
  }
45530
45870
  }
45531
45871
  updateSessionId(oldSessionId, newSessionId) {
@@ -45533,6 +45873,11 @@ class SyncProtocolHandler2 {
45533
45873
  if (session) {
45534
45874
  this.sessions.delete(oldSessionId);
45535
45875
  this.sessions.set(newSessionId, session);
45876
+ const rateLimitState = this.rateLimitStates.get(oldSessionId);
45877
+ if (rateLimitState) {
45878
+ this.rateLimitStates.delete(oldSessionId);
45879
+ this.rateLimitStates.set(newSessionId, rateLimitState);
45880
+ }
45536
45881
  this.subscriptionManager.updateSessionId(oldSessionId, newSessionId);
45537
45882
  }
45538
45883
  }
@@ -45541,6 +45886,29 @@ class SyncProtocolHandler2 {
45541
45886
  if (!session && message22.type !== "Connect") {
45542
45887
  throw new Error("Session not found");
45543
45888
  }
45889
+ if (session) {
45890
+ const rateLimitDecision = this.consumeRateLimit(sessionId);
45891
+ if (rateLimitDecision === "reject") {
45892
+ return [
45893
+ {
45894
+ type: "FatalError",
45895
+ error: "Rate limit exceeded, retry shortly"
45896
+ }
45897
+ ];
45898
+ }
45899
+ if (rateLimitDecision === "close") {
45900
+ try {
45901
+ session.websocket.close(1013, "Rate limit exceeded");
45902
+ } catch {}
45903
+ this.destroySession(sessionId);
45904
+ return [
45905
+ {
45906
+ type: "FatalError",
45907
+ error: "Rate limit exceeded"
45908
+ }
45909
+ ];
45910
+ }
45911
+ }
45544
45912
  switch (message22.type) {
45545
45913
  case "Connect":
45546
45914
  return this.handleConnect(sessionId, message22);
@@ -45597,6 +45965,15 @@ class SyncProtocolHandler2 {
45597
45965
  return [fatalError];
45598
45966
  }
45599
45967
  const startVersion = makeStateVersion2(session.querySetVersion, session.identityVersion, session.timestamp);
45968
+ const projectedActiveQueryCount = this.computeProjectedActiveQueryCount(session, message22);
45969
+ if (projectedActiveQueryCount > this.maxActiveQueriesPerSession) {
45970
+ return [
45971
+ {
45972
+ type: "FatalError",
45973
+ error: `Too many active queries: ${projectedActiveQueryCount} exceeds limit ${this.maxActiveQueriesPerSession}`
45974
+ }
45975
+ ];
45976
+ }
45600
45977
  session.querySetVersion = message22.newVersion;
45601
45978
  const modifications = [];
45602
45979
  for (const mod of message22.modifications) {
@@ -45920,7 +46297,54 @@ class SyncProtocolHandler2 {
45920
46297
  }, () => {
45921
46298
  return;
45922
46299
  });
45923
- return run;
46300
+ return this.withOperationTimeout(run);
46301
+ }
46302
+ withOperationTimeout(promise) {
46303
+ if (this.operationTimeoutMs <= 0) {
46304
+ return promise;
46305
+ }
46306
+ let timeoutHandle;
46307
+ const timeoutPromise = new Promise((_, reject) => {
46308
+ timeoutHandle = setTimeout(() => {
46309
+ reject(new Error(`Sync operation timed out after ${this.operationTimeoutMs}ms`));
46310
+ }, this.operationTimeoutMs);
46311
+ });
46312
+ return Promise.race([promise, timeoutPromise]).finally(() => {
46313
+ if (timeoutHandle) {
46314
+ clearTimeout(timeoutHandle);
46315
+ }
46316
+ });
46317
+ }
46318
+ consumeRateLimit(sessionId) {
46319
+ const state = this.rateLimitStates.get(sessionId);
46320
+ if (!state) {
46321
+ return "allow";
46322
+ }
46323
+ const now = Date.now();
46324
+ if (now - state.windowStartedAt >= this.rateLimitWindowMs) {
46325
+ state.windowStartedAt = now;
46326
+ state.messagesInWindow = 0;
46327
+ }
46328
+ state.messagesInWindow += 1;
46329
+ if (state.messagesInWindow <= this.maxMessagesPerWindow) {
46330
+ return "allow";
46331
+ }
46332
+ const hardLimit = Math.max(this.maxMessagesPerWindow + 1, this.maxMessagesPerWindow * RATE_LIMIT_HARD_MULTIPLIER2);
46333
+ if (state.messagesInWindow >= hardLimit) {
46334
+ return "close";
46335
+ }
46336
+ return "reject";
46337
+ }
46338
+ computeProjectedActiveQueryCount(session, message22) {
46339
+ const projected = new Set(session.activeQueries.keys());
46340
+ for (const mod of message22.modifications) {
46341
+ if (mod.type === "Add") {
46342
+ projected.add(mod.queryId);
46343
+ } else if (mod.type === "Remove") {
46344
+ projected.delete(mod.queryId);
46345
+ }
46346
+ }
46347
+ return projected.size;
45924
46348
  }
45925
46349
  sendPing(session) {
45926
46350
  if (!session.websocket || session.websocket.readyState !== WEBSOCKET_READY_STATE_OPEN2) {