@lunora/runtime 1.0.0-alpha.33 → 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.d.mts +1647 -1001
- package/dist/index.d.ts +1647 -1001
- package/dist/index.mjs +6 -3
- package/dist/packem_shared/HEALTH_PATH-e5J_NHBx.mjs +150 -0
- package/dist/packem_shared/{analyticsEngineSink-Bn7p0URe.mjs → analyticsEngineSink-DgL64WVC.mjs} +8 -2
- package/dist/packem_shared/argsFromQuery-BqPiQTPc.mjs +114 -0
- package/dist/packem_shared/{composeWorker-DAwO9LLs.mjs → composeWorker-UwHY2r8O.mjs} +282 -69
- package/dist/packem_shared/createKvCursorStore-g8aA6B4L.mjs +165 -0
- package/dist/packem_shared/emitLogEvent-BlGMnKZK.mjs +1 -0
- package/dist/packem_shared/method-guard-Qzw99aCj.mjs +3 -0
- package/dist/packem_shared/observability--NOFYBFc.mjs +47 -0
- package/dist/packem_shared/{otlp-DOLuy1Aj.mjs → otlp-DKZJCkdD.mjs} +1 -1
- package/package.json +3 -3
- package/dist/packem_shared/emitLogEvent-pEdtqAK8.mjs +0 -20
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-
|
|
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/
|
|
8
|
-
export { analyticsEngineSink, combineSinks, consoleSink, otlpSink, pipelineLogSink, sentrySink, webhookSink } from './packem_shared/analyticsEngineSink-
|
|
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 };
|
package/dist/packem_shared/{analyticsEngineSink-Bn7p0URe.mjs → analyticsEngineSink-DgL64WVC.mjs}
RENAMED
|
@@ -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-
|
|
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") {
|
|
@@ -139,9 +139,15 @@ const otlpLogBody = (event, serviceName) => {
|
|
|
139
139
|
}
|
|
140
140
|
return wrapResourceLogs(logRecord, "@lunora/runtime", serviceName);
|
|
141
141
|
};
|
|
142
|
+
const OTLP_GZIP_THRESHOLD = 1024;
|
|
143
|
+
const gzipEncode = async (text) => {
|
|
144
|
+
const stream = new Blob([text]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
145
|
+
return new Response(stream).arrayBuffer();
|
|
146
|
+
};
|
|
142
147
|
const otlpPost = (url, body, headers, context) => {
|
|
143
148
|
try {
|
|
144
|
-
const
|
|
149
|
+
const json = JSON.stringify(body);
|
|
150
|
+
const sent = (json.length < OTLP_GZIP_THRESHOLD ? fetch(url, { body: json, headers, method: "POST" }) : gzipEncode(json).then((gz) => fetch(url, { body: gz, headers: { ...headers, "content-encoding": "gzip" }, method: "POST" }))).catch(() => {
|
|
145
151
|
});
|
|
146
152
|
if (context?.waitUntil) {
|
|
147
153
|
context.waitUntil(sent);
|
|
@@ -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 };
|