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

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-DiWwOXXt.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-yLFNjHDt.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-c-U1WRy-.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 { w as wrapResourceSpans, o as otlpRandomHex, c as wrapResourceMetrics, e as wrapResourceLogs, f as encodeAttribute, g as otlpUnixNano, O as OTLP_SEVERITY, h as mergeHeaders, m as mergeResourceAttributes } from './otlp-resource-Dow6-F_u.mjs';
2
2
 
3
3
  const stringifyFieldValue = (value) => {
4
4
  if (typeof value === "string") {
@@ -12,12 +12,31 @@ const stringifyFieldValue = (value) => {
12
12
  };
13
13
  const coerceFieldValue = (value) => typeof value === "boolean" || typeof value === "number" || typeof value === "string" ? value : stringifyFieldValue(value);
14
14
 
15
- const shouldSkip = (event, onlyErrors) => onlyErrors === true && event.ok;
16
- const otlpTraceBody = (event, serviceName, endMs) => {
15
+ const otlpTraceBody = (event, serviceName, endMs, resourceAttributes) => {
17
16
  const attributes = [encodeAttribute("lunora.function_path", event.functionPath), encodeAttribute("lunora.ok", event.ok)];
17
+ if (event.method !== void 0) {
18
+ attributes.push(encodeAttribute("http.request.method", event.method));
19
+ }
20
+ if (event.path !== void 0) {
21
+ attributes.push(encodeAttribute("url.path", event.path));
22
+ }
23
+ attributes.push(encodeAttribute("http.route", event.functionPath));
24
+ if (event.scheme !== void 0) {
25
+ attributes.push(encodeAttribute("url.scheme", event.scheme));
26
+ }
27
+ if (event.host !== void 0) {
28
+ attributes.push(encodeAttribute("server.address", event.host));
29
+ }
30
+ if (event.port !== void 0) {
31
+ attributes.push(encodeAttribute("server.port", event.port));
32
+ }
33
+ if (event.userAgent !== void 0) {
34
+ attributes.push(encodeAttribute("user_agent.original", event.userAgent));
35
+ }
18
36
  if (event.shardKey !== void 0) {
19
37
  attributes.push(encodeAttribute("lunora.shard_key", event.shardKey));
20
38
  }
39
+ attributes.push(encodeAttribute("http.response.status_code", event.error?.status ?? 200));
21
40
  if (event.error) {
22
41
  attributes.push(encodeAttribute("error.type", event.error.code), encodeAttribute("lunora.error_status", event.error.status));
23
42
  }
@@ -42,7 +61,22 @@ const otlpTraceBody = (event, serviceName, endMs) => {
42
61
  status: event.ok ? { code: 1 } : { code: 2, message: event.error?.message ?? "" },
43
62
  traceId: event.traceId ?? otlpRandomHex(16)
44
63
  };
45
- return wrapResourceSpans(span, "@lunora/runtime", serviceName);
64
+ if (event.parentSpanId !== void 0) {
65
+ span.parentSpanId = event.parentSpanId;
66
+ }
67
+ if (event.traceFlags !== void 0) {
68
+ span.flags = event.traceFlags;
69
+ }
70
+ if (event.error) {
71
+ span.events = [
72
+ {
73
+ attributes: [encodeAttribute("exception.type", event.error.code), encodeAttribute("exception.message", event.error.message)],
74
+ name: "exception",
75
+ timeUnixNano: otlpUnixNano(endMs)
76
+ }
77
+ ];
78
+ }
79
+ return wrapResourceSpans(span, "@lunora/runtime", serviceName, resourceAttributes);
46
80
  };
47
81
  const encodeSignalAttributes = (reserved, caller) => {
48
82
  const byKey = /* @__PURE__ */ new Map([["lunora.function_path", encodeAttribute("lunora.function_path", reserved.functionPath)]]);
@@ -60,7 +94,7 @@ const encodeSignalAttributes = (reserved, caller) => {
60
94
  }
61
95
  return [...byKey.values()];
62
96
  };
63
- const otlpSpanBody = (event, serviceName) => {
97
+ const otlpSpanBody = (event, serviceName, resourceAttributes) => {
64
98
  const span = {
65
99
  attributes: encodeSignalAttributes(
66
100
  { errorType: event.error?.type, functionPath: event.functionPath, shardKey: event.shardKey, userId: event.userId },
@@ -80,14 +114,14 @@ const otlpSpanBody = (event, serviceName) => {
80
114
  status: event.ok ? { code: 1 } : { code: 2, message: event.error?.message ?? "" },
81
115
  traceId: event.traceId
82
116
  };
83
- return wrapResourceSpans(span, "@lunora/runtime", serviceName);
117
+ return wrapResourceSpans(span, "@lunora/runtime", serviceName, resourceAttributes);
84
118
  };
85
- const otlpMetricBody = (event, serviceName) => {
119
+ const otlpMetricBody = (event, serviceName, resourceAttributes) => {
86
120
  const timeUnixNano = otlpUnixNano(event.ts);
87
121
  const attributes = encodeSignalAttributes({ functionPath: event.functionPath, shardKey: event.shardKey }, event.attributes);
88
122
  const dataPoint = { asDouble: event.value, attributes, timeUnixNano };
89
123
  if (event.kind === "gauge") {
90
- return wrapResourceMetrics({ gauge: { dataPoints: [dataPoint] }, name: event.name }, "@lunora/runtime", serviceName);
124
+ return wrapResourceMetrics({ gauge: { dataPoints: [dataPoint] }, name: event.name }, "@lunora/runtime", serviceName, resourceAttributes);
91
125
  }
92
126
  if (event.kind === "histogram") {
93
127
  return wrapResourceMetrics(
@@ -112,16 +146,18 @@ const otlpMetricBody = (event, serviceName) => {
112
146
  name: event.name
113
147
  },
114
148
  "@lunora/runtime",
115
- serviceName
149
+ serviceName,
150
+ resourceAttributes
116
151
  );
117
152
  }
118
153
  return wrapResourceMetrics(
119
154
  { name: event.name, sum: { aggregationTemporality: 1, dataPoints: [dataPoint], isMonotonic: true } },
120
155
  "@lunora/runtime",
121
- serviceName
156
+ serviceName,
157
+ resourceAttributes
122
158
  );
123
159
  };
124
- const otlpLogBody = (event, serviceName) => {
160
+ const otlpLogBody = (event, serviceName, resourceAttributes) => {
125
161
  const logRecord = {
126
162
  // Caller-supplied structured fields become log-record attributes so a
127
163
  // pipeline can filter/index on them; precedence per `encodeSignalAttributes`.
@@ -137,7 +173,7 @@ const otlpLogBody = (event, serviceName) => {
137
173
  if (event.spanId !== void 0) {
138
174
  logRecord.spanId = event.spanId;
139
175
  }
140
- return wrapResourceLogs(logRecord, "@lunora/runtime", serviceName);
176
+ return wrapResourceLogs(logRecord, "@lunora/runtime", serviceName, resourceAttributes);
141
177
  };
142
178
  const OTLP_GZIP_THRESHOLD = 1024;
143
179
  const gzipEncode = async (text) => {
@@ -155,6 +191,8 @@ const otlpPost = (url, body, headers, context) => {
155
191
  } catch {
156
192
  }
157
193
  };
194
+
195
+ const shouldSkip = (event, onlyErrors) => onlyErrors === true && event.ok;
158
196
  const consoleSink = (options = {}) => {
159
197
  const { onlyErrors } = options;
160
198
  return {
@@ -312,8 +350,27 @@ const pipelineLogSink = (options) => {
312
350
  };
313
351
  };
314
352
  const otlpSink = (options) => {
315
- const { endpoint, headers, onlyErrors, token } = options;
353
+ const { deploymentEnvironment, detectResources, endpoint, headers, onlyErrors, resourceAttributes, serviceNamespace, serviceVersion, token } = options;
316
354
  const serviceName = options.serviceName ?? "lunora";
355
+ const staticAttributes = {
356
+ ...serviceVersion === void 0 ? {} : { "service.version": serviceVersion },
357
+ ...serviceNamespace === void 0 ? {} : { "service.namespace": serviceNamespace },
358
+ ...deploymentEnvironment === void 0 ? {} : { "deployment.environment": deploymentEnvironment },
359
+ ...resourceAttributes
360
+ };
361
+ const mergedByContext = /* @__PURE__ */ new WeakMap();
362
+ const resourceAttributesFor = (context) => {
363
+ if (detectResources !== true || context?.resourceAttributes === void 0) {
364
+ return staticAttributes;
365
+ }
366
+ const memoized = mergedByContext.get(context);
367
+ if (memoized !== void 0) {
368
+ return memoized;
369
+ }
370
+ const merged = mergeResourceAttributes(context.resourceAttributes(), staticAttributes);
371
+ mergedByContext.set(context, merged);
372
+ return merged;
373
+ };
317
374
  let base = endpoint;
318
375
  while (base.endsWith("/")) {
319
376
  base = base.slice(0, -1);
@@ -324,19 +381,19 @@ const otlpSink = (options) => {
324
381
  const mergedHeaders = mergeHeaders({ "content-type": "application/json" }, headers, token);
325
382
  return {
326
383
  onLog: (event, context) => {
327
- otlpPost(logsUrl, otlpLogBody(event, serviceName), mergedHeaders, context);
384
+ otlpPost(logsUrl, otlpLogBody(event, serviceName, resourceAttributesFor(context)), mergedHeaders, context);
328
385
  },
329
386
  onMetric: (event, context) => {
330
- otlpPost(metricsUrl, otlpMetricBody(event, serviceName), mergedHeaders, context);
387
+ otlpPost(metricsUrl, otlpMetricBody(event, serviceName, resourceAttributesFor(context)), mergedHeaders, context);
331
388
  },
332
389
  onRpc: (event, context) => {
333
390
  if (shouldSkip(event, onlyErrors)) {
334
391
  return;
335
392
  }
336
- otlpPost(tracesUrl, otlpTraceBody(event, serviceName, Date.now()), mergedHeaders, context);
393
+ otlpPost(tracesUrl, otlpTraceBody(event, serviceName, Date.now(), resourceAttributesFor(context)), mergedHeaders, context);
337
394
  },
338
395
  onSpan: (event, context) => {
339
- otlpPost(tracesUrl, otlpSpanBody(event, serviceName), mergedHeaders, context);
396
+ otlpPost(tracesUrl, otlpSpanBody(event, serviceName, resourceAttributesFor(context)), mergedHeaders, context);
340
397
  }
341
398
  };
342
399
  };
@@ -0,0 +1,121 @@
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, _url, context) => {
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({
94
+ args,
95
+ env,
96
+ functionPath: entry.functionPath,
97
+ request,
98
+ ...shardKey === void 0 ? {} : { shardKey },
99
+ ...context?.waitUntil === void 0 ? {} : { waitUntil: (promise) => context.waitUntil?.(promise) }
100
+ });
101
+ };
102
+ }
103
+ return routes;
104
+ };
105
+ const createRestRateLimit = (limiter, options) => async (request, functionPath) => {
106
+ const key = options.key ? options.key(request, functionPath) : request.headers.get("cf-connecting-ip") ?? void 0;
107
+ const status = await limiter.limit(options.name, key === void 0 ? {} : { key });
108
+ if (status.ok) {
109
+ return void 0;
110
+ }
111
+ const retryAfterSeconds = Math.max(1, Math.ceil(status.retryAfter / 1e3));
112
+ return Response.json(
113
+ { error: { code: "RATE_LIMITED", message: "Rate limit exceeded" } },
114
+ {
115
+ headers: { "content-type": "application/json", "retry-after": String(retryAfterSeconds) },
116
+ status: 429
117
+ }
118
+ );
119
+ };
120
+
121
+ export { argsFromQuery, buildRestRoutes, createRestRateLimit, readShardKey, restSurfaceFromRegistry };