@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.
Files changed (36) hide show
  1. package/dist/auth/jwt.d.ts +5 -0
  2. package/dist/auth/jwt.js +37 -5
  3. package/dist/http/api-router.d.ts +3 -1
  4. package/dist/http/api-router.js +62 -15
  5. package/dist/http/http-handler.js +53 -4
  6. package/dist/interfaces/execution-context.d.ts +2 -1
  7. package/dist/kernel/blob-store-gateway.js +7 -7
  8. package/dist/kernel/context-storage.js +6 -0
  9. package/dist/kernel/syscalls/action-syscalls.js +4 -6
  10. package/dist/kernel/syscalls/database-syscalls.js +1 -1
  11. package/dist/kernel/syscalls/js-router.d.ts +2 -2
  12. package/dist/kernel/syscalls/kernel-syscalls.d.ts +1 -1
  13. package/dist/kernel/syscalls/query-syscalls.js +2 -2
  14. package/dist/kernel/syscalls/router.d.ts +3 -2
  15. package/dist/kernel/syscalls/utils.d.ts +3 -3
  16. package/dist/kernel/syscalls/utils.js +3 -2
  17. package/dist/queryengine/developer-id.d.ts +8 -0
  18. package/dist/queryengine/developer-id.js +23 -2
  19. package/dist/router/router.d.ts +8 -3
  20. package/dist/runtime/runtime-context.d.ts +1 -1
  21. package/dist/scheduler/cron-executor.d.ts +6 -2
  22. package/dist/scheduler/cron-executor.js +54 -26
  23. package/dist/scheduler/scheduled-function-executor.d.ts +6 -2
  24. package/dist/scheduler/scheduled-function-executor.js +58 -36
  25. package/dist/sync/protocol-handler.d.ts +25 -0
  26. package/dist/sync/protocol-handler.js +111 -1
  27. package/dist/transactor/occ-transaction.js +3 -3
  28. package/dist/udf/execution-adapter.d.ts +1 -1
  29. package/dist/udf/execution-adapter.js +2 -2
  30. package/dist/udf/executor/inline.d.ts +1 -1
  31. package/dist/udf/executor/inline.js +3 -3
  32. package/dist/udf/executor/interface.d.ts +5 -2
  33. package/dist/udf/runtime/udf-setup.d.ts +4 -4
  34. package/dist/udf/runtime/udf-setup.js +11 -9
  35. package/dist/utils/long.d.ts +1 -1
  36. package/package.json +1 -1
@@ -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
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
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
- return cached;
348
+ JWKS_CACHE.delete(jwksUrl);
331
349
  }
332
350
  const jwks = createRemoteJWKSet(new URL(jwksUrl));
333
- JWKS_CACHE.set(jwksUrl, jwks);
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?: HeadersInit;
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;
@@ -1,6 +1,5 @@
1
1
  import { AdminAuthError, assertAdminToken, assertSystemToken, identityFromToken, isAdminToken, isSystemToken, JWTValidationError, SystemAuthError, } from "../auth";
2
- import { stringToHex, writtenTablesFromRanges } from "../utils";
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
- let storageId = decodeURIComponent(routeSegments[1]);
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: route,
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
- (route === "mutation" || route === "action") &&
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
- if (body.type !== "query" && body.type !== "mutation" && body.type !== "action") {
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, body.type, authForExecution, body.componentPath);
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 ((body.type === "mutation" || body.type === "action") && result.writtenRanges?.length) {
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?: any;
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 { parseDeveloperId } from "../queryengine/developer-id";
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 = parseDeveloperId(storageId);
54
+ const docId = parseStorageId(storageId);
55
55
  if (!docId) {
56
- console.error(`[BlobStoreGateway] Failed to parse storage ID: ${storageId}`);
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.error(`[BlobStoreGateway] Document not found for storage ID: ${storageId} (table: ${docId.table}, internalId: ${docId.internalId})`);
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.error(`[BlobStoreGateway] Blob not found in storage: ${docId.internalId}`);
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 = parseDeveloperId(storageId);
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 = parseDeveloperId(storageId);
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
- const { query: vectorQuery } = args;
46
- return this.queryRuntime.runVectorSearchAction(vectorQuery);
45
+ return this.queryRuntime.runVectorSearchAction(args.query);
47
46
  }
48
47
  async handleSearchAction(args) {
49
- const { query: searchQuery } = args;
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, any>) => Promise<any>;
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, any>): Promise<any>;
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, any>): Promise<any>;
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 { queryId } = args;
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 { queryId } = args;
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 SyncSyscallHandler = (args: any) => any;
2
- type AsyncSyscallHandler = (args: any) => Promise<any>;
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: any): any;
9
- export declare function resolveFunctionTarget(callArgs: any, currentComponentPath: string): FunctionTarget;
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: any): any;
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
- if (typeof callArgs.functionHandle.handle === "string") {
41
- return parseFunctionHandleTarget(callArgs.functionHandle.handle);
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
  */
@@ -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: ExecutionContext;
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: ExecutionContext) => Promise<Response>;
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: ExecutionContext, registry: Record<string, UdfFunction>) => Promise<Response | null>;
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/...