@ai-sdk/sandbox-vercel 1.0.71 → 1.0.72

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.
@@ -0,0 +1,760 @@
1
+ import {
2
+ HarnessCapabilityUnsupportedError,
3
+ type HarnessV1NetworkPolicy,
4
+ type HarnessV1RequestTransformation,
5
+ } from '@ai-sdk/harness';
6
+ import type {
7
+ NetworkPolicy,
8
+ NetworkPolicyKeyValueMatcher,
9
+ NetworkPolicyMatcher,
10
+ NetworkPolicyRule,
11
+ Sandbox,
12
+ } from '@vercel/sandbox';
13
+
14
+ const VERCEL_PROVIDER_ID = 'vercel-sandbox';
15
+
16
+ type NetworkAccessPolicy =
17
+ | { readonly mode: 'allow-all' }
18
+ | { readonly mode: 'deny-all' }
19
+ | {
20
+ readonly mode: 'custom';
21
+ readonly allowedHosts: ReadonlyArray<string>;
22
+ readonly allowedCIDRs: ReadonlyArray<string>;
23
+ readonly deniedCIDRs: ReadonlyArray<string>;
24
+ };
25
+
26
+ type ForwardRule = {
27
+ readonly host: string;
28
+ readonly match?: NetworkPolicyRule['match'];
29
+ readonly forwardURL: string;
30
+ };
31
+
32
+ type ManagedPolicyState = {
33
+ readonly accessPolicy: NetworkAccessPolicy;
34
+ readonly requestTransformations: ReadonlyArray<HarnessV1RequestTransformation>;
35
+ readonly forwardRules: ReadonlyArray<ForwardRule>;
36
+ readonly effectivePolicy: NetworkPolicy;
37
+ };
38
+
39
+ type CurrentPolicyInspection = {
40
+ readonly accessPolicy: NetworkAccessPolicy;
41
+ readonly forwardRules: ReadonlyArray<ForwardRule>;
42
+ readonly requestTransformationHosts: ReadonlyArray<string>;
43
+ readonly currentPolicy: NetworkPolicy;
44
+ };
45
+
46
+ /**
47
+ * Owns the complete Vercel Sandbox network-policy value for one live session.
48
+ * Vercel combines network access, request transformations, and forwarding in a
49
+ * single structure, so every mutation must be composed here before replacing
50
+ * that structure through `sandbox.update()`.
51
+ */
52
+ export class VercelNetworkPolicyManager {
53
+ readonly #sandbox: Sandbox;
54
+ #state: ManagedPolicyState | undefined;
55
+ #mutationQueue: Promise<void> = Promise.resolve();
56
+
57
+ constructor({ sandbox }: { sandbox: Sandbox }) {
58
+ this.#sandbox = sandbox;
59
+ }
60
+
61
+ setNetworkPolicy(policy: HarnessV1NetworkPolicy): Promise<void> {
62
+ return this.#enqueueMutation(async () => {
63
+ const inspection = this.#inspectPolicy();
64
+ if (
65
+ this.#state == null &&
66
+ inspection.requestTransformationHosts.length > 0
67
+ ) {
68
+ throw createPolicyConflictError(
69
+ 'Cannot set the network policy because the current Vercel Sandbox policy contains request transformations whose redacted values cannot be preserved safely. Replace them explicitly with setRequestTransformations() or rehydrate them with addRequestTransformations() first.',
70
+ );
71
+ }
72
+
73
+ await this.#applyState({
74
+ accessPolicy: toNetworkAccessPolicy(policy),
75
+ requestTransformations:
76
+ this.#state?.requestTransformations.map(cloneRequestTransformation) ??
77
+ [],
78
+ forwardRules: inspection.forwardRules.map(cloneForwardRule),
79
+ currentPolicy: inspection.currentPolicy,
80
+ });
81
+ });
82
+ }
83
+
84
+ setRequestTransformations(
85
+ transformations: ReadonlyArray<HarnessV1RequestTransformation>,
86
+ ): Promise<void> {
87
+ return this.#enqueueMutation(async () => {
88
+ const inspection = this.#inspectPolicy();
89
+ await this.#applyState({
90
+ accessPolicy: inspection.accessPolicy,
91
+ requestTransformations: transformations.map(cloneRequestTransformation),
92
+ forwardRules: inspection.forwardRules.map(cloneForwardRule),
93
+ currentPolicy: inspection.currentPolicy,
94
+ });
95
+ });
96
+ }
97
+
98
+ addRequestTransformations(
99
+ transformations: ReadonlyArray<HarnessV1RequestTransformation>,
100
+ ): Promise<void> {
101
+ return this.#enqueueMutation(async () => {
102
+ const inspection = this.#inspectPolicy();
103
+ const incomingTransformations = transformations.map(
104
+ cloneRequestTransformation,
105
+ );
106
+
107
+ if (this.#state == null) {
108
+ const incomingPolicy = composeNetworkPolicy({
109
+ accessPolicy: inspection.accessPolicy,
110
+ requestTransformations: incomingTransformations,
111
+ forwardRules: inspection.forwardRules,
112
+ });
113
+ const incomingHosts = getRequestTransformationHosts(incomingPolicy);
114
+
115
+ /*
116
+ * Vercel redacts transformed header values when a policy is read back.
117
+ * It can also normalize rule details, so initialization and resume can
118
+ * only attribute existing transformations by their materialized hosts.
119
+ * External transformations for the same hosts are indistinguishable
120
+ * and are necessarily treated as managed by this call.
121
+ */
122
+ if (
123
+ !isHostSetSubset({
124
+ subset: inspection.requestTransformationHosts,
125
+ superset: incomingHosts,
126
+ })
127
+ ) {
128
+ throw createPolicyConflictError(
129
+ 'Cannot add request transformations because the current Vercel Sandbox policy contains request transformations that cannot be attributed to this call. Their header values are redacted, so preserving them safely is not possible.',
130
+ );
131
+ }
132
+ }
133
+
134
+ await this.#applyState({
135
+ accessPolicy: inspection.accessPolicy,
136
+ requestTransformations:
137
+ this.#state == null
138
+ ? incomingTransformations
139
+ : [
140
+ ...this.#state.requestTransformations.map(
141
+ cloneRequestTransformation,
142
+ ),
143
+ ...incomingTransformations,
144
+ ],
145
+ forwardRules: inspection.forwardRules.map(cloneForwardRule),
146
+ currentPolicy: inspection.currentPolicy,
147
+ });
148
+ });
149
+ }
150
+
151
+ #enqueueMutation(operation: () => Promise<void>): Promise<void> {
152
+ /*
153
+ * Each mutation derives a complete replacement from private state. The
154
+ * whole inspect, compose, update, and commit sequence must therefore run in
155
+ * invocation order, while a failed operation must not poison the queue.
156
+ */
157
+ const result = this.#mutationQueue.then(operation);
158
+ this.#mutationQueue = result.then(
159
+ () => undefined,
160
+ () => undefined,
161
+ );
162
+ return result;
163
+ }
164
+
165
+ #inspectPolicy(): CurrentPolicyInspection {
166
+ /*
167
+ * Once initialized, this manager is the sole supported owner of the
168
+ * session's network policy. Direct native Sandbox policy mutations can
169
+ * make the private desired state stale and are intentionally not tracked.
170
+ */
171
+ if (this.#state != null) {
172
+ return {
173
+ accessPolicy: this.#state.accessPolicy,
174
+ forwardRules: this.#state.forwardRules,
175
+ requestTransformationHosts: [],
176
+ currentPolicy: this.#state.effectivePolicy,
177
+ };
178
+ }
179
+
180
+ return inspectCurrentPolicy(
181
+ this.#sandbox.currentSession().networkPolicy ?? 'allow-all',
182
+ );
183
+ }
184
+
185
+ async #applyState({
186
+ accessPolicy,
187
+ requestTransformations,
188
+ forwardRules,
189
+ currentPolicy,
190
+ }: {
191
+ accessPolicy: NetworkAccessPolicy;
192
+ requestTransformations: ReadonlyArray<HarnessV1RequestTransformation>;
193
+ forwardRules: ReadonlyArray<ForwardRule>;
194
+ currentPolicy: NetworkPolicy;
195
+ }): Promise<void> {
196
+ const effectivePolicy = composeNetworkPolicy({
197
+ accessPolicy,
198
+ requestTransformations,
199
+ forwardRules,
200
+ });
201
+
202
+ if (
203
+ !areNetworkPoliciesEqual({
204
+ first: currentPolicy,
205
+ second: effectivePolicy,
206
+ })
207
+ ) {
208
+ await this.#sandbox.update({ networkPolicy: effectivePolicy });
209
+ }
210
+
211
+ this.#state = {
212
+ accessPolicy: cloneNetworkAccessPolicy(accessPolicy),
213
+ requestTransformations: requestTransformations.map(
214
+ cloneRequestTransformation,
215
+ ),
216
+ forwardRules: forwardRules.map(cloneForwardRule),
217
+ effectivePolicy,
218
+ };
219
+ }
220
+ }
221
+
222
+ function inspectCurrentPolicy(policy: NetworkPolicy): CurrentPolicyInspection {
223
+ if (policy === 'allow-all' || policy === 'deny-all') {
224
+ return {
225
+ accessPolicy: { mode: policy },
226
+ forwardRules: [],
227
+ requestTransformationHosts: [],
228
+ currentPolicy: policy,
229
+ };
230
+ }
231
+
232
+ const allowedHosts = Array.isArray(policy.allow)
233
+ ? [...policy.allow]
234
+ : Object.keys(policy.allow ?? {});
235
+ const forwardRules: ForwardRule[] = [];
236
+ const requestTransformationHosts = new Set<string>();
237
+
238
+ /*
239
+ * Forwarding URLs and matchers survive Vercel's policy readback and can be
240
+ * retained verbatim. Transformed header values do not, so transformations
241
+ * are represented only by fingerprints until a caller supplies the values.
242
+ */
243
+ if (policy.allow != null && !Array.isArray(policy.allow)) {
244
+ for (const [host, rules] of Object.entries(policy.allow)) {
245
+ for (const rule of rules) {
246
+ if (hasRequestTransformation(rule)) {
247
+ requestTransformationHosts.add(host.toLowerCase());
248
+ }
249
+ if (rule.forwardURL != null) {
250
+ forwardRules.push({
251
+ host,
252
+ ...(rule.match == null ? {} : { match: cloneMatch(rule.match) }),
253
+ forwardURL: rule.forwardURL,
254
+ });
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ return {
261
+ accessPolicy: {
262
+ mode: 'custom',
263
+ allowedHosts: minimizeHostPatterns(allowedHosts),
264
+ allowedCIDRs: [...(policy.subnets?.allow ?? [])],
265
+ deniedCIDRs: [...(policy.subnets?.deny ?? [])],
266
+ },
267
+ forwardRules,
268
+ requestTransformationHosts: [...requestTransformationHosts],
269
+ currentPolicy: cloneNetworkPolicy(policy),
270
+ };
271
+ }
272
+
273
+ function toNetworkAccessPolicy(
274
+ policy: HarnessV1NetworkPolicy,
275
+ ): NetworkAccessPolicy {
276
+ switch (policy.mode) {
277
+ case 'allow-all':
278
+ return { mode: 'allow-all' };
279
+ case 'deny-all':
280
+ return { mode: 'deny-all' };
281
+ case 'custom': {
282
+ const allowedHosts = [...(policy.allowedHosts ?? [])];
283
+ const allowedCIDRs = [...(policy.allowedCIDRs ?? [])];
284
+ const deniedCIDRs = [...(policy.deniedCIDRs ?? [])];
285
+ if (
286
+ allowedHosts.length === 0 &&
287
+ allowedCIDRs.length === 0 &&
288
+ deniedCIDRs.length === 0
289
+ ) {
290
+ throw createPolicyConflictError(
291
+ 'Custom network policy requires at least one of allowedHosts, allowedCIDRs, or deniedCIDRs to be non-empty.',
292
+ );
293
+ }
294
+ return {
295
+ mode: 'custom',
296
+ allowedHosts,
297
+ allowedCIDRs,
298
+ deniedCIDRs,
299
+ };
300
+ }
301
+ }
302
+ }
303
+
304
+ function composeNetworkPolicy({
305
+ accessPolicy,
306
+ requestTransformations,
307
+ forwardRules,
308
+ }: {
309
+ accessPolicy: NetworkAccessPolicy;
310
+ requestTransformations: ReadonlyArray<HarnessV1RequestTransformation>;
311
+ forwardRules: ReadonlyArray<ForwardRule>;
312
+ }): NetworkPolicy {
313
+ /*
314
+ * Access is authoritative even though Vercel stores all three concepts in
315
+ * one allow map. Rules without a hostname intersection are omitted from the
316
+ * effective policy but remain in private desired state, so a later access
317
+ * change can activate them. CIDR access never activates hostname rules.
318
+ */
319
+ if (accessPolicy.mode === 'deny-all') {
320
+ return 'deny-all';
321
+ }
322
+
323
+ const rulesByHost = new Map<string, NetworkPolicyRule[]>();
324
+ const allowedHosts =
325
+ accessPolicy.mode === 'allow-all' ? ['*'] : accessPolicy.allowedHosts;
326
+
327
+ for (const host of allowedHosts) {
328
+ rulesByHost.set(host, []);
329
+ }
330
+
331
+ for (const transformation of requestTransformations) {
332
+ for (const host of getActiveHostPatterns({
333
+ allowedHosts,
334
+ ruleHost: transformation.match.host,
335
+ })) {
336
+ appendRule({
337
+ rulesByHost,
338
+ host,
339
+ rule: toVercelRequestTransformationRule(transformation),
340
+ });
341
+ }
342
+ }
343
+
344
+ /*
345
+ * Transformations and forwarding rules for one host must share the same
346
+ * Vercel allow-map array. Transformations are inserted first to match the
347
+ * ordering produced when the Vercel SDK reconstructs a policy.
348
+ */
349
+ for (const forwardRule of forwardRules) {
350
+ for (const host of getActiveHostPatterns({
351
+ allowedHosts,
352
+ ruleHost: forwardRule.host,
353
+ })) {
354
+ appendRule({
355
+ rulesByHost,
356
+ host,
357
+ rule: {
358
+ ...(forwardRule.match == null
359
+ ? {}
360
+ : { match: cloneMatch(forwardRule.match) }),
361
+ forwardURL: forwardRule.forwardURL,
362
+ },
363
+ });
364
+ }
365
+ }
366
+
367
+ const hasActiveRules = [...rulesByHost.values()].some(
368
+ rules => rules.length > 0,
369
+ );
370
+ if (!hasActiveRules) {
371
+ return toVercelAccessPolicy(accessPolicy);
372
+ }
373
+
374
+ return {
375
+ allow: Object.fromEntries(rulesByHost),
376
+ ...(accessPolicy.mode !== 'custom' ? {} : toVercelSubnets(accessPolicy)),
377
+ };
378
+ }
379
+
380
+ function toVercelAccessPolicy(
381
+ accessPolicy: NetworkAccessPolicy,
382
+ ): NetworkPolicy {
383
+ if (accessPolicy.mode === 'allow-all' || accessPolicy.mode === 'deny-all') {
384
+ return accessPolicy.mode;
385
+ }
386
+
387
+ return {
388
+ ...(accessPolicy.allowedHosts.length === 0
389
+ ? {}
390
+ : { allow: [...accessPolicy.allowedHosts] }),
391
+ ...toVercelSubnets(accessPolicy),
392
+ };
393
+ }
394
+
395
+ function toVercelSubnets(
396
+ accessPolicy: Extract<NetworkAccessPolicy, { mode: 'custom' }>,
397
+ ): Pick<Exclude<NetworkPolicy, string>, 'subnets'> {
398
+ if (
399
+ accessPolicy.allowedCIDRs.length === 0 &&
400
+ accessPolicy.deniedCIDRs.length === 0
401
+ ) {
402
+ return {};
403
+ }
404
+ return {
405
+ subnets: {
406
+ ...(accessPolicy.allowedCIDRs.length === 0
407
+ ? {}
408
+ : { allow: [...accessPolicy.allowedCIDRs] }),
409
+ ...(accessPolicy.deniedCIDRs.length === 0
410
+ ? {}
411
+ : { deny: [...accessPolicy.deniedCIDRs] }),
412
+ },
413
+ };
414
+ }
415
+
416
+ function getActiveHostPatterns({
417
+ allowedHosts,
418
+ ruleHost,
419
+ }: {
420
+ allowedHosts: ReadonlyArray<string>;
421
+ ruleHost: string;
422
+ }): ReadonlyArray<string> {
423
+ return minimizeHostPatterns(
424
+ allowedHosts.flatMap(allowedHost => {
425
+ const intersection = intersectHostPatterns({
426
+ first: allowedHost,
427
+ second: ruleHost,
428
+ });
429
+ return intersection == null ? [] : [intersection];
430
+ }),
431
+ );
432
+ }
433
+
434
+ function intersectHostPatterns({
435
+ first,
436
+ second,
437
+ }: {
438
+ first: string;
439
+ second: string;
440
+ }): string | undefined {
441
+ if (isHostPatternSubset({ candidate: first, container: second })) {
442
+ return first;
443
+ }
444
+ if (isHostPatternSubset({ candidate: second, container: first })) {
445
+ return second;
446
+ }
447
+ return undefined;
448
+ }
449
+
450
+ function isHostPatternSubset({
451
+ candidate,
452
+ container,
453
+ }: {
454
+ candidate: string;
455
+ container: string;
456
+ }): boolean {
457
+ const normalizedCandidate = candidate.toLowerCase();
458
+ const normalizedContainer = container.toLowerCase();
459
+ if (
460
+ normalizedCandidate === normalizedContainer ||
461
+ normalizedContainer === '*'
462
+ ) {
463
+ return true;
464
+ }
465
+ if (normalizedCandidate === '*' || !normalizedContainer.startsWith('*')) {
466
+ return false;
467
+ }
468
+
469
+ const containerSuffix = normalizedContainer.slice(1);
470
+ return normalizedCandidate.startsWith('*')
471
+ ? normalizedCandidate.slice(1).endsWith(containerSuffix)
472
+ : normalizedCandidate.endsWith(containerSuffix);
473
+ }
474
+
475
+ function minimizeHostPatterns(
476
+ patterns: ReadonlyArray<string>,
477
+ ): ReadonlyArray<string> {
478
+ const uniquePatterns = [...new Set(patterns)];
479
+ return uniquePatterns.filter(
480
+ pattern =>
481
+ !uniquePatterns.some(
482
+ other =>
483
+ other !== pattern &&
484
+ isHostPatternSubset({ candidate: pattern, container: other }),
485
+ ),
486
+ );
487
+ }
488
+
489
+ function appendRule({
490
+ rulesByHost,
491
+ host,
492
+ rule,
493
+ }: {
494
+ rulesByHost: Map<string, NetworkPolicyRule[]>;
495
+ host: string;
496
+ rule: NetworkPolicyRule;
497
+ }): void {
498
+ const rules = rulesByHost.get(host) ?? [];
499
+ rules.push(rule);
500
+ rulesByHost.set(host, rules);
501
+ }
502
+
503
+ function toVercelRequestTransformationRule(
504
+ transformation: HarnessV1RequestTransformation,
505
+ ): NetworkPolicyRule {
506
+ const {
507
+ host: _host,
508
+ method,
509
+ path,
510
+ queryString,
511
+ headers,
512
+ } = transformation.match;
513
+ const match = {
514
+ ...(path == null ? {} : { path: cloneMatcher(path) }),
515
+ ...(method == null ? {} : { method: [...method] }),
516
+ ...(queryString == null
517
+ ? {}
518
+ : { queryString: queryString.map(cloneKeyValueMatcher) }),
519
+ ...(headers == null ? {} : { headers: headers.map(cloneKeyValueMatcher) }),
520
+ };
521
+
522
+ return {
523
+ ...(Object.keys(match).length === 0 ? {} : { match }),
524
+ transform: [{ headers: { ...transformation.transform.headers } }],
525
+ };
526
+ }
527
+
528
+ function hasRequestTransformation(rule: NetworkPolicyRule): boolean {
529
+ return (
530
+ rule.transform?.some(
531
+ transformation =>
532
+ transformation.headers != null &&
533
+ Object.keys(transformation.headers).length > 0,
534
+ ) ?? false
535
+ );
536
+ }
537
+
538
+ function getRequestTransformationHosts(
539
+ policy: NetworkPolicy,
540
+ ): ReadonlyArray<string> {
541
+ if (
542
+ policy === 'allow-all' ||
543
+ policy === 'deny-all' ||
544
+ policy.allow == null ||
545
+ Array.isArray(policy.allow)
546
+ ) {
547
+ return [];
548
+ }
549
+
550
+ const hosts = new Set<string>();
551
+ for (const [host, rules] of Object.entries(policy.allow)) {
552
+ for (const rule of rules) {
553
+ if (hasRequestTransformation(rule)) {
554
+ hosts.add(host.toLowerCase());
555
+ }
556
+ }
557
+ }
558
+ return [...hosts];
559
+ }
560
+
561
+ function isHostSetSubset({
562
+ subset,
563
+ superset,
564
+ }: {
565
+ subset: ReadonlyArray<string>;
566
+ superset: ReadonlyArray<string>;
567
+ }): boolean {
568
+ const allowedHosts = new Set(superset.map(host => host.toLowerCase()));
569
+ return subset.every(host => allowedHosts.has(host.toLowerCase()));
570
+ }
571
+
572
+ function areNetworkPoliciesEqual({
573
+ first,
574
+ second,
575
+ }: {
576
+ first: NetworkPolicy;
577
+ second: NetworkPolicy;
578
+ }): boolean {
579
+ return stableSerialize(first) === stableSerialize(second);
580
+ }
581
+
582
+ function stableSerialize(value: unknown): string {
583
+ if (Array.isArray(value)) {
584
+ return `[${value.map(stableSerialize).join(',')}]`;
585
+ }
586
+ if (value != null && typeof value === 'object') {
587
+ return `{${Object.entries(value)
588
+ .filter(([, entryValue]) => entryValue !== undefined)
589
+ .sort(([firstKey], [secondKey]) => firstKey.localeCompare(secondKey))
590
+ .map(
591
+ ([key, entryValue]) =>
592
+ `${JSON.stringify(key)}:${stableSerialize(entryValue)}`,
593
+ )
594
+ .join(',')}}`;
595
+ }
596
+ return JSON.stringify(value);
597
+ }
598
+
599
+ function cloneRequestTransformation(
600
+ transformation: HarnessV1RequestTransformation,
601
+ ): HarnessV1RequestTransformation {
602
+ return {
603
+ match: {
604
+ host: transformation.match.host,
605
+ ...(transformation.match.path == null
606
+ ? {}
607
+ : { path: cloneHarnessMatcher(transformation.match.path) }),
608
+ ...(transformation.match.method == null
609
+ ? {}
610
+ : { method: [...transformation.match.method] }),
611
+ ...(transformation.match.queryString == null
612
+ ? {}
613
+ : {
614
+ queryString: transformation.match.queryString.map(
615
+ cloneHarnessKeyValueMatcher,
616
+ ),
617
+ }),
618
+ ...(transformation.match.headers == null
619
+ ? {}
620
+ : {
621
+ headers: transformation.match.headers.map(
622
+ cloneHarnessKeyValueMatcher,
623
+ ),
624
+ }),
625
+ },
626
+ transform: { headers: { ...transformation.transform.headers } },
627
+ };
628
+ }
629
+
630
+ function cloneForwardRule(rule: ForwardRule): ForwardRule {
631
+ return {
632
+ host: rule.host,
633
+ ...(rule.match == null ? {} : { match: cloneMatch(rule.match) }),
634
+ forwardURL: rule.forwardURL,
635
+ };
636
+ }
637
+
638
+ function cloneNetworkAccessPolicy(
639
+ policy: NetworkAccessPolicy,
640
+ ): NetworkAccessPolicy {
641
+ if (policy.mode === 'allow-all' || policy.mode === 'deny-all') {
642
+ return { mode: policy.mode };
643
+ }
644
+ return {
645
+ mode: 'custom',
646
+ allowedHosts: [...policy.allowedHosts],
647
+ allowedCIDRs: [...policy.allowedCIDRs],
648
+ deniedCIDRs: [...policy.deniedCIDRs],
649
+ };
650
+ }
651
+
652
+ function cloneNetworkPolicy(policy: NetworkPolicy): NetworkPolicy {
653
+ if (policy === 'allow-all' || policy === 'deny-all') {
654
+ return policy;
655
+ }
656
+ return {
657
+ ...(policy.allow == null
658
+ ? {}
659
+ : Array.isArray(policy.allow)
660
+ ? { allow: [...policy.allow] }
661
+ : {
662
+ allow: Object.fromEntries(
663
+ Object.entries(policy.allow).map(([host, rules]) => [
664
+ host,
665
+ rules.map(rule => ({
666
+ ...(rule.match == null
667
+ ? {}
668
+ : { match: cloneMatch(rule.match) }),
669
+ ...(rule.transform == null
670
+ ? {}
671
+ : {
672
+ transform: rule.transform.map(transform => ({
673
+ ...(transform.headers == null
674
+ ? {}
675
+ : { headers: { ...transform.headers } }),
676
+ })),
677
+ }),
678
+ ...(rule.forwardURL == null
679
+ ? {}
680
+ : { forwardURL: rule.forwardURL }),
681
+ })),
682
+ ]),
683
+ ),
684
+ }),
685
+ ...(policy.subnets == null
686
+ ? {}
687
+ : {
688
+ subnets: {
689
+ ...(policy.subnets.allow == null
690
+ ? {}
691
+ : { allow: [...policy.subnets.allow] }),
692
+ ...(policy.subnets.deny == null
693
+ ? {}
694
+ : { deny: [...policy.subnets.deny] }),
695
+ },
696
+ }),
697
+ };
698
+ }
699
+
700
+ function cloneMatch(
701
+ match: NonNullable<NetworkPolicyRule['match']>,
702
+ ): NonNullable<NetworkPolicyRule['match']> {
703
+ return {
704
+ ...(match.path == null ? {} : { path: cloneMatcher(match.path) }),
705
+ ...(match.method == null ? {} : { method: [...match.method] }),
706
+ ...(match.queryString == null
707
+ ? {}
708
+ : { queryString: match.queryString.map(cloneKeyValueMatcher) }),
709
+ ...(match.headers == null
710
+ ? {}
711
+ : { headers: match.headers.map(cloneKeyValueMatcher) }),
712
+ };
713
+ }
714
+
715
+ function cloneKeyValueMatcher(
716
+ matcher: NetworkPolicyKeyValueMatcher,
717
+ ): NetworkPolicyKeyValueMatcher {
718
+ return {
719
+ ...(matcher.key == null ? {} : { key: cloneMatcher(matcher.key) }),
720
+ ...(matcher.value == null ? {} : { value: cloneMatcher(matcher.value) }),
721
+ };
722
+ }
723
+
724
+ function cloneMatcher(matcher: NetworkPolicyMatcher): NetworkPolicyMatcher {
725
+ return { ...matcher };
726
+ }
727
+
728
+ function cloneHarnessKeyValueMatcher(
729
+ matcher: NonNullable<
730
+ HarnessV1RequestTransformation['match']['headers']
731
+ >[number],
732
+ ): NonNullable<HarnessV1RequestTransformation['match']['headers']>[number] {
733
+ return {
734
+ ...(matcher.key == null ? {} : { key: cloneHarnessMatcher(matcher.key) }),
735
+ ...(matcher.value == null
736
+ ? {}
737
+ : { value: cloneHarnessMatcher(matcher.value) }),
738
+ };
739
+ }
740
+
741
+ function cloneHarnessMatcher(
742
+ matcher: NonNullable<HarnessV1RequestTransformation['match']['path']>,
743
+ ): NonNullable<HarnessV1RequestTransformation['match']['path']> {
744
+ if ('exact' in matcher) {
745
+ return { exact: matcher.exact };
746
+ }
747
+ if ('startsWith' in matcher) {
748
+ return { startsWith: matcher.startsWith };
749
+ }
750
+ return { regex: matcher.regex };
751
+ }
752
+
753
+ function createPolicyConflictError(
754
+ message: string,
755
+ ): HarnessCapabilityUnsupportedError {
756
+ return new HarnessCapabilityUnsupportedError({
757
+ harnessId: VERCEL_PROVIDER_ID,
758
+ message,
759
+ });
760
+ }