@copilotkit/runtime 1.62.0 → 1.62.2-canary.1783457132
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/dist/package.cjs +2 -2
- package/dist/package.mjs +2 -2
- package/dist/v2/index.cjs +5 -1
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/index.mjs +2 -2
- package/dist/v2/runtime/core/fetch-handler.cjs +27 -0
- package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.mjs +27 -0
- package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.cjs +10 -0
- package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.mjs +10 -0
- package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
- package/dist/v2/runtime/core/hooks.cjs.map +1 -1
- package/dist/v2/runtime/core/hooks.d.cts +7 -0
- package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
- package/dist/v2/runtime/core/hooks.d.mts +7 -0
- package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
- package/dist/v2/runtime/core/hooks.mjs.map +1 -1
- package/dist/v2/runtime/core/runtime.cjs +5 -0
- package/dist/v2/runtime/core/runtime.cjs.map +1 -1
- package/dist/v2/runtime/core/runtime.d.cts +21 -0
- package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +21 -0
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/core/runtime.mjs +5 -0
- package/dist/v2/runtime/core/runtime.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs +2 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs +2 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/header-utils.cjs +169 -9
- package/dist/v2/runtime/handlers/header-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/header-utils.d.cts +54 -0
- package/dist/v2/runtime/handlers/header-utils.d.cts.map +1 -0
- package/dist/v2/runtime/handlers/header-utils.d.mts +54 -0
- package/dist/v2/runtime/handlers/header-utils.d.mts.map +1 -0
- package/dist/v2/runtime/handlers/header-utils.mjs +168 -9
- package/dist/v2/runtime/handlers/header-utils.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/memories.cjs +209 -0
- package/dist/v2/runtime/handlers/intelligence/memories.cjs.map +1 -0
- package/dist/v2/runtime/handlers/intelligence/memories.mjs +204 -0
- package/dist/v2/runtime/handlers/intelligence/memories.mjs.map +1 -0
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs +18 -5
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs +18 -5
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/connect.cjs +2 -2
- package/dist/v2/runtime/handlers/sse/connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/connect.mjs +3 -3
- package/dist/v2/runtime/handlers/sse/connect.mjs.map +1 -1
- package/dist/v2/runtime/index.d.cts +1 -1
- package/dist/v2/runtime/index.d.mts +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +71 -2
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +112 -0
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +112 -0
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +71 -2
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/dist/v2/runtime/runner/in-memory.cjs +221 -37
- package/dist/v2/runtime/runner/in-memory.cjs.map +1 -1
- package/dist/v2/runtime/runner/in-memory.d.cts +164 -3
- package/dist/v2/runtime/runner/in-memory.d.cts.map +1 -1
- package/dist/v2/runtime/runner/in-memory.d.mts +164 -3
- package/dist/v2/runtime/runner/in-memory.d.mts.map +1 -1
- package/dist/v2/runtime/runner/in-memory.mjs +218 -38
- package/dist/v2/runtime/runner/in-memory.mjs.map +1 -1
- package/dist/v2/runtime/runner/index.d.cts +1 -1
- package/dist/v2/runtime/runner/index.d.mts +1 -1
- package/dist/v2/runtime/runner/index.mjs +1 -1
- package/package.json +3 -3
|
@@ -2,25 +2,185 @@ require("reflect-metadata");
|
|
|
2
2
|
|
|
3
3
|
//#region src/v2/runtime/handlers/header-utils.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Exact header names (lowercased) stripped from forwarding by default.
|
|
6
|
+
*
|
|
7
|
+
* These are infrastructure/proxy/platform artifacts that no legitimate agent
|
|
8
|
+
* integration depends on receiving *forwarded from the inbound edge* — the
|
|
9
|
+
* inbound request has already traversed a browser, CDN/edge, load balancer, and
|
|
10
|
+
* hosting platform, each of which stamps its own `x-*` headers. Forwarding them
|
|
11
|
+
* verbatim to an arbitrary configured agent URL leaks client topology and, in
|
|
12
|
+
* the Copilot Cloud case, a platform credential (#5712).
|
|
13
|
+
*
|
|
14
|
+
* `x-amz-cf-id` and `x-copilotcloud-public-api-key` are also covered by the
|
|
15
|
+
* `x-amz-` / `x-copilotcloud-` prefixes below; the exact entries are kept
|
|
16
|
+
* intentionally as documentation anchors for the highest-severity headers
|
|
17
|
+
* (notably the platform API key), not as drift/oversight.
|
|
7
18
|
*/
|
|
8
|
-
|
|
19
|
+
const DEFAULT_DENY_HEADER_NAMES = new Set([
|
|
20
|
+
"x-forwarded-for",
|
|
21
|
+
"x-forwarded-proto",
|
|
22
|
+
"x-forwarded-host",
|
|
23
|
+
"x-forwarded-port",
|
|
24
|
+
"x-forwarded-server",
|
|
25
|
+
"x-real-ip",
|
|
26
|
+
"x-amzn-trace-id",
|
|
27
|
+
"x-amz-cf-id",
|
|
28
|
+
"x-cloud-trace-context",
|
|
29
|
+
"x-cache",
|
|
30
|
+
"x-served-by",
|
|
31
|
+
"x-request-id",
|
|
32
|
+
"x-copilotcloud-public-api-key"
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Header-name prefixes (lowercased) stripped from forwarding by default.
|
|
36
|
+
*
|
|
37
|
+
* Prefix matching covers the well-known platform/CDN families so a new member
|
|
38
|
+
* of a family (e.g. a future `x-vercel-foo`) is denied without a constant edit.
|
|
39
|
+
*/
|
|
40
|
+
const DEFAULT_DENY_HEADER_PREFIXES = [
|
|
41
|
+
"x-amz-",
|
|
42
|
+
"x-azure-",
|
|
43
|
+
"x-fastly-",
|
|
44
|
+
"x-vercel-",
|
|
45
|
+
"x-middleware-",
|
|
46
|
+
"x-copilotcloud-"
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Normalizes a public {@link ForwardHeadersConfig} (or `undefined`) into a
|
|
50
|
+
* fully-resolved {@link ResolvedForwardHeadersPolicy}.
|
|
51
|
+
*
|
|
52
|
+
* - `useDefaultDenylist` defaults to `true` (the built-in denylist is active
|
|
53
|
+
* on upgrade); pass `false` to restore the previous wide-open behavior.
|
|
54
|
+
* - `deny` / `denyPrefixes` extend (do not replace) the defaults.
|
|
55
|
+
* - `allow` activates allowlist mode only when it has at least one non-empty
|
|
56
|
+
* entry after normalization.
|
|
57
|
+
*
|
|
58
|
+
* All names/prefixes are trimmed, lowercased, and stripped of empty/
|
|
59
|
+
* whitespace-only entries before use. Trimming/lowercasing keeps matching a
|
|
60
|
+
* plain set/prefix check against the lowercased inbound keys; dropping empties
|
|
61
|
+
* is a safety guard: a stray `denyPrefixes: [""]` would make `startsWith("")`
|
|
62
|
+
* true for every header (silently denying ALL forwarding). Because empties are
|
|
63
|
+
* dropped BEFORE the allowlist-mode decision, an `allow: [""]` / `allow: [" "]`
|
|
64
|
+
* normalizes to an empty set and does NOT switch on allowlist mode — the runtime
|
|
65
|
+
* stays in denylist mode. Allowlist mode activates only when `allow` has at
|
|
66
|
+
* least one non-empty entry; these empty/whitespace-only entries are integrator
|
|
67
|
+
* typos, not intent, so we filter them.
|
|
68
|
+
*/
|
|
69
|
+
function normalizeHeaderEntries(entries) {
|
|
70
|
+
return (entries ?? []).map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
|
|
71
|
+
}
|
|
72
|
+
function resolveForwardHeadersPolicy(config) {
|
|
73
|
+
const denyNames = new Set(normalizeHeaderEntries(config?.deny));
|
|
74
|
+
const denyPrefixes = normalizeHeaderEntries(config?.denyPrefixes);
|
|
75
|
+
const allowEntries = normalizeHeaderEntries(config?.allow);
|
|
76
|
+
const allow = allowEntries.length > 0 ? new Set(allowEntries) : void 0;
|
|
77
|
+
return {
|
|
78
|
+
useDefaultDenylist: config?.useDefaultDenylist ?? true,
|
|
79
|
+
denyNames,
|
|
80
|
+
denyPrefixes,
|
|
81
|
+
allow
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* True iff the (already-lowercased) header name matches the integrator's OWN
|
|
86
|
+
* `deny` / `denyPrefixes`. This is the authoritative subtractive check: it is
|
|
87
|
+
* consulted in BOTH allowlist and denylist mode. It deliberately does NOT
|
|
88
|
+
* include the built-in {@link DEFAULT_DENY_HEADER_NAMES} /
|
|
89
|
+
* {@link DEFAULT_DENY_HEADER_PREFIXES} — an explicit `allow` opts the integrator
|
|
90
|
+
* back into a default-denied header on purpose, so only their own `deny`
|
|
91
|
+
* subtracts from an allowlist.
|
|
92
|
+
*/
|
|
93
|
+
function matchesIntegratorDeny(lower, policy) {
|
|
94
|
+
if (policy.denyNames.has(lower)) return true;
|
|
95
|
+
return policy.denyPrefixes.some((prefix) => lower.startsWith(prefix));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Determines if a header should be forwarded under the given resolved policy.
|
|
99
|
+
*
|
|
100
|
+
* The integrator's `deny` / `denyPrefixes` ALWAYS strip, including in allowlist
|
|
101
|
+
* mode: `allow` selects the candidate set, `deny` removes from it. A header the
|
|
102
|
+
* integrator lists in BOTH `allow` and `deny` is NOT forwarded — deny is
|
|
103
|
+
* authoritative so a security-motivated `deny` can never be silently defeated
|
|
104
|
+
* by an overlapping `allow` (the footgun this hardens against).
|
|
105
|
+
*
|
|
106
|
+
* Modes:
|
|
107
|
+
* - Allowlist (`policy.allow` set): forward iff the name is in `allow` AND is
|
|
108
|
+
* NOT matched by the integrator's `deny` / `denyPrefixes`. Nothing else
|
|
109
|
+
* forwards — not even the usual `authorization` / `x-*` eligibility.
|
|
110
|
+
* - Denylist (default): base eligibility is `authorization` or any `x-*`, then
|
|
111
|
+
* the built-in denylist (when enabled) and the integrator's own
|
|
112
|
+
* names/prefixes strip from that set.
|
|
113
|
+
*
|
|
114
|
+
* Note: the built-in default denylist applies ONLY in denylist mode; an
|
|
115
|
+
* explicit `allow` is treated as the integrator deliberately opting back into
|
|
116
|
+
* those headers, so only their OWN `deny` subtracts in allowlist mode.
|
|
117
|
+
*/
|
|
118
|
+
function shouldForwardHeader(headerName, policy) {
|
|
9
119
|
const lower = headerName.toLowerCase();
|
|
10
|
-
return lower
|
|
120
|
+
if (policy.allow) return policy.allow.has(lower) && !matchesIntegratorDeny(lower, policy);
|
|
121
|
+
if (!(lower === "authorization" || lower.startsWith("x-"))) return false;
|
|
122
|
+
if (policy.useDefaultDenylist) {
|
|
123
|
+
if (DEFAULT_DENY_HEADER_NAMES.has(lower)) return false;
|
|
124
|
+
if (DEFAULT_DENY_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix))) return false;
|
|
125
|
+
}
|
|
126
|
+
if (matchesIntegratorDeny(lower, policy)) return false;
|
|
127
|
+
return true;
|
|
11
128
|
}
|
|
12
129
|
/**
|
|
13
|
-
* Extracts headers that should be forwarded from a Request object
|
|
14
|
-
*
|
|
130
|
+
* Extracts headers that should be forwarded from a Request object, applying the
|
|
131
|
+
* resolved forwarding policy. Keys are normalized to the lowercased form the
|
|
132
|
+
* `Headers` iterator yields.
|
|
15
133
|
*/
|
|
16
|
-
function extractForwardableHeaders(request) {
|
|
134
|
+
function extractForwardableHeaders(request, policy) {
|
|
17
135
|
const forwardableHeaders = {};
|
|
18
136
|
request.headers.forEach((value, key) => {
|
|
19
|
-
if (shouldForwardHeader(key)) forwardableHeaders[key] = value;
|
|
137
|
+
if (shouldForwardHeader(key, policy)) forwardableHeaders[key] = value;
|
|
20
138
|
});
|
|
21
139
|
return forwardableHeaders;
|
|
22
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Merges forwardable inbound request headers onto the headers a server
|
|
143
|
+
* explicitly configured on an agent, letting the SERVER-CONFIGURED headers WIN
|
|
144
|
+
* on collision — a server-set service-to-service token (e.g. an IAM bearer)
|
|
145
|
+
* must never be silently overridden by a browser/edge/platform-injected inbound
|
|
146
|
+
* header (#5712).
|
|
147
|
+
*
|
|
148
|
+
* The collision check is case-insensitive: `extractForwardableHeaders`
|
|
149
|
+
* normalizes inbound keys to lowercase (`authorization`) while the server
|
|
150
|
+
* typically configures canonical casing (`Authorization`). A plain object
|
|
151
|
+
* spread would treat those as distinct keys and emit BOTH — which downstream
|
|
152
|
+
* (undici) comma-joins into a single invalid "multiple JWTs" value. So we drop
|
|
153
|
+
* any forwarded header the agent already sets, matched case-insensitively, and
|
|
154
|
+
* let non-colliding inbound headers pass through unchanged.
|
|
155
|
+
*
|
|
156
|
+
* The same comma-join hazard exists if the SERVER CONFIG ITSELF contains two
|
|
157
|
+
* case-variants of one header (e.g. both `Authorization` and `authorization`
|
|
158
|
+
* in `agent.headers`). A plain `{ ...serverHeaders }` spread would keep both,
|
|
159
|
+
* so we additionally collapse server-self case-collisions to a SINGLE entry,
|
|
160
|
+
* FIRST-OCCURRENCE WINS: the first key seen (in `Object.keys` order) keeps its
|
|
161
|
+
* exact casing and value, and any later case-variant of that name is dropped.
|
|
162
|
+
* Server-wins-over-inbound and case-insensitive inbound suppression are
|
|
163
|
+
* otherwise unchanged.
|
|
164
|
+
*
|
|
165
|
+
* Breadth (which inbound headers are eligible to forward at all) is decided by
|
|
166
|
+
* `policy` upstream in `extractForwardableHeaders` → `shouldForwardHeader`; the
|
|
167
|
+
* merge never re-widens or re-narrows the set.
|
|
168
|
+
*/
|
|
169
|
+
function mergeForwardableHeaders(serverHeaders, request, policy) {
|
|
170
|
+
const base = serverHeaders ?? {};
|
|
171
|
+
const merged = {};
|
|
172
|
+
const serverHeaderNames = /* @__PURE__ */ new Set();
|
|
173
|
+
for (const [name, value] of Object.entries(base)) {
|
|
174
|
+
const lower = name.toLowerCase();
|
|
175
|
+
if (serverHeaderNames.has(lower)) continue;
|
|
176
|
+
serverHeaderNames.add(lower);
|
|
177
|
+
merged[name] = value;
|
|
178
|
+
}
|
|
179
|
+
for (const [name, value] of Object.entries(extractForwardableHeaders(request, policy))) if (!serverHeaderNames.has(name.toLowerCase())) merged[name] = value;
|
|
180
|
+
return merged;
|
|
181
|
+
}
|
|
23
182
|
|
|
24
183
|
//#endregion
|
|
25
|
-
exports.
|
|
184
|
+
exports.mergeForwardableHeaders = mergeForwardableHeaders;
|
|
185
|
+
exports.resolveForwardHeadersPolicy = resolveForwardHeadersPolicy;
|
|
26
186
|
//# sourceMappingURL=header-utils.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"header-utils.cjs","names":[],"sources":["../../../../src/v2/runtime/handlers/header-utils.ts"],"sourcesContent":["/**\n * Determines if a header should be forwarded based on the allowlist.\n * Forwards: authorization header and all x-* custom headers.\n */\nexport function shouldForwardHeader(headerName: string): boolean {\n const lower = headerName.toLowerCase();\n return lower === \"authorization\" || lower.startsWith(\"x-\");\n}\n\n/**\n * Extracts headers that should be forwarded from a Request object.\n * Forwards only authorization and x-* headers.\n */\nexport function extractForwardableHeaders(\n request: Request,\n): Record<string, string> {\n const forwardableHeaders: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n if (shouldForwardHeader(key)) {\n forwardableHeaders[key] = value;\n }\n });\n return forwardableHeaders;\n}\n"],"mappings":";;;;;;;AAIA,SAAgB,oBAAoB,YAA6B;CAC/D,MAAM,QAAQ,WAAW,aAAa;AACtC,QAAO,UAAU,mBAAmB,MAAM,WAAW,KAAK;;;;;;AAO5D,SAAgB,0BACd,SACwB;CACxB,MAAM,qBAA6C,EAAE;AACrD,SAAQ,QAAQ,SAAS,OAAO,QAAQ;AACtC,MAAI,oBAAoB,IAAI,CAC1B,oBAAmB,OAAO;GAE5B;AACF,QAAO"}
|
|
1
|
+
{"version":3,"file":"header-utils.cjs","names":[],"sources":["../../../../src/v2/runtime/handlers/header-utils.ts"],"sourcesContent":["/**\n * Exact header names (lowercased) stripped from forwarding by default.\n *\n * These are infrastructure/proxy/platform artifacts that no legitimate agent\n * integration depends on receiving *forwarded from the inbound edge* — the\n * inbound request has already traversed a browser, CDN/edge, load balancer, and\n * hosting platform, each of which stamps its own `x-*` headers. Forwarding them\n * verbatim to an arbitrary configured agent URL leaks client topology and, in\n * the Copilot Cloud case, a platform credential (#5712).\n *\n * `x-amz-cf-id` and `x-copilotcloud-public-api-key` are also covered by the\n * `x-amz-` / `x-copilotcloud-` prefixes below; the exact entries are kept\n * intentionally as documentation anchors for the highest-severity headers\n * (notably the platform API key), not as drift/oversight.\n */\nexport const DEFAULT_DENY_HEADER_NAMES: ReadonlySet<string> = new Set([\n // Hop-by-hop / proxy topology\n \"x-forwarded-for\",\n \"x-forwarded-proto\",\n \"x-forwarded-host\",\n \"x-forwarded-port\",\n \"x-forwarded-server\",\n \"x-real-ip\",\n // Cloud / CDN tracing + infra\n \"x-amzn-trace-id\",\n \"x-amz-cf-id\",\n \"x-cloud-trace-context\",\n \"x-cache\",\n \"x-served-by\",\n \"x-request-id\",\n // CopilotKit platform credentials/identifiers\n \"x-copilotcloud-public-api-key\",\n]);\n\n/**\n * Header-name prefixes (lowercased) stripped from forwarding by default.\n *\n * Prefix matching covers the well-known platform/CDN families so a new member\n * of a family (e.g. a future `x-vercel-foo`) is denied without a constant edit.\n */\nexport const DEFAULT_DENY_HEADER_PREFIXES: readonly string[] = [\n \"x-amz-\", // AWS\n \"x-azure-\", // Azure Front Door\n \"x-fastly-\", // Fastly\n \"x-vercel-\", // Vercel\n \"x-middleware-\", // Next.js\n \"x-copilotcloud-\", // CopilotKit platform-internal\n];\n\n/**\n * Fully-resolved inbound-header forwarding policy read by the call sites.\n *\n * Distinct from the public `ForwardHeadersConfig` option an integrator passes:\n * the runtime resolves that option ONCE in its constructor into this shape\n * (lowercasing names/prefixes, defaulting `useDefaultDenylist`, building the\n * `allow` set) so the predicate stays branch-simple and the policy can never be\n * re-resolved divergently at a call site. See `resolveForwardHeadersPolicy`.\n */\nexport interface ResolvedForwardHeadersPolicy {\n /** When true, the built-in infra/platform denylist is active. */\n useDefaultDenylist: boolean;\n /** Extra exact names to strip (lowercased). */\n denyNames: ReadonlySet<string>;\n /** Extra prefixes to strip (lowercased). */\n denyPrefixes: readonly string[];\n /**\n * If set, allowlist mode: ONLY these (lowercased) names are candidates to\n * forward — and `denyNames` / `denyPrefixes` still subtract from them.\n */\n allow?: ReadonlySet<string>;\n}\n\n/**\n * Public, integrator-facing config for inbound-header forwarding. Resolved into\n * a {@link ResolvedForwardHeadersPolicy} by {@link resolveForwardHeadersPolicy}.\n */\nexport interface ForwardHeadersConfig {\n /** Strip the built-in infra/platform denylist. @default true */\n useDefaultDenylist?: boolean;\n /** Additional exact header names to strip (case-insensitive). */\n deny?: string[];\n /** Additional header-name prefixes to strip (case-insensitive). */\n denyPrefixes?: string[];\n /**\n * If set (with at least one non-empty entry), switch to allowlist mode: ONLY\n * these headers are candidates to forward, overriding the default `x-*` /\n * `authorization` eligibility (case-insensitive). `deny` / `denyPrefixes`\n * still apply and subtract from this set — a header listed in both `allow` and\n * `deny` is NOT forwarded.\n *\n * Footgun: in allowlist mode the built-in DEFAULT denylist (and\n * `useDefaultDenylist`) is BYPASSED — only your `allow` set, minus your own\n * `deny` / `denyPrefixes`, is forwarded. Do NOT allow-list protected/platform\n * headers (e.g. `x-copilotcloud-public-api-key`, `x-forwarded-*`) unless you\n * truly intend to forward them, since the default protection does not apply\n * here.\n */\n allow?: string[];\n}\n\n/**\n * Normalizes a public {@link ForwardHeadersConfig} (or `undefined`) into a\n * fully-resolved {@link ResolvedForwardHeadersPolicy}.\n *\n * - `useDefaultDenylist` defaults to `true` (the built-in denylist is active\n * on upgrade); pass `false` to restore the previous wide-open behavior.\n * - `deny` / `denyPrefixes` extend (do not replace) the defaults.\n * - `allow` activates allowlist mode only when it has at least one non-empty\n * entry after normalization.\n *\n * All names/prefixes are trimmed, lowercased, and stripped of empty/\n * whitespace-only entries before use. Trimming/lowercasing keeps matching a\n * plain set/prefix check against the lowercased inbound keys; dropping empties\n * is a safety guard: a stray `denyPrefixes: [\"\"]` would make `startsWith(\"\")`\n * true for every header (silently denying ALL forwarding). Because empties are\n * dropped BEFORE the allowlist-mode decision, an `allow: [\"\"]` / `allow: [\" \"]`\n * normalizes to an empty set and does NOT switch on allowlist mode — the runtime\n * stays in denylist mode. Allowlist mode activates only when `allow` has at\n * least one non-empty entry; these empty/whitespace-only entries are integrator\n * typos, not intent, so we filter them.\n */\nfunction normalizeHeaderEntries(entries: string[] | undefined): string[] {\n return (entries ?? [])\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0);\n}\n\nexport function resolveForwardHeadersPolicy(\n config: ForwardHeadersConfig | undefined,\n): ResolvedForwardHeadersPolicy {\n const denyNames = new Set<string>(normalizeHeaderEntries(config?.deny));\n const denyPrefixes = normalizeHeaderEntries(config?.denyPrefixes);\n const allowEntries = normalizeHeaderEntries(config?.allow);\n const allow =\n allowEntries.length > 0 ? new Set<string>(allowEntries) : undefined;\n\n return {\n useDefaultDenylist: config?.useDefaultDenylist ?? true,\n denyNames,\n denyPrefixes,\n allow,\n };\n}\n\n/**\n * True iff the (already-lowercased) header name matches the integrator's OWN\n * `deny` / `denyPrefixes`. This is the authoritative subtractive check: it is\n * consulted in BOTH allowlist and denylist mode. It deliberately does NOT\n * include the built-in {@link DEFAULT_DENY_HEADER_NAMES} /\n * {@link DEFAULT_DENY_HEADER_PREFIXES} — an explicit `allow` opts the integrator\n * back into a default-denied header on purpose, so only their own `deny`\n * subtracts from an allowlist.\n */\nfunction matchesIntegratorDeny(\n lower: string,\n policy: ResolvedForwardHeadersPolicy,\n): boolean {\n if (policy.denyNames.has(lower)) return true;\n return policy.denyPrefixes.some((prefix) => lower.startsWith(prefix));\n}\n\n/**\n * Determines if a header should be forwarded under the given resolved policy.\n *\n * The integrator's `deny` / `denyPrefixes` ALWAYS strip, including in allowlist\n * mode: `allow` selects the candidate set, `deny` removes from it. A header the\n * integrator lists in BOTH `allow` and `deny` is NOT forwarded — deny is\n * authoritative so a security-motivated `deny` can never be silently defeated\n * by an overlapping `allow` (the footgun this hardens against).\n *\n * Modes:\n * - Allowlist (`policy.allow` set): forward iff the name is in `allow` AND is\n * NOT matched by the integrator's `deny` / `denyPrefixes`. Nothing else\n * forwards — not even the usual `authorization` / `x-*` eligibility.\n * - Denylist (default): base eligibility is `authorization` or any `x-*`, then\n * the built-in denylist (when enabled) and the integrator's own\n * names/prefixes strip from that set.\n *\n * Note: the built-in default denylist applies ONLY in denylist mode; an\n * explicit `allow` is treated as the integrator deliberately opting back into\n * those headers, so only their OWN `deny` subtracts in allowlist mode.\n */\nexport function shouldForwardHeader(\n headerName: string,\n policy: ResolvedForwardHeadersPolicy,\n): boolean {\n const lower = headerName.toLowerCase();\n\n // Allowlist mode: forward iff explicitly allowed AND not subtracted by the\n // integrator's own deny/denyPrefixes. Nothing else forwards — not even the\n // usual `authorization` / `x-*` eligibility.\n if (policy.allow) {\n return policy.allow.has(lower) && !matchesIntegratorDeny(lower, policy);\n }\n\n // Base eligibility (unchanged): authorization + any x-*.\n const eligible = lower === \"authorization\" || lower.startsWith(\"x-\");\n if (!eligible) return false;\n\n // Built-in denylist (default-on): strip known infra/platform headers.\n if (policy.useDefaultDenylist) {\n if (DEFAULT_DENY_HEADER_NAMES.has(lower)) return false;\n if (\n DEFAULT_DENY_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix))\n ) {\n return false;\n }\n }\n\n // Integrator-supplied additions extend the denylist regardless of the default.\n if (matchesIntegratorDeny(lower, policy)) return false;\n\n return true;\n}\n\n/**\n * Extracts headers that should be forwarded from a Request object, applying the\n * resolved forwarding policy. Keys are normalized to the lowercased form the\n * `Headers` iterator yields.\n */\nexport function extractForwardableHeaders(\n request: Request,\n policy: ResolvedForwardHeadersPolicy,\n): Record<string, string> {\n const forwardableHeaders: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n if (shouldForwardHeader(key, policy)) {\n forwardableHeaders[key] = value;\n }\n });\n return forwardableHeaders;\n}\n\n/**\n * Merges forwardable inbound request headers onto the headers a server\n * explicitly configured on an agent, letting the SERVER-CONFIGURED headers WIN\n * on collision — a server-set service-to-service token (e.g. an IAM bearer)\n * must never be silently overridden by a browser/edge/platform-injected inbound\n * header (#5712).\n *\n * The collision check is case-insensitive: `extractForwardableHeaders`\n * normalizes inbound keys to lowercase (`authorization`) while the server\n * typically configures canonical casing (`Authorization`). A plain object\n * spread would treat those as distinct keys and emit BOTH — which downstream\n * (undici) comma-joins into a single invalid \"multiple JWTs\" value. So we drop\n * any forwarded header the agent already sets, matched case-insensitively, and\n * let non-colliding inbound headers pass through unchanged.\n *\n * The same comma-join hazard exists if the SERVER CONFIG ITSELF contains two\n * case-variants of one header (e.g. both `Authorization` and `authorization`\n * in `agent.headers`). A plain `{ ...serverHeaders }` spread would keep both,\n * so we additionally collapse server-self case-collisions to a SINGLE entry,\n * FIRST-OCCURRENCE WINS: the first key seen (in `Object.keys` order) keeps its\n * exact casing and value, and any later case-variant of that name is dropped.\n * Server-wins-over-inbound and case-insensitive inbound suppression are\n * otherwise unchanged.\n *\n * Breadth (which inbound headers are eligible to forward at all) is decided by\n * `policy` upstream in `extractForwardableHeaders` → `shouldForwardHeader`; the\n * merge never re-widens or re-narrows the set.\n */\nexport function mergeForwardableHeaders(\n serverHeaders: Record<string, string> | undefined,\n request: Request,\n policy: ResolvedForwardHeadersPolicy,\n): Record<string, string> {\n const base = serverHeaders ?? {};\n const merged: Record<string, string> = {};\n const serverHeaderNames = new Set<string>();\n // Collapse server-self case-collisions: first occurrence wins, later\n // case-variants of the same name are dropped.\n for (const [name, value] of Object.entries(base)) {\n const lower = name.toLowerCase();\n if (serverHeaderNames.has(lower)) {\n continue;\n }\n serverHeaderNames.add(lower);\n merged[name] = value;\n }\n for (const [name, value] of Object.entries(\n extractForwardableHeaders(request, policy),\n )) {\n if (!serverHeaderNames.has(name.toLowerCase())) {\n merged[name] = value;\n }\n }\n return merged;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAeA,MAAa,4BAAiD,IAAI,IAAI;CAEpE;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CAEA;CACD,CAAC;;;;;;;AAQF,MAAa,+BAAkD;CAC7D;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;;;;;;;;;;;;;;;AA0ED,SAAS,uBAAuB,SAAyC;AACvE,SAAQ,WAAW,EAAE,EAClB,KAAK,UAAU,MAAM,MAAM,CAAC,aAAa,CAAC,CAC1C,QAAQ,UAAU,MAAM,SAAS,EAAE;;AAGxC,SAAgB,4BACd,QAC8B;CAC9B,MAAM,YAAY,IAAI,IAAY,uBAAuB,QAAQ,KAAK,CAAC;CACvE,MAAM,eAAe,uBAAuB,QAAQ,aAAa;CACjE,MAAM,eAAe,uBAAuB,QAAQ,MAAM;CAC1D,MAAM,QACJ,aAAa,SAAS,IAAI,IAAI,IAAY,aAAa,GAAG;AAE5D,QAAO;EACL,oBAAoB,QAAQ,sBAAsB;EAClD;EACA;EACA;EACD;;;;;;;;;;;AAYH,SAAS,sBACP,OACA,QACS;AACT,KAAI,OAAO,UAAU,IAAI,MAAM,CAAE,QAAO;AACxC,QAAO,OAAO,aAAa,MAAM,WAAW,MAAM,WAAW,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBvE,SAAgB,oBACd,YACA,QACS;CACT,MAAM,QAAQ,WAAW,aAAa;AAKtC,KAAI,OAAO,MACT,QAAO,OAAO,MAAM,IAAI,MAAM,IAAI,CAAC,sBAAsB,OAAO,OAAO;AAKzE,KAAI,EADa,UAAU,mBAAmB,MAAM,WAAW,KAAK,EACrD,QAAO;AAGtB,KAAI,OAAO,oBAAoB;AAC7B,MAAI,0BAA0B,IAAI,MAAM,CAAE,QAAO;AACjD,MACE,6BAA6B,MAAM,WAAW,MAAM,WAAW,OAAO,CAAC,CAEvE,QAAO;;AAKX,KAAI,sBAAsB,OAAO,OAAO,CAAE,QAAO;AAEjD,QAAO;;;;;;;AAQT,SAAgB,0BACd,SACA,QACwB;CACxB,MAAM,qBAA6C,EAAE;AACrD,SAAQ,QAAQ,SAAS,OAAO,QAAQ;AACtC,MAAI,oBAAoB,KAAK,OAAO,CAClC,oBAAmB,OAAO;GAE5B;AACF,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BT,SAAgB,wBACd,eACA,SACA,QACwB;CACxB,MAAM,OAAO,iBAAiB,EAAE;CAChC,MAAM,SAAiC,EAAE;CACzC,MAAM,oCAAoB,IAAI,KAAa;AAG3C,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,EAAE;EAChD,MAAM,QAAQ,KAAK,aAAa;AAChC,MAAI,kBAAkB,IAAI,MAAM,CAC9B;AAEF,oBAAkB,IAAI,MAAM;AAC5B,SAAO,QAAQ;;AAEjB,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QACjC,0BAA0B,SAAS,OAAO,CAC3C,CACC,KAAI,CAAC,kBAAkB,IAAI,KAAK,aAAa,CAAC,CAC5C,QAAO,QAAQ;AAGnB,QAAO"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/v2/runtime/handlers/header-utils.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Fully-resolved inbound-header forwarding policy read by the call sites.
|
|
5
|
+
*
|
|
6
|
+
* Distinct from the public `ForwardHeadersConfig` option an integrator passes:
|
|
7
|
+
* the runtime resolves that option ONCE in its constructor into this shape
|
|
8
|
+
* (lowercasing names/prefixes, defaulting `useDefaultDenylist`, building the
|
|
9
|
+
* `allow` set) so the predicate stays branch-simple and the policy can never be
|
|
10
|
+
* re-resolved divergently at a call site. See `resolveForwardHeadersPolicy`.
|
|
11
|
+
*/
|
|
12
|
+
interface ResolvedForwardHeadersPolicy {
|
|
13
|
+
/** When true, the built-in infra/platform denylist is active. */
|
|
14
|
+
useDefaultDenylist: boolean;
|
|
15
|
+
/** Extra exact names to strip (lowercased). */
|
|
16
|
+
denyNames: ReadonlySet<string>;
|
|
17
|
+
/** Extra prefixes to strip (lowercased). */
|
|
18
|
+
denyPrefixes: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* If set, allowlist mode: ONLY these (lowercased) names are candidates to
|
|
21
|
+
* forward — and `denyNames` / `denyPrefixes` still subtract from them.
|
|
22
|
+
*/
|
|
23
|
+
allow?: ReadonlySet<string>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Public, integrator-facing config for inbound-header forwarding. Resolved into
|
|
27
|
+
* a {@link ResolvedForwardHeadersPolicy} by {@link resolveForwardHeadersPolicy}.
|
|
28
|
+
*/
|
|
29
|
+
interface ForwardHeadersConfig {
|
|
30
|
+
/** Strip the built-in infra/platform denylist. @default true */
|
|
31
|
+
useDefaultDenylist?: boolean;
|
|
32
|
+
/** Additional exact header names to strip (case-insensitive). */
|
|
33
|
+
deny?: string[];
|
|
34
|
+
/** Additional header-name prefixes to strip (case-insensitive). */
|
|
35
|
+
denyPrefixes?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* If set (with at least one non-empty entry), switch to allowlist mode: ONLY
|
|
38
|
+
* these headers are candidates to forward, overriding the default `x-*` /
|
|
39
|
+
* `authorization` eligibility (case-insensitive). `deny` / `denyPrefixes`
|
|
40
|
+
* still apply and subtract from this set — a header listed in both `allow` and
|
|
41
|
+
* `deny` is NOT forwarded.
|
|
42
|
+
*
|
|
43
|
+
* Footgun: in allowlist mode the built-in DEFAULT denylist (and
|
|
44
|
+
* `useDefaultDenylist`) is BYPASSED — only your `allow` set, minus your own
|
|
45
|
+
* `deny` / `denyPrefixes`, is forwarded. Do NOT allow-list protected/platform
|
|
46
|
+
* headers (e.g. `x-copilotcloud-public-api-key`, `x-forwarded-*`) unless you
|
|
47
|
+
* truly intend to forward them, since the default protection does not apply
|
|
48
|
+
* here.
|
|
49
|
+
*/
|
|
50
|
+
allow?: string[];
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { ForwardHeadersConfig, ResolvedForwardHeadersPolicy };
|
|
54
|
+
//# sourceMappingURL=header-utils.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header-utils.d.cts","names":[],"sources":["../../../../src/v2/runtime/handlers/header-utils.ts"],"mappings":";;;;;;;;;;;UA0DiB,4BAAA;;EAEf,kBAAA;;EAEA,SAAA,EAAW,WAAA;;EAEX,YAAA;;;;;EAKA,KAAA,GAAQ,WAAA;AAAA;;;;;UAOO,oBAAA;;EAEf,kBAAA;;EAEA,IAAA;;EAEA,YAAA;;;;;;;;;;;;;;;EAeA,KAAA;AAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
//#region src/v2/runtime/handlers/header-utils.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Fully-resolved inbound-header forwarding policy read by the call sites.
|
|
5
|
+
*
|
|
6
|
+
* Distinct from the public `ForwardHeadersConfig` option an integrator passes:
|
|
7
|
+
* the runtime resolves that option ONCE in its constructor into this shape
|
|
8
|
+
* (lowercasing names/prefixes, defaulting `useDefaultDenylist`, building the
|
|
9
|
+
* `allow` set) so the predicate stays branch-simple and the policy can never be
|
|
10
|
+
* re-resolved divergently at a call site. See `resolveForwardHeadersPolicy`.
|
|
11
|
+
*/
|
|
12
|
+
interface ResolvedForwardHeadersPolicy {
|
|
13
|
+
/** When true, the built-in infra/platform denylist is active. */
|
|
14
|
+
useDefaultDenylist: boolean;
|
|
15
|
+
/** Extra exact names to strip (lowercased). */
|
|
16
|
+
denyNames: ReadonlySet<string>;
|
|
17
|
+
/** Extra prefixes to strip (lowercased). */
|
|
18
|
+
denyPrefixes: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* If set, allowlist mode: ONLY these (lowercased) names are candidates to
|
|
21
|
+
* forward — and `denyNames` / `denyPrefixes` still subtract from them.
|
|
22
|
+
*/
|
|
23
|
+
allow?: ReadonlySet<string>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Public, integrator-facing config for inbound-header forwarding. Resolved into
|
|
27
|
+
* a {@link ResolvedForwardHeadersPolicy} by {@link resolveForwardHeadersPolicy}.
|
|
28
|
+
*/
|
|
29
|
+
interface ForwardHeadersConfig {
|
|
30
|
+
/** Strip the built-in infra/platform denylist. @default true */
|
|
31
|
+
useDefaultDenylist?: boolean;
|
|
32
|
+
/** Additional exact header names to strip (case-insensitive). */
|
|
33
|
+
deny?: string[];
|
|
34
|
+
/** Additional header-name prefixes to strip (case-insensitive). */
|
|
35
|
+
denyPrefixes?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* If set (with at least one non-empty entry), switch to allowlist mode: ONLY
|
|
38
|
+
* these headers are candidates to forward, overriding the default `x-*` /
|
|
39
|
+
* `authorization` eligibility (case-insensitive). `deny` / `denyPrefixes`
|
|
40
|
+
* still apply and subtract from this set — a header listed in both `allow` and
|
|
41
|
+
* `deny` is NOT forwarded.
|
|
42
|
+
*
|
|
43
|
+
* Footgun: in allowlist mode the built-in DEFAULT denylist (and
|
|
44
|
+
* `useDefaultDenylist`) is BYPASSED — only your `allow` set, minus your own
|
|
45
|
+
* `deny` / `denyPrefixes`, is forwarded. Do NOT allow-list protected/platform
|
|
46
|
+
* headers (e.g. `x-copilotcloud-public-api-key`, `x-forwarded-*`) unless you
|
|
47
|
+
* truly intend to forward them, since the default protection does not apply
|
|
48
|
+
* here.
|
|
49
|
+
*/
|
|
50
|
+
allow?: string[];
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { ForwardHeadersConfig, ResolvedForwardHeadersPolicy };
|
|
54
|
+
//# sourceMappingURL=header-utils.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header-utils.d.mts","names":[],"sources":["../../../../src/v2/runtime/handlers/header-utils.ts"],"mappings":";;;;;;;;;;;UA0DiB,4BAAA;;EAEf,kBAAA;;EAEA,SAAA,EAAW,WAAA;;EAEX,YAAA;;;;;EAKA,KAAA,GAAQ,WAAA;AAAA;;;;;UAOO,oBAAA;;EAEf,kBAAA;;EAEA,IAAA;;EAEA,YAAA;;;;;;;;;;;;;;;EAeA,KAAA;AAAA"}
|
|
@@ -1,25 +1,184 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
//#region src/v2/runtime/handlers/header-utils.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Exact header names (lowercased) stripped from forwarding by default.
|
|
5
|
+
*
|
|
6
|
+
* These are infrastructure/proxy/platform artifacts that no legitimate agent
|
|
7
|
+
* integration depends on receiving *forwarded from the inbound edge* — the
|
|
8
|
+
* inbound request has already traversed a browser, CDN/edge, load balancer, and
|
|
9
|
+
* hosting platform, each of which stamps its own `x-*` headers. Forwarding them
|
|
10
|
+
* verbatim to an arbitrary configured agent URL leaks client topology and, in
|
|
11
|
+
* the Copilot Cloud case, a platform credential (#5712).
|
|
12
|
+
*
|
|
13
|
+
* `x-amz-cf-id` and `x-copilotcloud-public-api-key` are also covered by the
|
|
14
|
+
* `x-amz-` / `x-copilotcloud-` prefixes below; the exact entries are kept
|
|
15
|
+
* intentionally as documentation anchors for the highest-severity headers
|
|
16
|
+
* (notably the platform API key), not as drift/oversight.
|
|
6
17
|
*/
|
|
7
|
-
|
|
18
|
+
const DEFAULT_DENY_HEADER_NAMES = new Set([
|
|
19
|
+
"x-forwarded-for",
|
|
20
|
+
"x-forwarded-proto",
|
|
21
|
+
"x-forwarded-host",
|
|
22
|
+
"x-forwarded-port",
|
|
23
|
+
"x-forwarded-server",
|
|
24
|
+
"x-real-ip",
|
|
25
|
+
"x-amzn-trace-id",
|
|
26
|
+
"x-amz-cf-id",
|
|
27
|
+
"x-cloud-trace-context",
|
|
28
|
+
"x-cache",
|
|
29
|
+
"x-served-by",
|
|
30
|
+
"x-request-id",
|
|
31
|
+
"x-copilotcloud-public-api-key"
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Header-name prefixes (lowercased) stripped from forwarding by default.
|
|
35
|
+
*
|
|
36
|
+
* Prefix matching covers the well-known platform/CDN families so a new member
|
|
37
|
+
* of a family (e.g. a future `x-vercel-foo`) is denied without a constant edit.
|
|
38
|
+
*/
|
|
39
|
+
const DEFAULT_DENY_HEADER_PREFIXES = [
|
|
40
|
+
"x-amz-",
|
|
41
|
+
"x-azure-",
|
|
42
|
+
"x-fastly-",
|
|
43
|
+
"x-vercel-",
|
|
44
|
+
"x-middleware-",
|
|
45
|
+
"x-copilotcloud-"
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes a public {@link ForwardHeadersConfig} (or `undefined`) into a
|
|
49
|
+
* fully-resolved {@link ResolvedForwardHeadersPolicy}.
|
|
50
|
+
*
|
|
51
|
+
* - `useDefaultDenylist` defaults to `true` (the built-in denylist is active
|
|
52
|
+
* on upgrade); pass `false` to restore the previous wide-open behavior.
|
|
53
|
+
* - `deny` / `denyPrefixes` extend (do not replace) the defaults.
|
|
54
|
+
* - `allow` activates allowlist mode only when it has at least one non-empty
|
|
55
|
+
* entry after normalization.
|
|
56
|
+
*
|
|
57
|
+
* All names/prefixes are trimmed, lowercased, and stripped of empty/
|
|
58
|
+
* whitespace-only entries before use. Trimming/lowercasing keeps matching a
|
|
59
|
+
* plain set/prefix check against the lowercased inbound keys; dropping empties
|
|
60
|
+
* is a safety guard: a stray `denyPrefixes: [""]` would make `startsWith("")`
|
|
61
|
+
* true for every header (silently denying ALL forwarding). Because empties are
|
|
62
|
+
* dropped BEFORE the allowlist-mode decision, an `allow: [""]` / `allow: [" "]`
|
|
63
|
+
* normalizes to an empty set and does NOT switch on allowlist mode — the runtime
|
|
64
|
+
* stays in denylist mode. Allowlist mode activates only when `allow` has at
|
|
65
|
+
* least one non-empty entry; these empty/whitespace-only entries are integrator
|
|
66
|
+
* typos, not intent, so we filter them.
|
|
67
|
+
*/
|
|
68
|
+
function normalizeHeaderEntries(entries) {
|
|
69
|
+
return (entries ?? []).map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
|
|
70
|
+
}
|
|
71
|
+
function resolveForwardHeadersPolicy(config) {
|
|
72
|
+
const denyNames = new Set(normalizeHeaderEntries(config?.deny));
|
|
73
|
+
const denyPrefixes = normalizeHeaderEntries(config?.denyPrefixes);
|
|
74
|
+
const allowEntries = normalizeHeaderEntries(config?.allow);
|
|
75
|
+
const allow = allowEntries.length > 0 ? new Set(allowEntries) : void 0;
|
|
76
|
+
return {
|
|
77
|
+
useDefaultDenylist: config?.useDefaultDenylist ?? true,
|
|
78
|
+
denyNames,
|
|
79
|
+
denyPrefixes,
|
|
80
|
+
allow
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* True iff the (already-lowercased) header name matches the integrator's OWN
|
|
85
|
+
* `deny` / `denyPrefixes`. This is the authoritative subtractive check: it is
|
|
86
|
+
* consulted in BOTH allowlist and denylist mode. It deliberately does NOT
|
|
87
|
+
* include the built-in {@link DEFAULT_DENY_HEADER_NAMES} /
|
|
88
|
+
* {@link DEFAULT_DENY_HEADER_PREFIXES} — an explicit `allow` opts the integrator
|
|
89
|
+
* back into a default-denied header on purpose, so only their own `deny`
|
|
90
|
+
* subtracts from an allowlist.
|
|
91
|
+
*/
|
|
92
|
+
function matchesIntegratorDeny(lower, policy) {
|
|
93
|
+
if (policy.denyNames.has(lower)) return true;
|
|
94
|
+
return policy.denyPrefixes.some((prefix) => lower.startsWith(prefix));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Determines if a header should be forwarded under the given resolved policy.
|
|
98
|
+
*
|
|
99
|
+
* The integrator's `deny` / `denyPrefixes` ALWAYS strip, including in allowlist
|
|
100
|
+
* mode: `allow` selects the candidate set, `deny` removes from it. A header the
|
|
101
|
+
* integrator lists in BOTH `allow` and `deny` is NOT forwarded — deny is
|
|
102
|
+
* authoritative so a security-motivated `deny` can never be silently defeated
|
|
103
|
+
* by an overlapping `allow` (the footgun this hardens against).
|
|
104
|
+
*
|
|
105
|
+
* Modes:
|
|
106
|
+
* - Allowlist (`policy.allow` set): forward iff the name is in `allow` AND is
|
|
107
|
+
* NOT matched by the integrator's `deny` / `denyPrefixes`. Nothing else
|
|
108
|
+
* forwards — not even the usual `authorization` / `x-*` eligibility.
|
|
109
|
+
* - Denylist (default): base eligibility is `authorization` or any `x-*`, then
|
|
110
|
+
* the built-in denylist (when enabled) and the integrator's own
|
|
111
|
+
* names/prefixes strip from that set.
|
|
112
|
+
*
|
|
113
|
+
* Note: the built-in default denylist applies ONLY in denylist mode; an
|
|
114
|
+
* explicit `allow` is treated as the integrator deliberately opting back into
|
|
115
|
+
* those headers, so only their OWN `deny` subtracts in allowlist mode.
|
|
116
|
+
*/
|
|
117
|
+
function shouldForwardHeader(headerName, policy) {
|
|
8
118
|
const lower = headerName.toLowerCase();
|
|
9
|
-
return lower
|
|
119
|
+
if (policy.allow) return policy.allow.has(lower) && !matchesIntegratorDeny(lower, policy);
|
|
120
|
+
if (!(lower === "authorization" || lower.startsWith("x-"))) return false;
|
|
121
|
+
if (policy.useDefaultDenylist) {
|
|
122
|
+
if (DEFAULT_DENY_HEADER_NAMES.has(lower)) return false;
|
|
123
|
+
if (DEFAULT_DENY_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix))) return false;
|
|
124
|
+
}
|
|
125
|
+
if (matchesIntegratorDeny(lower, policy)) return false;
|
|
126
|
+
return true;
|
|
10
127
|
}
|
|
11
128
|
/**
|
|
12
|
-
* Extracts headers that should be forwarded from a Request object
|
|
13
|
-
*
|
|
129
|
+
* Extracts headers that should be forwarded from a Request object, applying the
|
|
130
|
+
* resolved forwarding policy. Keys are normalized to the lowercased form the
|
|
131
|
+
* `Headers` iterator yields.
|
|
14
132
|
*/
|
|
15
|
-
function extractForwardableHeaders(request) {
|
|
133
|
+
function extractForwardableHeaders(request, policy) {
|
|
16
134
|
const forwardableHeaders = {};
|
|
17
135
|
request.headers.forEach((value, key) => {
|
|
18
|
-
if (shouldForwardHeader(key)) forwardableHeaders[key] = value;
|
|
136
|
+
if (shouldForwardHeader(key, policy)) forwardableHeaders[key] = value;
|
|
19
137
|
});
|
|
20
138
|
return forwardableHeaders;
|
|
21
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Merges forwardable inbound request headers onto the headers a server
|
|
142
|
+
* explicitly configured on an agent, letting the SERVER-CONFIGURED headers WIN
|
|
143
|
+
* on collision — a server-set service-to-service token (e.g. an IAM bearer)
|
|
144
|
+
* must never be silently overridden by a browser/edge/platform-injected inbound
|
|
145
|
+
* header (#5712).
|
|
146
|
+
*
|
|
147
|
+
* The collision check is case-insensitive: `extractForwardableHeaders`
|
|
148
|
+
* normalizes inbound keys to lowercase (`authorization`) while the server
|
|
149
|
+
* typically configures canonical casing (`Authorization`). A plain object
|
|
150
|
+
* spread would treat those as distinct keys and emit BOTH — which downstream
|
|
151
|
+
* (undici) comma-joins into a single invalid "multiple JWTs" value. So we drop
|
|
152
|
+
* any forwarded header the agent already sets, matched case-insensitively, and
|
|
153
|
+
* let non-colliding inbound headers pass through unchanged.
|
|
154
|
+
*
|
|
155
|
+
* The same comma-join hazard exists if the SERVER CONFIG ITSELF contains two
|
|
156
|
+
* case-variants of one header (e.g. both `Authorization` and `authorization`
|
|
157
|
+
* in `agent.headers`). A plain `{ ...serverHeaders }` spread would keep both,
|
|
158
|
+
* so we additionally collapse server-self case-collisions to a SINGLE entry,
|
|
159
|
+
* FIRST-OCCURRENCE WINS: the first key seen (in `Object.keys` order) keeps its
|
|
160
|
+
* exact casing and value, and any later case-variant of that name is dropped.
|
|
161
|
+
* Server-wins-over-inbound and case-insensitive inbound suppression are
|
|
162
|
+
* otherwise unchanged.
|
|
163
|
+
*
|
|
164
|
+
* Breadth (which inbound headers are eligible to forward at all) is decided by
|
|
165
|
+
* `policy` upstream in `extractForwardableHeaders` → `shouldForwardHeader`; the
|
|
166
|
+
* merge never re-widens or re-narrows the set.
|
|
167
|
+
*/
|
|
168
|
+
function mergeForwardableHeaders(serverHeaders, request, policy) {
|
|
169
|
+
const base = serverHeaders ?? {};
|
|
170
|
+
const merged = {};
|
|
171
|
+
const serverHeaderNames = /* @__PURE__ */ new Set();
|
|
172
|
+
for (const [name, value] of Object.entries(base)) {
|
|
173
|
+
const lower = name.toLowerCase();
|
|
174
|
+
if (serverHeaderNames.has(lower)) continue;
|
|
175
|
+
serverHeaderNames.add(lower);
|
|
176
|
+
merged[name] = value;
|
|
177
|
+
}
|
|
178
|
+
for (const [name, value] of Object.entries(extractForwardableHeaders(request, policy))) if (!serverHeaderNames.has(name.toLowerCase())) merged[name] = value;
|
|
179
|
+
return merged;
|
|
180
|
+
}
|
|
22
181
|
|
|
23
182
|
//#endregion
|
|
24
|
-
export {
|
|
183
|
+
export { mergeForwardableHeaders, resolveForwardHeadersPolicy };
|
|
25
184
|
//# sourceMappingURL=header-utils.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"header-utils.mjs","names":[],"sources":["../../../../src/v2/runtime/handlers/header-utils.ts"],"sourcesContent":["/**\n * Determines if a header should be forwarded based on the allowlist.\n * Forwards: authorization header and all x-* custom headers.\n */\nexport function shouldForwardHeader(headerName: string): boolean {\n const lower = headerName.toLowerCase();\n return lower === \"authorization\" || lower.startsWith(\"x-\");\n}\n\n/**\n * Extracts headers that should be forwarded from a Request object.\n * Forwards only authorization and x-* headers.\n */\nexport function extractForwardableHeaders(\n request: Request,\n): Record<string, string> {\n const forwardableHeaders: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n if (shouldForwardHeader(key)) {\n forwardableHeaders[key] = value;\n }\n });\n return forwardableHeaders;\n}\n"],"mappings":";;;;;;AAIA,SAAgB,oBAAoB,YAA6B;CAC/D,MAAM,QAAQ,WAAW,aAAa;AACtC,QAAO,UAAU,mBAAmB,MAAM,WAAW,KAAK;;;;;;AAO5D,SAAgB,0BACd,SACwB;CACxB,MAAM,qBAA6C,EAAE;AACrD,SAAQ,QAAQ,SAAS,OAAO,QAAQ;AACtC,MAAI,oBAAoB,IAAI,CAC1B,oBAAmB,OAAO;GAE5B;AACF,QAAO"}
|
|
1
|
+
{"version":3,"file":"header-utils.mjs","names":[],"sources":["../../../../src/v2/runtime/handlers/header-utils.ts"],"sourcesContent":["/**\n * Exact header names (lowercased) stripped from forwarding by default.\n *\n * These are infrastructure/proxy/platform artifacts that no legitimate agent\n * integration depends on receiving *forwarded from the inbound edge* — the\n * inbound request has already traversed a browser, CDN/edge, load balancer, and\n * hosting platform, each of which stamps its own `x-*` headers. Forwarding them\n * verbatim to an arbitrary configured agent URL leaks client topology and, in\n * the Copilot Cloud case, a platform credential (#5712).\n *\n * `x-amz-cf-id` and `x-copilotcloud-public-api-key` are also covered by the\n * `x-amz-` / `x-copilotcloud-` prefixes below; the exact entries are kept\n * intentionally as documentation anchors for the highest-severity headers\n * (notably the platform API key), not as drift/oversight.\n */\nexport const DEFAULT_DENY_HEADER_NAMES: ReadonlySet<string> = new Set([\n // Hop-by-hop / proxy topology\n \"x-forwarded-for\",\n \"x-forwarded-proto\",\n \"x-forwarded-host\",\n \"x-forwarded-port\",\n \"x-forwarded-server\",\n \"x-real-ip\",\n // Cloud / CDN tracing + infra\n \"x-amzn-trace-id\",\n \"x-amz-cf-id\",\n \"x-cloud-trace-context\",\n \"x-cache\",\n \"x-served-by\",\n \"x-request-id\",\n // CopilotKit platform credentials/identifiers\n \"x-copilotcloud-public-api-key\",\n]);\n\n/**\n * Header-name prefixes (lowercased) stripped from forwarding by default.\n *\n * Prefix matching covers the well-known platform/CDN families so a new member\n * of a family (e.g. a future `x-vercel-foo`) is denied without a constant edit.\n */\nexport const DEFAULT_DENY_HEADER_PREFIXES: readonly string[] = [\n \"x-amz-\", // AWS\n \"x-azure-\", // Azure Front Door\n \"x-fastly-\", // Fastly\n \"x-vercel-\", // Vercel\n \"x-middleware-\", // Next.js\n \"x-copilotcloud-\", // CopilotKit platform-internal\n];\n\n/**\n * Fully-resolved inbound-header forwarding policy read by the call sites.\n *\n * Distinct from the public `ForwardHeadersConfig` option an integrator passes:\n * the runtime resolves that option ONCE in its constructor into this shape\n * (lowercasing names/prefixes, defaulting `useDefaultDenylist`, building the\n * `allow` set) so the predicate stays branch-simple and the policy can never be\n * re-resolved divergently at a call site. See `resolveForwardHeadersPolicy`.\n */\nexport interface ResolvedForwardHeadersPolicy {\n /** When true, the built-in infra/platform denylist is active. */\n useDefaultDenylist: boolean;\n /** Extra exact names to strip (lowercased). */\n denyNames: ReadonlySet<string>;\n /** Extra prefixes to strip (lowercased). */\n denyPrefixes: readonly string[];\n /**\n * If set, allowlist mode: ONLY these (lowercased) names are candidates to\n * forward — and `denyNames` / `denyPrefixes` still subtract from them.\n */\n allow?: ReadonlySet<string>;\n}\n\n/**\n * Public, integrator-facing config for inbound-header forwarding. Resolved into\n * a {@link ResolvedForwardHeadersPolicy} by {@link resolveForwardHeadersPolicy}.\n */\nexport interface ForwardHeadersConfig {\n /** Strip the built-in infra/platform denylist. @default true */\n useDefaultDenylist?: boolean;\n /** Additional exact header names to strip (case-insensitive). */\n deny?: string[];\n /** Additional header-name prefixes to strip (case-insensitive). */\n denyPrefixes?: string[];\n /**\n * If set (with at least one non-empty entry), switch to allowlist mode: ONLY\n * these headers are candidates to forward, overriding the default `x-*` /\n * `authorization` eligibility (case-insensitive). `deny` / `denyPrefixes`\n * still apply and subtract from this set — a header listed in both `allow` and\n * `deny` is NOT forwarded.\n *\n * Footgun: in allowlist mode the built-in DEFAULT denylist (and\n * `useDefaultDenylist`) is BYPASSED — only your `allow` set, minus your own\n * `deny` / `denyPrefixes`, is forwarded. Do NOT allow-list protected/platform\n * headers (e.g. `x-copilotcloud-public-api-key`, `x-forwarded-*`) unless you\n * truly intend to forward them, since the default protection does not apply\n * here.\n */\n allow?: string[];\n}\n\n/**\n * Normalizes a public {@link ForwardHeadersConfig} (or `undefined`) into a\n * fully-resolved {@link ResolvedForwardHeadersPolicy}.\n *\n * - `useDefaultDenylist` defaults to `true` (the built-in denylist is active\n * on upgrade); pass `false` to restore the previous wide-open behavior.\n * - `deny` / `denyPrefixes` extend (do not replace) the defaults.\n * - `allow` activates allowlist mode only when it has at least one non-empty\n * entry after normalization.\n *\n * All names/prefixes are trimmed, lowercased, and stripped of empty/\n * whitespace-only entries before use. Trimming/lowercasing keeps matching a\n * plain set/prefix check against the lowercased inbound keys; dropping empties\n * is a safety guard: a stray `denyPrefixes: [\"\"]` would make `startsWith(\"\")`\n * true for every header (silently denying ALL forwarding). Because empties are\n * dropped BEFORE the allowlist-mode decision, an `allow: [\"\"]` / `allow: [\" \"]`\n * normalizes to an empty set and does NOT switch on allowlist mode — the runtime\n * stays in denylist mode. Allowlist mode activates only when `allow` has at\n * least one non-empty entry; these empty/whitespace-only entries are integrator\n * typos, not intent, so we filter them.\n */\nfunction normalizeHeaderEntries(entries: string[] | undefined): string[] {\n return (entries ?? [])\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0);\n}\n\nexport function resolveForwardHeadersPolicy(\n config: ForwardHeadersConfig | undefined,\n): ResolvedForwardHeadersPolicy {\n const denyNames = new Set<string>(normalizeHeaderEntries(config?.deny));\n const denyPrefixes = normalizeHeaderEntries(config?.denyPrefixes);\n const allowEntries = normalizeHeaderEntries(config?.allow);\n const allow =\n allowEntries.length > 0 ? new Set<string>(allowEntries) : undefined;\n\n return {\n useDefaultDenylist: config?.useDefaultDenylist ?? true,\n denyNames,\n denyPrefixes,\n allow,\n };\n}\n\n/**\n * True iff the (already-lowercased) header name matches the integrator's OWN\n * `deny` / `denyPrefixes`. This is the authoritative subtractive check: it is\n * consulted in BOTH allowlist and denylist mode. It deliberately does NOT\n * include the built-in {@link DEFAULT_DENY_HEADER_NAMES} /\n * {@link DEFAULT_DENY_HEADER_PREFIXES} — an explicit `allow` opts the integrator\n * back into a default-denied header on purpose, so only their own `deny`\n * subtracts from an allowlist.\n */\nfunction matchesIntegratorDeny(\n lower: string,\n policy: ResolvedForwardHeadersPolicy,\n): boolean {\n if (policy.denyNames.has(lower)) return true;\n return policy.denyPrefixes.some((prefix) => lower.startsWith(prefix));\n}\n\n/**\n * Determines if a header should be forwarded under the given resolved policy.\n *\n * The integrator's `deny` / `denyPrefixes` ALWAYS strip, including in allowlist\n * mode: `allow` selects the candidate set, `deny` removes from it. A header the\n * integrator lists in BOTH `allow` and `deny` is NOT forwarded — deny is\n * authoritative so a security-motivated `deny` can never be silently defeated\n * by an overlapping `allow` (the footgun this hardens against).\n *\n * Modes:\n * - Allowlist (`policy.allow` set): forward iff the name is in `allow` AND is\n * NOT matched by the integrator's `deny` / `denyPrefixes`. Nothing else\n * forwards — not even the usual `authorization` / `x-*` eligibility.\n * - Denylist (default): base eligibility is `authorization` or any `x-*`, then\n * the built-in denylist (when enabled) and the integrator's own\n * names/prefixes strip from that set.\n *\n * Note: the built-in default denylist applies ONLY in denylist mode; an\n * explicit `allow` is treated as the integrator deliberately opting back into\n * those headers, so only their OWN `deny` subtracts in allowlist mode.\n */\nexport function shouldForwardHeader(\n headerName: string,\n policy: ResolvedForwardHeadersPolicy,\n): boolean {\n const lower = headerName.toLowerCase();\n\n // Allowlist mode: forward iff explicitly allowed AND not subtracted by the\n // integrator's own deny/denyPrefixes. Nothing else forwards — not even the\n // usual `authorization` / `x-*` eligibility.\n if (policy.allow) {\n return policy.allow.has(lower) && !matchesIntegratorDeny(lower, policy);\n }\n\n // Base eligibility (unchanged): authorization + any x-*.\n const eligible = lower === \"authorization\" || lower.startsWith(\"x-\");\n if (!eligible) return false;\n\n // Built-in denylist (default-on): strip known infra/platform headers.\n if (policy.useDefaultDenylist) {\n if (DEFAULT_DENY_HEADER_NAMES.has(lower)) return false;\n if (\n DEFAULT_DENY_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix))\n ) {\n return false;\n }\n }\n\n // Integrator-supplied additions extend the denylist regardless of the default.\n if (matchesIntegratorDeny(lower, policy)) return false;\n\n return true;\n}\n\n/**\n * Extracts headers that should be forwarded from a Request object, applying the\n * resolved forwarding policy. Keys are normalized to the lowercased form the\n * `Headers` iterator yields.\n */\nexport function extractForwardableHeaders(\n request: Request,\n policy: ResolvedForwardHeadersPolicy,\n): Record<string, string> {\n const forwardableHeaders: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n if (shouldForwardHeader(key, policy)) {\n forwardableHeaders[key] = value;\n }\n });\n return forwardableHeaders;\n}\n\n/**\n * Merges forwardable inbound request headers onto the headers a server\n * explicitly configured on an agent, letting the SERVER-CONFIGURED headers WIN\n * on collision — a server-set service-to-service token (e.g. an IAM bearer)\n * must never be silently overridden by a browser/edge/platform-injected inbound\n * header (#5712).\n *\n * The collision check is case-insensitive: `extractForwardableHeaders`\n * normalizes inbound keys to lowercase (`authorization`) while the server\n * typically configures canonical casing (`Authorization`). A plain object\n * spread would treat those as distinct keys and emit BOTH — which downstream\n * (undici) comma-joins into a single invalid \"multiple JWTs\" value. So we drop\n * any forwarded header the agent already sets, matched case-insensitively, and\n * let non-colliding inbound headers pass through unchanged.\n *\n * The same comma-join hazard exists if the SERVER CONFIG ITSELF contains two\n * case-variants of one header (e.g. both `Authorization` and `authorization`\n * in `agent.headers`). A plain `{ ...serverHeaders }` spread would keep both,\n * so we additionally collapse server-self case-collisions to a SINGLE entry,\n * FIRST-OCCURRENCE WINS: the first key seen (in `Object.keys` order) keeps its\n * exact casing and value, and any later case-variant of that name is dropped.\n * Server-wins-over-inbound and case-insensitive inbound suppression are\n * otherwise unchanged.\n *\n * Breadth (which inbound headers are eligible to forward at all) is decided by\n * `policy` upstream in `extractForwardableHeaders` → `shouldForwardHeader`; the\n * merge never re-widens or re-narrows the set.\n */\nexport function mergeForwardableHeaders(\n serverHeaders: Record<string, string> | undefined,\n request: Request,\n policy: ResolvedForwardHeadersPolicy,\n): Record<string, string> {\n const base = serverHeaders ?? {};\n const merged: Record<string, string> = {};\n const serverHeaderNames = new Set<string>();\n // Collapse server-self case-collisions: first occurrence wins, later\n // case-variants of the same name are dropped.\n for (const [name, value] of Object.entries(base)) {\n const lower = name.toLowerCase();\n if (serverHeaderNames.has(lower)) {\n continue;\n }\n serverHeaderNames.add(lower);\n merged[name] = value;\n }\n for (const [name, value] of Object.entries(\n extractForwardableHeaders(request, policy),\n )) {\n if (!serverHeaderNames.has(name.toLowerCase())) {\n merged[name] = value;\n }\n }\n return merged;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAeA,MAAa,4BAAiD,IAAI,IAAI;CAEpE;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CAEA;CACD,CAAC;;;;;;;AAQF,MAAa,+BAAkD;CAC7D;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;;;;;;;;;;;;;;;AA0ED,SAAS,uBAAuB,SAAyC;AACvE,SAAQ,WAAW,EAAE,EAClB,KAAK,UAAU,MAAM,MAAM,CAAC,aAAa,CAAC,CAC1C,QAAQ,UAAU,MAAM,SAAS,EAAE;;AAGxC,SAAgB,4BACd,QAC8B;CAC9B,MAAM,YAAY,IAAI,IAAY,uBAAuB,QAAQ,KAAK,CAAC;CACvE,MAAM,eAAe,uBAAuB,QAAQ,aAAa;CACjE,MAAM,eAAe,uBAAuB,QAAQ,MAAM;CAC1D,MAAM,QACJ,aAAa,SAAS,IAAI,IAAI,IAAY,aAAa,GAAG;AAE5D,QAAO;EACL,oBAAoB,QAAQ,sBAAsB;EAClD;EACA;EACA;EACD;;;;;;;;;;;AAYH,SAAS,sBACP,OACA,QACS;AACT,KAAI,OAAO,UAAU,IAAI,MAAM,CAAE,QAAO;AACxC,QAAO,OAAO,aAAa,MAAM,WAAW,MAAM,WAAW,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBvE,SAAgB,oBACd,YACA,QACS;CACT,MAAM,QAAQ,WAAW,aAAa;AAKtC,KAAI,OAAO,MACT,QAAO,OAAO,MAAM,IAAI,MAAM,IAAI,CAAC,sBAAsB,OAAO,OAAO;AAKzE,KAAI,EADa,UAAU,mBAAmB,MAAM,WAAW,KAAK,EACrD,QAAO;AAGtB,KAAI,OAAO,oBAAoB;AAC7B,MAAI,0BAA0B,IAAI,MAAM,CAAE,QAAO;AACjD,MACE,6BAA6B,MAAM,WAAW,MAAM,WAAW,OAAO,CAAC,CAEvE,QAAO;;AAKX,KAAI,sBAAsB,OAAO,OAAO,CAAE,QAAO;AAEjD,QAAO;;;;;;;AAQT,SAAgB,0BACd,SACA,QACwB;CACxB,MAAM,qBAA6C,EAAE;AACrD,SAAQ,QAAQ,SAAS,OAAO,QAAQ;AACtC,MAAI,oBAAoB,KAAK,OAAO,CAClC,oBAAmB,OAAO;GAE5B;AACF,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BT,SAAgB,wBACd,eACA,SACA,QACwB;CACxB,MAAM,OAAO,iBAAiB,EAAE;CAChC,MAAM,SAAiC,EAAE;CACzC,MAAM,oCAAoB,IAAI,KAAa;AAG3C,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,EAAE;EAChD,MAAM,QAAQ,KAAK,aAAa;AAChC,MAAI,kBAAkB,IAAI,MAAM,CAC9B;AAEF,oBAAkB,IAAI,MAAM;AAC5B,SAAO,QAAQ;;AAEjB,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QACjC,0BAA0B,SAAS,OAAO,CAC3C,CACC,KAAI,CAAC,kBAAkB,IAAI,KAAK,aAAa,CAAC,CAC5C,QAAO,QAAQ;AAGnB,QAAO"}
|