@apifuse/provider-sdk 2.1.0-beta.4 → 2.1.0-beta.6

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 (42) hide show
  1. package/AUTHORING.md +24 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +23 -2
  4. package/SUBMISSION.md +2 -1
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +48 -5
  7. package/bin/apifuse-perf.ts +106 -26
  8. package/bin/apifuse-record.ts +142 -52
  9. package/bin/apifuse-submit-check.ts +1489 -3
  10. package/package.json +107 -92
  11. package/src/ceremonies/index.ts +8 -2
  12. package/src/choice-token.ts +1 -0
  13. package/src/cli/commands.ts +10 -8
  14. package/src/cli/create.ts +49 -1
  15. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  16. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  17. package/src/cli/templates/provider/README.md.tpl +18 -0
  18. package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
  19. package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
  20. package/src/config/loader.ts +19 -1
  21. package/src/contract-json.ts +75 -0
  22. package/src/contract-serialization.ts +89 -0
  23. package/src/contract-types.ts +52 -0
  24. package/src/contract.ts +215 -0
  25. package/src/define.ts +40 -5
  26. package/src/errors.ts +15 -0
  27. package/src/i18n/catalog.ts +156 -0
  28. package/src/index.ts +22 -1
  29. package/src/lint.ts +265 -46
  30. package/src/provider.ts +45 -2
  31. package/src/runtime/browser.ts +685 -30
  32. package/src/runtime/cache.ts +35 -89
  33. package/src/runtime/choice.ts +760 -0
  34. package/src/runtime/executor.ts +19 -2
  35. package/src/runtime/redis.ts +116 -0
  36. package/src/runtime/state.ts +487 -0
  37. package/src/runtime/stealth.ts +8 -1
  38. package/src/runtime/trace.ts +1 -1
  39. package/src/server/serve.ts +361 -46
  40. package/src/server/types.ts +2 -0
  41. package/src/testing/run.ts +16 -3
  42. package/src/types.ts +225 -18
package/src/lint.ts CHANGED
@@ -20,6 +20,7 @@ type ProviderAuthLike = {
20
20
  continue?: unknown;
21
21
  poll?: unknown;
22
22
  abort?: unknown;
23
+ refresh?: unknown;
23
24
  };
24
25
  };
25
26
 
@@ -52,9 +53,21 @@ export interface LintDiagnostic {
52
53
  field?: string;
53
54
  }
54
55
 
56
+ export type ProviderLintMode = "official" | "standalone";
57
+
58
+ type ProviderLintOptions = {
59
+ mode?: ProviderLintMode;
60
+ };
61
+
62
+ type ProviderSourceLike = {
63
+ authFlowSource?: string;
64
+ providerSourceFiles?: Record<string, string>;
65
+ operations?: Record<string, { handler?: unknown; source?: string }>;
66
+ };
67
+
55
68
  function lintAllowedHosts(
56
69
  providerId: string | undefined,
57
- allowedHosts: string[] | undefined,
70
+ allowedHosts: readonly string[] | undefined,
58
71
  ): LintDiagnostic[] {
59
72
  const prefix = providerId ? `Provider "${providerId}"` : "Provider";
60
73
 
@@ -114,7 +127,7 @@ function lintReviewed(
114
127
  ];
115
128
  }
116
129
 
117
- function hasReusableSecretKeys(keys: string[] | undefined): boolean {
130
+ function hasReusableSecretKeys(keys: readonly string[] | undefined): boolean {
118
131
  if (!keys) {
119
132
  return false;
120
133
  }
@@ -126,6 +139,18 @@ function hasReusableSecretKeys(keys: string[] | undefined): boolean {
126
139
  );
127
140
  }
128
141
 
142
+ function hasReusableReloginSecretKeys(
143
+ keys: readonly string[] | undefined,
144
+ ): boolean {
145
+ if (!keys) {
146
+ return false;
147
+ }
148
+
149
+ return keys.some((key) =>
150
+ /(password|passcode|secret|cookie|session)/i.test(key),
151
+ );
152
+ }
153
+
129
154
  function getAuthFlowSource(provider: {
130
155
  auth?: ProviderAuthLike;
131
156
  authFlowSource?: string;
@@ -139,6 +164,7 @@ function getAuthFlowSource(provider: {
139
164
  provider.auth?.flow?.continue,
140
165
  provider.auth?.flow?.poll,
141
166
  provider.auth?.flow?.abort,
167
+ provider.auth?.flow?.refresh,
142
168
  ];
143
169
 
144
170
  return parts
@@ -154,12 +180,12 @@ function lintAuthModel(provider: {
154
180
  id?: string;
155
181
  auth?: ProviderAuthLike;
156
182
  credential?: {
157
- keys?: string[];
183
+ keys?: readonly string[];
158
184
  storesReusableSecret?: boolean;
159
185
  justification?: string;
160
186
  };
161
187
  context?: {
162
- keys?: string[];
188
+ keys?: readonly string[];
163
189
  };
164
190
  authFlowSource?: string;
165
191
  }): LintDiagnostic[] {
@@ -211,6 +237,20 @@ function lintAuthModel(provider: {
211
237
  });
212
238
  }
213
239
 
240
+ if (
241
+ typeof provider.auth?.flow?.refresh === "function" &&
242
+ hasReusableReloginSecretKeys(credentialKeys) &&
243
+ (!provider.credential?.storesReusableSecret ||
244
+ !provider.credential.justification)
245
+ ) {
246
+ diagnostics.push({
247
+ rule: "auth-refresh-reusable-secret",
248
+ level: "error",
249
+ field: "credential",
250
+ message: `${providerLabel} must set storesReusableSecret and justification when auth.flow.refresh may silently re-login with reusable credential secrets.`,
251
+ });
252
+ }
253
+
214
254
  if (authMode === "platform-managed" && credentialKeys.length > 0) {
215
255
  diagnostics.push({
216
256
  rule: "platform-managed-no-credential-keys",
@@ -621,17 +661,189 @@ function lintStealthTransportUsage(provider: {
621
661
  );
622
662
  }
623
663
 
664
+ function lintCredentialWriteUsage(provider: {
665
+ operations?: Record<string, { handler?: unknown; source?: string }>;
666
+ }): LintDiagnostic[] {
667
+ if (!provider.operations) {
668
+ return [];
669
+ }
670
+
671
+ return Object.entries(provider.operations).flatMap(
672
+ ([operationKey, operation]) => {
673
+ const source = getOperationSource(operation);
674
+ if (!/\bctx\.credential\.(?:set|setMany)\s*\(/.test(source)) {
675
+ return [];
676
+ }
677
+
678
+ return [
679
+ {
680
+ rule: "ctx-credential-write-forbidden-in-handler",
681
+ level: "error" as const,
682
+ field: `operations.${operationKey}.handler`,
683
+ message:
684
+ "Operation handlers must not mutate credentials; return refreshed credentials from auth.flow.refresh instead.",
685
+ },
686
+ ];
687
+ },
688
+ );
689
+ }
690
+
691
+ function lintPlaywrightDirectImports(provider: {
692
+ authFlowSource?: string;
693
+ providerSourceFiles?: Record<string, string>;
694
+ operations?: Record<string, { handler?: unknown; source?: string }>;
695
+ }): LintDiagnostic[] {
696
+ const diagnostics: LintDiagnostic[] = [];
697
+ const importPattern =
698
+ /(?:import\s+(?:type\s+)?[\s\S]*?\s+from\s+["'](?:playwright|playwright-core)["']|require\(\s*["'](?:playwright|playwright-core)["']\s*\)|import\(\s*["'](?:playwright|playwright-core)["']\s*\))/;
699
+
700
+ if (provider.authFlowSource && importPattern.test(provider.authFlowSource)) {
701
+ diagnostics.push({
702
+ rule: "playwright-direct-import",
703
+ level: "warn",
704
+ field: "auth.flow",
705
+ message:
706
+ "Provider auth flow imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
707
+ });
708
+ }
709
+
710
+ for (const [filePath, source] of Object.entries(
711
+ provider.providerSourceFiles ?? {},
712
+ )) {
713
+ if (!importPattern.test(source)) {
714
+ continue;
715
+ }
716
+
717
+ diagnostics.push({
718
+ rule: "playwright-direct-import",
719
+ level: "warn",
720
+ field: `sourceFiles.${filePath}`,
721
+ message:
722
+ "Provider source imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
723
+ });
724
+ }
725
+
726
+ if (!provider.operations) {
727
+ return diagnostics;
728
+ }
729
+
730
+ for (const [operationKey, operation] of Object.entries(provider.operations)) {
731
+ const source = getOperationSource(operation);
732
+ if (!importPattern.test(source)) {
733
+ continue;
734
+ }
735
+
736
+ diagnostics.push({
737
+ rule: "playwright-direct-import",
738
+ level: "warn",
739
+ field: `operations.${operationKey}.handler`,
740
+ message:
741
+ "Operation source imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
742
+ });
743
+ }
744
+
745
+ return diagnostics;
746
+ }
747
+
748
+ type SelfHostedBrowserPattern = {
749
+ rule: string;
750
+ pattern: RegExp;
751
+ message: string;
752
+ };
753
+
754
+ const SELF_HOSTED_BROWSER_MESSAGE =
755
+ "Official browser providers must use ctx.browser backed by the managed CDP Pool; do not launch or connect to provider-local Chrome/CDP runtimes.";
756
+
757
+ const SELF_HOSTED_BROWSER_PATTERNS: readonly SelfHostedBrowserPattern[] = [
758
+ {
759
+ rule: "browser-self-hosted-launch",
760
+ pattern: /\b(?:playwright|chromium|firefox|webkit|puppeteer)\.launch\s*\(/,
761
+ message: `${SELF_HOSTED_BROWSER_MESSAGE} Replace direct Playwright/Puppeteer launch calls with ctx.browser.newPage() or ctx.browser.withIsolatedContext().`,
762
+ },
763
+ {
764
+ rule: "browser-self-hosted-child-process",
765
+ pattern:
766
+ /(?:\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync)\s*\([\s\S]{0,240}\b(?:google-chrome|chrome|chromium|chromium-browser)\b|\b(?:Bun\.)?spawn(?:Sync)?\s*\([\s\S]{0,240}\b(?:google-chrome|chrome|chromium|chromium-browser)\b|\$`[\s\S]{0,240}\b(?:google-chrome|chrome|chromium|chromium-browser)\b)/,
767
+ message: `${SELF_HOSTED_BROWSER_MESSAGE} Provider pods must not start Chrome with child_process, Bun.spawn, or shell commands.`,
768
+ },
769
+ {
770
+ rule: "browser-self-hosted-remote-debugging-port",
771
+ pattern:
772
+ /(?:\b(?:google-chrome|chrome|chromium|chromium-browser)\b[\s\S]{0,240}--remote-debugging-port\b|--remote-debugging-port(?:=|\s+))/,
773
+ message: `${SELF_HOSTED_BROWSER_MESSAGE} Provider entrypoints, Dockerfiles, and scripts must not start Chrome with a remote debugging port; use the managed CDP Pool instead.`,
774
+ },
775
+ {
776
+ rule: "browser-direct-cdp-version-poll",
777
+ pattern: /\/json\/version\b/,
778
+ message: `${SELF_HOSTED_BROWSER_MESSAGE} Do not poll /json/version from provider code; the SDK manages CDP leases through APIFUSE__CDP_POOL__URL.`,
779
+ },
780
+ {
781
+ rule: "browser-provider-local-cdp-env",
782
+ pattern:
783
+ /\b(?!APIFUSE__CDP_POOL__URL\b)[A-Z][A-Z0-9_]*_CDP_URL\b|process\.env(?:\.(?!APIFUSE__CDP_POOL__URL\b)[A-Z0-9_]*_CDP_URL\b|\[\s*["'`](?!APIFUSE__CDP_POOL__URL\b)[A-Z0-9_]*_CDP_URL["'`]\s*\])/,
784
+ message: `${SELF_HOSTED_BROWSER_MESSAGE} Do not read provider-local CDP endpoint env vars including AMAZON_CDP_URL or custom *_CDP_URL names; production uses APIFUSE__CDP_POOL__URL through ctx.browser.`,
785
+ },
786
+ ];
787
+
788
+ function lintSelfHostedBrowserPatterns(
789
+ provider: ProviderSourceLike,
790
+ options: ProviderLintOptions,
791
+ ): LintDiagnostic[] {
792
+ const diagnostics: LintDiagnostic[] = [];
793
+ const level = options.mode === "standalone" ? "warn" : "error";
794
+ const sources: Array<{ field: string; source: string }> = [];
795
+
796
+ if (provider.authFlowSource) {
797
+ sources.push({ field: "auth.flow", source: provider.authFlowSource });
798
+ }
799
+
800
+ for (const [filePath, source] of Object.entries(
801
+ provider.providerSourceFiles ?? {},
802
+ )) {
803
+ sources.push({ field: `sourceFiles.${filePath}`, source });
804
+ }
805
+
806
+ for (const [operationKey, operation] of Object.entries(
807
+ provider.operations ?? {},
808
+ )) {
809
+ const source = getOperationSource(operation);
810
+ if (source) {
811
+ sources.push({
812
+ field: `operations.${operationKey}.handler`,
813
+ source,
814
+ });
815
+ }
816
+ }
817
+
818
+ for (const { field, source } of sources) {
819
+ for (const item of SELF_HOSTED_BROWSER_PATTERNS) {
820
+ item.pattern.lastIndex = 0;
821
+ if (!item.pattern.test(source)) {
822
+ continue;
823
+ }
824
+ diagnostics.push({
825
+ rule: item.rule,
826
+ level,
827
+ field,
828
+ message: item.message,
829
+ });
830
+ }
831
+ }
832
+
833
+ return diagnostics;
834
+ }
835
+
624
836
  export function lintOperation(op: {
625
837
  description?: string;
626
838
  descriptionKey?: string;
627
- whenToUse?: string[];
628
- whenToUseKeys?: string[];
629
- whenNotToUse?: string[];
630
- whenNotToUseKeys?: string[];
839
+ whenToUse?: readonly string[];
840
+ whenToUseKeys?: readonly string[];
841
+ whenNotToUse?: readonly string[];
842
+ whenNotToUseKeys?: readonly string[];
631
843
  input: unknown;
632
844
  output: unknown;
633
845
  fixtures?: unknown;
634
- inputExamples?: unknown[];
846
+ inputExamples?: readonly unknown[];
635
847
  derivations?: Record<string, string>;
636
848
  }): LintDiagnostic[] {
637
849
  const diagnostics: LintDiagnostic[] = [];
@@ -743,48 +955,55 @@ export function lintOperation(op: {
743
955
  return diagnostics;
744
956
  }
745
957
 
746
- export function lintProvider(provider: {
747
- id?: string;
748
- allowedHosts?: string[];
749
- stealth?: unknown;
750
- auth?: ProviderAuthLike;
751
- credential?: {
752
- keys?: string[];
753
- storesReusableSecret?: boolean;
754
- justification?: string;
755
- };
756
- context?: {
757
- keys?: string[];
758
- };
759
- authFlowSource?: string;
760
- operations?: Record<
761
- string,
762
- {
763
- description?: string;
764
- descriptionKey?: string;
765
- whenToUse?: string[];
766
- whenToUseKeys?: string[];
767
- whenNotToUse?: string[];
768
- whenNotToUseKeys?: string[];
769
- input: unknown;
770
- output: unknown;
771
- fixtures?: unknown;
772
- inputExamples?: unknown[];
773
- derivations?: Record<string, string>;
774
- handler?: unknown;
775
- source?: string;
776
- }
777
- >;
778
- meta?: {
779
- contract?: ProviderContractMetaLike;
780
- };
781
- reviewed?: string;
782
- }): LintDiagnostic[] {
958
+ export function lintProvider(
959
+ provider: {
960
+ id?: string;
961
+ allowedHosts?: readonly string[];
962
+ stealth?: unknown;
963
+ auth?: ProviderAuthLike;
964
+ credential?: {
965
+ keys?: readonly string[];
966
+ storesReusableSecret?: boolean;
967
+ justification?: string;
968
+ };
969
+ context?: {
970
+ keys?: readonly string[];
971
+ };
972
+ authFlowSource?: string;
973
+ providerSourceFiles?: Record<string, string>;
974
+ operations?: Record<
975
+ string,
976
+ {
977
+ description?: string;
978
+ descriptionKey?: string;
979
+ whenToUse?: readonly string[];
980
+ whenToUseKeys?: readonly string[];
981
+ whenNotToUse?: readonly string[];
982
+ whenNotToUseKeys?: readonly string[];
983
+ input: unknown;
984
+ output: unknown;
985
+ fixtures?: unknown;
986
+ inputExamples?: readonly unknown[];
987
+ derivations?: Record<string, string>;
988
+ handler?: unknown;
989
+ source?: string;
990
+ }
991
+ >;
992
+ meta?: {
993
+ contract?: ProviderContractMetaLike;
994
+ };
995
+ reviewed?: string;
996
+ },
997
+ options: ProviderLintOptions = {},
998
+ ): LintDiagnostic[] {
783
999
  const diagnostics: LintDiagnostic[] = [
784
1000
  ...lintAllowedHosts(provider.id, provider.allowedHosts),
785
1001
  ...lintReviewed(provider.id, provider.reviewed),
786
1002
  ...lintAuthModel(provider),
787
1003
  ...lintStealthTransportUsage(provider),
1004
+ ...lintCredentialWriteUsage(provider),
1005
+ ...lintPlaywrightDirectImports(provider),
1006
+ ...lintSelfHostedBrowserPatterns(provider, options),
788
1007
  ];
789
1008
 
790
1009
  if (!provider.operations) {
package/src/provider.ts CHANGED
@@ -14,8 +14,24 @@ export {
14
14
  defineSmsOtpMatcher,
15
15
  every,
16
16
  } from "./define";
17
- export { AuthError, ProviderError, ValidationError } from "./errors";
18
- export { providerLocaleKey, qualifyProviderLocaleKey } from "./i18n";
17
+ export {
18
+ AuthError,
19
+ ProviderError,
20
+ SessionExpiredError,
21
+ TransportError,
22
+ ValidationError,
23
+ } from "./errors";
24
+ export {
25
+ getProviderLocalePath,
26
+ providerLocaleKey,
27
+ qualifyProviderLocaleKey,
28
+ } from "./i18n";
29
+ export {
30
+ type CreateProviderChoiceContextOptions,
31
+ createProviderChoiceContext,
32
+ createTestProviderChoiceContext,
33
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
34
+ } from "./runtime/choice";
19
35
  export {
20
36
  APIFUSE_DESCRIPTION_KEY_META_KEY,
21
37
  APIFUSE_REDACTION_MARKER,
@@ -34,7 +50,12 @@ export {
34
50
  z,
35
51
  } from "./schema";
36
52
  export type {
53
+ AuthMode,
37
54
  FlowContext,
55
+ HealthCheckAssertionContext,
56
+ HealthCheckCase,
57
+ HealthCheckSuite,
58
+ HealthCheckUnsupported,
38
59
  HealthJourneyDefinition,
39
60
  HealthJourneyEventContext,
40
61
  HealthJourneyManualTriggerPolicy,
@@ -43,16 +64,38 @@ export type {
43
64
  HttpRetryOptions,
44
65
  HttpRetrySummary,
45
66
  InferSchemaOutput,
67
+ OperationApprovalPolicy,
68
+ OperationContractMetadata,
46
69
  OperationDefinition,
70
+ OperationDocMeta,
71
+ OperationErrorCode,
72
+ OperationInputExample,
73
+ OperationLifecycle,
47
74
  OperationObservabilityConfig,
48
75
  OperationObservabilitySensitiveConfig,
76
+ OperationRelationships,
77
+ OperationRiskClass,
49
78
  OperationSensitivePath,
79
+ OperationToolRouterMetadata,
80
+ OperationTransport,
81
+ ProviderAccessVisibility,
82
+ ProviderChoiceBindingOptions,
83
+ ProviderChoiceContext,
84
+ ProviderChoiceIssueOptions,
85
+ ProviderChoiceParseOptions,
50
86
  ProviderContext,
87
+ ProviderDefinition,
88
+ ProviderLocale,
89
+ ProviderLocaleKey,
51
90
  ProviderLocaleKeyInput,
91
+ ProviderLogoProfile,
52
92
  ProviderProxyPolicy,
93
+ ProviderPublicConnectionMode,
94
+ ProviderPublicProfile,
53
95
  ProviderRuntimeState,
54
96
  ProviderStateDurationString,
55
97
  ProviderStateNamespace,
98
+ ProviderSupportLevel,
56
99
  SchemaLike,
57
100
  SmsOtpMatcherDefinition,
58
101
  StandardSchemaV1,