@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/AUTHORING.md +102 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +100 -28
  4. package/bin/apifuse-check.ts +78 -71
  5. package/bin/apifuse-create.ts +12 -0
  6. package/bin/apifuse-dev.ts +24 -61
  7. package/bin/apifuse-pack-check.ts +47 -0
  8. package/bin/apifuse-perf.ts +33 -32
  9. package/bin/apifuse-record.ts +17 -7
  10. package/bin/apifuse-test.ts +6 -4
  11. package/bin/apifuse.ts +36 -35
  12. package/package.json +28 -9
  13. package/src/ceremonies/index.ts +747 -0
  14. package/src/cli/commands.ts +87 -0
  15. package/src/cli/create.ts +845 -0
  16. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  17. package/src/cli/templates/provider/README.md.tpl +28 -0
  18. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  19. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  20. package/src/cli/templates/provider/index.ts.tpl +54 -0
  21. package/src/cli/templates/provider/start.ts.tpl +5 -0
  22. package/src/composite.ts +43 -0
  23. package/src/define.ts +527 -41
  24. package/src/dev.ts +2 -6
  25. package/src/errors.ts +42 -0
  26. package/src/index.ts +50 -38
  27. package/src/lint.ts +574 -0
  28. package/src/provider.ts +14 -0
  29. package/src/runtime/auth-flow.ts +67 -0
  30. package/src/runtime/credential.ts +95 -0
  31. package/src/runtime/env.ts +13 -0
  32. package/src/runtime/executor.ts +13 -14
  33. package/src/runtime/http.ts +10 -2
  34. package/src/runtime/insights.ts +3 -3
  35. package/src/runtime/key-derivation.ts +122 -0
  36. package/src/runtime/keyring.ts +148 -0
  37. package/src/runtime/namespace.ts +33 -0
  38. package/src/runtime/prevalidate.ts +252 -0
  39. package/src/runtime/tls.ts +20 -5
  40. package/src/runtime/waterfall.ts +0 -1
  41. package/src/schema.ts +77 -0
  42. package/src/serve.ts +1 -664
  43. package/src/server/index.ts +22 -0
  44. package/src/server/serve.ts +610 -0
  45. package/src/server/types.ts +78 -0
  46. package/src/stealth/profiles.ts +10 -93
  47. package/src/testing/run.ts +391 -32
  48. package/src/types.ts +364 -41
  49. package/bin/apifuse-init.ts +0 -387
  50. package/src/__tests__/auth.test.ts +0 -396
  51. package/src/__tests__/browser-auth.test.ts +0 -180
  52. package/src/__tests__/browser.test.ts +0 -632
  53. package/src/__tests__/define.test.ts +0 -225
  54. package/src/__tests__/errors.test.ts +0 -69
  55. package/src/__tests__/executor.test.ts +0 -214
  56. package/src/__tests__/http.test.ts +0 -238
  57. package/src/__tests__/insights.test.ts +0 -210
  58. package/src/__tests__/instrumentation.test.ts +0 -290
  59. package/src/__tests__/otlp.test.ts +0 -141
  60. package/src/__tests__/perf.test.ts +0 -60
  61. package/src/__tests__/providers-yaml.test.ts +0 -135
  62. package/src/__tests__/proxy.test.ts +0 -359
  63. package/src/__tests__/recipes.test.ts +0 -36
  64. package/src/__tests__/serve.test.ts +0 -233
  65. package/src/__tests__/session.test.ts +0 -231
  66. package/src/__tests__/state.test.ts +0 -100
  67. package/src/__tests__/stealth.test.ts +0 -57
  68. package/src/__tests__/testing.test.ts +0 -97
  69. package/src/__tests__/tls.test.ts +0 -345
  70. package/src/__tests__/types.test.ts +0 -142
  71. package/src/__tests__/utils.test.ts +0 -62
  72. package/src/__tests__/waterfall.test.ts +0 -270
  73. package/src/config/providers-yaml.ts +0 -370
  74. package/src/index.test.ts +0 -1
  75. package/src/protocol.ts +0 -183
  76. package/src/runtime/auth.ts +0 -245
  77. package/src/runtime/session.ts +0 -573
  78. package/src/runtime/state.ts +0 -124
package/src/lint.ts ADDED
@@ -0,0 +1,574 @@
1
+ import type { ZodType } from "zod";
2
+
3
+ type AuthModeLike =
4
+ | "none"
5
+ | "platform-managed"
6
+ | "credentials"
7
+ | "oauth2"
8
+ | "api-key";
9
+
10
+ type ProviderAuthLike = {
11
+ mode?: AuthModeLike;
12
+ flow?: {
13
+ start?: unknown;
14
+ continue?: unknown;
15
+ poll?: unknown;
16
+ abort?: unknown;
17
+ };
18
+ };
19
+
20
+ type SchemaLike = ZodType & {
21
+ description?: string;
22
+ def?: Record<string, unknown>;
23
+ _def?: Record<string, unknown>;
24
+ shape?: Record<string, SchemaLike> | (() => Record<string, SchemaLike>);
25
+ element?: SchemaLike;
26
+ items?: SchemaLike[];
27
+ options?: SchemaLike[] | Set<SchemaLike> | Map<string, SchemaLike>;
28
+ innerType?: SchemaLike;
29
+ sourceType?: () => SchemaLike;
30
+ unwrap?: () => SchemaLike;
31
+ in?: SchemaLike;
32
+ out?: SchemaLike;
33
+ left?: SchemaLike;
34
+ right?: SchemaLike;
35
+ };
36
+
37
+ export interface LintDiagnostic {
38
+ rule: string;
39
+ level: "error" | "warn";
40
+ message: string;
41
+ field?: string;
42
+ }
43
+
44
+ function lintAllowedHosts(
45
+ providerId: string | undefined,
46
+ allowedHosts: string[] | undefined,
47
+ ): LintDiagnostic[] {
48
+ const prefix = providerId ? `Provider "${providerId}"` : "Provider";
49
+
50
+ if (!allowedHosts) {
51
+ return [
52
+ {
53
+ rule: "allowed-hosts-required",
54
+ level: "error",
55
+ field: "allowedHosts",
56
+ message: `${prefix} must declare allowedHosts.`,
57
+ },
58
+ ];
59
+ }
60
+
61
+ if (allowedHosts.length === 0) {
62
+ return [
63
+ {
64
+ rule: "allowed-hosts-non-empty",
65
+ level: "error",
66
+ field: "allowedHosts",
67
+ message: `${prefix} must declare at least one allowed host.`,
68
+ },
69
+ ];
70
+ }
71
+
72
+ const wildcardHost = allowedHosts.find((host) => host.trim().includes("*"));
73
+ if (wildcardHost) {
74
+ return [
75
+ {
76
+ rule: "allowed-hosts-no-wildcards",
77
+ level: "error",
78
+ field: "allowedHosts",
79
+ message: `${prefix} must not declare wildcard allowedHosts entries like "${wildcardHost}".`,
80
+ },
81
+ ];
82
+ }
83
+
84
+ return [];
85
+ }
86
+
87
+ function lintReviewed(
88
+ providerId: string | undefined,
89
+ reviewed: string | undefined,
90
+ ): LintDiagnostic[] {
91
+ if (reviewed === "first-party" || reviewed === "community") {
92
+ return [];
93
+ }
94
+
95
+ const prefix = providerId ? `Provider "${providerId}"` : "Provider";
96
+ return [
97
+ {
98
+ rule: "reviewed-required",
99
+ level: "error",
100
+ field: "reviewed",
101
+ message: `${prefix} must declare reviewed as "first-party" or "community".`,
102
+ },
103
+ ];
104
+ }
105
+
106
+ function hasReusableSecretKeys(keys: string[] | undefined): boolean {
107
+ if (!keys) {
108
+ return false;
109
+ }
110
+
111
+ return keys.some((key) =>
112
+ /(access_token|refresh_token|password|secret|cookie|session|token|api[_-]?key)/i.test(
113
+ key,
114
+ ),
115
+ );
116
+ }
117
+
118
+ function getAuthFlowSource(provider: {
119
+ auth?: ProviderAuthLike;
120
+ authFlowSource?: string;
121
+ }): string {
122
+ if (provider.authFlowSource) {
123
+ return provider.authFlowSource;
124
+ }
125
+
126
+ const parts = [
127
+ provider.auth?.flow?.start,
128
+ provider.auth?.flow?.continue,
129
+ provider.auth?.flow?.poll,
130
+ provider.auth?.flow?.abort,
131
+ ];
132
+
133
+ return parts
134
+ .filter(
135
+ (part): part is (...args: unknown[]) => unknown =>
136
+ typeof part === "function",
137
+ )
138
+ .map((part) => part.toString())
139
+ .join("\n");
140
+ }
141
+
142
+ function lintAuthModel(provider: {
143
+ id?: string;
144
+ auth?: ProviderAuthLike;
145
+ credential?: {
146
+ keys?: string[];
147
+ storesReusableSecret?: boolean;
148
+ justification?: string;
149
+ };
150
+ context?: {
151
+ keys?: string[];
152
+ };
153
+ authFlowSource?: string;
154
+ }): LintDiagnostic[] {
155
+ const diagnostics: LintDiagnostic[] = [];
156
+ const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
157
+ const authMode = provider.auth?.mode;
158
+ const credentialKeys = provider.credential?.keys ?? [];
159
+
160
+ if (authMode === "api-key") {
161
+ diagnostics.push({
162
+ rule: "auth-mode-api-key-removed",
163
+ level: "error",
164
+ field: "auth.mode",
165
+ message: `${providerLabel} must not use auth.mode "api-key".`,
166
+ });
167
+ }
168
+
169
+ if (
170
+ (authMode === "credentials" || authMode === "oauth2") &&
171
+ typeof provider.auth?.flow?.continue !== "function"
172
+ ) {
173
+ diagnostics.push({
174
+ rule: "auth-flow-continue-required",
175
+ level: "error",
176
+ field: "auth.flow.continue",
177
+ message: `${providerLabel} must define auth.flow.continue for ${authMode} auth mode.`,
178
+ });
179
+ }
180
+
181
+ if (authMode === "credentials" && credentialKeys.length === 0) {
182
+ diagnostics.push({
183
+ rule: "credential-keys-required-when-credentials-mode",
184
+ level: "error",
185
+ field: "credential.keys",
186
+ message: `${providerLabel} must declare credential.keys for credentials auth mode.`,
187
+ });
188
+ }
189
+
190
+ if (
191
+ hasReusableSecretKeys(credentialKeys) &&
192
+ (!provider.credential?.storesReusableSecret ||
193
+ !provider.credential.justification)
194
+ ) {
195
+ diagnostics.push({
196
+ rule: "credential-reusable-secret",
197
+ level: "error",
198
+ field: "credential",
199
+ message: `${providerLabel} must set storesReusableSecret and justification when credential.keys includes reusable secrets.`,
200
+ });
201
+ }
202
+
203
+ if (authMode === "platform-managed" && credentialKeys.length > 0) {
204
+ diagnostics.push({
205
+ rule: "platform-managed-no-credential-keys",
206
+ level: "error",
207
+ field: "credential.keys",
208
+ message: `${providerLabel} must not declare credential.keys for platform-managed auth mode.`,
209
+ });
210
+ }
211
+
212
+ const authFlowSource = getAuthFlowSource(provider);
213
+ if (
214
+ authFlowSource.includes("ctx.context") &&
215
+ (provider.context?.keys?.length ?? 0) === 0
216
+ ) {
217
+ diagnostics.push({
218
+ rule: "context-keys-required",
219
+ level: "warn",
220
+ field: "context.keys",
221
+ message: `${providerLabel} should declare context.keys when auth flow code accesses ctx.context.*.`,
222
+ });
223
+ }
224
+
225
+ return diagnostics;
226
+ }
227
+
228
+ function isSchema(value: unknown): value is SchemaLike {
229
+ return (
230
+ !!value &&
231
+ typeof value === "object" &&
232
+ "safeParse" in value &&
233
+ typeof value.safeParse === "function"
234
+ );
235
+ }
236
+
237
+ function getSchemaDef(schema: SchemaLike): Record<string, unknown> {
238
+ const def = schema.def ?? schema._def;
239
+ if (def && typeof def === "object") {
240
+ return def;
241
+ }
242
+ return {};
243
+ }
244
+
245
+ function isSchemaRecord(value: unknown): value is Record<string, SchemaLike> {
246
+ if (!value || typeof value !== "object") {
247
+ return false;
248
+ }
249
+ for (const entry of Object.values(value)) {
250
+ if (!isSchema(entry)) {
251
+ return false;
252
+ }
253
+ }
254
+ return true;
255
+ }
256
+
257
+ function getObjectShape(schema: SchemaLike): Record<string, SchemaLike> {
258
+ const rawShape =
259
+ typeof schema.shape === "function" ? schema.shape() : schema.shape;
260
+ if (isSchemaRecord(rawShape)) {
261
+ return rawShape;
262
+ }
263
+
264
+ const defShape = getSchemaDef(schema).shape;
265
+ if (typeof defShape === "function") {
266
+ const resolved = defShape();
267
+ if (isSchemaRecord(resolved)) {
268
+ return resolved;
269
+ }
270
+ return {};
271
+ }
272
+
273
+ if (isSchemaRecord(defShape)) {
274
+ return defShape;
275
+ }
276
+ return {};
277
+ }
278
+
279
+ function getChildSchemas(
280
+ schema: SchemaLike,
281
+ ): Array<{ key: string; schema: SchemaLike }> {
282
+ const seen = new Map<string, SchemaLike>();
283
+ const def = getSchemaDef(schema);
284
+
285
+ const add = (key: string, value: unknown) => {
286
+ if (!isSchema(value)) {
287
+ return;
288
+ }
289
+ seen.set(`${key}:${seen.size}`, value);
290
+ };
291
+
292
+ for (const [key, value] of Object.entries(getObjectShape(schema))) {
293
+ add(key, value);
294
+ }
295
+
296
+ add("element", schema.element);
297
+ add("innerType", schema.innerType);
298
+ add("unwrap", schema.unwrap?.());
299
+ add("sourceType", schema.sourceType?.());
300
+ add("in", schema.in);
301
+ add("out", schema.out);
302
+ add("left", schema.left);
303
+ add("right", schema.right);
304
+
305
+ if (Array.isArray(schema.items)) {
306
+ for (const [index, item] of schema.items.entries()) {
307
+ add(String(index), item);
308
+ }
309
+ }
310
+
311
+ if (Array.isArray(def.items)) {
312
+ for (const [index, item] of def.items.entries()) {
313
+ add(String(index), item);
314
+ }
315
+ }
316
+
317
+ const options = schema.options ?? def.options;
318
+ if (Array.isArray(options)) {
319
+ for (const [index, option] of options.entries()) {
320
+ add(String(index), option);
321
+ }
322
+ } else if (options instanceof Set) {
323
+ for (const [index, option] of Array.from(options).entries()) {
324
+ add(String(index), option);
325
+ }
326
+ } else if (options instanceof Map) {
327
+ for (const [key, option] of options.entries()) {
328
+ add(String(key), option);
329
+ }
330
+ }
331
+
332
+ for (const key of [
333
+ "schema",
334
+ "innerType",
335
+ "type",
336
+ "valueType",
337
+ "keyType",
338
+ "item",
339
+ "rest",
340
+ "catchall",
341
+ "option",
342
+ "pipe",
343
+ "payload",
344
+ "shape",
345
+ ]) {
346
+ const value = def[key];
347
+ if (Array.isArray(value)) {
348
+ for (const [index, item] of value.entries()) {
349
+ add(`${key}.${index}`, item);
350
+ }
351
+ } else {
352
+ add(key, value);
353
+ }
354
+ }
355
+
356
+ return Array.from(seen.entries()).map(([entryKey, child]) => ({
357
+ key: entryKey.split(":")[0] ?? entryKey,
358
+ schema: child,
359
+ }));
360
+ }
361
+
362
+ function collectMissingDescriptions(
363
+ schema: unknown,
364
+ basePath: string,
365
+ seen = new Set<SchemaLike>(),
366
+ ): string[] {
367
+ if (!isSchema(schema) || seen.has(schema)) {
368
+ return [];
369
+ }
370
+
371
+ seen.add(schema);
372
+ const missing: string[] = [];
373
+ const currentPath = basePath || "schema";
374
+
375
+ if (!schema.description) {
376
+ missing.push(currentPath);
377
+ }
378
+
379
+ for (const child of getChildSchemas(schema)) {
380
+ const isWrapperNode = [
381
+ "unwrap",
382
+ "innerType",
383
+ "sourceType",
384
+ "schema",
385
+ "type",
386
+ "option",
387
+ "pipe",
388
+ "payload",
389
+ "item",
390
+ "rest",
391
+ "catchall",
392
+ ].includes(child.key);
393
+ const childPath = isWrapperNode
394
+ ? currentPath
395
+ : currentPath === "schema"
396
+ ? child.key
397
+ : /^\d+$/.test(child.key)
398
+ ? `${currentPath}[${child.key}]`
399
+ : child.key.startsWith("element")
400
+ ? `${currentPath}[]`
401
+ : `${currentPath}.${child.key}`;
402
+ missing.push(...collectMissingDescriptions(child.schema, childPath, seen));
403
+ }
404
+
405
+ return missing;
406
+ }
407
+
408
+ function uniqueFields(fields: string[]): string[] {
409
+ return Array.from(new Set(fields));
410
+ }
411
+
412
+ function isComplexSchema(
413
+ schema: unknown,
414
+ seen = new Set<SchemaLike>(),
415
+ ): boolean {
416
+ if (!isSchema(schema) || seen.has(schema)) {
417
+ return false;
418
+ }
419
+
420
+ seen.add(schema);
421
+ const children = getChildSchemas(schema);
422
+ const hasNestedComposite = children.some(({ schema: child }) => {
423
+ const childChildren = getChildSchemas(child);
424
+ return childChildren.length > 0;
425
+ });
426
+
427
+ return (
428
+ hasNestedComposite ||
429
+ children.some(({ schema: child }) => isComplexSchema(child, seen))
430
+ );
431
+ }
432
+
433
+ function hasBidirectionalFixtures(fixtures: unknown): boolean {
434
+ if (!fixtures || typeof fixtures !== "object") {
435
+ return true;
436
+ }
437
+
438
+ return "request" in fixtures && "response" in fixtures;
439
+ }
440
+
441
+ export function lintOperation(op: {
442
+ description: string;
443
+ input: unknown;
444
+ output: unknown;
445
+ fixtures?: unknown;
446
+ inputExamples?: unknown[];
447
+ derivations?: Record<string, string>;
448
+ }): LintDiagnostic[] {
449
+ const diagnostics: LintDiagnostic[] = [];
450
+ const description = op.description ?? "";
451
+
452
+ if (description.length < 150) {
453
+ diagnostics.push({
454
+ rule: "description-min-length",
455
+ level: "error",
456
+ field: "description",
457
+ message: "Operation description must be at least 150 characters.",
458
+ });
459
+ }
460
+
461
+ const lowerDescription = description.toLowerCase();
462
+ if (
463
+ !(lowerDescription.includes("use") && lowerDescription.includes("when"))
464
+ ) {
465
+ diagnostics.push({
466
+ rule: "description-has-when-clause",
467
+ level: "warn",
468
+ field: "description",
469
+ message: 'Operation description should include both "use" and "when".',
470
+ });
471
+ }
472
+
473
+ for (const field of uniqueFields(
474
+ collectMissingDescriptions(op.input, "input"),
475
+ )) {
476
+ diagnostics.push({
477
+ rule: "all-fields-described",
478
+ level: "error",
479
+ field,
480
+ message: `Schema field "${field}" is missing a description.`,
481
+ });
482
+ }
483
+
484
+ for (const field of uniqueFields(
485
+ collectMissingDescriptions(op.output, "output"),
486
+ )) {
487
+ diagnostics.push({
488
+ rule: "all-fields-described",
489
+ level: "error",
490
+ field,
491
+ message: `Schema field "${field}" is missing a description.`,
492
+ });
493
+ }
494
+
495
+ if (!hasBidirectionalFixtures(op.fixtures)) {
496
+ diagnostics.push({
497
+ rule: "fixtures-both-directions",
498
+ level: "error",
499
+ field: "fixtures",
500
+ message: "Fixtures must include both request and response.",
501
+ });
502
+ }
503
+
504
+ if (isComplexSchema(op.input) && (op.inputExamples?.length ?? 0) < 2) {
505
+ diagnostics.push({
506
+ rule: "complex-input-has-examples",
507
+ level: "warn",
508
+ field: "inputExamples",
509
+ message:
510
+ "Complex input schemas should provide at least 2 input examples.",
511
+ });
512
+ }
513
+
514
+ return diagnostics;
515
+ }
516
+
517
+ export function lintProvider(provider: {
518
+ id?: string;
519
+ allowedHosts?: string[];
520
+ auth?: ProviderAuthLike;
521
+ credential?: {
522
+ keys?: string[];
523
+ storesReusableSecret?: boolean;
524
+ justification?: string;
525
+ };
526
+ context?: {
527
+ keys?: string[];
528
+ };
529
+ authFlowSource?: string;
530
+ operations?: Record<
531
+ string,
532
+ {
533
+ description?: string;
534
+ input: unknown;
535
+ output: unknown;
536
+ fixtures?: unknown;
537
+ inputExamples?: unknown[];
538
+ derivations?: Record<string, string>;
539
+ }
540
+ >;
541
+ reviewed?: string;
542
+ }): LintDiagnostic[] {
543
+ const diagnostics: LintDiagnostic[] = [
544
+ ...lintAllowedHosts(provider.id, provider.allowedHosts),
545
+ ...lintReviewed(provider.id, provider.reviewed),
546
+ ...lintAuthModel(provider),
547
+ ];
548
+
549
+ if (!provider.operations) {
550
+ return diagnostics;
551
+ }
552
+
553
+ diagnostics.push(
554
+ ...Object.entries(provider.operations).flatMap(
555
+ ([operationKey, operation]) =>
556
+ lintOperation({
557
+ description: operation.description ?? "",
558
+ input: operation.input,
559
+ output: operation.output,
560
+ fixtures: operation.fixtures,
561
+ inputExamples: operation.inputExamples,
562
+ derivations: operation.derivations,
563
+ }).map((diagnostic) => ({
564
+ ...diagnostic,
565
+ field: diagnostic.field
566
+ ? `operations.${operationKey}.${diagnostic.field}`
567
+ : `operations.${operationKey}`,
568
+ message: `[${operationKey}] ${diagnostic.message}`,
569
+ })),
570
+ ),
571
+ );
572
+
573
+ return diagnostics;
574
+ }
@@ -0,0 +1,14 @@
1
+ export { z } from "zod";
2
+
3
+ export { createFormCeremony } from "./ceremonies";
4
+ export { defineCompositeOperation } from "./composite";
5
+ export { defineOperation, defineProvider } from "./define";
6
+ export { AuthError, ProviderError, ValidationError } from "./errors";
7
+ export type {
8
+ FlowContext,
9
+ InferSchemaOutput,
10
+ OperationDefinition,
11
+ ProviderContext,
12
+ SchemaLike,
13
+ StandardSchemaV1,
14
+ } from "./types";
@@ -0,0 +1,67 @@
1
+ import { ContextAccessError } from "../errors";
2
+ import type {
3
+ ContextScratchpad,
4
+ EnvContext,
5
+ FlowContext,
6
+ HttpClient,
7
+ } from "../types";
8
+
9
+ function normalizeAllowedKeys(allowedKeys: string[]): Set<string> {
10
+ return new Set(allowedKeys.filter((key) => key.trim().length > 0));
11
+ }
12
+
13
+ function assertAllowedKey(allowedKeys: Set<string>, key: string): void {
14
+ if (!allowedKeys.has(key)) {
15
+ throw new ContextAccessError(
16
+ `Context key "${key}" is not declared in context.keys.`,
17
+ );
18
+ }
19
+ }
20
+
21
+ export function createScratchpad(
22
+ allowedKeys: string[],
23
+ initial: Record<string, unknown> = {},
24
+ ): ContextScratchpad {
25
+ const normalizedAllowedKeys = normalizeAllowedKeys(allowedKeys);
26
+ const values = new Map<string, unknown>();
27
+
28
+ for (const [key, value] of Object.entries(initial)) {
29
+ assertAllowedKey(normalizedAllowedKeys, key);
30
+ values.set(key, value);
31
+ }
32
+
33
+ return {
34
+ get(key: string): unknown {
35
+ assertAllowedKey(normalizedAllowedKeys, key);
36
+ return values.get(key);
37
+ },
38
+ set(key: string, value: unknown): void {
39
+ assertAllowedKey(normalizedAllowedKeys, key);
40
+ values.set(key, value);
41
+ },
42
+ toJSON(): Record<string, unknown> {
43
+ return Object.fromEntries(values.entries());
44
+ },
45
+ };
46
+ }
47
+
48
+ export function createFlowContext(options: {
49
+ http: HttpClient;
50
+ env: EnvContext;
51
+ tenantId: string;
52
+ providerId: string;
53
+ connectionId?: string;
54
+ externalRef?: string;
55
+ allowedKeys: string[];
56
+ initialContext?: Record<string, unknown>;
57
+ }): FlowContext {
58
+ return {
59
+ connectionId: options.connectionId,
60
+ externalRef: options.externalRef,
61
+ tenantId: options.tenantId,
62
+ providerId: options.providerId,
63
+ http: options.http,
64
+ env: options.env,
65
+ context: createScratchpad(options.allowedKeys, options.initialContext),
66
+ };
67
+ }