@vivero/stoma 0.1.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/adapters/bun.d.ts +9 -0
- package/dist/adapters/bun.js +8 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +49 -0
- package/dist/adapters/cloudflare.js +85 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +9 -0
- package/dist/adapters/deno.js +8 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/durable-object.d.ts +63 -0
- package/dist/adapters/durable-object.js +46 -0
- package/dist/adapters/durable-object.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +53 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +9 -0
- package/dist/adapters/memory.js +14 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/node.d.ts +9 -0
- package/dist/adapters/node.js +8 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/postgres.d.ts +109 -0
- package/dist/adapters/postgres.js +242 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/redis.d.ts +116 -0
- package/dist/adapters/redis.js +194 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/testing.d.ts +32 -0
- package/dist/adapters/testing.js +33 -0
- package/dist/adapters/testing.js.map +1 -0
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +21 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +48 -0
- package/dist/config/merge.js +83 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/schema.d.ts +254 -0
- package/dist/config/schema.js +109 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/errors.d.ts +66 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/gateway.d.ts +44 -0
- package/dist/core/gateway.js +400 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health.d.ts +78 -0
- package/dist/core/health.js +65 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/pipeline.d.ts +62 -0
- package/dist/core/pipeline.js +214 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/protocol.d.ts +4 -0
- package/dist/core/protocol.js +1 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/scope.d.ts +67 -0
- package/dist/core/scope.js +44 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +252 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/admin.d.ts +32 -0
- package/dist/observability/admin.js +85 -0
- package/dist/observability/admin.js.map +1 -0
- package/dist/observability/metrics.d.ts +78 -0
- package/dist/observability/metrics.js +107 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/tracing.d.ts +149 -0
- package/dist/observability/tracing.js +191 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/policies/auth/api-key-auth.d.ts +64 -0
- package/dist/policies/auth/api-key-auth.js +93 -0
- package/dist/policies/auth/api-key-auth.js.map +1 -0
- package/dist/policies/auth/basic-auth.d.ts +33 -0
- package/dist/policies/auth/basic-auth.js +96 -0
- package/dist/policies/auth/basic-auth.js.map +1 -0
- package/dist/policies/auth/crypto.d.ts +29 -0
- package/dist/policies/auth/crypto.js +100 -0
- package/dist/policies/auth/crypto.js.map +1 -0
- package/dist/policies/auth/generate-http-signature.d.ts +30 -0
- package/dist/policies/auth/generate-http-signature.js +79 -0
- package/dist/policies/auth/generate-http-signature.js.map +1 -0
- package/dist/policies/auth/generate-jwt.d.ts +44 -0
- package/dist/policies/auth/generate-jwt.js +99 -0
- package/dist/policies/auth/generate-jwt.js.map +1 -0
- package/dist/policies/auth/http-signature-base.d.ts +55 -0
- package/dist/policies/auth/http-signature-base.js +140 -0
- package/dist/policies/auth/http-signature-base.js.map +1 -0
- package/dist/policies/auth/jws.d.ts +46 -0
- package/dist/policies/auth/jws.js +317 -0
- package/dist/policies/auth/jws.js.map +1 -0
- package/dist/policies/auth/jwt-auth.d.ts +64 -0
- package/dist/policies/auth/jwt-auth.js +266 -0
- package/dist/policies/auth/jwt-auth.js.map +1 -0
- package/dist/policies/auth/oauth2.d.ts +38 -0
- package/dist/policies/auth/oauth2.js +254 -0
- package/dist/policies/auth/oauth2.js.map +1 -0
- package/dist/policies/auth/rbac.d.ts +30 -0
- package/dist/policies/auth/rbac.js +115 -0
- package/dist/policies/auth/rbac.js.map +1 -0
- package/dist/policies/auth/verify-http-signature.d.ts +30 -0
- package/dist/policies/auth/verify-http-signature.js +147 -0
- package/dist/policies/auth/verify-http-signature.js.map +1 -0
- package/dist/policies/index.d.ts +51 -0
- package/dist/policies/index.js +109 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/mock.d.ts +60 -0
- package/dist/policies/mock.js +29 -0
- package/dist/policies/mock.js.map +1 -0
- package/dist/policies/observability/assign-metrics.d.ts +37 -0
- package/dist/policies/observability/assign-metrics.js +29 -0
- package/dist/policies/observability/assign-metrics.js.map +1 -0
- package/dist/policies/observability/metrics-reporter.d.ts +25 -0
- package/dist/policies/observability/metrics-reporter.js +62 -0
- package/dist/policies/observability/metrics-reporter.js.map +1 -0
- package/dist/policies/observability/request-log.d.ts +135 -0
- package/dist/policies/observability/request-log.js +134 -0
- package/dist/policies/observability/request-log.js.map +1 -0
- package/dist/policies/observability/server-timing.d.ts +35 -0
- package/dist/policies/observability/server-timing.js +89 -0
- package/dist/policies/observability/server-timing.js.map +1 -0
- package/dist/policies/proxy.d.ts +59 -0
- package/dist/policies/proxy.js +47 -0
- package/dist/policies/proxy.js.map +1 -0
- package/dist/policies/resilience/circuit-breaker.d.ts +4 -0
- package/dist/policies/resilience/circuit-breaker.js +280 -0
- package/dist/policies/resilience/circuit-breaker.js.map +1 -0
- package/dist/policies/resilience/latency-injection.d.ts +35 -0
- package/dist/policies/resilience/latency-injection.js +26 -0
- package/dist/policies/resilience/latency-injection.js.map +1 -0
- package/dist/policies/resilience/retry.d.ts +71 -0
- package/dist/policies/resilience/retry.js +79 -0
- package/dist/policies/resilience/retry.js.map +1 -0
- package/dist/policies/resilience/timeout.d.ts +32 -0
- package/dist/policies/resilience/timeout.js +46 -0
- package/dist/policies/resilience/timeout.js.map +1 -0
- package/dist/policies/sdk/define-policy.d.ts +176 -0
- package/dist/policies/sdk/define-policy.js +42 -0
- package/dist/policies/sdk/define-policy.js.map +1 -0
- package/dist/policies/sdk/helpers.d.ts +132 -0
- package/dist/policies/sdk/helpers.js +87 -0
- package/dist/policies/sdk/helpers.js.map +1 -0
- package/dist/policies/sdk/index.d.ts +10 -0
- package/dist/policies/sdk/index.js +35 -0
- package/dist/policies/sdk/index.js.map +1 -0
- package/dist/policies/sdk/priority.d.ts +44 -0
- package/dist/policies/sdk/priority.js +36 -0
- package/dist/policies/sdk/priority.js.map +1 -0
- package/dist/policies/sdk/testing.d.ts +53 -0
- package/dist/policies/sdk/testing.js +41 -0
- package/dist/policies/sdk/testing.js.map +1 -0
- package/dist/policies/sdk/trace.d.ts +73 -0
- package/dist/policies/sdk/trace.js +25 -0
- package/dist/policies/sdk/trace.js.map +1 -0
- package/dist/policies/traffic/cache.d.ts +4 -0
- package/dist/policies/traffic/cache.js +224 -0
- package/dist/policies/traffic/cache.js.map +1 -0
- package/dist/policies/traffic/dynamic-routing.d.ts +54 -0
- package/dist/policies/traffic/dynamic-routing.js +36 -0
- package/dist/policies/traffic/dynamic-routing.js.map +1 -0
- package/dist/policies/traffic/geo-ip-filter.d.ts +37 -0
- package/dist/policies/traffic/geo-ip-filter.js +74 -0
- package/dist/policies/traffic/geo-ip-filter.js.map +1 -0
- package/dist/policies/traffic/http-callout.d.ts +59 -0
- package/dist/policies/traffic/http-callout.js +69 -0
- package/dist/policies/traffic/http-callout.js.map +1 -0
- package/dist/policies/traffic/interrupt.d.ts +46 -0
- package/dist/policies/traffic/interrupt.js +38 -0
- package/dist/policies/traffic/interrupt.js.map +1 -0
- package/dist/policies/traffic/ip-filter.d.ts +47 -0
- package/dist/policies/traffic/ip-filter.js +57 -0
- package/dist/policies/traffic/ip-filter.js.map +1 -0
- package/dist/policies/traffic/json-threat-protection.d.ts +51 -0
- package/dist/policies/traffic/json-threat-protection.js +173 -0
- package/dist/policies/traffic/json-threat-protection.js.map +1 -0
- package/dist/policies/traffic/rate-limit.d.ts +4 -0
- package/dist/policies/traffic/rate-limit.js +145 -0
- package/dist/policies/traffic/rate-limit.js.map +1 -0
- package/dist/policies/traffic/regex-threat-protection.d.ts +54 -0
- package/dist/policies/traffic/regex-threat-protection.js +109 -0
- package/dist/policies/traffic/regex-threat-protection.js.map +1 -0
- package/dist/policies/traffic/request-limit.d.ts +27 -0
- package/dist/policies/traffic/request-limit.js +41 -0
- package/dist/policies/traffic/request-limit.js.map +1 -0
- package/dist/policies/traffic/resource-filter.d.ts +38 -0
- package/dist/policies/traffic/resource-filter.js +184 -0
- package/dist/policies/traffic/resource-filter.js.map +1 -0
- package/dist/policies/traffic/ssl-enforce.d.ts +27 -0
- package/dist/policies/traffic/ssl-enforce.js +38 -0
- package/dist/policies/traffic/ssl-enforce.js.map +1 -0
- package/dist/policies/traffic/traffic-shadow.d.ts +40 -0
- package/dist/policies/traffic/traffic-shadow.js +87 -0
- package/dist/policies/traffic/traffic-shadow.js.map +1 -0
- package/dist/policies/transform/assign-attributes.d.ts +33 -0
- package/dist/policies/transform/assign-attributes.js +38 -0
- package/dist/policies/transform/assign-attributes.js.map +1 -0
- package/dist/policies/transform/assign-content.d.ts +40 -0
- package/dist/policies/transform/assign-content.js +185 -0
- package/dist/policies/transform/assign-content.js.map +1 -0
- package/dist/policies/transform/cors.d.ts +57 -0
- package/dist/policies/transform/cors.js +23 -0
- package/dist/policies/transform/cors.js.map +1 -0
- package/dist/policies/transform/json-validation.d.ts +50 -0
- package/dist/policies/transform/json-validation.js +125 -0
- package/dist/policies/transform/json-validation.js.map +1 -0
- package/dist/policies/transform/override-method.d.ts +33 -0
- package/dist/policies/transform/override-method.js +48 -0
- package/dist/policies/transform/override-method.js.map +1 -0
- package/dist/policies/transform/request-validation.d.ts +59 -0
- package/dist/policies/transform/request-validation.js +121 -0
- package/dist/policies/transform/request-validation.js.map +1 -0
- package/dist/policies/transform/transform.d.ts +75 -0
- package/dist/policies/transform/transform.js +116 -0
- package/dist/policies/transform/transform.js.map +1 -0
- package/dist/policies/types.d.ts +4 -0
- package/dist/policies/types.js +1 -0
- package/dist/policies/types.js.map +1 -0
- package/dist/protocol-2fD3DJrL.d.ts +725 -0
- package/dist/utils/cidr.d.ts +58 -0
- package/dist/utils/cidr.js +107 -0
- package/dist/utils/cidr.js.map +1 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/debug.js +13 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/headers.d.ts +68 -0
- package/dist/utils/headers.js +25 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/ip.d.ts +64 -0
- package/dist/utils/ip.js +29 -0
- package/dist/utils/ip.js.map +1 -0
- package/dist/utils/redact.d.ts +30 -0
- package/dist/utils/redact.js +52 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/request-id.d.ts +11 -0
- package/dist/utils/request-id.js +7 -0
- package/dist/utils/request-id.js.map +1 -0
- package/dist/utils/timing-safe.d.ts +31 -0
- package/dist/utils/timing-safe.js +17 -0
- package/dist/utils/timing-safe.js.map +1 -0
- package/dist/utils/timing.d.ts +27 -0
- package/dist/utils/timing.js +12 -0
- package/dist/utils/timing.js.map +1 -0
- package/dist/utils/trace-context.d.ts +51 -0
- package/dist/utils/trace-context.js +37 -0
- package/dist/utils/trace-context.js.map +1 -0
- package/package.json +213 -0
|
@@ -0,0 +1,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":"AAWA,SAAS,cAAc,gBAAgB;AAiBvC,SAAS,YAAY,KAA8B,MAAoB;AACrE,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,MAAI,UAAmC;AACvC,WAAS,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,KAAK;AACzC,QAAI,WAAW,QAAQ,OAAO,YAAY,SAAU;AACpD,cAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,EAC5B;AACA,MAAI,WAAW,QAAQ,OAAO,YAAY,UAAU;AAClD,WAAO,QAAQ,MAAM,MAAM,SAAS,CAAC,CAAC;AAAA,EACxC;AACF;AAKA,SAAS,YACP,KACA,OACyB;AACzB,QAAM,SAAkC,CAAC;AACzC,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,QAAI,SAAkC;AACtC,QAAI,SAAkC;AACtC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAI,UAAU,QAAQ,OAAO,WAAW,SAAU;AAClD,UAAI,MAAM,MAAM,SAAS,GAAG;AAC1B,YAAI,MAAM,CAAC,KAAK,QAAQ;AACtB,iBAAO,MAAM,CAAC,CAAC,IAAI,OAAO,MAAM,CAAC,CAAC;AAAA,QACpC;AAAA,MACF,OAAO;AACL,YAAI,EAAE,MAAM,CAAC,KAAK,SAAS;AACzB,iBAAO,MAAM,CAAC,CAAC,IAAI,CAAC;AAAA,QACtB;AACA,iBAAS,OAAO,MAAM,CAAC,CAAC;AACxB,iBAAS,OAAO,MAAM,CAAC,CAAC;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,aACP,KACA,MACA,QACyB;AACzB,MAAI,SAAS,SAAS;AACpB,WAAO,YAAY,KAAK,MAAM;AAAA,EAChC;AAEA,QAAM,QAAQ,gBAAgB,GAAG;AACjC,aAAW,SAAS,QAAQ;AAC1B,gBAAY,OAAO,KAAK;AAAA,EAC1B;AACA,SAAO;AACT;AAsBO,MAAM,iBAA+B,6BAAmC;AAAA,EAC7E,MAAM;AAAA,EACN,UAAU,SAAS;AAAA,EACnB,QAAQ,CAAC,eAAe;AAAA,EACxB,UAAU;AAAA,IACR,cAAc,CAAC,kBAAkB;AAAA,IACjC,mBAAmB;AAAA,EACrB;AAAA,EACA,SAAS,OAAO,GAAG,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC7C,UAAM,KAAK;AAEX,QAAI,OAAO,OAAO,WAAW,GAAG;AAC9B,YAAM,wCAAwC;AAC9C;AAAA,IACF;AAEA,UAAM,cAAc,EAAE,IAAI,QAAQ,IAAI,cAAc,KAAK;AACzD,UAAM,cAAc,OAAO,aAAc;AAAA,MAAK,CAAC,OAC7C,YAAY,SAAS,EAAE;AAAA,IACzB;AAEA,QAAI,CAAC,aAAa;AAChB;AAAA,QACE;AAAA,QACA;AAAA,QACA,OAAO;AAAA,MACT;AACA;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,OAAO,MAAM,EAAE,IAAI,KAAK;AAC9B,aAAO,KAAK,MAAM,IAAI;AAAA,IACxB,QAAQ;AACN,YAAM,mDAAmD;AACzD;AAAA,IACF;AAEA,QAAI;AACJ,QAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,UAAI,OAAO,mBAAmB;AAC5B,mBAAW,KAAK;AAAA,UAAI,CAAC,SACnB,QAAQ,QAAQ,OAAO,SAAS,WAC5B;AAAA,YACE;AAAA,YACA,OAAO;AAAA,YACP,OAAO;AAAA,UACT,IACA;AAAA,QACN;AAAA,MACF,OAAO;AAEL,mBAAW;AAAA,MACb;AAAA,IACF,WAAW,QAAQ,QAAQ,OAAO,SAAS,UAAU;AACnD,iBAAW;AAAA,QACT;AAAA,QACA,OAAO;AAAA,QACP,OAAO;AAAA,MACT;AAAA,IACF,OAAO;AAEL,iBAAW;AAAA,IACb;AAEA;AAAA,MACE;AAAA,MACA,OAAO;AAAA,MACP,OAAO;AAAA,IACT;AAEA,MAAE,MAAM,IAAI,SAAS,KAAK,UAAU,QAAQ,GAAG;AAAA,MAC7C,QAAQ,EAAE,IAAI;AAAA,MACd,SAAS,EAAE,IAAI;AAAA,IACjB,CAAC;AAAA,EACH;AAAA,EACA,UAAU;AAAA,IACR,YAAY,OAAO,OAAO,EAAE,QAAQ,MAAM,MAAM;AAC9C,UAAI,OAAO,OAAO,WAAW,GAAG;AAC9B,cAAM,wCAAwC;AAC9C,eAAO,EAAE,QAAQ,WAAW;AAAA,MAC9B;AAEA,YAAM,cAAc,MAAM,QAAQ,IAAI,cAAc,KAAK;AACzD,YAAM,cAAc,OAAO,aAAc;AAAA,QAAK,CAAC,OAC7C,YAAY,SAAS,EAAE;AAAA,MACzB;AAEA,UAAI,CAAC,aAAa;AAChB;AAAA,UACE;AAAA,UACA;AAAA,UACA,OAAO;AAAA,QACT;AACA,eAAO,EAAE,QAAQ,WAAW;AAAA,MAC9B;AAGA,UAAI;AACJ,UAAI;AACF,YAAI,CAAC,MAAM,MAAM;AACf,iBAAO,EAAE,QAAQ,WAAW;AAAA,QAC9B;AACA,cAAM,UACJ,OAAO,MAAM,SAAS,WAClB,MAAM,OACN,IAAI,YAAY,EAAE,OAAO,MAAM,IAAI;AACzC,eAAO,KAAK,MAAM,OAAO;AAAA,MAC3B,QAAQ;AACN,cAAM,mDAAmD;AACzD,eAAO,EAAE,QAAQ,WAAW;AAAA,MAC9B;AAEA,UAAI;AACJ,UAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,YAAI,OAAO,mBAAmB;AAC5B,qBAAW,KAAK;AAAA,YAAI,CAAC,SACnB,QAAQ,QAAQ,OAAO,SAAS,WAC5B;AAAA,cACE;AAAA,cACA,OAAO;AAAA,cACP,OAAO;AAAA,YACT,IACA;AAAA,UACN;AAAA,QACF,OAAO;AACL,qBAAW;AAAA,QACb;AAAA,MACF,WAAW,QAAQ,QAAQ,OAAO,SAAS,UAAU;AACnD,mBAAW;AAAA,UACT;AAAA,UACA,OAAO;AAAA,UACP,OAAO;AAAA,QACT;AAAA,MACF,OAAO;AACL,mBAAW;AAAA,MACb;AAEA;AAAA,QACE;AAAA,QACA,OAAO;AAAA,QACP,OAAO;AAAA,MACT;AAEA,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW;AAAA,UACT;AAAA,YACE,MAAM;AAAA,YACN,IAAI;AAAA,YACJ,SAAS,KAAK,UAAU,QAAQ;AAAA,UAClC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;","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 };
|