@lunora/runtime 1.0.0-alpha.26 → 1.0.0-alpha.28
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
|
@@ -425,13 +425,6 @@ interface AuthAdmin {
|
|
|
425
425
|
userId: string;
|
|
426
426
|
}) => Promise<AuthUser>;
|
|
427
427
|
}
|
|
428
|
-
/**
|
|
429
|
-
* Read-only subset of {@link AuthAdmin}, kept as an alias for the former
|
|
430
|
-
* `authIntrospector` option (which the worker still honours as a browse-only
|
|
431
|
-
* fallback). Prefer wiring `authAdmin` with `@lunora/auth`'s `createAuthAdmin`
|
|
432
|
-
* so the mutation endpoints light up too.
|
|
433
|
-
*/
|
|
434
|
-
type AuthIntrospector = Pick<AuthAdmin, "listSessions" | "listUsers">;
|
|
435
428
|
/** Closure-scoped worker helpers the auth routes borrow (so this module stays out of the worker's god-closure). */
|
|
436
429
|
/**
|
|
437
430
|
* A compact, transport-safe description of one function argument — the runtime
|
|
@@ -2021,12 +2014,6 @@ interface WorkerOptions {
|
|
|
2021
2014
|
*/
|
|
2022
2015
|
authHandler?: (request: Request) => Promise<Response | undefined>;
|
|
2023
2016
|
/**
|
|
2024
|
-
* @deprecated Use {@link WorkerOptions.authAdmin} (an {@link AuthAdmin}),
|
|
2025
|
-
* which also lights up the user-management mutation endpoints. Still honored
|
|
2026
|
-
* as a read-only fallback for the browse endpoints.
|
|
2027
|
-
*/
|
|
2028
|
-
authIntrospector?: AuthIntrospector;
|
|
2029
|
-
/**
|
|
2030
2017
|
* Optional table-level authorization callback for fan-out RPC envelopes.
|
|
2031
2018
|
* Called after `resolveIdentity` and before `coordinator.fanOut` walks
|
|
2032
2019
|
* the registry. Returning `false` rejects the request with 403
|
|
@@ -2250,6 +2237,20 @@ interface WorkerOptions {
|
|
|
2250
2237
|
*/
|
|
2251
2238
|
queue?: QueueConsumerHandler;
|
|
2252
2239
|
/**
|
|
2240
|
+
* Enforce the ephemeral WS admin token: when `true`,
|
|
2241
|
+
* the worker's WS admin gate rejects the raw master admin token in the
|
|
2242
|
+
* `?token=` query parameter — only a short-lived sub-token minted by
|
|
2243
|
+
* `POST /_lunora/admin/ws-token` (or the master token in the
|
|
2244
|
+
* `Authorization` HEADER, which never leaks via URLs) authorizes. Off by
|
|
2245
|
+
* default (the master token in `?token=` keeps working); also settable per
|
|
2246
|
+
* deployment via `env.LUNORA_REQUIRE_EPHEMERAL_WS_TOKEN`
|
|
2247
|
+
* (`1`/`true`/`on`/`yes`/`enabled`), which the shard/relay Durable Objects
|
|
2248
|
+
* honor for their own upgrade gate too. Flipping it on is the step that
|
|
2249
|
+
* actually closes the URL/log leak — do so once every studio the
|
|
2250
|
+
* deployment uses mints ephemeral tokens.
|
|
2251
|
+
*/
|
|
2252
|
+
requireEphemeralWsToken?: boolean;
|
|
2253
|
+
/**
|
|
2253
2254
|
* Resolve the calling identity from the inbound RPC request. Called once
|
|
2254
2255
|
* per RPC (and per fan-out) before the request is forwarded to the
|
|
2255
2256
|
* shard. The returned `userId` becomes `ctx.auth.userId` on the shard
|
|
@@ -2348,6 +2349,17 @@ interface WorkerOptions {
|
|
|
2348
2349
|
*/
|
|
2349
2350
|
vectorIntrospector?: VectorIntrospector;
|
|
2350
2351
|
/**
|
|
2352
|
+
* Voice-session Durable Object namespaces, keyed by the agent's
|
|
2353
|
+
* `lunora/agents.ts` export name (e.g. `{ support: env.VOICE_SUPPORT }`).
|
|
2354
|
+
* Codegen wires this for every voice-enabled agent. When set, the worker
|
|
2355
|
+
* exposes `/_lunora/voice/<agentExportName>` — a WebSocket upgrade that
|
|
2356
|
+
* resolves the caller's identity, forwards it on the server-minted
|
|
2357
|
+
* `x-lunora-userid` / `x-lunora-identity` headers, and hands the socket to
|
|
2358
|
+
* the agent's `VoiceSessionDO`. Omit it (voice-free apps) and the route does
|
|
2359
|
+
* not exist.
|
|
2360
|
+
*/
|
|
2361
|
+
voiceAgents?: Record<string, ShardNamespaceLike>;
|
|
2362
|
+
/**
|
|
2351
2363
|
* Resolver for the Cloudflare Workflows REST client, built from the
|
|
2352
2364
|
* deployment `env` (its `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN`).
|
|
2353
2365
|
* Set by the codegen-emitted worker entry (which depends on
|
|
@@ -2851,4 +2863,4 @@ declare const otlpSink: (options: OtlpSinkOptions) => ObservabilitySink;
|
|
|
2851
2863
|
*/
|
|
2852
2864
|
declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
|
|
2853
2865
|
declare const VERSION: string;
|
|
2854
|
-
export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type
|
|
2866
|
+
export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type AuthPage, type AuthSession, type AuthUser, type AuthUserFieldSpec, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, type ListAuthUsersOptions, type LogEvent, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type OtlpSinkOptions, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, otlpSink, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
|
package/dist/index.d.ts
CHANGED
|
@@ -425,13 +425,6 @@ interface AuthAdmin {
|
|
|
425
425
|
userId: string;
|
|
426
426
|
}) => Promise<AuthUser>;
|
|
427
427
|
}
|
|
428
|
-
/**
|
|
429
|
-
* Read-only subset of {@link AuthAdmin}, kept as an alias for the former
|
|
430
|
-
* `authIntrospector` option (which the worker still honours as a browse-only
|
|
431
|
-
* fallback). Prefer wiring `authAdmin` with `@lunora/auth`'s `createAuthAdmin`
|
|
432
|
-
* so the mutation endpoints light up too.
|
|
433
|
-
*/
|
|
434
|
-
type AuthIntrospector = Pick<AuthAdmin, "listSessions" | "listUsers">;
|
|
435
428
|
/** Closure-scoped worker helpers the auth routes borrow (so this module stays out of the worker's god-closure). */
|
|
436
429
|
/**
|
|
437
430
|
* A compact, transport-safe description of one function argument — the runtime
|
|
@@ -2021,12 +2014,6 @@ interface WorkerOptions {
|
|
|
2021
2014
|
*/
|
|
2022
2015
|
authHandler?: (request: Request) => Promise<Response | undefined>;
|
|
2023
2016
|
/**
|
|
2024
|
-
* @deprecated Use {@link WorkerOptions.authAdmin} (an {@link AuthAdmin}),
|
|
2025
|
-
* which also lights up the user-management mutation endpoints. Still honored
|
|
2026
|
-
* as a read-only fallback for the browse endpoints.
|
|
2027
|
-
*/
|
|
2028
|
-
authIntrospector?: AuthIntrospector;
|
|
2029
|
-
/**
|
|
2030
2017
|
* Optional table-level authorization callback for fan-out RPC envelopes.
|
|
2031
2018
|
* Called after `resolveIdentity` and before `coordinator.fanOut` walks
|
|
2032
2019
|
* the registry. Returning `false` rejects the request with 403
|
|
@@ -2250,6 +2237,20 @@ interface WorkerOptions {
|
|
|
2250
2237
|
*/
|
|
2251
2238
|
queue?: QueueConsumerHandler;
|
|
2252
2239
|
/**
|
|
2240
|
+
* Enforce the ephemeral WS admin token: when `true`,
|
|
2241
|
+
* the worker's WS admin gate rejects the raw master admin token in the
|
|
2242
|
+
* `?token=` query parameter — only a short-lived sub-token minted by
|
|
2243
|
+
* `POST /_lunora/admin/ws-token` (or the master token in the
|
|
2244
|
+
* `Authorization` HEADER, which never leaks via URLs) authorizes. Off by
|
|
2245
|
+
* default (the master token in `?token=` keeps working); also settable per
|
|
2246
|
+
* deployment via `env.LUNORA_REQUIRE_EPHEMERAL_WS_TOKEN`
|
|
2247
|
+
* (`1`/`true`/`on`/`yes`/`enabled`), which the shard/relay Durable Objects
|
|
2248
|
+
* honor for their own upgrade gate too. Flipping it on is the step that
|
|
2249
|
+
* actually closes the URL/log leak — do so once every studio the
|
|
2250
|
+
* deployment uses mints ephemeral tokens.
|
|
2251
|
+
*/
|
|
2252
|
+
requireEphemeralWsToken?: boolean;
|
|
2253
|
+
/**
|
|
2253
2254
|
* Resolve the calling identity from the inbound RPC request. Called once
|
|
2254
2255
|
* per RPC (and per fan-out) before the request is forwarded to the
|
|
2255
2256
|
* shard. The returned `userId` becomes `ctx.auth.userId` on the shard
|
|
@@ -2348,6 +2349,17 @@ interface WorkerOptions {
|
|
|
2348
2349
|
*/
|
|
2349
2350
|
vectorIntrospector?: VectorIntrospector;
|
|
2350
2351
|
/**
|
|
2352
|
+
* Voice-session Durable Object namespaces, keyed by the agent's
|
|
2353
|
+
* `lunora/agents.ts` export name (e.g. `{ support: env.VOICE_SUPPORT }`).
|
|
2354
|
+
* Codegen wires this for every voice-enabled agent. When set, the worker
|
|
2355
|
+
* exposes `/_lunora/voice/<agentExportName>` — a WebSocket upgrade that
|
|
2356
|
+
* resolves the caller's identity, forwards it on the server-minted
|
|
2357
|
+
* `x-lunora-userid` / `x-lunora-identity` headers, and hands the socket to
|
|
2358
|
+
* the agent's `VoiceSessionDO`. Omit it (voice-free apps) and the route does
|
|
2359
|
+
* not exist.
|
|
2360
|
+
*/
|
|
2361
|
+
voiceAgents?: Record<string, ShardNamespaceLike>;
|
|
2362
|
+
/**
|
|
2351
2363
|
* Resolver for the Cloudflare Workflows REST client, built from the
|
|
2352
2364
|
* deployment `env` (its `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN`).
|
|
2353
2365
|
* Set by the codegen-emitted worker entry (which depends on
|
|
@@ -2851,4 +2863,4 @@ declare const otlpSink: (options: OtlpSinkOptions) => ObservabilitySink;
|
|
|
2851
2863
|
*/
|
|
2852
2864
|
declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
|
|
2853
2865
|
declare const VERSION: string;
|
|
2854
|
-
export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type
|
|
2866
|
+
export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type AuthPage, type AuthSession, type AuthUser, type AuthUserFieldSpec, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, type ListAuthUsersOptions, type LogEvent, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type OtlpSinkOptions, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, otlpSink, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
|
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-
|
|
2
|
+
export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-B6Pzfwhg.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';
|
|
@@ -21,6 +21,75 @@ const evictOldestEntry = (map, capacity) => {
|
|
|
21
21
|
const RELAY_NAME_INFIX = "::relay::";
|
|
22
22
|
const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
|
|
23
23
|
|
|
24
|
+
const textEncoder = new TextEncoder();
|
|
25
|
+
const toBase64Url = (bytes) => {
|
|
26
|
+
const binary = String.fromCodePoint(...bytes);
|
|
27
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
28
|
+
};
|
|
29
|
+
const fromBase64Url = (input) => {
|
|
30
|
+
const padded = input.replaceAll("-", "+").replaceAll("_", "/") + "===".slice((input.length + 3) % 4);
|
|
31
|
+
const binary = atob(padded);
|
|
32
|
+
const bytes = new Uint8Array(binary.length);
|
|
33
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
34
|
+
bytes[index] = binary.codePointAt(index) ?? 0;
|
|
35
|
+
}
|
|
36
|
+
return bytes;
|
|
37
|
+
};
|
|
38
|
+
const KEY_CACHE_MAX = 64;
|
|
39
|
+
const keyCache = /* @__PURE__ */ new Map();
|
|
40
|
+
const importHmacKey = async (secret) => {
|
|
41
|
+
const cached = keyCache.get(secret);
|
|
42
|
+
if (cached) {
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
evictOldestEntry(keyCache, KEY_CACHE_MAX);
|
|
46
|
+
const keyPromise = crypto.subtle.importKey("raw", textEncoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign", "verify"]);
|
|
47
|
+
keyCache.set(secret, keyPromise);
|
|
48
|
+
return keyPromise;
|
|
49
|
+
};
|
|
50
|
+
const signCanonical = async (secret, canonical) => {
|
|
51
|
+
const cryptoKey = await importHmacKey(secret);
|
|
52
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(canonical));
|
|
53
|
+
return toBase64Url(new Uint8Array(signature));
|
|
54
|
+
};
|
|
55
|
+
const verifyCanonical = async (secret, canonical, sigBytes) => {
|
|
56
|
+
const cryptoKey = await importHmacKey(secret);
|
|
57
|
+
return crypto.subtle.verify("HMAC", cryptoKey, sigBytes, textEncoder.encode(canonical));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const WS_ADMIN_TOKEN_VERSION = "v1";
|
|
61
|
+
const WS_ADMIN_TOKEN_TTL_MS = 6e4;
|
|
62
|
+
const mintWsAdminToken = async (secret, options = {}) => {
|
|
63
|
+
const expiresAtMs = (options.now ?? Date.now()) + (options.ttlMs ?? WS_ADMIN_TOKEN_TTL_MS);
|
|
64
|
+
const canonical = `${WS_ADMIN_TOKEN_VERSION}.${String(expiresAtMs)}`;
|
|
65
|
+
const signature = await signCanonical(secret, canonical);
|
|
66
|
+
return { expiresAtMs, token: `${canonical}.${signature}` };
|
|
67
|
+
};
|
|
68
|
+
const verifyWsAdminToken = async (secret, token, now = Date.now()) => {
|
|
69
|
+
if (secret.length === 0 || token.length === 0) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const parts = token.split(".");
|
|
73
|
+
if (parts.length !== 3) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const [version, expString, signature] = parts;
|
|
77
|
+
if (version !== WS_ADMIN_TOKEN_VERSION || signature.length === 0) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const expiresAtMs = Number(expString);
|
|
81
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
let signatureBytes;
|
|
85
|
+
try {
|
|
86
|
+
signatureBytes = fromBase64Url(signature);
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return verifyCanonical(secret, `${version}.${expString}`, signatureBytes);
|
|
91
|
+
};
|
|
92
|
+
|
|
24
93
|
const AUTH_BASE = "/_lunora/admin/auth";
|
|
25
94
|
const AUTH_ADMIN_ERROR_STATUS = {
|
|
26
95
|
INVITER_REQUIRED: 400,
|
|
@@ -1615,11 +1684,11 @@ const buildScheduledAdminRoutes = (deps) => {
|
|
|
1615
1684
|
const stub = resolveSchedulerStub(request);
|
|
1616
1685
|
return stub.fetch(new Request("https://scheduler.internal/status", { method: "GET" }));
|
|
1617
1686
|
};
|
|
1618
|
-
const handleScheduledWebSocket = (request) => {
|
|
1687
|
+
const handleScheduledWebSocket = async (request) => {
|
|
1619
1688
|
if (request.headers.get("Upgrade") !== "websocket") {
|
|
1620
1689
|
throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
|
|
1621
1690
|
}
|
|
1622
|
-
if (!checkWsAdmin(request)) {
|
|
1691
|
+
if (!await checkWsAdmin(request)) {
|
|
1623
1692
|
throw new LunoraError("admin authorization required", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
1624
1693
|
}
|
|
1625
1694
|
const namespace = requireSchedulerNamespace();
|
|
@@ -1917,12 +1986,26 @@ const NDJSON_ENCODER = new TextEncoder();
|
|
|
1917
1986
|
const RPC_PATH = "/_lunora/rpc";
|
|
1918
1987
|
const RPC_BATCH_PATH = "/_lunora/rpc-batch";
|
|
1919
1988
|
const WS_PATH = "/_lunora/ws";
|
|
1989
|
+
const VOICE_PATH_PREFIX = "/_lunora/voice/";
|
|
1920
1990
|
const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
|
|
1921
1991
|
const CRON_JOBS_RUN_PATH = "/_lunora/admin/cron-jobs/run";
|
|
1992
|
+
const ADMIN_WS_TOKEN_PATH = "/_lunora/admin/ws-token";
|
|
1922
1993
|
const ADMIN_PATH_PREFIX = "/_lunora/admin/";
|
|
1923
1994
|
const MIGRATE_PATH = "/_lunora/migrate";
|
|
1924
1995
|
const STATUS_PATH = "/_lunora/status";
|
|
1925
1996
|
const isAdminPath = (pathname) => pathname.startsWith(ADMIN_PATH_PREFIX) || pathname === MIGRATE_PATH;
|
|
1997
|
+
const REQUIRE_EPHEMERAL_ENV_VALUES = /* @__PURE__ */ new Set(["1", "enabled", "on", "true", "yes"]);
|
|
1998
|
+
const readForwardedIdentity = (request) => {
|
|
1999
|
+
const forwardedUserId = request.headers.get("x-lunora-userid");
|
|
2000
|
+
const forwardedIdentity = request.headers.get("x-lunora-identity");
|
|
2001
|
+
if (forwardedUserId === null && forwardedIdentity === null) {
|
|
2002
|
+
return void 0;
|
|
2003
|
+
}
|
|
2004
|
+
return {
|
|
2005
|
+
...forwardedIdentity === null ? {} : { identity: forwardedIdentity },
|
|
2006
|
+
...forwardedUserId === null ? {} : { userId: forwardedUserId }
|
|
2007
|
+
};
|
|
2008
|
+
};
|
|
1926
2009
|
const DEFAULT_AUTH_BASE_PATH = "/api/auth";
|
|
1927
2010
|
const RECORD_AUTH_EVENT_OP = "__lunora_admin__:recordAuthEvent";
|
|
1928
2011
|
const AUTH_ATTEMPT_SEGMENTS = ["/sign-in", "/sign-up", "/callback"];
|
|
@@ -2176,12 +2259,21 @@ const checkAdminAuth = (request, expected) => {
|
|
|
2176
2259
|
}
|
|
2177
2260
|
return constantTimeEqual(expected, rest.join(" ").trim());
|
|
2178
2261
|
};
|
|
2179
|
-
const checkAdminWsToken = (request, expected) => {
|
|
2262
|
+
const checkAdminWsToken = async (request, expected, requireEphemeral) => {
|
|
2180
2263
|
if (!expected || expected.length === 0) {
|
|
2181
2264
|
return false;
|
|
2182
2265
|
}
|
|
2183
2266
|
const supplied = new URL(request.url).searchParams.get("token");
|
|
2184
|
-
|
|
2267
|
+
if (supplied === null) {
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
if (await verifyWsAdminToken(expected, supplied)) {
|
|
2271
|
+
return true;
|
|
2272
|
+
}
|
|
2273
|
+
if (requireEphemeral) {
|
|
2274
|
+
return false;
|
|
2275
|
+
}
|
|
2276
|
+
return constantTimeEqual(expected, supplied);
|
|
2185
2277
|
};
|
|
2186
2278
|
const createWorker = (options) => {
|
|
2187
2279
|
const defaultShard = options.defaultShardKey ?? "__root__";
|
|
@@ -2190,11 +2282,20 @@ const createWorker = (options) => {
|
|
|
2190
2282
|
const schedulerDO = options.schedulerDO === void 0 ? void 0 : applyJurisdiction(options.schedulerDO, options.jurisdiction);
|
|
2191
2283
|
let envAdminToken;
|
|
2192
2284
|
const effectiveAdminToken = () => options.adminToken ?? envAdminToken;
|
|
2285
|
+
let envRequireEphemeralWsToken;
|
|
2286
|
+
const effectiveRequireEphemeralWsToken = () => options.requireEphemeralWsToken ?? envRequireEphemeralWsToken ?? false;
|
|
2193
2287
|
const resolveAdminTokenFromEnv = (env) => {
|
|
2288
|
+
const record = env ?? {};
|
|
2289
|
+
if (envRequireEphemeralWsToken === void 0 && options.requireEphemeralWsToken === void 0) {
|
|
2290
|
+
const raw = record["LUNORA_REQUIRE_EPHEMERAL_WS_TOKEN"];
|
|
2291
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
2292
|
+
envRequireEphemeralWsToken = REQUIRE_EPHEMERAL_ENV_VALUES.has(raw.trim().toLowerCase());
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2194
2295
|
if (envAdminToken !== void 0 || options.adminToken !== void 0) {
|
|
2195
2296
|
return;
|
|
2196
2297
|
}
|
|
2197
|
-
const value =
|
|
2298
|
+
const value = record["LUNORA_ADMIN_TOKEN"];
|
|
2198
2299
|
if (typeof value === "string" && value.length > 0) {
|
|
2199
2300
|
envAdminToken = value;
|
|
2200
2301
|
}
|
|
@@ -2240,7 +2341,7 @@ const createWorker = (options) => {
|
|
|
2240
2341
|
resolveForwardContext: resolveAdminForwardContext,
|
|
2241
2342
|
shardDO
|
|
2242
2343
|
});
|
|
2243
|
-
const dispatchToShard = async (functionPath, args, shardKey, mutationId) => {
|
|
2344
|
+
const dispatchToShard = async (functionPath, args, shardKey, mutationId, forwardedIdentity) => {
|
|
2244
2345
|
if (options.authorizeShard) {
|
|
2245
2346
|
const allowed = await options.authorizeShard(null, shardKey);
|
|
2246
2347
|
if (!allowed) {
|
|
@@ -2248,6 +2349,12 @@ const createWorker = (options) => {
|
|
|
2248
2349
|
}
|
|
2249
2350
|
}
|
|
2250
2351
|
const headers = { "content-type": "application/json", "x-lunora-system": "1" };
|
|
2352
|
+
if (forwardedIdentity?.userId !== void 0 && forwardedIdentity.userId.length > 0) {
|
|
2353
|
+
headers["x-lunora-userid"] = forwardedIdentity.userId;
|
|
2354
|
+
}
|
|
2355
|
+
if (forwardedIdentity?.identity !== void 0 && forwardedIdentity.identity.length > 0) {
|
|
2356
|
+
headers["x-lunora-identity"] = forwardedIdentity.identity;
|
|
2357
|
+
}
|
|
2251
2358
|
if (mutationId !== void 0 && mutationId.length > 0) {
|
|
2252
2359
|
headers["x-lunora-mutation-id"] = mutationId;
|
|
2253
2360
|
}
|
|
@@ -2258,16 +2365,17 @@ const createWorker = (options) => {
|
|
|
2258
2365
|
});
|
|
2259
2366
|
return forwardToShard(shardDO, shardKey, forwarded);
|
|
2260
2367
|
};
|
|
2261
|
-
const
|
|
2368
|
+
const startWorkflowInstance = async (binding, args, env, label) => {
|
|
2262
2369
|
const candidate = env?.[binding];
|
|
2263
2370
|
if (!candidate || typeof candidate.create !== "function") {
|
|
2264
|
-
throw new LunoraError(
|
|
2371
|
+
throw new LunoraError(`${label} targets workflow binding "${binding}", which is not bound on env`, {
|
|
2265
2372
|
code: "CRON_JOB_FAILED",
|
|
2266
2373
|
status: 500
|
|
2267
2374
|
});
|
|
2268
2375
|
}
|
|
2269
|
-
await candidate.create({ params:
|
|
2376
|
+
await candidate.create({ params: args });
|
|
2270
2377
|
};
|
|
2378
|
+
const startCronWorkflow = async (binding, job, env) => startWorkflowInstance(binding, job.args ?? {}, env, `cron job "${job.name}"`);
|
|
2271
2379
|
const runOneCronJob = async (job, env) => {
|
|
2272
2380
|
if (job.workflow) {
|
|
2273
2381
|
await startCronWorkflow(job.workflow, job, env);
|
|
@@ -2364,13 +2472,18 @@ const createWorker = (options) => {
|
|
|
2364
2472
|
throw new LunoraError("Scheduler dispatch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
|
|
2365
2473
|
}
|
|
2366
2474
|
const candidate = body ?? {};
|
|
2475
|
+
const args = candidate.args ?? {};
|
|
2476
|
+
if (typeof candidate.workflow === "string" && candidate.workflow.length > 0) {
|
|
2477
|
+
await startWorkflowInstance(candidate.workflow, args, env, "scheduled workflow");
|
|
2478
|
+
return Response.json({ ok: true }, { status: 200 });
|
|
2479
|
+
}
|
|
2367
2480
|
if (typeof candidate.functionPath !== "string" || candidate.functionPath.length === 0) {
|
|
2368
2481
|
throw new LunoraError("Scheduler dispatch is missing `functionPath`", { code: "BAD_REQUEST", status: 400 });
|
|
2369
2482
|
}
|
|
2370
|
-
const args = candidate.args ?? {};
|
|
2371
2483
|
const shardKey = typeof candidate.shardKey === "string" && candidate.shardKey.length > 0 ? candidate.shardKey : defaultShard;
|
|
2372
2484
|
const mutationId = typeof candidate.id === "string" && candidate.id.length > 0 ? candidate.id : void 0;
|
|
2373
|
-
const
|
|
2485
|
+
const identity = readForwardedIdentity(request);
|
|
2486
|
+
const response = await dispatchToShard(candidate.functionPath, args, shardKey, mutationId, identity);
|
|
2374
2487
|
await releasePoolSlot(candidate);
|
|
2375
2488
|
return response;
|
|
2376
2489
|
};
|
|
@@ -2423,7 +2536,7 @@ const createWorker = (options) => {
|
|
|
2423
2536
|
return resolveShard(requireSchedulerNamespace(), options.schedulerInstanceName ?? "default");
|
|
2424
2537
|
};
|
|
2425
2538
|
const scheduledAdminRoutes = buildScheduledAdminRoutes({
|
|
2426
|
-
checkWsAdmin: (request) => requestIsAdmin(request) || checkAdminWsToken(request, effectiveAdminToken()),
|
|
2539
|
+
checkWsAdmin: async (request) => requestIsAdmin(request) || checkAdminWsToken(request, effectiveAdminToken(), effectiveRequireEphemeralWsToken()),
|
|
2427
2540
|
requireSchedulerNamespace,
|
|
2428
2541
|
resolveSchedulerStub,
|
|
2429
2542
|
schedulerInstanceName: options.schedulerInstanceName ?? "default"
|
|
@@ -2563,6 +2676,59 @@ const createWorker = (options) => {
|
|
|
2563
2676
|
}
|
|
2564
2677
|
return forwardToShard(shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
|
|
2565
2678
|
};
|
|
2679
|
+
const handleVoiceUpgrade = async (request, env, url) => {
|
|
2680
|
+
const { voiceAgents } = options;
|
|
2681
|
+
if (voiceAgents === void 0) {
|
|
2682
|
+
return new Response("Not found", { status: 404 });
|
|
2683
|
+
}
|
|
2684
|
+
if (request.headers.get("Upgrade") !== "websocket") {
|
|
2685
|
+
return new Response("Expected a WebSocket upgrade", { headers: { allow: "GET" }, status: 426 });
|
|
2686
|
+
}
|
|
2687
|
+
const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
|
|
2688
|
+
if (blockedUpgrade) {
|
|
2689
|
+
return blockedUpgrade;
|
|
2690
|
+
}
|
|
2691
|
+
let agentName;
|
|
2692
|
+
try {
|
|
2693
|
+
agentName = decodeURIComponent(url.pathname.slice(VOICE_PATH_PREFIX.length));
|
|
2694
|
+
} catch {
|
|
2695
|
+
return new Response("Unknown voice agent", { status: 404 });
|
|
2696
|
+
}
|
|
2697
|
+
const namespace = Object.hasOwn(voiceAgents, agentName) ? voiceAgents[agentName] : void 0;
|
|
2698
|
+
if (namespace === void 0) {
|
|
2699
|
+
return new Response("Unknown voice agent", { status: 404 });
|
|
2700
|
+
}
|
|
2701
|
+
const threadKey = url.searchParams.get("threadKey");
|
|
2702
|
+
if (threadKey === null || threadKey.length === 0) {
|
|
2703
|
+
return new Response("Missing threadKey", { status: 400 });
|
|
2704
|
+
}
|
|
2705
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2706
|
+
if (options.authorizeShard) {
|
|
2707
|
+
const allowed = await options.authorizeShard(identity, threadKey);
|
|
2708
|
+
if (!allowed) {
|
|
2709
|
+
return new Response("Forbidden", { status: 403 });
|
|
2710
|
+
}
|
|
2711
|
+
} else {
|
|
2712
|
+
guardUnauthenticatedShardAccess("shard");
|
|
2713
|
+
}
|
|
2714
|
+
const upgradeHeaders = new Headers(request.headers);
|
|
2715
|
+
upgradeHeaders.delete("x-lunora-userid");
|
|
2716
|
+
upgradeHeaders.delete("x-lunora-identity");
|
|
2717
|
+
upgradeHeaders.delete("x-lunora-identity-exp");
|
|
2718
|
+
const forwardedUserId = forwardedHeaders["x-lunora-userid"];
|
|
2719
|
+
const forwardedIdentity = forwardedHeaders["x-lunora-identity"];
|
|
2720
|
+
const forwardedExp = forwardedHeaders["x-lunora-identity-exp"];
|
|
2721
|
+
if (forwardedUserId !== void 0) {
|
|
2722
|
+
upgradeHeaders.set("x-lunora-userid", forwardedUserId);
|
|
2723
|
+
}
|
|
2724
|
+
if (forwardedIdentity !== void 0) {
|
|
2725
|
+
upgradeHeaders.set("x-lunora-identity", forwardedIdentity);
|
|
2726
|
+
}
|
|
2727
|
+
if (forwardedExp !== void 0) {
|
|
2728
|
+
upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
|
|
2729
|
+
}
|
|
2730
|
+
return forwardToShard(namespace, threadKey, new Request(request, { headers: upgradeHeaders }));
|
|
2731
|
+
};
|
|
2566
2732
|
const authorizeFanOutEnvelope = async (fanOut, functionPath, identity) => {
|
|
2567
2733
|
if (options.authorizeFanOut) {
|
|
2568
2734
|
const allowed = await options.authorizeFanOut(identity, fanOut.table, functionPath);
|
|
@@ -2911,33 +3077,22 @@ const createWorker = (options) => {
|
|
|
2911
3077
|
const tables = options.backupTables;
|
|
2912
3078
|
let rows = 0;
|
|
2913
3079
|
let bytes = 0;
|
|
2914
|
-
|
|
2915
|
-
const
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
};
|
|
2924
|
-
try {
|
|
2925
|
-
await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow, shardDO);
|
|
2926
|
-
streamController.close();
|
|
2927
|
-
} catch (error) {
|
|
2928
|
-
streamError = error instanceof Error ? error : new Error(String(error));
|
|
2929
|
-
streamController.error(error);
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
});
|
|
3080
|
+
const parts = [];
|
|
3081
|
+
const writeRow = (row) => {
|
|
3082
|
+
const line = `${JSON.stringify(row)}
|
|
3083
|
+
`;
|
|
3084
|
+
rows += 1;
|
|
3085
|
+
bytes += NDJSON_ENCODER.encode(line).byteLength;
|
|
3086
|
+
parts.push(line);
|
|
3087
|
+
};
|
|
3088
|
+
await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow, shardDO);
|
|
2933
3089
|
const prefix = options.backupPrefix ?? "backups/";
|
|
2934
3090
|
const timestamp = new Date(controller.scheduledTime).toISOString();
|
|
2935
3091
|
const fileKey = `${prefix}lunora-backup-${timestamp.replaceAll(/[.:]/gu, "-")}.ndjson`;
|
|
2936
3092
|
const manifestKey = `${fileKey}.manifest.json`;
|
|
2937
|
-
await store.put(fileKey,
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
}
|
|
3093
|
+
await store.put(fileKey, new Blob(parts, { type: "application/x-ndjson" }), {
|
|
3094
|
+
httpMetadata: { contentType: "application/x-ndjson" }
|
|
3095
|
+
});
|
|
2941
3096
|
const manifest = {
|
|
2942
3097
|
bytes,
|
|
2943
3098
|
createdAt: timestamp,
|
|
@@ -3023,6 +3178,24 @@ const createWorker = (options) => {
|
|
|
3023
3178
|
[RPC_BATCH_PATH]: (request, env, _url, context) => handleBatchRpc(request, env, context),
|
|
3024
3179
|
[SCHEDULER_DISPATCH_PATH]: (request, env) => handleSchedulerDispatch(request, env),
|
|
3025
3180
|
[CRON_JOBS_RUN_PATH]: (request, env) => handleRunCronJob(request, env),
|
|
3181
|
+
// Mint a short-lived HMAC-signed WS admin sub-token. Gated by the master
|
|
3182
|
+
// admin bearer (header) / `adminGate`; the studio then sends the minted
|
|
3183
|
+
// token — not the master credential — in the WS `?token=`
|
|
3184
|
+
// query string. Signed with the master token itself, so both isolates
|
|
3185
|
+
// verify statelessly and rotating `LUNORA_ADMIN_TOKEN` invalidates every
|
|
3186
|
+
// outstanding sub-token. `no-store` keeps the token out of caches.
|
|
3187
|
+
[ADMIN_WS_TOKEN_PATH]: async (request) => {
|
|
3188
|
+
if (request.method !== "POST") {
|
|
3189
|
+
throw new LunoraError("ws-token endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
3190
|
+
}
|
|
3191
|
+
assertAdminAuthorized(request);
|
|
3192
|
+
const signingSecret = effectiveAdminToken();
|
|
3193
|
+
if (signingSecret === void 0) {
|
|
3194
|
+
throw new LunoraError("ws-token minting requires a configured admin token", { code: "ADMIN_TOKEN_NOT_CONFIGURED", status: 400 });
|
|
3195
|
+
}
|
|
3196
|
+
const minted = await mintWsAdminToken(signingSecret);
|
|
3197
|
+
return Response.json(minted, { headers: { "cache-control": "no-store" } });
|
|
3198
|
+
},
|
|
3026
3199
|
// Extracted handler clusters built above, merged in (mirroring the auth
|
|
3027
3200
|
// plane below): orchestration (migrate / rank / rankpage / shard-traffic /
|
|
3028
3201
|
// pitr), data-movement (export / import / sync / connector-sync / apply),
|
|
@@ -3040,8 +3213,7 @@ const createWorker = (options) => {
|
|
|
3040
3213
|
// `AuthAdmin` op, dispatched by the descriptor table in `./auth-admin-routes`.
|
|
3041
3214
|
...buildAuthAdminRoutes({
|
|
3042
3215
|
assertAdmin: assertAdminAuthorized,
|
|
3043
|
-
|
|
3044
|
-
getAuthAdmin: () => options.authAdmin ?? options.authIntrospector,
|
|
3216
|
+
getAuthAdmin: () => options.authAdmin,
|
|
3045
3217
|
parsePaging,
|
|
3046
3218
|
queryParameter,
|
|
3047
3219
|
readJsonBody: readJsonBodyWithLimit
|
|
@@ -3091,6 +3263,9 @@ const createWorker = (options) => {
|
|
|
3091
3263
|
await applyAdminGate(request, url.pathname);
|
|
3092
3264
|
return internalRoute(request, env, url, context);
|
|
3093
3265
|
}
|
|
3266
|
+
if (options.voiceAgents !== void 0 && url.pathname.startsWith(VOICE_PATH_PREFIX)) {
|
|
3267
|
+
return handleVoiceUpgrade(request, env, url);
|
|
3268
|
+
}
|
|
3094
3269
|
const httpRouteResponse = await dispatchHttpRoute(request, env, context);
|
|
3095
3270
|
if (httpRouteResponse) {
|
|
3096
3271
|
return httpRouteResponse;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/runtime",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.28",
|
|
4
4
|
"description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.5"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": "^22.15.0 || >=24.11.0"
|