@vivero/stoma 0.1.0-rc.5

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.
Files changed (251) hide show
  1. package/dist/adapters/bun.d.ts +9 -0
  2. package/dist/adapters/bun.js +8 -0
  3. package/dist/adapters/bun.js.map +1 -0
  4. package/dist/adapters/cloudflare.d.ts +49 -0
  5. package/dist/adapters/cloudflare.js +85 -0
  6. package/dist/adapters/cloudflare.js.map +1 -0
  7. package/dist/adapters/deno.d.ts +9 -0
  8. package/dist/adapters/deno.js +8 -0
  9. package/dist/adapters/deno.js.map +1 -0
  10. package/dist/adapters/durable-object.d.ts +63 -0
  11. package/dist/adapters/durable-object.js +46 -0
  12. package/dist/adapters/durable-object.js.map +1 -0
  13. package/dist/adapters/index.d.ts +13 -0
  14. package/dist/adapters/index.js +53 -0
  15. package/dist/adapters/index.js.map +1 -0
  16. package/dist/adapters/memory.d.ts +9 -0
  17. package/dist/adapters/memory.js +14 -0
  18. package/dist/adapters/memory.js.map +1 -0
  19. package/dist/adapters/node.d.ts +9 -0
  20. package/dist/adapters/node.js +8 -0
  21. package/dist/adapters/node.js.map +1 -0
  22. package/dist/adapters/postgres.d.ts +109 -0
  23. package/dist/adapters/postgres.js +242 -0
  24. package/dist/adapters/postgres.js.map +1 -0
  25. package/dist/adapters/redis.d.ts +116 -0
  26. package/dist/adapters/redis.js +194 -0
  27. package/dist/adapters/redis.js.map +1 -0
  28. package/dist/adapters/testing.d.ts +32 -0
  29. package/dist/adapters/testing.js +33 -0
  30. package/dist/adapters/testing.js.map +1 -0
  31. package/dist/adapters/types.d.ts +4 -0
  32. package/dist/adapters/types.js +1 -0
  33. package/dist/adapters/types.js.map +1 -0
  34. package/dist/config/index.d.ts +11 -0
  35. package/dist/config/index.js +21 -0
  36. package/dist/config/index.js.map +1 -0
  37. package/dist/config/merge.d.ts +48 -0
  38. package/dist/config/merge.js +83 -0
  39. package/dist/config/merge.js.map +1 -0
  40. package/dist/config/schema.d.ts +254 -0
  41. package/dist/config/schema.js +109 -0
  42. package/dist/config/schema.js.map +1 -0
  43. package/dist/core/errors.d.ts +66 -0
  44. package/dist/core/errors.js +47 -0
  45. package/dist/core/errors.js.map +1 -0
  46. package/dist/core/gateway.d.ts +44 -0
  47. package/dist/core/gateway.js +390 -0
  48. package/dist/core/gateway.js.map +1 -0
  49. package/dist/core/health.d.ts +78 -0
  50. package/dist/core/health.js +65 -0
  51. package/dist/core/health.js.map +1 -0
  52. package/dist/core/pipeline.d.ts +62 -0
  53. package/dist/core/pipeline.js +214 -0
  54. package/dist/core/pipeline.js.map +1 -0
  55. package/dist/core/protocol.d.ts +4 -0
  56. package/dist/core/protocol.js +1 -0
  57. package/dist/core/protocol.js.map +1 -0
  58. package/dist/core/scope.d.ts +67 -0
  59. package/dist/core/scope.js +44 -0
  60. package/dist/core/scope.js.map +1 -0
  61. package/dist/core/types.d.ts +252 -0
  62. package/dist/core/types.js +1 -0
  63. package/dist/core/types.js.map +1 -0
  64. package/dist/index.d.ts +57 -0
  65. package/dist/index.js +158 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/observability/admin.d.ts +32 -0
  68. package/dist/observability/admin.js +85 -0
  69. package/dist/observability/admin.js.map +1 -0
  70. package/dist/observability/metrics.d.ts +78 -0
  71. package/dist/observability/metrics.js +107 -0
  72. package/dist/observability/metrics.js.map +1 -0
  73. package/dist/observability/tracing.d.ts +149 -0
  74. package/dist/observability/tracing.js +191 -0
  75. package/dist/observability/tracing.js.map +1 -0
  76. package/dist/policies/auth/api-key-auth.d.ts +64 -0
  77. package/dist/policies/auth/api-key-auth.js +93 -0
  78. package/dist/policies/auth/api-key-auth.js.map +1 -0
  79. package/dist/policies/auth/basic-auth.d.ts +33 -0
  80. package/dist/policies/auth/basic-auth.js +96 -0
  81. package/dist/policies/auth/basic-auth.js.map +1 -0
  82. package/dist/policies/auth/crypto.d.ts +29 -0
  83. package/dist/policies/auth/crypto.js +100 -0
  84. package/dist/policies/auth/crypto.js.map +1 -0
  85. package/dist/policies/auth/generate-http-signature.d.ts +30 -0
  86. package/dist/policies/auth/generate-http-signature.js +79 -0
  87. package/dist/policies/auth/generate-http-signature.js.map +1 -0
  88. package/dist/policies/auth/generate-jwt.d.ts +44 -0
  89. package/dist/policies/auth/generate-jwt.js +99 -0
  90. package/dist/policies/auth/generate-jwt.js.map +1 -0
  91. package/dist/policies/auth/http-signature-base.d.ts +55 -0
  92. package/dist/policies/auth/http-signature-base.js +140 -0
  93. package/dist/policies/auth/http-signature-base.js.map +1 -0
  94. package/dist/policies/auth/jws.d.ts +46 -0
  95. package/dist/policies/auth/jws.js +317 -0
  96. package/dist/policies/auth/jws.js.map +1 -0
  97. package/dist/policies/auth/jwt-auth.d.ts +64 -0
  98. package/dist/policies/auth/jwt-auth.js +268 -0
  99. package/dist/policies/auth/jwt-auth.js.map +1 -0
  100. package/dist/policies/auth/oauth2.d.ts +38 -0
  101. package/dist/policies/auth/oauth2.js +254 -0
  102. package/dist/policies/auth/oauth2.js.map +1 -0
  103. package/dist/policies/auth/rbac.d.ts +30 -0
  104. package/dist/policies/auth/rbac.js +115 -0
  105. package/dist/policies/auth/rbac.js.map +1 -0
  106. package/dist/policies/auth/verify-http-signature.d.ts +30 -0
  107. package/dist/policies/auth/verify-http-signature.js +147 -0
  108. package/dist/policies/auth/verify-http-signature.js.map +1 -0
  109. package/dist/policies/index.d.ts +51 -0
  110. package/dist/policies/index.js +109 -0
  111. package/dist/policies/index.js.map +1 -0
  112. package/dist/policies/mock.d.ts +60 -0
  113. package/dist/policies/mock.js +29 -0
  114. package/dist/policies/mock.js.map +1 -0
  115. package/dist/policies/observability/assign-metrics.d.ts +37 -0
  116. package/dist/policies/observability/assign-metrics.js +29 -0
  117. package/dist/policies/observability/assign-metrics.js.map +1 -0
  118. package/dist/policies/observability/metrics-reporter.d.ts +25 -0
  119. package/dist/policies/observability/metrics-reporter.js +62 -0
  120. package/dist/policies/observability/metrics-reporter.js.map +1 -0
  121. package/dist/policies/observability/request-log.d.ts +135 -0
  122. package/dist/policies/observability/request-log.js +124 -0
  123. package/dist/policies/observability/request-log.js.map +1 -0
  124. package/dist/policies/observability/server-timing.d.ts +35 -0
  125. package/dist/policies/observability/server-timing.js +89 -0
  126. package/dist/policies/observability/server-timing.js.map +1 -0
  127. package/dist/policies/proxy.d.ts +59 -0
  128. package/dist/policies/proxy.js +47 -0
  129. package/dist/policies/proxy.js.map +1 -0
  130. package/dist/policies/resilience/circuit-breaker.d.ts +4 -0
  131. package/dist/policies/resilience/circuit-breaker.js +280 -0
  132. package/dist/policies/resilience/circuit-breaker.js.map +1 -0
  133. package/dist/policies/resilience/latency-injection.d.ts +35 -0
  134. package/dist/policies/resilience/latency-injection.js +26 -0
  135. package/dist/policies/resilience/latency-injection.js.map +1 -0
  136. package/dist/policies/resilience/retry.d.ts +71 -0
  137. package/dist/policies/resilience/retry.js +79 -0
  138. package/dist/policies/resilience/retry.js.map +1 -0
  139. package/dist/policies/resilience/timeout.d.ts +32 -0
  140. package/dist/policies/resilience/timeout.js +46 -0
  141. package/dist/policies/resilience/timeout.js.map +1 -0
  142. package/dist/policies/sdk/define-policy.d.ts +176 -0
  143. package/dist/policies/sdk/define-policy.js +42 -0
  144. package/dist/policies/sdk/define-policy.js.map +1 -0
  145. package/dist/policies/sdk/helpers.d.ts +132 -0
  146. package/dist/policies/sdk/helpers.js +87 -0
  147. package/dist/policies/sdk/helpers.js.map +1 -0
  148. package/dist/policies/sdk/index.d.ts +10 -0
  149. package/dist/policies/sdk/index.js +35 -0
  150. package/dist/policies/sdk/index.js.map +1 -0
  151. package/dist/policies/sdk/priority.d.ts +44 -0
  152. package/dist/policies/sdk/priority.js +36 -0
  153. package/dist/policies/sdk/priority.js.map +1 -0
  154. package/dist/policies/sdk/testing.d.ts +53 -0
  155. package/dist/policies/sdk/testing.js +41 -0
  156. package/dist/policies/sdk/testing.js.map +1 -0
  157. package/dist/policies/sdk/trace.d.ts +73 -0
  158. package/dist/policies/sdk/trace.js +25 -0
  159. package/dist/policies/sdk/trace.js.map +1 -0
  160. package/dist/policies/traffic/cache.d.ts +4 -0
  161. package/dist/policies/traffic/cache.js +224 -0
  162. package/dist/policies/traffic/cache.js.map +1 -0
  163. package/dist/policies/traffic/dynamic-routing.d.ts +54 -0
  164. package/dist/policies/traffic/dynamic-routing.js +36 -0
  165. package/dist/policies/traffic/dynamic-routing.js.map +1 -0
  166. package/dist/policies/traffic/geo-ip-filter.d.ts +37 -0
  167. package/dist/policies/traffic/geo-ip-filter.js +74 -0
  168. package/dist/policies/traffic/geo-ip-filter.js.map +1 -0
  169. package/dist/policies/traffic/http-callout.d.ts +59 -0
  170. package/dist/policies/traffic/http-callout.js +69 -0
  171. package/dist/policies/traffic/http-callout.js.map +1 -0
  172. package/dist/policies/traffic/interrupt.d.ts +46 -0
  173. package/dist/policies/traffic/interrupt.js +38 -0
  174. package/dist/policies/traffic/interrupt.js.map +1 -0
  175. package/dist/policies/traffic/ip-filter.d.ts +47 -0
  176. package/dist/policies/traffic/ip-filter.js +57 -0
  177. package/dist/policies/traffic/ip-filter.js.map +1 -0
  178. package/dist/policies/traffic/json-threat-protection.d.ts +51 -0
  179. package/dist/policies/traffic/json-threat-protection.js +173 -0
  180. package/dist/policies/traffic/json-threat-protection.js.map +1 -0
  181. package/dist/policies/traffic/rate-limit.d.ts +4 -0
  182. package/dist/policies/traffic/rate-limit.js +145 -0
  183. package/dist/policies/traffic/rate-limit.js.map +1 -0
  184. package/dist/policies/traffic/regex-threat-protection.d.ts +54 -0
  185. package/dist/policies/traffic/regex-threat-protection.js +109 -0
  186. package/dist/policies/traffic/regex-threat-protection.js.map +1 -0
  187. package/dist/policies/traffic/request-limit.d.ts +27 -0
  188. package/dist/policies/traffic/request-limit.js +41 -0
  189. package/dist/policies/traffic/request-limit.js.map +1 -0
  190. package/dist/policies/traffic/resource-filter.d.ts +38 -0
  191. package/dist/policies/traffic/resource-filter.js +184 -0
  192. package/dist/policies/traffic/resource-filter.js.map +1 -0
  193. package/dist/policies/traffic/ssl-enforce.d.ts +27 -0
  194. package/dist/policies/traffic/ssl-enforce.js +38 -0
  195. package/dist/policies/traffic/ssl-enforce.js.map +1 -0
  196. package/dist/policies/traffic/traffic-shadow.d.ts +40 -0
  197. package/dist/policies/traffic/traffic-shadow.js +87 -0
  198. package/dist/policies/traffic/traffic-shadow.js.map +1 -0
  199. package/dist/policies/transform/assign-attributes.d.ts +33 -0
  200. package/dist/policies/transform/assign-attributes.js +38 -0
  201. package/dist/policies/transform/assign-attributes.js.map +1 -0
  202. package/dist/policies/transform/assign-content.d.ts +40 -0
  203. package/dist/policies/transform/assign-content.js +185 -0
  204. package/dist/policies/transform/assign-content.js.map +1 -0
  205. package/dist/policies/transform/cors.d.ts +57 -0
  206. package/dist/policies/transform/cors.js +23 -0
  207. package/dist/policies/transform/cors.js.map +1 -0
  208. package/dist/policies/transform/json-validation.d.ts +50 -0
  209. package/dist/policies/transform/json-validation.js +125 -0
  210. package/dist/policies/transform/json-validation.js.map +1 -0
  211. package/dist/policies/transform/override-method.d.ts +33 -0
  212. package/dist/policies/transform/override-method.js +48 -0
  213. package/dist/policies/transform/override-method.js.map +1 -0
  214. package/dist/policies/transform/request-validation.d.ts +59 -0
  215. package/dist/policies/transform/request-validation.js +121 -0
  216. package/dist/policies/transform/request-validation.js.map +1 -0
  217. package/dist/policies/transform/transform.d.ts +75 -0
  218. package/dist/policies/transform/transform.js +116 -0
  219. package/dist/policies/transform/transform.js.map +1 -0
  220. package/dist/policies/types.d.ts +4 -0
  221. package/dist/policies/types.js +1 -0
  222. package/dist/policies/types.js.map +1 -0
  223. package/dist/protocol-2fD3DJrL.d.ts +725 -0
  224. package/dist/utils/cidr.d.ts +58 -0
  225. package/dist/utils/cidr.js +107 -0
  226. package/dist/utils/cidr.js.map +1 -0
  227. package/dist/utils/debug.d.ts +1 -0
  228. package/dist/utils/debug.js +13 -0
  229. package/dist/utils/debug.js.map +1 -0
  230. package/dist/utils/headers.d.ts +68 -0
  231. package/dist/utils/headers.js +25 -0
  232. package/dist/utils/headers.js.map +1 -0
  233. package/dist/utils/ip.d.ts +58 -0
  234. package/dist/utils/ip.js +28 -0
  235. package/dist/utils/ip.js.map +1 -0
  236. package/dist/utils/redact.d.ts +30 -0
  237. package/dist/utils/redact.js +52 -0
  238. package/dist/utils/redact.js.map +1 -0
  239. package/dist/utils/request-id.d.ts +11 -0
  240. package/dist/utils/request-id.js +7 -0
  241. package/dist/utils/request-id.js.map +1 -0
  242. package/dist/utils/timing-safe.d.ts +31 -0
  243. package/dist/utils/timing-safe.js +17 -0
  244. package/dist/utils/timing-safe.js.map +1 -0
  245. package/dist/utils/timing.d.ts +27 -0
  246. package/dist/utils/timing.js +12 -0
  247. package/dist/utils/timing.js.map +1 -0
  248. package/dist/utils/trace-context.d.ts +51 -0
  249. package/dist/utils/trace-context.js +37 -0
  250. package/dist/utils/trace-context.js.map +1 -0
  251. package/package.json +202 -0
@@ -0,0 +1,78 @@
1
+ import { RouteConfig } from './types.js';
2
+ import 'hono';
3
+ import '../protocol-2fD3DJrL.js';
4
+ import '../policies/sdk/trace.js';
5
+ import '@vivero/stoma-core';
6
+ import '../observability/metrics.js';
7
+ import '../observability/tracing.js';
8
+
9
+ /**
10
+ * Health check route factory with optional upstream probing.
11
+ *
12
+ * @module health
13
+ */
14
+
15
+ interface HealthConfig {
16
+ /** Health endpoint path. Default: "/health". */
17
+ path?: string;
18
+ /** URLs to probe for upstream health. */
19
+ upstreamProbes?: string[];
20
+ /** Include individual upstream statuses in response. Default: false. */
21
+ includeUpstreamStatus?: boolean;
22
+ /** Timeout in ms for each upstream probe. Default: 5000. */
23
+ probeTimeoutMs?: number;
24
+ /** HTTP method for upstream probes. Default: `"HEAD"`. */
25
+ probeMethod?: string;
26
+ /** Status code returned when all probes are unhealthy. Default: 503. */
27
+ unhealthyStatusCode?: number;
28
+ }
29
+ /**
30
+ * Create a health check route for liveness and upstream probing.
31
+ *
32
+ * Returns a {@link RouteConfig} (not a Policy) - add it directly to the
33
+ * gateway's `routes` array. Without upstream probes, returns a simple
34
+ * `{ status: "healthy" }` response. With probes, performs concurrent HEAD
35
+ * requests (5s timeout each) and reports aggregate status:
36
+ * - `"healthy"` - all probes passed
37
+ * - `"degraded"` - some probes failed
38
+ * - `"unhealthy"` - all probes failed (returns 503)
39
+ *
40
+ * @security Enabling `includeUpstreamStatus: true` causes the response to
41
+ * include the URLs and availability status of internal upstream services.
42
+ * On public-facing endpoints this leaks internal service topology, which
43
+ * can aid attackers in reconnaissance (identifying internal hostnames,
44
+ * ports, and service availability patterns). Restrict health routes that
45
+ * expose upstream status to internal or admin-only paths, or protect them
46
+ * with an authentication policy.
47
+ *
48
+ * @param config - Endpoint path, upstream probe URLs, and status detail toggle. All fields optional.
49
+ * @returns A {@link RouteConfig} for a GET health endpoint.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { createGateway } from "@vivero/stoma";
54
+ * import { health } from "@vivero/stoma/policies";
55
+ *
56
+ * createGateway({
57
+ * routes: [
58
+ * // Simple liveness check at /health
59
+ * health(),
60
+ *
61
+ * // Probe upstreams with detailed status at /healthz
62
+ * health({
63
+ * path: "/healthz",
64
+ * upstreamProbes: [
65
+ * "https://api.example.com/health",
66
+ * "https://auth.example.com/health",
67
+ * ],
68
+ * includeUpstreamStatus: true,
69
+ * }),
70
+ *
71
+ * // ...other routes
72
+ * ],
73
+ * });
74
+ * ```
75
+ */
76
+ declare function health<TBindings = Record<string, unknown>>(config?: HealthConfig): RouteConfig<TBindings>;
77
+
78
+ export { type HealthConfig, health };
@@ -0,0 +1,65 @@
1
+ function health(config) {
2
+ const path = config?.path ?? "/health";
3
+ const probes = config?.upstreamProbes ?? [];
4
+ const includeStatus = config?.includeUpstreamStatus ?? false;
5
+ const probeTimeoutMs = config?.probeTimeoutMs ?? 5e3;
6
+ const probeMethod = config?.probeMethod ?? "HEAD";
7
+ const unhealthyStatusCode = config?.unhealthyStatusCode ?? 503;
8
+ return {
9
+ path,
10
+ methods: ["GET"],
11
+ pipeline: {
12
+ upstream: {
13
+ type: "handler",
14
+ handler: async (c) => {
15
+ if (probes.length === 0) {
16
+ return c.json({
17
+ status: "healthy",
18
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19
+ });
20
+ }
21
+ const results = await Promise.all(
22
+ probes.map(async (url) => {
23
+ const start = Date.now();
24
+ try {
25
+ const res = await fetch(url, {
26
+ method: probeMethod,
27
+ signal: AbortSignal.timeout(probeTimeoutMs)
28
+ });
29
+ return {
30
+ url,
31
+ status: res.ok ? "healthy" : "unhealthy",
32
+ latencyMs: Date.now() - start
33
+ };
34
+ } catch {
35
+ return {
36
+ url,
37
+ status: "unhealthy",
38
+ latencyMs: Date.now() - start
39
+ };
40
+ }
41
+ })
42
+ );
43
+ const allHealthy = results.every((r) => r.status === "healthy");
44
+ const allUnhealthy = results.every((r) => r.status === "unhealthy");
45
+ const status = allHealthy ? "healthy" : allUnhealthy ? "unhealthy" : "degraded";
46
+ const body = {
47
+ status,
48
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
49
+ };
50
+ if (includeStatus) {
51
+ body.upstreams = results;
52
+ }
53
+ return c.json(
54
+ body,
55
+ status === "unhealthy" ? unhealthyStatusCode : 200
56
+ );
57
+ }
58
+ }
59
+ }
60
+ };
61
+ }
62
+ export {
63
+ health
64
+ };
65
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/health.ts"],"sourcesContent":["/**\n * Health check route factory with optional upstream probing.\n *\n * @module health\n */\nimport type { RouteConfig } from \"./types\";\n\nexport interface HealthConfig {\n /** Health endpoint path. Default: \"/health\". */\n path?: string;\n /** URLs to probe for upstream health. */\n upstreamProbes?: string[];\n /** Include individual upstream statuses in response. Default: false. */\n includeUpstreamStatus?: boolean;\n /** Timeout in ms for each upstream probe. Default: 5000. */\n probeTimeoutMs?: number;\n /** HTTP method for upstream probes. Default: `\"HEAD\"`. */\n probeMethod?: string;\n /** Status code returned when all probes are unhealthy. Default: 503. */\n unhealthyStatusCode?: number;\n}\n\ninterface UpstreamStatus {\n url: string;\n status: \"healthy\" | \"unhealthy\";\n latencyMs: number;\n}\n\n/**\n * Create a health check route for liveness and upstream probing.\n *\n * Returns a {@link RouteConfig} (not a Policy) - add it directly to the\n * gateway's `routes` array. Without upstream probes, returns a simple\n * `{ status: \"healthy\" }` response. With probes, performs concurrent HEAD\n * requests (5s timeout each) and reports aggregate status:\n * - `\"healthy\"` - all probes passed\n * - `\"degraded\"` - some probes failed\n * - `\"unhealthy\"` - all probes failed (returns 503)\n *\n * @security Enabling `includeUpstreamStatus: true` causes the response to\n * include the URLs and availability status of internal upstream services.\n * On public-facing endpoints this leaks internal service topology, which\n * can aid attackers in reconnaissance (identifying internal hostnames,\n * ports, and service availability patterns). Restrict health routes that\n * expose upstream status to internal or admin-only paths, or protect them\n * with an authentication policy.\n *\n * @param config - Endpoint path, upstream probe URLs, and status detail toggle. All fields optional.\n * @returns A {@link RouteConfig} for a GET health endpoint.\n *\n * @example\n * ```ts\n * import { createGateway } from \"@vivero/stoma\";\n * import { health } from \"@vivero/stoma/policies\";\n *\n * createGateway({\n * routes: [\n * // Simple liveness check at /health\n * health(),\n *\n * // Probe upstreams with detailed status at /healthz\n * health({\n * path: \"/healthz\",\n * upstreamProbes: [\n * \"https://api.example.com/health\",\n * \"https://auth.example.com/health\",\n * ],\n * includeUpstreamStatus: true,\n * }),\n *\n * // ...other routes\n * ],\n * });\n * ```\n */\nexport function health<TBindings = Record<string, unknown>>(\n config?: HealthConfig\n): RouteConfig<TBindings> {\n const path = config?.path ?? \"/health\";\n const probes = config?.upstreamProbes ?? [];\n const includeStatus = config?.includeUpstreamStatus ?? false;\n const probeTimeoutMs = config?.probeTimeoutMs ?? 5000;\n const probeMethod = config?.probeMethod ?? \"HEAD\";\n const unhealthyStatusCode = config?.unhealthyStatusCode ?? 503;\n\n return {\n path,\n methods: [\"GET\"],\n pipeline: {\n upstream: {\n type: \"handler\",\n handler: async (c) => {\n if (probes.length === 0) {\n return c.json({\n status: \"healthy\",\n timestamp: new Date().toISOString(),\n });\n }\n\n const results = await Promise.all(\n probes.map(async (url): Promise<UpstreamStatus> => {\n const start = Date.now();\n try {\n const res = await fetch(url, {\n method: probeMethod,\n signal: AbortSignal.timeout(probeTimeoutMs),\n });\n return {\n url,\n status: res.ok ? \"healthy\" : \"unhealthy\",\n latencyMs: Date.now() - start,\n };\n } catch {\n return {\n url,\n status: \"unhealthy\",\n latencyMs: Date.now() - start,\n };\n }\n })\n );\n\n const allHealthy = results.every((r) => r.status === \"healthy\");\n const allUnhealthy = results.every((r) => r.status === \"unhealthy\");\n\n const status = allHealthy\n ? \"healthy\"\n : allUnhealthy\n ? \"unhealthy\"\n : \"degraded\";\n\n const body: Record<string, unknown> = {\n status,\n timestamp: new Date().toISOString(),\n };\n\n if (includeStatus) {\n body.upstreams = results;\n }\n\n return c.json(\n body,\n status === \"unhealthy\" ? (unhealthyStatusCode as 503) : 200\n );\n },\n },\n },\n };\n}\n"],"mappings":"AA2EO,SAAS,OACd,QACwB;AACxB,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,SAAS,QAAQ,kBAAkB,CAAC;AAC1C,QAAM,gBAAgB,QAAQ,yBAAyB;AACvD,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,sBAAsB,QAAQ,uBAAuB;AAE3D,SAAO;AAAA,IACL;AAAA,IACA,SAAS,CAAC,KAAK;AAAA,IACf,UAAU;AAAA,MACR,UAAU;AAAA,QACR,MAAM;AAAA,QACN,SAAS,OAAO,MAAM;AACpB,cAAI,OAAO,WAAW,GAAG;AACvB,mBAAO,EAAE,KAAK;AAAA,cACZ,QAAQ;AAAA,cACR,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,YACpC,CAAC;AAAA,UACH;AAEA,gBAAM,UAAU,MAAM,QAAQ;AAAA,YAC5B,OAAO,IAAI,OAAO,QAAiC;AACjD,oBAAM,QAAQ,KAAK,IAAI;AACvB,kBAAI;AACF,sBAAM,MAAM,MAAM,MAAM,KAAK;AAAA,kBAC3B,QAAQ;AAAA,kBACR,QAAQ,YAAY,QAAQ,cAAc;AAAA,gBAC5C,CAAC;AACD,uBAAO;AAAA,kBACL;AAAA,kBACA,QAAQ,IAAI,KAAK,YAAY;AAAA,kBAC7B,WAAW,KAAK,IAAI,IAAI;AAAA,gBAC1B;AAAA,cACF,QAAQ;AACN,uBAAO;AAAA,kBACL;AAAA,kBACA,QAAQ;AAAA,kBACR,WAAW,KAAK,IAAI,IAAI;AAAA,gBAC1B;AAAA,cACF;AAAA,YACF,CAAC;AAAA,UACH;AAEA,gBAAM,aAAa,QAAQ,MAAM,CAAC,MAAM,EAAE,WAAW,SAAS;AAC9D,gBAAM,eAAe,QAAQ,MAAM,CAAC,MAAM,EAAE,WAAW,WAAW;AAElE,gBAAM,SAAS,aACX,YACA,eACE,cACA;AAEN,gBAAM,OAAgC;AAAA,YACpC;AAAA,YACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,UACpC;AAEA,cAAI,eAAe;AACjB,iBAAK,YAAY;AAAA,UACnB;AAEA,iBAAO,EAAE;AAAA,YACP;AAAA,YACA,WAAW,cAAe,sBAA8B;AAAA,UAC1D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,62 @@
1
+ import { Context, MiddlewareHandler } from 'hono';
2
+ import { h as PolicyContext, P as Policy, G as GatewayAdapter } from '../protocol-2fD3DJrL.js';
3
+ import { TracingConfig } from '../observability/tracing.js';
4
+ import { DebugLogger } from '@vivero/stoma-core';
5
+ import { DebugHeadersConfig } from './types.js';
6
+ import '../policies/sdk/trace.js';
7
+ import '../observability/metrics.js';
8
+
9
+ /**
10
+ * Policy pipeline - merges, sorts, and wraps policies as Hono middleware.
11
+ *
12
+ * The pipeline is the core execution model: global policies are merged with
13
+ * route-level policies (route wins on name collision), sorted by priority
14
+ * (ascending), and converted to Hono middleware handlers. A context injector
15
+ * runs first on every request to set the request ID, timing, and debug factory.
16
+ *
17
+ * @module pipeline
18
+ */
19
+
20
+ /**
21
+ * Merge global and route-level policies, deduplicate by name
22
+ * (route-level wins), and sort by priority ascending.
23
+ *
24
+ * @param globalPolicies - Policies from `GatewayConfig.policies`.
25
+ * @param routePolicies - Policies from `RouteConfig.pipeline.policies`.
26
+ * @param debug - Optional debug logger for tracing overrides and chain order.
27
+ * @returns Sorted array of merged policies.
28
+ */
29
+ declare function buildPolicyChain(globalPolicies: Policy[], routePolicies: Policy[], debug?: DebugLogger, defaultPriority?: number): Policy[];
30
+ /**
31
+ * Convert a sorted Policy array into an array of Hono MiddlewareHandlers.
32
+ *
33
+ * @param policies - Sorted policy array from {@link buildPolicyChain}.
34
+ * @returns Hono middleware handlers in execution order.
35
+ */
36
+ declare function policiesToMiddleware(policies: Policy[]): MiddlewareHandler[];
37
+ /**
38
+ * Create the context-injection middleware that runs at the start of every
39
+ * pipeline. Sets requestId, startTime, gateway metadata, and debug factory
40
+ * on Hono's context variables. Also adds `x-request-id` to the response.
41
+ *
42
+ * @param gatewayName - Gateway name from config.
43
+ * @param routePath - The matched route path pattern.
44
+ * @param debugFactory - Factory for creating namespaced debug loggers.
45
+ * @param requestIdHeader - Response header name for the request ID.
46
+ * @param adapter - Optional runtime adapter for runtime-specific capabilities.
47
+ * @param debugHeaders - Client-requested debug headers configuration.
48
+ * @returns Hono middleware that injects {@link PolicyContext}.
49
+ */
50
+ declare function createContextInjector(gatewayName: string, routePath: string, debugFactory?: (namespace: string) => DebugLogger, requestIdHeader?: string, adapter?: GatewayAdapter, debugHeaders?: boolean | DebugHeadersConfig, tracing?: TracingConfig): MiddlewareHandler;
51
+ /**
52
+ * Retrieve the {@link PolicyContext} from a Hono context.
53
+ *
54
+ * Returns `undefined` if called outside the gateway pipeline (e.g. in
55
+ * a standalone Hono app without context injection).
56
+ *
57
+ * @param c - The Hono request context.
58
+ * @returns The gateway context, or `undefined` if not in a gateway pipeline.
59
+ */
60
+ declare function getGatewayContext(c: Context): PolicyContext | undefined;
61
+
62
+ export { buildPolicyChain, createContextInjector, getGatewayContext, policiesToMiddleware };
@@ -0,0 +1,214 @@
1
+ import {
2
+ generateOtelSpanId,
3
+ SemConv,
4
+ SpanBuilder,
5
+ shouldSample
6
+ } from "../observability/tracing";
7
+ import {
8
+ getCollectedDebugHeaders,
9
+ parseDebugRequest
10
+ } from "../policies/sdk/helpers";
11
+ import {
12
+ TRACE_DETAILS_KEY,
13
+ TRACE_ENTRIES_KEY,
14
+ TRACE_REQUESTED_KEY
15
+ } from "../policies/sdk/trace";
16
+ import { noopDebugLogger } from "../utils/debug";
17
+ import { toSelfTimes } from "../utils/timing";
18
+ import {
19
+ formatTraceparent,
20
+ generateSpanId,
21
+ generateTraceContext,
22
+ parseTraceparent
23
+ } from "../utils/trace-context";
24
+ const noopDebugFactory = () => noopDebugLogger;
25
+ const GATEWAY_CONTEXT_KEY = "gateway";
26
+ function buildPolicyChain(globalPolicies, routePolicies, debug, defaultPriority = 100) {
27
+ const policyMap = /* @__PURE__ */ new Map();
28
+ for (const p of globalPolicies) {
29
+ policyMap.set(p.name, p);
30
+ }
31
+ for (const p of routePolicies) {
32
+ if (policyMap.has(p.name)) {
33
+ debug?.(`policy "${p.name}" overridden by route-level policy`);
34
+ }
35
+ policyMap.set(p.name, p);
36
+ }
37
+ const sorted = Array.from(policyMap.values()).sort(
38
+ (a, b) => (a.priority ?? defaultPriority) - (b.priority ?? defaultPriority)
39
+ );
40
+ if (sorted.length > 0) {
41
+ debug?.(
42
+ `chain: ${sorted.map((p) => `${p.name}:${p.priority ?? defaultPriority}`).join(" -> ")}`
43
+ );
44
+ }
45
+ return sorted;
46
+ }
47
+ function policiesToMiddleware(policies) {
48
+ return policies.map((policy) => {
49
+ const originalHandler = policy.handler;
50
+ const policyPriority = policy.priority ?? 100;
51
+ const wrappedHandler = async (c, next) => {
52
+ const start = Date.now();
53
+ if (c.get(TRACE_REQUESTED_KEY) !== true && c.get("_otelSpans") === void 0) {
54
+ try {
55
+ return await originalHandler(c, next);
56
+ } finally {
57
+ const durationMs = Date.now() - start;
58
+ const timings = c.get("_policyTimings") ?? [];
59
+ timings.push({ name: policy.name, durationMs });
60
+ c.set("_policyTimings", timings);
61
+ }
62
+ }
63
+ let calledNext = false;
64
+ let errorMsg = null;
65
+ let handlerResult;
66
+ try {
67
+ const tracedNext = async () => {
68
+ calledNext = true;
69
+ await next();
70
+ };
71
+ handlerResult = await originalHandler(c, tracedNext);
72
+ } catch (err) {
73
+ errorMsg = err instanceof Error ? err.message : String(err);
74
+ throw err;
75
+ } finally {
76
+ const durationMs = Date.now() - start;
77
+ const timings = c.get("_policyTimings") ?? [];
78
+ timings.push({ name: policy.name, durationMs });
79
+ c.set("_policyTimings", timings);
80
+ if (c.get(TRACE_REQUESTED_KEY) === true) {
81
+ const entries = c.get(TRACE_ENTRIES_KEY) ?? [];
82
+ entries.push({
83
+ name: policy.name,
84
+ priority: policyPriority,
85
+ durationMs,
86
+ calledNext,
87
+ error: errorMsg
88
+ });
89
+ c.set(TRACE_ENTRIES_KEY, entries);
90
+ }
91
+ const otelSpans = c.get("_otelSpans");
92
+ if (otelSpans !== void 0) {
93
+ const rootSpan = c.get("_otelRootSpan");
94
+ const span = new SpanBuilder(
95
+ `policy:${policy.name}`,
96
+ "INTERNAL",
97
+ rootSpan.traceId,
98
+ generateOtelSpanId(),
99
+ rootSpan.spanId,
100
+ start
101
+ );
102
+ span.setAttribute("policy.name", policy.name).setAttribute("policy.priority", policyPriority);
103
+ if (errorMsg) {
104
+ span.setStatus("ERROR", errorMsg);
105
+ }
106
+ otelSpans.push(span.end());
107
+ }
108
+ }
109
+ return handlerResult;
110
+ };
111
+ return wrappedHandler;
112
+ });
113
+ }
114
+ function createContextInjector(gatewayName, routePath, debugFactory = noopDebugFactory, requestIdHeader = "x-request-id", adapter, debugHeaders, tracing) {
115
+ const debugHeadersConfig = debugHeaders === true ? {} : debugHeaders || void 0;
116
+ const debugRequestHeader = debugHeadersConfig?.requestHeader ?? "x-stoma-debug";
117
+ const debugAllow = debugHeadersConfig?.allow;
118
+ return async (c, next) => {
119
+ const incomingTraceparent = c.req.header("traceparent") ?? null;
120
+ const parsed = parseTraceparent(incomingTraceparent);
121
+ const traceId = parsed?.traceId ?? generateTraceContext().traceId;
122
+ const spanId = generateSpanId();
123
+ const ctx = {
124
+ requestId: crypto.randomUUID(),
125
+ startTime: Date.now(),
126
+ gatewayName,
127
+ routePath,
128
+ traceId,
129
+ spanId,
130
+ debug: debugFactory,
131
+ adapter
132
+ };
133
+ c.set(GATEWAY_CONTEXT_KEY, ctx);
134
+ let otelRootSpan;
135
+ if (tracing && shouldSample(tracing.sampleRate ?? 1)) {
136
+ const otelSpanId = generateOtelSpanId();
137
+ otelRootSpan = new SpanBuilder(
138
+ `${c.req.method} ${routePath}`,
139
+ "SERVER",
140
+ traceId,
141
+ otelSpanId,
142
+ parsed?.parentId,
143
+ ctx.startTime
144
+ );
145
+ otelRootSpan.setAttribute(SemConv.HTTP_METHOD, c.req.method).setAttribute(SemConv.HTTP_ROUTE, routePath).setAttribute(SemConv.URL_PATH, new URL(c.req.url).pathname).setAttribute("gateway.name", gatewayName);
146
+ c.set("_otelRootSpan", otelRootSpan);
147
+ c.set("_otelSpans", []);
148
+ }
149
+ if (debugHeadersConfig) {
150
+ parseDebugRequest(c, debugRequestHeader, debugAllow);
151
+ }
152
+ await next();
153
+ c.res.headers.set(requestIdHeader, ctx.requestId);
154
+ c.res.headers.set(
155
+ "traceparent",
156
+ formatTraceparent({
157
+ version: "00",
158
+ traceId: ctx.traceId,
159
+ parentId: ctx.spanId,
160
+ flags: parsed?.flags ?? "01"
161
+ })
162
+ );
163
+ if (debugHeadersConfig) {
164
+ const collected = getCollectedDebugHeaders(c);
165
+ if (collected) {
166
+ for (const [name, value] of collected) {
167
+ c.res.headers.set(name, value);
168
+ }
169
+ }
170
+ }
171
+ if (c.get(TRACE_REQUESTED_KEY) === true) {
172
+ const rawEntries = c.get(TRACE_ENTRIES_KEY);
173
+ if (rawEntries && rawEntries.length > 0) {
174
+ const details = c.get(TRACE_DETAILS_KEY);
175
+ const selfEntries = toSelfTimes(rawEntries);
176
+ const entries = selfEntries.map((baseline) => {
177
+ const detail = details?.get(baseline.name);
178
+ return detail ? { ...baseline, detail } : baseline;
179
+ });
180
+ const trace = {
181
+ requestId: ctx.requestId,
182
+ traceId: ctx.traceId,
183
+ route: routePath,
184
+ totalMs: Date.now() - ctx.startTime,
185
+ entries
186
+ };
187
+ c.res.headers.set("x-stoma-trace", JSON.stringify(trace));
188
+ }
189
+ }
190
+ if (otelRootSpan) {
191
+ otelRootSpan.setAttribute(SemConv.HTTP_STATUS_CODE, c.res.status).setStatus(
192
+ c.res.status >= 500 ? "ERROR" : c.res.status >= 400 ? "UNSET" : "OK"
193
+ );
194
+ const rootReadable = otelRootSpan.end();
195
+ const childSpans = c.get("_otelSpans") ?? [];
196
+ const allSpans = [rootReadable, ...childSpans];
197
+ const exportPromise = tracing.exporter.export(allSpans).catch(() => {
198
+ });
199
+ if (adapter?.waitUntil) {
200
+ adapter.waitUntil(exportPromise);
201
+ }
202
+ }
203
+ };
204
+ }
205
+ function getGatewayContext(c) {
206
+ return c.get(GATEWAY_CONTEXT_KEY);
207
+ }
208
+ export {
209
+ buildPolicyChain,
210
+ createContextInjector,
211
+ getGatewayContext,
212
+ policiesToMiddleware
213
+ };
214
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/pipeline.ts"],"sourcesContent":["/**\n * Policy pipeline - merges, sorts, and wraps policies as Hono middleware.\n *\n * The pipeline is the core execution model: global policies are merged with\n * route-level policies (route wins on name collision), sorted by priority\n * (ascending), and converted to Hono middleware handlers. A context injector\n * runs first on every request to set the request ID, timing, and debug factory.\n *\n * @module pipeline\n */\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport type { GatewayAdapter } from \"../adapters/types\";\nimport type { ReadableSpan, TracingConfig } from \"../observability/tracing\";\nimport {\n generateOtelSpanId,\n SemConv,\n SpanBuilder,\n shouldSample,\n} from \"../observability/tracing\";\nimport {\n getCollectedDebugHeaders,\n parseDebugRequest,\n} from \"../policies/sdk/helpers\";\nimport {\n type PolicyTrace,\n type PolicyTraceBaseline,\n type PolicyTraceDetail,\n type PolicyTraceEntry,\n TRACE_DETAILS_KEY,\n TRACE_ENTRIES_KEY,\n TRACE_REQUESTED_KEY,\n} from \"../policies/sdk/trace\";\nimport type { Policy, PolicyContext } from \"../policies/types\";\nimport type { DebugLogger } from \"../utils/debug\";\nimport { noopDebugLogger } from \"../utils/debug\";\nimport { toSelfTimes } from \"../utils/timing\";\nimport {\n formatTraceparent,\n generateSpanId,\n generateTraceContext,\n parseTraceparent,\n} from \"../utils/trace-context\";\nimport type { DebugHeadersConfig } from \"./types\";\n\nconst noopDebugFactory = () => noopDebugLogger;\n\nconst GATEWAY_CONTEXT_KEY = \"gateway\";\n\n/**\n * Merge global and route-level policies, deduplicate by name\n * (route-level wins), and sort by priority ascending.\n *\n * @param globalPolicies - Policies from `GatewayConfig.policies`.\n * @param routePolicies - Policies from `RouteConfig.pipeline.policies`.\n * @param debug - Optional debug logger for tracing overrides and chain order.\n * @returns Sorted array of merged policies.\n */\nexport function buildPolicyChain(\n globalPolicies: Policy[],\n routePolicies: Policy[],\n debug?: DebugLogger,\n defaultPriority = 100\n): Policy[] {\n const policyMap = new Map<string, Policy>();\n\n // Global policies first\n for (const p of globalPolicies) {\n policyMap.set(p.name, p);\n }\n\n // Route-level policies override global by name\n for (const p of routePolicies) {\n if (policyMap.has(p.name)) {\n debug?.(`policy \"${p.name}\" overridden by route-level policy`);\n }\n policyMap.set(p.name, p);\n }\n\n // Sort by priority ascending (lower = earlier)\n const sorted = Array.from(policyMap.values()).sort(\n (a, b) => (a.priority ?? defaultPriority) - (b.priority ?? defaultPriority)\n );\n\n if (sorted.length > 0) {\n debug?.(\n `chain: ${sorted.map((p) => `${p.name}:${p.priority ?? defaultPriority}`).join(\" -> \")}`\n );\n }\n\n return sorted;\n}\n\n/**\n * Convert a sorted Policy array into an array of Hono MiddlewareHandlers.\n *\n * @param policies - Sorted policy array from {@link buildPolicyChain}.\n * @returns Hono middleware handlers in execution order.\n */\nexport function policiesToMiddleware(policies: Policy[]): MiddlewareHandler[] {\n return policies.map((policy) => {\n const originalHandler = policy.handler;\n const policyPriority = policy.priority ?? 100;\n // Wrap with skip logic and per-policy timing.\n // Uses try-finally so timing is recorded even when a policy throws\n // (e.g. timeout throwing GatewayError on deadline exceeded).\n const wrappedHandler: MiddlewareHandler = async (c, next) => {\n const start = Date.now();\n\n // Fast path: when neither policy trace nor OTel tracing is active,\n // avoid allocating trace-only variables and the tracedNext closure.\n if (\n c.get(TRACE_REQUESTED_KEY) !== true &&\n c.get(\"_otelSpans\") === undefined\n ) {\n try {\n // Return the handler result so Hono's compose() can finalize the\n // context when a policy short-circuits by returning a Response\n // (e.g. Hono's CORS middleware returns a 204 for OPTIONS preflight).\n return await originalHandler(c, next);\n } finally {\n const durationMs = Date.now() - start;\n const timings = (c.get(\"_policyTimings\") ?? []) as Array<{\n name: string;\n durationMs: number;\n }>;\n timings.push({ name: policy.name, durationMs });\n c.set(\"_policyTimings\", timings);\n }\n }\n\n // Slow path: tracing active (policy trace, OTel, or both) -\n // track calledNext, errors, and push entries.\n let calledNext = false;\n let errorMsg: string | null = null;\n let handlerResult: Response | void;\n\n try {\n const tracedNext = async () => {\n calledNext = true;\n await next();\n };\n handlerResult = await originalHandler(c, tracedNext);\n } catch (err) {\n errorMsg = err instanceof Error ? err.message : String(err);\n throw err;\n } finally {\n const durationMs = Date.now() - start;\n\n // Accumulate per-policy timing data for observability policies\n const timings = (c.get(\"_policyTimings\") ?? []) as Array<{\n name: string;\n durationMs: number;\n }>;\n timings.push({ name: policy.name, durationMs });\n c.set(\"_policyTimings\", timings);\n\n // Accumulate trace baseline entries (policy trace system)\n if (c.get(TRACE_REQUESTED_KEY) === true) {\n const entries = (c.get(TRACE_ENTRIES_KEY) ??\n []) as PolicyTraceBaseline[];\n entries.push({\n name: policy.name,\n priority: policyPriority,\n durationMs,\n calledNext,\n error: errorMsg,\n });\n c.set(TRACE_ENTRIES_KEY, entries);\n }\n\n // OTel: create INTERNAL child span per policy\n const otelSpans = c.get(\"_otelSpans\") as ReadableSpan[] | undefined;\n if (otelSpans !== undefined) {\n const rootSpan = c.get(\"_otelRootSpan\") as SpanBuilder;\n const span = new SpanBuilder(\n `policy:${policy.name}`,\n \"INTERNAL\",\n rootSpan.traceId,\n generateOtelSpanId(),\n rootSpan.spanId,\n start\n );\n span\n .setAttribute(\"policy.name\", policy.name)\n .setAttribute(\"policy.priority\", policyPriority);\n if (errorMsg) {\n span.setStatus(\"ERROR\", errorMsg);\n }\n otelSpans.push(span.end());\n }\n }\n\n return handlerResult;\n };\n return wrappedHandler;\n });\n}\n\n/**\n * Create the context-injection middleware that runs at the start of every\n * pipeline. Sets requestId, startTime, gateway metadata, and debug factory\n * on Hono's context variables. Also adds `x-request-id` to the response.\n *\n * @param gatewayName - Gateway name from config.\n * @param routePath - The matched route path pattern.\n * @param debugFactory - Factory for creating namespaced debug loggers.\n * @param requestIdHeader - Response header name for the request ID.\n * @param adapter - Optional runtime adapter for runtime-specific capabilities.\n * @param debugHeaders - Client-requested debug headers configuration.\n * @returns Hono middleware that injects {@link PolicyContext}.\n */\nexport function createContextInjector(\n gatewayName: string,\n routePath: string,\n debugFactory: (namespace: string) => DebugLogger = noopDebugFactory,\n requestIdHeader = \"x-request-id\",\n adapter?: GatewayAdapter,\n debugHeaders?: boolean | DebugHeadersConfig,\n tracing?: TracingConfig\n): MiddlewareHandler {\n // Pre-compute debug header config once at construction time\n const debugHeadersConfig =\n debugHeaders === true\n ? ({} as DebugHeadersConfig)\n : debugHeaders || undefined;\n const debugRequestHeader =\n debugHeadersConfig?.requestHeader ?? \"x-stoma-debug\";\n const debugAllow = debugHeadersConfig?.allow;\n\n return async (c, next) => {\n // Parse incoming traceparent or generate a new trace context\n const incomingTraceparent = c.req.header(\"traceparent\") ?? null;\n const parsed = parseTraceparent(incomingTraceparent);\n const traceId = parsed?.traceId ?? generateTraceContext().traceId;\n const spanId = generateSpanId();\n\n const ctx: PolicyContext = {\n requestId: crypto.randomUUID(),\n startTime: Date.now(),\n gatewayName,\n routePath,\n traceId,\n spanId,\n debug: debugFactory,\n adapter,\n };\n c.set(GATEWAY_CONTEXT_KEY, ctx);\n\n // OTel tracing: create root SERVER span if sampled\n let otelRootSpan: SpanBuilder | undefined;\n if (tracing && shouldSample(tracing.sampleRate ?? 1.0)) {\n const otelSpanId = generateOtelSpanId();\n otelRootSpan = new SpanBuilder(\n `${c.req.method} ${routePath}`,\n \"SERVER\",\n traceId,\n otelSpanId,\n parsed?.parentId,\n ctx.startTime\n );\n otelRootSpan\n .setAttribute(SemConv.HTTP_METHOD, c.req.method)\n .setAttribute(SemConv.HTTP_ROUTE, routePath)\n .setAttribute(SemConv.URL_PATH, new URL(c.req.url).pathname)\n .setAttribute(\"gateway.name\", gatewayName);\n\n c.set(\"_otelRootSpan\", otelRootSpan);\n c.set(\"_otelSpans\", [] as ReadableSpan[]);\n }\n\n // Parse client debug header request before policies run\n if (debugHeadersConfig) {\n parseDebugRequest(c, debugRequestHeader, debugAllow);\n }\n\n await next();\n\n // Set request ID header on the final response (after downstream runs)\n // so it's present even when handlers return raw Response objects\n c.res.headers.set(requestIdHeader, ctx.requestId);\n // Propagate W3C traceparent on the response\n c.res.headers.set(\n \"traceparent\",\n formatTraceparent({\n version: \"00\",\n traceId: ctx.traceId,\n parentId: ctx.spanId,\n flags: parsed?.flags ?? \"01\",\n })\n );\n\n // Emit collected debug headers on the response\n if (debugHeadersConfig) {\n const collected = getCollectedDebugHeaders(c);\n if (collected) {\n for (const [name, value] of collected) {\n c.res.headers.set(name, value);\n }\n }\n }\n\n // Emit policy trace when tracing was requested\n if (c.get(TRACE_REQUESTED_KEY) === true) {\n const rawEntries = c.get(TRACE_ENTRIES_KEY) as\n | PolicyTraceBaseline[]\n | undefined;\n if (rawEntries && rawEntries.length > 0) {\n const details = c.get(TRACE_DETAILS_KEY) as\n | Map<string, PolicyTraceDetail>\n | undefined;\n\n // Convert inclusive timings to self-time (execution order)\n const selfEntries = toSelfTimes(rawEntries);\n\n // Merge baseline + detail\n const entries: PolicyTraceEntry[] = selfEntries.map((baseline) => {\n const detail = details?.get(baseline.name);\n return detail ? { ...baseline, detail } : baseline;\n });\n\n const trace: PolicyTrace = {\n requestId: ctx.requestId,\n traceId: ctx.traceId,\n route: routePath,\n totalMs: Date.now() - ctx.startTime,\n entries,\n };\n\n c.res.headers.set(\"x-stoma-trace\", JSON.stringify(trace));\n }\n }\n\n // OTel tracing: finalize root span and export\n if (otelRootSpan) {\n otelRootSpan\n .setAttribute(SemConv.HTTP_STATUS_CODE, c.res.status)\n .setStatus(\n c.res.status >= 500 ? \"ERROR\" : c.res.status >= 400 ? \"UNSET\" : \"OK\"\n );\n\n const rootReadable = otelRootSpan.end();\n const childSpans = (c.get(\"_otelSpans\") ?? []) as ReadableSpan[];\n const allSpans = [rootReadable, ...childSpans];\n\n // Export asynchronously - don't block the response\n const exportPromise = tracing!.exporter.export(allSpans).catch(() => {\n // Swallow export errors - tracing must never break the request\n });\n\n if (adapter?.waitUntil) {\n adapter.waitUntil(exportPromise);\n }\n }\n };\n}\n\n/**\n * Retrieve the {@link PolicyContext} from a Hono context.\n *\n * Returns `undefined` if called outside the gateway pipeline (e.g. in\n * a standalone Hono app without context injection).\n *\n * @param c - The Hono request context.\n * @returns The gateway context, or `undefined` if not in a gateway pipeline.\n */\nexport function getGatewayContext(c: Context): PolicyContext | undefined {\n return c.get(GATEWAY_CONTEXT_KEY) as PolicyContext | undefined;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,4 @@
1
+ export { A as AttributeMutation, B as BodyMutation, H as HeaderMutation, M as Mutation, i as PolicyContinue, j as PolicyEvalContext, k as PolicyEvaluator, l as PolicyImmediateResponse, m as PolicyInput, n as PolicyReject, o as PolicyResult, p as ProcessingPhase, q as ProtocolType, S as StatusMutation } from '../protocol-2fD3DJrL.js';
2
+ import '../policies/sdk/trace.js';
3
+ import '@vivero/stoma-core';
4
+ import 'hono';
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,67 @@
1
+ import { P as Policy } from '../protocol-2fD3DJrL.js';
2
+ import { RouteConfig } from './types.js';
3
+ import 'hono';
4
+ import '../policies/sdk/trace.js';
5
+ import '@vivero/stoma-core';
6
+ import '../observability/metrics.js';
7
+ import '../observability/tracing.js';
8
+
9
+ /**
10
+ * Route scoping - group routes under a shared path prefix with shared policies.
11
+ *
12
+ * `scope()` transforms an array of {@link RouteConfig} by prepending a path
13
+ * prefix, prepending shared policies, and merging metadata. The output is a
14
+ * flat `RouteConfig[]` that can be spread directly into `GatewayConfig.routes`.
15
+ *
16
+ * Nesting works naturally - pass the output of an inner `scope()` as the
17
+ * `routes` of an outer `scope()`.
18
+ *
19
+ * @module scope
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { createGateway, scope, jwtAuth, rateLimit } from "@vivero/stoma";
24
+ *
25
+ * const gateway = createGateway({
26
+ * routes: [
27
+ * ...scope({
28
+ * prefix: "/api/v1",
29
+ * policies: [jwtAuth({ secret: "..." })],
30
+ * routes: [
31
+ * { path: "/users", pipeline: { upstream: { type: "url", target: "https://users.internal" } } },
32
+ * { path: "/orders", pipeline: { upstream: { type: "url", target: "https://orders.internal" } } },
33
+ * ],
34
+ * }),
35
+ * ],
36
+ * });
37
+ * ```
38
+ */
39
+
40
+ /**
41
+ * Configuration for a route scope (group).
42
+ *
43
+ * @typeParam TBindings - Worker bindings type, propagated to child routes.
44
+ */
45
+ interface ScopeConfig<TBindings = Record<string, unknown>> {
46
+ /** Path prefix prepended to all child routes (e.g. "/api/v1") */
47
+ prefix: string;
48
+ /** Policies prepended to every child route's pipeline policies */
49
+ policies?: Policy[];
50
+ /** Child routes to scope */
51
+ routes: RouteConfig<TBindings>[];
52
+ /** Metadata merged into every child route (child wins on conflict) */
53
+ metadata?: Record<string, unknown>;
54
+ }
55
+ /**
56
+ * Group routes under a shared path prefix with shared policies and metadata.
57
+ *
58
+ * Returns a flat array of transformed {@link RouteConfig} objects ready to be
59
+ * spread into `GatewayConfig.routes`.
60
+ *
61
+ * @typeParam TBindings - Worker bindings type, propagated to child routes.
62
+ * @param config - Scope configuration.
63
+ * @returns Array of route configs with prefix, policies, and metadata applied.
64
+ */
65
+ declare function scope<TBindings = Record<string, unknown>>(config: ScopeConfig<TBindings>): RouteConfig<TBindings>[];
66
+
67
+ export { type ScopeConfig, scope };
@@ -0,0 +1,44 @@
1
+ function normalizePrefix(prefix) {
2
+ let normalized = prefix;
3
+ if (!normalized.startsWith("/")) {
4
+ normalized = `/${normalized}`;
5
+ }
6
+ if (normalized.length > 1 && normalized.endsWith("/")) {
7
+ normalized = normalized.slice(0, -1);
8
+ }
9
+ return normalized;
10
+ }
11
+ function joinPaths(prefix, path) {
12
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
13
+ if (normalizedPath === "/") {
14
+ return prefix;
15
+ }
16
+ if (prefix.endsWith("/") && normalizedPath.startsWith("/")) {
17
+ return `${prefix}${normalizedPath.slice(1)}`;
18
+ }
19
+ return `${prefix}${normalizedPath}`;
20
+ }
21
+ function scope(config) {
22
+ const prefix = normalizePrefix(config.prefix);
23
+ const scopePolicies = config.policies ?? [];
24
+ const scopeMetadata = config.metadata ?? {};
25
+ return config.routes.map((route) => {
26
+ const fullPath = joinPaths(prefix, route.path);
27
+ const routePolicies = route.pipeline.policies ?? [];
28
+ const mergedPolicies = scopePolicies.length > 0 || routePolicies.length > 0 ? [...scopePolicies, ...routePolicies] : void 0;
29
+ const mergedMetadata = Object.keys(scopeMetadata).length > 0 || route.metadata ? { ...scopeMetadata, ...route.metadata } : void 0;
30
+ return {
31
+ ...route,
32
+ path: fullPath,
33
+ pipeline: {
34
+ ...route.pipeline,
35
+ ...mergedPolicies !== void 0 ? { policies: mergedPolicies } : {}
36
+ },
37
+ ...mergedMetadata !== void 0 ? { metadata: mergedMetadata } : {}
38
+ };
39
+ });
40
+ }
41
+ export {
42
+ scope
43
+ };
44
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/scope.ts"],"sourcesContent":["/**\n * Route scoping - group routes under a shared path prefix with shared policies.\n *\n * `scope()` transforms an array of {@link RouteConfig} by prepending a path\n * prefix, prepending shared policies, and merging metadata. The output is a\n * flat `RouteConfig[]` that can be spread directly into `GatewayConfig.routes`.\n *\n * Nesting works naturally - pass the output of an inner `scope()` as the\n * `routes` of an outer `scope()`.\n *\n * @module scope\n *\n * @example\n * ```ts\n * import { createGateway, scope, jwtAuth, rateLimit } from \"@vivero/stoma\";\n *\n * const gateway = createGateway({\n * routes: [\n * ...scope({\n * prefix: \"/api/v1\",\n * policies: [jwtAuth({ secret: \"...\" })],\n * routes: [\n * { path: \"/users\", pipeline: { upstream: { type: \"url\", target: \"https://users.internal\" } } },\n * { path: \"/orders\", pipeline: { upstream: { type: \"url\", target: \"https://orders.internal\" } } },\n * ],\n * }),\n * ],\n * });\n * ```\n */\nimport type { Policy } from \"../policies/types\";\nimport type { RouteConfig } from \"./types\";\n\n/**\n * Configuration for a route scope (group).\n *\n * @typeParam TBindings - Worker bindings type, propagated to child routes.\n */\nexport interface ScopeConfig<TBindings = Record<string, unknown>> {\n /** Path prefix prepended to all child routes (e.g. \"/api/v1\") */\n prefix: string;\n /** Policies prepended to every child route's pipeline policies */\n policies?: Policy[];\n /** Child routes to scope */\n routes: RouteConfig<TBindings>[];\n /** Metadata merged into every child route (child wins on conflict) */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Normalize a path prefix: ensure leading `/`, strip trailing `/`.\n *\n * @internal\n */\nfunction normalizePrefix(prefix: string): string {\n let normalized = prefix;\n\n // Ensure leading slash\n if (!normalized.startsWith(\"/\")) {\n normalized = `/${normalized}`;\n }\n\n // Strip trailing slash (unless the prefix is just \"/\")\n if (normalized.length > 1 && normalized.endsWith(\"/\")) {\n normalized = normalized.slice(0, -1);\n }\n\n return normalized;\n}\n\n/**\n * Join a normalized prefix and a route path, avoiding double slashes.\n *\n * @internal\n */\nfunction joinPaths(prefix: string, path: string): string {\n // Ensure path has leading slash\n const normalizedPath = path.startsWith(\"/\") ? path : `/${path}`;\n\n // Root path \"/\" means \"the scope prefix itself\" — return prefix without\n // a trailing slash so that `/environment` + `/` → `/environment`, not\n // `/environment/` (which Hono treats as a different route).\n if (normalizedPath === \"/\") {\n return prefix;\n }\n\n // Avoid double slashes at the join point\n if (prefix.endsWith(\"/\") && normalizedPath.startsWith(\"/\")) {\n return `${prefix}${normalizedPath.slice(1)}`;\n }\n\n return `${prefix}${normalizedPath}`;\n}\n\n/**\n * Group routes under a shared path prefix with shared policies and metadata.\n *\n * Returns a flat array of transformed {@link RouteConfig} objects ready to be\n * spread into `GatewayConfig.routes`.\n *\n * @typeParam TBindings - Worker bindings type, propagated to child routes.\n * @param config - Scope configuration.\n * @returns Array of route configs with prefix, policies, and metadata applied.\n */\nexport function scope<TBindings = Record<string, unknown>>(\n config: ScopeConfig<TBindings>\n): RouteConfig<TBindings>[] {\n const prefix = normalizePrefix(config.prefix);\n const scopePolicies = config.policies ?? [];\n const scopeMetadata = config.metadata ?? {};\n\n return config.routes.map((route) => {\n const fullPath = joinPaths(prefix, route.path);\n\n // Prepend scope policies before route policies (scope runs first)\n const routePolicies = route.pipeline.policies ?? [];\n const mergedPolicies =\n scopePolicies.length > 0 || routePolicies.length > 0\n ? [...scopePolicies, ...routePolicies]\n : undefined;\n\n // Merge metadata: scope metadata as base, child wins on conflict\n const mergedMetadata =\n Object.keys(scopeMetadata).length > 0 || route.metadata\n ? { ...scopeMetadata, ...route.metadata }\n : undefined;\n\n return {\n ...route,\n path: fullPath,\n pipeline: {\n ...route.pipeline,\n ...(mergedPolicies !== undefined ? { policies: mergedPolicies } : {}),\n },\n ...(mergedMetadata !== undefined ? { metadata: mergedMetadata } : {}),\n };\n });\n}\n"],"mappings":"AAsDA,SAAS,gBAAgB,QAAwB;AAC/C,MAAI,aAAa;AAGjB,MAAI,CAAC,WAAW,WAAW,GAAG,GAAG;AAC/B,iBAAa,IAAI,UAAU;AAAA,EAC7B;AAGA,MAAI,WAAW,SAAS,KAAK,WAAW,SAAS,GAAG,GAAG;AACrD,iBAAa,WAAW,MAAM,GAAG,EAAE;AAAA,EACrC;AAEA,SAAO;AACT;AAOA,SAAS,UAAU,QAAgB,MAAsB;AAEvD,QAAM,iBAAiB,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI;AAK7D,MAAI,mBAAmB,KAAK;AAC1B,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,SAAS,GAAG,KAAK,eAAe,WAAW,GAAG,GAAG;AAC1D,WAAO,GAAG,MAAM,GAAG,eAAe,MAAM,CAAC,CAAC;AAAA,EAC5C;AAEA,SAAO,GAAG,MAAM,GAAG,cAAc;AACnC;AAYO,SAAS,MACd,QAC0B;AAC1B,QAAM,SAAS,gBAAgB,OAAO,MAAM;AAC5C,QAAM,gBAAgB,OAAO,YAAY,CAAC;AAC1C,QAAM,gBAAgB,OAAO,YAAY,CAAC;AAE1C,SAAO,OAAO,OAAO,IAAI,CAAC,UAAU;AAClC,UAAM,WAAW,UAAU,QAAQ,MAAM,IAAI;AAG7C,UAAM,gBAAgB,MAAM,SAAS,YAAY,CAAC;AAClD,UAAM,iBACJ,cAAc,SAAS,KAAK,cAAc,SAAS,IAC/C,CAAC,GAAG,eAAe,GAAG,aAAa,IACnC;AAGN,UAAM,iBACJ,OAAO,KAAK,aAAa,EAAE,SAAS,KAAK,MAAM,WAC3C,EAAE,GAAG,eAAe,GAAG,MAAM,SAAS,IACtC;AAEN,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,MACN,UAAU;AAAA,QACR,GAAG,MAAM;AAAA,QACT,GAAI,mBAAmB,SAAY,EAAE,UAAU,eAAe,IAAI,CAAC;AAAA,MACrE;AAAA,MACA,GAAI,mBAAmB,SAAY,EAAE,UAAU,eAAe,IAAI,CAAC;AAAA,IACrE;AAAA,EACF,CAAC;AACH;","names":[]}