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