@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.
- package/dist/assets/dashboard/dashboard.js +1 -1
- package/dist/assets/manifest.json +14 -14
- package/dist/assets/runtime-bun/server/index.js +512 -110
- package/dist/assets/runtime-cf/runtime.bundle.js +21217 -437
- package/dist/assets/runtime-node/server/index.js +544 -120
- package/dist/cli.js +2 -9
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20046
|
+
JWKS_CACHE.delete(jwksUrl);
|
|
20008
20047
|
}
|
|
20009
20048
|
const jwks = createRemoteJWKSet(new URL(jwksUrl));
|
|
20010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25087
|
+
JWKS_CACHE2.delete(jwksUrl);
|
|
24923
25088
|
}
|
|
24924
25089
|
const jwks = createRemoteJWKSet2(new URL(jwksUrl));
|
|
24925
|
-
|
|
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:
|
|
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 && (
|
|
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"
|
|
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
|
-
|
|
36238
|
-
|
|
36239
|
-
|
|
36240
|
-
|
|
36241
|
-
|
|
36242
|
-
|
|
36243
|
-
|
|
36244
|
-
|
|
36245
|
-
|
|
36246
|
-
|
|
36247
|
-
|
|
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
|
-
|
|
36266
|
-
|
|
36267
|
-
|
|
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
|
-
|
|
36270
|
-
const
|
|
36271
|
-
|
|
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
|
-
|
|
36498
|
-
|
|
36499
|
-
|
|
36500
|
-
|
|
36501
|
-
|
|
36502
|
-
|
|
36503
|
-
|
|
36504
|
-
|
|
36505
|
-
|
|
36506
|
-
|
|
36507
|
-
|
|
36508
|
-
|
|
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
|
-
|
|
36516
|
-
|
|
36517
|
-
|
|
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
|
-
|
|
36520
|
-
const
|
|
36521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44586
|
+
JWKS_CACHE4.delete(jwksUrl);
|
|
44281
44587
|
}
|
|
44282
44588
|
const jwks = createRemoteJWKSet3(new URL(jwksUrl));
|
|
44283
|
-
|
|
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) {
|