@concavejs/runtime-node 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/factory.js +544 -120
- package/dist/index.js +663 -185
- package/dist/server/index.js +544 -120
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12514,6 +12514,30 @@ class BaseSqliteDocStore {
|
|
|
12514
12514
|
return scored.slice(0, limit);
|
|
12515
12515
|
}
|
|
12516
12516
|
}
|
|
12517
|
+
function createSerializedTransactionRunner(hooks) {
|
|
12518
|
+
let queue = Promise.resolve();
|
|
12519
|
+
return async function runInTransaction(fn) {
|
|
12520
|
+
const run = queue.then(async () => {
|
|
12521
|
+
await hooks.begin();
|
|
12522
|
+
try {
|
|
12523
|
+
const result = await fn();
|
|
12524
|
+
await hooks.commit();
|
|
12525
|
+
return result;
|
|
12526
|
+
} catch (error) {
|
|
12527
|
+
try {
|
|
12528
|
+
await hooks.rollback();
|
|
12529
|
+
} catch {}
|
|
12530
|
+
throw error;
|
|
12531
|
+
}
|
|
12532
|
+
});
|
|
12533
|
+
queue = run.then(() => {
|
|
12534
|
+
return;
|
|
12535
|
+
}, () => {
|
|
12536
|
+
return;
|
|
12537
|
+
});
|
|
12538
|
+
return run;
|
|
12539
|
+
};
|
|
12540
|
+
}
|
|
12517
12541
|
|
|
12518
12542
|
class Long22 {
|
|
12519
12543
|
low;
|
|
@@ -12666,8 +12690,14 @@ class NodePreparedStatement {
|
|
|
12666
12690
|
|
|
12667
12691
|
class NodeSqliteAdapter {
|
|
12668
12692
|
db;
|
|
12693
|
+
runSerializedTransaction;
|
|
12669
12694
|
constructor(db) {
|
|
12670
12695
|
this.db = db;
|
|
12696
|
+
this.runSerializedTransaction = createSerializedTransactionRunner({
|
|
12697
|
+
begin: () => this.db.exec("BEGIN TRANSACTION"),
|
|
12698
|
+
commit: () => this.db.exec("COMMIT"),
|
|
12699
|
+
rollback: () => this.db.exec("ROLLBACK")
|
|
12700
|
+
});
|
|
12671
12701
|
}
|
|
12672
12702
|
exec(sql) {
|
|
12673
12703
|
this.db.exec(sql);
|
|
@@ -12676,15 +12706,7 @@ class NodeSqliteAdapter {
|
|
|
12676
12706
|
return new NodePreparedStatement(this.db.prepare(sql));
|
|
12677
12707
|
}
|
|
12678
12708
|
async transaction(fn) {
|
|
12679
|
-
|
|
12680
|
-
this.db.exec("BEGIN TRANSACTION");
|
|
12681
|
-
const result = await fn();
|
|
12682
|
-
this.db.exec("COMMIT");
|
|
12683
|
-
return result;
|
|
12684
|
-
} catch (error) {
|
|
12685
|
-
this.db.exec("ROLLBACK");
|
|
12686
|
-
throw error;
|
|
12687
|
-
}
|
|
12709
|
+
return this.runSerializedTransaction(fn);
|
|
12688
12710
|
}
|
|
12689
12711
|
hexToBuffer(hex) {
|
|
12690
12712
|
return Buffer.from(hexToArrayBuffer32(hex));
|
|
@@ -12788,7 +12810,7 @@ var __defProp14, __export5 = (target, all) => {
|
|
|
12788
12810
|
} catch {
|
|
12789
12811
|
return;
|
|
12790
12812
|
}
|
|
12791
|
-
}, AsyncLocalStorageCtor5, snapshotContext5, transactionContext5, idGeneratorContext5, CALL_CONTEXT_SYMBOL5, globalCallContext5, callContext5, JWKS_CACHE5, debug5 = () => {}, Convex25, UZERO22, TWO_PWR_16_DBL22, TWO_PWR_32_DBL22, TWO_PWR_64_DBL22, MAX_UNSIGNED_VALUE22, SqliteDocStore;
|
|
12813
|
+
}, AsyncLocalStorageCtor5, snapshotContext5, transactionContext5, idGeneratorContext5, CALL_CONTEXT_SYMBOL5, globalCallContext5, callContext5, DEFAULT_JWKS_CACHE_TTL_MS5, MAX_JWKS_CACHE_TTL_MS5, JWKS_CACHE5, debug5 = () => {}, Convex25, UZERO22, TWO_PWR_16_DBL22, TWO_PWR_32_DBL22, TWO_PWR_64_DBL22, MAX_UNSIGNED_VALUE22, SqliteDocStore;
|
|
12792
12814
|
var init_dist = __esm(() => {
|
|
12793
12815
|
__defProp14 = Object.defineProperty;
|
|
12794
12816
|
init_base645 = __esm5(() => {
|
|
@@ -13886,6 +13908,8 @@ var init_dist = __esm(() => {
|
|
|
13886
13908
|
init_values5();
|
|
13887
13909
|
init_index_manager5();
|
|
13888
13910
|
init_interface6();
|
|
13911
|
+
DEFAULT_JWKS_CACHE_TTL_MS5 = 5 * 60 * 1000;
|
|
13912
|
+
MAX_JWKS_CACHE_TTL_MS5 = 24 * 60 * 60 * 1000;
|
|
13889
13913
|
JWKS_CACHE5 = new Map;
|
|
13890
13914
|
init_values5();
|
|
13891
13915
|
init_schema_service5();
|
|
@@ -13941,6 +13965,9 @@ class FsBlobStore {
|
|
|
13941
13965
|
return false;
|
|
13942
13966
|
}
|
|
13943
13967
|
}
|
|
13968
|
+
isNotFoundError(error) {
|
|
13969
|
+
return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
13970
|
+
}
|
|
13944
13971
|
generateStorageId() {
|
|
13945
13972
|
return crypto.randomUUID();
|
|
13946
13973
|
}
|
|
@@ -13981,31 +14008,25 @@ class FsBlobStore {
|
|
|
13981
14008
|
async get(storageId) {
|
|
13982
14009
|
const objectPath = this.getObjectPath(storageId);
|
|
13983
14010
|
try {
|
|
13984
|
-
const fileExists = await this.pathExists(objectPath);
|
|
13985
|
-
if (!fileExists) {
|
|
13986
|
-
return null;
|
|
13987
|
-
}
|
|
13988
14011
|
const data = await readFile(objectPath);
|
|
13989
14012
|
const metadata = await this.getMetadata(storageId);
|
|
13990
14013
|
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
13991
14014
|
return new Blob([arrayBuffer], { type: metadata?.contentType || "application/octet-stream" });
|
|
13992
14015
|
} catch (error) {
|
|
14016
|
+
if (this.isNotFoundError(error)) {
|
|
14017
|
+
return null;
|
|
14018
|
+
}
|
|
13993
14019
|
console.error("Error reading object from filesystem:", error);
|
|
13994
|
-
|
|
14020
|
+
throw error;
|
|
13995
14021
|
}
|
|
13996
14022
|
}
|
|
13997
14023
|
async delete(storageId) {
|
|
13998
14024
|
const objectPath = this.getObjectPath(storageId);
|
|
13999
14025
|
const metadataPath = this.getMetadataPath(storageId);
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
]);
|
|
14005
|
-
} catch (error) {
|
|
14006
|
-
console.error("Error deleting object from filesystem:", error);
|
|
14007
|
-
throw error;
|
|
14008
|
-
}
|
|
14026
|
+
await Promise.all([
|
|
14027
|
+
this.unlinkIfExists(objectPath),
|
|
14028
|
+
this.unlinkIfExists(metadataPath)
|
|
14029
|
+
]);
|
|
14009
14030
|
}
|
|
14010
14031
|
async getUrl(storageId) {
|
|
14011
14032
|
const objectPath = this.getObjectPath(storageId);
|
|
@@ -14019,15 +14040,25 @@ class FsBlobStore {
|
|
|
14019
14040
|
async getMetadata(storageId) {
|
|
14020
14041
|
const metadataPath = this.getMetadataPath(storageId);
|
|
14021
14042
|
try {
|
|
14022
|
-
const fileExists = await this.pathExists(metadataPath);
|
|
14023
|
-
if (!fileExists) {
|
|
14024
|
-
return null;
|
|
14025
|
-
}
|
|
14026
14043
|
const data = await readFile(metadataPath, "utf-8");
|
|
14027
14044
|
return JSON.parse(data);
|
|
14028
14045
|
} catch (error) {
|
|
14046
|
+
if (this.isNotFoundError(error)) {
|
|
14047
|
+
return null;
|
|
14048
|
+
}
|
|
14029
14049
|
console.error("Error reading metadata from filesystem:", error);
|
|
14030
|
-
|
|
14050
|
+
throw error;
|
|
14051
|
+
}
|
|
14052
|
+
}
|
|
14053
|
+
async unlinkIfExists(path2) {
|
|
14054
|
+
try {
|
|
14055
|
+
await unlink(path2);
|
|
14056
|
+
} catch (error) {
|
|
14057
|
+
if (this.isNotFoundError(error)) {
|
|
14058
|
+
return;
|
|
14059
|
+
}
|
|
14060
|
+
console.error("Error deleting object from filesystem:", error);
|
|
14061
|
+
throw error;
|
|
14031
14062
|
}
|
|
14032
14063
|
}
|
|
14033
14064
|
}
|
|
@@ -18555,12 +18586,15 @@ var WELL_KNOWN_JWKS_URLS = {
|
|
|
18555
18586
|
firebase: () => "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com"
|
|
18556
18587
|
};
|
|
18557
18588
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 60;
|
|
18589
|
+
var DEFAULT_JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
18590
|
+
var MAX_JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
18558
18591
|
var JWKS_CACHE = new Map;
|
|
18559
18592
|
var defaultValidationConfig;
|
|
18560
18593
|
var adminAuthConfig;
|
|
18561
18594
|
var systemAuthConfig;
|
|
18562
18595
|
function setJwtValidationConfig(config) {
|
|
18563
18596
|
defaultValidationConfig = config;
|
|
18597
|
+
JWKS_CACHE.clear();
|
|
18564
18598
|
}
|
|
18565
18599
|
function getJwtValidationConfig() {
|
|
18566
18600
|
return defaultValidationConfig;
|
|
@@ -18688,7 +18722,8 @@ function resolveJwtValidationConfigFromEnv(env) {
|
|
|
18688
18722
|
const secret = getEnvValue("AUTH_SECRET", env) ?? getEnvValue("CONCAVE_JWT_SECRET", env) ?? getEnvValue("JWT_SECRET", env);
|
|
18689
18723
|
const skipVerification = parseBoolean(getEnvValue("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean(getEnvValue("CONCAVE_JWT_SKIP_VERIFICATION", env));
|
|
18690
18724
|
const clockTolerance = parseNumber(getEnvValue("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber(getEnvValue("CONCAVE_JWT_CLOCK_TOLERANCE", env));
|
|
18691
|
-
|
|
18725
|
+
const jwksCacheTtlMs = parseNumber(getEnvValue("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber(getEnvValue("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
|
|
18726
|
+
if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
|
|
18692
18727
|
return;
|
|
18693
18728
|
}
|
|
18694
18729
|
return {
|
|
@@ -18697,7 +18732,8 @@ function resolveJwtValidationConfigFromEnv(env) {
|
|
|
18697
18732
|
audience,
|
|
18698
18733
|
secret,
|
|
18699
18734
|
skipVerification,
|
|
18700
|
-
clockTolerance
|
|
18735
|
+
clockTolerance,
|
|
18736
|
+
jwksCacheTtlMs
|
|
18701
18737
|
};
|
|
18702
18738
|
}
|
|
18703
18739
|
function normalizeList(value) {
|
|
@@ -18741,15 +18777,33 @@ function validateClaims(claims, config) {
|
|
|
18741
18777
|
throw new JWTValidationError("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
|
|
18742
18778
|
}
|
|
18743
18779
|
}
|
|
18744
|
-
function getRemoteJwks(jwksUrl) {
|
|
18780
|
+
function getRemoteJwks(jwksUrl, config) {
|
|
18781
|
+
const now = Date.now();
|
|
18745
18782
|
const cached = JWKS_CACHE.get(jwksUrl);
|
|
18783
|
+
if (cached && cached.expiresAtMs > now) {
|
|
18784
|
+
return cached.resolver;
|
|
18785
|
+
}
|
|
18746
18786
|
if (cached) {
|
|
18747
|
-
|
|
18787
|
+
JWKS_CACHE.delete(jwksUrl);
|
|
18748
18788
|
}
|
|
18749
18789
|
const jwks = createRemoteJWKSet(new URL(jwksUrl));
|
|
18750
|
-
|
|
18790
|
+
const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig?.jwksCacheTtlMs;
|
|
18791
|
+
const ttlMs = resolveJwksCacheTtlMs(configuredTtl);
|
|
18792
|
+
JWKS_CACHE.set(jwksUrl, {
|
|
18793
|
+
resolver: jwks,
|
|
18794
|
+
expiresAtMs: now + ttlMs
|
|
18795
|
+
});
|
|
18751
18796
|
return jwks;
|
|
18752
18797
|
}
|
|
18798
|
+
function resolveJwksCacheTtlMs(configuredTtl) {
|
|
18799
|
+
if (configuredTtl === undefined) {
|
|
18800
|
+
return DEFAULT_JWKS_CACHE_TTL_MS;
|
|
18801
|
+
}
|
|
18802
|
+
if (!Number.isFinite(configuredTtl)) {
|
|
18803
|
+
return DEFAULT_JWKS_CACHE_TTL_MS;
|
|
18804
|
+
}
|
|
18805
|
+
return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS, Math.floor(configuredTtl)));
|
|
18806
|
+
}
|
|
18753
18807
|
function decodeJwtUnsafe(token) {
|
|
18754
18808
|
if (!token)
|
|
18755
18809
|
return null;
|
|
@@ -18782,7 +18836,7 @@ async function verifyJwt(token, config) {
|
|
|
18782
18836
|
const key = new TextEncoder().encode(effectiveConfig.secret);
|
|
18783
18837
|
({ payload } = await jwtVerify(token, key, options));
|
|
18784
18838
|
} else {
|
|
18785
|
-
({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl), options));
|
|
18839
|
+
({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl, effectiveConfig), options));
|
|
18786
18840
|
}
|
|
18787
18841
|
const claims = payload;
|
|
18788
18842
|
validateClaims(claims, effectiveConfig);
|
|
@@ -19870,7 +19924,7 @@ class ForbiddenInQueriesOrMutations extends Error {
|
|
|
19870
19924
|
this.name = "ForbiddenInQueriesOrMutations";
|
|
19871
19925
|
}
|
|
19872
19926
|
}
|
|
19873
|
-
async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, deterministicSeed, mutationTransaction, udfExecutor, componentPath) {
|
|
19927
|
+
async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, deterministicSeed, mutationTransaction, udfExecutor, componentPath, snapshotOverride) {
|
|
19874
19928
|
const ambientIdentity = getAuthContext();
|
|
19875
19929
|
let effectiveAuth;
|
|
19876
19930
|
if (auth && typeof auth === "object" && "tokenType" in auth) {
|
|
@@ -19885,7 +19939,7 @@ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, dete
|
|
|
19885
19939
|
const inheritedSnapshot = snapshotContext.getStore() ?? null;
|
|
19886
19940
|
const existingIdGenerator = idGeneratorContext.getStore() ?? undefined;
|
|
19887
19941
|
const idGenerator = existingIdGenerator ?? (deterministicSeed ? createDeterministicIdGenerator(deterministicSeed) : undefined);
|
|
19888
|
-
const convex = new UdfKernel(docstore, effectiveAuth, storage2, inheritedSnapshot, mutationTransaction, udfExecutor, componentPath, idGenerator);
|
|
19942
|
+
const convex = new UdfKernel(docstore, effectiveAuth, storage2, snapshotOverride ?? inheritedSnapshot, mutationTransaction, udfExecutor, componentPath, idGenerator);
|
|
19889
19943
|
convex.clearAccessLogs();
|
|
19890
19944
|
const logLines = [];
|
|
19891
19945
|
const logger = (level) => {
|
|
@@ -19941,7 +19995,7 @@ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, dete
|
|
|
19941
19995
|
};
|
|
19942
19996
|
} finally {}
|
|
19943
19997
|
}
|
|
19944
|
-
function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath) {
|
|
19998
|
+
function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath, snapshotOverride) {
|
|
19945
19999
|
const tnow = Date.now();
|
|
19946
20000
|
const seed = resolveSeed("query", requestId, tnow);
|
|
19947
20001
|
const rng = udfRng(seed);
|
|
@@ -19953,7 +20007,7 @@ function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, compo
|
|
|
19953
20007
|
fetch: forbiddenAsyncOp("fetch"),
|
|
19954
20008
|
setInterval: forbiddenAsyncOp("setInterval"),
|
|
19955
20009
|
setTimeout: forbiddenAsyncOp("setTimeout")
|
|
19956
|
-
}, auth, "query", storage2, seed, undefined, udfExecutor, componentPath);
|
|
20010
|
+
}, auth, "query", storage2, seed, undefined, udfExecutor, componentPath, snapshotOverride);
|
|
19957
20011
|
}
|
|
19958
20012
|
function runUdfMutation(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath) {
|
|
19959
20013
|
const tnow = Date.now();
|
|
@@ -20397,7 +20451,7 @@ class InlineUdfExecutor {
|
|
|
20397
20451
|
this.moduleRegistry = options.moduleRegistry;
|
|
20398
20452
|
this.logSink = options.logSink;
|
|
20399
20453
|
}
|
|
20400
|
-
async execute(functionPath, args, udfType, auth, componentPath, requestId) {
|
|
20454
|
+
async execute(functionPath, args, udfType, auth, componentPath, requestId, snapshotTimestamp) {
|
|
20401
20455
|
const [moduleName, functionName2] = this.parseUdfPath(functionPath);
|
|
20402
20456
|
const finalRequestId = requestId ?? this.requestIdFactory(udfType, functionPath);
|
|
20403
20457
|
const isSystemFunction = moduleName === "_system" || functionPath.startsWith("_system:");
|
|
@@ -20422,7 +20476,7 @@ class InlineUdfExecutor {
|
|
|
20422
20476
|
const runWithType = () => {
|
|
20423
20477
|
switch (udfType) {
|
|
20424
20478
|
case "query":
|
|
20425
|
-
return runUdfQuery(this.docstore, runUdf2, auth, this.blobstore, finalRequestId, this, componentPath);
|
|
20479
|
+
return runUdfQuery(this.docstore, runUdf2, auth, this.blobstore, finalRequestId, this, componentPath, snapshotTimestamp);
|
|
20426
20480
|
case "mutation":
|
|
20427
20481
|
return runUdfMutation(this.docstore, runUdf2, auth, this.blobstore, finalRequestId, this, componentPath);
|
|
20428
20482
|
case "action":
|
|
@@ -20476,7 +20530,7 @@ class InlineUdfExecutor {
|
|
|
20476
20530
|
async executeHttp(request, auth, requestId) {
|
|
20477
20531
|
const url = new URL(request.url);
|
|
20478
20532
|
const runHttpUdf = async () => {
|
|
20479
|
-
const httpModule = await this.loadModule("http"
|
|
20533
|
+
const httpModule = await this.loadModule("http");
|
|
20480
20534
|
const router = httpModule?.default;
|
|
20481
20535
|
if (!router?.isRouter || typeof router.lookup !== "function") {
|
|
20482
20536
|
throw new Error("convex/http.ts must export a default httpRouter()");
|
|
@@ -20610,7 +20664,7 @@ class UdfExecutionAdapter {
|
|
|
20610
20664
|
this.executor = executor;
|
|
20611
20665
|
this.callType = callType;
|
|
20612
20666
|
}
|
|
20613
|
-
async executeUdf(path, jsonArgs, type, auth, componentPath, requestId) {
|
|
20667
|
+
async executeUdf(path, jsonArgs, type, auth, componentPath, requestId, snapshotTimestamp) {
|
|
20614
20668
|
const convexArgs = convertClientArgs(jsonArgs);
|
|
20615
20669
|
const target = normalizeExecutionTarget(path, componentPath);
|
|
20616
20670
|
let authContext2;
|
|
@@ -20648,7 +20702,7 @@ class UdfExecutionAdapter {
|
|
|
20648
20702
|
return runWithAuth(userIdentity, async () => {
|
|
20649
20703
|
const executeWithContext = this.callType === "client" ? runAsClientCall : runAsServerCall;
|
|
20650
20704
|
return executeWithContext(async () => {
|
|
20651
|
-
return await this.executor.execute(target.path, convexArgs, type, authContext2 ?? userIdentity, normalizeComponentPath2(target.componentPath), requestId);
|
|
20705
|
+
return await this.executor.execute(target.path, convexArgs, type, authContext2 ?? userIdentity, normalizeComponentPath2(target.componentPath), requestId, snapshotTimestamp);
|
|
20652
20706
|
});
|
|
20653
20707
|
});
|
|
20654
20708
|
}
|
|
@@ -20960,6 +21014,39 @@ async function resolveAuthContext(bodyAuth, headerToken, headerIdentity) {
|
|
|
20960
21014
|
}
|
|
20961
21015
|
return bodyAuth;
|
|
20962
21016
|
}
|
|
21017
|
+
function parseTimestampInput(value) {
|
|
21018
|
+
if (value === undefined || value === null) {
|
|
21019
|
+
return;
|
|
21020
|
+
}
|
|
21021
|
+
if (typeof value === "bigint") {
|
|
21022
|
+
return value >= 0n ? value : undefined;
|
|
21023
|
+
}
|
|
21024
|
+
if (typeof value === "number") {
|
|
21025
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
21026
|
+
return;
|
|
21027
|
+
}
|
|
21028
|
+
return BigInt(value);
|
|
21029
|
+
}
|
|
21030
|
+
if (typeof value === "string") {
|
|
21031
|
+
const trimmed = value.trim();
|
|
21032
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
21033
|
+
return;
|
|
21034
|
+
}
|
|
21035
|
+
try {
|
|
21036
|
+
return BigInt(trimmed);
|
|
21037
|
+
} catch {
|
|
21038
|
+
return;
|
|
21039
|
+
}
|
|
21040
|
+
}
|
|
21041
|
+
return;
|
|
21042
|
+
}
|
|
21043
|
+
async function resolveSnapshotTimestamp(options, request) {
|
|
21044
|
+
const fromCallback = options.getSnapshotTimestamp ? await options.getSnapshotTimestamp(request) : undefined;
|
|
21045
|
+
if (typeof fromCallback === "bigint") {
|
|
21046
|
+
return fromCallback;
|
|
21047
|
+
}
|
|
21048
|
+
return BigInt(Date.now());
|
|
21049
|
+
}
|
|
20963
21050
|
async function handleCoreHttpApiRequest(request, options) {
|
|
20964
21051
|
const url = new URL(request.url);
|
|
20965
21052
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
@@ -20999,6 +21086,19 @@ async function handleCoreHttpApiRequest(request, options) {
|
|
|
20999
21086
|
throw error;
|
|
21000
21087
|
}
|
|
21001
21088
|
const route = routeSegments[0];
|
|
21089
|
+
if (route === "query_ts") {
|
|
21090
|
+
if (request.method !== "POST") {
|
|
21091
|
+
return {
|
|
21092
|
+
handled: true,
|
|
21093
|
+
response: apply(Response.json({ error: "Method not allowed" }, { status: 405 }))
|
|
21094
|
+
};
|
|
21095
|
+
}
|
|
21096
|
+
const snapshotTimestamp = await resolveSnapshotTimestamp(options, request);
|
|
21097
|
+
return {
|
|
21098
|
+
handled: true,
|
|
21099
|
+
response: apply(Response.json({ ts: snapshotTimestamp.toString() }))
|
|
21100
|
+
};
|
|
21101
|
+
}
|
|
21002
21102
|
if (route === "storage") {
|
|
21003
21103
|
if (!options.storage) {
|
|
21004
21104
|
return {
|
|
@@ -21056,7 +21156,7 @@ async function handleCoreHttpApiRequest(request, options) {
|
|
|
21056
21156
|
}
|
|
21057
21157
|
}
|
|
21058
21158
|
}
|
|
21059
|
-
if (route === "query" || route === "mutation" || route === "action") {
|
|
21159
|
+
if (route === "query" || route === "mutation" || route === "action" || route === "query_at_ts") {
|
|
21060
21160
|
if (request.method !== "POST") {
|
|
21061
21161
|
return {
|
|
21062
21162
|
handled: true,
|
|
@@ -21079,7 +21179,7 @@ async function handleCoreHttpApiRequest(request, options) {
|
|
|
21079
21179
|
response: apply(Response.json({ error: "Invalid request body" }, { status: 400 }))
|
|
21080
21180
|
};
|
|
21081
21181
|
}
|
|
21082
|
-
const { path, args, format, auth: bodyAuth, componentPath } = body;
|
|
21182
|
+
const { path, args, format, auth: bodyAuth, componentPath, ts } = body;
|
|
21083
21183
|
if (!path || typeof path !== "string") {
|
|
21084
21184
|
return {
|
|
21085
21185
|
handled: true,
|
|
@@ -21108,6 +21208,14 @@ async function handleCoreHttpApiRequest(request, options) {
|
|
|
21108
21208
|
};
|
|
21109
21209
|
}
|
|
21110
21210
|
const jsonArgs = rawArgs ?? {};
|
|
21211
|
+
const executionType = route === "query_at_ts" ? "query" : route;
|
|
21212
|
+
const snapshotTimestamp = route === "query_at_ts" ? parseTimestampInput(ts) : undefined;
|
|
21213
|
+
if (route === "query_at_ts" && snapshotTimestamp === undefined) {
|
|
21214
|
+
return {
|
|
21215
|
+
handled: true,
|
|
21216
|
+
response: apply(Response.json({ error: "Invalid or missing ts" }, { status: 400 }))
|
|
21217
|
+
};
|
|
21218
|
+
}
|
|
21111
21219
|
let authForExecution;
|
|
21112
21220
|
try {
|
|
21113
21221
|
authForExecution = await resolveAuthContext(bodyAuth, headerToken, headerIdentity);
|
|
@@ -21121,11 +21229,12 @@ async function handleCoreHttpApiRequest(request, options) {
|
|
|
21121
21229
|
throw error;
|
|
21122
21230
|
}
|
|
21123
21231
|
const executionParams = {
|
|
21124
|
-
type:
|
|
21232
|
+
type: executionType,
|
|
21125
21233
|
path,
|
|
21126
21234
|
args: jsonArgs,
|
|
21127
21235
|
auth: authForExecution,
|
|
21128
21236
|
componentPath,
|
|
21237
|
+
snapshotTimestamp,
|
|
21129
21238
|
request
|
|
21130
21239
|
};
|
|
21131
21240
|
try {
|
|
@@ -21140,7 +21249,7 @@ async function handleCoreHttpApiRequest(request, options) {
|
|
|
21140
21249
|
throw validationError;
|
|
21141
21250
|
}
|
|
21142
21251
|
const result = await options.executeFunction(executionParams);
|
|
21143
|
-
if (options.notifyWrites && (
|
|
21252
|
+
if (options.notifyWrites && (executionType === "mutation" || executionType === "action") && (result.writtenRanges?.length || result.writtenTables?.length)) {
|
|
21144
21253
|
await options.notifyWrites(result.writtenRanges, result.writtenTables ?? writtenTablesFromRanges(result.writtenRanges));
|
|
21145
21254
|
}
|
|
21146
21255
|
return {
|
|
@@ -21191,7 +21300,7 @@ function stripApiVersionPrefix(pathname) {
|
|
|
21191
21300
|
}
|
|
21192
21301
|
function isReservedApiPath(pathname) {
|
|
21193
21302
|
const normalizedPath = stripApiVersionPrefix(pathname);
|
|
21194
|
-
if (normalizedPath === "/api/execute" || normalizedPath === "/api/sync" || normalizedPath === "/api/reset-test-state" || normalizedPath === "/api/query" || normalizedPath === "/api/mutation" || normalizedPath === "/api/action") {
|
|
21303
|
+
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") {
|
|
21195
21304
|
return true;
|
|
21196
21305
|
}
|
|
21197
21306
|
if (normalizedPath === "/api/storage" || normalizedPath.startsWith("/api/storage/")) {
|
|
@@ -21262,7 +21371,7 @@ class HttpHandler {
|
|
|
21262
21371
|
}
|
|
21263
21372
|
};
|
|
21264
21373
|
const coreResult = await handleCoreHttpApiRequest(request, {
|
|
21265
|
-
executeFunction: async ({ type, path, args, auth, componentPath }) => this.adapter.executeUdf(path, args, type, auth, componentPath),
|
|
21374
|
+
executeFunction: async ({ type, path, args, auth, componentPath, snapshotTimestamp }) => this.adapter.executeUdf(path, args, type, auth, componentPath, undefined, snapshotTimestamp),
|
|
21266
21375
|
notifyWrites,
|
|
21267
21376
|
storage: this.docstore && this.blobstore ? {
|
|
21268
21377
|
store: async (blob) => {
|
|
@@ -21281,7 +21390,16 @@ class HttpHandler {
|
|
|
21281
21390
|
return { blob: blob ?? null };
|
|
21282
21391
|
}
|
|
21283
21392
|
} : undefined,
|
|
21284
|
-
corsHeaders
|
|
21393
|
+
corsHeaders,
|
|
21394
|
+
getSnapshotTimestamp: () => {
|
|
21395
|
+
const oracle = this.docstore?.timestampOracle;
|
|
21396
|
+
const oracleTimestamp = typeof oracle?.beginSnapshot === "function" ? oracle.beginSnapshot() : typeof oracle?.getCurrentTimestamp === "function" ? oracle.getCurrentTimestamp() : undefined;
|
|
21397
|
+
const wallClock = BigInt(Date.now());
|
|
21398
|
+
if (typeof oracleTimestamp === "bigint" && oracleTimestamp > wallClock) {
|
|
21399
|
+
return oracleTimestamp;
|
|
21400
|
+
}
|
|
21401
|
+
return wallClock;
|
|
21402
|
+
}
|
|
21285
21403
|
});
|
|
21286
21404
|
if (coreResult?.handled) {
|
|
21287
21405
|
return coreResult.response;
|
|
@@ -21656,6 +21774,18 @@ function encodeServerMessage(message2) {
|
|
|
21656
21774
|
}
|
|
21657
21775
|
}
|
|
21658
21776
|
}
|
|
21777
|
+
// ../core/dist/docstore/index.js
|
|
21778
|
+
init_interface();
|
|
21779
|
+
|
|
21780
|
+
// ../core/dist/docstore/vector-index-registration.js
|
|
21781
|
+
async function getVectorIndexesFromSchema(schemaService) {
|
|
21782
|
+
try {
|
|
21783
|
+
return await schemaService.getAllVectorIndexes();
|
|
21784
|
+
} catch (error) {
|
|
21785
|
+
console.warn("[Vector] Failed to extract vector indexes from schema:", error);
|
|
21786
|
+
return [];
|
|
21787
|
+
}
|
|
21788
|
+
}
|
|
21659
21789
|
// ../core/dist/scheduler/scheduled-function-executor.js
|
|
21660
21790
|
var SCHEDULED_FUNCTIONS_TABLE = "_scheduled_functions";
|
|
21661
21791
|
|
|
@@ -21668,6 +21798,8 @@ class ScheduledFunctionExecutor {
|
|
|
21668
21798
|
logger;
|
|
21669
21799
|
runMutationInTransaction;
|
|
21670
21800
|
tableName;
|
|
21801
|
+
maxConcurrentJobs;
|
|
21802
|
+
scanPageSize;
|
|
21671
21803
|
constructor(options) {
|
|
21672
21804
|
this.docstore = options.docstore;
|
|
21673
21805
|
this.udfExecutor = options.udfExecutor;
|
|
@@ -21677,46 +21809,68 @@ class ScheduledFunctionExecutor {
|
|
|
21677
21809
|
this.logger = options.logger ?? console;
|
|
21678
21810
|
this.runMutationInTransaction = options.runMutationInTransaction;
|
|
21679
21811
|
this.tableName = options.tableName ?? SCHEDULED_FUNCTIONS_TABLE;
|
|
21812
|
+
this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
|
|
21813
|
+
this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
|
|
21680
21814
|
}
|
|
21681
21815
|
async runDueJobs() {
|
|
21682
21816
|
const tableId = stringToHex(this.tableName);
|
|
21683
|
-
const allJobs = await this.docstore.scan(tableId);
|
|
21684
21817
|
const now = this.now();
|
|
21685
|
-
|
|
21686
|
-
|
|
21687
|
-
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
|
|
21691
|
-
|
|
21692
|
-
|
|
21693
|
-
|
|
21694
|
-
|
|
21695
|
-
|
|
21696
|
-
executed: 0,
|
|
21697
|
-
nextScheduledTime: this.computeNextScheduledTime(allJobs)
|
|
21698
|
-
};
|
|
21699
|
-
}
|
|
21700
|
-
await Promise.all(pendingJobs.map((job) => {
|
|
21701
|
-
const jobValue = job.value?.value;
|
|
21702
|
-
if (!jobValue) {
|
|
21703
|
-
throw new Error("Job value unexpectedly missing after filter");
|
|
21818
|
+
let executed = 0;
|
|
21819
|
+
const inFlight = new Set;
|
|
21820
|
+
const schedule = async (jobValue) => {
|
|
21821
|
+
const run = this.executeJob(jobValue, tableId).then(() => {
|
|
21822
|
+
executed += 1;
|
|
21823
|
+
}).finally(() => {
|
|
21824
|
+
inFlight.delete(run);
|
|
21825
|
+
});
|
|
21826
|
+
inFlight.add(run);
|
|
21827
|
+
if (inFlight.size >= this.maxConcurrentJobs) {
|
|
21828
|
+
await Promise.race(inFlight);
|
|
21704
21829
|
}
|
|
21705
|
-
return this.executeJob(jobValue, tableId);
|
|
21706
|
-
}));
|
|
21707
|
-
return {
|
|
21708
|
-
executed: pendingJobs.length,
|
|
21709
|
-
nextScheduledTime: this.computeNextScheduledTime(await this.docstore.scan(tableId))
|
|
21710
21830
|
};
|
|
21831
|
+
await this.forEachScheduledJob(async (jobValue) => {
|
|
21832
|
+
const state = jobValue.state;
|
|
21833
|
+
const scheduledTime = jobValue.scheduledTime;
|
|
21834
|
+
if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
|
|
21835
|
+
return;
|
|
21836
|
+
}
|
|
21837
|
+
if (scheduledTime <= now) {
|
|
21838
|
+
await schedule(jobValue);
|
|
21839
|
+
}
|
|
21840
|
+
});
|
|
21841
|
+
await Promise.all(inFlight);
|
|
21842
|
+
return { executed, nextScheduledTime: await this.getNextScheduledTime() };
|
|
21711
21843
|
}
|
|
21712
21844
|
async getNextScheduledTime() {
|
|
21713
|
-
|
|
21714
|
-
|
|
21715
|
-
|
|
21845
|
+
let nextScheduledTime = null;
|
|
21846
|
+
await this.forEachScheduledJob(async (jobValue) => {
|
|
21847
|
+
const state = jobValue.state;
|
|
21848
|
+
const scheduledTime = jobValue.scheduledTime;
|
|
21849
|
+
if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
|
|
21850
|
+
return;
|
|
21851
|
+
}
|
|
21852
|
+
if (nextScheduledTime === null || scheduledTime < nextScheduledTime) {
|
|
21853
|
+
nextScheduledTime = scheduledTime;
|
|
21854
|
+
}
|
|
21855
|
+
});
|
|
21856
|
+
return nextScheduledTime;
|
|
21716
21857
|
}
|
|
21717
|
-
|
|
21718
|
-
const
|
|
21719
|
-
|
|
21858
|
+
async forEachScheduledJob(visitor) {
|
|
21859
|
+
const tableId = stringToHex(this.tableName);
|
|
21860
|
+
let cursor2 = null;
|
|
21861
|
+
while (true) {
|
|
21862
|
+
const page = await this.docstore.scanPaginated(tableId, cursor2, this.scanPageSize, Order.Asc);
|
|
21863
|
+
for (const doc of page.documents) {
|
|
21864
|
+
const value = doc.value?.value;
|
|
21865
|
+
if (value && typeof value === "object") {
|
|
21866
|
+
await visitor(value);
|
|
21867
|
+
}
|
|
21868
|
+
}
|
|
21869
|
+
if (!page.hasMore || !page.nextCursor) {
|
|
21870
|
+
break;
|
|
21871
|
+
}
|
|
21872
|
+
cursor2 = page.nextCursor;
|
|
21873
|
+
}
|
|
21720
21874
|
}
|
|
21721
21875
|
async executeJob(jobValue, tableId) {
|
|
21722
21876
|
const jobId = jobValue?._id;
|
|
@@ -21849,6 +22003,8 @@ class CronExecutor {
|
|
|
21849
22003
|
allocateTimestamp;
|
|
21850
22004
|
now;
|
|
21851
22005
|
logger;
|
|
22006
|
+
maxConcurrentJobs;
|
|
22007
|
+
scanPageSize;
|
|
21852
22008
|
constructor(options) {
|
|
21853
22009
|
this.docstore = options.docstore;
|
|
21854
22010
|
this.udfExecutor = options.udfExecutor;
|
|
@@ -21856,6 +22012,8 @@ class CronExecutor {
|
|
|
21856
22012
|
this.allocateTimestamp = options.allocateTimestamp;
|
|
21857
22013
|
this.now = options.now ?? (() => Date.now());
|
|
21858
22014
|
this.logger = options.logger ?? console;
|
|
22015
|
+
this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
|
|
22016
|
+
this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
|
|
21859
22017
|
}
|
|
21860
22018
|
async syncCronSpecs(cronSpecs) {
|
|
21861
22019
|
const tableId = stringToHex(CRONS_TABLE);
|
|
@@ -21941,33 +22099,58 @@ class CronExecutor {
|
|
|
21941
22099
|
}
|
|
21942
22100
|
async runDueJobs() {
|
|
21943
22101
|
const tableId = stringToHex(CRONS_TABLE);
|
|
21944
|
-
const allJobs = await this.docstore.scan(tableId);
|
|
21945
22102
|
const now = this.now();
|
|
21946
|
-
|
|
21947
|
-
|
|
21948
|
-
|
|
21949
|
-
|
|
21950
|
-
|
|
21951
|
-
|
|
21952
|
-
|
|
21953
|
-
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
return {
|
|
21959
|
-
executed: dueJobs.length,
|
|
21960
|
-
nextScheduledTime: this.computeNextScheduledTime(updatedJobs)
|
|
22103
|
+
let executed = 0;
|
|
22104
|
+
const inFlight = new Set;
|
|
22105
|
+
const schedule = async (job) => {
|
|
22106
|
+
const run = this.executeJob(job, tableId).then(() => {
|
|
22107
|
+
executed += 1;
|
|
22108
|
+
}).finally(() => {
|
|
22109
|
+
inFlight.delete(run);
|
|
22110
|
+
});
|
|
22111
|
+
inFlight.add(run);
|
|
22112
|
+
if (inFlight.size >= this.maxConcurrentJobs) {
|
|
22113
|
+
await Promise.race(inFlight);
|
|
22114
|
+
}
|
|
21961
22115
|
};
|
|
22116
|
+
await this.forEachCronJob(async (job) => {
|
|
22117
|
+
const value = job.value?.value;
|
|
22118
|
+
if (!value || typeof value.nextRun !== "number") {
|
|
22119
|
+
return;
|
|
22120
|
+
}
|
|
22121
|
+
if (value.nextRun <= now) {
|
|
22122
|
+
await schedule(job);
|
|
22123
|
+
}
|
|
22124
|
+
});
|
|
22125
|
+
await Promise.all(inFlight);
|
|
22126
|
+
return { executed, nextScheduledTime: await this.getNextScheduledTime() };
|
|
21962
22127
|
}
|
|
21963
22128
|
async getNextScheduledTime() {
|
|
21964
|
-
|
|
21965
|
-
|
|
21966
|
-
|
|
22129
|
+
let nextScheduledTime = null;
|
|
22130
|
+
await this.forEachCronJob(async (job) => {
|
|
22131
|
+
const nextRun = job.value?.value?.nextRun;
|
|
22132
|
+
if (typeof nextRun !== "number") {
|
|
22133
|
+
return;
|
|
22134
|
+
}
|
|
22135
|
+
if (nextScheduledTime === null || nextRun < nextScheduledTime) {
|
|
22136
|
+
nextScheduledTime = nextRun;
|
|
22137
|
+
}
|
|
22138
|
+
});
|
|
22139
|
+
return nextScheduledTime;
|
|
21967
22140
|
}
|
|
21968
|
-
|
|
21969
|
-
const
|
|
21970
|
-
|
|
22141
|
+
async forEachCronJob(visitor) {
|
|
22142
|
+
const tableId = stringToHex(CRONS_TABLE);
|
|
22143
|
+
let cursor2 = null;
|
|
22144
|
+
while (true) {
|
|
22145
|
+
const page = await this.docstore.scanPaginated(tableId, cursor2, this.scanPageSize, Order.Asc);
|
|
22146
|
+
for (const job of page.documents) {
|
|
22147
|
+
await visitor(job);
|
|
22148
|
+
}
|
|
22149
|
+
if (!page.hasMore || !page.nextCursor) {
|
|
22150
|
+
break;
|
|
22151
|
+
}
|
|
22152
|
+
cursor2 = page.nextCursor;
|
|
22153
|
+
}
|
|
21971
22154
|
}
|
|
21972
22155
|
async executeJob(job, tableId) {
|
|
21973
22156
|
const value = job.value?.value;
|
|
@@ -22187,18 +22370,6 @@ var import_sender = __toESM(require_sender(), 1);
|
|
|
22187
22370
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
22188
22371
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
22189
22372
|
|
|
22190
|
-
// ../core/dist/docstore/index.js
|
|
22191
|
-
init_interface();
|
|
22192
|
-
|
|
22193
|
-
// ../core/dist/docstore/vector-index-registration.js
|
|
22194
|
-
async function getVectorIndexesFromSchema(schemaService) {
|
|
22195
|
-
try {
|
|
22196
|
-
return await schemaService.getAllVectorIndexes();
|
|
22197
|
-
} catch (error) {
|
|
22198
|
-
console.warn("[Vector] Failed to extract vector indexes from schema:", error);
|
|
22199
|
-
return [];
|
|
22200
|
-
}
|
|
22201
|
-
}
|
|
22202
22373
|
// ../runtime-base/dist/sync/index.js
|
|
22203
22374
|
import { AsyncLocalStorage as AsyncLocalStorage32 } from "node:async_hooks";
|
|
22204
22375
|
import { AsyncLocalStorage as AsyncLocalStorage5 } from "node:async_hooks";
|
|
@@ -28047,6 +28218,8 @@ class SystemAuthError2 extends Error {
|
|
|
28047
28218
|
}
|
|
28048
28219
|
}
|
|
28049
28220
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS2 = 60;
|
|
28221
|
+
var DEFAULT_JWKS_CACHE_TTL_MS2 = 5 * 60 * 1000;
|
|
28222
|
+
var MAX_JWKS_CACHE_TTL_MS2 = 24 * 60 * 60 * 1000;
|
|
28050
28223
|
var JWKS_CACHE2 = new Map;
|
|
28051
28224
|
var defaultValidationConfig2;
|
|
28052
28225
|
var adminAuthConfig2;
|
|
@@ -28142,7 +28315,8 @@ function resolveJwtValidationConfigFromEnv2(env) {
|
|
|
28142
28315
|
const secret = getEnvValue2("AUTH_SECRET", env) ?? getEnvValue2("CONCAVE_JWT_SECRET", env) ?? getEnvValue2("JWT_SECRET", env);
|
|
28143
28316
|
const skipVerification = parseBoolean2(getEnvValue2("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean2(getEnvValue2("CONCAVE_JWT_SKIP_VERIFICATION", env));
|
|
28144
28317
|
const clockTolerance = parseNumber2(getEnvValue2("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber2(getEnvValue2("CONCAVE_JWT_CLOCK_TOLERANCE", env));
|
|
28145
|
-
|
|
28318
|
+
const jwksCacheTtlMs = parseNumber2(getEnvValue2("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber2(getEnvValue2("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
|
|
28319
|
+
if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
|
|
28146
28320
|
return;
|
|
28147
28321
|
}
|
|
28148
28322
|
return {
|
|
@@ -28151,7 +28325,8 @@ function resolveJwtValidationConfigFromEnv2(env) {
|
|
|
28151
28325
|
audience,
|
|
28152
28326
|
secret,
|
|
28153
28327
|
skipVerification,
|
|
28154
|
-
clockTolerance
|
|
28328
|
+
clockTolerance,
|
|
28329
|
+
jwksCacheTtlMs
|
|
28155
28330
|
};
|
|
28156
28331
|
}
|
|
28157
28332
|
function normalizeList2(value) {
|
|
@@ -28195,15 +28370,33 @@ function validateClaims2(claims, config) {
|
|
|
28195
28370
|
throw new JWTValidationError2("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
|
|
28196
28371
|
}
|
|
28197
28372
|
}
|
|
28198
|
-
function getRemoteJwks2(jwksUrl) {
|
|
28373
|
+
function getRemoteJwks2(jwksUrl, config) {
|
|
28374
|
+
const now = Date.now();
|
|
28199
28375
|
const cached = JWKS_CACHE2.get(jwksUrl);
|
|
28376
|
+
if (cached && cached.expiresAtMs > now) {
|
|
28377
|
+
return cached.resolver;
|
|
28378
|
+
}
|
|
28200
28379
|
if (cached) {
|
|
28201
|
-
|
|
28380
|
+
JWKS_CACHE2.delete(jwksUrl);
|
|
28202
28381
|
}
|
|
28203
28382
|
const jwks = createRemoteJWKSet2(new URL(jwksUrl));
|
|
28204
|
-
|
|
28383
|
+
const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig2?.jwksCacheTtlMs;
|
|
28384
|
+
const ttlMs = resolveJwksCacheTtlMs2(configuredTtl);
|
|
28385
|
+
JWKS_CACHE2.set(jwksUrl, {
|
|
28386
|
+
resolver: jwks,
|
|
28387
|
+
expiresAtMs: now + ttlMs
|
|
28388
|
+
});
|
|
28205
28389
|
return jwks;
|
|
28206
28390
|
}
|
|
28391
|
+
function resolveJwksCacheTtlMs2(configuredTtl) {
|
|
28392
|
+
if (configuredTtl === undefined) {
|
|
28393
|
+
return DEFAULT_JWKS_CACHE_TTL_MS2;
|
|
28394
|
+
}
|
|
28395
|
+
if (!Number.isFinite(configuredTtl)) {
|
|
28396
|
+
return DEFAULT_JWKS_CACHE_TTL_MS2;
|
|
28397
|
+
}
|
|
28398
|
+
return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS2, Math.floor(configuredTtl)));
|
|
28399
|
+
}
|
|
28207
28400
|
function decodeJwtUnsafe2(token) {
|
|
28208
28401
|
if (!token)
|
|
28209
28402
|
return null;
|
|
@@ -28236,7 +28429,7 @@ async function verifyJwt2(token, config) {
|
|
|
28236
28429
|
const key = new TextEncoder().encode(effectiveConfig.secret);
|
|
28237
28430
|
({ payload } = await jwtVerify2(token, key, options));
|
|
28238
28431
|
} else {
|
|
28239
|
-
({ payload } = await jwtVerify2(token, getRemoteJwks2(effectiveConfig.jwksUrl), options));
|
|
28432
|
+
({ payload } = await jwtVerify2(token, getRemoteJwks2(effectiveConfig.jwksUrl, effectiveConfig), options));
|
|
28240
28433
|
}
|
|
28241
28434
|
const claims = payload;
|
|
28242
28435
|
validateClaims2(claims, effectiveConfig);
|
|
@@ -28291,7 +28484,7 @@ class UdfExecutionAdapter2 {
|
|
|
28291
28484
|
this.executor = executor2;
|
|
28292
28485
|
this.callType = callType;
|
|
28293
28486
|
}
|
|
28294
|
-
async executeUdf(path, jsonArgs, type, auth2, componentPath, requestId) {
|
|
28487
|
+
async executeUdf(path, jsonArgs, type, auth2, componentPath, requestId, snapshotTimestamp) {
|
|
28295
28488
|
const convexArgs = convertClientArgs2(jsonArgs);
|
|
28296
28489
|
const target = normalizeExecutionTarget2(path, componentPath);
|
|
28297
28490
|
let authContext22;
|
|
@@ -28329,7 +28522,7 @@ class UdfExecutionAdapter2 {
|
|
|
28329
28522
|
return runWithAuth2(userIdentity, async () => {
|
|
28330
28523
|
const executeWithContext = this.callType === "client" ? runAsClientCall2 : runAsServerCall2;
|
|
28331
28524
|
return executeWithContext(async () => {
|
|
28332
|
-
return await this.executor.execute(target.path, convexArgs, type, authContext22 ?? userIdentity, normalizeComponentPath3(target.componentPath), requestId);
|
|
28525
|
+
return await this.executor.execute(target.path, convexArgs, type, authContext22 ?? userIdentity, normalizeComponentPath3(target.componentPath), requestId, snapshotTimestamp);
|
|
28333
28526
|
});
|
|
28334
28527
|
});
|
|
28335
28528
|
}
|
|
@@ -30603,6 +30796,11 @@ var WEBSOCKET_READY_STATE_OPEN = 1;
|
|
|
30603
30796
|
var BACKPRESSURE_HIGH_WATER_MARK = 100;
|
|
30604
30797
|
var BACKPRESSURE_BUFFER_LIMIT = 1024 * 1024;
|
|
30605
30798
|
var SLOW_CLIENT_TIMEOUT_MS = 30000;
|
|
30799
|
+
var DEFAULT_RATE_LIMIT_WINDOW_MS = 5000;
|
|
30800
|
+
var DEFAULT_MAX_MESSAGES_PER_WINDOW = 1000;
|
|
30801
|
+
var DEFAULT_OPERATION_TIMEOUT_MS = 15000;
|
|
30802
|
+
var DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION = 1000;
|
|
30803
|
+
var RATE_LIMIT_HARD_MULTIPLIER = 5;
|
|
30606
30804
|
|
|
30607
30805
|
class SyncSession {
|
|
30608
30806
|
websocket;
|
|
@@ -30631,15 +30829,25 @@ class SyncSession {
|
|
|
30631
30829
|
class SyncProtocolHandler {
|
|
30632
30830
|
udfExecutor;
|
|
30633
30831
|
sessions = new Map;
|
|
30832
|
+
rateLimitStates = new Map;
|
|
30634
30833
|
subscriptionManager;
|
|
30635
30834
|
instanceName;
|
|
30636
30835
|
backpressureController;
|
|
30637
30836
|
heartbeatController;
|
|
30638
30837
|
isDev;
|
|
30838
|
+
maxMessagesPerWindow;
|
|
30839
|
+
rateLimitWindowMs;
|
|
30840
|
+
operationTimeoutMs;
|
|
30841
|
+
maxActiveQueriesPerSession;
|
|
30639
30842
|
constructor(instanceName, udfExecutor, options) {
|
|
30640
30843
|
this.udfExecutor = udfExecutor;
|
|
30641
30844
|
this.instanceName = instanceName;
|
|
30642
30845
|
this.isDev = options?.isDev ?? true;
|
|
30846
|
+
this.maxMessagesPerWindow = Math.max(1, options?.maxMessagesPerWindow ?? DEFAULT_MAX_MESSAGES_PER_WINDOW);
|
|
30847
|
+
this.rateLimitWindowMs = Math.max(1, options?.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS);
|
|
30848
|
+
const configuredOperationTimeout = options?.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
30849
|
+
this.operationTimeoutMs = Number.isFinite(configuredOperationTimeout) ? Math.max(0, Math.floor(configuredOperationTimeout)) : DEFAULT_OPERATION_TIMEOUT_MS;
|
|
30850
|
+
this.maxActiveQueriesPerSession = Math.max(1, options?.maxActiveQueriesPerSession ?? DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION);
|
|
30643
30851
|
this.subscriptionManager = new SubscriptionManager2;
|
|
30644
30852
|
this.backpressureController = new SessionBackpressureController({
|
|
30645
30853
|
websocketReadyStateOpen: WEBSOCKET_READY_STATE_OPEN,
|
|
@@ -30657,6 +30865,10 @@ class SyncProtocolHandler {
|
|
|
30657
30865
|
createSession(sessionId, websocket) {
|
|
30658
30866
|
const session = new SyncSession(websocket);
|
|
30659
30867
|
this.sessions.set(sessionId, session);
|
|
30868
|
+
this.rateLimitStates.set(sessionId, {
|
|
30869
|
+
windowStartedAt: Date.now(),
|
|
30870
|
+
messagesInWindow: 0
|
|
30871
|
+
});
|
|
30660
30872
|
return session;
|
|
30661
30873
|
}
|
|
30662
30874
|
getSession(sessionId) {
|
|
@@ -30671,6 +30883,7 @@ class SyncProtocolHandler {
|
|
|
30671
30883
|
session.isDraining = false;
|
|
30672
30884
|
this.subscriptionManager.unsubscribeAll(sessionId);
|
|
30673
30885
|
this.sessions.delete(sessionId);
|
|
30886
|
+
this.rateLimitStates.delete(sessionId);
|
|
30674
30887
|
}
|
|
30675
30888
|
}
|
|
30676
30889
|
updateSessionId(oldSessionId, newSessionId) {
|
|
@@ -30678,6 +30891,11 @@ class SyncProtocolHandler {
|
|
|
30678
30891
|
if (session) {
|
|
30679
30892
|
this.sessions.delete(oldSessionId);
|
|
30680
30893
|
this.sessions.set(newSessionId, session);
|
|
30894
|
+
const rateLimitState = this.rateLimitStates.get(oldSessionId);
|
|
30895
|
+
if (rateLimitState) {
|
|
30896
|
+
this.rateLimitStates.delete(oldSessionId);
|
|
30897
|
+
this.rateLimitStates.set(newSessionId, rateLimitState);
|
|
30898
|
+
}
|
|
30681
30899
|
this.subscriptionManager.updateSessionId(oldSessionId, newSessionId);
|
|
30682
30900
|
}
|
|
30683
30901
|
}
|
|
@@ -30686,6 +30904,29 @@ class SyncProtocolHandler {
|
|
|
30686
30904
|
if (!session && message22.type !== "Connect") {
|
|
30687
30905
|
throw new Error("Session not found");
|
|
30688
30906
|
}
|
|
30907
|
+
if (session) {
|
|
30908
|
+
const rateLimitDecision = this.consumeRateLimit(sessionId);
|
|
30909
|
+
if (rateLimitDecision === "reject") {
|
|
30910
|
+
return [
|
|
30911
|
+
{
|
|
30912
|
+
type: "FatalError",
|
|
30913
|
+
error: "Rate limit exceeded, retry shortly"
|
|
30914
|
+
}
|
|
30915
|
+
];
|
|
30916
|
+
}
|
|
30917
|
+
if (rateLimitDecision === "close") {
|
|
30918
|
+
try {
|
|
30919
|
+
session.websocket.close(1013, "Rate limit exceeded");
|
|
30920
|
+
} catch {}
|
|
30921
|
+
this.destroySession(sessionId);
|
|
30922
|
+
return [
|
|
30923
|
+
{
|
|
30924
|
+
type: "FatalError",
|
|
30925
|
+
error: "Rate limit exceeded"
|
|
30926
|
+
}
|
|
30927
|
+
];
|
|
30928
|
+
}
|
|
30929
|
+
}
|
|
30689
30930
|
switch (message22.type) {
|
|
30690
30931
|
case "Connect":
|
|
30691
30932
|
return this.handleConnect(sessionId, message22);
|
|
@@ -30742,6 +30983,15 @@ class SyncProtocolHandler {
|
|
|
30742
30983
|
return [fatalError];
|
|
30743
30984
|
}
|
|
30744
30985
|
const startVersion = makeStateVersion(session.querySetVersion, session.identityVersion, session.timestamp);
|
|
30986
|
+
const projectedActiveQueryCount = this.computeProjectedActiveQueryCount(session, message22);
|
|
30987
|
+
if (projectedActiveQueryCount > this.maxActiveQueriesPerSession) {
|
|
30988
|
+
return [
|
|
30989
|
+
{
|
|
30990
|
+
type: "FatalError",
|
|
30991
|
+
error: `Too many active queries: ${projectedActiveQueryCount} exceeds limit ${this.maxActiveQueriesPerSession}`
|
|
30992
|
+
}
|
|
30993
|
+
];
|
|
30994
|
+
}
|
|
30745
30995
|
session.querySetVersion = message22.newVersion;
|
|
30746
30996
|
const modifications = [];
|
|
30747
30997
|
for (const mod of message22.modifications) {
|
|
@@ -31065,7 +31315,54 @@ class SyncProtocolHandler {
|
|
|
31065
31315
|
}, () => {
|
|
31066
31316
|
return;
|
|
31067
31317
|
});
|
|
31068
|
-
return run;
|
|
31318
|
+
return this.withOperationTimeout(run);
|
|
31319
|
+
}
|
|
31320
|
+
withOperationTimeout(promise) {
|
|
31321
|
+
if (this.operationTimeoutMs <= 0) {
|
|
31322
|
+
return promise;
|
|
31323
|
+
}
|
|
31324
|
+
let timeoutHandle;
|
|
31325
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
31326
|
+
timeoutHandle = setTimeout(() => {
|
|
31327
|
+
reject(new Error(`Sync operation timed out after ${this.operationTimeoutMs}ms`));
|
|
31328
|
+
}, this.operationTimeoutMs);
|
|
31329
|
+
});
|
|
31330
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
31331
|
+
if (timeoutHandle) {
|
|
31332
|
+
clearTimeout(timeoutHandle);
|
|
31333
|
+
}
|
|
31334
|
+
});
|
|
31335
|
+
}
|
|
31336
|
+
consumeRateLimit(sessionId) {
|
|
31337
|
+
const state = this.rateLimitStates.get(sessionId);
|
|
31338
|
+
if (!state) {
|
|
31339
|
+
return "allow";
|
|
31340
|
+
}
|
|
31341
|
+
const now = Date.now();
|
|
31342
|
+
if (now - state.windowStartedAt >= this.rateLimitWindowMs) {
|
|
31343
|
+
state.windowStartedAt = now;
|
|
31344
|
+
state.messagesInWindow = 0;
|
|
31345
|
+
}
|
|
31346
|
+
state.messagesInWindow += 1;
|
|
31347
|
+
if (state.messagesInWindow <= this.maxMessagesPerWindow) {
|
|
31348
|
+
return "allow";
|
|
31349
|
+
}
|
|
31350
|
+
const hardLimit = Math.max(this.maxMessagesPerWindow + 1, this.maxMessagesPerWindow * RATE_LIMIT_HARD_MULTIPLIER);
|
|
31351
|
+
if (state.messagesInWindow >= hardLimit) {
|
|
31352
|
+
return "close";
|
|
31353
|
+
}
|
|
31354
|
+
return "reject";
|
|
31355
|
+
}
|
|
31356
|
+
computeProjectedActiveQueryCount(session, message22) {
|
|
31357
|
+
const projected = new Set(session.activeQueries.keys());
|
|
31358
|
+
for (const mod of message22.modifications) {
|
|
31359
|
+
if (mod.type === "Add") {
|
|
31360
|
+
projected.add(mod.queryId);
|
|
31361
|
+
} else if (mod.type === "Remove") {
|
|
31362
|
+
projected.delete(mod.queryId);
|
|
31363
|
+
}
|
|
31364
|
+
}
|
|
31365
|
+
return projected.size;
|
|
31069
31366
|
}
|
|
31070
31367
|
sendPing(session) {
|
|
31071
31368
|
if (!session.websocket || session.websocket.readyState !== WEBSOCKET_READY_STATE_OPEN) {
|
|
@@ -37311,6 +37608,8 @@ function decodeJwtClaimsToken3(token) {
|
|
|
37311
37608
|
return null;
|
|
37312
37609
|
}
|
|
37313
37610
|
}
|
|
37611
|
+
var DEFAULT_JWKS_CACHE_TTL_MS3 = 5 * 60 * 1000;
|
|
37612
|
+
var MAX_JWKS_CACHE_TTL_MS3 = 24 * 60 * 60 * 1000;
|
|
37314
37613
|
var JWKS_CACHE3 = new Map;
|
|
37315
37614
|
function decodeJwtUnsafe3(token) {
|
|
37316
37615
|
if (!token)
|
|
@@ -38128,6 +38427,8 @@ class ScheduledFunctionExecutor2 {
|
|
|
38128
38427
|
logger;
|
|
38129
38428
|
runMutationInTransaction;
|
|
38130
38429
|
tableName;
|
|
38430
|
+
maxConcurrentJobs;
|
|
38431
|
+
scanPageSize;
|
|
38131
38432
|
constructor(options) {
|
|
38132
38433
|
this.docstore = options.docstore;
|
|
38133
38434
|
this.udfExecutor = options.udfExecutor;
|
|
@@ -38137,46 +38438,68 @@ class ScheduledFunctionExecutor2 {
|
|
|
38137
38438
|
this.logger = options.logger ?? console;
|
|
38138
38439
|
this.runMutationInTransaction = options.runMutationInTransaction;
|
|
38139
38440
|
this.tableName = options.tableName ?? SCHEDULED_FUNCTIONS_TABLE2;
|
|
38441
|
+
this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
|
|
38442
|
+
this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
|
|
38140
38443
|
}
|
|
38141
38444
|
async runDueJobs() {
|
|
38142
38445
|
const tableId = stringToHex4(this.tableName);
|
|
38143
|
-
const allJobs = await this.docstore.scan(tableId);
|
|
38144
38446
|
const now = this.now();
|
|
38145
|
-
|
|
38146
|
-
|
|
38147
|
-
|
|
38148
|
-
|
|
38149
|
-
|
|
38150
|
-
|
|
38151
|
-
|
|
38152
|
-
|
|
38153
|
-
|
|
38154
|
-
|
|
38155
|
-
|
|
38156
|
-
executed: 0,
|
|
38157
|
-
nextScheduledTime: this.computeNextScheduledTime(allJobs)
|
|
38158
|
-
};
|
|
38159
|
-
}
|
|
38160
|
-
await Promise.all(pendingJobs.map((job) => {
|
|
38161
|
-
const jobValue = job.value?.value;
|
|
38162
|
-
if (!jobValue) {
|
|
38163
|
-
throw new Error("Job value unexpectedly missing after filter");
|
|
38447
|
+
let executed = 0;
|
|
38448
|
+
const inFlight = new Set;
|
|
38449
|
+
const schedule = async (jobValue) => {
|
|
38450
|
+
const run = this.executeJob(jobValue, tableId).then(() => {
|
|
38451
|
+
executed += 1;
|
|
38452
|
+
}).finally(() => {
|
|
38453
|
+
inFlight.delete(run);
|
|
38454
|
+
});
|
|
38455
|
+
inFlight.add(run);
|
|
38456
|
+
if (inFlight.size >= this.maxConcurrentJobs) {
|
|
38457
|
+
await Promise.race(inFlight);
|
|
38164
38458
|
}
|
|
38165
|
-
return this.executeJob(jobValue, tableId);
|
|
38166
|
-
}));
|
|
38167
|
-
return {
|
|
38168
|
-
executed: pendingJobs.length,
|
|
38169
|
-
nextScheduledTime: this.computeNextScheduledTime(await this.docstore.scan(tableId))
|
|
38170
38459
|
};
|
|
38460
|
+
await this.forEachScheduledJob(async (jobValue) => {
|
|
38461
|
+
const state = jobValue.state;
|
|
38462
|
+
const scheduledTime = jobValue.scheduledTime;
|
|
38463
|
+
if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
|
|
38464
|
+
return;
|
|
38465
|
+
}
|
|
38466
|
+
if (scheduledTime <= now) {
|
|
38467
|
+
await schedule(jobValue);
|
|
38468
|
+
}
|
|
38469
|
+
});
|
|
38470
|
+
await Promise.all(inFlight);
|
|
38471
|
+
return { executed, nextScheduledTime: await this.getNextScheduledTime() };
|
|
38171
38472
|
}
|
|
38172
38473
|
async getNextScheduledTime() {
|
|
38173
|
-
|
|
38174
|
-
|
|
38175
|
-
|
|
38474
|
+
let nextScheduledTime = null;
|
|
38475
|
+
await this.forEachScheduledJob(async (jobValue) => {
|
|
38476
|
+
const state = jobValue.state;
|
|
38477
|
+
const scheduledTime = jobValue.scheduledTime;
|
|
38478
|
+
if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
|
|
38479
|
+
return;
|
|
38480
|
+
}
|
|
38481
|
+
if (nextScheduledTime === null || scheduledTime < nextScheduledTime) {
|
|
38482
|
+
nextScheduledTime = scheduledTime;
|
|
38483
|
+
}
|
|
38484
|
+
});
|
|
38485
|
+
return nextScheduledTime;
|
|
38176
38486
|
}
|
|
38177
|
-
|
|
38178
|
-
const
|
|
38179
|
-
|
|
38487
|
+
async forEachScheduledJob(visitor) {
|
|
38488
|
+
const tableId = stringToHex4(this.tableName);
|
|
38489
|
+
let cursor22 = null;
|
|
38490
|
+
while (true) {
|
|
38491
|
+
const page = await this.docstore.scanPaginated(tableId, cursor22, this.scanPageSize, Order3.Asc);
|
|
38492
|
+
for (const doc of page.documents) {
|
|
38493
|
+
const value = doc.value?.value;
|
|
38494
|
+
if (value && typeof value === "object") {
|
|
38495
|
+
await visitor(value);
|
|
38496
|
+
}
|
|
38497
|
+
}
|
|
38498
|
+
if (!page.hasMore || !page.nextCursor) {
|
|
38499
|
+
break;
|
|
38500
|
+
}
|
|
38501
|
+
cursor22 = page.nextCursor;
|
|
38502
|
+
}
|
|
38180
38503
|
}
|
|
38181
38504
|
async executeJob(jobValue, tableId) {
|
|
38182
38505
|
const jobId = jobValue?._id;
|
|
@@ -38308,6 +38631,8 @@ class CronExecutor2 {
|
|
|
38308
38631
|
allocateTimestamp;
|
|
38309
38632
|
now;
|
|
38310
38633
|
logger;
|
|
38634
|
+
maxConcurrentJobs;
|
|
38635
|
+
scanPageSize;
|
|
38311
38636
|
constructor(options) {
|
|
38312
38637
|
this.docstore = options.docstore;
|
|
38313
38638
|
this.udfExecutor = options.udfExecutor;
|
|
@@ -38315,6 +38640,8 @@ class CronExecutor2 {
|
|
|
38315
38640
|
this.allocateTimestamp = options.allocateTimestamp;
|
|
38316
38641
|
this.now = options.now ?? (() => Date.now());
|
|
38317
38642
|
this.logger = options.logger ?? console;
|
|
38643
|
+
this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
|
|
38644
|
+
this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
|
|
38318
38645
|
}
|
|
38319
38646
|
async syncCronSpecs(cronSpecs) {
|
|
38320
38647
|
const tableId = stringToHex4(CRONS_TABLE2);
|
|
@@ -38400,33 +38727,58 @@ class CronExecutor2 {
|
|
|
38400
38727
|
}
|
|
38401
38728
|
async runDueJobs() {
|
|
38402
38729
|
const tableId = stringToHex4(CRONS_TABLE2);
|
|
38403
|
-
const allJobs = await this.docstore.scan(tableId);
|
|
38404
38730
|
const now = this.now();
|
|
38405
|
-
|
|
38406
|
-
|
|
38407
|
-
|
|
38408
|
-
|
|
38409
|
-
|
|
38410
|
-
|
|
38411
|
-
|
|
38412
|
-
|
|
38413
|
-
|
|
38414
|
-
|
|
38415
|
-
|
|
38416
|
-
|
|
38417
|
-
return {
|
|
38418
|
-
executed: dueJobs.length,
|
|
38419
|
-
nextScheduledTime: this.computeNextScheduledTime(updatedJobs)
|
|
38731
|
+
let executed = 0;
|
|
38732
|
+
const inFlight = new Set;
|
|
38733
|
+
const schedule = async (job) => {
|
|
38734
|
+
const run = this.executeJob(job, tableId).then(() => {
|
|
38735
|
+
executed += 1;
|
|
38736
|
+
}).finally(() => {
|
|
38737
|
+
inFlight.delete(run);
|
|
38738
|
+
});
|
|
38739
|
+
inFlight.add(run);
|
|
38740
|
+
if (inFlight.size >= this.maxConcurrentJobs) {
|
|
38741
|
+
await Promise.race(inFlight);
|
|
38742
|
+
}
|
|
38420
38743
|
};
|
|
38744
|
+
await this.forEachCronJob(async (job) => {
|
|
38745
|
+
const value = job.value?.value;
|
|
38746
|
+
if (!value || typeof value.nextRun !== "number") {
|
|
38747
|
+
return;
|
|
38748
|
+
}
|
|
38749
|
+
if (value.nextRun <= now) {
|
|
38750
|
+
await schedule(job);
|
|
38751
|
+
}
|
|
38752
|
+
});
|
|
38753
|
+
await Promise.all(inFlight);
|
|
38754
|
+
return { executed, nextScheduledTime: await this.getNextScheduledTime() };
|
|
38421
38755
|
}
|
|
38422
38756
|
async getNextScheduledTime() {
|
|
38423
|
-
|
|
38424
|
-
|
|
38425
|
-
|
|
38757
|
+
let nextScheduledTime = null;
|
|
38758
|
+
await this.forEachCronJob(async (job) => {
|
|
38759
|
+
const nextRun = job.value?.value?.nextRun;
|
|
38760
|
+
if (typeof nextRun !== "number") {
|
|
38761
|
+
return;
|
|
38762
|
+
}
|
|
38763
|
+
if (nextScheduledTime === null || nextRun < nextScheduledTime) {
|
|
38764
|
+
nextScheduledTime = nextRun;
|
|
38765
|
+
}
|
|
38766
|
+
});
|
|
38767
|
+
return nextScheduledTime;
|
|
38426
38768
|
}
|
|
38427
|
-
|
|
38428
|
-
const
|
|
38429
|
-
|
|
38769
|
+
async forEachCronJob(visitor) {
|
|
38770
|
+
const tableId = stringToHex4(CRONS_TABLE2);
|
|
38771
|
+
let cursor22 = null;
|
|
38772
|
+
while (true) {
|
|
38773
|
+
const page = await this.docstore.scanPaginated(tableId, cursor22, this.scanPageSize, Order3.Asc);
|
|
38774
|
+
for (const job of page.documents) {
|
|
38775
|
+
await visitor(job);
|
|
38776
|
+
}
|
|
38777
|
+
if (!page.hasMore || !page.nextCursor) {
|
|
38778
|
+
break;
|
|
38779
|
+
}
|
|
38780
|
+
cursor22 = page.nextCursor;
|
|
38781
|
+
}
|
|
38430
38782
|
}
|
|
38431
38783
|
async executeJob(job, tableId) {
|
|
38432
38784
|
const value = job.value?.value;
|
|
@@ -46034,6 +46386,8 @@ class SystemAuthError3 extends Error {
|
|
|
46034
46386
|
}
|
|
46035
46387
|
}
|
|
46036
46388
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS3 = 60;
|
|
46389
|
+
var DEFAULT_JWKS_CACHE_TTL_MS4 = 5 * 60 * 1000;
|
|
46390
|
+
var MAX_JWKS_CACHE_TTL_MS4 = 24 * 60 * 60 * 1000;
|
|
46037
46391
|
var JWKS_CACHE4 = new Map;
|
|
46038
46392
|
var defaultValidationConfig3;
|
|
46039
46393
|
var adminAuthConfig3;
|
|
@@ -46129,7 +46483,8 @@ function resolveJwtValidationConfigFromEnv3(env) {
|
|
|
46129
46483
|
const secret = getEnvValue3("AUTH_SECRET", env) ?? getEnvValue3("CONCAVE_JWT_SECRET", env) ?? getEnvValue3("JWT_SECRET", env);
|
|
46130
46484
|
const skipVerification = parseBoolean3(getEnvValue3("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean3(getEnvValue3("CONCAVE_JWT_SKIP_VERIFICATION", env));
|
|
46131
46485
|
const clockTolerance = parseNumber3(getEnvValue3("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber3(getEnvValue3("CONCAVE_JWT_CLOCK_TOLERANCE", env));
|
|
46132
|
-
|
|
46486
|
+
const jwksCacheTtlMs = parseNumber3(getEnvValue3("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber3(getEnvValue3("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
|
|
46487
|
+
if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
|
|
46133
46488
|
return;
|
|
46134
46489
|
}
|
|
46135
46490
|
return {
|
|
@@ -46138,7 +46493,8 @@ function resolveJwtValidationConfigFromEnv3(env) {
|
|
|
46138
46493
|
audience,
|
|
46139
46494
|
secret,
|
|
46140
46495
|
skipVerification,
|
|
46141
|
-
clockTolerance
|
|
46496
|
+
clockTolerance,
|
|
46497
|
+
jwksCacheTtlMs
|
|
46142
46498
|
};
|
|
46143
46499
|
}
|
|
46144
46500
|
function normalizeList3(value) {
|
|
@@ -46182,15 +46538,33 @@ function validateClaims3(claims, config) {
|
|
|
46182
46538
|
throw new JWTValidationError3("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
|
|
46183
46539
|
}
|
|
46184
46540
|
}
|
|
46185
|
-
function getRemoteJwks3(jwksUrl) {
|
|
46541
|
+
function getRemoteJwks3(jwksUrl, config) {
|
|
46542
|
+
const now = Date.now();
|
|
46186
46543
|
const cached = JWKS_CACHE4.get(jwksUrl);
|
|
46544
|
+
if (cached && cached.expiresAtMs > now) {
|
|
46545
|
+
return cached.resolver;
|
|
46546
|
+
}
|
|
46187
46547
|
if (cached) {
|
|
46188
|
-
|
|
46548
|
+
JWKS_CACHE4.delete(jwksUrl);
|
|
46189
46549
|
}
|
|
46190
46550
|
const jwks = createRemoteJWKSet3(new URL(jwksUrl));
|
|
46191
|
-
|
|
46551
|
+
const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig3?.jwksCacheTtlMs;
|
|
46552
|
+
const ttlMs = resolveJwksCacheTtlMs3(configuredTtl);
|
|
46553
|
+
JWKS_CACHE4.set(jwksUrl, {
|
|
46554
|
+
resolver: jwks,
|
|
46555
|
+
expiresAtMs: now + ttlMs
|
|
46556
|
+
});
|
|
46192
46557
|
return jwks;
|
|
46193
46558
|
}
|
|
46559
|
+
function resolveJwksCacheTtlMs3(configuredTtl) {
|
|
46560
|
+
if (configuredTtl === undefined) {
|
|
46561
|
+
return DEFAULT_JWKS_CACHE_TTL_MS4;
|
|
46562
|
+
}
|
|
46563
|
+
if (!Number.isFinite(configuredTtl)) {
|
|
46564
|
+
return DEFAULT_JWKS_CACHE_TTL_MS4;
|
|
46565
|
+
}
|
|
46566
|
+
return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS4, Math.floor(configuredTtl)));
|
|
46567
|
+
}
|
|
46194
46568
|
function decodeJwtUnsafe4(token) {
|
|
46195
46569
|
if (!token)
|
|
46196
46570
|
return null;
|
|
@@ -46223,7 +46597,7 @@ async function verifyJwt3(token, config) {
|
|
|
46223
46597
|
const key = new TextEncoder().encode(effectiveConfig.secret);
|
|
46224
46598
|
({ payload } = await jwtVerify3(token, key, options));
|
|
46225
46599
|
} else {
|
|
46226
|
-
({ payload } = await jwtVerify3(token, getRemoteJwks3(effectiveConfig.jwksUrl), options));
|
|
46600
|
+
({ payload } = await jwtVerify3(token, getRemoteJwks3(effectiveConfig.jwksUrl, effectiveConfig), options));
|
|
46227
46601
|
}
|
|
46228
46602
|
const claims = payload;
|
|
46229
46603
|
validateClaims3(claims, effectiveConfig);
|
|
@@ -47366,6 +47740,11 @@ var WEBSOCKET_READY_STATE_OPEN2 = 1;
|
|
|
47366
47740
|
var BACKPRESSURE_HIGH_WATER_MARK2 = 100;
|
|
47367
47741
|
var BACKPRESSURE_BUFFER_LIMIT2 = 1024 * 1024;
|
|
47368
47742
|
var SLOW_CLIENT_TIMEOUT_MS2 = 30000;
|
|
47743
|
+
var DEFAULT_RATE_LIMIT_WINDOW_MS2 = 5000;
|
|
47744
|
+
var DEFAULT_MAX_MESSAGES_PER_WINDOW2 = 1000;
|
|
47745
|
+
var DEFAULT_OPERATION_TIMEOUT_MS2 = 15000;
|
|
47746
|
+
var DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION2 = 1000;
|
|
47747
|
+
var RATE_LIMIT_HARD_MULTIPLIER2 = 5;
|
|
47369
47748
|
|
|
47370
47749
|
class SyncSession2 {
|
|
47371
47750
|
websocket;
|
|
@@ -47394,15 +47773,25 @@ class SyncSession2 {
|
|
|
47394
47773
|
class SyncProtocolHandler2 {
|
|
47395
47774
|
udfExecutor;
|
|
47396
47775
|
sessions = new Map;
|
|
47776
|
+
rateLimitStates = new Map;
|
|
47397
47777
|
subscriptionManager;
|
|
47398
47778
|
instanceName;
|
|
47399
47779
|
backpressureController;
|
|
47400
47780
|
heartbeatController;
|
|
47401
47781
|
isDev;
|
|
47782
|
+
maxMessagesPerWindow;
|
|
47783
|
+
rateLimitWindowMs;
|
|
47784
|
+
operationTimeoutMs;
|
|
47785
|
+
maxActiveQueriesPerSession;
|
|
47402
47786
|
constructor(instanceName, udfExecutor, options) {
|
|
47403
47787
|
this.udfExecutor = udfExecutor;
|
|
47404
47788
|
this.instanceName = instanceName;
|
|
47405
47789
|
this.isDev = options?.isDev ?? true;
|
|
47790
|
+
this.maxMessagesPerWindow = Math.max(1, options?.maxMessagesPerWindow ?? DEFAULT_MAX_MESSAGES_PER_WINDOW2);
|
|
47791
|
+
this.rateLimitWindowMs = Math.max(1, options?.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS2);
|
|
47792
|
+
const configuredOperationTimeout = options?.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS2;
|
|
47793
|
+
this.operationTimeoutMs = Number.isFinite(configuredOperationTimeout) ? Math.max(0, Math.floor(configuredOperationTimeout)) : DEFAULT_OPERATION_TIMEOUT_MS2;
|
|
47794
|
+
this.maxActiveQueriesPerSession = Math.max(1, options?.maxActiveQueriesPerSession ?? DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION2);
|
|
47406
47795
|
this.subscriptionManager = new SubscriptionManager4;
|
|
47407
47796
|
this.backpressureController = new SessionBackpressureController2({
|
|
47408
47797
|
websocketReadyStateOpen: WEBSOCKET_READY_STATE_OPEN2,
|
|
@@ -47420,6 +47809,10 @@ class SyncProtocolHandler2 {
|
|
|
47420
47809
|
createSession(sessionId, websocket) {
|
|
47421
47810
|
const session = new SyncSession2(websocket);
|
|
47422
47811
|
this.sessions.set(sessionId, session);
|
|
47812
|
+
this.rateLimitStates.set(sessionId, {
|
|
47813
|
+
windowStartedAt: Date.now(),
|
|
47814
|
+
messagesInWindow: 0
|
|
47815
|
+
});
|
|
47423
47816
|
return session;
|
|
47424
47817
|
}
|
|
47425
47818
|
getSession(sessionId) {
|
|
@@ -47434,6 +47827,7 @@ class SyncProtocolHandler2 {
|
|
|
47434
47827
|
session.isDraining = false;
|
|
47435
47828
|
this.subscriptionManager.unsubscribeAll(sessionId);
|
|
47436
47829
|
this.sessions.delete(sessionId);
|
|
47830
|
+
this.rateLimitStates.delete(sessionId);
|
|
47437
47831
|
}
|
|
47438
47832
|
}
|
|
47439
47833
|
updateSessionId(oldSessionId, newSessionId) {
|
|
@@ -47441,6 +47835,11 @@ class SyncProtocolHandler2 {
|
|
|
47441
47835
|
if (session) {
|
|
47442
47836
|
this.sessions.delete(oldSessionId);
|
|
47443
47837
|
this.sessions.set(newSessionId, session);
|
|
47838
|
+
const rateLimitState = this.rateLimitStates.get(oldSessionId);
|
|
47839
|
+
if (rateLimitState) {
|
|
47840
|
+
this.rateLimitStates.delete(oldSessionId);
|
|
47841
|
+
this.rateLimitStates.set(newSessionId, rateLimitState);
|
|
47842
|
+
}
|
|
47444
47843
|
this.subscriptionManager.updateSessionId(oldSessionId, newSessionId);
|
|
47445
47844
|
}
|
|
47446
47845
|
}
|
|
@@ -47449,6 +47848,29 @@ class SyncProtocolHandler2 {
|
|
|
47449
47848
|
if (!session && message22.type !== "Connect") {
|
|
47450
47849
|
throw new Error("Session not found");
|
|
47451
47850
|
}
|
|
47851
|
+
if (session) {
|
|
47852
|
+
const rateLimitDecision = this.consumeRateLimit(sessionId);
|
|
47853
|
+
if (rateLimitDecision === "reject") {
|
|
47854
|
+
return [
|
|
47855
|
+
{
|
|
47856
|
+
type: "FatalError",
|
|
47857
|
+
error: "Rate limit exceeded, retry shortly"
|
|
47858
|
+
}
|
|
47859
|
+
];
|
|
47860
|
+
}
|
|
47861
|
+
if (rateLimitDecision === "close") {
|
|
47862
|
+
try {
|
|
47863
|
+
session.websocket.close(1013, "Rate limit exceeded");
|
|
47864
|
+
} catch {}
|
|
47865
|
+
this.destroySession(sessionId);
|
|
47866
|
+
return [
|
|
47867
|
+
{
|
|
47868
|
+
type: "FatalError",
|
|
47869
|
+
error: "Rate limit exceeded"
|
|
47870
|
+
}
|
|
47871
|
+
];
|
|
47872
|
+
}
|
|
47873
|
+
}
|
|
47452
47874
|
switch (message22.type) {
|
|
47453
47875
|
case "Connect":
|
|
47454
47876
|
return this.handleConnect(sessionId, message22);
|
|
@@ -47505,6 +47927,15 @@ class SyncProtocolHandler2 {
|
|
|
47505
47927
|
return [fatalError];
|
|
47506
47928
|
}
|
|
47507
47929
|
const startVersion = makeStateVersion2(session.querySetVersion, session.identityVersion, session.timestamp);
|
|
47930
|
+
const projectedActiveQueryCount = this.computeProjectedActiveQueryCount(session, message22);
|
|
47931
|
+
if (projectedActiveQueryCount > this.maxActiveQueriesPerSession) {
|
|
47932
|
+
return [
|
|
47933
|
+
{
|
|
47934
|
+
type: "FatalError",
|
|
47935
|
+
error: `Too many active queries: ${projectedActiveQueryCount} exceeds limit ${this.maxActiveQueriesPerSession}`
|
|
47936
|
+
}
|
|
47937
|
+
];
|
|
47938
|
+
}
|
|
47508
47939
|
session.querySetVersion = message22.newVersion;
|
|
47509
47940
|
const modifications = [];
|
|
47510
47941
|
for (const mod of message22.modifications) {
|
|
@@ -47828,7 +48259,54 @@ class SyncProtocolHandler2 {
|
|
|
47828
48259
|
}, () => {
|
|
47829
48260
|
return;
|
|
47830
48261
|
});
|
|
47831
|
-
return run;
|
|
48262
|
+
return this.withOperationTimeout(run);
|
|
48263
|
+
}
|
|
48264
|
+
withOperationTimeout(promise) {
|
|
48265
|
+
if (this.operationTimeoutMs <= 0) {
|
|
48266
|
+
return promise;
|
|
48267
|
+
}
|
|
48268
|
+
let timeoutHandle;
|
|
48269
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
48270
|
+
timeoutHandle = setTimeout(() => {
|
|
48271
|
+
reject(new Error(`Sync operation timed out after ${this.operationTimeoutMs}ms`));
|
|
48272
|
+
}, this.operationTimeoutMs);
|
|
48273
|
+
});
|
|
48274
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
48275
|
+
if (timeoutHandle) {
|
|
48276
|
+
clearTimeout(timeoutHandle);
|
|
48277
|
+
}
|
|
48278
|
+
});
|
|
48279
|
+
}
|
|
48280
|
+
consumeRateLimit(sessionId) {
|
|
48281
|
+
const state = this.rateLimitStates.get(sessionId);
|
|
48282
|
+
if (!state) {
|
|
48283
|
+
return "allow";
|
|
48284
|
+
}
|
|
48285
|
+
const now = Date.now();
|
|
48286
|
+
if (now - state.windowStartedAt >= this.rateLimitWindowMs) {
|
|
48287
|
+
state.windowStartedAt = now;
|
|
48288
|
+
state.messagesInWindow = 0;
|
|
48289
|
+
}
|
|
48290
|
+
state.messagesInWindow += 1;
|
|
48291
|
+
if (state.messagesInWindow <= this.maxMessagesPerWindow) {
|
|
48292
|
+
return "allow";
|
|
48293
|
+
}
|
|
48294
|
+
const hardLimit = Math.max(this.maxMessagesPerWindow + 1, this.maxMessagesPerWindow * RATE_LIMIT_HARD_MULTIPLIER2);
|
|
48295
|
+
if (state.messagesInWindow >= hardLimit) {
|
|
48296
|
+
return "close";
|
|
48297
|
+
}
|
|
48298
|
+
return "reject";
|
|
48299
|
+
}
|
|
48300
|
+
computeProjectedActiveQueryCount(session, message22) {
|
|
48301
|
+
const projected = new Set(session.activeQueries.keys());
|
|
48302
|
+
for (const mod of message22.modifications) {
|
|
48303
|
+
if (mod.type === "Add") {
|
|
48304
|
+
projected.add(mod.queryId);
|
|
48305
|
+
} else if (mod.type === "Remove") {
|
|
48306
|
+
projected.delete(mod.queryId);
|
|
48307
|
+
}
|
|
48308
|
+
}
|
|
48309
|
+
return projected.size;
|
|
47832
48310
|
}
|
|
47833
48311
|
sendPing(session) {
|
|
47834
48312
|
if (!session.websocket || session.websocket.readyState !== WEBSOCKET_READY_STATE_OPEN2) {
|