@lunora/runtime 1.0.0-alpha.26 → 1.0.0-alpha.27

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/index.d.mts CHANGED
@@ -2348,6 +2348,17 @@ interface WorkerOptions {
2348
2348
  */
2349
2349
  vectorIntrospector?: VectorIntrospector;
2350
2350
  /**
2351
+ * Voice-session Durable Object namespaces, keyed by the agent's
2352
+ * `lunora/agents.ts` export name (e.g. `{ support: env.VOICE_SUPPORT }`).
2353
+ * Codegen wires this for every voice-enabled agent. When set, the worker
2354
+ * exposes `/_lunora/voice/<agentExportName>` — a WebSocket upgrade that
2355
+ * resolves the caller's identity, forwards it on the server-minted
2356
+ * `x-lunora-userid` / `x-lunora-identity` headers, and hands the socket to
2357
+ * the agent's `VoiceSessionDO`. Omit it (voice-free apps) and the route does
2358
+ * not exist.
2359
+ */
2360
+ voiceAgents?: Record<string, ShardNamespaceLike>;
2361
+ /**
2351
2362
  * Resolver for the Cloudflare Workflows REST client, built from the
2352
2363
  * deployment `env` (its `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN`).
2353
2364
  * Set by the codegen-emitted worker entry (which depends on
package/dist/index.d.ts CHANGED
@@ -2348,6 +2348,17 @@ interface WorkerOptions {
2348
2348
  */
2349
2349
  vectorIntrospector?: VectorIntrospector;
2350
2350
  /**
2351
+ * Voice-session Durable Object namespaces, keyed by the agent's
2352
+ * `lunora/agents.ts` export name (e.g. `{ support: env.VOICE_SUPPORT }`).
2353
+ * Codegen wires this for every voice-enabled agent. When set, the worker
2354
+ * exposes `/_lunora/voice/&lt;agentExportName>` — a WebSocket upgrade that
2355
+ * resolves the caller's identity, forwards it on the server-minted
2356
+ * `x-lunora-userid` / `x-lunora-identity` headers, and hands the socket to
2357
+ * the agent's `VoiceSessionDO`. Omit it (voice-free apps) and the route does
2358
+ * not exist.
2359
+ */
2360
+ voiceAgents?: Record<string, ShardNamespaceLike>;
2361
+ /**
2351
2362
  * Resolver for the Cloudflare Workflows REST client, built from the
2352
2363
  * deployment `env` (its `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN`).
2353
2364
  * Set by the codegen-emitted worker entry (which depends on
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  export { toAirbyteMessages, toFivetranResponse } from './packem_shared/toAirbyteMessages-DrHdplb4.mjs';
2
- export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-BMxMYwKq.mjs';
2
+ export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-UZKPkF3Q.mjs';
3
3
  export { createCrossShardRelationCapabilities } from './packem_shared/createCrossShardRelationCapabilities-CbcWjkAn.mjs';
4
4
  export { DEFAULT_REGISTRY_CACHE_TTL_MS, SHARD_REGISTRY_DO_NAME, createDynamicShardRegistry } from './packem_shared/DEFAULT_REGISTRY_CACHE_TTL_MS-B3pA7aXp.mjs';
5
5
  export { LunoraError, toErrorResponse } from './packem_shared/LunoraError-Bpb9EFJ3.mjs';
@@ -1917,12 +1917,24 @@ const NDJSON_ENCODER = new TextEncoder();
1917
1917
  const RPC_PATH = "/_lunora/rpc";
1918
1918
  const RPC_BATCH_PATH = "/_lunora/rpc-batch";
1919
1919
  const WS_PATH = "/_lunora/ws";
1920
+ const VOICE_PATH_PREFIX = "/_lunora/voice/";
1920
1921
  const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
1921
1922
  const CRON_JOBS_RUN_PATH = "/_lunora/admin/cron-jobs/run";
1922
1923
  const ADMIN_PATH_PREFIX = "/_lunora/admin/";
1923
1924
  const MIGRATE_PATH = "/_lunora/migrate";
1924
1925
  const STATUS_PATH = "/_lunora/status";
1925
1926
  const isAdminPath = (pathname) => pathname.startsWith(ADMIN_PATH_PREFIX) || pathname === MIGRATE_PATH;
1927
+ const readForwardedIdentity = (request) => {
1928
+ const forwardedUserId = request.headers.get("x-lunora-userid");
1929
+ const forwardedIdentity = request.headers.get("x-lunora-identity");
1930
+ if (forwardedUserId === null && forwardedIdentity === null) {
1931
+ return void 0;
1932
+ }
1933
+ return {
1934
+ ...forwardedIdentity === null ? {} : { identity: forwardedIdentity },
1935
+ ...forwardedUserId === null ? {} : { userId: forwardedUserId }
1936
+ };
1937
+ };
1926
1938
  const DEFAULT_AUTH_BASE_PATH = "/api/auth";
1927
1939
  const RECORD_AUTH_EVENT_OP = "__lunora_admin__:recordAuthEvent";
1928
1940
  const AUTH_ATTEMPT_SEGMENTS = ["/sign-in", "/sign-up", "/callback"];
@@ -2240,7 +2252,7 @@ const createWorker = (options) => {
2240
2252
  resolveForwardContext: resolveAdminForwardContext,
2241
2253
  shardDO
2242
2254
  });
2243
- const dispatchToShard = async (functionPath, args, shardKey, mutationId) => {
2255
+ const dispatchToShard = async (functionPath, args, shardKey, mutationId, forwardedIdentity) => {
2244
2256
  if (options.authorizeShard) {
2245
2257
  const allowed = await options.authorizeShard(null, shardKey);
2246
2258
  if (!allowed) {
@@ -2248,6 +2260,12 @@ const createWorker = (options) => {
2248
2260
  }
2249
2261
  }
2250
2262
  const headers = { "content-type": "application/json", "x-lunora-system": "1" };
2263
+ if (forwardedIdentity?.userId !== void 0 && forwardedIdentity.userId.length > 0) {
2264
+ headers["x-lunora-userid"] = forwardedIdentity.userId;
2265
+ }
2266
+ if (forwardedIdentity?.identity !== void 0 && forwardedIdentity.identity.length > 0) {
2267
+ headers["x-lunora-identity"] = forwardedIdentity.identity;
2268
+ }
2251
2269
  if (mutationId !== void 0 && mutationId.length > 0) {
2252
2270
  headers["x-lunora-mutation-id"] = mutationId;
2253
2271
  }
@@ -2258,16 +2276,17 @@ const createWorker = (options) => {
2258
2276
  });
2259
2277
  return forwardToShard(shardDO, shardKey, forwarded);
2260
2278
  };
2261
- const startCronWorkflow = async (binding, job, env) => {
2279
+ const startWorkflowInstance = async (binding, args, env, label) => {
2262
2280
  const candidate = env?.[binding];
2263
2281
  if (!candidate || typeof candidate.create !== "function") {
2264
- throw new LunoraError(`cron job "${job.name}" targets workflow binding "${binding}", which is not bound on env`, {
2282
+ throw new LunoraError(`${label} targets workflow binding "${binding}", which is not bound on env`, {
2265
2283
  code: "CRON_JOB_FAILED",
2266
2284
  status: 500
2267
2285
  });
2268
2286
  }
2269
- await candidate.create({ params: job.args ?? {} });
2287
+ await candidate.create({ params: args });
2270
2288
  };
2289
+ const startCronWorkflow = async (binding, job, env) => startWorkflowInstance(binding, job.args ?? {}, env, `cron job "${job.name}"`);
2271
2290
  const runOneCronJob = async (job, env) => {
2272
2291
  if (job.workflow) {
2273
2292
  await startCronWorkflow(job.workflow, job, env);
@@ -2364,13 +2383,18 @@ const createWorker = (options) => {
2364
2383
  throw new LunoraError("Scheduler dispatch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
2365
2384
  }
2366
2385
  const candidate = body ?? {};
2386
+ const args = candidate.args ?? {};
2387
+ if (typeof candidate.workflow === "string" && candidate.workflow.length > 0) {
2388
+ await startWorkflowInstance(candidate.workflow, args, env, "scheduled workflow");
2389
+ return Response.json({ ok: true }, { status: 200 });
2390
+ }
2367
2391
  if (typeof candidate.functionPath !== "string" || candidate.functionPath.length === 0) {
2368
2392
  throw new LunoraError("Scheduler dispatch is missing `functionPath`", { code: "BAD_REQUEST", status: 400 });
2369
2393
  }
2370
- const args = candidate.args ?? {};
2371
2394
  const shardKey = typeof candidate.shardKey === "string" && candidate.shardKey.length > 0 ? candidate.shardKey : defaultShard;
2372
2395
  const mutationId = typeof candidate.id === "string" && candidate.id.length > 0 ? candidate.id : void 0;
2373
- const response = await dispatchToShard(candidate.functionPath, args, shardKey, mutationId);
2396
+ const identity = readForwardedIdentity(request);
2397
+ const response = await dispatchToShard(candidate.functionPath, args, shardKey, mutationId, identity);
2374
2398
  await releasePoolSlot(candidate);
2375
2399
  return response;
2376
2400
  };
@@ -2563,6 +2587,59 @@ const createWorker = (options) => {
2563
2587
  }
2564
2588
  return forwardToShard(shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
2565
2589
  };
2590
+ const handleVoiceUpgrade = async (request, env, url) => {
2591
+ const { voiceAgents } = options;
2592
+ if (voiceAgents === void 0) {
2593
+ return new Response("Not found", { status: 404 });
2594
+ }
2595
+ if (request.headers.get("Upgrade") !== "websocket") {
2596
+ return new Response("Expected a WebSocket upgrade", { headers: { allow: "GET" }, status: 426 });
2597
+ }
2598
+ const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
2599
+ if (blockedUpgrade) {
2600
+ return blockedUpgrade;
2601
+ }
2602
+ let agentName;
2603
+ try {
2604
+ agentName = decodeURIComponent(url.pathname.slice(VOICE_PATH_PREFIX.length));
2605
+ } catch {
2606
+ return new Response("Unknown voice agent", { status: 404 });
2607
+ }
2608
+ const namespace = Object.hasOwn(voiceAgents, agentName) ? voiceAgents[agentName] : void 0;
2609
+ if (namespace === void 0) {
2610
+ return new Response("Unknown voice agent", { status: 404 });
2611
+ }
2612
+ const threadKey = url.searchParams.get("threadKey");
2613
+ if (threadKey === null || threadKey.length === 0) {
2614
+ return new Response("Missing threadKey", { status: 400 });
2615
+ }
2616
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2617
+ if (options.authorizeShard) {
2618
+ const allowed = await options.authorizeShard(identity, threadKey);
2619
+ if (!allowed) {
2620
+ return new Response("Forbidden", { status: 403 });
2621
+ }
2622
+ } else {
2623
+ guardUnauthenticatedShardAccess("shard");
2624
+ }
2625
+ const upgradeHeaders = new Headers(request.headers);
2626
+ upgradeHeaders.delete("x-lunora-userid");
2627
+ upgradeHeaders.delete("x-lunora-identity");
2628
+ upgradeHeaders.delete("x-lunora-identity-exp");
2629
+ const forwardedUserId = forwardedHeaders["x-lunora-userid"];
2630
+ const forwardedIdentity = forwardedHeaders["x-lunora-identity"];
2631
+ const forwardedExp = forwardedHeaders["x-lunora-identity-exp"];
2632
+ if (forwardedUserId !== void 0) {
2633
+ upgradeHeaders.set("x-lunora-userid", forwardedUserId);
2634
+ }
2635
+ if (forwardedIdentity !== void 0) {
2636
+ upgradeHeaders.set("x-lunora-identity", forwardedIdentity);
2637
+ }
2638
+ if (forwardedExp !== void 0) {
2639
+ upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
2640
+ }
2641
+ return forwardToShard(namespace, threadKey, new Request(request, { headers: upgradeHeaders }));
2642
+ };
2566
2643
  const authorizeFanOutEnvelope = async (fanOut, functionPath, identity) => {
2567
2644
  if (options.authorizeFanOut) {
2568
2645
  const allowed = await options.authorizeFanOut(identity, fanOut.table, functionPath);
@@ -3091,6 +3168,9 @@ const createWorker = (options) => {
3091
3168
  await applyAdminGate(request, url.pathname);
3092
3169
  return internalRoute(request, env, url, context);
3093
3170
  }
3171
+ if (options.voiceAgents !== void 0 && url.pathname.startsWith(VOICE_PATH_PREFIX)) {
3172
+ return handleVoiceUpgrade(request, env, url);
3173
+ }
3094
3174
  const httpRouteResponse = await dispatchHttpRoute(request, env, context);
3095
3175
  if (httpRouteResponse) {
3096
3176
  return httpRouteResponse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "1.0.0-alpha.26",
3
+ "version": "1.0.0-alpha.27",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
5
  "keywords": [
6
6
  "cloudflare",