@vivero/stoma 0.1.0-rc.10
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/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/adapters/bun.d.ts +9 -0
- package/dist/adapters/bun.js +8 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +49 -0
- package/dist/adapters/cloudflare.js +85 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +9 -0
- package/dist/adapters/deno.js +8 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/durable-object.d.ts +63 -0
- package/dist/adapters/durable-object.js +46 -0
- package/dist/adapters/durable-object.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +53 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +9 -0
- package/dist/adapters/memory.js +14 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/node.d.ts +9 -0
- package/dist/adapters/node.js +8 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/postgres.d.ts +109 -0
- package/dist/adapters/postgres.js +242 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/redis.d.ts +116 -0
- package/dist/adapters/redis.js +194 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/testing.d.ts +32 -0
- package/dist/adapters/testing.js +33 -0
- package/dist/adapters/testing.js.map +1 -0
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +21 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +48 -0
- package/dist/config/merge.js +83 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/schema.d.ts +254 -0
- package/dist/config/schema.js +109 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/errors.d.ts +66 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/gateway.d.ts +44 -0
- package/dist/core/gateway.js +400 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health.d.ts +78 -0
- package/dist/core/health.js +65 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/pipeline.d.ts +62 -0
- package/dist/core/pipeline.js +214 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/protocol.d.ts +4 -0
- package/dist/core/protocol.js +1 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/scope.d.ts +67 -0
- package/dist/core/scope.js +44 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +252 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/admin.d.ts +32 -0
- package/dist/observability/admin.js +85 -0
- package/dist/observability/admin.js.map +1 -0
- package/dist/observability/metrics.d.ts +78 -0
- package/dist/observability/metrics.js +107 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/tracing.d.ts +149 -0
- package/dist/observability/tracing.js +191 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/policies/auth/api-key-auth.d.ts +64 -0
- package/dist/policies/auth/api-key-auth.js +93 -0
- package/dist/policies/auth/api-key-auth.js.map +1 -0
- package/dist/policies/auth/basic-auth.d.ts +33 -0
- package/dist/policies/auth/basic-auth.js +96 -0
- package/dist/policies/auth/basic-auth.js.map +1 -0
- package/dist/policies/auth/crypto.d.ts +29 -0
- package/dist/policies/auth/crypto.js +100 -0
- package/dist/policies/auth/crypto.js.map +1 -0
- package/dist/policies/auth/generate-http-signature.d.ts +30 -0
- package/dist/policies/auth/generate-http-signature.js +79 -0
- package/dist/policies/auth/generate-http-signature.js.map +1 -0
- package/dist/policies/auth/generate-jwt.d.ts +44 -0
- package/dist/policies/auth/generate-jwt.js +99 -0
- package/dist/policies/auth/generate-jwt.js.map +1 -0
- package/dist/policies/auth/http-signature-base.d.ts +55 -0
- package/dist/policies/auth/http-signature-base.js +140 -0
- package/dist/policies/auth/http-signature-base.js.map +1 -0
- package/dist/policies/auth/jws.d.ts +46 -0
- package/dist/policies/auth/jws.js +317 -0
- package/dist/policies/auth/jws.js.map +1 -0
- package/dist/policies/auth/jwt-auth.d.ts +64 -0
- package/dist/policies/auth/jwt-auth.js +266 -0
- package/dist/policies/auth/jwt-auth.js.map +1 -0
- package/dist/policies/auth/oauth2.d.ts +38 -0
- package/dist/policies/auth/oauth2.js +254 -0
- package/dist/policies/auth/oauth2.js.map +1 -0
- package/dist/policies/auth/rbac.d.ts +30 -0
- package/dist/policies/auth/rbac.js +115 -0
- package/dist/policies/auth/rbac.js.map +1 -0
- package/dist/policies/auth/verify-http-signature.d.ts +30 -0
- package/dist/policies/auth/verify-http-signature.js +147 -0
- package/dist/policies/auth/verify-http-signature.js.map +1 -0
- package/dist/policies/index.d.ts +51 -0
- package/dist/policies/index.js +109 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/mock.d.ts +60 -0
- package/dist/policies/mock.js +29 -0
- package/dist/policies/mock.js.map +1 -0
- package/dist/policies/observability/assign-metrics.d.ts +37 -0
- package/dist/policies/observability/assign-metrics.js +29 -0
- package/dist/policies/observability/assign-metrics.js.map +1 -0
- package/dist/policies/observability/metrics-reporter.d.ts +25 -0
- package/dist/policies/observability/metrics-reporter.js +62 -0
- package/dist/policies/observability/metrics-reporter.js.map +1 -0
- package/dist/policies/observability/request-log.d.ts +135 -0
- package/dist/policies/observability/request-log.js +134 -0
- package/dist/policies/observability/request-log.js.map +1 -0
- package/dist/policies/observability/server-timing.d.ts +35 -0
- package/dist/policies/observability/server-timing.js +89 -0
- package/dist/policies/observability/server-timing.js.map +1 -0
- package/dist/policies/proxy.d.ts +59 -0
- package/dist/policies/proxy.js +47 -0
- package/dist/policies/proxy.js.map +1 -0
- package/dist/policies/resilience/circuit-breaker.d.ts +4 -0
- package/dist/policies/resilience/circuit-breaker.js +280 -0
- package/dist/policies/resilience/circuit-breaker.js.map +1 -0
- package/dist/policies/resilience/latency-injection.d.ts +35 -0
- package/dist/policies/resilience/latency-injection.js +26 -0
- package/dist/policies/resilience/latency-injection.js.map +1 -0
- package/dist/policies/resilience/retry.d.ts +71 -0
- package/dist/policies/resilience/retry.js +79 -0
- package/dist/policies/resilience/retry.js.map +1 -0
- package/dist/policies/resilience/timeout.d.ts +32 -0
- package/dist/policies/resilience/timeout.js +46 -0
- package/dist/policies/resilience/timeout.js.map +1 -0
- package/dist/policies/sdk/define-policy.d.ts +176 -0
- package/dist/policies/sdk/define-policy.js +42 -0
- package/dist/policies/sdk/define-policy.js.map +1 -0
- package/dist/policies/sdk/helpers.d.ts +132 -0
- package/dist/policies/sdk/helpers.js +87 -0
- package/dist/policies/sdk/helpers.js.map +1 -0
- package/dist/policies/sdk/index.d.ts +10 -0
- package/dist/policies/sdk/index.js +35 -0
- package/dist/policies/sdk/index.js.map +1 -0
- package/dist/policies/sdk/priority.d.ts +44 -0
- package/dist/policies/sdk/priority.js +36 -0
- package/dist/policies/sdk/priority.js.map +1 -0
- package/dist/policies/sdk/testing.d.ts +53 -0
- package/dist/policies/sdk/testing.js +41 -0
- package/dist/policies/sdk/testing.js.map +1 -0
- package/dist/policies/sdk/trace.d.ts +73 -0
- package/dist/policies/sdk/trace.js +25 -0
- package/dist/policies/sdk/trace.js.map +1 -0
- package/dist/policies/traffic/cache.d.ts +4 -0
- package/dist/policies/traffic/cache.js +224 -0
- package/dist/policies/traffic/cache.js.map +1 -0
- package/dist/policies/traffic/dynamic-routing.d.ts +54 -0
- package/dist/policies/traffic/dynamic-routing.js +36 -0
- package/dist/policies/traffic/dynamic-routing.js.map +1 -0
- package/dist/policies/traffic/geo-ip-filter.d.ts +37 -0
- package/dist/policies/traffic/geo-ip-filter.js +74 -0
- package/dist/policies/traffic/geo-ip-filter.js.map +1 -0
- package/dist/policies/traffic/http-callout.d.ts +59 -0
- package/dist/policies/traffic/http-callout.js +69 -0
- package/dist/policies/traffic/http-callout.js.map +1 -0
- package/dist/policies/traffic/interrupt.d.ts +46 -0
- package/dist/policies/traffic/interrupt.js +38 -0
- package/dist/policies/traffic/interrupt.js.map +1 -0
- package/dist/policies/traffic/ip-filter.d.ts +47 -0
- package/dist/policies/traffic/ip-filter.js +57 -0
- package/dist/policies/traffic/ip-filter.js.map +1 -0
- package/dist/policies/traffic/json-threat-protection.d.ts +51 -0
- package/dist/policies/traffic/json-threat-protection.js +173 -0
- package/dist/policies/traffic/json-threat-protection.js.map +1 -0
- package/dist/policies/traffic/rate-limit.d.ts +4 -0
- package/dist/policies/traffic/rate-limit.js +145 -0
- package/dist/policies/traffic/rate-limit.js.map +1 -0
- package/dist/policies/traffic/regex-threat-protection.d.ts +54 -0
- package/dist/policies/traffic/regex-threat-protection.js +109 -0
- package/dist/policies/traffic/regex-threat-protection.js.map +1 -0
- package/dist/policies/traffic/request-limit.d.ts +27 -0
- package/dist/policies/traffic/request-limit.js +41 -0
- package/dist/policies/traffic/request-limit.js.map +1 -0
- package/dist/policies/traffic/resource-filter.d.ts +38 -0
- package/dist/policies/traffic/resource-filter.js +184 -0
- package/dist/policies/traffic/resource-filter.js.map +1 -0
- package/dist/policies/traffic/ssl-enforce.d.ts +27 -0
- package/dist/policies/traffic/ssl-enforce.js +38 -0
- package/dist/policies/traffic/ssl-enforce.js.map +1 -0
- package/dist/policies/traffic/traffic-shadow.d.ts +40 -0
- package/dist/policies/traffic/traffic-shadow.js +87 -0
- package/dist/policies/traffic/traffic-shadow.js.map +1 -0
- package/dist/policies/transform/assign-attributes.d.ts +33 -0
- package/dist/policies/transform/assign-attributes.js +38 -0
- package/dist/policies/transform/assign-attributes.js.map +1 -0
- package/dist/policies/transform/assign-content.d.ts +40 -0
- package/dist/policies/transform/assign-content.js +185 -0
- package/dist/policies/transform/assign-content.js.map +1 -0
- package/dist/policies/transform/cors.d.ts +57 -0
- package/dist/policies/transform/cors.js +23 -0
- package/dist/policies/transform/cors.js.map +1 -0
- package/dist/policies/transform/json-validation.d.ts +50 -0
- package/dist/policies/transform/json-validation.js +125 -0
- package/dist/policies/transform/json-validation.js.map +1 -0
- package/dist/policies/transform/override-method.d.ts +33 -0
- package/dist/policies/transform/override-method.js +48 -0
- package/dist/policies/transform/override-method.js.map +1 -0
- package/dist/policies/transform/request-validation.d.ts +59 -0
- package/dist/policies/transform/request-validation.js +121 -0
- package/dist/policies/transform/request-validation.js.map +1 -0
- package/dist/policies/transform/transform.d.ts +75 -0
- package/dist/policies/transform/transform.js +116 -0
- package/dist/policies/transform/transform.js.map +1 -0
- package/dist/policies/types.d.ts +4 -0
- package/dist/policies/types.js +1 -0
- package/dist/policies/types.js.map +1 -0
- package/dist/protocol-2fD3DJrL.d.ts +725 -0
- package/dist/utils/cidr.d.ts +58 -0
- package/dist/utils/cidr.js +107 -0
- package/dist/utils/cidr.js.map +1 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/debug.js +13 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/headers.d.ts +68 -0
- package/dist/utils/headers.js +25 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/ip.d.ts +64 -0
- package/dist/utils/ip.js +29 -0
- package/dist/utils/ip.js.map +1 -0
- package/dist/utils/redact.d.ts +30 -0
- package/dist/utils/redact.js +52 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/request-id.d.ts +11 -0
- package/dist/utils/request-id.js +7 -0
- package/dist/utils/request-id.js.map +1 -0
- package/dist/utils/timing-safe.d.ts +31 -0
- package/dist/utils/timing-safe.js +17 -0
- package/dist/utils/timing-safe.js.map +1 -0
- package/dist/utils/timing.d.ts +27 -0
- package/dist/utils/timing.js +12 -0
- package/dist/utils/timing.js.map +1 -0
- package/dist/utils/trace-context.d.ts +51 -0
- package/dist/utils/trace-context.js +37 -0
- package/dist/utils/trace-context.js.map +1 -0
- package/package.json +213 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/observability/metrics-reporter.ts"],"sourcesContent":["/**\n * Metrics reporter policy - records request counts, latencies, and errors.\n *\n * Plugs into any {@link MetricsCollector} implementation and records\n * standard gateway metrics per request. Runs at priority 1 (just after\n * request-log at 0). Merges any custom tags from `assignMetrics`.\n *\n * @module metrics-reporter\n */\nimport type { MetricsCollector } from \"../../observability/metrics\";\nimport { definePolicy, Priority, safeCall } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface MetricsReporterConfig extends PolicyConfig {\n /** The metrics collector to record to. */\n collector: MetricsCollector;\n}\n\n/**\n * Record standard gateway metrics for every request.\n *\n * Metrics recorded:\n * - `gateway_requests_total` (counter) - total requests, tagged by method/path/status/gateway\n * - `gateway_request_duration_ms` (histogram) - end-to-end request duration\n * - `gateway_request_errors_total` (counter) - requests with status >= 400\n * - `gateway_policy_duration_ms` (histogram) - per-policy timing when available\n *\n * @param config - Must include a {@link MetricsCollector} instance.\n * @returns A {@link Policy} at priority 1.\n */\nexport const metricsReporter =\n /*#__PURE__*/ definePolicy<MetricsReporterConfig>({\n name: \"metrics-reporter\",\n priority: Priority.METRICS,\n httpOnly: true,\n handler: async (c, next, { config, debug, gateway }) => {\n const startTime = Date.now();\n\n await next();\n\n // Collector failures must never crash the request pipeline\n await safeCall(\n async () => {\n const dynamicTagsRaw = c.get(\"_metricsTags\") as\n | Record<string, unknown>\n | undefined;\n const dynamicTags: Record<string, string> = {};\n if (dynamicTagsRaw) {\n for (const [key, value] of Object.entries(dynamicTagsRaw)) {\n if (typeof value === \"string\") {\n dynamicTags[key] = value;\n }\n }\n }\n\n const url = new URL(c.req.url);\n const tags: Record<string, string> = {\n ...dynamicTags,\n method: c.req.method,\n path: gateway?.routePath ?? url.pathname,\n status: String(c.res.status),\n gateway: gateway?.gatewayName ?? \"unknown\",\n };\n\n // Total requests counter\n config.collector.increment(\"gateway_requests_total\", 1, tags);\n\n // Request duration histogram\n const duration = Date.now() - startTime;\n config.collector.histogram(\n \"gateway_request_duration_ms\",\n duration,\n tags\n );\n\n // Error counter (4xx and 5xx)\n if (c.res.status >= 400) {\n config.collector.increment(\"gateway_request_errors_total\", 1, tags);\n }\n\n // Per-policy timing (if available from pipeline instrumentation)\n const timings = c.get(\"_policyTimings\") as\n | Array<{ name: string; durationMs: number }>\n | undefined;\n if (timings) {\n for (const t of timings) {\n config.collector.histogram(\n \"gateway_policy_duration_ms\",\n t.durationMs,\n {\n ...dynamicTags,\n policy: t.name,\n gateway: gateway?.gatewayName ?? \"unknown\",\n }\n );\n }\n }\n },\n undefined,\n debug,\n \"collector\"\n );\n },\n });\n"],"mappings":"AAUA,SAAS,cAAc,UAAU,gBAAgB;AAoB1C,MAAM,kBACG,6BAAoC;AAAA,EAChD,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,UAAU;AAAA,EACV,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,OAAO,QAAQ,MAAM;AACtD,UAAM,YAAY,KAAK,IAAI;AAE3B,UAAM,KAAK;AAGX,UAAM;AAAA,MACJ,YAAY;AACV,cAAM,iBAAiB,EAAE,IAAI,cAAc;AAG3C,cAAM,cAAsC,CAAC;AAC7C,YAAI,gBAAgB;AAClB,qBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,cAAc,GAAG;AACzD,gBAAI,OAAO,UAAU,UAAU;AAC7B,0BAAY,GAAG,IAAI;AAAA,YACrB;AAAA,UACF;AAAA,QACF;AAEA,cAAM,MAAM,IAAI,IAAI,EAAE,IAAI,GAAG;AAC7B,cAAM,OAA+B;AAAA,UACnC,GAAG;AAAA,UACH,QAAQ,EAAE,IAAI;AAAA,UACd,MAAM,SAAS,aAAa,IAAI;AAAA,UAChC,QAAQ,OAAO,EAAE,IAAI,MAAM;AAAA,UAC3B,SAAS,SAAS,eAAe;AAAA,QACnC;AAGA,eAAO,UAAU,UAAU,0BAA0B,GAAG,IAAI;AAG5D,cAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,eAAO,UAAU;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAGA,YAAI,EAAE,IAAI,UAAU,KAAK;AACvB,iBAAO,UAAU,UAAU,gCAAgC,GAAG,IAAI;AAAA,QACpE;AAGA,cAAM,UAAU,EAAE,IAAI,gBAAgB;AAGtC,YAAI,SAAS;AACX,qBAAW,KAAK,SAAS;AACvB,mBAAO,UAAU;AAAA,cACf;AAAA,cACA,EAAE;AAAA,cACF;AAAA,gBACE,GAAG;AAAA,gBACH,QAAQ,EAAE;AAAA,gBACV,SAAS,SAAS,eAAe;AAAA,cACnC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
interface RequestLogConfig extends PolicyConfig {
|
|
7
|
+
/** Additional fields to extract from the request */
|
|
8
|
+
extractFields?: (c: unknown) => Record<string, unknown>;
|
|
9
|
+
/** Custom log sink - defaults to console.log with structured JSON */
|
|
10
|
+
sink?: (entry: LogEntry) => void | Promise<void>;
|
|
11
|
+
/** Ordered list of headers to inspect for the client IP. Default: `["cf-connecting-ip", "x-forwarded-for"]`. */
|
|
12
|
+
ipHeaders?: string[];
|
|
13
|
+
/** Log request body (opt-in). Default: `false`. */
|
|
14
|
+
logRequestBody?: boolean;
|
|
15
|
+
/** Log response body (opt-in). Default: `false`. */
|
|
16
|
+
logResponseBody?: boolean;
|
|
17
|
+
/** Maximum body size in bytes to capture. Default: `8192`. */
|
|
18
|
+
maxBodyLength?: number;
|
|
19
|
+
/** JSON field paths to redact from logged bodies (e.g., `["password", "*.secret"]`). */
|
|
20
|
+
redactPaths?: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Structured log entry emitted for each request/response pair. */
|
|
23
|
+
interface LogEntry {
|
|
24
|
+
/** ISO 8601 timestamp when the log was emitted. */
|
|
25
|
+
timestamp: string;
|
|
26
|
+
/** Unique request ID for distributed tracing. */
|
|
27
|
+
requestId: string;
|
|
28
|
+
/** HTTP method (e.g. `"GET"`, `"POST"`). */
|
|
29
|
+
method: string;
|
|
30
|
+
/** URL pathname (without query string). */
|
|
31
|
+
path: string;
|
|
32
|
+
/** HTTP response status code. */
|
|
33
|
+
statusCode: number;
|
|
34
|
+
/** End-to-end request duration in milliseconds. */
|
|
35
|
+
durationMs: number;
|
|
36
|
+
/** Client IP from `CF-Connecting-IP` or `X-Forwarded-For`. */
|
|
37
|
+
clientIp: string;
|
|
38
|
+
/** Client User-Agent header value. */
|
|
39
|
+
userAgent: string;
|
|
40
|
+
/** Gateway name from config. */
|
|
41
|
+
gatewayName: string;
|
|
42
|
+
/** Matched route path pattern. */
|
|
43
|
+
routePath: string;
|
|
44
|
+
/** Upstream target identifier (e.g. origin URL, service binding name, or `"handler"`). */
|
|
45
|
+
upstream: string;
|
|
46
|
+
/** W3C Trace Context - 32-hex trace ID. */
|
|
47
|
+
traceId?: string;
|
|
48
|
+
/** W3C Trace Context - 16-hex span ID for this gateway request. */
|
|
49
|
+
spanId?: string;
|
|
50
|
+
/** Captured request body (when `logRequestBody` is enabled). */
|
|
51
|
+
requestBody?: unknown;
|
|
52
|
+
/** Captured response body (when `logResponseBody` is enabled). */
|
|
53
|
+
responseBody?: unknown;
|
|
54
|
+
/** Custom fields from `extractFields` callback. */
|
|
55
|
+
extra?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Emit structured JSON logs for every request/response pair.
|
|
59
|
+
*
|
|
60
|
+
* Captures method, path, status, duration, client IP, user agent, and
|
|
61
|
+
* gateway context (request ID, gateway name, route path). Runs at priority 0
|
|
62
|
+
* so it wraps the entire pipeline and measures end-to-end latency.
|
|
63
|
+
*
|
|
64
|
+
* By default, logs are written to `console.log` as JSON lines. Provide a
|
|
65
|
+
* custom `sink` to route logs to an external service (e.g., Logflare,
|
|
66
|
+
* Datadog, or a Durable Object buffer).
|
|
67
|
+
*
|
|
68
|
+
* ## Data boundary: request logs vs analytics
|
|
69
|
+
*
|
|
70
|
+
* Request logs and analytics (`@vivero/stoma-analytics`) serve
|
|
71
|
+
* different purposes and deliberately carry different fields.
|
|
72
|
+
*
|
|
73
|
+
* **Request logs** (this policy) are for **debugging and operational triage**.
|
|
74
|
+
* Fields are high-cardinality — grep-friendly, not GROUP BY-friendly:
|
|
75
|
+
*
|
|
76
|
+
* | Field | Why it's here |
|
|
77
|
+
* |--------------|--------------------------------------------------------|
|
|
78
|
+
* | requestId | Unique per request — grep to find a single transaction |
|
|
79
|
+
* | path | Actual URL e.g. /users/42 (high cardinality) |
|
|
80
|
+
* | clientIp | PII, high cardinality — abuse investigation only |
|
|
81
|
+
* | userAgent | High cardinality — debug specific client issues |
|
|
82
|
+
* | spanId | Distributed tracing span correlation |
|
|
83
|
+
* | requestBody | Deep debugging (opt-in, redactable) |
|
|
84
|
+
* | responseBody | Deep debugging (opt-in, redactable) |
|
|
85
|
+
*
|
|
86
|
+
* **Overlapping fields** (appear in both logs and analytics):
|
|
87
|
+
*
|
|
88
|
+
* | Field | Why both need it |
|
|
89
|
+
* |-------------|--------------------------------------------------------|
|
|
90
|
+
* | timestamp | Time-series bucketing (analytics) / grep by time (logs)|
|
|
91
|
+
* | gatewayName | GROUP BY gateway (analytics) / filter logs by gateway |
|
|
92
|
+
* | routePath | GROUP BY route pattern (analytics) / filter by route |
|
|
93
|
+
* | method | GROUP BY method (analytics) / filter logs by method |
|
|
94
|
+
* | statusCode | Error rate dashboards (analytics) / grep errors (logs) |
|
|
95
|
+
* | durationMs | AVG/P99 latency (analytics) / slow request triage |
|
|
96
|
+
* | traceId | Dashboard anomaly drill-down → find matching log lines |
|
|
97
|
+
*
|
|
98
|
+
* **Analytics-only fields** (NOT in request logs):
|
|
99
|
+
*
|
|
100
|
+
* | Field | Why only analytics |
|
|
101
|
+
* |--------------|-------------------------------------------------------|
|
|
102
|
+
* | responseSize | SUM bandwidth, detect payload bloat — aggregate only |
|
|
103
|
+
* | dimensions | Extensible low-cardinality facets for GROUP BY |
|
|
104
|
+
*
|
|
105
|
+
* @param config - Custom field extraction, body logging, and sink. All fields optional.
|
|
106
|
+
* @returns A {@link Policy} at priority 0 (runs first, wraps everything).
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* import { createGateway } from "@vivero/stoma";
|
|
111
|
+
* import { requestLog } from "@vivero/stoma/policies";
|
|
112
|
+
*
|
|
113
|
+
* // Default structured JSON logging to console
|
|
114
|
+
* createGateway({
|
|
115
|
+
* policies: [requestLog()],
|
|
116
|
+
* routes: [...],
|
|
117
|
+
* });
|
|
118
|
+
*
|
|
119
|
+
* // With body logging and redaction
|
|
120
|
+
* requestLog({
|
|
121
|
+
* logRequestBody: true,
|
|
122
|
+
* logResponseBody: true,
|
|
123
|
+
* redactPaths: ["password", "*.secret", "auth.token"],
|
|
124
|
+
* sink: async (entry) => {
|
|
125
|
+
* await fetch("https://logs.example.com/ingest", {
|
|
126
|
+
* method: "POST",
|
|
127
|
+
* body: JSON.stringify(entry),
|
|
128
|
+
* });
|
|
129
|
+
* },
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
declare const requestLog: (config?: RequestLogConfig | undefined) => Policy;
|
|
134
|
+
|
|
135
|
+
export { type LogEntry, type RequestLogConfig, requestLog };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { extractClientIp } from "../../utils/ip";
|
|
2
|
+
import { redactFields } from "../../utils/redact";
|
|
3
|
+
import { definePolicy, Priority, safeCall } from "../sdk";
|
|
4
|
+
const DEFAULT_MAX_BODY_LENGTH = 8192;
|
|
5
|
+
const requestLog = /* @__PURE__ */ definePolicy({
|
|
6
|
+
name: "request-log",
|
|
7
|
+
priority: Priority.OBSERVABILITY,
|
|
8
|
+
httpOnly: true,
|
|
9
|
+
handler: async (c, next, { config, debug, gateway }) => {
|
|
10
|
+
const sink = config.sink ?? defaultSink;
|
|
11
|
+
const maxBodyLength = config.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH;
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
let requestBody;
|
|
14
|
+
if (config.logRequestBody) {
|
|
15
|
+
requestBody = await captureRequestBody(
|
|
16
|
+
c.req.raw,
|
|
17
|
+
maxBodyLength,
|
|
18
|
+
config.redactPaths
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
await next();
|
|
22
|
+
const url = new URL(c.req.url);
|
|
23
|
+
const entry = {
|
|
24
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25
|
+
requestId: gateway?.requestId ?? c.res.headers.get("x-request-id") ?? "unknown",
|
|
26
|
+
method: c.req.method,
|
|
27
|
+
path: url.pathname,
|
|
28
|
+
statusCode: c.res.status,
|
|
29
|
+
durationMs: Date.now() - startTime,
|
|
30
|
+
clientIp: extractClientIp(c.req.raw.headers, {
|
|
31
|
+
ipHeaders: config.ipHeaders,
|
|
32
|
+
fallbackAddress: getRemoteAddress(c)
|
|
33
|
+
}),
|
|
34
|
+
userAgent: c.req.header("user-agent") ?? "unknown",
|
|
35
|
+
gatewayName: gateway?.gatewayName ?? "unknown",
|
|
36
|
+
routePath: gateway?.routePath ?? url.pathname,
|
|
37
|
+
upstream: c.get("_upstreamTarget") ?? "unknown",
|
|
38
|
+
traceId: gateway?.traceId,
|
|
39
|
+
spanId: gateway?.spanId
|
|
40
|
+
};
|
|
41
|
+
if (requestBody !== void 0) {
|
|
42
|
+
entry.requestBody = requestBody;
|
|
43
|
+
}
|
|
44
|
+
if (config.logResponseBody) {
|
|
45
|
+
const responseBody = await captureResponseBody(
|
|
46
|
+
c,
|
|
47
|
+
maxBodyLength,
|
|
48
|
+
config.redactPaths
|
|
49
|
+
);
|
|
50
|
+
if (responseBody !== void 0) {
|
|
51
|
+
entry.responseBody = responseBody;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (config.extractFields) {
|
|
55
|
+
try {
|
|
56
|
+
entry.extra = config.extractFields(c);
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await safeCall(
|
|
61
|
+
() => Promise.resolve(sink(entry)),
|
|
62
|
+
void 0,
|
|
63
|
+
debug,
|
|
64
|
+
"sink()"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
async function captureRequestBody(req, maxLength, redactPaths) {
|
|
69
|
+
try {
|
|
70
|
+
const cloned = req.clone();
|
|
71
|
+
const text = await cloned.text();
|
|
72
|
+
if (!text) return void 0;
|
|
73
|
+
const truncated = text.length > maxLength ? `${text.slice(0, maxLength)}...[truncated]` : text;
|
|
74
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
75
|
+
if (contentType.includes("application/json")) {
|
|
76
|
+
try {
|
|
77
|
+
let parsed = JSON.parse(
|
|
78
|
+
truncated.endsWith("...[truncated]") ? text.slice(0, maxLength) : text
|
|
79
|
+
);
|
|
80
|
+
if (redactPaths?.length) {
|
|
81
|
+
parsed = redactFields(parsed, { paths: redactPaths });
|
|
82
|
+
}
|
|
83
|
+
return parsed;
|
|
84
|
+
} catch {
|
|
85
|
+
return truncated;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return truncated;
|
|
89
|
+
} catch {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function captureResponseBody(c, maxLength, redactPaths) {
|
|
94
|
+
try {
|
|
95
|
+
const cloned = c.res.clone();
|
|
96
|
+
const text = await cloned.text();
|
|
97
|
+
if (!text) return void 0;
|
|
98
|
+
const truncated = text.length > maxLength ? `${text.slice(0, maxLength)}...[truncated]` : text;
|
|
99
|
+
const contentType = c.res.headers.get("content-type") ?? "";
|
|
100
|
+
if (contentType.includes("application/json")) {
|
|
101
|
+
try {
|
|
102
|
+
let parsed = JSON.parse(
|
|
103
|
+
truncated.endsWith("...[truncated]") ? text.slice(0, maxLength) : text
|
|
104
|
+
);
|
|
105
|
+
if (redactPaths?.length) {
|
|
106
|
+
parsed = redactFields(parsed, { paths: redactPaths });
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
} catch {
|
|
110
|
+
return truncated;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return truncated;
|
|
114
|
+
} catch {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function getRemoteAddress(c) {
|
|
119
|
+
try {
|
|
120
|
+
const env = c.env;
|
|
121
|
+
const incoming = env?.incoming;
|
|
122
|
+
const addr = incoming?.socket?.remoteAddress;
|
|
123
|
+
if (typeof addr === "string" && addr) return addr;
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
function defaultSink(entry) {
|
|
129
|
+
console.log(JSON.stringify(entry));
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
requestLog
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=request-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/observability/request-log.ts"],"sourcesContent":["/**\n * Request logging policy - structured JSON logs for every request.\n *\n * @module request-log\n */\nimport { extractClientIp } from \"../../utils/ip\";\nimport { redactFields } from \"../../utils/redact\";\nimport { definePolicy, Priority, safeCall } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface RequestLogConfig extends PolicyConfig {\n /** Additional fields to extract from the request */\n extractFields?: (c: unknown) => Record<string, unknown>;\n /** Custom log sink - defaults to console.log with structured JSON */\n sink?: (entry: LogEntry) => void | Promise<void>;\n /** Ordered list of headers to inspect for the client IP. Default: `[\"cf-connecting-ip\", \"x-forwarded-for\"]`. */\n ipHeaders?: string[];\n /** Log request body (opt-in). Default: `false`. */\n logRequestBody?: boolean;\n /** Log response body (opt-in). Default: `false`. */\n logResponseBody?: boolean;\n /** Maximum body size in bytes to capture. Default: `8192`. */\n maxBodyLength?: number;\n /** JSON field paths to redact from logged bodies (e.g., `[\"password\", \"*.secret\"]`). */\n redactPaths?: string[];\n}\n\n/** Structured log entry emitted for each request/response pair. */\nexport interface LogEntry {\n /** ISO 8601 timestamp when the log was emitted. */\n timestamp: string;\n /** Unique request ID for distributed tracing. */\n requestId: string;\n /** HTTP method (e.g. `\"GET\"`, `\"POST\"`). */\n method: string;\n /** URL pathname (without query string). */\n path: string;\n /** HTTP response status code. */\n statusCode: number;\n /** End-to-end request duration in milliseconds. */\n durationMs: number;\n /** Client IP from `CF-Connecting-IP` or `X-Forwarded-For`. */\n clientIp: string;\n /** Client User-Agent header value. */\n userAgent: string;\n /** Gateway name from config. */\n gatewayName: string;\n /** Matched route path pattern. */\n routePath: string;\n /** Upstream target identifier (e.g. origin URL, service binding name, or `\"handler\"`). */\n upstream: string;\n /** W3C Trace Context - 32-hex trace ID. */\n traceId?: string;\n /** W3C Trace Context - 16-hex span ID for this gateway request. */\n spanId?: string;\n /** Captured request body (when `logRequestBody` is enabled). */\n requestBody?: unknown;\n /** Captured response body (when `logResponseBody` is enabled). */\n responseBody?: unknown;\n /** Custom fields from `extractFields` callback. */\n extra?: Record<string, unknown>;\n}\n\nconst DEFAULT_MAX_BODY_LENGTH = 8192;\n\n/**\n * Emit structured JSON logs for every request/response pair.\n *\n * Captures method, path, status, duration, client IP, user agent, and\n * gateway context (request ID, gateway name, route path). Runs at priority 0\n * so it wraps the entire pipeline and measures end-to-end latency.\n *\n * By default, logs are written to `console.log` as JSON lines. Provide a\n * custom `sink` to route logs to an external service (e.g., Logflare,\n * Datadog, or a Durable Object buffer).\n *\n * ## Data boundary: request logs vs analytics\n *\n * Request logs and analytics (`@vivero/stoma-analytics`) serve\n * different purposes and deliberately carry different fields.\n *\n * **Request logs** (this policy) are for **debugging and operational triage**.\n * Fields are high-cardinality — grep-friendly, not GROUP BY-friendly:\n *\n * | Field | Why it's here |\n * |--------------|--------------------------------------------------------|\n * | requestId | Unique per request — grep to find a single transaction |\n * | path | Actual URL e.g. /users/42 (high cardinality) |\n * | clientIp | PII, high cardinality — abuse investigation only |\n * | userAgent | High cardinality — debug specific client issues |\n * | spanId | Distributed tracing span correlation |\n * | requestBody | Deep debugging (opt-in, redactable) |\n * | responseBody | Deep debugging (opt-in, redactable) |\n *\n * **Overlapping fields** (appear in both logs and analytics):\n *\n * | Field | Why both need it |\n * |-------------|--------------------------------------------------------|\n * | timestamp | Time-series bucketing (analytics) / grep by time (logs)|\n * | gatewayName | GROUP BY gateway (analytics) / filter logs by gateway |\n * | routePath | GROUP BY route pattern (analytics) / filter by route |\n * | method | GROUP BY method (analytics) / filter logs by method |\n * | statusCode | Error rate dashboards (analytics) / grep errors (logs) |\n * | durationMs | AVG/P99 latency (analytics) / slow request triage |\n * | traceId | Dashboard anomaly drill-down → find matching log lines |\n *\n * **Analytics-only fields** (NOT in request logs):\n *\n * | Field | Why only analytics |\n * |--------------|-------------------------------------------------------|\n * | responseSize | SUM bandwidth, detect payload bloat — aggregate only |\n * | dimensions | Extensible low-cardinality facets for GROUP BY |\n *\n * @param config - Custom field extraction, body logging, and sink. All fields optional.\n * @returns A {@link Policy} at priority 0 (runs first, wraps everything).\n *\n * @example\n * ```ts\n * import { createGateway } from \"@vivero/stoma\";\n * import { requestLog } from \"@vivero/stoma/policies\";\n *\n * // Default structured JSON logging to console\n * createGateway({\n * policies: [requestLog()],\n * routes: [...],\n * });\n *\n * // With body logging and redaction\n * requestLog({\n * logRequestBody: true,\n * logResponseBody: true,\n * redactPaths: [\"password\", \"*.secret\", \"auth.token\"],\n * sink: async (entry) => {\n * await fetch(\"https://logs.example.com/ingest\", {\n * method: \"POST\",\n * body: JSON.stringify(entry),\n * });\n * },\n * });\n * ```\n */\nexport const requestLog = /*#__PURE__*/ definePolicy<RequestLogConfig>({\n name: \"request-log\",\n priority: Priority.OBSERVABILITY,\n httpOnly: true,\n handler: async (c, next, { config, debug, gateway }) => {\n const sink = config.sink ?? defaultSink;\n const maxBodyLength = config.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH;\n const startTime = Date.now();\n\n // Capture request body before downstream consumes it\n let requestBody: unknown;\n if (config.logRequestBody) {\n requestBody = await captureRequestBody(\n c.req.raw,\n maxBodyLength,\n config.redactPaths\n );\n }\n\n // Let downstream run\n await next();\n\n // Build log entry from gateway context and response\n const url = new URL(c.req.url);\n\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n requestId:\n gateway?.requestId ?? c.res.headers.get(\"x-request-id\") ?? \"unknown\",\n method: c.req.method,\n path: url.pathname,\n statusCode: c.res.status,\n durationMs: Date.now() - startTime,\n clientIp: extractClientIp(c.req.raw.headers, {\n ipHeaders: config.ipHeaders,\n fallbackAddress: getRemoteAddress(c),\n }),\n userAgent: c.req.header(\"user-agent\") ?? \"unknown\",\n gatewayName: gateway?.gatewayName ?? \"unknown\",\n routePath: gateway?.routePath ?? url.pathname,\n upstream: (c.get(\"_upstreamTarget\") as string | undefined) ?? \"unknown\",\n traceId: gateway?.traceId,\n spanId: gateway?.spanId,\n };\n\n if (requestBody !== undefined) {\n entry.requestBody = requestBody;\n }\n\n // Capture response body after downstream\n if (config.logResponseBody) {\n const responseBody = await captureResponseBody(\n c,\n maxBodyLength,\n config.redactPaths\n );\n if (responseBody !== undefined) {\n entry.responseBody = responseBody;\n }\n }\n\n // Extract custom fields\n if (config.extractFields) {\n try {\n entry.extra = config.extractFields(c);\n } catch {\n // Don't let field extraction break the request\n }\n }\n\n // Sink failure must never crash the request pipeline\n await safeCall(\n () => Promise.resolve(sink(entry)),\n undefined,\n debug,\n \"sink()\"\n );\n },\n});\n\n/**\n * Capture and optionally redact the request body.\n * Clones the request to avoid consuming the body stream.\n */\nasync function captureRequestBody(\n req: Request,\n maxLength: number,\n redactPaths?: string[]\n): Promise<unknown> {\n try {\n // Clone so the original body stream stays available for downstream\n const cloned = req.clone();\n const text = await cloned.text();\n if (!text) return undefined;\n\n const truncated =\n text.length > maxLength\n ? `${text.slice(0, maxLength)}...[truncated]`\n : text;\n\n const contentType = req.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n try {\n let parsed: unknown = JSON.parse(\n truncated.endsWith(\"...[truncated]\") ? text.slice(0, maxLength) : text\n );\n if (redactPaths?.length) {\n parsed = redactFields(parsed, { paths: redactPaths });\n }\n return parsed;\n } catch {\n // Invalid JSON - return as string\n return truncated;\n }\n }\n\n return truncated;\n } catch {\n // Never break the request pipeline\n return undefined;\n }\n}\n\n/**\n * Capture and optionally redact the response body.\n * Clones the response to avoid consuming the body.\n */\nasync function captureResponseBody(\n c: { res: Response },\n maxLength: number,\n redactPaths?: string[]\n): Promise<unknown> {\n try {\n // Clone the response so the original body remains consumable\n const cloned = c.res.clone();\n const text = await cloned.text();\n if (!text) return undefined;\n\n const truncated =\n text.length > maxLength\n ? `${text.slice(0, maxLength)}...[truncated]`\n : text;\n\n const contentType = c.res.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n try {\n let parsed: unknown = JSON.parse(\n truncated.endsWith(\"...[truncated]\") ? text.slice(0, maxLength) : text\n );\n if (redactPaths?.length) {\n parsed = redactFields(parsed, { paths: redactPaths });\n }\n return parsed;\n } catch {\n return truncated;\n }\n }\n\n return truncated;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Try to extract the remote address from the Hono env.\n *\n * `@hono/node-server` stores the Node.js `IncomingMessage` as `c.env.incoming`,\n * giving access to `socket.remoteAddress`. This is a duck-typed check so the\n * gateway library doesn't depend on any specific runtime adapter.\n */\nfunction getRemoteAddress(c: { env?: unknown }): string | undefined {\n try {\n const env = c.env as Record<string, unknown> | undefined;\n // @hono/node-server: env.incoming is a Node IncomingMessage\n const incoming = env?.incoming as\n | { socket?: { remoteAddress?: string } }\n | undefined;\n const addr = incoming?.socket?.remoteAddress;\n if (typeof addr === \"string\" && addr) return addr;\n } catch {\n // Never break logging\n }\n return undefined;\n}\n\nfunction defaultSink(entry: LogEntry): void {\n console.log(JSON.stringify(entry));\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import { Context } from 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
/** Visibility mode controlling when timing headers are emitted. */
|
|
7
|
+
type ServerTimingVisibility = "always" | "debug-only" | "conditional";
|
|
8
|
+
interface ServerTimingConfig extends PolicyConfig {
|
|
9
|
+
/** Emit the `Server-Timing` header with per-policy breakdown. Default: `true`. */
|
|
10
|
+
serverTimingHeader?: boolean;
|
|
11
|
+
/** Emit the `X-Response-Time` header with total gateway time. Default: `true`. */
|
|
12
|
+
responseTimeHeader?: boolean;
|
|
13
|
+
/** Number of decimal places for duration values. Default: `1`. */
|
|
14
|
+
precision?: number;
|
|
15
|
+
/** Add a `total` entry to `Server-Timing`. Default: `true`. */
|
|
16
|
+
includeTotal?: boolean;
|
|
17
|
+
/** Optional function to generate a description for each timing entry. */
|
|
18
|
+
descriptionFn?: (name: string) => string;
|
|
19
|
+
/** Controls when timing headers are emitted. Default: `"debug-only"`. */
|
|
20
|
+
visibility?: ServerTimingVisibility;
|
|
21
|
+
/** Required when `visibility` is `"conditional"`. Called per-request to decide. */
|
|
22
|
+
visibilityFn?: (c: Context) => boolean | Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Emit W3C `Server-Timing` and `X-Response-Time` response headers.
|
|
26
|
+
*
|
|
27
|
+
* Reads per-policy timing data from the pipeline instrumentation and
|
|
28
|
+
* formats it as standard headers visible in browser DevTools.
|
|
29
|
+
*
|
|
30
|
+
* @param config - Optional configuration for headers, precision, and visibility.
|
|
31
|
+
* @returns A {@link Policy} at priority 1 (METRICS).
|
|
32
|
+
*/
|
|
33
|
+
declare const serverTiming: (config?: ServerTimingConfig | undefined) => Policy;
|
|
34
|
+
|
|
35
|
+
export { type ServerTimingConfig, type ServerTimingVisibility, serverTiming };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { GatewayError } from "../../core/errors";
|
|
2
|
+
import { escapeHeaderValue } from "../../utils/headers";
|
|
3
|
+
import { toSelfTimes } from "../../utils/timing";
|
|
4
|
+
import { definePolicy, isDebugRequested, Priority } from "../sdk";
|
|
5
|
+
function sanitizeMetricName(name) {
|
|
6
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7
|
+
}
|
|
8
|
+
function formatEntry(name, durationMs, precision, descriptionFn) {
|
|
9
|
+
const sanitized = sanitizeMetricName(name);
|
|
10
|
+
const dur = durationMs.toFixed(precision);
|
|
11
|
+
const desc = descriptionFn?.(name);
|
|
12
|
+
if (desc) {
|
|
13
|
+
return `${sanitized};dur=${dur};desc="${escapeHeaderValue(desc)}"`;
|
|
14
|
+
}
|
|
15
|
+
return `${sanitized};dur=${dur}`;
|
|
16
|
+
}
|
|
17
|
+
const serverTiming = /* @__PURE__ */ definePolicy({
|
|
18
|
+
name: "server-timing",
|
|
19
|
+
priority: Priority.METRICS,
|
|
20
|
+
httpOnly: true,
|
|
21
|
+
defaults: {
|
|
22
|
+
serverTimingHeader: true,
|
|
23
|
+
responseTimeHeader: true,
|
|
24
|
+
precision: 1,
|
|
25
|
+
includeTotal: true,
|
|
26
|
+
visibility: "debug-only"
|
|
27
|
+
},
|
|
28
|
+
validate: (config) => {
|
|
29
|
+
if (config.visibility === "conditional" && typeof config.visibilityFn !== "function") {
|
|
30
|
+
throw new GatewayError(
|
|
31
|
+
500,
|
|
32
|
+
"config-error",
|
|
33
|
+
'serverTiming: visibility "conditional" requires a visibilityFn'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
handler: async (c, next, { config, debug }) => {
|
|
38
|
+
const startTime = Date.now();
|
|
39
|
+
await next();
|
|
40
|
+
let visible;
|
|
41
|
+
switch (config.visibility) {
|
|
42
|
+
case "always":
|
|
43
|
+
visible = true;
|
|
44
|
+
break;
|
|
45
|
+
case "conditional":
|
|
46
|
+
visible = await config.visibilityFn(c);
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
visible = isDebugRequested(c);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (!visible) {
|
|
53
|
+
debug("skipping - visibility check failed");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const totalMs = Date.now() - startTime;
|
|
57
|
+
const precision = config.precision;
|
|
58
|
+
const rawTimings = c.get("_policyTimings");
|
|
59
|
+
const selfTimings = rawTimings ? toSelfTimes(rawTimings) : void 0;
|
|
60
|
+
if (config.serverTimingHeader) {
|
|
61
|
+
const entries = [];
|
|
62
|
+
if (config.includeTotal) {
|
|
63
|
+
entries.push(
|
|
64
|
+
formatEntry("total", totalMs, precision, config.descriptionFn)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (selfTimings) {
|
|
68
|
+
for (const t of selfTimings) {
|
|
69
|
+
entries.push(
|
|
70
|
+
formatEntry(t.name, t.durationMs, precision, config.descriptionFn)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (entries.length > 0) {
|
|
75
|
+
c.res.headers.set("server-timing", entries.join(", "));
|
|
76
|
+
debug("Server-Timing: %s", entries.join(", "));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (config.responseTimeHeader) {
|
|
80
|
+
const value = `${totalMs.toFixed(precision)}ms`;
|
|
81
|
+
c.res.headers.set("x-response-time", value);
|
|
82
|
+
debug("X-Response-Time: %s", value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
export {
|
|
87
|
+
serverTiming
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=server-timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/observability/server-timing.ts"],"sourcesContent":["/**\n * Server-Timing policy - surfaces per-policy timing as W3C Server-Timing headers.\n *\n * Reads the `_policyTimings` context key populated by the pipeline and\n * emits standard `Server-Timing` and `X-Response-Time` response headers.\n * Browser DevTools display `Server-Timing` entries natively.\n *\n * Visibility modes control when headers are emitted:\n * - `\"always\"` - every response (dev, internal APIs)\n * - `\"debug-only\"` - only when the client sent `x-stoma-debug` (safe for production)\n * - `\"conditional\"` - user-provided predicate function\n *\n * @module server-timing\n */\nimport type { Context } from \"hono\";\nimport { GatewayError } from \"../../core/errors\";\nimport { escapeHeaderValue } from \"../../utils/headers\";\nimport { toSelfTimes } from \"../../utils/timing\";\nimport { definePolicy, isDebugRequested, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\n/** Visibility mode controlling when timing headers are emitted. */\nexport type ServerTimingVisibility = \"always\" | \"debug-only\" | \"conditional\";\n\nexport interface ServerTimingConfig extends PolicyConfig {\n /** Emit the `Server-Timing` header with per-policy breakdown. Default: `true`. */\n serverTimingHeader?: boolean;\n /** Emit the `X-Response-Time` header with total gateway time. Default: `true`. */\n responseTimeHeader?: boolean;\n /** Number of decimal places for duration values. Default: `1`. */\n precision?: number;\n /** Add a `total` entry to `Server-Timing`. Default: `true`. */\n includeTotal?: boolean;\n /** Optional function to generate a description for each timing entry. */\n descriptionFn?: (name: string) => string;\n /** Controls when timing headers are emitted. Default: `\"debug-only\"`. */\n visibility?: ServerTimingVisibility;\n /** Required when `visibility` is `\"conditional\"`. Called per-request to decide. */\n visibilityFn?: (c: Context) => boolean | Promise<boolean>;\n}\n\n// -- helpers (file-private) ---------------------------------------------------\n\n/** Strip characters outside the Server-Timing token charset. */\nfunction sanitizeMetricName(name: string): string {\n return name.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n}\n\n/** Build a single Server-Timing entry string. */\nfunction formatEntry(\n name: string,\n durationMs: number,\n precision: number,\n descriptionFn?: (name: string) => string\n): string {\n const sanitized = sanitizeMetricName(name);\n const dur = durationMs.toFixed(precision);\n const desc = descriptionFn?.(name);\n if (desc) {\n return `${sanitized};dur=${dur};desc=\"${escapeHeaderValue(desc)}\"`;\n }\n return `${sanitized};dur=${dur}`;\n}\n\n// -- policy -------------------------------------------------------------------\n\n/**\n * Emit W3C `Server-Timing` and `X-Response-Time` response headers.\n *\n * Reads per-policy timing data from the pipeline instrumentation and\n * formats it as standard headers visible in browser DevTools.\n *\n * @param config - Optional configuration for headers, precision, and visibility.\n * @returns A {@link Policy} at priority 1 (METRICS).\n */\nexport const serverTiming = /*#__PURE__*/ definePolicy<ServerTimingConfig>({\n name: \"server-timing\",\n priority: Priority.METRICS,\n httpOnly: true,\n defaults: {\n serverTimingHeader: true,\n responseTimeHeader: true,\n precision: 1,\n includeTotal: true,\n visibility: \"debug-only\",\n },\n validate: (config) => {\n if (\n config.visibility === \"conditional\" &&\n typeof config.visibilityFn !== \"function\"\n ) {\n throw new GatewayError(\n 500,\n \"config-error\",\n 'serverTiming: visibility \"conditional\" requires a visibilityFn'\n );\n }\n },\n handler: async (c, next, { config, debug }) => {\n const startTime = Date.now();\n\n await next();\n\n // Evaluate visibility\n let visible: boolean;\n switch (config.visibility) {\n case \"always\":\n visible = true;\n break;\n case \"conditional\":\n visible = await config.visibilityFn!(c);\n break;\n default:\n visible = isDebugRequested(c);\n break;\n }\n\n if (!visible) {\n debug(\"skipping - visibility check failed\");\n return;\n }\n\n const totalMs = Date.now() - startTime;\n const precision = config.precision!;\n\n // Read per-policy timings accumulated by policiesToMiddleware().\n // These are inclusive (onion-model) - convert to self-time for the header.\n const rawTimings = c.get(\"_policyTimings\") as\n | Array<{ name: string; durationMs: number }>\n | undefined;\n const selfTimings = rawTimings ? toSelfTimes(rawTimings) : undefined;\n\n // Server-Timing header\n if (config.serverTimingHeader) {\n const entries: string[] = [];\n\n if (config.includeTotal) {\n entries.push(\n formatEntry(\"total\", totalMs, precision, config.descriptionFn)\n );\n }\n\n if (selfTimings) {\n for (const t of selfTimings) {\n entries.push(\n formatEntry(t.name, t.durationMs, precision, config.descriptionFn)\n );\n }\n }\n\n if (entries.length > 0) {\n c.res.headers.set(\"server-timing\", entries.join(\", \"));\n debug(\"Server-Timing: %s\", entries.join(\", \"));\n }\n }\n\n // X-Response-Time header\n if (config.responseTimeHeader) {\n const value = `${totalMs.toFixed(precision)}ms`;\n c.res.headers.set(\"x-response-time\", value);\n debug(\"X-Response-Time: %s\", value);\n }\n },\n});\n"],"mappings":"AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAC5B,SAAS,cAAc,kBAAkB,gBAAgB;AA0BzD,SAAS,mBAAmB,MAAsB;AAChD,SAAO,KAAK,QAAQ,mBAAmB,GAAG;AAC5C;AAGA,SAAS,YACP,MACA,YACA,WACA,eACQ;AACR,QAAM,YAAY,mBAAmB,IAAI;AACzC,QAAM,MAAM,WAAW,QAAQ,SAAS;AACxC,QAAM,OAAO,gBAAgB,IAAI;AACjC,MAAI,MAAM;AACR,WAAO,GAAG,SAAS,QAAQ,GAAG,UAAU,kBAAkB,IAAI,CAAC;AAAA,EACjE;AACA,SAAO,GAAG,SAAS,QAAQ,GAAG;AAChC;AAaO,MAAM,eAA6B,6BAAiC;AAAA,EACzE,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,UAAU;AAAA,EACV,UAAU;AAAA,IACR,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,IACpB,WAAW;AAAA,IACX,cAAc;AAAA,IACd,YAAY;AAAA,EACd;AAAA,EACA,UAAU,CAAC,WAAW;AACpB,QACE,OAAO,eAAe,iBACtB,OAAO,OAAO,iBAAiB,YAC/B;AACA,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC7C,UAAM,YAAY,KAAK,IAAI;AAE3B,UAAM,KAAK;AAGX,QAAI;AACJ,YAAQ,OAAO,YAAY;AAAA,MACzB,KAAK;AACH,kBAAU;AACV;AAAA,MACF,KAAK;AACH,kBAAU,MAAM,OAAO,aAAc,CAAC;AACtC;AAAA,MACF;AACE,kBAAU,iBAAiB,CAAC;AAC5B;AAAA,IACJ;AAEA,QAAI,CAAC,SAAS;AACZ,YAAM,oCAAoC;AAC1C;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,IAAI,IAAI;AAC7B,UAAM,YAAY,OAAO;AAIzB,UAAM,aAAa,EAAE,IAAI,gBAAgB;AAGzC,UAAM,cAAc,aAAa,YAAY,UAAU,IAAI;AAG3D,QAAI,OAAO,oBAAoB;AAC7B,YAAM,UAAoB,CAAC;AAE3B,UAAI,OAAO,cAAc;AACvB,gBAAQ;AAAA,UACN,YAAY,SAAS,SAAS,WAAW,OAAO,aAAa;AAAA,QAC/D;AAAA,MACF;AAEA,UAAI,aAAa;AACf,mBAAW,KAAK,aAAa;AAC3B,kBAAQ;AAAA,YACN,YAAY,EAAE,MAAM,EAAE,YAAY,WAAW,OAAO,aAAa;AAAA,UACnE;AAAA,QACF;AAAA,MACF;AAEA,UAAI,QAAQ,SAAS,GAAG;AACtB,UAAE,IAAI,QAAQ,IAAI,iBAAiB,QAAQ,KAAK,IAAI,CAAC;AACrD,cAAM,qBAAqB,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC/C;AAAA,IACF;AAGA,QAAI,OAAO,oBAAoB;AAC7B,YAAM,QAAQ,GAAG,QAAQ,QAAQ,SAAS,CAAC;AAC3C,QAAE,IAAI,QAAQ,IAAI,mBAAmB,KAAK;AAC1C,YAAM,uBAAuB,KAAK;AAAA,IACpC;AAAA,EACF;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import './sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Proxy policy - per-route header manipulation and timeout control.
|
|
8
|
+
*
|
|
9
|
+
* @module proxy
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface ProxyPolicyConfig extends PolicyConfig {
|
|
13
|
+
/** Headers to add to the proxied request */
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
/** Headers to strip from the proxied request */
|
|
16
|
+
stripHeaders?: string[];
|
|
17
|
+
/** Timeout in milliseconds. Default: 30000. */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Whether to preserve the inbound Host header when proxying to URL upstreams.
|
|
21
|
+
* Default: false (Host is rewritten to the upstream target host).
|
|
22
|
+
*/
|
|
23
|
+
preserveHost?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Apply additional header manipulation and timeout control to the upstream call.
|
|
27
|
+
*
|
|
28
|
+
* Use this when you need per-route header injection, header stripping, or
|
|
29
|
+
* a custom timeout that wraps the upstream dispatch. The core proxy
|
|
30
|
+
* forwarding (URL, Service Binding, Handler) is handled by the gateway's
|
|
31
|
+
* upstream handler - this policy layers on top of it.
|
|
32
|
+
*
|
|
33
|
+
* `preserveHost` applies to URL upstreams, instructing the upstream handler
|
|
34
|
+
* not to rewrite the Host header to the target host.
|
|
35
|
+
*
|
|
36
|
+
* Handles Cloudflare Workers' immutable `Request.headers` by cloning the
|
|
37
|
+
* request when header modifications are needed.
|
|
38
|
+
*
|
|
39
|
+
* @param config - Headers to add/strip, timeout, and host preservation. All fields optional.
|
|
40
|
+
* @returns A {@link Policy} at priority 95 (runs late, just before the upstream call).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { proxy } from "@vivero/stoma/policies";
|
|
45
|
+
*
|
|
46
|
+
* // Add an internal auth header and strip cookies for the upstream
|
|
47
|
+
* proxy({
|
|
48
|
+
* headers: { "x-internal-key": "secret-123" },
|
|
49
|
+
* stripHeaders: ["cookie", "x-forwarded-for"],
|
|
50
|
+
* timeout: 10_000,
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* // Preserve the original Host header for virtual-host routing
|
|
54
|
+
* proxy({ preserveHost: true });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function proxy(config?: ProxyPolicyConfig): Policy;
|
|
58
|
+
|
|
59
|
+
export { type ProxyPolicyConfig, proxy };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { withModifiedHeaders } from "../utils/headers";
|
|
2
|
+
import { Priority, withSkip } from "./sdk";
|
|
3
|
+
function proxy(config) {
|
|
4
|
+
const timeout = config?.timeout ?? 3e4;
|
|
5
|
+
const handler = async (c, next) => {
|
|
6
|
+
if (config?.preserveHost) {
|
|
7
|
+
c.set("_preserveHost", true);
|
|
8
|
+
}
|
|
9
|
+
if (config?.stripHeaders || config?.headers) {
|
|
10
|
+
const stripHeaders = config?.stripHeaders;
|
|
11
|
+
const headersToSet = config?.headers;
|
|
12
|
+
withModifiedHeaders(c, (headers) => {
|
|
13
|
+
if (stripHeaders) {
|
|
14
|
+
for (const header of stripHeaders) {
|
|
15
|
+
headers.delete(header);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (headersToSet) {
|
|
19
|
+
for (const [key, value] of Object.entries(headersToSet)) {
|
|
20
|
+
headers.set(key, value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (timeout > 0) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
28
|
+
try {
|
|
29
|
+
await next();
|
|
30
|
+
} finally {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
await next();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
name: "proxy",
|
|
39
|
+
priority: Priority.PROXY,
|
|
40
|
+
handler: withSkip(config?.skip, handler),
|
|
41
|
+
httpOnly: true
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
proxy
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/policies/proxy.ts"],"sourcesContent":["/**\n * Proxy policy - per-route header manipulation and timeout control.\n *\n * @module proxy\n */\n\nimport { withModifiedHeaders } from \"../utils/headers\";\nimport { Priority, withSkip } from \"./sdk\";\nimport type { Policy, PolicyConfig } from \"./types\";\n\nexport interface ProxyPolicyConfig extends PolicyConfig {\n /** Headers to add to the proxied request */\n headers?: Record<string, string>;\n /** Headers to strip from the proxied request */\n stripHeaders?: string[];\n /** Timeout in milliseconds. Default: 30000. */\n timeout?: number;\n /**\n * Whether to preserve the inbound Host header when proxying to URL upstreams.\n * Default: false (Host is rewritten to the upstream target host).\n */\n preserveHost?: boolean;\n}\n\n/**\n * Apply additional header manipulation and timeout control to the upstream call.\n *\n * Use this when you need per-route header injection, header stripping, or\n * a custom timeout that wraps the upstream dispatch. The core proxy\n * forwarding (URL, Service Binding, Handler) is handled by the gateway's\n * upstream handler - this policy layers on top of it.\n *\n * `preserveHost` applies to URL upstreams, instructing the upstream handler\n * not to rewrite the Host header to the target host.\n *\n * Handles Cloudflare Workers' immutable `Request.headers` by cloning the\n * request when header modifications are needed.\n *\n * @param config - Headers to add/strip, timeout, and host preservation. All fields optional.\n * @returns A {@link Policy} at priority 95 (runs late, just before the upstream call).\n *\n * @example\n * ```ts\n * import { proxy } from \"@vivero/stoma/policies\";\n *\n * // Add an internal auth header and strip cookies for the upstream\n * proxy({\n * headers: { \"x-internal-key\": \"secret-123\" },\n * stripHeaders: [\"cookie\", \"x-forwarded-for\"],\n * timeout: 10_000,\n * });\n *\n * // Preserve the original Host header for virtual-host routing\n * proxy({ preserveHost: true });\n * ```\n */\nexport function proxy(config?: ProxyPolicyConfig): Policy {\n const timeout = config?.timeout ?? 30_000;\n\n const handler: import(\"hono\").MiddlewareHandler = async (c, next) => {\n if (config?.preserveHost) {\n c.set(\"_preserveHost\", true);\n }\n\n // Workers runtime has immutable Request.headers - clone into mutable copy\n if (config?.stripHeaders || config?.headers) {\n const stripHeaders = config?.stripHeaders;\n const headersToSet = config?.headers;\n withModifiedHeaders(c, (headers) => {\n if (stripHeaders) {\n for (const header of stripHeaders) {\n headers.delete(header);\n }\n }\n\n if (headersToSet) {\n for (const [key, value] of Object.entries(headersToSet)) {\n headers.set(key, value);\n }\n }\n });\n }\n\n // Apply timeout via AbortSignal\n if (timeout > 0) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n try {\n await next();\n } finally {\n clearTimeout(timer);\n }\n } else {\n await next();\n }\n };\n\n return {\n name: \"proxy\",\n priority: Priority.PROXY,\n handler: withSkip(config?.skip, handler),\n httpOnly: true,\n };\n}\n"],"mappings":"AAMA,SAAS,2BAA2B;AACpC,SAAS,UAAU,gBAAgB;AAiD5B,SAAS,MAAM,QAAoC;AACxD,QAAM,UAAU,QAAQ,WAAW;AAEnC,QAAM,UAA4C,OAAO,GAAG,SAAS;AACnE,QAAI,QAAQ,cAAc;AACxB,QAAE,IAAI,iBAAiB,IAAI;AAAA,IAC7B;AAGA,QAAI,QAAQ,gBAAgB,QAAQ,SAAS;AAC3C,YAAM,eAAe,QAAQ;AAC7B,YAAM,eAAe,QAAQ;AAC7B,0BAAoB,GAAG,CAAC,YAAY;AAClC,YAAI,cAAc;AAChB,qBAAW,UAAU,cAAc;AACjC,oBAAQ,OAAO,MAAM;AAAA,UACvB;AAAA,QACF;AAEA,YAAI,cAAc;AAChB,qBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACvD,oBAAQ,IAAI,KAAK,KAAK;AAAA,UACxB;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAGA,QAAI,UAAU,GAAG;AACf,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO;AAC1D,UAAI;AACF,cAAM,KAAK;AAAA,MACb,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF,OAAO;AACL,YAAM,KAAK;AAAA,IACb;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,SAAS;AAAA,IACnB,SAAS,SAAS,QAAQ,MAAM,OAAO;AAAA,IACvC,UAAU;AAAA,EACZ;AACF;","names":[]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import 'hono';
|
|
2
|
+
export { v as CircuitBreakerConfig, a as CircuitBreakerSnapshot, b as CircuitBreakerStore, c as CircuitState, e as InMemoryCircuitBreakerStore, s as circuitBreaker } from '../../protocol-2fD3DJrL.js';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|