@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.
Files changed (254) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/LICENSE +21 -0
  3. package/README.md +325 -0
  4. package/dist/adapters/bun.d.ts +9 -0
  5. package/dist/adapters/bun.js +8 -0
  6. package/dist/adapters/bun.js.map +1 -0
  7. package/dist/adapters/cloudflare.d.ts +49 -0
  8. package/dist/adapters/cloudflare.js +85 -0
  9. package/dist/adapters/cloudflare.js.map +1 -0
  10. package/dist/adapters/deno.d.ts +9 -0
  11. package/dist/adapters/deno.js +8 -0
  12. package/dist/adapters/deno.js.map +1 -0
  13. package/dist/adapters/durable-object.d.ts +63 -0
  14. package/dist/adapters/durable-object.js +46 -0
  15. package/dist/adapters/durable-object.js.map +1 -0
  16. package/dist/adapters/index.d.ts +13 -0
  17. package/dist/adapters/index.js +53 -0
  18. package/dist/adapters/index.js.map +1 -0
  19. package/dist/adapters/memory.d.ts +9 -0
  20. package/dist/adapters/memory.js +14 -0
  21. package/dist/adapters/memory.js.map +1 -0
  22. package/dist/adapters/node.d.ts +9 -0
  23. package/dist/adapters/node.js +8 -0
  24. package/dist/adapters/node.js.map +1 -0
  25. package/dist/adapters/postgres.d.ts +109 -0
  26. package/dist/adapters/postgres.js +242 -0
  27. package/dist/adapters/postgres.js.map +1 -0
  28. package/dist/adapters/redis.d.ts +116 -0
  29. package/dist/adapters/redis.js +194 -0
  30. package/dist/adapters/redis.js.map +1 -0
  31. package/dist/adapters/testing.d.ts +32 -0
  32. package/dist/adapters/testing.js +33 -0
  33. package/dist/adapters/testing.js.map +1 -0
  34. package/dist/adapters/types.d.ts +4 -0
  35. package/dist/adapters/types.js +1 -0
  36. package/dist/adapters/types.js.map +1 -0
  37. package/dist/config/index.d.ts +11 -0
  38. package/dist/config/index.js +21 -0
  39. package/dist/config/index.js.map +1 -0
  40. package/dist/config/merge.d.ts +48 -0
  41. package/dist/config/merge.js +83 -0
  42. package/dist/config/merge.js.map +1 -0
  43. package/dist/config/schema.d.ts +254 -0
  44. package/dist/config/schema.js +109 -0
  45. package/dist/config/schema.js.map +1 -0
  46. package/dist/core/errors.d.ts +66 -0
  47. package/dist/core/errors.js +47 -0
  48. package/dist/core/errors.js.map +1 -0
  49. package/dist/core/gateway.d.ts +44 -0
  50. package/dist/core/gateway.js +400 -0
  51. package/dist/core/gateway.js.map +1 -0
  52. package/dist/core/health.d.ts +78 -0
  53. package/dist/core/health.js +65 -0
  54. package/dist/core/health.js.map +1 -0
  55. package/dist/core/pipeline.d.ts +62 -0
  56. package/dist/core/pipeline.js +214 -0
  57. package/dist/core/pipeline.js.map +1 -0
  58. package/dist/core/protocol.d.ts +4 -0
  59. package/dist/core/protocol.js +1 -0
  60. package/dist/core/protocol.js.map +1 -0
  61. package/dist/core/scope.d.ts +67 -0
  62. package/dist/core/scope.js +44 -0
  63. package/dist/core/scope.js.map +1 -0
  64. package/dist/core/types.d.ts +252 -0
  65. package/dist/core/types.js +1 -0
  66. package/dist/core/types.js.map +1 -0
  67. package/dist/index.d.ts +57 -0
  68. package/dist/index.js +158 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/observability/admin.d.ts +32 -0
  71. package/dist/observability/admin.js +85 -0
  72. package/dist/observability/admin.js.map +1 -0
  73. package/dist/observability/metrics.d.ts +78 -0
  74. package/dist/observability/metrics.js +107 -0
  75. package/dist/observability/metrics.js.map +1 -0
  76. package/dist/observability/tracing.d.ts +149 -0
  77. package/dist/observability/tracing.js +191 -0
  78. package/dist/observability/tracing.js.map +1 -0
  79. package/dist/policies/auth/api-key-auth.d.ts +64 -0
  80. package/dist/policies/auth/api-key-auth.js +93 -0
  81. package/dist/policies/auth/api-key-auth.js.map +1 -0
  82. package/dist/policies/auth/basic-auth.d.ts +33 -0
  83. package/dist/policies/auth/basic-auth.js +96 -0
  84. package/dist/policies/auth/basic-auth.js.map +1 -0
  85. package/dist/policies/auth/crypto.d.ts +29 -0
  86. package/dist/policies/auth/crypto.js +100 -0
  87. package/dist/policies/auth/crypto.js.map +1 -0
  88. package/dist/policies/auth/generate-http-signature.d.ts +30 -0
  89. package/dist/policies/auth/generate-http-signature.js +79 -0
  90. package/dist/policies/auth/generate-http-signature.js.map +1 -0
  91. package/dist/policies/auth/generate-jwt.d.ts +44 -0
  92. package/dist/policies/auth/generate-jwt.js +99 -0
  93. package/dist/policies/auth/generate-jwt.js.map +1 -0
  94. package/dist/policies/auth/http-signature-base.d.ts +55 -0
  95. package/dist/policies/auth/http-signature-base.js +140 -0
  96. package/dist/policies/auth/http-signature-base.js.map +1 -0
  97. package/dist/policies/auth/jws.d.ts +46 -0
  98. package/dist/policies/auth/jws.js +317 -0
  99. package/dist/policies/auth/jws.js.map +1 -0
  100. package/dist/policies/auth/jwt-auth.d.ts +64 -0
  101. package/dist/policies/auth/jwt-auth.js +266 -0
  102. package/dist/policies/auth/jwt-auth.js.map +1 -0
  103. package/dist/policies/auth/oauth2.d.ts +38 -0
  104. package/dist/policies/auth/oauth2.js +254 -0
  105. package/dist/policies/auth/oauth2.js.map +1 -0
  106. package/dist/policies/auth/rbac.d.ts +30 -0
  107. package/dist/policies/auth/rbac.js +115 -0
  108. package/dist/policies/auth/rbac.js.map +1 -0
  109. package/dist/policies/auth/verify-http-signature.d.ts +30 -0
  110. package/dist/policies/auth/verify-http-signature.js +147 -0
  111. package/dist/policies/auth/verify-http-signature.js.map +1 -0
  112. package/dist/policies/index.d.ts +51 -0
  113. package/dist/policies/index.js +109 -0
  114. package/dist/policies/index.js.map +1 -0
  115. package/dist/policies/mock.d.ts +60 -0
  116. package/dist/policies/mock.js +29 -0
  117. package/dist/policies/mock.js.map +1 -0
  118. package/dist/policies/observability/assign-metrics.d.ts +37 -0
  119. package/dist/policies/observability/assign-metrics.js +29 -0
  120. package/dist/policies/observability/assign-metrics.js.map +1 -0
  121. package/dist/policies/observability/metrics-reporter.d.ts +25 -0
  122. package/dist/policies/observability/metrics-reporter.js +62 -0
  123. package/dist/policies/observability/metrics-reporter.js.map +1 -0
  124. package/dist/policies/observability/request-log.d.ts +135 -0
  125. package/dist/policies/observability/request-log.js +134 -0
  126. package/dist/policies/observability/request-log.js.map +1 -0
  127. package/dist/policies/observability/server-timing.d.ts +35 -0
  128. package/dist/policies/observability/server-timing.js +89 -0
  129. package/dist/policies/observability/server-timing.js.map +1 -0
  130. package/dist/policies/proxy.d.ts +59 -0
  131. package/dist/policies/proxy.js +47 -0
  132. package/dist/policies/proxy.js.map +1 -0
  133. package/dist/policies/resilience/circuit-breaker.d.ts +4 -0
  134. package/dist/policies/resilience/circuit-breaker.js +280 -0
  135. package/dist/policies/resilience/circuit-breaker.js.map +1 -0
  136. package/dist/policies/resilience/latency-injection.d.ts +35 -0
  137. package/dist/policies/resilience/latency-injection.js +26 -0
  138. package/dist/policies/resilience/latency-injection.js.map +1 -0
  139. package/dist/policies/resilience/retry.d.ts +71 -0
  140. package/dist/policies/resilience/retry.js +79 -0
  141. package/dist/policies/resilience/retry.js.map +1 -0
  142. package/dist/policies/resilience/timeout.d.ts +32 -0
  143. package/dist/policies/resilience/timeout.js +46 -0
  144. package/dist/policies/resilience/timeout.js.map +1 -0
  145. package/dist/policies/sdk/define-policy.d.ts +176 -0
  146. package/dist/policies/sdk/define-policy.js +42 -0
  147. package/dist/policies/sdk/define-policy.js.map +1 -0
  148. package/dist/policies/sdk/helpers.d.ts +132 -0
  149. package/dist/policies/sdk/helpers.js +87 -0
  150. package/dist/policies/sdk/helpers.js.map +1 -0
  151. package/dist/policies/sdk/index.d.ts +10 -0
  152. package/dist/policies/sdk/index.js +35 -0
  153. package/dist/policies/sdk/index.js.map +1 -0
  154. package/dist/policies/sdk/priority.d.ts +44 -0
  155. package/dist/policies/sdk/priority.js +36 -0
  156. package/dist/policies/sdk/priority.js.map +1 -0
  157. package/dist/policies/sdk/testing.d.ts +53 -0
  158. package/dist/policies/sdk/testing.js +41 -0
  159. package/dist/policies/sdk/testing.js.map +1 -0
  160. package/dist/policies/sdk/trace.d.ts +73 -0
  161. package/dist/policies/sdk/trace.js +25 -0
  162. package/dist/policies/sdk/trace.js.map +1 -0
  163. package/dist/policies/traffic/cache.d.ts +4 -0
  164. package/dist/policies/traffic/cache.js +224 -0
  165. package/dist/policies/traffic/cache.js.map +1 -0
  166. package/dist/policies/traffic/dynamic-routing.d.ts +54 -0
  167. package/dist/policies/traffic/dynamic-routing.js +36 -0
  168. package/dist/policies/traffic/dynamic-routing.js.map +1 -0
  169. package/dist/policies/traffic/geo-ip-filter.d.ts +37 -0
  170. package/dist/policies/traffic/geo-ip-filter.js +74 -0
  171. package/dist/policies/traffic/geo-ip-filter.js.map +1 -0
  172. package/dist/policies/traffic/http-callout.d.ts +59 -0
  173. package/dist/policies/traffic/http-callout.js +69 -0
  174. package/dist/policies/traffic/http-callout.js.map +1 -0
  175. package/dist/policies/traffic/interrupt.d.ts +46 -0
  176. package/dist/policies/traffic/interrupt.js +38 -0
  177. package/dist/policies/traffic/interrupt.js.map +1 -0
  178. package/dist/policies/traffic/ip-filter.d.ts +47 -0
  179. package/dist/policies/traffic/ip-filter.js +57 -0
  180. package/dist/policies/traffic/ip-filter.js.map +1 -0
  181. package/dist/policies/traffic/json-threat-protection.d.ts +51 -0
  182. package/dist/policies/traffic/json-threat-protection.js +173 -0
  183. package/dist/policies/traffic/json-threat-protection.js.map +1 -0
  184. package/dist/policies/traffic/rate-limit.d.ts +4 -0
  185. package/dist/policies/traffic/rate-limit.js +145 -0
  186. package/dist/policies/traffic/rate-limit.js.map +1 -0
  187. package/dist/policies/traffic/regex-threat-protection.d.ts +54 -0
  188. package/dist/policies/traffic/regex-threat-protection.js +109 -0
  189. package/dist/policies/traffic/regex-threat-protection.js.map +1 -0
  190. package/dist/policies/traffic/request-limit.d.ts +27 -0
  191. package/dist/policies/traffic/request-limit.js +41 -0
  192. package/dist/policies/traffic/request-limit.js.map +1 -0
  193. package/dist/policies/traffic/resource-filter.d.ts +38 -0
  194. package/dist/policies/traffic/resource-filter.js +184 -0
  195. package/dist/policies/traffic/resource-filter.js.map +1 -0
  196. package/dist/policies/traffic/ssl-enforce.d.ts +27 -0
  197. package/dist/policies/traffic/ssl-enforce.js +38 -0
  198. package/dist/policies/traffic/ssl-enforce.js.map +1 -0
  199. package/dist/policies/traffic/traffic-shadow.d.ts +40 -0
  200. package/dist/policies/traffic/traffic-shadow.js +87 -0
  201. package/dist/policies/traffic/traffic-shadow.js.map +1 -0
  202. package/dist/policies/transform/assign-attributes.d.ts +33 -0
  203. package/dist/policies/transform/assign-attributes.js +38 -0
  204. package/dist/policies/transform/assign-attributes.js.map +1 -0
  205. package/dist/policies/transform/assign-content.d.ts +40 -0
  206. package/dist/policies/transform/assign-content.js +185 -0
  207. package/dist/policies/transform/assign-content.js.map +1 -0
  208. package/dist/policies/transform/cors.d.ts +57 -0
  209. package/dist/policies/transform/cors.js +23 -0
  210. package/dist/policies/transform/cors.js.map +1 -0
  211. package/dist/policies/transform/json-validation.d.ts +50 -0
  212. package/dist/policies/transform/json-validation.js +125 -0
  213. package/dist/policies/transform/json-validation.js.map +1 -0
  214. package/dist/policies/transform/override-method.d.ts +33 -0
  215. package/dist/policies/transform/override-method.js +48 -0
  216. package/dist/policies/transform/override-method.js.map +1 -0
  217. package/dist/policies/transform/request-validation.d.ts +59 -0
  218. package/dist/policies/transform/request-validation.js +121 -0
  219. package/dist/policies/transform/request-validation.js.map +1 -0
  220. package/dist/policies/transform/transform.d.ts +75 -0
  221. package/dist/policies/transform/transform.js +116 -0
  222. package/dist/policies/transform/transform.js.map +1 -0
  223. package/dist/policies/types.d.ts +4 -0
  224. package/dist/policies/types.js +1 -0
  225. package/dist/policies/types.js.map +1 -0
  226. package/dist/protocol-2fD3DJrL.d.ts +725 -0
  227. package/dist/utils/cidr.d.ts +58 -0
  228. package/dist/utils/cidr.js +107 -0
  229. package/dist/utils/cidr.js.map +1 -0
  230. package/dist/utils/debug.d.ts +1 -0
  231. package/dist/utils/debug.js +13 -0
  232. package/dist/utils/debug.js.map +1 -0
  233. package/dist/utils/headers.d.ts +68 -0
  234. package/dist/utils/headers.js +25 -0
  235. package/dist/utils/headers.js.map +1 -0
  236. package/dist/utils/ip.d.ts +64 -0
  237. package/dist/utils/ip.js +29 -0
  238. package/dist/utils/ip.js.map +1 -0
  239. package/dist/utils/redact.d.ts +30 -0
  240. package/dist/utils/redact.js +52 -0
  241. package/dist/utils/redact.js.map +1 -0
  242. package/dist/utils/request-id.d.ts +11 -0
  243. package/dist/utils/request-id.js +7 -0
  244. package/dist/utils/request-id.js.map +1 -0
  245. package/dist/utils/timing-safe.d.ts +31 -0
  246. package/dist/utils/timing-safe.js +17 -0
  247. package/dist/utils/timing-safe.js.map +1 -0
  248. package/dist/utils/timing.d.ts +27 -0
  249. package/dist/utils/timing.js +12 -0
  250. package/dist/utils/timing.js.map +1 -0
  251. package/dist/utils/trace-context.d.ts +51 -0
  252. package/dist/utils/trace-context.js +37 -0
  253. package/dist/utils/trace-context.js.map +1 -0
  254. package/package.json +213 -0
@@ -0,0 +1,53 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { MiddlewareHandler, Hono } from 'hono';
3
+ import { TestAdapter } from '../../adapters/testing.js';
4
+ import { P as Policy } from '../../protocol-2fD3DJrL.js';
5
+ import './trace.js';
6
+ import '@vivero/stoma-core';
7
+
8
+ interface PolicyTestHarnessOptions {
9
+ /**
10
+ * Custom upstream handler. Receives the Hono context after the policy
11
+ * runs. Default: returns `{ ok: true }` with status 200.
12
+ */
13
+ upstream?: MiddlewareHandler;
14
+ /** Route path pattern for the test app. Default: `"/*"`. */
15
+ path?: string;
16
+ /** Gateway name injected into context. Default: `"test-gateway"`. */
17
+ gatewayName?: string;
18
+ /** Custom adapter to use. If not provided, a {@link TestAdapter} is created. */
19
+ adapter?: TestAdapter;
20
+ }
21
+ /**
22
+ * Create a minimal test app with a single policy, error handling,
23
+ * gateway context injection, and a configurable upstream.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { createPolicyTestHarness } from "@vivero/stoma/policies";
28
+ * import { myPolicy } from "./my-policy";
29
+ *
30
+ * const { request, adapter } = createPolicyTestHarness(myPolicy({ max: 10 }));
31
+ *
32
+ * it("should allow valid requests", async () => {
33
+ * const res = await request("/test");
34
+ * expect(res.status).toBe(200);
35
+ * // Await any background work (e.g. waitUntil)
36
+ * await adapter.waitAll();
37
+ * });
38
+ * ```
39
+ *
40
+ * @param policy - The policy instance to test.
41
+ * @param options - Optional upstream, path, and gateway name.
42
+ * @returns An object with `request()`, `app`, and the `adapter` used.
43
+ */
44
+ declare function createPolicyTestHarness(policy: Policy, options?: PolicyTestHarnessOptions): {
45
+ /** The underlying Hono app for advanced test scenarios. */
46
+ app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
47
+ /** The adapter used by the harness. Call `adapter.waitAll()` to await background tasks. */
48
+ adapter: TestAdapter;
49
+ /** Make a test request through the policy pipeline. */
50
+ request: (reqPath: string, init?: RequestInit) => Response | Promise<Response>;
51
+ };
52
+
53
+ export { type PolicyTestHarnessOptions, createPolicyTestHarness };
@@ -0,0 +1,41 @@
1
+ import { Hono } from "hono";
2
+ import { TestAdapter } from "../../adapters/testing";
3
+ import { errorToResponse, GatewayError } from "../../core/errors";
4
+ import { createContextInjector } from "../../core/pipeline";
5
+ function createPolicyTestHarness(policy, options) {
6
+ const path = options?.path ?? "/*";
7
+ const gatewayName = options?.gatewayName ?? "test-gateway";
8
+ const adapter = options?.adapter ?? new TestAdapter();
9
+ const app = new Hono();
10
+ app.use(
11
+ path,
12
+ createContextInjector(gatewayName, path, void 0, void 0, adapter)
13
+ );
14
+ app.use(path, async (c, next) => {
15
+ try {
16
+ await policy.handler(c, next);
17
+ } catch (err) {
18
+ if (err instanceof GatewayError) {
19
+ return errorToResponse(err);
20
+ }
21
+ throw err;
22
+ }
23
+ });
24
+ if (options?.upstream) {
25
+ app.all(path, options.upstream);
26
+ } else {
27
+ app.all(path, (c) => c.json({ ok: true }));
28
+ }
29
+ return {
30
+ /** The underlying Hono app for advanced test scenarios. */
31
+ app,
32
+ /** The adapter used by the harness. Call `adapter.waitAll()` to await background tasks. */
33
+ adapter,
34
+ /** Make a test request through the policy pipeline. */
35
+ request: (reqPath, init) => app.request(reqPath, init)
36
+ };
37
+ }
38
+ export {
39
+ createPolicyTestHarness
40
+ };
41
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/sdk/testing.ts"],"sourcesContent":["/**\n * Test harness for policy authors.\n *\n * Eliminates the repeated 15-line `makeApp()` boilerplate from policy\n * test files. Creates a minimal Hono app with the policy under test,\n * a GatewayError handler, gateway context injection, and a configurable\n * upstream.\n *\n * @module testing\n */\n\nimport type { MiddlewareHandler } from \"hono\";\nimport { Hono } from \"hono\";\nimport { TestAdapter } from \"../../adapters/testing\";\nimport { errorToResponse, GatewayError } from \"../../core/errors\";\nimport { createContextInjector } from \"../../core/pipeline\";\nimport type { Policy } from \"../types\";\n\nexport interface PolicyTestHarnessOptions {\n /**\n * Custom upstream handler. Receives the Hono context after the policy\n * runs. Default: returns `{ ok: true }` with status 200.\n */\n upstream?: MiddlewareHandler;\n /** Route path pattern for the test app. Default: `\"/*\"`. */\n path?: string;\n /** Gateway name injected into context. Default: `\"test-gateway\"`. */\n gatewayName?: string;\n /** Custom adapter to use. If not provided, a {@link TestAdapter} is created. */\n adapter?: TestAdapter;\n}\n\n/**\n * Create a minimal test app with a single policy, error handling,\n * gateway context injection, and a configurable upstream.\n *\n * @example\n * ```ts\n * import { createPolicyTestHarness } from \"@vivero/stoma/policies\";\n * import { myPolicy } from \"./my-policy\";\n *\n * const { request, adapter } = createPolicyTestHarness(myPolicy({ max: 10 }));\n *\n * it(\"should allow valid requests\", async () => {\n * const res = await request(\"/test\");\n * expect(res.status).toBe(200);\n * // Await any background work (e.g. waitUntil)\n * await adapter.waitAll();\n * });\n * ```\n *\n * @param policy - The policy instance to test.\n * @param options - Optional upstream, path, and gateway name.\n * @returns An object with `request()`, `app`, and the `adapter` used.\n */\nexport function createPolicyTestHarness(\n policy: Policy,\n options?: PolicyTestHarnessOptions\n) {\n const path = options?.path ?? \"/*\";\n const gatewayName = options?.gatewayName ?? \"test-gateway\";\n const adapter = options?.adapter ?? new TestAdapter();\n\n const app = new Hono();\n\n // 1. Inject gateway context (requestId, debug factory, etc.)\n app.use(\n path,\n createContextInjector(gatewayName, path, undefined, undefined, adapter)\n );\n\n // 2. Run the policy with GatewayError → structured JSON handling\n app.use(path, async (c, next) => {\n try {\n await policy.handler(c, next);\n } catch (err) {\n if (err instanceof GatewayError) {\n return errorToResponse(err);\n }\n throw err;\n }\n });\n\n // 3. Upstream handler\n if (options?.upstream) {\n app.all(path, options.upstream);\n } else {\n app.all(path, (c) => c.json({ ok: true }));\n }\n\n return {\n /** The underlying Hono app for advanced test scenarios. */\n app,\n /** The adapter used by the harness. Call `adapter.waitAll()` to await background tasks. */\n adapter,\n /** Make a test request through the policy pipeline. */\n request: (reqPath: string, init?: RequestInit) =>\n app.request(reqPath, init),\n };\n}\n"],"mappings":"AAYA,SAAS,YAAY;AACrB,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB,oBAAoB;AAC9C,SAAS,6BAA6B;AAwC/B,SAAS,wBACd,QACA,SACA;AACA,QAAM,OAAO,SAAS,QAAQ;AAC9B,QAAM,cAAc,SAAS,eAAe;AAC5C,QAAM,UAAU,SAAS,WAAW,IAAI,YAAY;AAEpD,QAAM,MAAM,IAAI,KAAK;AAGrB,MAAI;AAAA,IACF;AAAA,IACA,sBAAsB,aAAa,MAAM,QAAW,QAAW,OAAO;AAAA,EACxE;AAGA,MAAI,IAAI,MAAM,OAAO,GAAG,SAAS;AAC/B,QAAI;AACF,YAAM,OAAO,QAAQ,GAAG,IAAI;AAAA,IAC9B,SAAS,KAAK;AACZ,UAAI,eAAe,cAAc;AAC/B,eAAO,gBAAgB,GAAG;AAAA,MAC5B;AACA,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AAGD,MAAI,SAAS,UAAU;AACrB,QAAI,IAAI,MAAM,QAAQ,QAAQ;AAAA,EAChC,OAAO;AACL,QAAI,IAAI,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,CAAC;AAAA,EAC3C;AAEA,SAAO;AAAA;AAAA,IAEL;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA,SAAS,CAAC,SAAiB,SACzB,IAAI,QAAQ,SAAS,IAAI;AAAA,EAC7B;AACF;","names":[]}
@@ -0,0 +1,73 @@
1
+ import { Context } from 'hono';
2
+
3
+ /**
4
+ * Policy trace - structured per-policy trace entries for deep debugging.
5
+ *
6
+ * Provides a zero-overhead reporter API (`TraceReporter`) that policies
7
+ * call to record what they did. When tracing is not active, the reporter
8
+ * is a no-op constant - no allocations, no Map lookups.
9
+ *
10
+ * @module trace
11
+ */
12
+
13
+ /** Set by `parseDebugRequest()` when the client requests tracing. */
14
+ declare const TRACE_REQUESTED_KEY = "_stomaTraceRequested";
15
+ /** Array of `PolicyTraceBaseline` entries pushed by `policiesToMiddleware()`. */
16
+ declare const TRACE_ENTRIES_KEY = "_stomaTraceEntries";
17
+ /** Map of policy name → `PolicyTraceDetail` set by `policyTrace()`. */
18
+ declare const TRACE_DETAILS_KEY = "_stomaTraceDetails";
19
+ /** Auto-captured by the pipeline for every policy when tracing is active. */
20
+ interface PolicyTraceBaseline {
21
+ name: string;
22
+ priority: number;
23
+ durationMs: number;
24
+ calledNext: boolean;
25
+ error: string | null;
26
+ }
27
+ /** Policy-reported detail (cooperative opt-in via `trace()`). */
28
+ interface PolicyTraceDetail {
29
+ action: string;
30
+ data?: Record<string, unknown>;
31
+ }
32
+ /** Combined baseline + optional detail for a single policy. */
33
+ interface PolicyTraceEntry extends PolicyTraceBaseline {
34
+ detail?: PolicyTraceDetail;
35
+ }
36
+ /** Full trace payload emitted as the `x-stoma-trace` response header. */
37
+ interface PolicyTrace {
38
+ requestId: string;
39
+ traceId: string;
40
+ route: string;
41
+ totalMs: number;
42
+ entries: PolicyTraceEntry[];
43
+ }
44
+ /**
45
+ * A trace reporter function. Always callable - no-op when tracing is inactive.
46
+ *
47
+ * @param action - Human-readable action string (e.g. `"HIT"`, `"allowed"`).
48
+ * @param data - Optional structured context data.
49
+ */
50
+ type TraceReporter = (action: string, data?: Record<string, unknown>) => void;
51
+ /** Shared no-op reporter instance - zero overhead when tracing is off. */
52
+ declare const noopTraceReporter: TraceReporter;
53
+ /**
54
+ * Get a trace reporter for a specific policy.
55
+ *
56
+ * When tracing is active (`_stomaTraceRequested` is truthy), returns a
57
+ * function that stores the detail on the context. When inactive, returns
58
+ * {@link noopTraceReporter} - a no-op with zero overhead.
59
+ *
60
+ * @param c - Hono request context.
61
+ * @param policyName - Policy name used as the Map key.
62
+ * @returns A {@link TraceReporter} - always callable.
63
+ */
64
+ declare function policyTrace(c: Context, policyName: string): TraceReporter;
65
+ /**
66
+ * Fast-path check: is tracing requested for this request?
67
+ *
68
+ * @param c - Hono request context.
69
+ * @returns `true` when the client requested tracing via `x-stoma-debug: trace`.
70
+ */
71
+ declare function isTraceRequested(c: Context): boolean;
72
+
73
+ export { type PolicyTrace, type PolicyTraceBaseline, type PolicyTraceDetail, type PolicyTraceEntry, TRACE_DETAILS_KEY, TRACE_ENTRIES_KEY, TRACE_REQUESTED_KEY, type TraceReporter, isTraceRequested, noopTraceReporter, policyTrace };
@@ -0,0 +1,25 @@
1
+ const TRACE_REQUESTED_KEY = "_stomaTraceRequested";
2
+ const TRACE_ENTRIES_KEY = "_stomaTraceEntries";
3
+ const TRACE_DETAILS_KEY = "_stomaTraceDetails";
4
+ const noopTraceReporter = () => {
5
+ };
6
+ function policyTrace(c, policyName) {
7
+ if (!c.get(TRACE_REQUESTED_KEY)) return noopTraceReporter;
8
+ return (action, data) => {
9
+ const details = c.get(TRACE_DETAILS_KEY) ?? /* @__PURE__ */ new Map();
10
+ details.set(policyName, { action, data });
11
+ c.set(TRACE_DETAILS_KEY, details);
12
+ };
13
+ }
14
+ function isTraceRequested(c) {
15
+ return c.get(TRACE_REQUESTED_KEY) === true;
16
+ }
17
+ export {
18
+ TRACE_DETAILS_KEY,
19
+ TRACE_ENTRIES_KEY,
20
+ TRACE_REQUESTED_KEY,
21
+ isTraceRequested,
22
+ noopTraceReporter,
23
+ policyTrace
24
+ };
25
+ //# sourceMappingURL=trace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/sdk/trace.ts"],"sourcesContent":["/**\n * Policy trace - structured per-policy trace entries for deep debugging.\n *\n * Provides a zero-overhead reporter API (`TraceReporter`) that policies\n * call to record what they did. When tracing is not active, the reporter\n * is a no-op constant - no allocations, no Map lookups.\n *\n * @module trace\n */\nimport type { Context } from \"hono\";\n\n// ---------------------------------------------------------------------------\n// Context key constants\n// ---------------------------------------------------------------------------\n\n/** Set by `parseDebugRequest()` when the client requests tracing. */\nexport const TRACE_REQUESTED_KEY = \"_stomaTraceRequested\";\n\n/** Array of `PolicyTraceBaseline` entries pushed by `policiesToMiddleware()`. */\nexport const TRACE_ENTRIES_KEY = \"_stomaTraceEntries\";\n\n/** Map of policy name → `PolicyTraceDetail` set by `policyTrace()`. */\nexport const TRACE_DETAILS_KEY = \"_stomaTraceDetails\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Auto-captured by the pipeline for every policy when tracing is active. */\nexport interface PolicyTraceBaseline {\n name: string;\n priority: number;\n durationMs: number;\n calledNext: boolean;\n error: string | null;\n}\n\n/** Policy-reported detail (cooperative opt-in via `trace()`). */\nexport interface PolicyTraceDetail {\n action: string;\n data?: Record<string, unknown>;\n}\n\n/** Combined baseline + optional detail for a single policy. */\nexport interface PolicyTraceEntry extends PolicyTraceBaseline {\n detail?: PolicyTraceDetail;\n}\n\n/** Full trace payload emitted as the `x-stoma-trace` response header. */\nexport interface PolicyTrace {\n requestId: string;\n traceId: string;\n route: string;\n totalMs: number;\n entries: PolicyTraceEntry[];\n}\n\n// ---------------------------------------------------------------------------\n// Reporter API\n// ---------------------------------------------------------------------------\n\n/**\n * A trace reporter function. Always callable - no-op when tracing is inactive.\n *\n * @param action - Human-readable action string (e.g. `\"HIT\"`, `\"allowed\"`).\n * @param data - Optional structured context data.\n */\nexport type TraceReporter = (\n action: string,\n data?: Record<string, unknown>\n) => void;\n\n/** Shared no-op reporter instance - zero overhead when tracing is off. */\nexport const noopTraceReporter: TraceReporter = () => {};\n\n/**\n * Get a trace reporter for a specific policy.\n *\n * When tracing is active (`_stomaTraceRequested` is truthy), returns a\n * function that stores the detail on the context. When inactive, returns\n * {@link noopTraceReporter} - a no-op with zero overhead.\n *\n * @param c - Hono request context.\n * @param policyName - Policy name used as the Map key.\n * @returns A {@link TraceReporter} - always callable.\n */\nexport function policyTrace(c: Context, policyName: string): TraceReporter {\n if (!c.get(TRACE_REQUESTED_KEY)) return noopTraceReporter;\n\n return (action: string, data?: Record<string, unknown>) => {\n const details = (c.get(TRACE_DETAILS_KEY) ??\n new Map<string, PolicyTraceDetail>()) as Map<string, PolicyTraceDetail>;\n details.set(policyName, { action, data });\n c.set(TRACE_DETAILS_KEY, details);\n };\n}\n\n/**\n * Fast-path check: is tracing requested for this request?\n *\n * @param c - Hono request context.\n * @returns `true` when the client requested tracing via `x-stoma-debug: trace`.\n */\nexport function isTraceRequested(c: Context): boolean {\n return c.get(TRACE_REQUESTED_KEY) === true;\n}\n"],"mappings":"AAgBO,MAAM,sBAAsB;AAG5B,MAAM,oBAAoB;AAG1B,MAAM,oBAAoB;AAmD1B,MAAM,oBAAmC,MAAM;AAAC;AAahD,SAAS,YAAY,GAAY,YAAmC;AACzE,MAAI,CAAC,EAAE,IAAI,mBAAmB,EAAG,QAAO;AAExC,SAAO,CAAC,QAAgB,SAAmC;AACzD,UAAM,UAAW,EAAE,IAAI,iBAAiB,KACtC,oBAAI,IAA+B;AACrC,YAAQ,IAAI,YAAY,EAAE,QAAQ,KAAK,CAAC;AACxC,MAAE,IAAI,mBAAmB,OAAO;AAAA,EAClC;AACF;AAQO,SAAS,iBAAiB,GAAqB;AACpD,SAAO,EAAE,IAAI,mBAAmB,MAAM;AACxC;","names":[]}
@@ -0,0 +1,4 @@
1
+ import 'hono';
2
+ export { u as CacheConfig, C as CacheStore, I as InMemoryCacheStore, d as InMemoryCacheStoreOptions, r as cache } from '../../protocol-2fD3DJrL.js';
3
+ import '../sdk/trace.js';
4
+ import '@vivero/stoma-core';
@@ -0,0 +1,224 @@
1
+ import {
2
+ Priority,
3
+ policyDebug,
4
+ policyTrace,
5
+ resolveConfig,
6
+ safeCall,
7
+ setDebugHeader,
8
+ withSkip
9
+ } from "../sdk";
10
+ const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([204, 304]);
11
+ function safeBody(body, status) {
12
+ return NULL_BODY_STATUSES.has(status) ? null : body ?? null;
13
+ }
14
+ class InMemoryCacheStore {
15
+ entries = /* @__PURE__ */ new Map();
16
+ maxEntries;
17
+ constructor(options) {
18
+ this.maxEntries = options?.maxEntries;
19
+ }
20
+ async get(key) {
21
+ const entry = this.entries.get(key);
22
+ if (!entry) return null;
23
+ if (Date.now() > entry.expiresAt) {
24
+ this.entries.delete(key);
25
+ return null;
26
+ }
27
+ this.entries.delete(key);
28
+ this.entries.set(key, entry);
29
+ return new Response(safeBody(entry.body, entry.status), {
30
+ status: entry.status,
31
+ headers: entry.headers
32
+ });
33
+ }
34
+ async put(key, response, ttlSeconds) {
35
+ const body = await response.arrayBuffer();
36
+ const headers = [];
37
+ response.headers.forEach((value, name) => {
38
+ headers.push([name, value]);
39
+ });
40
+ if (this.maxEntries && !this.entries.has(key) && this.entries.size >= this.maxEntries) {
41
+ const oldestKey = this.entries.keys().next().value;
42
+ if (oldestKey !== void 0) {
43
+ this.entries.delete(oldestKey);
44
+ }
45
+ }
46
+ this.entries.set(key, {
47
+ body,
48
+ status: response.status,
49
+ headers,
50
+ expiresAt: Date.now() + ttlSeconds * 1e3
51
+ });
52
+ }
53
+ async delete(key) {
54
+ return this.entries.delete(key);
55
+ }
56
+ /** Remove all entries (for testing) */
57
+ clear() {
58
+ this.entries.clear();
59
+ }
60
+ /** Current number of entries (for testing/inspection) */
61
+ get size() {
62
+ return this.entries.size;
63
+ }
64
+ /** Release all cached entries. */
65
+ destroy() {
66
+ this.entries.clear();
67
+ }
68
+ }
69
+ const INTERNAL_EXPIRES_HEADER = "x-stoma-internal-expires-at";
70
+ const BODY_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
71
+ function parseCacheControlDirectives(header) {
72
+ if (!header) return [];
73
+ return header.split(",").map((part) => part.trim().split("=")[0].trim().toLowerCase());
74
+ }
75
+ function cache(config) {
76
+ const resolved = resolveConfig(
77
+ {
78
+ ttlSeconds: 300,
79
+ methods: ["GET"],
80
+ respectCacheControl: true,
81
+ cacheStatusHeader: "x-cache",
82
+ bypassDirectives: ["no-store", "no-cache"]
83
+ },
84
+ config
85
+ );
86
+ const normalizedMethods = resolved.methods.map((m) => m.toUpperCase());
87
+ let store = config?.store;
88
+ if (!store) {
89
+ store = new InMemoryCacheStore();
90
+ }
91
+ const resolvedStore = store;
92
+ const statusHeader = resolved.cacheStatusHeader;
93
+ async function buildKey(c) {
94
+ if (config?.cacheKeyFn) return await config.cacheKeyFn(c);
95
+ let key = `${c.req.method}:${c.req.url}`;
96
+ if (BODY_METHODS.has(c.req.method.toUpperCase())) {
97
+ try {
98
+ const bodyText = await c.req.raw.clone().text();
99
+ if (bodyText) {
100
+ const hashBuffer = await crypto.subtle.digest(
101
+ "SHA-256",
102
+ new TextEncoder().encode(bodyText)
103
+ );
104
+ const hashArray = new Uint8Array(hashBuffer);
105
+ let hashHex = "";
106
+ for (const b of hashArray) {
107
+ hashHex += b.toString(16).padStart(2, "0");
108
+ }
109
+ key += `|body:${hashHex}`;
110
+ }
111
+ } catch {
112
+ }
113
+ }
114
+ if (config?.varyHeaders) {
115
+ const vary = config.varyHeaders.map((h) => c.req.header(h) ?? "").join("|");
116
+ key += `|vary:${vary}`;
117
+ }
118
+ return key;
119
+ }
120
+ const handler = async (c, next) => {
121
+ const debug = policyDebug(c, "cache");
122
+ const trace = policyTrace(c, "cache");
123
+ if (!normalizedMethods.includes(c.req.method.toUpperCase())) {
124
+ trace("SKIP", { method: c.req.method });
125
+ await next();
126
+ c.res.headers.set(statusHeader, "SKIP");
127
+ return;
128
+ }
129
+ const key = await buildKey(c);
130
+ setDebugHeader(c, "x-stoma-cache-key", key);
131
+ setDebugHeader(c, "x-stoma-cache-ttl", resolved.ttlSeconds);
132
+ const cached = await safeCall(
133
+ () => resolvedStore.get(key),
134
+ null,
135
+ debug,
136
+ "store.get()"
137
+ );
138
+ if (cached) {
139
+ debug(`HIT ${key}`);
140
+ setDebugHeader(c, "x-stoma-cache-status", "HIT");
141
+ trace("HIT", { key });
142
+ const expiresAtStr = cached.headers.get(INTERNAL_EXPIRES_HEADER);
143
+ if (expiresAtStr) {
144
+ const remaining = Math.max(
145
+ 0,
146
+ Math.ceil((Number(expiresAtStr) - Date.now()) / 1e3)
147
+ );
148
+ setDebugHeader(c, "x-stoma-cache-expires-in", remaining);
149
+ }
150
+ const body = await cached.arrayBuffer();
151
+ const res = new Response(safeBody(body, cached.status), {
152
+ status: cached.status,
153
+ headers: cached.headers
154
+ });
155
+ res.headers.delete(INTERNAL_EXPIRES_HEADER);
156
+ res.headers.set(statusHeader, "HIT");
157
+ c.res = res;
158
+ return;
159
+ }
160
+ await next();
161
+ if (c.res.status >= 500) {
162
+ debug(`SKIP ${key} (status=${c.res.status})`);
163
+ setDebugHeader(c, "x-stoma-cache-status", "SKIP");
164
+ c.res.headers.set(statusHeader, "SKIP");
165
+ return;
166
+ }
167
+ if (resolved.cacheableStatuses && !resolved.cacheableStatuses.includes(c.res.status)) {
168
+ debug(`SKIP ${key} (status=${c.res.status} not in cacheableStatuses)`);
169
+ setDebugHeader(c, "x-stoma-cache-status", "SKIP");
170
+ c.res.headers.set(statusHeader, "SKIP");
171
+ return;
172
+ }
173
+ if (resolved.respectCacheControl) {
174
+ const cc = c.res.headers.get("cache-control") ?? "";
175
+ const directives = parseCacheControlDirectives(cc);
176
+ if (resolved.bypassDirectives.some(
177
+ (d) => directives.includes(d.toLowerCase())
178
+ )) {
179
+ debug(`BYPASS ${key} (cache-control: ${cc})`);
180
+ setDebugHeader(c, "x-stoma-cache-status", "BYPASS");
181
+ trace("BYPASS", { key, directive: cc });
182
+ c.res.headers.set(statusHeader, "BYPASS");
183
+ return;
184
+ }
185
+ }
186
+ debug(`MISS ${key} (ttl=${resolved.ttlSeconds}s)`);
187
+ setDebugHeader(c, "x-stoma-cache-status", "MISS");
188
+ trace("MISS", { key, ttl: resolved.ttlSeconds });
189
+ const storeClone = c.res.clone();
190
+ const storeBody = safeBody(
191
+ await storeClone.arrayBuffer(),
192
+ storeClone.status
193
+ );
194
+ const storeHeaders = new Headers(storeClone.headers);
195
+ storeHeaders.set(
196
+ INTERNAL_EXPIRES_HEADER,
197
+ String(Date.now() + resolved.ttlSeconds * 1e3)
198
+ );
199
+ await safeCall(
200
+ () => resolvedStore.put(
201
+ key,
202
+ new Response(storeBody, {
203
+ status: storeClone.status,
204
+ headers: storeHeaders
205
+ }),
206
+ resolved.ttlSeconds
207
+ ),
208
+ void 0,
209
+ debug,
210
+ "store.put()"
211
+ );
212
+ c.res.headers.set(statusHeader, "MISS");
213
+ };
214
+ return {
215
+ name: "cache",
216
+ priority: Priority.CACHE,
217
+ handler: withSkip(config?.skip, handler)
218
+ };
219
+ }
220
+ export {
221
+ InMemoryCacheStore,
222
+ cache
223
+ };
224
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/traffic/cache.ts"],"sourcesContent":["/**\n * Response caching policy with pluggable storage backends.\n *\n * @module cache\n */\nimport type { Context } from \"hono\";\nimport {\n Priority,\n policyDebug,\n policyTrace,\n resolveConfig,\n safeCall,\n setDebugHeader,\n withSkip,\n} from \"../sdk\";\nimport type { Policy, PolicyConfig } from \"../types\";\n\n// Per HTTP spec, 204 and 304 responses must not carry a body.\n// Node.js enforces this strictly; workerd is lenient. Guard all\n// Response reconstruction so we don't silently break on Node/Bun/Deno.\nconst NULL_BODY_STATUSES = new Set([204, 304]);\nfunction safeBody(\n body: BodyInit | null | undefined,\n status: number\n): BodyInit | null {\n return NULL_BODY_STATUSES.has(status) ? null : (body ?? null);\n}\n\n// --- Store interface ---\n\n/** Pluggable cache storage backend */\nexport interface CacheStore {\n /** Retrieve a cached response by key. Returns null on miss. */\n get(key: string): Promise<Response | null>;\n /** Store a response under key with a TTL in seconds. */\n put(key: string, response: Response, ttlSeconds: number): Promise<void>;\n /** Delete a cached entry. Returns true if something was removed. */\n delete(key: string): Promise<boolean>;\n /** Optional cleanup - clear expired entries, release resources. */\n destroy?(): void;\n}\n\n// --- In-memory default ---\n\ninterface CacheEntry {\n body: ArrayBuffer;\n status: number;\n headers: [string, string][];\n expiresAt: number;\n}\n\n/** Options for the in-memory cache store. */\nexport interface InMemoryCacheStoreOptions {\n /** Maximum number of cached entries. When exceeded, the oldest entry is evicted (LRU). */\n maxEntries?: number;\n}\n\nexport class InMemoryCacheStore implements CacheStore {\n private entries = new Map<string, CacheEntry>();\n private maxEntries: number | undefined;\n\n constructor(options?: InMemoryCacheStoreOptions) {\n this.maxEntries = options?.maxEntries;\n }\n\n async get(key: string): Promise<Response | null> {\n const entry = this.entries.get(key);\n if (!entry) return null;\n if (Date.now() > entry.expiresAt) {\n this.entries.delete(key);\n return null;\n }\n // Move to end for LRU ordering (most recently accessed = last)\n this.entries.delete(key);\n this.entries.set(key, entry);\n return new Response(safeBody(entry.body, entry.status), {\n status: entry.status,\n headers: entry.headers,\n });\n }\n\n async put(\n key: string,\n response: Response,\n ttlSeconds: number\n ): Promise<void> {\n const body = await response.arrayBuffer();\n const headers: [string, string][] = [];\n response.headers.forEach((value, name) => {\n headers.push([name, value]);\n });\n\n // Evict oldest entry (first key in Map iteration order) when at capacity\n if (\n this.maxEntries &&\n !this.entries.has(key) &&\n this.entries.size >= this.maxEntries\n ) {\n const oldestKey = this.entries.keys().next().value;\n if (oldestKey !== undefined) {\n this.entries.delete(oldestKey);\n }\n }\n\n this.entries.set(key, {\n body,\n status: response.status,\n headers,\n expiresAt: Date.now() + ttlSeconds * 1000,\n });\n }\n\n async delete(key: string): Promise<boolean> {\n return this.entries.delete(key);\n }\n\n /** Remove all entries (for testing) */\n clear(): void {\n this.entries.clear();\n }\n\n /** Current number of entries (for testing/inspection) */\n get size(): number {\n return this.entries.size;\n }\n\n /** Release all cached entries. */\n destroy(): void {\n this.entries.clear();\n }\n}\n\n// --- Helpers ---\n\n/** Internal header embedded in cached responses to track expiry time. Stripped before serving. */\nconst INTERNAL_EXPIRES_HEADER = \"x-stoma-internal-expires-at\";\n\n/** Methods that typically carry a request body. */\nconst BODY_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\"]);\n\n/**\n * Parse Cache-Control into individual directive names (lowercase, no values).\n *\n * `\"public, max-age=3600, no-store\"` → `[\"public\", \"max-age\", \"no-store\"]`\n */\nfunction parseCacheControlDirectives(header: string): string[] {\n if (!header) return [];\n return header\n .split(\",\")\n .map((part) => part.trim().split(\"=\")[0].trim().toLowerCase());\n}\n\n// --- Policy ---\n\nexport interface CacheConfig extends PolicyConfig {\n /** Cache TTL in seconds. Default: 300. */\n ttlSeconds?: number;\n /** HTTP methods to cache. Default: [\"GET\"]. Case-insensitive. */\n methods?: string[];\n /** Custom cache key builder. Supports async for body-based keys. Default: method + url (+ body hash for POST/PUT/PATCH). */\n cacheKeyFn?: (c: Context) => string | Promise<string>;\n /** Only cache responses with these status codes. When set, responses with other statuses are not cached (5xx is always excluded regardless). */\n cacheableStatuses?: number[];\n /** Vary cache key on these request headers. */\n varyHeaders?: string[];\n /** Storage backend. Default: InMemoryCacheStore. */\n store?: CacheStore;\n /** Respect upstream Cache-Control directives. Default: true. */\n respectCacheControl?: boolean;\n /** Response header name for cache status (HIT/MISS/BYPASS/SKIP). Default: `\"x-cache\"`. */\n cacheStatusHeader?: string;\n /** Cache-Control directives that trigger a bypass. Matched at the directive level, not substring. Default: `[\"no-store\", \"no-cache\"]`. */\n bypassDirectives?: string[];\n}\n\n/**\n * Cache upstream responses to reduce latency and load.\n *\n * Sets a cache status header on **every** response:\n * - `HIT` - served from cache\n * - `MISS` - fetched from upstream, now cached\n * - `BYPASS` - upstream Cache-Control directive prevented caching\n * - `SKIP` - not eligible for caching (wrong method or server error status)\n *\n * Server error responses (5xx) are never cached. Store failures degrade\n * gracefully via {@link safeCall} - a broken cache store never crashes the\n * request.\n *\n * For methods with a request body (POST, PUT, PATCH), the default cache key\n * includes a SHA-256 hash of the body to prevent key collisions across\n * different payloads.\n *\n * @param config - Cache TTL, storage backend, and key strategy. All fields optional.\n * @returns A {@link Policy} at priority 40.\n *\n * @example\n * ```ts\n * // Simple 5-minute in-memory cache for GET requests\n * cache({ ttlSeconds: 300 });\n *\n * // Cache with Vary on Accept-Language and custom store\n * cache({\n * ttlSeconds: 600,\n * varyHeaders: [\"accept-language\"],\n * store: new CacheApiCacheStore(caches.default),\n * });\n * ```\n */\nexport function cache(config?: CacheConfig): Policy {\n const resolved = resolveConfig<CacheConfig>(\n {\n ttlSeconds: 300,\n methods: [\"GET\"],\n respectCacheControl: true,\n cacheStatusHeader: \"x-cache\",\n bypassDirectives: [\"no-store\", \"no-cache\"],\n },\n config\n );\n\n // Normalize methods to uppercase for case-insensitive matching\n const normalizedMethods = resolved.methods!.map((m) => m.toUpperCase());\n\n let store = config?.store;\n if (!store) {\n store = new InMemoryCacheStore();\n }\n const resolvedStore = store;\n\n const statusHeader = resolved.cacheStatusHeader!;\n\n async function buildKey(c: Context): Promise<string> {\n if (config?.cacheKeyFn) return await config.cacheKeyFn(c);\n\n let key = `${c.req.method}:${c.req.url}`;\n\n // Include body hash for methods that carry a body\n if (BODY_METHODS.has(c.req.method.toUpperCase())) {\n try {\n const bodyText = await c.req.raw.clone().text();\n if (bodyText) {\n const hashBuffer = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(bodyText)\n );\n const hashArray = new Uint8Array(hashBuffer);\n let hashHex = \"\";\n for (const b of hashArray) {\n hashHex += b.toString(16).padStart(2, \"0\");\n }\n key += `|body:${hashHex}`;\n }\n } catch {\n // Body already consumed or unreadable - use URL-only key\n }\n }\n\n if (config?.varyHeaders) {\n const vary = config.varyHeaders\n .map((h) => c.req.header(h) ?? \"\")\n .join(\"|\");\n key += `|vary:${vary}`;\n }\n return key;\n }\n\n const handler: import(\"hono\").MiddlewareHandler = async (c, next) => {\n const debug = policyDebug(c, \"cache\");\n const trace = policyTrace(c, \"cache\");\n\n // Non-cacheable method - pass through with SKIP status\n if (!normalizedMethods.includes(c.req.method.toUpperCase())) {\n trace(\"SKIP\", { method: c.req.method });\n await next();\n c.res.headers.set(statusHeader, \"SKIP\");\n return;\n }\n\n const key = await buildKey(c);\n setDebugHeader(c, \"x-stoma-cache-key\", key);\n setDebugHeader(c, \"x-stoma-cache-ttl\", resolved.ttlSeconds!);\n\n // Check cache (resilient to store failures)\n const cached = await safeCall(\n () => resolvedStore.get(key),\n null,\n debug,\n \"store.get()\"\n );\n if (cached) {\n debug(`HIT ${key}`);\n setDebugHeader(c, \"x-stoma-cache-status\", \"HIT\");\n trace(\"HIT\", { key });\n // Compute remaining TTL from the internal expiry header (if present)\n const expiresAtStr = cached.headers.get(INTERNAL_EXPIRES_HEADER);\n if (expiresAtStr) {\n const remaining = Math.max(\n 0,\n Math.ceil((Number(expiresAtStr) - Date.now()) / 1000)\n );\n setDebugHeader(c, \"x-stoma-cache-expires-in\", remaining);\n }\n // Return cached response, re-creating so headers can be modified\n const body = await cached.arrayBuffer();\n const res = new Response(safeBody(body, cached.status), {\n status: cached.status,\n headers: cached.headers,\n });\n res.headers.delete(INTERNAL_EXPIRES_HEADER);\n res.headers.set(statusHeader, \"HIT\");\n c.res = res;\n return;\n }\n\n await next();\n\n // Never cache server error responses (5xx)\n if (c.res.status >= 500) {\n debug(`SKIP ${key} (status=${c.res.status})`);\n setDebugHeader(c, \"x-stoma-cache-status\", \"SKIP\");\n c.res.headers.set(statusHeader, \"SKIP\");\n return;\n }\n\n // Only cache responses matching cacheableStatuses (when configured)\n if (\n resolved.cacheableStatuses &&\n !resolved.cacheableStatuses.includes(c.res.status)\n ) {\n debug(`SKIP ${key} (status=${c.res.status} not in cacheableStatuses)`);\n setDebugHeader(c, \"x-stoma-cache-status\", \"SKIP\");\n c.res.headers.set(statusHeader, \"SKIP\");\n return;\n }\n\n // Check upstream Cache-Control (directive-level matching, not substring)\n if (resolved.respectCacheControl) {\n const cc = c.res.headers.get(\"cache-control\") ?? \"\";\n const directives = parseCacheControlDirectives(cc);\n if (\n resolved.bypassDirectives!.some((d) =>\n directives.includes(d.toLowerCase())\n )\n ) {\n debug(`BYPASS ${key} (cache-control: ${cc})`);\n setDebugHeader(c, \"x-stoma-cache-status\", \"BYPASS\");\n trace(\"BYPASS\", { key, directive: cc });\n c.res.headers.set(statusHeader, \"BYPASS\");\n return;\n }\n }\n\n // Store a clone (with internal expiry header) and mark as MISS\n debug(`MISS ${key} (ttl=${resolved.ttlSeconds}s)`);\n setDebugHeader(c, \"x-stoma-cache-status\", \"MISS\");\n trace(\"MISS\", { key, ttl: resolved.ttlSeconds! });\n const storeClone = c.res.clone();\n const storeBody = safeBody(\n await storeClone.arrayBuffer(),\n storeClone.status\n );\n const storeHeaders = new Headers(storeClone.headers);\n storeHeaders.set(\n INTERNAL_EXPIRES_HEADER,\n String(Date.now() + resolved.ttlSeconds! * 1000)\n );\n await safeCall(\n () =>\n resolvedStore.put(\n key,\n new Response(storeBody, {\n status: storeClone.status,\n headers: storeHeaders,\n }),\n resolved.ttlSeconds!\n ),\n undefined,\n debug,\n \"store.put()\"\n );\n c.res.headers.set(statusHeader, \"MISS\");\n };\n\n return {\n name: \"cache\",\n priority: Priority.CACHE,\n handler: withSkip(config?.skip, handler),\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,54 @@
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
+ interface RoutingRule {
7
+ /** Human-readable rule name for debugging. */
8
+ name?: string;
9
+ /** Condition that determines if this rule applies. */
10
+ condition: (c: Context) => boolean | Promise<boolean>;
11
+ /** Target upstream URL to route to. */
12
+ target: string;
13
+ /** Optional path rewrite function. */
14
+ rewritePath?: (path: string) => string;
15
+ /** Optional headers to add to the upstream request. */
16
+ headers?: Record<string, string>;
17
+ }
18
+ interface DynamicRoutingConfig extends PolicyConfig {
19
+ /** Ordered list of routing rules. First match wins. Required. */
20
+ rules: Array<RoutingRule>;
21
+ /** If true and no rule matches, call next() normally. If false, throw 404. Default: true. */
22
+ fallthrough?: boolean;
23
+ }
24
+ /**
25
+ * Evaluate routing rules and expose the first match on request context.
26
+ *
27
+ * Evaluates rules in order. The first matching rule's target, rewritePath,
28
+ * and headers are set as context variables for downstream consumption.
29
+ *
30
+ * @param config - Routing rules and fallthrough behavior.
31
+ * @returns A {@link Policy} at priority 50 (REQUEST_TRANSFORM).
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * dynamicRouting({
36
+ * rules: [
37
+ * {
38
+ * name: "v2-api",
39
+ * condition: (c) => c.req.header("x-api-version") === "2",
40
+ * target: "https://api-v2.internal",
41
+ * rewritePath: (path) => path.replace("/api/", "/v2/"),
42
+ * },
43
+ * {
44
+ * name: "default",
45
+ * condition: () => true,
46
+ * target: "https://api-v1.internal",
47
+ * },
48
+ * ],
49
+ * });
50
+ * ```
51
+ */
52
+ declare const dynamicRouting: (config: DynamicRoutingConfig) => Policy;
53
+
54
+ export { type DynamicRoutingConfig, type RoutingRule, dynamicRouting };
@@ -0,0 +1,36 @@
1
+ import { GatewayError } from "../../core/errors";
2
+ import { definePolicy, Priority } from "../sdk";
3
+ const dynamicRouting = /* @__PURE__ */ definePolicy({
4
+ name: "dynamic-routing",
5
+ priority: Priority.REQUEST_TRANSFORM,
6
+ httpOnly: true,
7
+ defaults: { fallthrough: true },
8
+ handler: async (c, next, { config, debug }) => {
9
+ for (const rule of config.rules) {
10
+ const matched = await rule.condition(c);
11
+ if (matched) {
12
+ debug(
13
+ `matched rule ${rule.name ? `"${rule.name}"` : "(unnamed)"} \u2192 target=${rule.target}`
14
+ );
15
+ c.set("_dynamicTarget", rule.target);
16
+ if (rule.rewritePath) {
17
+ c.set("_dynamicRewrite", rule.rewritePath);
18
+ }
19
+ if (rule.headers) {
20
+ c.set("_dynamicHeaders", rule.headers);
21
+ }
22
+ await next();
23
+ return;
24
+ }
25
+ }
26
+ if (!config.fallthrough) {
27
+ throw new GatewayError(404, "no_route", "No routing rule matched");
28
+ }
29
+ debug("no rule matched, falling through");
30
+ await next();
31
+ }
32
+ });
33
+ export {
34
+ dynamicRouting
35
+ };
36
+ //# sourceMappingURL=dynamic-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/traffic/dynamic-routing.ts"],"sourcesContent":["/**\n * Dynamic routing policy - evaluate routing rules and expose match data on context.\n *\n * Evaluates an ordered list of routing rules against the request context.\n * The first matching rule sets context variables that downstream handlers\n * can consume (`_dynamicTarget`, `_dynamicRewrite`, `_dynamicHeaders`).\n *\n * @module dynamic-routing\n */\nimport type { Context } from \"hono\";\nimport { GatewayError } from \"../../core/errors\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface RoutingRule {\n /** Human-readable rule name for debugging. */\n name?: string;\n /** Condition that determines if this rule applies. */\n condition: (c: Context) => boolean | Promise<boolean>;\n /** Target upstream URL to route to. */\n target: string;\n /** Optional path rewrite function. */\n rewritePath?: (path: string) => string;\n /** Optional headers to add to the upstream request. */\n headers?: Record<string, string>;\n}\n\nexport interface DynamicRoutingConfig extends PolicyConfig {\n /** Ordered list of routing rules. First match wins. Required. */\n rules: Array<RoutingRule>;\n /** If true and no rule matches, call next() normally. If false, throw 404. Default: true. */\n fallthrough?: boolean;\n}\n\n/**\n * Evaluate routing rules and expose the first match on request context.\n *\n * Evaluates rules in order. The first matching rule's target, rewritePath,\n * and headers are set as context variables for downstream consumption.\n *\n * @param config - Routing rules and fallthrough behavior.\n * @returns A {@link Policy} at priority 50 (REQUEST_TRANSFORM).\n *\n * @example\n * ```ts\n * dynamicRouting({\n * rules: [\n * {\n * name: \"v2-api\",\n * condition: (c) => c.req.header(\"x-api-version\") === \"2\",\n * target: \"https://api-v2.internal\",\n * rewritePath: (path) => path.replace(\"/api/\", \"/v2/\"),\n * },\n * {\n * name: \"default\",\n * condition: () => true,\n * target: \"https://api-v1.internal\",\n * },\n * ],\n * });\n * ```\n */\nexport const dynamicRouting = /*#__PURE__*/ definePolicy<DynamicRoutingConfig>({\n name: \"dynamic-routing\",\n priority: Priority.REQUEST_TRANSFORM,\n httpOnly: true,\n defaults: { fallthrough: true },\n handler: async (c, next, { config, debug }) => {\n for (const rule of config.rules) {\n const matched = await rule.condition(c);\n if (matched) {\n debug(\n `matched rule ${rule.name ? `\"${rule.name}\"` : \"(unnamed)\"}` +\n ` → target=${rule.target}`\n );\n c.set(\"_dynamicTarget\", rule.target);\n if (rule.rewritePath) {\n c.set(\"_dynamicRewrite\", rule.rewritePath);\n }\n if (rule.headers) {\n c.set(\"_dynamicHeaders\", rule.headers);\n }\n await next();\n return;\n }\n }\n\n // No rule matched\n if (!config.fallthrough) {\n throw new GatewayError(404, \"no_route\", \"No routing rule matched\");\n }\n\n debug(\"no rule matched, falling through\");\n await next();\n },\n});\n"],"mappings":"AAUA,SAAS,oBAAoB;AAC7B,SAAS,cAAc,gBAAgB;AAmDhC,MAAM,iBAA+B,6BAAmC;AAAA,EAC7E,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,UAAU;AAAA,EACV,UAAU,EAAE,aAAa,KAAK;AAAA,EAC9B,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC7C,eAAW,QAAQ,OAAO,OAAO;AAC/B,YAAM,UAAU,MAAM,KAAK,UAAU,CAAC;AACtC,UAAI,SAAS;AACX;AAAA,UACE,gBAAgB,KAAK,OAAO,IAAI,KAAK,IAAI,MAAM,WAAW,kBAC3C,KAAK,MAAM;AAAA,QAC5B;AACA,UAAE,IAAI,kBAAkB,KAAK,MAAM;AACnC,YAAI,KAAK,aAAa;AACpB,YAAE,IAAI,mBAAmB,KAAK,WAAW;AAAA,QAC3C;AACA,YAAI,KAAK,SAAS;AAChB,YAAE,IAAI,mBAAmB,KAAK,OAAO;AAAA,QACvC;AACA,cAAM,KAAK;AACX;AAAA,MACF;AAAA,IACF;AAGA,QAAI,CAAC,OAAO,aAAa;AACvB,YAAM,IAAI,aAAa,KAAK,YAAY,yBAAyB;AAAA,IACnE;AAEA,UAAM,kCAAkC;AACxC,UAAM,KAAK;AAAA,EACb;AACF,CAAC;","names":[]}
@@ -0,0 +1,37 @@
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 GeoIpFilterConfig extends PolicyConfig {
7
+ /** Country codes to allow (e.g. `["US", "CA", "GB"]`). Used in "allow" mode. */
8
+ allow?: string[];
9
+ /** Country codes to deny. Used in "deny" mode. */
10
+ deny?: string[];
11
+ /** Filter mode. Default: `"deny"`. */
12
+ mode?: "allow" | "deny";
13
+ /** Header name to read the country code from. Default: `"cf-ipcountry"`. */
14
+ countryHeader?: string;
15
+ }
16
+ /**
17
+ * Block or allow requests based on geographic country code.
18
+ *
19
+ * Reads the country from the configured header (default `cf-ipcountry`,
20
+ * set by Cloudflare). Supports allowlist and denylist modes. Country
21
+ * sets are pre-computed once at construction time for efficiency.
22
+ *
23
+ * @param config - Country filter rules and mode selection.
24
+ * @returns A policy at priority 1 (IP_FILTER).
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * // Allow only US, Canada, and UK
29
+ * geoIpFilter({ mode: "allow", allow: ["US", "CA", "GB"] });
30
+ *
31
+ * // Block specific countries
32
+ * geoIpFilter({ deny: ["CN", "RU"] });
33
+ * ```
34
+ */
35
+ declare function geoIpFilter(config?: GeoIpFilterConfig): Policy;
36
+
37
+ export { type GeoIpFilterConfig, geoIpFilter };