@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,38 @@
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 ResourceFilterConfig extends PolicyConfig {
7
+ /** Filter mode: "deny" removes listed fields, "allow" keeps only listed fields */
8
+ mode: "allow" | "deny";
9
+ /** Field paths to filter. Supports dot-notation (e.g. "user.password") */
10
+ fields: string[];
11
+ /** Content types to filter. Default: ["application/json"] */
12
+ contentTypes?: string[];
13
+ /** Apply filtering to array items. Default: true */
14
+ applyToArrayItems?: boolean;
15
+ }
16
+ /**
17
+ * Strip or allow fields from JSON responses.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { resourceFilter } from "@vivero/stoma";
22
+ *
23
+ * // Remove sensitive fields
24
+ * resourceFilter({
25
+ * mode: "deny",
26
+ * fields: ["password", "user.ssn"],
27
+ * });
28
+ *
29
+ * // Keep only specific fields
30
+ * resourceFilter({
31
+ * mode: "allow",
32
+ * fields: ["id", "name", "email"],
33
+ * });
34
+ * ```
35
+ */
36
+ declare const resourceFilter: (config: ResourceFilterConfig) => Policy;
37
+
38
+ export { type ResourceFilterConfig, resourceFilter };
@@ -0,0 +1,184 @@
1
+ import { definePolicy, Priority } from "../sdk";
2
+ function deleteField(obj, path) {
3
+ const parts = path.split(".");
4
+ let current = obj;
5
+ for (let i = 0; i < parts.length - 1; i++) {
6
+ if (current == null || typeof current !== "object") return;
7
+ current = current[parts[i]];
8
+ }
9
+ if (current != null && typeof current === "object") {
10
+ delete current[parts[parts.length - 1]];
11
+ }
12
+ }
13
+ function allowFields(obj, paths) {
14
+ const result = {};
15
+ for (const path of paths) {
16
+ const parts = path.split(".");
17
+ let source = obj;
18
+ let target = result;
19
+ for (let i = 0; i < parts.length; i++) {
20
+ if (source == null || typeof source !== "object") break;
21
+ if (i === parts.length - 1) {
22
+ if (parts[i] in source) {
23
+ target[parts[i]] = source[parts[i]];
24
+ }
25
+ } else {
26
+ if (!(parts[i] in target)) {
27
+ target[parts[i]] = {};
28
+ }
29
+ target = target[parts[i]];
30
+ source = source[parts[i]];
31
+ }
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ function filterObject(obj, mode, fields) {
37
+ if (mode === "allow") {
38
+ return allowFields(obj, fields);
39
+ }
40
+ const clone = structuredClone(obj);
41
+ for (const field of fields) {
42
+ deleteField(clone, field);
43
+ }
44
+ return clone;
45
+ }
46
+ const resourceFilter = /* @__PURE__ */ definePolicy({
47
+ name: "resource-filter",
48
+ priority: Priority.RESPONSE_TRANSFORM,
49
+ phases: ["response-body"],
50
+ defaults: {
51
+ contentTypes: ["application/json"],
52
+ applyToArrayItems: true
53
+ },
54
+ handler: async (c, next, { config, debug }) => {
55
+ await next();
56
+ if (config.fields.length === 0) {
57
+ debug("no fields configured - passing through");
58
+ return;
59
+ }
60
+ const contentType = c.res.headers.get("content-type") ?? "";
61
+ const matchedType = config.contentTypes.some(
62
+ (ct) => contentType.includes(ct)
63
+ );
64
+ if (!matchedType) {
65
+ debug(
66
+ "skipping - response content type %s not in %o",
67
+ contentType,
68
+ config.contentTypes
69
+ );
70
+ return;
71
+ }
72
+ let body;
73
+ try {
74
+ const text = await c.res.text();
75
+ body = JSON.parse(text);
76
+ } catch {
77
+ debug("response body is not valid JSON - passing through");
78
+ return;
79
+ }
80
+ let filtered;
81
+ if (Array.isArray(body)) {
82
+ if (config.applyToArrayItems) {
83
+ filtered = body.map(
84
+ (item) => item != null && typeof item === "object" ? filterObject(
85
+ item,
86
+ config.mode,
87
+ config.fields
88
+ ) : item
89
+ );
90
+ } else {
91
+ filtered = body;
92
+ }
93
+ } else if (body != null && typeof body === "object") {
94
+ filtered = filterObject(
95
+ body,
96
+ config.mode,
97
+ config.fields
98
+ );
99
+ } else {
100
+ filtered = body;
101
+ }
102
+ debug(
103
+ "filtered response with mode=%s fields=%o",
104
+ config.mode,
105
+ config.fields
106
+ );
107
+ c.res = new Response(JSON.stringify(filtered), {
108
+ status: c.res.status,
109
+ headers: c.res.headers
110
+ });
111
+ },
112
+ evaluate: {
113
+ onResponse: async (input, { config, debug }) => {
114
+ if (config.fields.length === 0) {
115
+ debug("no fields configured - passing through");
116
+ return { action: "continue" };
117
+ }
118
+ const contentType = input.headers.get("content-type") ?? "";
119
+ const matchedType = config.contentTypes.some(
120
+ (ct) => contentType.includes(ct)
121
+ );
122
+ if (!matchedType) {
123
+ debug(
124
+ "skipping - response content type %s not in %o",
125
+ contentType,
126
+ config.contentTypes
127
+ );
128
+ return { action: "continue" };
129
+ }
130
+ let body;
131
+ try {
132
+ if (!input.body) {
133
+ return { action: "continue" };
134
+ }
135
+ const bodyStr = typeof input.body === "string" ? input.body : new TextDecoder().decode(input.body);
136
+ body = JSON.parse(bodyStr);
137
+ } catch {
138
+ debug("response body is not valid JSON - passing through");
139
+ return { action: "continue" };
140
+ }
141
+ let filtered;
142
+ if (Array.isArray(body)) {
143
+ if (config.applyToArrayItems) {
144
+ filtered = body.map(
145
+ (item) => item != null && typeof item === "object" ? filterObject(
146
+ item,
147
+ config.mode,
148
+ config.fields
149
+ ) : item
150
+ );
151
+ } else {
152
+ filtered = body;
153
+ }
154
+ } else if (body != null && typeof body === "object") {
155
+ filtered = filterObject(
156
+ body,
157
+ config.mode,
158
+ config.fields
159
+ );
160
+ } else {
161
+ filtered = body;
162
+ }
163
+ debug(
164
+ "filtered response with mode=%s fields=%o",
165
+ config.mode,
166
+ config.fields
167
+ );
168
+ return {
169
+ action: "continue",
170
+ mutations: [
171
+ {
172
+ type: "body",
173
+ op: "replace",
174
+ content: JSON.stringify(filtered)
175
+ }
176
+ ]
177
+ };
178
+ }
179
+ }
180
+ });
181
+ export {
182
+ resourceFilter
183
+ };
184
+ //# sourceMappingURL=resource-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/traffic/resource-filter.ts"],"sourcesContent":["/**\n * Resource filter policy - strip or allow fields from JSON responses.\n *\n * Runs after the upstream response and modifies the JSON body by either\n * removing specified fields (deny mode) or keeping only specified fields\n * (allow mode). Supports dot-notation for nested field paths.\n *\n * @module resource-filter\n */\n\nimport type { Mutation } from \"../../core/protocol\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface ResourceFilterConfig extends PolicyConfig {\n /** Filter mode: \"deny\" removes listed fields, \"allow\" keeps only listed fields */\n mode: \"allow\" | \"deny\";\n /** Field paths to filter. Supports dot-notation (e.g. \"user.password\") */\n fields: string[];\n /** Content types to filter. Default: [\"application/json\"] */\n contentTypes?: string[];\n /** Apply filtering to array items. Default: true */\n applyToArrayItems?: boolean;\n}\n\n/**\n * Delete a nested field from an object using dot-notation path.\n */\nfunction deleteField(obj: Record<string, unknown>, path: string): void {\n const parts = path.split(\".\");\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (current == null || typeof current !== \"object\") return;\n current = current[parts[i]] as Record<string, unknown>;\n }\n if (current != null && typeof current === \"object\") {\n delete current[parts[parts.length - 1]];\n }\n}\n\n/**\n * Build a new object containing only the specified field paths.\n */\nfunction allowFields(\n obj: Record<string, unknown>,\n paths: string[]\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const path of paths) {\n const parts = path.split(\".\");\n let source: Record<string, unknown> = obj;\n let target: Record<string, unknown> = result;\n for (let i = 0; i < parts.length; i++) {\n if (source == null || typeof source !== \"object\") break;\n if (i === parts.length - 1) {\n if (parts[i] in source) {\n target[parts[i]] = source[parts[i]];\n }\n } else {\n if (!(parts[i] in target)) {\n target[parts[i]] = {};\n }\n target = target[parts[i]] as Record<string, unknown>;\n source = source[parts[i]] as Record<string, unknown>;\n }\n }\n }\n return result;\n}\n\n/**\n * Apply field filtering to a single object based on mode.\n */\nfunction filterObject(\n obj: Record<string, unknown>,\n mode: \"allow\" | \"deny\",\n fields: string[]\n): Record<string, unknown> {\n if (mode === \"allow\") {\n return allowFields(obj, fields);\n }\n // deny mode - clone shallowly and delete fields\n const clone = structuredClone(obj);\n for (const field of fields) {\n deleteField(clone, field);\n }\n return clone;\n}\n\n/**\n * Strip or allow fields from JSON responses.\n *\n * @example\n * ```ts\n * import { resourceFilter } from \"@vivero/stoma\";\n *\n * // Remove sensitive fields\n * resourceFilter({\n * mode: \"deny\",\n * fields: [\"password\", \"user.ssn\"],\n * });\n *\n * // Keep only specific fields\n * resourceFilter({\n * mode: \"allow\",\n * fields: [\"id\", \"name\", \"email\"],\n * });\n * ```\n */\nexport const resourceFilter = /*#__PURE__*/ definePolicy<ResourceFilterConfig>({\n name: \"resource-filter\",\n priority: Priority.RESPONSE_TRANSFORM,\n phases: [\"response-body\"],\n defaults: {\n contentTypes: [\"application/json\"],\n applyToArrayItems: true,\n },\n handler: async (c, next, { config, debug }) => {\n await next();\n\n if (config.fields.length === 0) {\n debug(\"no fields configured - passing through\");\n return;\n }\n\n const contentType = c.res.headers.get(\"content-type\") ?? \"\";\n const matchedType = config.contentTypes!.some((ct) =>\n contentType.includes(ct)\n );\n\n if (!matchedType) {\n debug(\n \"skipping - response content type %s not in %o\",\n contentType,\n config.contentTypes\n );\n return;\n }\n\n let body: unknown;\n try {\n const text = await c.res.text();\n body = JSON.parse(text);\n } catch {\n debug(\"response body is not valid JSON - passing through\");\n return;\n }\n\n let filtered: unknown;\n if (Array.isArray(body)) {\n if (config.applyToArrayItems) {\n filtered = body.map((item) =>\n item != null && typeof item === \"object\"\n ? filterObject(\n item as Record<string, unknown>,\n config.mode,\n config.fields\n )\n : item\n );\n } else {\n // applyToArrayItems: false - don't filter array items\n filtered = body;\n }\n } else if (body != null && typeof body === \"object\") {\n filtered = filterObject(\n body as Record<string, unknown>,\n config.mode,\n config.fields\n );\n } else {\n // Primitive JSON value - nothing to filter\n filtered = body;\n }\n\n debug(\n \"filtered response with mode=%s fields=%o\",\n config.mode,\n config.fields\n );\n\n c.res = new Response(JSON.stringify(filtered), {\n status: c.res.status,\n headers: c.res.headers,\n });\n },\n evaluate: {\n onResponse: async (input, { config, debug }) => {\n if (config.fields.length === 0) {\n debug(\"no fields configured - passing through\");\n return { action: \"continue\" };\n }\n\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 - response content type %s not in %o\",\n contentType,\n config.contentTypes\n );\n return { action: \"continue\" };\n }\n\n // Parse body\n let body: unknown;\n try {\n if (!input.body) {\n return { action: \"continue\" };\n }\n const bodyStr =\n typeof input.body === \"string\"\n ? input.body\n : new TextDecoder().decode(input.body);\n body = JSON.parse(bodyStr);\n } catch {\n debug(\"response body is not valid JSON - passing through\");\n return { action: \"continue\" };\n }\n\n let filtered: unknown;\n if (Array.isArray(body)) {\n if (config.applyToArrayItems) {\n filtered = body.map((item) =>\n item != null && typeof item === \"object\"\n ? filterObject(\n item as Record<string, unknown>,\n config.mode,\n config.fields\n )\n : item\n );\n } else {\n filtered = body;\n }\n } else if (body != null && typeof body === \"object\") {\n filtered = filterObject(\n body as Record<string, unknown>,\n config.mode,\n config.fields\n );\n } else {\n filtered = body;\n }\n\n debug(\n \"filtered response with mode=%s fields=%o\",\n config.mode,\n config.fields\n );\n\n return {\n action: \"continue\",\n mutations: [\n {\n type: \"body\",\n op: \"replace\",\n content: JSON.stringify(filtered),\n } as Mutation,\n ],\n };\n },\n },\n});\n"],"mappings":"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","names":[]}
@@ -0,0 +1,27 @@
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 SslEnforceConfig extends PolicyConfig {
7
+ /** Redirect HTTP to HTTPS (301). If false, block with 403. Default: true. */
8
+ redirect?: boolean;
9
+ /** HSTS max-age in seconds. Default: 31536000 (1 year). */
10
+ hstsMaxAge?: number;
11
+ /** Add includeSubDomains to HSTS header. Default: false. */
12
+ includeSubDomains?: boolean;
13
+ /** Add preload to HSTS header. Default: false. */
14
+ preload?: boolean;
15
+ }
16
+ /**
17
+ * Enforce HTTPS and append HSTS headers on secure responses.
18
+ *
19
+ * Detects protocol from `x-forwarded-proto` (or request URL protocol).
20
+ * For non-HTTPS requests, either redirects to HTTPS (301) or throws 403.
21
+ *
22
+ * @param config - Redirect behavior and HSTS settings.
23
+ * @returns A {@link Policy} at priority 5 (EARLY).
24
+ */
25
+ declare const sslEnforce: (config?: SslEnforceConfig | undefined) => Policy;
26
+
27
+ export { type SslEnforceConfig, sslEnforce };
@@ -0,0 +1,38 @@
1
+ import { GatewayError } from "../../core/errors";
2
+ import { definePolicy, Priority } from "../sdk";
3
+ const sslEnforce = /* @__PURE__ */ definePolicy({
4
+ name: "ssl-enforce",
5
+ priority: Priority.EARLY,
6
+ httpOnly: true,
7
+ defaults: {
8
+ redirect: true,
9
+ hstsMaxAge: 31536e3,
10
+ includeSubDomains: false,
11
+ preload: false
12
+ },
13
+ handler: async (c, next, { config }) => {
14
+ const proto = c.req.header("x-forwarded-proto") ?? new URL(c.req.url).protocol.replace(":", "");
15
+ const isHttps = proto === "https";
16
+ if (!isHttps) {
17
+ if (config.redirect) {
18
+ const url = new URL(c.req.url);
19
+ url.protocol = "https:";
20
+ c.res = new Response(null, {
21
+ status: 301,
22
+ headers: { Location: url.toString() }
23
+ });
24
+ return;
25
+ }
26
+ throw new GatewayError(403, "ssl_required", "HTTPS is required");
27
+ }
28
+ await next();
29
+ let hsts = `max-age=${config.hstsMaxAge}`;
30
+ if (config.includeSubDomains) hsts += "; includeSubDomains";
31
+ if (config.preload) hsts += "; preload";
32
+ c.res.headers.set("Strict-Transport-Security", hsts);
33
+ }
34
+ });
35
+ export {
36
+ sslEnforce
37
+ };
38
+ //# sourceMappingURL=ssl-enforce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/traffic/ssl-enforce.ts"],"sourcesContent":["/**\n * SSL enforcement policy - redirect or reject non-HTTPS requests.\n *\n * @module ssl-enforce\n */\n\nimport { GatewayError } from \"../../core/errors\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface SslEnforceConfig extends PolicyConfig {\n /** Redirect HTTP to HTTPS (301). If false, block with 403. Default: true. */\n redirect?: boolean;\n /** HSTS max-age in seconds. Default: 31536000 (1 year). */\n hstsMaxAge?: number;\n /** Add includeSubDomains to HSTS header. Default: false. */\n includeSubDomains?: boolean;\n /** Add preload to HSTS header. Default: false. */\n preload?: boolean;\n}\n\n/**\n * Enforce HTTPS and append HSTS headers on secure responses.\n *\n * Detects protocol from `x-forwarded-proto` (or request URL protocol).\n * For non-HTTPS requests, either redirects to HTTPS (301) or throws 403.\n *\n * @param config - Redirect behavior and HSTS settings.\n * @returns A {@link Policy} at priority 5 (EARLY).\n */\nexport const sslEnforce = /*#__PURE__*/ definePolicy<SslEnforceConfig>({\n name: \"ssl-enforce\",\n priority: Priority.EARLY,\n httpOnly: true,\n defaults: {\n redirect: true,\n hstsMaxAge: 31536000,\n includeSubDomains: false,\n preload: false,\n },\n handler: async (c, next, { config }) => {\n const proto =\n c.req.header(\"x-forwarded-proto\") ??\n new URL(c.req.url).protocol.replace(\":\", \"\");\n const isHttps = proto === \"https\";\n\n if (!isHttps) {\n if (config.redirect) {\n const url = new URL(c.req.url);\n url.protocol = \"https:\";\n c.res = new Response(null, {\n status: 301,\n headers: { Location: url.toString() },\n });\n return;\n }\n throw new GatewayError(403, \"ssl_required\", \"HTTPS is required\");\n }\n\n await next();\n\n let hsts = `max-age=${config.hstsMaxAge}`;\n if (config.includeSubDomains) hsts += \"; includeSubDomains\";\n if (config.preload) hsts += \"; preload\";\n c.res.headers.set(\"Strict-Transport-Security\", hsts);\n },\n});\n"],"mappings":"AAMA,SAAS,oBAAoB;AAC7B,SAAS,cAAc,gBAAgB;AAuBhC,MAAM,aAA2B,6BAA+B;AAAA,EACrE,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,UAAU;AAAA,EACV,UAAU;AAAA,IACR,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,mBAAmB;AAAA,IACnB,SAAS;AAAA,EACX;AAAA,EACA,SAAS,OAAO,GAAG,MAAM,EAAE,OAAO,MAAM;AACtC,UAAM,QACJ,EAAE,IAAI,OAAO,mBAAmB,KAChC,IAAI,IAAI,EAAE,IAAI,GAAG,EAAE,SAAS,QAAQ,KAAK,EAAE;AAC7C,UAAM,UAAU,UAAU;AAE1B,QAAI,CAAC,SAAS;AACZ,UAAI,OAAO,UAAU;AACnB,cAAM,MAAM,IAAI,IAAI,EAAE,IAAI,GAAG;AAC7B,YAAI,WAAW;AACf,UAAE,MAAM,IAAI,SAAS,MAAM;AAAA,UACzB,QAAQ;AAAA,UACR,SAAS,EAAE,UAAU,IAAI,SAAS,EAAE;AAAA,QACtC,CAAC;AACD;AAAA,MACF;AACA,YAAM,IAAI,aAAa,KAAK,gBAAgB,mBAAmB;AAAA,IACjE;AAEA,UAAM,KAAK;AAEX,QAAI,OAAO,WAAW,OAAO,UAAU;AACvC,QAAI,OAAO,kBAAmB,SAAQ;AACtC,QAAI,OAAO,QAAS,SAAQ;AAC5B,MAAE,IAAI,QAAQ,IAAI,6BAA6B,IAAI;AAAA,EACrD;AACF,CAAC;","names":[]}
@@ -0,0 +1,40 @@
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 TrafficShadowConfig extends PolicyConfig {
7
+ /** URL of the shadow upstream (required). */
8
+ target: string;
9
+ /** Percentage of traffic to mirror, 0-100. Default: `100`. */
10
+ percentage?: number;
11
+ /** Only mirror these HTTP methods. Default: `["GET", "POST", "PUT", "PATCH", "DELETE"]`. */
12
+ methods?: string[];
13
+ /** Include request body in shadow request. Default: `true`. */
14
+ mirrorBody?: boolean;
15
+ /** Timeout for shadow request in ms. Default: `5000`. */
16
+ timeout?: number;
17
+ /** Optional error handler for shadow failures. Default: silent. */
18
+ onError?: (error: unknown) => void;
19
+ }
20
+ /**
21
+ * Traffic shadow policy.
22
+ *
23
+ * Mirrors traffic to a secondary upstream after the primary response
24
+ * is ready. The shadow request is fire-and-forget and never affects
25
+ * the primary response.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { trafficShadow } from "@vivero/stoma";
30
+ *
31
+ * trafficShadow({
32
+ * target: "https://shadow.internal",
33
+ * percentage: 10,
34
+ * methods: ["POST", "PUT"],
35
+ * });
36
+ * ```
37
+ */
38
+ declare const trafficShadow: (config: TrafficShadowConfig) => Policy;
39
+
40
+ export { type TrafficShadowConfig, trafficShadow };
@@ -0,0 +1,87 @@
1
+ import { getGatewayContext } from "../../core/pipeline";
2
+ import { definePolicy, Priority } from "../sdk";
3
+ const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
4
+ "connection",
5
+ "keep-alive",
6
+ "proxy-authenticate",
7
+ "proxy-authorization",
8
+ "te",
9
+ "trailer",
10
+ "transfer-encoding",
11
+ "upgrade"
12
+ ]);
13
+ const trafficShadow = /* @__PURE__ */ definePolicy({
14
+ name: "traffic-shadow",
15
+ priority: Priority.RESPONSE_TRANSFORM,
16
+ httpOnly: true,
17
+ defaults: {
18
+ target: "",
19
+ percentage: 100,
20
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
21
+ mirrorBody: true,
22
+ timeout: 5e3
23
+ },
24
+ handler: async (c, next, { config, debug }) => {
25
+ let shadowBody = null;
26
+ if (config.mirrorBody) {
27
+ try {
28
+ const cloned = c.req.raw.clone();
29
+ shadowBody = await cloned.arrayBuffer();
30
+ if (shadowBody.byteLength === 0) {
31
+ shadowBody = null;
32
+ }
33
+ } catch {
34
+ shadowBody = null;
35
+ }
36
+ }
37
+ await next();
38
+ const method = c.req.method.toUpperCase();
39
+ const allowedMethods = new Set(
40
+ (config.methods ?? []).map((m) => m.toUpperCase())
41
+ );
42
+ if (!allowedMethods.has(method)) {
43
+ debug("method %s not in shadow methods - skipping", method);
44
+ return;
45
+ }
46
+ const roll = Math.random() * 100;
47
+ if (roll >= (config.percentage ?? 100)) {
48
+ debug("rolled %.1f >= %d%% - skipping shadow", roll, config.percentage);
49
+ return;
50
+ }
51
+ const originalUrl = new URL(c.req.url);
52
+ const targetBase = config.target.replace(/\/$/, "");
53
+ const shadowUrl = `${targetBase}${originalUrl.pathname}${originalUrl.search}`;
54
+ const headers = new Headers();
55
+ for (const [key, value] of c.req.raw.headers.entries()) {
56
+ if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
57
+ headers.set(key, value);
58
+ }
59
+ }
60
+ debug("shadowing %s %s \u2192 %s", method, originalUrl.pathname, shadowUrl);
61
+ const controller = new AbortController();
62
+ const timeoutId = setTimeout(
63
+ () => controller.abort(),
64
+ config.timeout ?? 5e3
65
+ );
66
+ const shadowPromise = fetch(shadowUrl, {
67
+ method,
68
+ headers,
69
+ body: config.mirrorBody && shadowBody ? shadowBody : void 0,
70
+ signal: controller.signal,
71
+ redirect: "manual"
72
+ }).catch((err) => {
73
+ debug("shadow request failed: %s", String(err));
74
+ config.onError?.(err);
75
+ }).finally(() => {
76
+ clearTimeout(timeoutId);
77
+ });
78
+ const ctx = getGatewayContext(c);
79
+ if (ctx?.adapter?.waitUntil) {
80
+ ctx.adapter.waitUntil(shadowPromise);
81
+ }
82
+ }
83
+ });
84
+ export {
85
+ trafficShadow
86
+ };
87
+ //# sourceMappingURL=traffic-shadow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/traffic/traffic-shadow.ts"],"sourcesContent":["/**\n * Traffic shadow (mirroring) policy.\n *\n * Mirrors a configurable percentage of traffic to a secondary upstream\n * without affecting the primary response. Shadow requests are fire-and-forget.\n *\n * @module traffic-shadow\n */\nimport { getGatewayContext } from \"../../core/pipeline\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\n/** Headers that must not be forwarded to the shadow upstream. */\nconst HOP_BY_HOP_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\nexport interface TrafficShadowConfig extends PolicyConfig {\n /** URL of the shadow upstream (required). */\n target: string;\n /** Percentage of traffic to mirror, 0-100. Default: `100`. */\n percentage?: number;\n /** Only mirror these HTTP methods. Default: `[\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"]`. */\n methods?: string[];\n /** Include request body in shadow request. Default: `true`. */\n mirrorBody?: boolean;\n /** Timeout for shadow request in ms. Default: `5000`. */\n timeout?: number;\n /** Optional error handler for shadow failures. Default: silent. */\n onError?: (error: unknown) => void;\n}\n\n/**\n * Traffic shadow policy.\n *\n * Mirrors traffic to a secondary upstream after the primary response\n * is ready. The shadow request is fire-and-forget and never affects\n * the primary response.\n *\n * @example\n * ```ts\n * import { trafficShadow } from \"@vivero/stoma\";\n *\n * trafficShadow({\n * target: \"https://shadow.internal\",\n * percentage: 10,\n * methods: [\"POST\", \"PUT\"],\n * });\n * ```\n */\nexport const trafficShadow = /*#__PURE__*/ definePolicy<TrafficShadowConfig>({\n name: \"traffic-shadow\",\n priority: Priority.RESPONSE_TRANSFORM,\n httpOnly: true,\n defaults: {\n target: \"\",\n percentage: 100,\n methods: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n mirrorBody: true,\n timeout: 5000,\n },\n handler: async (c, next, { config, debug }) => {\n // Clone the request body before next() consumes it\n let shadowBody: ArrayBuffer | null = null;\n if (config.mirrorBody) {\n try {\n const cloned = c.req.raw.clone();\n shadowBody = await cloned.arrayBuffer();\n if (shadowBody.byteLength === 0) {\n shadowBody = null;\n }\n } catch {\n // No body or unreadable - proceed without body\n shadowBody = null;\n }\n }\n\n // Run the primary pipeline first\n await next();\n\n // Determine if this request should be shadowed\n const method = c.req.method.toUpperCase();\n const allowedMethods = new Set(\n (config.methods ?? []).map((m) => m.toUpperCase())\n );\n\n if (!allowedMethods.has(method)) {\n debug(\"method %s not in shadow methods - skipping\", method);\n return;\n }\n\n const roll = Math.random() * 100;\n if (roll >= (config.percentage ?? 100)) {\n debug(\"rolled %.1f >= %d%% - skipping shadow\", roll, config.percentage);\n return;\n }\n\n // Build the shadow URL: target base + original path + query\n const originalUrl = new URL(c.req.url);\n const targetBase = config.target.replace(/\\/$/, \"\");\n const shadowUrl = `${targetBase}${originalUrl.pathname}${originalUrl.search}`;\n\n // Copy headers, stripping hop-by-hop\n const headers = new Headers();\n for (const [key, value] of c.req.raw.headers.entries()) {\n if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {\n headers.set(key, value);\n }\n }\n\n debug(\"shadowing %s %s → %s\", method, originalUrl.pathname, shadowUrl);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n config.timeout ?? 5000\n );\n\n const shadowPromise = fetch(shadowUrl, {\n method,\n headers,\n body: config.mirrorBody && shadowBody ? shadowBody : undefined,\n signal: controller.signal,\n redirect: \"manual\",\n })\n .catch((err) => {\n debug(\"shadow request failed: %s\", String(err));\n config.onError?.(err);\n })\n .finally(() => {\n clearTimeout(timeoutId);\n });\n\n // Use adapter.waitUntil if available, otherwise fire-and-forget.\n const ctx = getGatewayContext(c);\n if (ctx?.adapter?.waitUntil) {\n ctx.adapter.waitUntil(shadowPromise);\n }\n },\n});\n"],"mappings":"AAQA,SAAS,yBAAyB;AAClC,SAAS,cAAc,gBAAgB;AAIvC,MAAM,qBAAqB,oBAAI,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAmCM,MAAM,gBAA8B,6BAAkC;AAAA,EAC3E,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,UAAU;AAAA,EACV,UAAU;AAAA,IACR,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAS,CAAC,OAAO,QAAQ,OAAO,SAAS,QAAQ;AAAA,IACjD,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,MAAM,MAAM;AAE7C,QAAI,aAAiC;AACrC,QAAI,OAAO,YAAY;AACrB,UAAI;AACF,cAAM,SAAS,EAAE,IAAI,IAAI,MAAM;AAC/B,qBAAa,MAAM,OAAO,YAAY;AACtC,YAAI,WAAW,eAAe,GAAG;AAC/B,uBAAa;AAAA,QACf;AAAA,MACF,QAAQ;AAEN,qBAAa;AAAA,MACf;AAAA,IACF;AAGA,UAAM,KAAK;AAGX,UAAM,SAAS,EAAE,IAAI,OAAO,YAAY;AACxC,UAAM,iBAAiB,IAAI;AAAA,OACxB,OAAO,WAAW,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AAAA,IACnD;AAEA,QAAI,CAAC,eAAe,IAAI,MAAM,GAAG;AAC/B,YAAM,8CAA8C,MAAM;AAC1D;AAAA,IACF;AAEA,UAAM,OAAO,KAAK,OAAO,IAAI;AAC7B,QAAI,SAAS,OAAO,cAAc,MAAM;AACtC,YAAM,yCAAyC,MAAM,OAAO,UAAU;AACtE;AAAA,IACF;AAGA,UAAM,cAAc,IAAI,IAAI,EAAE,IAAI,GAAG;AACrC,UAAM,aAAa,OAAO,OAAO,QAAQ,OAAO,EAAE;AAClD,UAAM,YAAY,GAAG,UAAU,GAAG,YAAY,QAAQ,GAAG,YAAY,MAAM;AAG3E,UAAM,UAAU,IAAI,QAAQ;AAC5B,eAAW,CAAC,KAAK,KAAK,KAAK,EAAE,IAAI,IAAI,QAAQ,QAAQ,GAAG;AACtD,UAAI,CAAC,mBAAmB,IAAI,IAAI,YAAY,CAAC,GAAG;AAC9C,gBAAQ,IAAI,KAAK,KAAK;AAAA,MACxB;AAAA,IACF;AAEA,UAAM,6BAAwB,QAAQ,YAAY,UAAU,SAAS;AAErE,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY;AAAA,MAChB,MAAM,WAAW,MAAM;AAAA,MACvB,OAAO,WAAW;AAAA,IACpB;AAEA,UAAM,gBAAgB,MAAM,WAAW;AAAA,MACrC;AAAA,MACA;AAAA,MACA,MAAM,OAAO,cAAc,aAAa,aAAa;AAAA,MACrD,QAAQ,WAAW;AAAA,MACnB,UAAU;AAAA,IACZ,CAAC,EACE,MAAM,CAAC,QAAQ;AACd,YAAM,6BAA6B,OAAO,GAAG,CAAC;AAC9C,aAAO,UAAU,GAAG;AAAA,IACtB,CAAC,EACA,QAAQ,MAAM;AACb,mBAAa,SAAS;AAAA,IACxB,CAAC;AAGH,UAAM,MAAM,kBAAkB,CAAC;AAC/B,QAAI,KAAK,SAAS,WAAW;AAC3B,UAAI,QAAQ,UAAU,aAAa;AAAA,IACrC;AAAA,EACF;AACF,CAAC;","names":[]}
@@ -0,0 +1,33 @@
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 AssignAttributesConfig extends PolicyConfig {
7
+ /**
8
+ * Key-value pairs to set on the Hono context.
9
+ * Values can be static strings or functions that receive the context.
10
+ */
11
+ attributes: Record<string, string | ((c: Context) => string | Promise<string>)>;
12
+ }
13
+ /**
14
+ * Set key-value attributes on the Hono request context.
15
+ *
16
+ * @param config - Must include `attributes` - a record of keys to values or resolver functions.
17
+ * @returns A {@link Policy} at priority 50 (REQUEST_TRANSFORM).
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { assignAttributes } from "@vivero/stoma";
22
+ *
23
+ * assignAttributes({
24
+ * attributes: {
25
+ * "x-tenant": "acme",
26
+ * "x-request-path": (c) => new URL(c.req.url).pathname,
27
+ * },
28
+ * });
29
+ * ```
30
+ */
31
+ declare const assignAttributes: (config: AssignAttributesConfig) => Policy;
32
+
33
+ export { type AssignAttributesConfig, assignAttributes };
@@ -0,0 +1,38 @@
1
+ import { definePolicy, Priority } from "../sdk";
2
+ const assignAttributes = /* @__PURE__ */ definePolicy({
3
+ name: "assign-attributes",
4
+ priority: Priority.REQUEST_TRANSFORM,
5
+ phases: ["request-headers"],
6
+ handler: async (c, next, { config, debug }) => {
7
+ for (const [key, value] of Object.entries(config.attributes)) {
8
+ if (typeof value === "function") {
9
+ const resolved = await value(c);
10
+ c.set(key, resolved);
11
+ debug("set %s = %s (dynamic)", key, resolved);
12
+ } else {
13
+ c.set(key, value);
14
+ debug("set %s = %s (static)", key, value);
15
+ }
16
+ }
17
+ await next();
18
+ },
19
+ evaluate: {
20
+ onRequest: async (_input, { config, debug }) => {
21
+ const mutations = [];
22
+ for (const [key, value] of Object.entries(config.attributes)) {
23
+ const resolved = typeof value === "function" ? value({}) : value;
24
+ debug("set %s = %s", key, resolved);
25
+ mutations.push({
26
+ type: "attribute",
27
+ key,
28
+ value: resolved
29
+ });
30
+ }
31
+ return { action: "continue", mutations };
32
+ }
33
+ }
34
+ });
35
+ export {
36
+ assignAttributes
37
+ };
38
+ //# sourceMappingURL=assign-attributes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/policies/transform/assign-attributes.ts"],"sourcesContent":["/**\n * Assign arbitrary key-value attributes to the Hono context.\n *\n * Downstream middleware and handlers can read the attributes via `c.get(key)`.\n * Values can be static strings or dynamic functions that receive the Hono\n * context (sync or async).\n *\n * @module assign-attributes\n */\nimport type { Context } from \"hono\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface AssignAttributesConfig extends PolicyConfig {\n /**\n * Key-value pairs to set on the Hono context.\n * Values can be static strings or functions that receive the context.\n */\n attributes: Record<\n string,\n string | ((c: Context) => string | Promise<string>)\n >;\n}\n\n/**\n * Set key-value attributes on the Hono request context.\n *\n * @param config - Must include `attributes` - a record of keys to values or resolver functions.\n * @returns A {@link Policy} at priority 50 (REQUEST_TRANSFORM).\n *\n * @example\n * ```ts\n * import { assignAttributes } from \"@vivero/stoma\";\n *\n * assignAttributes({\n * attributes: {\n * \"x-tenant\": \"acme\",\n * \"x-request-path\": (c) => new URL(c.req.url).pathname,\n * },\n * });\n * ```\n */\nexport const assignAttributes =\n /*#__PURE__*/ definePolicy<AssignAttributesConfig>({\n name: \"assign-attributes\",\n priority: Priority.REQUEST_TRANSFORM,\n phases: [\"request-headers\"],\n handler: async (c, next, { config, debug }) => {\n for (const [key, value] of Object.entries(config.attributes)) {\n if (typeof value === \"function\") {\n const resolved = await value(c);\n c.set(key, resolved);\n debug(\"set %s = %s (dynamic)\", key, resolved);\n } else {\n c.set(key, value);\n debug(\"set %s = %s (static)\", key, value);\n }\n }\n\n await next();\n },\n evaluate: {\n onRequest: async (_input, { config, debug }) => {\n const mutations = [];\n for (const [key, value] of Object.entries(config.attributes)) {\n const resolved =\n typeof value === \"function\" ? value({} as Context) : value;\n debug(\"set %s = %s\", key, resolved);\n mutations.push({\n type: \"attribute\" as const,\n key,\n value: resolved,\n });\n }\n return { action: \"continue\", mutations };\n },\n },\n });\n"],"mappings":"AAUA,SAAS,cAAc,gBAAgB;AAgChC,MAAM,mBACG,6BAAqC;AAAA,EACjD,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,QAAQ,CAAC,iBAAiB;AAAA,EAC1B,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC7C,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,UAAU,GAAG;AAC5D,UAAI,OAAO,UAAU,YAAY;AAC/B,cAAM,WAAW,MAAM,MAAM,CAAC;AAC9B,UAAE,IAAI,KAAK,QAAQ;AACnB,cAAM,yBAAyB,KAAK,QAAQ;AAAA,MAC9C,OAAO;AACL,UAAE,IAAI,KAAK,KAAK;AAChB,cAAM,wBAAwB,KAAK,KAAK;AAAA,MAC1C;AAAA,IACF;AAEA,UAAM,KAAK;AAAA,EACb;AAAA,EACA,UAAU;AAAA,IACR,WAAW,OAAO,QAAQ,EAAE,QAAQ,MAAM,MAAM;AAC9C,YAAM,YAAY,CAAC;AACnB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,UAAU,GAAG;AAC5D,cAAM,WACJ,OAAO,UAAU,aAAa,MAAM,CAAC,CAAY,IAAI;AACvD,cAAM,eAAe,KAAK,QAAQ;AAClC,kBAAU,KAAK;AAAA,UACb,MAAM;AAAA,UACN;AAAA,UACA,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AACA,aAAO,EAAE,QAAQ,YAAY,UAAU;AAAA,IACzC;AAAA,EACF;AACF,CAAC;","names":[]}
@@ -0,0 +1,40 @@
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
+ /** A field value - either a static value or a function resolving to one. */
7
+ type FieldValue = unknown | ((c: Context) => unknown | Promise<unknown>);
8
+ interface AssignContentConfig extends PolicyConfig {
9
+ /** Fields to set/override in the JSON request body. */
10
+ request?: Record<string, FieldValue>;
11
+ /** Fields to set/override in the JSON response body. */
12
+ response?: Record<string, FieldValue>;
13
+ /** Only modify bodies with these content types. Default: `["application/json"]`. */
14
+ contentTypes?: string[];
15
+ }
16
+ /**
17
+ * Assign content policy.
18
+ *
19
+ * Injects or overrides fields in JSON request and/or response bodies.
20
+ * Useful for injecting tenant IDs, timestamps, metadata, or other
21
+ * fields that should be transparently added by the gateway.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { assignContent } from "@vivero/stoma";
26
+ *
27
+ * assignContent({
28
+ * request: {
29
+ * tenantId: "acme",
30
+ * timestamp: (c) => new Date().toISOString(),
31
+ * },
32
+ * response: {
33
+ * gateway: "stoma",
34
+ * },
35
+ * });
36
+ * ```
37
+ */
38
+ declare const assignContent: (config?: AssignContentConfig | undefined) => Policy;
39
+
40
+ export { type AssignContentConfig, assignContent };