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

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 (212) hide show
  1. package/AUTHORING.md +208 -2
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +114 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +80 -0
  8. package/bin/apifuse-pack-smoke.ts +303 -2
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -30
  155. package/src/ceremonies/index.ts +8 -2
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +120 -1
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -48
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1224 -9
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1688 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -9
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -4
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +939 -195
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1157 -75
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1107 -59
  211. package/src/runtime/tls.ts +0 -434
  212. package/src/types/playwright-stealth.d.ts +0 -9
package/src/lint.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { ZodType } from "zod";
2
2
 
3
+ import { lintPublicSchemaFieldNames } from "./public-schema-field-lint";
4
+ import {
5
+ APIFUSE_DESCRIPTION_KEY_META_KEY,
6
+ APIFUSE_SENSITIVE_META_KEY,
7
+ } from "./schema";
8
+
3
9
  type AuthModeLike =
4
10
  | "none"
5
11
  | "platform-managed"
@@ -14,13 +20,19 @@ type ProviderAuthLike = {
14
20
  continue?: unknown;
15
21
  poll?: unknown;
16
22
  abort?: unknown;
23
+ refresh?: unknown;
17
24
  };
18
25
  };
19
26
 
27
+ type ProviderContractMetaLike = {
28
+ publicSchemaFieldNames?: "normalized";
29
+ };
30
+
20
31
  type SchemaLike = ZodType & {
21
32
  description?: string;
22
33
  def?: Record<string, unknown>;
23
34
  _def?: Record<string, unknown>;
35
+ meta?: () => Record<string, unknown> | undefined;
24
36
  shape?: Record<string, SchemaLike> | (() => Record<string, SchemaLike>);
25
37
  element?: SchemaLike;
26
38
  items?: SchemaLike[];
@@ -41,9 +53,21 @@ export interface LintDiagnostic {
41
53
  field?: string;
42
54
  }
43
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
+
44
68
  function lintAllowedHosts(
45
69
  providerId: string | undefined,
46
- allowedHosts: string[] | undefined,
70
+ allowedHosts: readonly string[] | undefined,
47
71
  ): LintDiagnostic[] {
48
72
  const prefix = providerId ? `Provider "${providerId}"` : "Provider";
49
73
 
@@ -103,7 +127,7 @@ function lintReviewed(
103
127
  ];
104
128
  }
105
129
 
106
- function hasReusableSecretKeys(keys: string[] | undefined): boolean {
130
+ function hasReusableSecretKeys(keys: readonly string[] | undefined): boolean {
107
131
  if (!keys) {
108
132
  return false;
109
133
  }
@@ -115,6 +139,18 @@ function hasReusableSecretKeys(keys: string[] | undefined): boolean {
115
139
  );
116
140
  }
117
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
+
118
154
  function getAuthFlowSource(provider: {
119
155
  auth?: ProviderAuthLike;
120
156
  authFlowSource?: string;
@@ -128,6 +164,7 @@ function getAuthFlowSource(provider: {
128
164
  provider.auth?.flow?.continue,
129
165
  provider.auth?.flow?.poll,
130
166
  provider.auth?.flow?.abort,
167
+ provider.auth?.flow?.refresh,
131
168
  ];
132
169
 
133
170
  return parts
@@ -143,12 +180,12 @@ function lintAuthModel(provider: {
143
180
  id?: string;
144
181
  auth?: ProviderAuthLike;
145
182
  credential?: {
146
- keys?: string[];
183
+ keys?: readonly string[];
147
184
  storesReusableSecret?: boolean;
148
185
  justification?: string;
149
186
  };
150
187
  context?: {
151
- keys?: string[];
188
+ keys?: readonly string[];
152
189
  };
153
190
  authFlowSource?: string;
154
191
  }): LintDiagnostic[] {
@@ -200,6 +237,20 @@ function lintAuthModel(provider: {
200
237
  });
201
238
  }
202
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
+
203
254
  if (authMode === "platform-managed" && credentialKeys.length > 0) {
204
255
  diagnostics.push({
205
256
  rule: "platform-managed-no-credential-keys",
@@ -359,7 +410,60 @@ function getChildSchemas(
359
410
  }));
360
411
  }
361
412
 
362
- function collectMissingDescriptions(
413
+ function uniqueFields(fields: string[]): string[] {
414
+ return Array.from(new Set(fields));
415
+ }
416
+
417
+ function isSensitiveSchema(schema: unknown): boolean {
418
+ if (!schema || typeof schema !== "object" || !("meta" in schema)) {
419
+ return false;
420
+ }
421
+ const meta = schema.meta;
422
+ if (typeof meta !== "function") return false;
423
+ const metadata = meta.call(schema);
424
+ return (
425
+ !!metadata &&
426
+ typeof metadata === "object" &&
427
+ Reflect.get(metadata, APIFUSE_SENSITIVE_META_KEY) === true
428
+ );
429
+ }
430
+
431
+ function getSchemaMetadata(schema: SchemaLike): Record<string, unknown> {
432
+ return schema.meta?.() ?? {};
433
+ }
434
+
435
+ function getSchemaDescriptionKey(schema: SchemaLike): string | undefined {
436
+ const value = Reflect.get(
437
+ getSchemaMetadata(schema),
438
+ APIFUSE_DESCRIPTION_KEY_META_KEY,
439
+ );
440
+ return typeof value === "string" && value.length > 0 ? value : undefined;
441
+ }
442
+
443
+ const SENSITIVE_FIELD_NAMES = new Set([
444
+ "apikey",
445
+ "authorization",
446
+ "cookie",
447
+ "secret",
448
+ "secrets",
449
+ "token",
450
+ "accesstoken",
451
+ "refreshtoken",
452
+ "password",
453
+ "passwd",
454
+ "otp",
455
+ "otpcode",
456
+ "phone",
457
+ "phonenumber",
458
+ "paymenturl",
459
+ ]);
460
+
461
+ function isSensitiveFieldName(name: string): boolean {
462
+ const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
463
+ return SENSITIVE_FIELD_NAMES.has(normalized);
464
+ }
465
+
466
+ function collectUnmarkedSensitiveFields(
363
467
  schema: unknown,
364
468
  basePath: string,
365
469
  seen = new Set<SchemaLike>(),
@@ -367,13 +471,80 @@ function collectMissingDescriptions(
367
471
  if (!isSchema(schema) || seen.has(schema)) {
368
472
  return [];
369
473
  }
474
+ seen.add(schema);
475
+ const out: string[] = [];
476
+ for (const [key, child] of Object.entries(getObjectShape(schema))) {
477
+ const childPath = basePath ? `${basePath}.${key}` : key;
478
+ if (isSensitiveFieldName(key) && !isSensitiveSchema(child)) {
479
+ out.push(childPath);
480
+ }
481
+ out.push(...collectUnmarkedSensitiveFields(child, childPath, seen));
482
+ }
483
+ for (const child of getChildSchemas(schema)) {
484
+ if (Object.hasOwn(getObjectShape(schema), child.key)) continue;
485
+ const isWrapperNode = [
486
+ "unwrap",
487
+ "innerType",
488
+ "sourceType",
489
+ "schema",
490
+ "type",
491
+ "in",
492
+ "out",
493
+ "option",
494
+ "pipe",
495
+ "payload",
496
+ "item",
497
+ "rest",
498
+ "catchall",
499
+ "keyType",
500
+ "valueType",
501
+ ].includes(child.key);
502
+ const childPath =
503
+ child.key === "element" || child.key.startsWith("element.")
504
+ ? `${basePath}[]`
505
+ : isWrapperNode || child.key.startsWith("pipe.")
506
+ ? basePath
507
+ : basePath
508
+ ? `${basePath}.${child.key}`
509
+ : child.key;
510
+ out.push(...collectUnmarkedSensitiveFields(child.schema, childPath, seen));
511
+ }
512
+ return out;
513
+ }
514
+
515
+ function collectSchemaDescriptionKeyDiagnostics(
516
+ schema: unknown,
517
+ basePath: string,
518
+ seen = new Set<SchemaLike>(),
519
+ requireCurrentDescription = true,
520
+ ): LintDiagnostic[] {
521
+ if (!isSchema(schema) || seen.has(schema)) {
522
+ return [];
523
+ }
370
524
 
371
525
  seen.add(schema);
372
- const missing: string[] = [];
526
+ const diagnostics: LintDiagnostic[] = [];
373
527
  const currentPath = basePath || "schema";
528
+ const hasDescriptionKey = getSchemaDescriptionKey(schema) !== undefined;
374
529
 
375
- if (!schema.description) {
376
- missing.push(currentPath);
530
+ if (schema.description && !hasDescriptionKey) {
531
+ diagnostics.push({
532
+ rule: "schema-description-raw-prose",
533
+ level: "error",
534
+ field: currentPath,
535
+ message: `Schema field "${currentPath}" must use .describeKey() or describeKey() instead of raw static prose.`,
536
+ });
537
+ }
538
+
539
+ if (requireCurrentDescription && !hasDescriptionKey) {
540
+ diagnostics.push({
541
+ rule: "schema-description-key-required",
542
+ level: "error",
543
+ field: currentPath,
544
+ message: schema.description
545
+ ? `Schema field "${currentPath}" has a raw description but is missing .describeKey() or describeKey() metadata.`
546
+ : `Schema field "${currentPath}" is missing .describeKey() or describeKey() metadata.`,
547
+ });
377
548
  }
378
549
 
379
550
  for (const child of getChildSchemas(schema)) {
@@ -383,30 +554,42 @@ function collectMissingDescriptions(
383
554
  "sourceType",
384
555
  "schema",
385
556
  "type",
557
+ "in",
558
+ "out",
386
559
  "option",
387
560
  "pipe",
388
561
  "payload",
389
562
  "item",
390
563
  "rest",
391
564
  "catchall",
565
+ "keyType",
566
+ "valueType",
392
567
  ].includes(child.key);
568
+ const isStructuralNode =
569
+ isWrapperNode ||
570
+ child.key.startsWith("pipe.") ||
571
+ child.key === "element" ||
572
+ child.key.startsWith("element.");
393
573
  const childPath = isWrapperNode
394
574
  ? currentPath
395
575
  : currentPath === "schema"
396
576
  ? child.key
397
577
  : /^\d+$/.test(child.key)
398
578
  ? `${currentPath}[${child.key}]`
399
- : child.key.startsWith("element")
579
+ : child.key === "element" || child.key.startsWith("element.")
400
580
  ? `${currentPath}[]`
401
581
  : `${currentPath}.${child.key}`;
402
- missing.push(...collectMissingDescriptions(child.schema, childPath, seen));
582
+ diagnostics.push(
583
+ ...collectSchemaDescriptionKeyDiagnostics(
584
+ child.schema,
585
+ childPath,
586
+ seen,
587
+ !isStructuralNode,
588
+ ),
589
+ );
403
590
  }
404
591
 
405
- return missing;
406
- }
407
-
408
- function uniqueFields(fields: string[]): string[] {
409
- return Array.from(new Set(fields));
592
+ return diagnostics;
410
593
  }
411
594
 
412
595
  function isComplexSchema(
@@ -438,60 +621,296 @@ function hasBidirectionalFixtures(fixtures: unknown): boolean {
438
621
  return "request" in fixtures && "response" in fixtures;
439
622
  }
440
623
 
624
+ function getOperationSource(operation: {
625
+ handler?: unknown;
626
+ source?: string;
627
+ }): string {
628
+ if (operation.source) {
629
+ return operation.source;
630
+ }
631
+ return typeof operation.handler === "function"
632
+ ? operation.handler.toString()
633
+ : "";
634
+ }
635
+
636
+ function lintStealthTransportUsage(provider: {
637
+ id?: string;
638
+ stealth?: unknown;
639
+ operations?: Record<string, { handler?: unknown; source?: string }>;
640
+ }): LintDiagnostic[] {
641
+ if (provider.stealth || !provider.operations) {
642
+ return [];
643
+ }
644
+
645
+ const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
646
+ return Object.entries(provider.operations).flatMap(
647
+ ([operationKey, operation]) => {
648
+ const source = getOperationSource(operation);
649
+ if (!/\bctx\.stealth\b/.test(source)) {
650
+ return [];
651
+ }
652
+ return [
653
+ {
654
+ rule: "stealth-config-required",
655
+ level: "error" as const,
656
+ field: `operations.${operationKey}`,
657
+ message: `${providerLabel} operation "${operationKey}" uses ctx.stealth but provider.stealth is not declared.`,
658
+ },
659
+ ];
660
+ },
661
+ );
662
+ }
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
+
441
836
  export function lintOperation(op: {
442
- description: string;
837
+ description?: string;
838
+ descriptionKey?: string;
839
+ whenToUse?: readonly string[];
840
+ whenToUseKeys?: readonly string[];
841
+ whenNotToUse?: readonly string[];
842
+ whenNotToUseKeys?: readonly string[];
443
843
  input: unknown;
444
844
  output: unknown;
445
845
  fixtures?: unknown;
446
- inputExamples?: unknown[];
846
+ inputExamples?: readonly unknown[];
447
847
  derivations?: Record<string, string>;
448
848
  }): LintDiagnostic[] {
449
849
  const diagnostics: LintDiagnostic[] = [];
450
850
  const description = op.description ?? "";
851
+ const hasDescriptionKey =
852
+ typeof op.descriptionKey === "string" && op.descriptionKey.length > 0;
451
853
 
452
- if (description.length < 150) {
854
+ if (description.trim().length > 0 && !hasDescriptionKey) {
453
855
  diagnostics.push({
454
- rule: "description-min-length",
856
+ rule: "operation-description-raw-prose",
455
857
  level: "error",
456
858
  field: "description",
457
- message: "Operation description must be at least 150 characters.",
859
+ message:
860
+ "Operation description must use descriptionKey instead of raw static prose.",
458
861
  });
459
862
  }
460
863
 
461
- const lowerDescription = description.toLowerCase();
462
- if (
463
- !(lowerDescription.includes("use") && lowerDescription.includes("when"))
464
- ) {
864
+ if (!hasDescriptionKey && description.length < 150) {
465
865
  diagnostics.push({
466
- rule: "description-has-when-clause",
467
- level: "warn",
866
+ rule: "description-min-length",
867
+ level: "error",
468
868
  field: "description",
469
- message: 'Operation description should include both "use" and "when".',
869
+ message: "Operation description must be at least 150 characters.",
470
870
  });
471
871
  }
472
872
 
473
- for (const field of uniqueFields(
474
- collectMissingDescriptions(op.input, "input"),
475
- )) {
873
+ if ((op.whenToUse?.length ?? 0) > 0 && !(op.whenToUseKeys?.length ?? 0)) {
476
874
  diagnostics.push({
477
- rule: "all-fields-described",
875
+ rule: "operation-when-to-use-raw-prose",
478
876
  level: "error",
479
- field,
480
- message: `Schema field "${field}" is missing a description.`,
877
+ field: "whenToUse",
878
+ message:
879
+ "Operation whenToUse must use whenToUseKeys instead of raw static prose.",
481
880
  });
482
881
  }
483
882
 
484
- for (const field of uniqueFields(
485
- collectMissingDescriptions(op.output, "output"),
486
- )) {
883
+ if (
884
+ (op.whenNotToUse?.length ?? 0) > 0 &&
885
+ !(op.whenNotToUseKeys?.length ?? 0)
886
+ ) {
487
887
  diagnostics.push({
488
- rule: "all-fields-described",
888
+ rule: "operation-when-not-to-use-raw-prose",
489
889
  level: "error",
490
- field,
491
- message: `Schema field "${field}" is missing a description.`,
890
+ field: "whenNotToUse",
891
+ message:
892
+ "Operation whenNotToUse must use whenNotToUseKeys instead of raw static prose.",
893
+ });
894
+ }
895
+
896
+ const lowerDescription = description.toLowerCase();
897
+ if (
898
+ !hasDescriptionKey &&
899
+ !(lowerDescription.includes("use") && lowerDescription.includes("when"))
900
+ ) {
901
+ diagnostics.push({
902
+ rule: "description-has-when-clause",
903
+ level: "warn",
904
+ field: "description",
905
+ message: 'Operation description should include both "use" and "when".',
492
906
  });
493
907
  }
494
908
 
909
+ diagnostics.push(
910
+ ...collectSchemaDescriptionKeyDiagnostics(op.input, "input"),
911
+ ...collectSchemaDescriptionKeyDiagnostics(op.output, "output"),
912
+ );
913
+
495
914
  if (!hasBidirectionalFixtures(op.fixtures)) {
496
915
  diagnostics.push({
497
916
  rule: "fixtures-both-directions",
@@ -511,39 +930,80 @@ export function lintOperation(op: {
511
930
  });
512
931
  }
513
932
 
933
+ for (const field of uniqueFields(
934
+ collectUnmarkedSensitiveFields(op.input, "input"),
935
+ )) {
936
+ diagnostics.push({
937
+ rule: "sensitive-field-unmarked",
938
+ level: "warn",
939
+ field,
940
+ message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
941
+ });
942
+ }
943
+
944
+ for (const field of uniqueFields(
945
+ collectUnmarkedSensitiveFields(op.output, "output"),
946
+ )) {
947
+ diagnostics.push({
948
+ rule: "sensitive-field-unmarked",
949
+ level: "warn",
950
+ field,
951
+ message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
952
+ });
953
+ }
954
+
514
955
  return diagnostics;
515
956
  }
516
957
 
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[] {
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[] {
543
999
  const diagnostics: LintDiagnostic[] = [
544
1000
  ...lintAllowedHosts(provider.id, provider.allowedHosts),
545
1001
  ...lintReviewed(provider.id, provider.reviewed),
546
1002
  ...lintAuthModel(provider),
1003
+ ...lintStealthTransportUsage(provider),
1004
+ ...lintCredentialWriteUsage(provider),
1005
+ ...lintPlaywrightDirectImports(provider),
1006
+ ...lintSelfHostedBrowserPatterns(provider, options),
547
1007
  ];
548
1008
 
549
1009
  if (!provider.operations) {
@@ -553,14 +1013,28 @@ export function lintProvider(provider: {
553
1013
  diagnostics.push(
554
1014
  ...Object.entries(provider.operations).flatMap(
555
1015
  ([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) => ({
1016
+ [
1017
+ ...lintOperation({
1018
+ description: operation.description ?? "",
1019
+ descriptionKey: operation.descriptionKey,
1020
+ whenToUse: operation.whenToUse,
1021
+ whenToUseKeys: operation.whenToUseKeys,
1022
+ whenNotToUse: operation.whenNotToUse,
1023
+ whenNotToUseKeys: operation.whenNotToUseKeys,
1024
+ input: operation.input,
1025
+ output: operation.output,
1026
+ fixtures: operation.fixtures,
1027
+ inputExamples: operation.inputExamples,
1028
+ derivations: operation.derivations,
1029
+ }),
1030
+ ...lintPublicSchemaFieldNames(
1031
+ provider.id,
1032
+ operationKey,
1033
+ operation.input,
1034
+ operation.output,
1035
+ provider.meta?.contract?.publicSchemaFieldNames === "normalized",
1036
+ ),
1037
+ ].map((diagnostic) => ({
564
1038
  ...diagnostic,
565
1039
  field: diagnostic.field
566
1040
  ? `operations.${operationKey}.${diagnostic.field}`