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

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 (79) hide show
  1. package/AUTHORING.md +93 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +133 -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 +87 -0
  8. package/bin/apifuse-pack-smoke.ts +122 -0
  9. package/bin/apifuse-perf.ts +33 -32
  10. package/bin/apifuse-record.ts +17 -7
  11. package/bin/apifuse-test.ts +6 -4
  12. package/bin/apifuse.ts +36 -35
  13. package/package.json +29 -9
  14. package/src/ceremonies/index.ts +768 -0
  15. package/src/cli/commands.ts +87 -0
  16. package/src/cli/create.ts +845 -0
  17. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  18. package/src/cli/templates/provider/README.md.tpl +41 -0
  19. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  20. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  21. package/src/cli/templates/provider/index.ts.tpl +58 -0
  22. package/src/cli/templates/provider/start.ts.tpl +5 -0
  23. package/src/config/loader.ts +61 -1
  24. package/src/define.ts +565 -41
  25. package/src/dev.ts +2 -6
  26. package/src/errors.ts +42 -0
  27. package/src/index.ts +44 -38
  28. package/src/lint.ts +574 -0
  29. package/src/provider.ts +13 -0
  30. package/src/runtime/auth-flow.ts +67 -0
  31. package/src/runtime/credential.ts +95 -0
  32. package/src/runtime/env.ts +13 -0
  33. package/src/runtime/executor.ts +13 -14
  34. package/src/runtime/http.ts +36 -12
  35. package/src/runtime/insights.ts +3 -3
  36. package/src/runtime/key-derivation.ts +122 -0
  37. package/src/runtime/keyring.ts +148 -0
  38. package/src/runtime/namespace.ts +33 -0
  39. package/src/runtime/prevalidate.ts +252 -0
  40. package/src/runtime/tls.ts +41 -17
  41. package/src/runtime/waterfall.ts +0 -1
  42. package/src/schema.ts +77 -0
  43. package/src/serve.ts +1 -664
  44. package/src/server/index.ts +22 -0
  45. package/src/server/serve.ts +624 -0
  46. package/src/server/types.ts +78 -0
  47. package/src/stealth/profiles.ts +10 -93
  48. package/src/testing/run.ts +391 -32
  49. package/src/types.ts +390 -41
  50. package/bin/apifuse-init.ts +0 -387
  51. package/src/__tests__/auth.test.ts +0 -396
  52. package/src/__tests__/browser-auth.test.ts +0 -180
  53. package/src/__tests__/browser.test.ts +0 -632
  54. package/src/__tests__/define.test.ts +0 -225
  55. package/src/__tests__/errors.test.ts +0 -69
  56. package/src/__tests__/executor.test.ts +0 -214
  57. package/src/__tests__/http.test.ts +0 -238
  58. package/src/__tests__/insights.test.ts +0 -210
  59. package/src/__tests__/instrumentation.test.ts +0 -290
  60. package/src/__tests__/otlp.test.ts +0 -141
  61. package/src/__tests__/perf.test.ts +0 -60
  62. package/src/__tests__/providers-yaml.test.ts +0 -135
  63. package/src/__tests__/proxy.test.ts +0 -359
  64. package/src/__tests__/recipes.test.ts +0 -36
  65. package/src/__tests__/serve.test.ts +0 -233
  66. package/src/__tests__/session.test.ts +0 -231
  67. package/src/__tests__/state.test.ts +0 -100
  68. package/src/__tests__/stealth.test.ts +0 -57
  69. package/src/__tests__/testing.test.ts +0 -97
  70. package/src/__tests__/tls.test.ts +0 -345
  71. package/src/__tests__/types.test.ts +0 -142
  72. package/src/__tests__/utils.test.ts +0 -62
  73. package/src/__tests__/waterfall.test.ts +0 -270
  74. package/src/config/providers-yaml.ts +0 -370
  75. package/src/index.test.ts +0 -1
  76. package/src/protocol.ts +0 -183
  77. package/src/runtime/auth.ts +0 -245
  78. package/src/runtime/session.ts +0 -573
  79. 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,13 @@
1
+ export { z } from "zod";
2
+
3
+ export { createFormCeremony } from "./ceremonies";
4
+ export { defineOperation, defineProvider } from "./define";
5
+ export { AuthError, ProviderError, ValidationError } from "./errors";
6
+ export type {
7
+ FlowContext,
8
+ InferSchemaOutput,
9
+ OperationDefinition,
10
+ ProviderContext,
11
+ SchemaLike,
12
+ StandardSchemaV1,
13
+ } 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
+ }