@lunora/runtime 1.0.0-alpha.34 → 1.0.0-alpha.35

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.mjs CHANGED
@@ -1,14 +1,17 @@
1
1
  export { toAirbyteMessages, toFivetranResponse } from './packem_shared/toAirbyteMessages-DrHdplb4.mjs';
2
- export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-DAwO9LLs.mjs';
2
+ export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-UwHY2r8O.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';
6
+ export { createKvCursorStore, createMemoryCursorStore, defineExportSink, r2Sink, runExportTap, sanitizeChange, webhookExportSink } from './packem_shared/createKvCursorStore-g8aA6B4L.mjs';
7
+ export { HEALTH_PATH, HEALTH_READY_PATH, buildHealthRoutes, d1Probe, durableObjectProbe, presenceProbe } from './packem_shared/HEALTH_PATH-e5J_NHBx.mjs';
6
8
  export { LOG_ARCHIVE_PATH, resolveLogArchiveFromEnv } from './packem_shared/LOG_ARCHIVE_PATH-CNs0bznX.mjs';
7
- export { emitLogEvent, emitRpcEvent } from './packem_shared/emitLogEvent-pEdtqAK8.mjs';
8
- export { analyticsEngineSink, combineSinks, consoleSink, otlpSink, pipelineLogSink, sentrySink, webhookSink } from './packem_shared/analyticsEngineSink-DE48mQA9.mjs';
9
+ export { e as emitLogEvent, a as emitRpcEvent } from './packem_shared/observability--NOFYBFc.mjs';
10
+ export { analyticsEngineSink, combineSinks, consoleSink, otlpSink, pipelineLogSink, sentrySink, webhookSink } from './packem_shared/analyticsEngineSink-DgL64WVC.mjs';
9
11
  export { D as DEFAULT_LOG_COLUMNS, a as DEFAULT_LOG_LIMIT, c as createPipelineLogReader } from './packem_shared/pipeline-log-reader-BXULGNC3.mjs';
10
12
  export { createQueryCoordinator, createStaticShardRegistry, mergeStrategyForAggregate } from './packem_shared/createQueryCoordinator-DNCJzOZE.mjs';
11
13
  export { applyJurisdiction, resolveShard } from './packem_shared/applyJurisdiction-BkZtTkct.mjs';
14
+ export { argsFromQuery, buildRestRoutes, createRestRateLimit, readShardKey, restSurfaceFromRegistry } from './packem_shared/argsFromQuery-BqPiQTPc.mjs';
12
15
  export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity } from './packem_shared/decorateResponse-DRWQFNhF.mjs';
13
16
  export { LOG_ARCHIVE_NOT_CONFIGURED } from './packem_shared/LOG_ARCHIVE_NOT_CONFIGURED-acNcguqc.mjs';
14
17
  export { NOOP_EXECUTION_CONTEXT } from './packem_shared/NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
@@ -0,0 +1,150 @@
1
+ import { LunoraError } from './LunoraError-Bpb9EFJ3.mjs';
2
+ import { m as methodGuard } from './method-guard-Qzw99aCj.mjs';
3
+
4
+ const HEALTH_PATH = "/_lunora/health";
5
+ const HEALTH_READY_PATH = "/_lunora/health/ready";
6
+ const toCheckerType = (kind) => {
7
+ if (kind === "liveness") {
8
+ return ["liveness"];
9
+ }
10
+ if (kind === "readiness") {
11
+ return ["readiness"];
12
+ }
13
+ return ["liveness", "readiness"];
14
+ };
15
+ class HealthRegistry {
16
+ #checkers = /* @__PURE__ */ new Map();
17
+ addChecker(name, run, options) {
18
+ this.#checkers.set(name, { run, types: options.type });
19
+ }
20
+ async getReport(filter) {
21
+ const selected = [...this.#checkers].filter(([, checker]) => filter === void 0 || checker.types.includes(filter));
22
+ const entries = await Promise.all(selected.map(async ([name, checker]) => [name, await checker.run()]));
23
+ return {
24
+ healthy: entries.every(([, result]) => result.health.healthy),
25
+ report: Object.fromEntries(entries)
26
+ };
27
+ }
28
+ }
29
+ const buildRegistry = (probes) => {
30
+ const registry = new HealthRegistry();
31
+ const criticalNames = /* @__PURE__ */ new Set();
32
+ for (const probe of probes) {
33
+ if (probe.critical) {
34
+ criticalNames.add(probe.name);
35
+ }
36
+ registry.addChecker(
37
+ probe.name,
38
+ async () => {
39
+ let result;
40
+ try {
41
+ result = await probe.check();
42
+ } catch (error) {
43
+ result = { healthy: false, message: error instanceof Error ? error.message : "probe failed" };
44
+ }
45
+ return {
46
+ health: {
47
+ healthy: result.healthy,
48
+ ...result.message === void 0 ? {} : { message: result.message }
49
+ }
50
+ };
51
+ },
52
+ { type: toCheckerType(probe.kind) }
53
+ );
54
+ }
55
+ return { criticalNames, registry };
56
+ };
57
+ const buildBody = (report, criticalNames, posture, appName, appVersion) => {
58
+ const checks = [];
59
+ let anyCriticalDown = false;
60
+ let anyDown = false;
61
+ for (const [name, entry] of Object.entries(report)) {
62
+ const critical = criticalNames.has(name);
63
+ const up = entry.health.healthy;
64
+ if (!up) {
65
+ anyDown = true;
66
+ if (critical) {
67
+ anyCriticalDown = true;
68
+ }
69
+ }
70
+ checks.push({
71
+ critical,
72
+ ...posture === "admin" && entry.health.message !== void 0 ? { message: entry.health.message } : {},
73
+ name,
74
+ status: up ? "up" : "down"
75
+ });
76
+ }
77
+ checks.sort((a, b) => a.name.localeCompare(b.name));
78
+ let status = "healthy";
79
+ if (anyCriticalDown) {
80
+ status = "unhealthy";
81
+ } else if (anyDown) {
82
+ status = "degraded";
83
+ }
84
+ return {
85
+ anyCriticalDown,
86
+ body: { appName, appVersion, checks, status, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
87
+ };
88
+ };
89
+ const buildHealthRoutes = (deps) => {
90
+ const { appName = "lunora", appVersion = "0.0.0", auth = "public", isAdmin, resolveProbes } = deps;
91
+ const gate = (request) => {
92
+ if (auth === "admin" && !isAdmin(request)) {
93
+ throw new LunoraError("health endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
94
+ }
95
+ };
96
+ const respond = async (request, env, probeKind) => {
97
+ const wrongMethod = methodGuard(request, ["GET", "HEAD"]);
98
+ if (wrongMethod) {
99
+ return wrongMethod;
100
+ }
101
+ gate(request);
102
+ const { criticalNames, registry } = buildRegistry(resolveProbes(env));
103
+ const { healthy, report } = await registry.getReport(probeKind === "readiness" ? "readiness" : void 0);
104
+ const { anyCriticalDown, body } = buildBody(report, criticalNames, auth, appName, appVersion);
105
+ const down = probeKind === "readiness" ? !healthy : anyCriticalDown;
106
+ return Response.json(body, { headers: { "cache-control": "no-store" }, status: down ? 503 : 200 });
107
+ };
108
+ return {
109
+ [HEALTH_PATH]: (request, env) => respond(request, env, "aggregate"),
110
+ [HEALTH_READY_PATH]: (request, env) => respond(request, env, "readiness")
111
+ };
112
+ };
113
+ const durableObjectProbe = (name, namespace, shardKey) => {
114
+ return {
115
+ check: async () => {
116
+ try {
117
+ const stub = namespace.get(namespace.idFromName(shardKey));
118
+ await stub.fetch(new Request("https://shard.internal/_lunora/status", { method: "GET" }));
119
+ return { healthy: true };
120
+ } catch {
121
+ return { healthy: false, message: "durable object unreachable" };
122
+ }
123
+ },
124
+ critical: true,
125
+ name
126
+ };
127
+ };
128
+ const d1Probe = (name, database) => {
129
+ return {
130
+ check: async () => {
131
+ try {
132
+ await database.prepare("SELECT 1").first();
133
+ return { healthy: true };
134
+ } catch {
135
+ return { healthy: false, message: "d1 query failed" };
136
+ }
137
+ },
138
+ critical: true,
139
+ name
140
+ };
141
+ };
142
+ const presenceProbe = (name, bound) => {
143
+ return {
144
+ check: () => bound ? { healthy: true } : { healthy: false, message: "binding not configured" },
145
+ critical: false,
146
+ name
147
+ };
148
+ };
149
+
150
+ export { HEALTH_PATH, HEALTH_READY_PATH, buildHealthRoutes, d1Probe, durableObjectProbe, presenceProbe };
@@ -1,4 +1,4 @@
1
- import { m as mergeHeaders, w as wrapResourceSpans, o as otlpRandomHex, a as wrapResourceMetrics, c as wrapResourceLogs, e as encodeAttribute, O as OTLP_SEVERITY, d as otlpUnixNano } from './otlp-DOLuy1Aj.mjs';
1
+ import { m as mergeHeaders, w as wrapResourceSpans, o as otlpRandomHex, a as wrapResourceMetrics, c as wrapResourceLogs, e as encodeAttribute, O as OTLP_SEVERITY, d as otlpUnixNano } from './otlp-DKZJCkdD.mjs';
2
2
 
3
3
  const stringifyFieldValue = (value) => {
4
4
  if (typeof value === "string") {
@@ -0,0 +1,114 @@
1
+ import { m as methodGuard } from './method-guard-Qzw99aCj.mjs';
2
+
3
+ const REST_PATH_PREFIX = "/_lunora/rest";
4
+ const splitFunctionPath = (functionPath) => {
5
+ const colon = functionPath.indexOf(":");
6
+ if (colon <= 0 || colon >= functionPath.length - 1 || functionPath.indexOf(":", colon + 1) !== -1) {
7
+ return void 0;
8
+ }
9
+ return { name: functionPath.slice(colon + 1), namespace: functionPath.slice(0, colon) };
10
+ };
11
+ const restPathForFunction = (functionPath) => {
12
+ const parts = splitFunctionPath(functionPath);
13
+ if (parts === void 0) {
14
+ return void 0;
15
+ }
16
+ return `${REST_PATH_PREFIX}/${parts.namespace}/${parts.name}`;
17
+ };
18
+ const restMethodForKind = (kind) => kind === "query" ? "GET" : "POST";
19
+ const describeRestSurface = (procedures) => {
20
+ const entries = [];
21
+ for (const procedure of procedures) {
22
+ if (procedure.exposure?.rest !== true || procedure.kind === "stream") {
23
+ continue;
24
+ }
25
+ const parts = splitFunctionPath(procedure.functionPath);
26
+ const path = restPathForFunction(procedure.functionPath);
27
+ if (parts === void 0 || path === void 0) {
28
+ continue;
29
+ }
30
+ entries.push({
31
+ functionPath: procedure.functionPath,
32
+ kind: procedure.kind,
33
+ method: restMethodForKind(procedure.kind),
34
+ name: parts.name,
35
+ namespace: parts.namespace,
36
+ path
37
+ });
38
+ }
39
+ entries.sort((a, b) => a.path.localeCompare(b.path));
40
+ return entries;
41
+ };
42
+
43
+ const registryToSurfaceInput = (functions) => Object.entries(functions).map(([functionPath, entry]) => {
44
+ return { exposure: entry.expose, functionPath, kind: entry.kind };
45
+ });
46
+ const restSurfaceFromRegistry = (functions) => describeRestSurface(registryToSurfaceInput(functions));
47
+ const readShardKey = (url, request) => {
48
+ const fromQuery = url.searchParams.get("shardKey");
49
+ if (fromQuery !== null && fromQuery !== "") {
50
+ return fromQuery;
51
+ }
52
+ const fromHeader = request.headers.get("x-lunora-shard-key");
53
+ return fromHeader === null || fromHeader === "" ? void 0 : fromHeader;
54
+ };
55
+ const argsFromQuery = (url) => {
56
+ const args = {};
57
+ for (const [key, value] of url.searchParams.entries()) {
58
+ if (key === "shardKey") {
59
+ continue;
60
+ }
61
+ try {
62
+ args[key] = JSON.parse(value);
63
+ } catch {
64
+ args[key] = value;
65
+ }
66
+ }
67
+ return args;
68
+ };
69
+ const buildRestRoutes = (deps) => {
70
+ const { functions, invoke, rateLimit, readJsonBody } = deps;
71
+ const routes = {};
72
+ for (const entry of restSurfaceFromRegistry(functions)) {
73
+ const allowed = entry.kind === "query" ? ["GET", "POST"] : ["POST"];
74
+ routes[entry.path] = async (request, env) => {
75
+ const wrongMethod = methodGuard(request, allowed);
76
+ if (wrongMethod) {
77
+ return wrongMethod;
78
+ }
79
+ const url = new URL(request.url);
80
+ if (rateLimit) {
81
+ const limited = await rateLimit(request, entry.functionPath);
82
+ if (limited) {
83
+ return limited;
84
+ }
85
+ }
86
+ let args;
87
+ if (request.method === "GET") {
88
+ args = argsFromQuery(url);
89
+ } else {
90
+ args = request.body === null ? {} : await readJsonBody(request);
91
+ }
92
+ const shardKey = readShardKey(url, request);
93
+ return invoke({ args, env, functionPath: entry.functionPath, request, ...shardKey === void 0 ? {} : { shardKey } });
94
+ };
95
+ }
96
+ return routes;
97
+ };
98
+ const createRestRateLimit = (limiter, options) => async (request, functionPath) => {
99
+ const key = options.key ? options.key(request, functionPath) : request.headers.get("cf-connecting-ip") ?? void 0;
100
+ const status = await limiter.limit(options.name, key === void 0 ? {} : { key });
101
+ if (status.ok) {
102
+ return void 0;
103
+ }
104
+ const retryAfterSeconds = Math.max(1, Math.ceil(status.retryAfter / 1e3));
105
+ return Response.json(
106
+ { error: { code: "RATE_LIMITED", message: "Rate limit exceeded" } },
107
+ {
108
+ headers: { "content-type": "application/json", "retry-after": String(retryAfterSeconds) },
109
+ status: 429
110
+ }
111
+ );
112
+ };
113
+
114
+ export { argsFromQuery, buildRestRoutes, createRestRateLimit, readShardKey, restSurfaceFromRegistry };