@concavejs/core 0.0.1-alpha.5 → 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/auth/jwt.d.ts +5 -0
- package/dist/auth/jwt.js +37 -5
- package/dist/http/api-router.d.ts +3 -1
- package/dist/http/api-router.js +62 -15
- package/dist/http/http-handler.js +53 -4
- package/dist/interfaces/execution-context.d.ts +2 -1
- package/dist/kernel/blob-store-gateway.js +7 -7
- package/dist/kernel/context-storage.js +6 -0
- package/dist/kernel/syscalls/action-syscalls.js +4 -6
- package/dist/kernel/syscalls/database-syscalls.js +1 -1
- package/dist/kernel/syscalls/js-router.d.ts +2 -2
- package/dist/kernel/syscalls/kernel-syscalls.d.ts +1 -1
- package/dist/kernel/syscalls/query-syscalls.js +2 -2
- package/dist/kernel/syscalls/router.d.ts +3 -2
- package/dist/kernel/syscalls/utils.d.ts +3 -3
- package/dist/kernel/syscalls/utils.js +3 -2
- package/dist/queryengine/developer-id.d.ts +8 -0
- package/dist/queryengine/developer-id.js +23 -2
- package/dist/router/router.d.ts +8 -3
- package/dist/runtime/runtime-context.d.ts +1 -1
- package/dist/scheduler/cron-executor.d.ts +6 -2
- package/dist/scheduler/cron-executor.js +54 -26
- package/dist/scheduler/scheduled-function-executor.d.ts +6 -2
- package/dist/scheduler/scheduled-function-executor.js +58 -36
- package/dist/sync/protocol-handler.d.ts +25 -0
- package/dist/sync/protocol-handler.js +111 -1
- package/dist/transactor/occ-transaction.js +3 -3
- package/dist/udf/execution-adapter.d.ts +1 -1
- package/dist/udf/execution-adapter.js +2 -2
- package/dist/udf/executor/inline.d.ts +1 -1
- package/dist/udf/executor/inline.js +3 -3
- package/dist/udf/executor/interface.d.ts +5 -2
- package/dist/udf/runtime/udf-setup.d.ts +4 -4
- package/dist/udf/runtime/udf-setup.js +11 -9
- package/dist/utils/long.d.ts +1 -1
- package/package.json +1 -1
package/dist/auth/jwt.d.ts
CHANGED
|
@@ -36,6 +36,11 @@ export type JWTValidationConfig = {
|
|
|
36
36
|
secret?: string;
|
|
37
37
|
skipVerification?: boolean;
|
|
38
38
|
clockTolerance?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Optional TTL (in milliseconds) for cached remote JWKS resolvers.
|
|
41
|
+
* Defaults to 5 minutes when omitted.
|
|
42
|
+
*/
|
|
43
|
+
jwksCacheTtlMs?: number;
|
|
39
44
|
};
|
|
40
45
|
export type JWTValidationErrorCode = "MISSING_CONFIG" | "INVALID_SIGNATURE" | "TOKEN_EXPIRED" | "TOKEN_NOT_ACTIVE" | "CLAIM_VALIDATION_FAILED" | "MISSING_SUBJECT" | "MISSING_ISSUER" | "INVALID_TOKEN";
|
|
41
46
|
export declare class JWTValidationError extends Error {
|
package/dist/auth/jwt.js
CHANGED
|
@@ -123,12 +123,17 @@ export const WELL_KNOWN_JWKS_URLS = {
|
|
|
123
123
|
firebase: () => "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com",
|
|
124
124
|
};
|
|
125
125
|
const DEFAULT_CLOCK_TOLERANCE_SECONDS = 60;
|
|
126
|
+
const DEFAULT_JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
127
|
+
const MAX_JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
126
128
|
const JWKS_CACHE = new Map();
|
|
127
129
|
let defaultValidationConfig;
|
|
128
130
|
let adminAuthConfig;
|
|
129
131
|
let systemAuthConfig;
|
|
130
132
|
export function setJwtValidationConfig(config) {
|
|
131
133
|
defaultValidationConfig = config;
|
|
134
|
+
// Config updates can change issuer/audience/JWKS source semantics.
|
|
135
|
+
// Clearing resolver cache avoids stale long-lived entries across reconfiguration.
|
|
136
|
+
JWKS_CACHE.clear();
|
|
132
137
|
}
|
|
133
138
|
export function getJwtValidationConfig() {
|
|
134
139
|
return defaultValidationConfig;
|
|
@@ -268,7 +273,15 @@ export function resolveJwtValidationConfigFromEnv(env) {
|
|
|
268
273
|
parseBoolean(getEnvValue("CONCAVE_JWT_SKIP_VERIFICATION", env));
|
|
269
274
|
const clockTolerance = parseNumber(getEnvValue("AUTH_CLOCK_TOLERANCE", env)) ??
|
|
270
275
|
parseNumber(getEnvValue("CONCAVE_JWT_CLOCK_TOLERANCE", env));
|
|
271
|
-
|
|
276
|
+
const jwksCacheTtlMs = parseNumber(getEnvValue("AUTH_JWKS_CACHE_TTL_MS", env)) ??
|
|
277
|
+
parseNumber(getEnvValue("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
|
|
278
|
+
if (!jwksUrl &&
|
|
279
|
+
!issuer &&
|
|
280
|
+
!audience &&
|
|
281
|
+
!secret &&
|
|
282
|
+
skipVerification === undefined &&
|
|
283
|
+
clockTolerance === undefined &&
|
|
284
|
+
jwksCacheTtlMs === undefined) {
|
|
272
285
|
return undefined;
|
|
273
286
|
}
|
|
274
287
|
return {
|
|
@@ -278,6 +291,7 @@ export function resolveJwtValidationConfigFromEnv(env) {
|
|
|
278
291
|
secret,
|
|
279
292
|
skipVerification,
|
|
280
293
|
clockTolerance,
|
|
294
|
+
jwksCacheTtlMs,
|
|
281
295
|
};
|
|
282
296
|
}
|
|
283
297
|
function normalizeList(value) {
|
|
@@ -324,15 +338,33 @@ function validateClaims(claims, config) {
|
|
|
324
338
|
throw new JWTValidationError("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
|
|
325
339
|
}
|
|
326
340
|
}
|
|
327
|
-
function getRemoteJwks(jwksUrl) {
|
|
341
|
+
function getRemoteJwks(jwksUrl, config) {
|
|
342
|
+
const now = Date.now();
|
|
328
343
|
const cached = JWKS_CACHE.get(jwksUrl);
|
|
344
|
+
if (cached && cached.expiresAtMs > now) {
|
|
345
|
+
return cached.resolver;
|
|
346
|
+
}
|
|
329
347
|
if (cached) {
|
|
330
|
-
|
|
348
|
+
JWKS_CACHE.delete(jwksUrl);
|
|
331
349
|
}
|
|
332
350
|
const jwks = createRemoteJWKSet(new URL(jwksUrl));
|
|
333
|
-
|
|
351
|
+
const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig?.jwksCacheTtlMs;
|
|
352
|
+
const ttlMs = resolveJwksCacheTtlMs(configuredTtl);
|
|
353
|
+
JWKS_CACHE.set(jwksUrl, {
|
|
354
|
+
resolver: jwks,
|
|
355
|
+
expiresAtMs: now + ttlMs,
|
|
356
|
+
});
|
|
334
357
|
return jwks;
|
|
335
358
|
}
|
|
359
|
+
function resolveJwksCacheTtlMs(configuredTtl) {
|
|
360
|
+
if (configuredTtl === undefined) {
|
|
361
|
+
return DEFAULT_JWKS_CACHE_TTL_MS;
|
|
362
|
+
}
|
|
363
|
+
if (!Number.isFinite(configuredTtl)) {
|
|
364
|
+
return DEFAULT_JWKS_CACHE_TTL_MS;
|
|
365
|
+
}
|
|
366
|
+
return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS, Math.floor(configuredTtl)));
|
|
367
|
+
}
|
|
336
368
|
export function decodeJwtUnsafe(token) {
|
|
337
369
|
if (!token)
|
|
338
370
|
return null;
|
|
@@ -366,7 +398,7 @@ export async function verifyJwt(token, config) {
|
|
|
366
398
|
({ payload } = await jwtVerify(token, key, options));
|
|
367
399
|
}
|
|
368
400
|
else {
|
|
369
|
-
({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl), options));
|
|
401
|
+
({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl, effectiveConfig), options));
|
|
370
402
|
}
|
|
371
403
|
const claims = payload;
|
|
372
404
|
validateClaims(claims, effectiveConfig);
|
|
@@ -7,6 +7,7 @@ export interface FunctionExecutionParams {
|
|
|
7
7
|
args: Record<string, any>;
|
|
8
8
|
auth?: AuthContext | UserIdentityAttributes;
|
|
9
9
|
componentPath?: string;
|
|
10
|
+
snapshotTimestamp?: bigint;
|
|
10
11
|
request: Request;
|
|
11
12
|
}
|
|
12
13
|
export interface FunctionExecutionResult {
|
|
@@ -24,7 +25,7 @@ export interface StorageStoreResult {
|
|
|
24
25
|
}
|
|
25
26
|
export interface StorageGetResult {
|
|
26
27
|
blob: Blob | null;
|
|
27
|
-
headers?:
|
|
28
|
+
headers?: Record<string, string> | [string, string][] | Headers;
|
|
28
29
|
}
|
|
29
30
|
export interface StorageAdapter {
|
|
30
31
|
store(blob: Blob, request: Request): Promise<StorageStoreResult>;
|
|
@@ -36,6 +37,7 @@ export interface CoreHttpApiOptions {
|
|
|
36
37
|
notifyWrites?: (writtenRanges?: SerializedKeyRange[], writtenTables?: string[], commitTimestamp?: bigint) => Promise<void> | void;
|
|
37
38
|
storage?: StorageAdapter;
|
|
38
39
|
corsHeaders?: Record<string, string>;
|
|
40
|
+
getSnapshotTimestamp?: (request: Request) => Promise<bigint> | bigint;
|
|
39
41
|
}
|
|
40
42
|
export interface CoreHttpApiResult {
|
|
41
43
|
handled: boolean;
|
package/dist/http/api-router.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AdminAuthError, assertAdminToken, assertSystemToken, identityFromToken, isAdminToken, isSystemToken, JWTValidationError, SystemAuthError, } from "../auth";
|
|
2
|
-
import {
|
|
3
|
-
import { isValidDocumentId } from "../id-codec";
|
|
2
|
+
import { writtenTablesFromRanges } from "../utils";
|
|
4
3
|
import { InternalFunctionAccessError } from "../errors";
|
|
5
4
|
export function computeCorsHeaders(request) {
|
|
6
5
|
const origin = request.headers.get("Origin");
|
|
@@ -82,6 +81,40 @@ export async function resolveAuthContext(bodyAuth, headerToken, headerIdentity)
|
|
|
82
81
|
}
|
|
83
82
|
return bodyAuth;
|
|
84
83
|
}
|
|
84
|
+
function parseTimestampInput(value) {
|
|
85
|
+
if (value === undefined || value === null) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === "bigint") {
|
|
89
|
+
return value >= 0n ? value : undefined;
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === "number") {
|
|
92
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return BigInt(value);
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === "string") {
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return BigInt(trimmed);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
async function resolveSnapshotTimestamp(options, request) {
|
|
112
|
+
const fromCallback = options.getSnapshotTimestamp ? await options.getSnapshotTimestamp(request) : undefined;
|
|
113
|
+
if (typeof fromCallback === "bigint") {
|
|
114
|
+
return fromCallback;
|
|
115
|
+
}
|
|
116
|
+
return BigInt(Date.now());
|
|
117
|
+
}
|
|
85
118
|
export async function handleCoreHttpApiRequest(request, options) {
|
|
86
119
|
const url = new URL(request.url);
|
|
87
120
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
@@ -125,6 +158,19 @@ export async function handleCoreHttpApiRequest(request, options) {
|
|
|
125
158
|
throw error;
|
|
126
159
|
}
|
|
127
160
|
const route = routeSegments[0];
|
|
161
|
+
if (route === "query_ts") {
|
|
162
|
+
if (request.method !== "POST") {
|
|
163
|
+
return {
|
|
164
|
+
handled: true,
|
|
165
|
+
response: apply(Response.json({ error: "Method not allowed" }, { status: 405 })),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const snapshotTimestamp = await resolveSnapshotTimestamp(options, request);
|
|
169
|
+
return {
|
|
170
|
+
handled: true,
|
|
171
|
+
response: apply(Response.json({ ts: snapshotTimestamp.toString() })),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
128
174
|
if (route === "storage") {
|
|
129
175
|
if (!options.storage) {
|
|
130
176
|
return {
|
|
@@ -154,18 +200,10 @@ export async function handleCoreHttpApiRequest(request, options) {
|
|
|
154
200
|
}
|
|
155
201
|
if (request.method === "GET" && routeSegments.length === 2) {
|
|
156
202
|
try {
|
|
157
|
-
|
|
158
|
-
const originalId = storageId;
|
|
159
|
-
// If the ID doesn't have a table prefix and isn't a valid Base32 document ID,
|
|
160
|
-
// it's likely a raw hex storage ID. Prepend the default _storage table prefix.
|
|
161
|
-
if (!storageId.includes(":") && !isValidDocumentId(storageId)) {
|
|
162
|
-
const tableHex = stringToHex("_storage");
|
|
163
|
-
storageId = `${tableHex}:${storageId}`;
|
|
164
|
-
}
|
|
203
|
+
const storageId = decodeURIComponent(routeSegments[1]);
|
|
165
204
|
const result = await options.storage.get(storageId, request);
|
|
166
205
|
const blob = result.blob;
|
|
167
206
|
if (!blob) {
|
|
168
|
-
console.error(`[CoreHttpApi] Storage object not found. ID: ${storageId} (original: ${originalId})`);
|
|
169
207
|
return {
|
|
170
208
|
handled: true,
|
|
171
209
|
response: apply(new Response("Object not found", { status: 404 })),
|
|
@@ -192,7 +230,7 @@ export async function handleCoreHttpApiRequest(request, options) {
|
|
|
192
230
|
}
|
|
193
231
|
}
|
|
194
232
|
}
|
|
195
|
-
if (route === "query" || route === "mutation" || route === "action") {
|
|
233
|
+
if (route === "query" || route === "mutation" || route === "action" || route === "query_at_ts") {
|
|
196
234
|
if (request.method !== "POST") {
|
|
197
235
|
return {
|
|
198
236
|
handled: true,
|
|
@@ -216,7 +254,7 @@ export async function handleCoreHttpApiRequest(request, options) {
|
|
|
216
254
|
response: apply(Response.json({ error: "Invalid request body" }, { status: 400 })),
|
|
217
255
|
};
|
|
218
256
|
}
|
|
219
|
-
const { path, args, format, auth: bodyAuth, componentPath } = body;
|
|
257
|
+
const { path, args, format, auth: bodyAuth, componentPath, ts } = body;
|
|
220
258
|
if (!path || typeof path !== "string") {
|
|
221
259
|
return {
|
|
222
260
|
handled: true,
|
|
@@ -245,6 +283,14 @@ export async function handleCoreHttpApiRequest(request, options) {
|
|
|
245
283
|
};
|
|
246
284
|
}
|
|
247
285
|
const jsonArgs = rawArgs ?? {};
|
|
286
|
+
const executionType = route === "query_at_ts" ? "query" : route;
|
|
287
|
+
const snapshotTimestamp = route === "query_at_ts" ? parseTimestampInput(ts) : undefined;
|
|
288
|
+
if (route === "query_at_ts" && snapshotTimestamp === undefined) {
|
|
289
|
+
return {
|
|
290
|
+
handled: true,
|
|
291
|
+
response: apply(Response.json({ error: "Invalid or missing ts" }, { status: 400 })),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
248
294
|
let authForExecution;
|
|
249
295
|
try {
|
|
250
296
|
authForExecution = await resolveAuthContext(bodyAuth, headerToken, headerIdentity);
|
|
@@ -261,11 +307,12 @@ export async function handleCoreHttpApiRequest(request, options) {
|
|
|
261
307
|
throw error;
|
|
262
308
|
}
|
|
263
309
|
const executionParams = {
|
|
264
|
-
type:
|
|
310
|
+
type: executionType,
|
|
265
311
|
path,
|
|
266
312
|
args: jsonArgs,
|
|
267
313
|
auth: authForExecution,
|
|
268
314
|
componentPath,
|
|
315
|
+
snapshotTimestamp,
|
|
269
316
|
request,
|
|
270
317
|
};
|
|
271
318
|
try {
|
|
@@ -282,7 +329,7 @@ export async function handleCoreHttpApiRequest(request, options) {
|
|
|
282
329
|
}
|
|
283
330
|
const result = await options.executeFunction(executionParams);
|
|
284
331
|
if (options.notifyWrites &&
|
|
285
|
-
(
|
|
332
|
+
(executionType === "mutation" || executionType === "action") &&
|
|
286
333
|
(result.writtenRanges?.length || result.writtenTables?.length)) {
|
|
287
334
|
await options.notifyWrites(result.writtenRanges, result.writtenTables ?? writtenTablesFromRanges(result.writtenRanges));
|
|
288
335
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Delegates Convex-style HTTP API routes to shared core logic while
|
|
5
5
|
* wiring runtime execution adapters and docstore.
|
|
6
6
|
*/
|
|
7
|
+
import { loadConvexModule } from "../udf";
|
|
7
8
|
import { createClientAdapter } from "../udf/execution-adapter";
|
|
8
9
|
import { UdfKernel } from "../queryengine";
|
|
9
10
|
import { applyCors, computeCorsHeaders, handleCoreHttpApiRequest, resolveAuthContext } from "./index";
|
|
@@ -19,6 +20,8 @@ function isReservedApiPath(pathname) {
|
|
|
19
20
|
normalizedPath === "/api/sync" ||
|
|
20
21
|
normalizedPath === "/api/reset-test-state" ||
|
|
21
22
|
normalizedPath === "/api/query" ||
|
|
23
|
+
normalizedPath === "/api/query_ts" ||
|
|
24
|
+
normalizedPath === "/api/query_at_ts" ||
|
|
22
25
|
normalizedPath === "/api/mutation" ||
|
|
23
26
|
normalizedPath === "/api/action") {
|
|
24
27
|
return true;
|
|
@@ -107,7 +110,7 @@ export class HttpHandler {
|
|
|
107
110
|
}
|
|
108
111
|
};
|
|
109
112
|
const coreResult = await handleCoreHttpApiRequest(request, {
|
|
110
|
-
executeFunction: async ({ type, path, args, auth, componentPath }) => this.adapter.executeUdf(path, args, type, auth, componentPath),
|
|
113
|
+
executeFunction: async ({ type, path, args, auth, componentPath, snapshotTimestamp }) => this.adapter.executeUdf(path, args, type, auth, componentPath, undefined, snapshotTimestamp),
|
|
111
114
|
notifyWrites,
|
|
112
115
|
storage: this.docstore && this.blobstore
|
|
113
116
|
? {
|
|
@@ -129,6 +132,19 @@ export class HttpHandler {
|
|
|
129
132
|
}
|
|
130
133
|
: undefined,
|
|
131
134
|
corsHeaders,
|
|
135
|
+
getSnapshotTimestamp: () => {
|
|
136
|
+
const oracle = this.docstore?.timestampOracle;
|
|
137
|
+
const oracleTimestamp = typeof oracle?.beginSnapshot === "function"
|
|
138
|
+
? oracle.beginSnapshot()
|
|
139
|
+
: typeof oracle?.getCurrentTimestamp === "function"
|
|
140
|
+
? oracle.getCurrentTimestamp()
|
|
141
|
+
: undefined;
|
|
142
|
+
const wallClock = BigInt(Date.now());
|
|
143
|
+
if (typeof oracleTimestamp === "bigint" && oracleTimestamp > wallClock) {
|
|
144
|
+
return oracleTimestamp;
|
|
145
|
+
}
|
|
146
|
+
return wallClock;
|
|
147
|
+
},
|
|
132
148
|
});
|
|
133
149
|
if (coreResult?.handled) {
|
|
134
150
|
return coreResult.response;
|
|
@@ -161,10 +177,43 @@ export class HttpHandler {
|
|
|
161
177
|
return apply(Response.json({ error: "Invalid function path" }, { status: 400 }));
|
|
162
178
|
}
|
|
163
179
|
const isFunctionHandle = body.path.startsWith("function://");
|
|
180
|
+
// Normalize slash-separated paths to colon format (e.g. "messages/list" → "messages:list")
|
|
181
|
+
if (!isFunctionHandle && !body.path.includes(":") && body.path.includes("/")) {
|
|
182
|
+
const lastSlash = body.path.lastIndexOf("/");
|
|
183
|
+
body.path = body.path.substring(0, lastSlash) + ":" + body.path.substring(lastSlash + 1);
|
|
184
|
+
}
|
|
164
185
|
if (!isFunctionHandle && !body.path.includes(":")) {
|
|
165
186
|
return apply(Response.json({ error: "Invalid function path" }, { status: 400 }));
|
|
166
187
|
}
|
|
167
|
-
|
|
188
|
+
let udfType;
|
|
189
|
+
if (body.type === "query" || body.type === "mutation" || body.type === "action") {
|
|
190
|
+
udfType = body.type;
|
|
191
|
+
}
|
|
192
|
+
else if (body.type === undefined || body.type === null) {
|
|
193
|
+
// Auto-detect type by loading the module and inspecting the export
|
|
194
|
+
try {
|
|
195
|
+
const modulePath = body.path.split(":")[0];
|
|
196
|
+
const functionName = body.path.split(":")[1] ?? "default";
|
|
197
|
+
const module = await loadConvexModule(modulePath, { hint: "udf" });
|
|
198
|
+
const func = module[functionName];
|
|
199
|
+
if (!func) {
|
|
200
|
+
return apply(Response.json({ error: `Function ${body.path} not found` }, { status: 404 }));
|
|
201
|
+
}
|
|
202
|
+
if (func.isQuery)
|
|
203
|
+
udfType = "query";
|
|
204
|
+
else if (func.isMutation)
|
|
205
|
+
udfType = "mutation";
|
|
206
|
+
else if (func.isAction)
|
|
207
|
+
udfType = "action";
|
|
208
|
+
else {
|
|
209
|
+
return apply(Response.json({ error: `Function ${body.path} is not a valid query, mutation, or action` }, { status: 400 }));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return apply(Response.json({ error: `Could not auto-detect function type: ${error.message}` }, { status: 400 }));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
168
217
|
return apply(Response.json({ error: "Invalid function type" }, { status: 400 }));
|
|
169
218
|
}
|
|
170
219
|
try {
|
|
@@ -216,10 +265,10 @@ export class HttpHandler {
|
|
|
216
265
|
return apply(Response.json({ error: "Invalid args" }, { status: 400 }));
|
|
217
266
|
}
|
|
218
267
|
const normalizedArgs = rawArgs ?? {};
|
|
219
|
-
const result = await this.adapter.executeUdf(body.path, normalizedArgs,
|
|
268
|
+
const result = await this.adapter.executeUdf(body.path, normalizedArgs, udfType, authForExecution, body.componentPath);
|
|
220
269
|
const writtenTables = writtenTablesFromRanges(result.writtenRanges);
|
|
221
270
|
// Notify subscriptions for mutations/actions with writes
|
|
222
|
-
if ((
|
|
271
|
+
if ((udfType === "mutation" || udfType === "action") && result.writtenRanges?.length) {
|
|
223
272
|
await notifyWrites(result.writtenRanges, writtenTables, result.commitTimestamp);
|
|
224
273
|
}
|
|
225
274
|
return apply(Response.json({
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* - Distributed Mode: May read from cache or remote, writes go via coordinator
|
|
8
8
|
*/
|
|
9
9
|
import type { Transactor, TransactionHandle } from "./transactor";
|
|
10
|
+
import type { UserIdentityAttributes } from "convex/server";
|
|
10
11
|
export interface AuthContext {
|
|
11
12
|
type: "Admin" | "User" | "None";
|
|
12
|
-
userIdentity?:
|
|
13
|
+
userIdentity?: UserIdentityAttributes;
|
|
13
14
|
}
|
|
14
15
|
export interface ExecutionContext {
|
|
15
16
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { encodeDocumentId, internalIdToHex } from "../id-codec";
|
|
2
|
-
import {
|
|
2
|
+
import { parseStorageId } from "../queryengine/developer-id";
|
|
3
3
|
import { serializeDeveloperId, stringToHex } from "../utils/utils";
|
|
4
4
|
export class BlobStoreGateway {
|
|
5
5
|
context;
|
|
@@ -51,19 +51,19 @@ export class BlobStoreGateway {
|
|
|
51
51
|
if (!this.storage) {
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
|
-
const docId =
|
|
54
|
+
const docId = parseStorageId(storageId);
|
|
55
55
|
if (!docId) {
|
|
56
|
-
console.
|
|
56
|
+
console.debug(`[BlobStoreGateway] Failed to parse storage ID: ${storageId}`);
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
const docValue = await this.queryRuntime.getVisibleDocumentById(storageId, docId);
|
|
60
60
|
if (!docValue) {
|
|
61
|
-
console.
|
|
61
|
+
console.debug(`[BlobStoreGateway] Document not found for storage ID: ${storageId} (table: ${docId.table}, internalId: ${docId.internalId}, ts: ${this.context.snapshotTimestamp})`);
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
64
64
|
const storedBlob = await this.storage.get(docId.internalId);
|
|
65
65
|
if (!storedBlob) {
|
|
66
|
-
console.
|
|
66
|
+
console.debug(`[BlobStoreGateway] Blob not found in storage: ${docId.internalId}`);
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
return storedBlob instanceof Blob ? storedBlob : new Blob([storedBlob]);
|
|
@@ -72,7 +72,7 @@ export class BlobStoreGateway {
|
|
|
72
72
|
if (!this.storage) {
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
|
-
const docId =
|
|
75
|
+
const docId = parseStorageId(storageId);
|
|
76
76
|
if (!docId) {
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
@@ -85,7 +85,7 @@ export class BlobStoreGateway {
|
|
|
85
85
|
}
|
|
86
86
|
async delete(storageId) {
|
|
87
87
|
const storage = this.requireStorage();
|
|
88
|
-
const docId =
|
|
88
|
+
const docId = parseStorageId(storageId);
|
|
89
89
|
if (!docId) {
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
@@ -30,18 +30,24 @@ export class ContextStorage {
|
|
|
30
30
|
}
|
|
31
31
|
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : undefined;
|
|
32
32
|
}
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
34
|
run(value, callback) {
|
|
34
35
|
if (this.als) {
|
|
36
|
+
// ALS.run handles both sync and async callbacks correctly
|
|
35
37
|
return this.als.run(value, callback);
|
|
36
38
|
}
|
|
37
39
|
this.stack.push(value);
|
|
38
40
|
const result = callback();
|
|
39
41
|
if (result && typeof result.then === "function") {
|
|
42
|
+
// For async callbacks, clean up the stack when the promise settles.
|
|
43
|
+
// The cast is safe: the callback returned a Promise<R> which is assignable to R
|
|
44
|
+
// when the caller expects a Promise return (which is always the case for async callbacks).
|
|
40
45
|
return result.finally(() => {
|
|
41
46
|
this.stack.pop();
|
|
42
47
|
});
|
|
43
48
|
}
|
|
44
49
|
this.stack.pop();
|
|
50
|
+
// At this point we've ruled out the Promise branch above, so result is R
|
|
45
51
|
return result;
|
|
46
52
|
}
|
|
47
53
|
}
|
|
@@ -34,19 +34,17 @@ export class ActionSyscalls {
|
|
|
34
34
|
async handleRunUdf(args) {
|
|
35
35
|
const target = resolveFunctionTarget(args, this.context.componentPath);
|
|
36
36
|
const udfArguments = normalizeUdfArgsPayload(args.args);
|
|
37
|
-
const type = args.udfType ?? args.type ?? "mutation";
|
|
37
|
+
const type = (args.udfType ?? args.type ?? "mutation");
|
|
38
38
|
return await this.invocationManager.execute(target.udfPath, udfArguments, type, target.componentPath);
|
|
39
39
|
}
|
|
40
|
-
handleCreateFunctionHandle(args) {
|
|
40
|
+
async handleCreateFunctionHandle(args) {
|
|
41
41
|
const target = resolveFunctionTarget(args, this.context.componentPath);
|
|
42
42
|
return formatFunctionHandle(target.componentPath ?? "", target.udfPath);
|
|
43
43
|
}
|
|
44
44
|
async handleVectorSearch(args) {
|
|
45
|
-
|
|
46
|
-
return this.queryRuntime.runVectorSearchAction(vectorQuery);
|
|
45
|
+
return this.queryRuntime.runVectorSearchAction(args.query);
|
|
47
46
|
}
|
|
48
47
|
async handleSearchAction(args) {
|
|
49
|
-
|
|
50
|
-
return this.queryRuntime.runSearchAction(searchQuery);
|
|
48
|
+
return this.queryRuntime.runSearchAction(args.query);
|
|
51
49
|
}
|
|
52
50
|
}
|
|
@@ -181,7 +181,7 @@ export class DatabaseSyscalls {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
else {
|
|
184
|
-
for (const [key, val] of Object.entries(value ?? {})) {
|
|
184
|
+
for (const [key, val] of Object.entries((value ?? {}))) {
|
|
185
185
|
if (val === "$undefined" || val === undefined) {
|
|
186
186
|
delete newValue[key];
|
|
187
187
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
type JsSyscallHandler = (args: Record<string,
|
|
1
|
+
type JsSyscallHandler = (args: Record<string, unknown>) => Promise<unknown>;
|
|
2
2
|
export declare class JsSyscallRouter {
|
|
3
3
|
private readonly handlers;
|
|
4
4
|
register(op: string, handler: JsSyscallHandler): void;
|
|
5
5
|
has(op: string): boolean;
|
|
6
|
-
dispatch(op: string, args: Record<string,
|
|
6
|
+
dispatch(op: string, args: Record<string, unknown>): Promise<unknown>;
|
|
7
7
|
}
|
|
8
8
|
export {};
|
|
@@ -5,5 +5,5 @@ export declare class KernelSyscalls {
|
|
|
5
5
|
constructor(resources: KernelResources);
|
|
6
6
|
syscall(op: string, jsonArgs: string): string;
|
|
7
7
|
asyncSyscall(op: string, jsonArgs: string): Promise<string>;
|
|
8
|
-
jsSyscall(op: string, args: Record<string,
|
|
8
|
+
jsSyscall(op: string, args: Record<string, unknown>): Promise<unknown>;
|
|
9
9
|
}
|
|
@@ -19,13 +19,13 @@ export class QuerySyscalls {
|
|
|
19
19
|
return { queryId };
|
|
20
20
|
}
|
|
21
21
|
handleQueryCleanup(args) {
|
|
22
|
-
const
|
|
22
|
+
const queryId = args.queryId;
|
|
23
23
|
delete this.pendingQueries[queryId];
|
|
24
24
|
delete this.queryResults[queryId];
|
|
25
25
|
return {};
|
|
26
26
|
}
|
|
27
27
|
async handleQueryStreamNext(args) {
|
|
28
|
-
const
|
|
28
|
+
const queryId = args.queryId;
|
|
29
29
|
if (this.pendingQueries[queryId]) {
|
|
30
30
|
const query = this.pendingQueries[queryId];
|
|
31
31
|
delete this.pendingQueries[queryId];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
type
|
|
2
|
-
type
|
|
1
|
+
type SyscallArgs = Record<string, unknown>;
|
|
2
|
+
type SyncSyscallHandler = (args: SyscallArgs) => unknown;
|
|
3
|
+
type AsyncSyscallHandler = (args: SyscallArgs) => Promise<unknown>;
|
|
3
4
|
export declare class SyscallRouter {
|
|
4
5
|
private readonly syncSyscalls;
|
|
5
6
|
private readonly asyncSyscalls;
|
|
@@ -5,11 +5,11 @@ export interface FunctionTarget {
|
|
|
5
5
|
componentPath?: string;
|
|
6
6
|
}
|
|
7
7
|
export declare function resolveTableName(docId: InternalDocumentId, tableRegistry: TableRegistry): Promise<string>;
|
|
8
|
-
export declare function normalizeUdfArgsPayload(udfArgs:
|
|
9
|
-
export declare function resolveFunctionTarget(callArgs:
|
|
8
|
+
export declare function normalizeUdfArgsPayload(udfArgs: unknown): unknown;
|
|
9
|
+
export declare function resolveFunctionTarget(callArgs: Record<string, unknown>, currentComponentPath: string): FunctionTarget;
|
|
10
10
|
export declare function parseFunctionHandleTarget(handle: string): FunctionTarget;
|
|
11
11
|
export declare function parseReferenceAddress(reference: string, currentComponentPath: string): FunctionTarget;
|
|
12
12
|
export declare function joinComponentScope(base: string, child: string): string;
|
|
13
13
|
export declare function normalizeComponentPathValue(path?: string | null): string | undefined;
|
|
14
14
|
export declare function formatFunctionHandle(componentPath: string | undefined, udfPath: string): string;
|
|
15
|
-
export declare function evaluatePatchValue(value:
|
|
15
|
+
export declare function evaluatePatchValue(value: unknown): unknown;
|
|
@@ -37,8 +37,9 @@ export function resolveFunctionTarget(callArgs, currentComponentPath) {
|
|
|
37
37
|
return parseFunctionHandleTarget(callArgs.functionHandle);
|
|
38
38
|
}
|
|
39
39
|
if (callArgs && typeof callArgs.functionHandle === "object" && callArgs.functionHandle !== null) {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const handle = callArgs.functionHandle;
|
|
41
|
+
if (typeof handle.handle === "string") {
|
|
42
|
+
return parseFunctionHandleTarget(handle.handle);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
if (callArgs && typeof callArgs.reference === "string") {
|
|
@@ -14,6 +14,14 @@ import type { InternalDocumentId } from "../docstore/interface";
|
|
|
14
14
|
* @returns The parsed InternalDocumentId or null if invalid
|
|
15
15
|
*/
|
|
16
16
|
export declare function parseDeveloperId(developerId: string): InternalDocumentId | null;
|
|
17
|
+
/**
|
|
18
|
+
* Parse a storage ID, which may be a full developer ID or a raw internal ID.
|
|
19
|
+
* If it's a raw internal ID (32-char hex), it defaults to the _storage table.
|
|
20
|
+
*
|
|
21
|
+
* @param storageId - The storage ID string
|
|
22
|
+
* @returns The parsed InternalDocumentId or null if invalid
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseStorageId(storageId: string): InternalDocumentId | null;
|
|
17
25
|
/**
|
|
18
26
|
* Check if an InternalDocumentId uses the component format (has tableNumber).
|
|
19
27
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { deserializeDeveloperId } from "../utils/utils";
|
|
2
|
-
import { isValidDocumentId, decodeDocumentId, internalIdToHex } from "../id-codec";
|
|
1
|
+
import { deserializeDeveloperId, stringToHex } from "../utils/utils";
|
|
2
|
+
import { isValidDocumentId, decodeDocumentId, internalIdToHex, INTERNAL_ID_LENGTH } from "../id-codec";
|
|
3
3
|
/**
|
|
4
4
|
* Parse a developer-facing ID string into an InternalDocumentId.
|
|
5
5
|
*
|
|
@@ -38,6 +38,27 @@ export function parseDeveloperId(developerId) {
|
|
|
38
38
|
}
|
|
39
39
|
return { table: parts.table, internalId: parts.internalId };
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse a storage ID, which may be a full developer ID or a raw internal ID.
|
|
43
|
+
* If it's a raw internal ID (32-char hex), it defaults to the _storage table.
|
|
44
|
+
*
|
|
45
|
+
* @param storageId - The storage ID string
|
|
46
|
+
* @returns The parsed InternalDocumentId or null if invalid
|
|
47
|
+
*/
|
|
48
|
+
export function parseStorageId(storageId) {
|
|
49
|
+
const parsed = parseDeveloperId(storageId);
|
|
50
|
+
if (parsed) {
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
// If it's a 32-character hex string, it's likely a raw internal ID for storage
|
|
54
|
+
if (storageId.length === INTERNAL_ID_LENGTH * 2 && /^[0-9a-fA-F]+$/.test(storageId)) {
|
|
55
|
+
return {
|
|
56
|
+
table: stringToHex("_storage"),
|
|
57
|
+
internalId: storageId.toLowerCase(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
41
62
|
/**
|
|
42
63
|
* Check if an InternalDocumentId uses the component format (has tableNumber).
|
|
43
64
|
*/
|
package/dist/router/router.d.ts
CHANGED
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
* Runtime adapter for Concave CLI
|
|
3
3
|
* This is a minimal runtime library used by the generated entry module
|
|
4
4
|
*/
|
|
5
|
+
/** Minimal execution context compatible with Cloudflare Workers, Bun, and Node.js */
|
|
6
|
+
export interface RuntimeExecutionContext {
|
|
7
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
8
|
+
passThroughOnException?(): void;
|
|
9
|
+
}
|
|
5
10
|
export type ConcaveContext = {
|
|
6
11
|
env: any;
|
|
7
12
|
request: Request;
|
|
8
|
-
ctx:
|
|
13
|
+
ctx: RuntimeExecutionContext;
|
|
9
14
|
};
|
|
10
15
|
export type UdfFunction = (ctx: ConcaveContext, args: unknown) => Promise<unknown>;
|
|
11
16
|
export interface ConcaveRouter {
|
|
12
|
-
fetch: (request: Request, env: any, ctx:
|
|
17
|
+
fetch: (request: Request, env: any, ctx: RuntimeExecutionContext) => Promise<Response>;
|
|
13
18
|
}
|
|
14
19
|
export interface RouterOptions {
|
|
15
20
|
/**
|
|
@@ -17,7 +22,7 @@ export interface RouterOptions {
|
|
|
17
22
|
* If provided, this handler is called first and can choose to handle the request
|
|
18
23
|
* or delegate to the default router by returning null
|
|
19
24
|
*/
|
|
20
|
-
customFetch?: (request: Request, env: any, ctx:
|
|
25
|
+
customFetch?: (request: Request, env: any, ctx: RuntimeExecutionContext, registry: Record<string, UdfFunction>) => Promise<Response | null>;
|
|
21
26
|
/**
|
|
22
27
|
* Base path for routes (default: "")
|
|
23
28
|
* Example: "/api" would make routes accessible at /api/convex/...
|