@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,185 @@
1
+ import { definePolicy, Priority } from "../sdk";
2
+ async function resolveFields(c, fields) {
3
+ const resolved = {};
4
+ for (const [key, value] of Object.entries(fields)) {
5
+ if (typeof value === "function") {
6
+ resolved[key] = await value(c);
7
+ } else {
8
+ resolved[key] = value;
9
+ }
10
+ }
11
+ return resolved;
12
+ }
13
+ function contentTypeMatches(contentType, allowedTypes) {
14
+ if (!contentType) return false;
15
+ return allowedTypes.some((ct) => contentType.includes(ct));
16
+ }
17
+ const assignContent = /* @__PURE__ */ definePolicy({
18
+ name: "assign-content",
19
+ priority: Priority.REQUEST_TRANSFORM,
20
+ defaults: {
21
+ contentTypes: ["application/json"]
22
+ },
23
+ handler: async (c, next, { config, debug }) => {
24
+ if (config.request) {
25
+ const reqContentType = c.req.header("content-type");
26
+ if (contentTypeMatches(reqContentType, config.contentTypes)) {
27
+ let body = {};
28
+ try {
29
+ const cloned = c.req.raw.clone();
30
+ const text = await cloned.text();
31
+ if (text) {
32
+ body = JSON.parse(text);
33
+ }
34
+ } catch {
35
+ }
36
+ const resolved = await resolveFields(c, config.request);
37
+ Object.assign(body, resolved);
38
+ debug(
39
+ "assigned %d fields to request body",
40
+ Object.keys(resolved).length
41
+ );
42
+ const newReq = new Request(c.req.url, {
43
+ method: c.req.method,
44
+ headers: c.req.raw.headers,
45
+ body: JSON.stringify(body),
46
+ // @ts-expect-error -- duplex required for streams in some runtimes
47
+ duplex: "half"
48
+ });
49
+ Object.defineProperty(c.req, "raw", {
50
+ value: newReq,
51
+ configurable: true
52
+ });
53
+ } else {
54
+ debug(
55
+ "request content-type %s not in allowed types - skipping request modification",
56
+ reqContentType
57
+ );
58
+ }
59
+ }
60
+ await next();
61
+ if (config.response) {
62
+ const resContentType = c.res.headers.get("content-type");
63
+ if (contentTypeMatches(resContentType ?? void 0, config.contentTypes)) {
64
+ let body = {};
65
+ try {
66
+ const text = await c.res.text();
67
+ if (text) {
68
+ body = JSON.parse(text);
69
+ }
70
+ } catch {
71
+ }
72
+ const resolved = await resolveFields(c, config.response);
73
+ Object.assign(body, resolved);
74
+ debug(
75
+ "assigned %d fields to response body",
76
+ Object.keys(resolved).length
77
+ );
78
+ const newRes = new Response(JSON.stringify(body), {
79
+ status: c.res.status,
80
+ headers: c.res.headers
81
+ });
82
+ c.res = newRes;
83
+ } else {
84
+ debug(
85
+ "response content-type %s not in allowed types - skipping response modification",
86
+ resContentType
87
+ );
88
+ }
89
+ }
90
+ },
91
+ evaluate: {
92
+ onRequest: async (input, { config, debug }) => {
93
+ if (!config.request) {
94
+ return { action: "continue" };
95
+ }
96
+ const contentType = input.headers.get("content-type") ?? "";
97
+ if (!contentTypeMatches(contentType, config.contentTypes)) {
98
+ debug(
99
+ "request content-type %s not in allowed types - skipping request modification",
100
+ contentType
101
+ );
102
+ return { action: "continue" };
103
+ }
104
+ let body = {};
105
+ try {
106
+ if (input.body) {
107
+ const bodyStr = typeof input.body === "string" ? input.body : new TextDecoder().decode(input.body);
108
+ if (bodyStr) {
109
+ body = JSON.parse(bodyStr);
110
+ }
111
+ }
112
+ } catch {
113
+ }
114
+ for (const [key, value] of Object.entries(config.request)) {
115
+ if (typeof value === "function") {
116
+ body[key] = value({});
117
+ } else {
118
+ body[key] = value;
119
+ }
120
+ }
121
+ debug(
122
+ "assigned %d fields to request body",
123
+ Object.keys(config.request).length
124
+ );
125
+ return {
126
+ action: "continue",
127
+ mutations: [
128
+ {
129
+ type: "body",
130
+ op: "replace",
131
+ content: JSON.stringify(body)
132
+ }
133
+ ]
134
+ };
135
+ },
136
+ onResponse: async (input, { config, debug }) => {
137
+ if (!config.response) {
138
+ return { action: "continue" };
139
+ }
140
+ const contentType = input.headers.get("content-type") ?? "";
141
+ if (!contentTypeMatches(contentType, config.contentTypes)) {
142
+ debug(
143
+ "response content-type %s not in allowed types - skipping response modification",
144
+ contentType
145
+ );
146
+ return { action: "continue" };
147
+ }
148
+ let body = {};
149
+ try {
150
+ if (input.body) {
151
+ const bodyStr = typeof input.body === "string" ? input.body : new TextDecoder().decode(input.body);
152
+ if (bodyStr) {
153
+ body = JSON.parse(bodyStr);
154
+ }
155
+ }
156
+ } catch {
157
+ }
158
+ for (const [key, value] of Object.entries(config.response)) {
159
+ if (typeof value === "function") {
160
+ body[key] = value({});
161
+ } else {
162
+ body[key] = value;
163
+ }
164
+ }
165
+ debug(
166
+ "assigned %d fields to response body",
167
+ Object.keys(config.response).length
168
+ );
169
+ return {
170
+ action: "continue",
171
+ mutations: [
172
+ {
173
+ type: "body",
174
+ op: "replace",
175
+ content: JSON.stringify(body)
176
+ }
177
+ ]
178
+ };
179
+ }
180
+ }
181
+ });
182
+ export {
183
+ assignContent
184
+ };
185
+ //# sourceMappingURL=assign-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/transform/assign-content.ts"],"sourcesContent":["/**\n * Assign (inject/override) fields in JSON request and/or response bodies.\n *\n * Supports static values and dynamic functions that receive the Hono context.\n * Only modifies bodies with matching content types (default: application/json).\n *\n * @module assign-content\n */\nimport type { Context } from \"hono\";\nimport type { Mutation } from \"../../core/protocol\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\n/** A field value - either a static value or a function resolving to one. */\ntype FieldValue = unknown | ((c: Context) => unknown | Promise<unknown>);\n\nexport interface AssignContentConfig extends PolicyConfig {\n /** Fields to set/override in the JSON request body. */\n request?: Record<string, FieldValue>;\n /** Fields to set/override in the JSON response body. */\n response?: Record<string, FieldValue>;\n /** Only modify bodies with these content types. Default: `[\"application/json\"]`. */\n contentTypes?: string[];\n}\n\n/**\n * Resolve a field map - evaluates dynamic functions, keeps static values.\n */\nasync function resolveFields(\n c: Context,\n fields: Record<string, FieldValue>\n): Promise<Record<string, unknown>> {\n const resolved: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(fields)) {\n if (typeof value === \"function\") {\n resolved[key] = await (\n value as (c: Context) => unknown | Promise<unknown>\n )(c);\n } else {\n resolved[key] = value;\n }\n }\n return resolved;\n}\n\n/**\n * Check if a content-type header matches any of the allowed types.\n */\nfunction contentTypeMatches(\n contentType: string | undefined,\n allowedTypes: string[]\n): boolean {\n if (!contentType) return false;\n return allowedTypes.some((ct) => contentType.includes(ct));\n}\n\n/**\n * Assign content policy.\n *\n * Injects or overrides fields in JSON request and/or response bodies.\n * Useful for injecting tenant IDs, timestamps, metadata, or other\n * fields that should be transparently added by the gateway.\n *\n * @example\n * ```ts\n * import { assignContent } from \"@vivero/stoma\";\n *\n * assignContent({\n * request: {\n * tenantId: \"acme\",\n * timestamp: (c) => new Date().toISOString(),\n * },\n * response: {\n * gateway: \"stoma\",\n * },\n * });\n * ```\n */\nexport const assignContent = /*#__PURE__*/ definePolicy<AssignContentConfig>({\n name: \"assign-content\",\n priority: Priority.REQUEST_TRANSFORM,\n defaults: {\n contentTypes: [\"application/json\"],\n },\n handler: async (c, next, { config, debug }) => {\n // Request phase - modify request body before upstream\n if (config.request) {\n const reqContentType = c.req.header(\"content-type\");\n if (contentTypeMatches(reqContentType, config.contentTypes!)) {\n let body: Record<string, unknown> = {};\n\n // Try to parse existing body\n try {\n const cloned = c.req.raw.clone();\n const text = await cloned.text();\n if (text) {\n body = JSON.parse(text) as Record<string, unknown>;\n }\n } catch {\n // No body or invalid JSON - start with empty object\n }\n\n // Resolve and merge fields\n const resolved = await resolveFields(c, config.request);\n Object.assign(body, resolved);\n\n debug(\n \"assigned %d fields to request body\",\n Object.keys(resolved).length\n );\n\n // Replace the request with modified body\n const newReq = new Request(c.req.url, {\n method: c.req.method,\n headers: c.req.raw.headers,\n body: JSON.stringify(body),\n // @ts-expect-error -- duplex required for streams in some runtimes\n duplex: \"half\",\n });\n Object.defineProperty(c.req, \"raw\", {\n value: newReq,\n configurable: true,\n });\n } else {\n debug(\n \"request content-type %s not in allowed types - skipping request modification\",\n reqContentType\n );\n }\n }\n\n await next();\n\n // Response phase - modify response body after upstream\n if (config.response) {\n const resContentType = c.res.headers.get(\"content-type\");\n if (\n contentTypeMatches(resContentType ?? undefined, config.contentTypes!)\n ) {\n let body: Record<string, unknown> = {};\n\n try {\n const text = await c.res.text();\n if (text) {\n body = JSON.parse(text) as Record<string, unknown>;\n }\n } catch {\n // Invalid JSON - start with empty object\n }\n\n // Resolve and merge fields\n const resolved = await resolveFields(c, config.response);\n Object.assign(body, resolved);\n\n debug(\n \"assigned %d fields to response body\",\n Object.keys(resolved).length\n );\n\n // Create new response with modified body, preserving status and headers\n const newRes = new Response(JSON.stringify(body), {\n status: c.res.status,\n headers: c.res.headers,\n });\n c.res = newRes;\n } else {\n debug(\n \"response content-type %s not in allowed types - skipping response modification\",\n resContentType\n );\n }\n }\n },\n evaluate: {\n onRequest: async (input, { config, debug }) => {\n if (!config.request) {\n return { action: \"continue\" };\n }\n\n const contentType = input.headers.get(\"content-type\") ?? \"\";\n if (!contentTypeMatches(contentType, config.contentTypes!)) {\n debug(\n \"request content-type %s not in allowed types - skipping request modification\",\n contentType\n );\n return { action: \"continue\" };\n }\n\n // Parse existing body or start empty\n let body: Record<string, unknown> = {};\n try {\n if (input.body) {\n const bodyStr =\n typeof input.body === \"string\"\n ? input.body\n : new TextDecoder().decode(input.body);\n if (bodyStr) {\n body = JSON.parse(bodyStr);\n }\n }\n } catch {\n // Invalid JSON - start with empty object\n }\n\n // Resolve and merge fields (can't use dynamic functions in evaluate)\n for (const [key, value] of Object.entries(config.request)) {\n if (typeof value === \"function\") {\n body[key] = value({} as Context);\n } else {\n body[key] = value;\n }\n }\n\n debug(\n \"assigned %d fields to request body\",\n Object.keys(config.request).length\n );\n\n return {\n action: \"continue\",\n mutations: [\n {\n type: \"body\",\n op: \"replace\",\n content: JSON.stringify(body),\n } as Mutation,\n ],\n };\n },\n onResponse: async (input, { config, debug }) => {\n if (!config.response) {\n return { action: \"continue\" };\n }\n\n const contentType = input.headers.get(\"content-type\") ?? \"\";\n if (!contentTypeMatches(contentType, config.contentTypes!)) {\n debug(\n \"response content-type %s not in allowed types - skipping response modification\",\n contentType\n );\n return { action: \"continue\" };\n }\n\n // Parse existing body or start empty\n let body: Record<string, unknown> = {};\n try {\n if (input.body) {\n const bodyStr =\n typeof input.body === \"string\"\n ? input.body\n : new TextDecoder().decode(input.body);\n if (bodyStr) {\n body = JSON.parse(bodyStr);\n }\n }\n } catch {\n // Invalid JSON - start with empty object\n }\n\n // Resolve and merge fields (can't use dynamic functions in evaluate)\n for (const [key, value] of Object.entries(config.response)) {\n if (typeof value === \"function\") {\n body[key] = value({} as Context);\n } else {\n body[key] = value;\n }\n }\n\n debug(\n \"assigned %d fields to response body\",\n Object.keys(config.response).length\n );\n\n return {\n action: \"continue\",\n mutations: [\n {\n type: \"body\",\n op: \"replace\",\n content: JSON.stringify(body),\n } as Mutation,\n ],\n };\n },\n },\n});\n"],"mappings":"AAUA,SAAS,cAAc,gBAAgB;AAkBvC,eAAe,cACb,GACA,QACkC;AAClC,QAAM,WAAoC,CAAC;AAC3C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,OAAO,UAAU,YAAY;AAC/B,eAAS,GAAG,IAAI,MACd,MACA,CAAC;AAAA,IACL,OAAO;AACL,eAAS,GAAG,IAAI;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,mBACP,aACA,cACS;AACT,MAAI,CAAC,YAAa,QAAO;AACzB,SAAO,aAAa,KAAK,CAAC,OAAO,YAAY,SAAS,EAAE,CAAC;AAC3D;AAwBO,MAAM,gBAA8B,6BAAkC;AAAA,EAC3E,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,UAAU;AAAA,IACR,cAAc,CAAC,kBAAkB;AAAA,EACnC;AAAA,EACA,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,MAAM,MAAM;AAE7C,QAAI,OAAO,SAAS;AAClB,YAAM,iBAAiB,EAAE,IAAI,OAAO,cAAc;AAClD,UAAI,mBAAmB,gBAAgB,OAAO,YAAa,GAAG;AAC5D,YAAI,OAAgC,CAAC;AAGrC,YAAI;AACF,gBAAM,SAAS,EAAE,IAAI,IAAI,MAAM;AAC/B,gBAAM,OAAO,MAAM,OAAO,KAAK;AAC/B,cAAI,MAAM;AACR,mBAAO,KAAK,MAAM,IAAI;AAAA,UACxB;AAAA,QACF,QAAQ;AAAA,QAER;AAGA,cAAM,WAAW,MAAM,cAAc,GAAG,OAAO,OAAO;AACtD,eAAO,OAAO,MAAM,QAAQ;AAE5B;AAAA,UACE;AAAA,UACA,OAAO,KAAK,QAAQ,EAAE;AAAA,QACxB;AAGA,cAAM,SAAS,IAAI,QAAQ,EAAE,IAAI,KAAK;AAAA,UACpC,QAAQ,EAAE,IAAI;AAAA,UACd,SAAS,EAAE,IAAI,IAAI;AAAA,UACnB,MAAM,KAAK,UAAU,IAAI;AAAA;AAAA,UAEzB,QAAQ;AAAA,QACV,CAAC;AACD,eAAO,eAAe,EAAE,KAAK,OAAO;AAAA,UAClC,OAAO;AAAA,UACP,cAAc;AAAA,QAChB,CAAC;AAAA,MACH,OAAO;AACL;AAAA,UACE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,UAAM,KAAK;AAGX,QAAI,OAAO,UAAU;AACnB,YAAM,iBAAiB,EAAE,IAAI,QAAQ,IAAI,cAAc;AACvD,UACE,mBAAmB,kBAAkB,QAAW,OAAO,YAAa,GACpE;AACA,YAAI,OAAgC,CAAC;AAErC,YAAI;AACF,gBAAM,OAAO,MAAM,EAAE,IAAI,KAAK;AAC9B,cAAI,MAAM;AACR,mBAAO,KAAK,MAAM,IAAI;AAAA,UACxB;AAAA,QACF,QAAQ;AAAA,QAER;AAGA,cAAM,WAAW,MAAM,cAAc,GAAG,OAAO,QAAQ;AACvD,eAAO,OAAO,MAAM,QAAQ;AAE5B;AAAA,UACE;AAAA,UACA,OAAO,KAAK,QAAQ,EAAE;AAAA,QACxB;AAGA,cAAM,SAAS,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,UAChD,QAAQ,EAAE,IAAI;AAAA,UACd,SAAS,EAAE,IAAI;AAAA,QACjB,CAAC;AACD,UAAE,MAAM;AAAA,MACV,OAAO;AACL;AAAA,UACE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,UAAU;AAAA,IACR,WAAW,OAAO,OAAO,EAAE,QAAQ,MAAM,MAAM;AAC7C,UAAI,CAAC,OAAO,SAAS;AACnB,eAAO,EAAE,QAAQ,WAAW;AAAA,MAC9B;AAEA,YAAM,cAAc,MAAM,QAAQ,IAAI,cAAc,KAAK;AACzD,UAAI,CAAC,mBAAmB,aAAa,OAAO,YAAa,GAAG;AAC1D;AAAA,UACE;AAAA,UACA;AAAA,QACF;AACA,eAAO,EAAE,QAAQ,WAAW;AAAA,MAC9B;AAGA,UAAI,OAAgC,CAAC;AACrC,UAAI;AACF,YAAI,MAAM,MAAM;AACd,gBAAM,UACJ,OAAO,MAAM,SAAS,WAClB,MAAM,OACN,IAAI,YAAY,EAAE,OAAO,MAAM,IAAI;AACzC,cAAI,SAAS;AACX,mBAAO,KAAK,MAAM,OAAO;AAAA,UAC3B;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAGA,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,OAAO,GAAG;AACzD,YAAI,OAAO,UAAU,YAAY;AAC/B,eAAK,GAAG,IAAI,MAAM,CAAC,CAAY;AAAA,QACjC,OAAO;AACL,eAAK,GAAG,IAAI;AAAA,QACd;AAAA,MACF;AAEA;AAAA,QACE;AAAA,QACA,OAAO,KAAK,OAAO,OAAO,EAAE;AAAA,MAC9B;AAEA,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW;AAAA,UACT;AAAA,YACE,MAAM;AAAA,YACN,IAAI;AAAA,YACJ,SAAS,KAAK,UAAU,IAAI;AAAA,UAC9B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,YAAY,OAAO,OAAO,EAAE,QAAQ,MAAM,MAAM;AAC9C,UAAI,CAAC,OAAO,UAAU;AACpB,eAAO,EAAE,QAAQ,WAAW;AAAA,MAC9B;AAEA,YAAM,cAAc,MAAM,QAAQ,IAAI,cAAc,KAAK;AACzD,UAAI,CAAC,mBAAmB,aAAa,OAAO,YAAa,GAAG;AAC1D;AAAA,UACE;AAAA,UACA;AAAA,QACF;AACA,eAAO,EAAE,QAAQ,WAAW;AAAA,MAC9B;AAGA,UAAI,OAAgC,CAAC;AACrC,UAAI;AACF,YAAI,MAAM,MAAM;AACd,gBAAM,UACJ,OAAO,MAAM,SAAS,WAClB,MAAM,OACN,IAAI,YAAY,EAAE,OAAO,MAAM,IAAI;AACzC,cAAI,SAAS;AACX,mBAAO,KAAK,MAAM,OAAO;AAAA,UAC3B;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAGA,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,QAAQ,GAAG;AAC1D,YAAI,OAAO,UAAU,YAAY;AAC/B,eAAK,GAAG,IAAI,MAAM,CAAC,CAAY;AAAA,QACjC,OAAO;AACL,eAAK,GAAG,IAAI;AAAA,QACd;AAAA,MACF;AAEA;AAAA,QACE;AAAA,QACA,OAAO,KAAK,OAAO,QAAQ,EAAE;AAAA,MAC/B;AAEA,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW;AAAA,UACT;AAAA,YACE,MAAM;AAAA,YACN,IAAI;AAAA,YACJ,SAAS,KAAK,UAAU,IAAI;AAAA,UAC9B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;","names":[]}
@@ -0,0 +1,57 @@
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 CorsConfig extends PolicyConfig {
7
+ /** Allowed origins. Default: "*" */
8
+ origins?: string | string[] | ((origin: string) => boolean);
9
+ /** Allowed HTTP methods. Default: all. */
10
+ methods?: string[];
11
+ /** Headers the client is allowed to send. */
12
+ allowHeaders?: string[];
13
+ /** Headers exposed to the client. */
14
+ exposeHeaders?: string[];
15
+ /** Max age for preflight cache in seconds. Default: 86400. */
16
+ maxAge?: number;
17
+ /** Allow credentials. Default: false. */
18
+ credentials?: boolean;
19
+ }
20
+ /**
21
+ * Add Cross-Origin Resource Sharing headers to gateway responses.
22
+ *
23
+ * Wraps Hono's built-in CORS middleware as a composable policy. Handles both
24
+ * simple and preflight (OPTIONS) requests. Runs at priority 5 so CORS headers
25
+ * are applied before auth or other policies reject the request.
26
+ *
27
+ * @param config - Origin rules, allowed methods/headers, and credentials. All fields optional.
28
+ * @returns A {@link Policy} at priority 5 (runs very early).
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { createGateway } from "@vivero/stoma";
33
+ * import { cors } from "@vivero/stoma/policies";
34
+ *
35
+ * // Allow any origin (default)
36
+ * createGateway({
37
+ * policies: [cors()],
38
+ * routes: [{ path: "/api/*", pipeline: { upstream: { type: "url", target: "https://api.example.com" } } }],
39
+ * });
40
+ *
41
+ * // Restrict to specific origins with credentials
42
+ * cors({
43
+ * origins: ["https://app.example.com", "https://staging.example.com"],
44
+ * methods: ["GET", "POST", "PUT", "DELETE"],
45
+ * credentials: true,
46
+ * maxAge: 3600,
47
+ * });
48
+ *
49
+ * // Dynamic origin validation
50
+ * cors({
51
+ * origins: (origin) => origin.endsWith(".example.com"),
52
+ * });
53
+ * ```
54
+ */
55
+ declare function cors(config?: CorsConfig): Policy;
56
+
57
+ export { type CorsConfig, cors };
@@ -0,0 +1,23 @@
1
+ import { cors as honoCors } from "hono/cors";
2
+ import { Priority, withSkip } from "../sdk";
3
+ function cors(config) {
4
+ const origin = config?.origins ?? "*";
5
+ const honoMiddleware = honoCors({
6
+ origin: typeof origin === "function" ? (o) => origin(o) ? o : "" : origin,
7
+ allowMethods: config?.methods,
8
+ allowHeaders: config?.allowHeaders,
9
+ exposeHeaders: config?.exposeHeaders,
10
+ maxAge: config?.maxAge,
11
+ credentials: config?.credentials
12
+ });
13
+ return {
14
+ name: "cors",
15
+ priority: Priority.EARLY,
16
+ handler: withSkip(config?.skip, honoMiddleware),
17
+ httpOnly: true
18
+ };
19
+ }
20
+ export {
21
+ cors
22
+ };
23
+ //# sourceMappingURL=cors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/transform/cors.ts"],"sourcesContent":["/**\n * CORS policy wrapping Hono's built-in CORS middleware.\n *\n * @module cors\n */\nimport { cors as honoCors } from \"hono/cors\";\nimport { Priority, withSkip } from \"../sdk\";\nimport type { Policy, PolicyConfig } from \"../types\";\n\nexport interface CorsConfig extends PolicyConfig {\n /** Allowed origins. Default: \"*\" */\n origins?: string | string[] | ((origin: string) => boolean);\n /** Allowed HTTP methods. Default: all. */\n methods?: string[];\n /** Headers the client is allowed to send. */\n allowHeaders?: string[];\n /** Headers exposed to the client. */\n exposeHeaders?: string[];\n /** Max age for preflight cache in seconds. Default: 86400. */\n maxAge?: number;\n /** Allow credentials. Default: false. */\n credentials?: boolean;\n}\n\n/**\n * Add Cross-Origin Resource Sharing headers to gateway responses.\n *\n * Wraps Hono's built-in CORS middleware as a composable policy. Handles both\n * simple and preflight (OPTIONS) requests. Runs at priority 5 so CORS headers\n * are applied before auth or other policies reject the request.\n *\n * @param config - Origin rules, allowed methods/headers, and credentials. All fields optional.\n * @returns A {@link Policy} at priority 5 (runs very early).\n *\n * @example\n * ```ts\n * import { createGateway } from \"@vivero/stoma\";\n * import { cors } from \"@vivero/stoma/policies\";\n *\n * // Allow any origin (default)\n * createGateway({\n * policies: [cors()],\n * routes: [{ path: \"/api/*\", pipeline: { upstream: { type: \"url\", target: \"https://api.example.com\" } } }],\n * });\n *\n * // Restrict to specific origins with credentials\n * cors({\n * origins: [\"https://app.example.com\", \"https://staging.example.com\"],\n * methods: [\"GET\", \"POST\", \"PUT\", \"DELETE\"],\n * credentials: true,\n * maxAge: 3600,\n * });\n *\n * // Dynamic origin validation\n * cors({\n * origins: (origin) => origin.endsWith(\".example.com\"),\n * });\n * ```\n */\nexport function cors(config?: CorsConfig): Policy {\n const origin = config?.origins ?? \"*\";\n const honoMiddleware = honoCors({\n origin: typeof origin === \"function\" ? (o) => (origin(o) ? o : \"\") : origin,\n allowMethods: config?.methods,\n allowHeaders: config?.allowHeaders,\n exposeHeaders: config?.exposeHeaders,\n maxAge: config?.maxAge,\n credentials: config?.credentials,\n });\n\n return {\n name: \"cors\",\n priority: Priority.EARLY,\n handler: withSkip(config?.skip, honoMiddleware),\n httpOnly: true,\n };\n}\n"],"mappings":"AAKA,SAAS,QAAQ,gBAAgB;AACjC,SAAS,UAAU,gBAAgB;AAqD5B,SAAS,KAAK,QAA6B;AAChD,QAAM,SAAS,QAAQ,WAAW;AAClC,QAAM,iBAAiB,SAAS;AAAA,IAC9B,QAAQ,OAAO,WAAW,aAAa,CAAC,MAAO,OAAO,CAAC,IAAI,IAAI,KAAM;AAAA,IACrE,cAAc,QAAQ;AAAA,IACtB,cAAc,QAAQ;AAAA,IACtB,eAAe,QAAQ;AAAA,IACvB,QAAQ,QAAQ;AAAA,IAChB,aAAa,QAAQ;AAAA,EACvB,CAAC;AAED,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,SAAS;AAAA,IACnB,SAAS,SAAS,QAAQ,MAAM,cAAc;AAAA,IAC9C,UAAU;AAAA,EACZ;AACF;","names":[]}
@@ -0,0 +1,50 @@
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
+ /** Result shape returned by the user-provided validation function. */
7
+ interface JsonValidationResult {
8
+ valid: boolean;
9
+ errors?: string[];
10
+ }
11
+ interface JsonValidationConfig extends PolicyConfig {
12
+ /** Custom validation function. Takes parsed body, returns validation result. */
13
+ validate?: (body: unknown) => JsonValidationResult | Promise<JsonValidationResult>;
14
+ /** Content types to validate. Default: ["application/json"] */
15
+ contentTypes?: string[];
16
+ /** HTTP status code on validation failure. Default: 422 */
17
+ rejectStatus?: number;
18
+ /** Include validation errors in response. Default: true */
19
+ errorDetail?: boolean;
20
+ }
21
+ /**
22
+ * Pluggable JSON body validation policy.
23
+ *
24
+ * Validates the request body using a user-provided function. When no
25
+ * `validate` function is configured, checks that the body is parseable JSON.
26
+ * Requests with content types not in the configured list pass through
27
+ * without validation.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { jsonValidation } from "@vivero/stoma";
32
+ *
33
+ * // With Zod
34
+ * jsonValidation({
35
+ * validate: (body) => {
36
+ * const result = myZodSchema.safeParse(body);
37
+ * return {
38
+ * valid: result.success,
39
+ * errors: result.success ? undefined : result.error.issues.map(i => i.message),
40
+ * };
41
+ * },
42
+ * });
43
+ *
44
+ * // Just validate JSON is parseable (no validate function)
45
+ * jsonValidation();
46
+ * ```
47
+ */
48
+ declare const jsonValidation: (config?: JsonValidationConfig | undefined) => Policy;
49
+
50
+ export { type JsonValidationConfig, type JsonValidationResult, jsonValidation };
@@ -0,0 +1,125 @@
1
+ import { GatewayError } from "../../core/errors";
2
+ import { definePolicy, Priority } from "../sdk";
3
+ const jsonValidation = /* @__PURE__ */ definePolicy({
4
+ name: "json-validation",
5
+ priority: Priority.AUTH,
6
+ phases: ["request-body"],
7
+ defaults: {
8
+ contentTypes: ["application/json"],
9
+ rejectStatus: 422,
10
+ errorDetail: true
11
+ },
12
+ handler: async (c, next, { config, debug }) => {
13
+ const contentType = c.req.header("content-type") ?? "";
14
+ const matchedType = config.contentTypes.some(
15
+ (ct) => contentType.includes(ct)
16
+ );
17
+ if (!matchedType) {
18
+ debug(
19
+ "skipping - content type %s not in %o",
20
+ contentType,
21
+ config.contentTypes
22
+ );
23
+ await next();
24
+ return;
25
+ }
26
+ let parsed;
27
+ try {
28
+ const cloned = c.req.raw.clone();
29
+ const text = await cloned.text();
30
+ if (!text) {
31
+ debug("empty body with JSON content type");
32
+ throw new GatewayError(
33
+ config.rejectStatus,
34
+ "validation_failed",
35
+ "Request body is empty"
36
+ );
37
+ }
38
+ parsed = JSON.parse(text);
39
+ } catch (err) {
40
+ if (err instanceof GatewayError) throw err;
41
+ debug("body parse failed");
42
+ throw new GatewayError(
43
+ config.rejectStatus,
44
+ "validation_failed",
45
+ "Request body is not valid JSON"
46
+ );
47
+ }
48
+ if (!config.validate) {
49
+ debug("no validator configured - JSON parsed successfully");
50
+ await next();
51
+ return;
52
+ }
53
+ const result = await config.validate(parsed);
54
+ if (!result.valid) {
55
+ const message = config.errorDetail && result.errors && result.errors.length > 0 ? `Validation failed: ${result.errors.join("; ")}` : "Validation failed";
56
+ debug("validation failed: %s", message);
57
+ throw new GatewayError(
58
+ config.rejectStatus,
59
+ "validation_failed",
60
+ message
61
+ );
62
+ }
63
+ debug("validation passed");
64
+ await next();
65
+ },
66
+ evaluate: {
67
+ onRequest: async (input, { config, debug }) => {
68
+ const contentType = input.headers.get("content-type") ?? "";
69
+ const matchedType = config.contentTypes.some(
70
+ (ct) => contentType.includes(ct)
71
+ );
72
+ if (!matchedType) {
73
+ debug(
74
+ "skipping - content type %s not in %o",
75
+ contentType,
76
+ config.contentTypes
77
+ );
78
+ return { action: "continue" };
79
+ }
80
+ let parsed;
81
+ try {
82
+ if (!input.body) {
83
+ debug("empty body with JSON content type");
84
+ return {
85
+ action: "reject",
86
+ status: config.rejectStatus,
87
+ code: "validation_failed",
88
+ message: "Request body is empty"
89
+ };
90
+ }
91
+ const bodyStr = typeof input.body === "string" ? input.body : new TextDecoder().decode(input.body);
92
+ parsed = JSON.parse(bodyStr);
93
+ } catch {
94
+ debug("body parse failed");
95
+ return {
96
+ action: "reject",
97
+ status: config.rejectStatus,
98
+ code: "validation_failed",
99
+ message: "Request body is not valid JSON"
100
+ };
101
+ }
102
+ if (!config.validate) {
103
+ debug("no validator configured - JSON parsed successfully");
104
+ return { action: "continue" };
105
+ }
106
+ const result = await config.validate(parsed);
107
+ if (!result.valid) {
108
+ const message = config.errorDetail && result.errors && result.errors.length > 0 ? `Validation failed: ${result.errors.join("; ")}` : "Validation failed";
109
+ debug("validation failed: %s", message);
110
+ return {
111
+ action: "reject",
112
+ status: config.rejectStatus,
113
+ code: "validation_failed",
114
+ message
115
+ };
116
+ }
117
+ debug("validation passed");
118
+ return { action: "continue" };
119
+ }
120
+ }
121
+ });
122
+ export {
123
+ jsonValidation
124
+ };
125
+ //# sourceMappingURL=json-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/transform/json-validation.ts"],"sourcesContent":["/**\n * JSON body validation policy - pluggable validation with zero dependencies.\n *\n * Accepts a user-provided `validate` function that can wrap any validation\n * library (Zod, AJV, Valibot, TypeBox, etc.). When no validator is provided,\n * the policy simply checks that the request body is valid JSON.\n *\n * @module json-validation\n */\n\nimport { GatewayError } from \"../../core/errors\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\n/** Result shape returned by the user-provided validation function. */\nexport interface JsonValidationResult {\n valid: boolean;\n errors?: string[];\n}\n\nexport interface JsonValidationConfig extends PolicyConfig {\n /** Custom validation function. Takes parsed body, returns validation result. */\n validate?: (\n body: unknown\n ) => JsonValidationResult | Promise<JsonValidationResult>;\n /** Content types to validate. Default: [\"application/json\"] */\n contentTypes?: string[];\n /** HTTP status code on validation failure. Default: 422 */\n rejectStatus?: number;\n /** Include validation errors in response. Default: true */\n errorDetail?: boolean;\n}\n\n/**\n * Pluggable JSON body validation policy.\n *\n * Validates the request body using a user-provided function. When no\n * `validate` function is configured, checks that the body is parseable JSON.\n * Requests with content types not in the configured list pass through\n * without validation.\n *\n * @example\n * ```ts\n * import { jsonValidation } from \"@vivero/stoma\";\n *\n * // With Zod\n * jsonValidation({\n * validate: (body) => {\n * const result = myZodSchema.safeParse(body);\n * return {\n * valid: result.success,\n * errors: result.success ? undefined : result.error.issues.map(i => i.message),\n * };\n * },\n * });\n *\n * // Just validate JSON is parseable (no validate function)\n * jsonValidation();\n * ```\n */\nexport const jsonValidation = /*#__PURE__*/ definePolicy<JsonValidationConfig>({\n name: \"json-validation\",\n priority: Priority.AUTH,\n phases: [\"request-body\"],\n defaults: {\n contentTypes: [\"application/json\"],\n rejectStatus: 422,\n errorDetail: true,\n },\n handler: async (c, next, { config, debug }) => {\n const contentType = c.req.header(\"content-type\") ?? \"\";\n const matchedType = config.contentTypes!.some((ct) =>\n contentType.includes(ct)\n );\n\n if (!matchedType) {\n debug(\n \"skipping - content type %s not in %o\",\n contentType,\n config.contentTypes\n );\n await next();\n return;\n }\n\n // Clone the request to avoid consuming the body stream for downstream handlers\n let parsed: unknown;\n try {\n const cloned = c.req.raw.clone();\n const text = await cloned.text();\n if (!text) {\n // Empty body with JSON content-type - treat as parse failure\n debug(\"empty body with JSON content type\");\n throw new GatewayError(\n config.rejectStatus!,\n \"validation_failed\",\n \"Request body is empty\"\n );\n }\n parsed = JSON.parse(text);\n } catch (err) {\n if (err instanceof GatewayError) throw err;\n debug(\"body parse failed\");\n throw new GatewayError(\n config.rejectStatus!,\n \"validation_failed\",\n \"Request body is not valid JSON\"\n );\n }\n\n // If no validate function provided, valid JSON parse is sufficient\n if (!config.validate) {\n debug(\"no validator configured - JSON parsed successfully\");\n await next();\n return;\n }\n\n const result = await config.validate(parsed);\n\n if (!result.valid) {\n const message =\n config.errorDetail && result.errors && result.errors.length > 0\n ? `Validation failed: ${result.errors.join(\"; \")}`\n : \"Validation failed\";\n debug(\"validation failed: %s\", message);\n throw new GatewayError(\n config.rejectStatus!,\n \"validation_failed\",\n message\n );\n }\n\n debug(\"validation passed\");\n await next();\n },\n evaluate: {\n onRequest: async (input, { config, debug }) => {\n const contentType = input.headers.get(\"content-type\") ?? \"\";\n const matchedType = config.contentTypes!.some((ct) =>\n contentType.includes(ct)\n );\n\n if (!matchedType) {\n debug(\n \"skipping - content type %s not in %o\",\n contentType,\n config.contentTypes\n );\n return { action: \"continue\" };\n }\n\n // Parse body\n let parsed: unknown;\n try {\n if (!input.body) {\n debug(\"empty body with JSON content type\");\n return {\n action: \"reject\",\n status: config.rejectStatus!,\n code: \"validation_failed\",\n message: \"Request body is empty\",\n };\n }\n const bodyStr =\n typeof input.body === \"string\"\n ? input.body\n : new TextDecoder().decode(input.body);\n parsed = JSON.parse(bodyStr);\n } catch {\n debug(\"body parse failed\");\n return {\n action: \"reject\",\n status: config.rejectStatus!,\n code: \"validation_failed\",\n message: \"Request body is not valid JSON\",\n };\n }\n\n // If no validate function provided, valid JSON parse is sufficient\n if (!config.validate) {\n debug(\"no validator configured - JSON parsed successfully\");\n return { action: \"continue\" };\n }\n\n const result = await config.validate(parsed);\n\n if (!result.valid) {\n const message =\n config.errorDetail && result.errors && result.errors.length > 0\n ? `Validation failed: ${result.errors.join(\"; \")}`\n : \"Validation failed\";\n debug(\"validation failed: %s\", message);\n return {\n action: \"reject\",\n status: config.rejectStatus!,\n code: \"validation_failed\",\n message,\n };\n }\n\n debug(\"validation passed\");\n return { action: \"continue\" };\n },\n },\n});\n"],"mappings":"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","names":[]}
@@ -0,0 +1,33 @@
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 OverrideMethodConfig extends PolicyConfig {
7
+ /** Header name to read the override method from. Default: `"X-HTTP-Method-Override"`. */
8
+ header?: string;
9
+ /** Methods allowed as overrides. Default: `["GET", "PUT", "PATCH", "DELETE"]`. */
10
+ allowedMethods?: string[];
11
+ }
12
+ /**
13
+ * Override the HTTP method of a POST request via a header.
14
+ *
15
+ * Only applies to POST requests - the industry-standard approach for
16
+ * tunneling other methods through POST. Non-POST requests with the
17
+ * override header are ignored.
18
+ *
19
+ * @param config - Header name and allowed override methods.
20
+ * @returns A policy at priority 5 (EARLY).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // Default: reads X-HTTP-Method-Override header
25
+ * overrideMethod();
26
+ *
27
+ * // Custom header and restricted methods
28
+ * overrideMethod({ header: "X-Method", allowedMethods: ["PUT", "PATCH"] });
29
+ * ```
30
+ */
31
+ declare const overrideMethod: (config?: OverrideMethodConfig | undefined) => Policy;
32
+
33
+ export { type OverrideMethodConfig, overrideMethod };
@@ -0,0 +1,48 @@
1
+ import { GatewayError } from "../../core/errors";
2
+ import { definePolicy, Priority } from "../sdk";
3
+ const overrideMethod = /* @__PURE__ */ definePolicy({
4
+ name: "override-method",
5
+ priority: Priority.EARLY,
6
+ phases: ["request-headers"],
7
+ defaults: {
8
+ header: "X-HTTP-Method-Override",
9
+ allowedMethods: ["GET", "PUT", "PATCH", "DELETE"]
10
+ },
11
+ handler: async (c, next, { config, debug }) => {
12
+ const overrideValue = c.req.header(config.header);
13
+ if (!overrideValue) {
14
+ await next();
15
+ return;
16
+ }
17
+ if (c.req.method !== "POST") {
18
+ debug(`ignoring override on ${c.req.method} request`);
19
+ await next();
20
+ return;
21
+ }
22
+ const method = overrideValue.toUpperCase();
23
+ const allowed = new Set(
24
+ (config.allowedMethods ?? []).map((m) => m.toUpperCase())
25
+ );
26
+ if (!allowed.has(method)) {
27
+ throw new GatewayError(
28
+ 400,
29
+ "invalid_method_override",
30
+ `Method override not allowed: ${method}`
31
+ );
32
+ }
33
+ debug(`overriding POST \u2192 ${method}`);
34
+ const newReq = new Request(c.req.url, {
35
+ method,
36
+ headers: c.req.raw.headers,
37
+ body: c.req.raw.body,
38
+ // @ts-expect-error -- duplex is required for streams but not in all type definitions
39
+ duplex: "half"
40
+ });
41
+ Object.defineProperty(c.req, "raw", { value: newReq, configurable: true });
42
+ await next();
43
+ }
44
+ });
45
+ export {
46
+ overrideMethod
47
+ };
48
+ //# sourceMappingURL=override-method.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/transform/override-method.ts"],"sourcesContent":["/**\n * HTTP method override policy.\n *\n * Allows clients to tunnel PUT/PATCH/DELETE through POST requests\n * using a configurable header (default `X-HTTP-Method-Override`).\n * This is useful when firewalls, proxies, or browser limitations\n * prevent the use of certain HTTP methods.\n *\n * @module override-method\n */\nimport { GatewayError } from \"../../core/errors\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface OverrideMethodConfig extends PolicyConfig {\n /** Header name to read the override method from. Default: `\"X-HTTP-Method-Override\"`. */\n header?: string;\n /** Methods allowed as overrides. Default: `[\"GET\", \"PUT\", \"PATCH\", \"DELETE\"]`. */\n allowedMethods?: string[];\n}\n\n/**\n * Override the HTTP method of a POST request via a header.\n *\n * Only applies to POST requests - the industry-standard approach for\n * tunneling other methods through POST. Non-POST requests with the\n * override header are ignored.\n *\n * @param config - Header name and allowed override methods.\n * @returns A policy at priority 5 (EARLY).\n *\n * @example\n * ```ts\n * // Default: reads X-HTTP-Method-Override header\n * overrideMethod();\n *\n * // Custom header and restricted methods\n * overrideMethod({ header: \"X-Method\", allowedMethods: [\"PUT\", \"PATCH\"] });\n * ```\n */\nexport const overrideMethod = /*#__PURE__*/ definePolicy<OverrideMethodConfig>({\n name: \"override-method\",\n priority: Priority.EARLY,\n phases: [\"request-headers\"],\n defaults: {\n header: \"X-HTTP-Method-Override\",\n allowedMethods: [\"GET\", \"PUT\", \"PATCH\", \"DELETE\"],\n },\n handler: async (c, next, { config, debug }) => {\n const overrideValue = c.req.header(config.header!);\n\n if (!overrideValue) {\n await next();\n return;\n }\n\n // Only override POST requests\n if (c.req.method !== \"POST\") {\n debug(`ignoring override on ${c.req.method} request`);\n await next();\n return;\n }\n\n const method = overrideValue.toUpperCase();\n const allowed = new Set(\n (config.allowedMethods ?? []).map((m) => m.toUpperCase())\n );\n\n if (!allowed.has(method)) {\n throw new GatewayError(\n 400,\n \"invalid_method_override\",\n `Method override not allowed: ${method}`\n );\n }\n\n debug(`overriding POST → ${method}`);\n\n // Override the method by replacing the raw Request object\n const newReq = new Request(c.req.url, {\n method,\n headers: c.req.raw.headers,\n body: c.req.raw.body,\n // @ts-expect-error -- duplex is required for streams but not in all type definitions\n duplex: \"half\",\n });\n Object.defineProperty(c.req, \"raw\", { value: newReq, configurable: true });\n\n await next();\n },\n});\n"],"mappings":"AAUA,SAAS,oBAAoB;AAC7B,SAAS,cAAc,gBAAgB;AA6BhC,MAAM,iBAA+B,6BAAmC;AAAA,EAC7E,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,QAAQ,CAAC,iBAAiB;AAAA,EAC1B,UAAU;AAAA,IACR,QAAQ;AAAA,IACR,gBAAgB,CAAC,OAAO,OAAO,SAAS,QAAQ;AAAA,EAClD;AAAA,EACA,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC7C,UAAM,gBAAgB,EAAE,IAAI,OAAO,OAAO,MAAO;AAEjD,QAAI,CAAC,eAAe;AAClB,YAAM,KAAK;AACX;AAAA,IACF;AAGA,QAAI,EAAE,IAAI,WAAW,QAAQ;AAC3B,YAAM,wBAAwB,EAAE,IAAI,MAAM,UAAU;AACpD,YAAM,KAAK;AACX;AAAA,IACF;AAEA,UAAM,SAAS,cAAc,YAAY;AACzC,UAAM,UAAU,IAAI;AAAA,OACjB,OAAO,kBAAkB,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AAAA,IAC1D;AAEA,QAAI,CAAC,QAAQ,IAAI,MAAM,GAAG;AACxB,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA,gCAAgC,MAAM;AAAA,MACxC;AAAA,IACF;AAEA,UAAM,0BAAqB,MAAM,EAAE;AAGnC,UAAM,SAAS,IAAI,QAAQ,EAAE,IAAI,KAAK;AAAA,MACpC;AAAA,MACA,SAAS,EAAE,IAAI,IAAI;AAAA,MACnB,MAAM,EAAE,IAAI,IAAI;AAAA;AAAA,MAEhB,QAAQ;AAAA,IACV,CAAC;AACD,WAAO,eAAe,EAAE,KAAK,OAAO,EAAE,OAAO,QAAQ,cAAc,KAAK,CAAC;AAEzE,UAAM,KAAK;AAAA,EACb;AACF,CAAC;","names":[]}
@@ -0,0 +1,59 @@
1
+ import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
2
+ import 'hono';
3
+ import '../sdk/trace.js';
4
+ import '@vivero/stoma-core';
5
+
6
+ /** Result shape returned by validation functions that provide error details. */
7
+ interface ValidationResult {
8
+ valid: boolean;
9
+ errors?: string[];
10
+ }
11
+ interface RequestValidationConfig extends PolicyConfig {
12
+ /**
13
+ * Synchronous validation function.
14
+ * Return `true`/`false` or an object with optional error details.
15
+ */
16
+ validate?: (body: unknown) => boolean | ValidationResult;
17
+ /**
18
+ * Async validation function (e.g., for remote schema validation).
19
+ * If both `validate` and `validateAsync` are provided, `validateAsync` takes precedence.
20
+ */
21
+ validateAsync?: (body: unknown) => Promise<boolean | ValidationResult>;
22
+ /**
23
+ * Only validate these content types.
24
+ * Requests with other content types pass through without validation.
25
+ * Default: `["application/json"]`.
26
+ */
27
+ contentTypes?: string[];
28
+ /** Custom error message prefix. Default: `"Request validation failed"`. */
29
+ errorMessage?: string;
30
+ }
31
+ /**
32
+ * Pluggable request body validation policy.
33
+ *
34
+ * Validates the request body using a user-provided sync or async function.
35
+ * Requests with content types not in the configured list pass through
36
+ * without validation.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { requestValidation } from "@vivero/stoma";
41
+ *
42
+ * // Simple boolean validator
43
+ * requestValidation({
44
+ * validate: (body) => body != null && typeof body === "object",
45
+ * });
46
+ *
47
+ * // Detailed validation with error messages
48
+ * requestValidation({
49
+ * validate: (body) => {
50
+ * const errors: string[] = [];
51
+ * if (!body || typeof body !== "object") errors.push("Body must be an object");
52
+ * return { valid: errors.length === 0, errors };
53
+ * },
54
+ * });
55
+ * ```
56
+ */
57
+ declare const requestValidation: (config?: RequestValidationConfig | undefined) => Policy;
58
+
59
+ export { type RequestValidationConfig, requestValidation };