@apifuse/provider-sdk 2.2.0-beta.62 → 2.2.0-beta.64

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 (261) hide show
  1. package/AUTHORING.md +367 -9
  2. package/CHANGELOG.md +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +1 -0
  5. package/bin/apifuse-check.ts +84 -2
  6. package/bin/apifuse-dev.ts +11 -31
  7. package/bin/apifuse-migrate-deployment.ts +200 -0
  8. package/bin/apifuse-pack-check.ts +11 -4
  9. package/bin/apifuse-pack-types.ts +187 -4
  10. package/bin/apifuse-record.ts +44 -32
  11. package/bin/apifuse-submit-check.ts +9 -5
  12. package/bin/apifuse-test.ts +3 -1
  13. package/dist/cli/commands.d.ts +1 -1
  14. package/dist/cli/commands.js +11 -0
  15. package/dist/cli/deployment-intent.d.ts +178 -0
  16. package/dist/cli/deployment-intent.js +277 -0
  17. package/dist/cli/migrate-deployment-intent.d.ts +132 -0
  18. package/dist/cli/migrate-deployment-intent.js +1552 -0
  19. package/dist/cli/migrate-provider-shape.d.ts +5 -1
  20. package/dist/cli/migrate-provider-shape.js +4 -2
  21. package/dist/cli/resolver-runtime.d.ts +21 -0
  22. package/dist/cli/resolver-runtime.js +31 -0
  23. package/dist/define.d.ts +3 -0
  24. package/dist/define.js +5 -0
  25. package/dist/dev.d.ts +3 -1
  26. package/dist/dev.js +6 -1
  27. package/dist/engine.d.ts +35 -1
  28. package/dist/engine.js +21 -2
  29. package/dist/error-resolution.d.ts +1 -0
  30. package/dist/error-resolution.js +54 -0
  31. package/dist/errors.d.ts +39 -1
  32. package/dist/errors.js +50 -0
  33. package/dist/handle-meta.d.ts +36 -1
  34. package/dist/handle-meta.js +16 -0
  35. package/dist/handle.d.ts +23 -5
  36. package/dist/handle.js +54 -8
  37. package/dist/health-scenario.d.ts +38 -0
  38. package/dist/i18n/error-messages.d.ts +86 -0
  39. package/dist/i18n/error-messages.js +192 -0
  40. package/dist/i18n/index.d.ts +1 -0
  41. package/dist/i18n/index.js +1 -0
  42. package/dist/i18n/keys.d.ts +12 -0
  43. package/dist/i18n/keys.js +76 -5
  44. package/dist/index.d.ts +11 -3
  45. package/dist/index.js +9 -1
  46. package/dist/lint.d.ts +15 -0
  47. package/dist/lint.js +469 -82
  48. package/dist/provider.d.ts +6 -6
  49. package/dist/provider.js +3 -3
  50. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  51. package/dist/runtime/browser-telemetry-binding.js +83 -0
  52. package/dist/runtime/browser-telemetry.d.ts +76 -0
  53. package/dist/runtime/browser-telemetry.js +165 -0
  54. package/dist/runtime/browser.d.ts +3 -0
  55. package/dist/runtime/browser.js +208 -47
  56. package/dist/runtime/cache-telemetry.d.ts +71 -0
  57. package/dist/runtime/cache-telemetry.js +177 -0
  58. package/dist/runtime/cache.js +67 -7
  59. package/dist/runtime/capability-response.d.ts +3 -0
  60. package/dist/runtime/capability-response.js +56 -0
  61. package/dist/runtime/capability-telemetry.d.ts +46 -0
  62. package/dist/runtime/capability-telemetry.js +68 -0
  63. package/dist/runtime/diagnostic-env.d.ts +3 -0
  64. package/dist/runtime/diagnostic-env.js +10 -1
  65. package/dist/runtime/engine-mode.d.ts +74 -0
  66. package/dist/runtime/engine-mode.js +152 -0
  67. package/dist/runtime/handle.js +9 -1
  68. package/dist/runtime/http-telemetry-guard.d.ts +10 -0
  69. package/dist/runtime/http-telemetry-guard.js +62 -10
  70. package/dist/runtime/http.js +141 -35
  71. package/dist/runtime/instrumentation.js +92 -13
  72. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  73. package/dist/runtime/ocr-telemetry.js +192 -0
  74. package/dist/runtime/ocr.js +35 -8
  75. package/dist/runtime/request-telemetry.d.ts +2 -0
  76. package/dist/runtime/request-telemetry.js +2 -1
  77. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  78. package/dist/runtime/resolver-runtime-options.js +33 -0
  79. package/dist/runtime/resolver-shared.d.ts +3 -2
  80. package/dist/runtime/resolver-shared.js +4 -4
  81. package/dist/runtime/resolver-telemetry.d.ts +1 -0
  82. package/dist/runtime/resolver-telemetry.js +1 -1
  83. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  84. package/dist/runtime/resolver-vendors/browser.js +46 -0
  85. package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
  86. package/dist/runtime/resolver-vendors/hosts.js +35 -0
  87. package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
  88. package/dist/runtime/resolver-vendors/types.d.ts +5 -4
  89. package/dist/runtime/resolver.d.ts +4 -1
  90. package/dist/runtime/resolver.js +46 -13
  91. package/dist/runtime/state-telemetry.d.ts +79 -0
  92. package/dist/runtime/state-telemetry.js +194 -0
  93. package/dist/runtime/state.d.ts +3 -0
  94. package/dist/runtime/state.js +26 -7
  95. package/dist/runtime/stealth-cookies.d.ts +5 -1
  96. package/dist/runtime/stealth-cookies.js +21 -1
  97. package/dist/runtime/stealth-owned-headers.d.ts +26 -0
  98. package/dist/runtime/stealth-owned-headers.js +37 -0
  99. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  100. package/dist/runtime/stealth-telemetry.js +179 -0
  101. package/dist/runtime/stealth.d.ts +4 -1
  102. package/dist/runtime/stealth.js +228 -85
  103. package/dist/runtime/stt-telemetry.d.ts +77 -0
  104. package/dist/runtime/stt-telemetry.js +176 -0
  105. package/dist/runtime/stt.js +15 -6
  106. package/dist/runtime-boundary-lint.d.ts +132 -0
  107. package/dist/runtime-boundary-lint.js +586 -0
  108. package/dist/server/error-stack-frames.d.ts +35 -0
  109. package/dist/server/error-stack-frames.js +219 -0
  110. package/dist/server/index.d.ts +7 -0
  111. package/dist/server/index.js +4 -0
  112. package/dist/server/sdk-version.d.ts +3 -0
  113. package/dist/server/sdk-version.js +19 -0
  114. package/dist/server/self-test.js +1 -18
  115. package/dist/server/sensitive-values.d.ts +1 -1
  116. package/dist/server/sensitive-values.js +9 -0
  117. package/dist/server/serve-implementation.d.ts +51 -2
  118. package/dist/server/serve-implementation.js +337 -90
  119. package/dist/server/types.d.ts +1 -0
  120. package/dist/server/types.js +7 -0
  121. package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
  122. package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
  123. package/dist/stateful/stateful-provider-runtime-context.js +1 -0
  124. package/dist/types.d.ts +123 -1
  125. package/dist/typescript-module.d.ts +39 -0
  126. package/dist/typescript-module.js +72 -0
  127. package/package.json +6 -1
  128. package/src/cli/commands.ts +14 -0
  129. package/src/cli/deployment-intent.ts +364 -0
  130. package/src/cli/migrate-deployment-intent.ts +1908 -0
  131. package/src/cli/migrate-provider-shape.ts +10 -4
  132. package/src/cli/resolver-runtime.ts +72 -0
  133. package/src/define.ts +13 -0
  134. package/src/dev.ts +9 -2
  135. package/src/engine.ts +38 -2
  136. package/src/error-resolution.ts +58 -0
  137. package/src/errors.ts +77 -1
  138. package/src/handle-meta.ts +51 -1
  139. package/src/handle.ts +92 -10
  140. package/src/health-scenario.ts +38 -0
  141. package/src/i18n/error-messages.ts +255 -0
  142. package/src/i18n/index.ts +1 -0
  143. package/src/i18n/keys.ts +80 -6
  144. package/src/index.ts +86 -0
  145. package/src/lint.ts +604 -91
  146. package/src/provider.ts +12 -0
  147. package/src/runtime/browser-telemetry-binding.ts +92 -0
  148. package/src/runtime/browser-telemetry.ts +289 -0
  149. package/src/runtime/browser.ts +249 -65
  150. package/src/runtime/cache-telemetry.ts +247 -0
  151. package/src/runtime/cache.ts +68 -17
  152. package/src/runtime/capability-response.ts +53 -0
  153. package/src/runtime/capability-telemetry.ts +101 -0
  154. package/src/runtime/diagnostic-env.ts +11 -1
  155. package/src/runtime/engine-mode.ts +233 -0
  156. package/src/runtime/handle.ts +9 -1
  157. package/src/runtime/http-telemetry-guard.ts +66 -13
  158. package/src/runtime/http.ts +168 -37
  159. package/src/runtime/instrumentation.ts +105 -16
  160. package/src/runtime/ocr-telemetry.ts +308 -0
  161. package/src/runtime/ocr.ts +114 -74
  162. package/src/runtime/request-telemetry.ts +2 -1
  163. package/src/runtime/resolver-runtime-options.ts +50 -0
  164. package/src/runtime/resolver-shared.ts +9 -4
  165. package/src/runtime/resolver-telemetry.ts +1 -1
  166. package/src/runtime/resolver-vendors/browser.ts +65 -7
  167. package/src/runtime/resolver-vendors/hosts.ts +43 -0
  168. package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
  169. package/src/runtime/resolver-vendors/types.ts +5 -4
  170. package/src/runtime/resolver.ts +64 -21
  171. package/src/runtime/state-telemetry.ts +298 -0
  172. package/src/runtime/state.ts +46 -45
  173. package/src/runtime/stealth-cookies.ts +24 -1
  174. package/src/runtime/stealth-owned-headers.ts +41 -0
  175. package/src/runtime/stealth-telemetry.ts +304 -0
  176. package/src/runtime/stealth.ts +355 -150
  177. package/src/runtime/stt-telemetry.ts +282 -0
  178. package/src/runtime/stt.ts +93 -71
  179. package/src/runtime-boundary-lint.ts +703 -0
  180. package/src/server/error-stack-frames.ts +214 -0
  181. package/src/server/index.ts +53 -0
  182. package/src/server/sdk-version.ts +22 -0
  183. package/src/server/self-test.ts +1 -21
  184. package/src/server/sensitive-values.ts +14 -0
  185. package/src/server/serve-implementation.ts +569 -108
  186. package/src/server/types.ts +7 -0
  187. package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
  188. package/src/stateful/stateful-provider-runtime-context.ts +4 -0
  189. package/src/types.ts +128 -1
  190. package/src/typescript-module.ts +100 -0
  191. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
  192. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
  193. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
  194. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
  195. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
  196. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
  197. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
  198. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
  199. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
  200. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
  201. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
  202. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
  203. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
  204. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
  205. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
  206. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
  207. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
  208. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
  209. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
  210. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
  211. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
  212. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
  213. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
  214. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
  215. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
  216. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
  217. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
  218. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
  219. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
  220. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
  221. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
  222. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
  246. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
  247. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
  248. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
  249. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
  250. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
  251. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
  252. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
  253. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
  254. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
  255. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
  256. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
  257. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
  258. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
  259. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
  260. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
  261. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
package/dist/lint.js CHANGED
@@ -1,17 +1,12 @@
1
- import { createRequire } from "node:module";
2
- import { SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "./error-resolution.js";
3
- import { APIFUSE_HANDLE_META_KEY, handleFieldDescription, handleIssuerList, isHandleIssuedBy, isHandleFieldMeta, } from "./handle-meta.js";
1
+ import { SDK_CANONICAL_ERROR_CODE_RETRYABILITY, SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "./error-resolution.js";
2
+ import { APIFUSE_HANDLE_META_KEY, handleFieldDescription, handleFieldNameFor, handleHasDirectionalFieldNames, handleIssuerList, isHandleFieldMeta, isHandleIssuedBy, } from "./handle-meta.js";
3
+ import { PROVIDER_ERROR_CATALOG_NAMESPACE } from "./i18n/error-messages.js";
4
+ import { getProviderLocaleSegments, isProviderLocaleKey, } from "./i18n/keys.js";
4
5
  import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
6
+ import { isStealthOwnedHeaderName, SDK_OWNED_CHROME_HEADER_PREFIX, } from "./runtime/stealth-owned-headers.js";
7
+ import { lintRuntimeBoundary, scriptKindForSourceFile } from "./runtime-boundary-lint.js";
5
8
  import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
6
- const requireModule = createRequire(import.meta.url);
7
- // `typeof import(...)` keeps the type without emitting a static import: the
8
- // typescript package is a CLI-only dependency and src/lint.ts is production
9
- // runtime, which the typescript-import-boundary test enforces.
10
- let typeScriptModule;
11
- function getTypeScript() {
12
- typeScriptModule ??= requireModule("typescript");
13
- return typeScriptModule;
14
- }
9
+ import { getTypeScript } from "./typescript-module.js";
15
10
  const CREDENTIAL_BEARING_AUTH_MODES = [
16
11
  "credentials",
17
12
  "oauth2",
@@ -750,7 +745,14 @@ function lintSelfHostedBrowserPatterns(provider, options) {
750
745
  }
751
746
  return diagnostics;
752
747
  }
753
- const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(?:ProviderError|ValidationError)\s*\(/g;
748
+ // Captures the constructor name so each rule can pick its own scope: the
749
+ // undeclared-code rule keeps its historical ProviderError/ValidationError
750
+ // surface, while the localization rules also cover AuthError.
751
+ const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(ProviderError|AuthError|ValidationError)\s*\(/g;
752
+ const UNDECLARED_CODE_ERROR_CLASSES = new Set([
753
+ "ProviderError",
754
+ "ValidationError",
755
+ ]);
754
756
  const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
755
757
  const RECORDED_FIXTURE_SOURCE_FILE_PATTERN = /(?:^|\/)__fixtures__(?:\/|$)|(?:^|\/)__tests__\/fixtures(?:\/|$)/;
756
758
  const JAVASCRIPT_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
@@ -758,6 +760,11 @@ const VERSIONED_PROFILE_LITERAL_PATTERN = /\b(?:chrome|chromium|firefox|safari|e
758
760
  const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
759
761
  const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
760
762
  const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
763
+ // Calls that write a header entry as (name, value): Headers/Map `set` and
764
+ // `append`, Node's `setHeader`. Predicates, replacements, and logging calls
765
+ // take string arguments too and must not count as header writes.
766
+ const HEADER_SETTER_CALLEE_PATTERN = /^(?:set|append|setHeader|addHeader)$/;
767
+ const OWNED_CLIENT_HINT_HEADER_PREFIX = "sec-ch-ua";
761
768
  function staticStringText(node) {
762
769
  if (!node)
763
770
  return undefined;
@@ -778,11 +785,83 @@ function staticPropertyName(node) {
778
785
  function isSecChUaHeaderName(value) {
779
786
  return value?.toLowerCase() === "sec-ch-ua";
780
787
  }
781
- function collectBrowserVersionLiteralFindings(source) {
788
+ /**
789
+ * True for the code paths that reach the stealth transport: `ctx.stealth`,
790
+ * `ctx["stealth"]`, and `const { stealth } = ctx`. Read from the AST so a
791
+ * comment or string that mentions ctx.stealth does not scope a ctx.http file
792
+ * into this rule.
793
+ */
794
+ function isStealthContextAccess(node) {
795
+ const ts = getTypeScript();
796
+ if (ts.isPropertyAccessExpression(node)) {
797
+ return (ts.isIdentifier(node.expression) &&
798
+ node.expression.text === "ctx" &&
799
+ node.name.text === "stealth");
800
+ }
801
+ if (ts.isElementAccessExpression(node)) {
802
+ return (ts.isIdentifier(node.expression) &&
803
+ node.expression.text === "ctx" &&
804
+ staticStringText(node.argumentExpression) === "stealth");
805
+ }
806
+ if (ts.isBindingElement(node)) {
807
+ const key = node.propertyName ?? node.name;
808
+ if (!ts.isIdentifier(key) || key.text !== "stealth")
809
+ return false;
810
+ const declaration = node.parent?.parent;
811
+ return (declaration !== undefined &&
812
+ ts.isVariableDeclaration(declaration) &&
813
+ declaration.initializer !== undefined &&
814
+ ts.isIdentifier(declaration.initializer) &&
815
+ declaration.initializer.text === "ctx");
816
+ }
817
+ return false;
818
+ }
819
+ function isStaticUndefined(node) {
820
+ if (!node)
821
+ return false;
822
+ const ts = getTypeScript();
823
+ return (ts.isIdentifier(node) && node.text === "undefined") || ts.isVoidExpression(node);
824
+ }
825
+ function calleeName(node) {
782
826
  const ts = getTypeScript();
783
- const sourceFile = ts.createSourceFile("provider-source.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
784
- const findings = [];
827
+ if (ts.isPropertyAccessExpression(node.expression))
828
+ return node.expression.name.text;
829
+ if (ts.isIdentifier(node.expression))
830
+ return node.expression.text;
831
+ return undefined;
832
+ }
833
+ /**
834
+ * Header names this rule reports in ctx.stealth files: the Sec-Fetch-* and
835
+ * client-hint (sec-ch-ua*) families, restricted to what the stealth runtime
836
+ * really rejects (isStealthOwnedHeaderName). Deliberately narrower than the
837
+ * runtime rejection: host, connection, and accept-encoding are left alone
838
+ * because ctx.http callers set them legitimately in the same files, and
839
+ * user-agent only reaches a finding through the "user-agent" kind, which
840
+ * matches a versioned literal value rather than the name — a user-agent read
841
+ * from a variable is rejected by the runtime and stays silent here. Widening
842
+ * the set needs a per-file transport scope and a suppression path first.
843
+ */
844
+ function isReportedOwnedHeaderName(value) {
845
+ if (value === undefined)
846
+ return false;
847
+ const name = value.toLowerCase();
848
+ if (!isStealthOwnedHeaderName(name))
849
+ return false;
850
+ if (name.startsWith(SDK_OWNED_CHROME_HEADER_PREFIX)) {
851
+ return name.length > SDK_OWNED_CHROME_HEADER_PREFIX.length;
852
+ }
853
+ return name.startsWith(OWNED_CLIENT_HINT_HEADER_PREFIX);
854
+ }
855
+ function collectBrowserVersionLiteralFindings(fileName, source) {
856
+ const ts = getTypeScript();
857
+ const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKindForSourceFile(ts, fileName));
858
+ let findings = [];
785
859
  const seen = new Set();
860
+ // Owned-header entries only count once the file is known to reach
861
+ // ctx.stealth, which may appear after the entry; collect them and decide
862
+ // after the walk.
863
+ let stealthAccess = false;
864
+ const ownedHeaderEntries = [];
786
865
  const addFinding = (kind, literal, position) => {
787
866
  const key = `${kind}:${position}:${literal}`;
788
867
  if (seen.has(key))
@@ -805,23 +884,73 @@ function collectBrowserVersionLiteralFindings(source) {
805
884
  addFinding("sec-ch-ua", "sec-ch-ua", node.getStart(sourceFile));
806
885
  }
807
886
  };
887
+ // A header entry `name: value` in any of the shapes below. In a ctx.stealth
888
+ // file an owned name is the finding (the runtime rejects it whatever the
889
+ // value); elsewhere only a versioned sec-ch-ua value is. A statically
890
+ // undefined value is exempt only in the "record" shapes, the ones the
891
+ // runtime drops before the ownership check (`normalizedHeaderEntries`):
892
+ // `Headers.set(name, undefined)` stringifies to "undefined" and still
893
+ // throws, so it stays a finding.
894
+ const inspectHeaderEntry = (nameNode, name, valueNode, ownedHeaderShape) => {
895
+ if (name === undefined)
896
+ return;
897
+ if (ownedHeaderShape !== false &&
898
+ isReportedOwnedHeaderName(name) &&
899
+ !(ownedHeaderShape === "record" && isStaticUndefined(valueNode))) {
900
+ ownedHeaderEntries.push({
901
+ name,
902
+ position: nameNode.getStart(sourceFile),
903
+ valuePosition: valueNode?.getStart(sourceFile),
904
+ });
905
+ }
906
+ if (isSecChUaHeaderName(name))
907
+ inspectSecChUaValue(valueNode);
908
+ };
808
909
  const visit = (node) => {
809
910
  const text = staticStringText(node);
810
911
  if (text !== undefined)
811
912
  inspectLiteral(text, node.getStart(sourceFile));
812
- if (ts.isPropertyAssignment(node) && isSecChUaHeaderName(staticPropertyName(node.name))) {
813
- inspectSecChUaValue(node.initializer);
814
- }
815
- if (ts.isCallExpression(node) && isSecChUaHeaderName(staticStringText(node.arguments[0]))) {
816
- inspectSecChUaValue(node.arguments[1]);
817
- }
818
- if (ts.isArrayLiteralExpression(node) &&
819
- isSecChUaHeaderName(staticStringText(node.elements[0]))) {
820
- inspectSecChUaValue(node.elements[1]);
913
+ if (isStealthContextAccess(node))
914
+ stealthAccess = true;
915
+ if (ts.isPropertyAssignment(node)) {
916
+ inspectHeaderEntry(node.name, staticPropertyName(node.name), node.initializer, "record");
917
+ }
918
+ if (ts.isCallExpression(node) && node.arguments[0]) {
919
+ // Only `headers.set(name, value)`-style calls write a header;
920
+ // `name.startsWith("sec-fetch-")` or `log("sec-fetch-dest", x)` do not.
921
+ inspectHeaderEntry(node.arguments[0], staticStringText(node.arguments[0]), node.arguments[1], node.arguments.length >= 2 && HEADER_SETTER_CALLEE_PATTERN.test(calleeName(node) ?? "")
922
+ ? "entry"
923
+ : false);
924
+ }
925
+ if (ts.isArrayLiteralExpression(node) && node.elements[0]) {
926
+ // Only a `[name, value]` tuple is a header. A name list such as
927
+ // `new Set(["sec-ch-ua-mobile", "sec-ch-ua-platform"])` is not: it is
928
+ // the argument of a constructor, or its second element is itself an
929
+ // owned name.
930
+ const second = node.elements[1];
931
+ inspectHeaderEntry(node.elements[0], staticStringText(node.elements[0]), second, node.elements.length === 2 &&
932
+ !ts.isNewExpression(node.parent) &&
933
+ !isStealthOwnedHeaderName(staticStringText(second) ?? "")
934
+ ? "entry"
935
+ : false);
936
+ }
937
+ if (ts.isBinaryExpression(node) &&
938
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
939
+ ts.isElementAccessExpression(node.left)) {
940
+ const nameNode = node.left.argumentExpression;
941
+ inspectHeaderEntry(nameNode, staticStringText(nameNode), node.right, "record");
821
942
  }
822
943
  ts.forEachChild(node, visit);
823
944
  };
824
945
  visit(sourceFile);
946
+ if (stealthAccess) {
947
+ // The owned name is the finding; do not also report its sec-ch-ua value.
948
+ const shadowedValues = new Set(ownedHeaderEntries.map((entry) => entry.valuePosition));
949
+ findings = findings.filter((finding) => !(finding.kind === "sec-ch-ua" && shadowedValues.has(finding.position)));
950
+ for (const entry of ownedHeaderEntries) {
951
+ addFinding("owned-header", entry.name, entry.position);
952
+ }
953
+ }
825
954
  return findings.sort((left, right) => left.position - right.position);
826
955
  }
827
956
  function browserVersionLiteralMessage(finding) {
@@ -832,6 +961,8 @@ function browserVersionLiteralMessage(finding) {
832
961
  return `Hardcoded User-Agent browser version "${finding.literal}" can disagree with the stealth TLS fingerprint. Remove the literal. ctx.stealth owns User-Agent and rejects a caller value with STEALTH_HEADER_OVERRIDE_UNSUPPORTED, so omit the header there and select the profile with stealth: { browser: "chrome", os: "macos" }. For ctx.http only, derive it from getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.`;
833
962
  case "sec-ch-ua":
834
963
  return 'Hardcoded sec-ch-ua versions can disagree with the stealth TLS fingerprint. Remove the literal and let ctx.stealth generate client hints from stealth: { browser: "chrome", os: "macos" }. ctx.stealth also owns User-Agent, so omit that header there as well; for ctx.http only, derive it from getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.';
964
+ case "owned-header":
965
+ return `ctx.stealth owns the "${finding.literal}" header and rejects a caller value with STEALTH_HEADER_OVERRIDE_UNSUPPORTED (HTTP 500 at request time). Remove it; declare stealth: { requestClass: "navigation" | "xhr" | "post" } to drive Sec-Fetch-* and select the profile with stealth: { browser: "chrome", os: "macos" } for client hints.`;
835
966
  }
836
967
  }
837
968
  function lintBrowserVersionLiterals(provider) {
@@ -841,19 +972,27 @@ function lintBrowserVersionLiterals(provider) {
841
972
  !RECORDED_FIXTURE_SOURCE_FILE_PATTERN.test(filePath));
842
973
  if (sourceFiles.length > 0) {
843
974
  for (const [filePath, source] of sourceFiles) {
844
- sources.push({ field: `sourceFiles.${filePath}`, source });
975
+ sources.push({ field: `sourceFiles.${filePath}`, fileName: filePath, source });
845
976
  }
846
977
  }
847
978
  else {
848
979
  if (provider.authFlowSource)
849
- sources.push({ field: "auth.flow", source: provider.authFlowSource });
980
+ sources.push({
981
+ field: "auth.flow",
982
+ fileName: "auth-flow.ts",
983
+ source: provider.authFlowSource,
984
+ });
850
985
  for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
851
986
  const source = getOperationSource(operation);
852
987
  if (source)
853
- sources.push({ field: `operations.${operationKey}.handler`, source });
988
+ sources.push({
989
+ field: `operations.${operationKey}.handler`,
990
+ fileName: `${operationKey}.ts`,
991
+ source,
992
+ });
854
993
  }
855
994
  }
856
- return sources.flatMap(({ field, source }) => collectBrowserVersionLiteralFindings(source).map((finding) => ({
995
+ return sources.flatMap(({ field, fileName, source }) => collectBrowserVersionLiteralFindings(fileName, source).map((finding) => ({
857
996
  rule: "browser-version-literal",
858
997
  level: "error",
859
998
  field,
@@ -959,14 +1098,14 @@ function extractBalancedCallArguments(source, startIndex) {
959
1098
  return undefined;
960
1099
  }
961
1100
  /**
962
- * Collects literal string values of top-level `code:` properties inside a
963
- * ProviderError/ValidationError options object. Only plain `"..."` / `'...'`
964
- * literals at options-object depth count; computed codes (identifiers,
1101
+ * Collects literal string values of the named top-level properties inside a
1102
+ * ProviderError/AuthError/ValidationError options object. Only plain `"..."` /
1103
+ * `'...'` literals at options-object depth count; computed values (identifiers,
965
1104
  * ternaries, template substitutions, concatenations, escapes) are skipped
966
- * silently so the rule never guesses.
1105
+ * silently so the rules never guess. The first literal wins per property name.
967
1106
  */
968
- function collectLiteralErrorCodeValues(args) {
969
- const codes = [];
1107
+ function collectLiteralErrorOptionValues(args, optionNames) {
1108
+ const collected = {};
970
1109
  let braceDepth = 0;
971
1110
  let parenDepth = 0;
972
1111
  let bracketDepth = 0;
@@ -976,7 +1115,7 @@ function collectLiteralErrorCodeValues(args) {
976
1115
  if (char === '"' || char === "'" || char === "`") {
977
1116
  const end = skipStringLiteral(args, index);
978
1117
  if (end < 0) {
979
- return codes;
1118
+ return collected;
980
1119
  }
981
1120
  index = end;
982
1121
  previousSignificantChar = char;
@@ -985,7 +1124,7 @@ function collectLiteralErrorCodeValues(args) {
985
1124
  if (char === "/" && args[index + 1] === "/") {
986
1125
  const newline = args.indexOf("\n", index);
987
1126
  if (newline === -1) {
988
- return codes;
1127
+ return collected;
989
1128
  }
990
1129
  index = newline;
991
1130
  continue;
@@ -993,7 +1132,7 @@ function collectLiteralErrorCodeValues(args) {
993
1132
  if (char === "/" && args[index + 1] === "*") {
994
1133
  const end = args.indexOf("*/", index + 2);
995
1134
  if (end === -1) {
996
- return codes;
1135
+ return collected;
997
1136
  }
998
1137
  index = end + 1;
999
1138
  continue;
@@ -1022,9 +1161,13 @@ function collectLiteralErrorCodeValues(args) {
1022
1161
  else if (braceDepth === 1 &&
1023
1162
  parenDepth === 0 &&
1024
1163
  bracketDepth === 0 &&
1025
- (previousSignificantChar === "{" || previousSignificantChar === ",") &&
1026
- args.startsWith("code", index)) {
1027
- let cursor = index + "code".length;
1164
+ (previousSignificantChar === "{" || previousSignificantChar === ",")) {
1165
+ const optionName = optionNames.find((name) => startsWithOptionName(args, index, name));
1166
+ if (optionName === undefined) {
1167
+ previousSignificantChar = char;
1168
+ continue;
1169
+ }
1170
+ let cursor = index + optionName.length;
1028
1171
  while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
1029
1172
  cursor++;
1030
1173
  }
@@ -1045,32 +1188,72 @@ function collectLiteralErrorCodeValues(args) {
1045
1188
  const nextChar = after < args.length ? (args[after] ?? "") : "";
1046
1189
  if (!value.includes("\\") &&
1047
1190
  (nextChar === "," || nextChar === "}" || nextChar === "")) {
1048
- codes.push(value);
1191
+ collected[optionName] ??= value;
1049
1192
  }
1050
1193
  index = end;
1051
1194
  previousSignificantChar = quote;
1052
1195
  continue;
1053
1196
  }
1054
- return codes;
1197
+ return collected;
1055
1198
  }
1056
1199
  }
1057
1200
  }
1058
1201
  previousSignificantChar = char;
1059
1202
  }
1060
- return codes;
1203
+ return collected;
1061
1204
  }
1062
- function collectLiteralThrownErrorCodes(source) {
1063
- const codes = [];
1205
+ /** True when `name` starts at `index` and is not the prefix of a longer identifier. */
1206
+ function startsWithOptionName(args, index, name) {
1207
+ if (!args.startsWith(name, index))
1208
+ return false;
1209
+ const next = args[index + name.length] ?? "";
1210
+ return !/[A-Za-z0-9_$]/.test(next);
1211
+ }
1212
+ const THROWN_ERROR_LOCALE_OPTION_NAMES = ["code", "messageKey", "fixKey"];
1213
+ function collectThrownErrorConstructionSites(source) {
1214
+ const sites = [];
1064
1215
  THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = 0;
1065
1216
  for (let match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source); match; match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source)) {
1066
1217
  const argsStart = match.index + match[0].length;
1067
1218
  const args = extractBalancedCallArguments(source, argsStart);
1068
1219
  if (args !== undefined) {
1069
- codes.push(...collectLiteralErrorCodeValues(args));
1220
+ const values = collectLiteralErrorOptionValues(args, THROWN_ERROR_LOCALE_OPTION_NAMES);
1221
+ sites.push({
1222
+ errorClass: match[1] ?? "",
1223
+ ...(values.code === undefined ? {} : { code: values.code }),
1224
+ ...(values.messageKey === undefined ? {} : { messageKey: values.messageKey }),
1225
+ ...(values.fixKey === undefined ? {} : { fixKey: values.fixKey }),
1226
+ });
1070
1227
  }
1071
1228
  THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = argsStart;
1072
1229
  }
1073
- return codes;
1230
+ return sites;
1231
+ }
1232
+ /**
1233
+ * The provider sources a throw-site rule scans, with the diagnostic field path
1234
+ * each one reports under. Prefers the CLI-collected source files (whole-repo
1235
+ * coverage) and falls back to the handler/auth-flow function text available
1236
+ * when `lintProvider` is called without them. Test sources are excluded.
1237
+ */
1238
+ function collectThrowSiteLintSources(provider) {
1239
+ const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath));
1240
+ if (sourceFiles.length > 0) {
1241
+ return sourceFiles.map(([filePath, source]) => ({
1242
+ field: `sourceFiles.${filePath}`,
1243
+ source,
1244
+ }));
1245
+ }
1246
+ const sources = [];
1247
+ if (provider.authFlowSource) {
1248
+ sources.push({ field: "auth.flow", source: provider.authFlowSource });
1249
+ }
1250
+ for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1251
+ const source = getOperationSource(operation);
1252
+ if (source) {
1253
+ sources.push({ field: `operations.${operationKey}.handler`, source });
1254
+ }
1255
+ }
1256
+ return sources;
1074
1257
  }
1075
1258
  /**
1076
1259
  * Static counterpart of the runtime `unregistered_provider_error_code`
@@ -1102,38 +1285,216 @@ function lintUndeclaredThrownErrorCodes(provider) {
1102
1285
  }
1103
1286
  }
1104
1287
  }
1105
- const sources = [];
1106
- const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath));
1107
- if (sourceFiles.length > 0) {
1108
- for (const [filePath, source] of sourceFiles) {
1109
- sources.push({ field: `sourceFiles.${filePath}`, source });
1288
+ const sources = collectThrowSiteLintSources(provider);
1289
+ const diagnostics = [];
1290
+ for (const { field, source } of sources) {
1291
+ const undeclaredCodes = new Set(collectThrownErrorConstructionSites(source)
1292
+ .filter((site) => UNDECLARED_CODE_ERROR_CLASSES.has(site.errorClass))
1293
+ .flatMap((site) => (site.code === undefined ? [] : [site.code]))
1294
+ .filter((code) => !knownCodes.has(code)));
1295
+ for (const code of undeclaredCodes) {
1296
+ diagnostics.push({
1297
+ rule: "thrown-error-code-undeclared",
1298
+ level: "warn",
1299
+ field,
1300
+ message: `Thrown error code "${code}" (${field}) is neither SDK-registered nor declared in any operation's errorCodes; at runtime it serves HTTP 500 and emits the unregistered_provider_error_code signal. Declare it in the owning operation's errorCodes with status and retryable.`,
1301
+ });
1110
1302
  }
1111
1303
  }
1112
- else {
1113
- if (provider.authFlowSource) {
1114
- sources.push({ field: "auth.flow", source: provider.authFlowSource });
1115
- }
1116
- for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1117
- const source = getOperationSource(operation);
1118
- if (source) {
1119
- sources.push({ field: `operations.${operationKey}.handler`, source });
1304
+ return diagnostics;
1305
+ }
1306
+ /**
1307
+ * Flags an operation `errorCodes` entry whose declared `status` or `retryable`
1308
+ * contradicts the SDK's canonical resolution for that code.
1309
+ *
1310
+ * For a code the SDK status-maps but does not runtime-own (UPSTREAM_ERROR,
1311
+ * BLOCKED, and the fleet-consensus codes registered alongside them), the
1312
+ * declaration still wins at runtime — `toStatusCode` reads the declaration
1313
+ * before the canonical map. So registering a code never silently rewrites an
1314
+ * existing declaration's status, and it must not: that would change a served
1315
+ * status behind the author's back. What it does create is a fleet that answers
1316
+ * one code two ways. This rule is where that divergence surfaces, by name, with
1317
+ * the canonical value to converge on.
1318
+ *
1319
+ * Codes the SDK runtime-owns are handled separately: `defineProvider` already
1320
+ * warns there, because for those the declaration really is ignored.
1321
+ *
1322
+ * Warning level, deliberately: the divergent providers are live and converge on
1323
+ * their own schedule, so this must not fail `apifuse check`.
1324
+ */
1325
+ function lintErrorCodeDeclarationConflicts(provider) {
1326
+ const diagnostics = [];
1327
+ const providerId = provider.id ?? "unknown";
1328
+ for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1329
+ for (const [index, entry] of (operation.errorCodes ?? []).entries()) {
1330
+ if (typeof entry?.code !== "string")
1331
+ continue;
1332
+ // SDK-owned codes ignore the declaration outright; defineProvider owns
1333
+ // that message so authors are not told two different things.
1334
+ if (SDK_RUNTIME_OWNED_ERROR_CODES.has(entry.code))
1335
+ continue;
1336
+ const field = `operations.${operationKey}.errorCodes[${index}]`;
1337
+ const canonicalStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(entry.code);
1338
+ if (canonicalStatus !== undefined && entry.status !== undefined) {
1339
+ if (entry.status !== canonicalStatus) {
1340
+ diagnostics.push({
1341
+ rule: "error-code-status-conflicts-sdk",
1342
+ level: "warn",
1343
+ field: `${field}.status`,
1344
+ message: `Provider "${providerId}" operation "${operationKey}" declares status ${entry.status} for SDK-registered error code "${entry.code}", whose canonical status is ${canonicalStatus}. The declaration still wins at runtime, so this operation serves ${entry.status} while the rest of the fleet serves ${canonicalStatus} for the same code. Drop the status (the SDK supplies ${canonicalStatus}) or, if ${entry.status} is genuinely the right answer here, throw a distinct code that means that.`,
1345
+ });
1346
+ }
1347
+ }
1348
+ const canonicalRetryable = SDK_CANONICAL_ERROR_CODE_RETRYABILITY.get(entry.code);
1349
+ if (canonicalRetryable !== undefined &&
1350
+ entry.retryable !== undefined &&
1351
+ entry.retryable !== canonicalRetryable) {
1352
+ diagnostics.push({
1353
+ rule: "error-code-retryable-conflicts-sdk",
1354
+ level: "warn",
1355
+ field: `${field}.retryable`,
1356
+ message: `Provider "${providerId}" operation "${operationKey}" declares retryable ${entry.retryable} for SDK-registered error code "${entry.code}", whose canonical retryability is ${canonicalRetryable}. The declaration still wins at runtime, so callers and the Gateway retry policy get a different answer here than everywhere else the same code is thrown. Drop the retryable (the SDK supplies ${canonicalRetryable}) or throw a distinct code for the retryable condition.`,
1357
+ });
1120
1358
  }
1121
1359
  }
1122
1360
  }
1361
+ return diagnostics;
1362
+ }
1363
+ function errorLocaleKeyStatus(catalog, key) {
1364
+ if (!isProviderLocaleKey(key))
1365
+ return "malformed";
1366
+ const value = getProviderLocaleSegments(catalog, key.split("."));
1367
+ return typeof value === "string" && value.trim().length > 0 ? "ok" : "missing";
1368
+ }
1369
+ function hasDerivedErrorCatalogText(catalog, code, field) {
1370
+ const value = getProviderLocaleSegments(catalog, [PROVIDER_ERROR_CATALOG_NAMESPACE, code, field]);
1371
+ return typeof value === "string" && value.trim().length > 0;
1372
+ }
1373
+ /**
1374
+ * True when a thrown code reaches the caller as a real provider error whose
1375
+ * text the declared/derived catalog path can actually reach.
1376
+ *
1377
+ * A code qualifies either because the provider declares it in an operation's
1378
+ * `errorCodes`, or because the SDK registers a canonical status for it. The
1379
+ * second arm matters because registering a code is precisely what lets a
1380
+ * provider *delete* the declaration — AUTHORING tells it to for
1381
+ * `UPSTREAM_AUTH_ERROR`, `UPSTREAM_SCHEMA_ERROR` and `INVALID_REQUEST` — and an
1382
+ * undeclared throw of a registered code is no longer the HTTP 500 that
1383
+ * `thrown-error-code-undeclared` reports. Without this arm the two rules leave
1384
+ * a silent hole exactly where the fleet is being told to converge: a 502
1385
+ * `UPSTREAM_SCHEMA_ERROR` served untranslated with nothing warning about it.
1386
+ *
1387
+ * Codes the SDK runtime-owns are excluded: `sdkOwnsErrorResolution` skips the
1388
+ * declaration and derived candidates for them at serve time, so the catalog
1389
+ * entry this rule asks for would never take effect.
1390
+ */
1391
+ function isCatalogReachableThrownCode(code, declaredCodes) {
1392
+ if (declaredCodes.has(code))
1393
+ return true;
1394
+ return (!SDK_RUNTIME_OWNED_ERROR_CODES.has(code) && SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.has(code));
1395
+ }
1396
+ /**
1397
+ * Authoring rules for localized provider error text (see
1398
+ * `ProviderErrorOptions.messageKey`).
1399
+ *
1400
+ * - `error-locale-key-missing` / `error-locale-key-malformed` (**error**): a
1401
+ * literal `messageKey`/`fixKey`, whether on a throw site or on an
1402
+ * `errorCodes[]` declaration, that the provider's `en` catalog does not
1403
+ * resolve to non-empty text, or that is not a legal locale dot path. At
1404
+ * runtime such a key is silently skipped and the caller gets the English
1405
+ * literal, so nothing fails loudly without this rule.
1406
+ * - `thrown-error-message-not-localized` (**warn**): a throw site whose literal
1407
+ * `code` is declared in the provider's `errorCodes` — or SDK-registered, and
1408
+ * so servable without a declaration — but that carries no `messageKey`, no
1409
+ * declaration-level `messageKey`, and no derived `errors.<code>.message` in
1410
+ * `en`. Such an error is served untranslated to every caller. Warning while
1411
+ * the ~2,000 existing sites migrate; it is promoted to error per provider as
1412
+ * each migration wave lands.
1413
+ *
1414
+ * Both rules are skipped entirely when the caller did not supply
1415
+ * `localeCatalogEn`: without the catalog they could only guess.
1416
+ */
1417
+ function lintErrorMessageLocalization(provider) {
1418
+ const catalog = provider.localeCatalogEn;
1419
+ if (!catalog)
1420
+ return [];
1123
1421
  const diagnostics = [];
1124
- for (const { field, source } of sources) {
1125
- const undeclaredCodes = new Set(collectLiteralThrownErrorCodes(source).filter((code) => !knownCodes.has(code)));
1126
- for (const code of undeclaredCodes) {
1422
+ const declaredCodes = new Set();
1423
+ const codesWithDeclaredMessageKey = new Set();
1424
+ for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1425
+ for (const entry of operation.errorCodes ?? []) {
1426
+ if (typeof entry?.code !== "string")
1427
+ continue;
1428
+ declaredCodes.add(entry.code);
1429
+ for (const keyField of ["messageKey", "fixKey"]) {
1430
+ const key = entry[keyField];
1431
+ if (typeof key !== "string")
1432
+ continue;
1433
+ if (keyField === "messageKey")
1434
+ codesWithDeclaredMessageKey.add(entry.code);
1435
+ const status = errorLocaleKeyStatus(catalog, key);
1436
+ if (status === "ok")
1437
+ continue;
1438
+ diagnostics.push(errorLocaleKeyDiagnostic(status, `operations.${operationKey}.errorCodes`, key, `errorCodes entry "${entry.code}" of operation "${operationKey}"`));
1439
+ }
1440
+ }
1441
+ }
1442
+ for (const { field, source } of collectThrowSiteLintSources(provider)) {
1443
+ const reportedKeys = new Set();
1444
+ const reportedCodes = new Set();
1445
+ for (const site of collectThrownErrorConstructionSites(source)) {
1446
+ for (const key of [site.messageKey, site.fixKey]) {
1447
+ if (key === undefined || reportedKeys.has(key))
1448
+ continue;
1449
+ const status = errorLocaleKeyStatus(catalog, key);
1450
+ if (status === "ok")
1451
+ continue;
1452
+ reportedKeys.add(key);
1453
+ diagnostics.push(errorLocaleKeyDiagnostic(status, field, key, `a thrown error in ${field}`));
1454
+ }
1455
+ const code = site.code;
1456
+ if (code === undefined || reportedCodes.has(code))
1457
+ continue;
1458
+ if (site.messageKey !== undefined)
1459
+ continue;
1460
+ if (!isCatalogReachableThrownCode(code, declaredCodes))
1461
+ continue;
1462
+ if (codesWithDeclaredMessageKey.has(code))
1463
+ continue;
1464
+ if (hasDerivedErrorCatalogText(catalog, code, "message"))
1465
+ continue;
1466
+ reportedCodes.add(code);
1467
+ const origin = declaredCodes.has(code)
1468
+ ? `declared code "${code}"`
1469
+ : `SDK-registered code "${code}"`;
1470
+ const declarationClause = declaredCodes.has(code)
1471
+ ? "its errorCodes declaration has none, and"
1472
+ : "no errorCodes declaration supplies one, and";
1127
1473
  diagnostics.push({
1128
- rule: "thrown-error-code-undeclared",
1474
+ rule: "thrown-error-message-not-localized",
1129
1475
  level: "warn",
1130
1476
  field,
1131
- message: `Thrown error code "${code}" (${field}) is neither SDK-registered nor declared in any operation's errorCodes; at runtime it serves HTTP 500 and emits the unregistered_provider_error_code signal. Declare it in the owning operation's errorCodes with status and retryable.`,
1477
+ message: `${site.errorClass} with ${origin} (${field}) has no messageKey, ${declarationClause} locales/en.json has no "${PROVIDER_ERROR_CATALOG_NAMESPACE}.${code}.message"; this error is served untranslated to ko and ja callers. Add messageKey (or "${PROVIDER_ERROR_CATALOG_NAMESPACE}.${code}.message" to every locale catalog).`,
1132
1478
  });
1133
1479
  }
1134
1480
  }
1135
1481
  return diagnostics;
1136
1482
  }
1483
+ function errorLocaleKeyDiagnostic(status, field, key, owner) {
1484
+ return status === "malformed"
1485
+ ? {
1486
+ rule: "error-locale-key-malformed",
1487
+ level: "error",
1488
+ field,
1489
+ message: `Locale key ${JSON.stringify(key)} on ${owner} is not a locale dot path such as "errors.upstreamSchema.message" or "errors.UPSTREAM_SCHEMA_ERROR.message"; segments are camelCase, an array index, or — directly under "errors" — the error code itself; the SDK skips a malformed key at serve time and the caller gets the English literal instead.`,
1490
+ }
1491
+ : {
1492
+ rule: "error-locale-key-missing",
1493
+ level: "error",
1494
+ field,
1495
+ message: `Locale key ${JSON.stringify(key)} on ${owner} is missing from locales/en.json (or resolves to a non-string/empty value); the SDK falls back to the English literal at serve time, so the key never takes effect.`,
1496
+ };
1497
+ }
1137
1498
  function collectHandleFields(schema, basePath, key = undefined, seen = new Set()) {
1138
1499
  if (!isSchema(schema))
1139
1500
  return [];
@@ -1204,6 +1565,7 @@ function readHandleKindDeclarations(value) {
1204
1565
  return (typeof record.name === "string" &&
1205
1566
  (record.type === "cursor" || record.type === "draft") &&
1206
1567
  typeof record.fieldName === "string" &&
1568
+ (record.outputFieldName === undefined || typeof record.outputFieldName === "string") &&
1207
1569
  (record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy)));
1208
1570
  });
1209
1571
  }
@@ -1239,7 +1601,13 @@ function lintHandleDeclarations(provider) {
1239
1601
  for (const { side, fields } of positions) {
1240
1602
  for (const occurrence of fields) {
1241
1603
  const field = `operations.${operationKey}.${occurrence.path}`;
1242
- const { kind, fieldName } = occurrence.meta;
1604
+ const { kind } = occurrence.meta;
1605
+ // A kind may name one handle differently in each direction (the canon's
1606
+ // `cursor` in, `next_cursor` out). The rule is still exact: each side
1607
+ // must use the key declared for THAT side, so the handle's identity
1608
+ // stays readable from the property key and the round trip has exactly
1609
+ // one edge — read `output`, send back under `input`.
1610
+ const expected = handleFieldNameFor(occurrence.meta, side);
1243
1611
  if (!kindByName.has(kind)) {
1244
1612
  diagnostics.push({
1245
1613
  rule: "handle-kind-undeclared",
@@ -1248,12 +1616,16 @@ function lintHandleDeclarations(provider) {
1248
1616
  message: `${providerLabel} operation "${operationKey}" ${side} field "${occurrence.path}" is a handle of kind "${kind}", which is not declared in provider.handle. Add the kind to handle: [...] or remove the field.`,
1249
1617
  });
1250
1618
  }
1251
- if (occurrence.key !== fieldName) {
1619
+ if (occurrence.key !== expected) {
1620
+ const directional = handleHasDirectionalFieldNames(occurrence.meta);
1621
+ const advice = directional
1622
+ ? `Use the kind's ${side} fieldName on every ${side} schema; its ${side === "input" ? "output" : "input"} fieldName is "${handleFieldNameFor(occurrence.meta, side === "input" ? "output" : "input")}".`
1623
+ : "Use the same key in every schema so the LLM sees one name.";
1252
1624
  diagnostics.push({
1253
1625
  rule: "handle-field-name",
1254
1626
  level: "error",
1255
1627
  field,
1256
- message: `${providerLabel} operation "${operationKey}" ${side} declares handle kind "${kind}" under property "${occurrence.key}" but the kind's fieldName is "${fieldName}". Use the same key in every schema so the LLM sees one name.`,
1628
+ message: `${providerLabel} operation "${operationKey}" ${side} declares handle kind "${kind}" under property "${occurrence.key}" but the kind's ${directional ? `${side} ` : ""}fieldName is "${expected}". ${advice}`,
1257
1629
  });
1258
1630
  }
1259
1631
  if (side === "input") {
@@ -1280,7 +1652,7 @@ function lintHandleDeclarations(provider) {
1280
1652
  rule: "handle-field-parity",
1281
1653
  level: "error",
1282
1654
  field,
1283
- message: `${providerLabel} handle kind "${kind.name}" is never issued: no operation output contains its field "${kind.fieldName}". Add ${kind.name}.field() to the issuing operation's output.`,
1655
+ message: `${providerLabel} handle kind "${kind.name}" is never issued: no operation output contains its field "${handleFieldNameFor(kind, "output")}". Add ${kind.name}.field() to the issuing operation's output.`,
1284
1656
  });
1285
1657
  }
1286
1658
  if (!accepted) {
@@ -1288,7 +1660,7 @@ function lintHandleDeclarations(provider) {
1288
1660
  rule: "handle-field-parity",
1289
1661
  level: "error",
1290
1662
  field,
1291
- message: `${providerLabel} handle kind "${kind.name}" is never accepted: no operation input contains its field "${kind.fieldName}". Add ${kind.name}.field() to the consuming operation's input.`,
1663
+ message: `${providerLabel} handle kind "${kind.name}" is never accepted: no operation input contains its field "${handleFieldNameFor(kind, "input")}". Add ${kind.name}.field() to the consuming operation's input.`,
1292
1664
  });
1293
1665
  }
1294
1666
  if (kind.issuedBy === undefined)
@@ -1315,7 +1687,7 @@ function lintHandleDeclarations(provider) {
1315
1687
  rule: "handle-issued-by",
1316
1688
  level: "error",
1317
1689
  field,
1318
- message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind.fieldName}" handle field.`,
1690
+ message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${handleFieldNameFor(kind, "output")}" handle field.`,
1319
1691
  });
1320
1692
  }
1321
1693
  else {
@@ -1325,7 +1697,7 @@ function lintHandleDeclarations(provider) {
1325
1697
  field,
1326
1698
  message: `${providerLabel} handle kind "${kind.name}" declares issuedBy ${known
1327
1699
  .map((operationKey) => `"${operationKey}"`)
1328
- .join(", ")}, but none of those operations' outputs has a "${kind.fieldName}" handle field. At least one listed issuer must output it.`,
1700
+ .join(", ")}, but none of those operations' outputs has a "${handleFieldNameFor(kind, "output")}" handle field. At least one listed issuer must output it.`,
1329
1701
  });
1330
1702
  }
1331
1703
  }
@@ -1350,7 +1722,7 @@ const LEGACY_CHOICE_IDENTIFIERS = new Set([
1350
1722
  * the provider) while code inside `${}` interpolations still does. Falls back
1351
1723
  * to a raw-source regex when TypeScript is not installed.
1352
1724
  */
1353
- function findLegacyChoiceUsage(source) {
1725
+ function findLegacyChoiceUsage(fileName, source) {
1354
1726
  let ts;
1355
1727
  try {
1356
1728
  ts = getTypeScript();
@@ -1358,7 +1730,7 @@ function findLegacyChoiceUsage(source) {
1358
1730
  catch {
1359
1731
  return LEGACY_CHOICE_USAGE_PATTERN.exec(source)?.[0];
1360
1732
  }
1361
- const file = ts.createSourceFile("provider.ts", source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
1733
+ const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, false, scriptKindForSourceFile(ts, fileName));
1362
1734
  let found;
1363
1735
  const visit = (node) => {
1364
1736
  if (found)
@@ -1382,19 +1754,23 @@ function findLegacyChoiceUsage(source) {
1382
1754
  function lintLegacyChoiceUsage(provider) {
1383
1755
  const sources = [];
1384
1756
  if (provider.authFlowSource) {
1385
- sources.push({ field: "auth.flow", source: provider.authFlowSource });
1757
+ sources.push({ field: "auth.flow", fileName: "auth-flow.ts", source: provider.authFlowSource });
1386
1758
  }
1387
1759
  for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
1388
- sources.push({ field: `sourceFiles.${filePath}`, source });
1760
+ sources.push({ field: `sourceFiles.${filePath}`, fileName: filePath, source });
1389
1761
  }
1390
1762
  for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1391
1763
  const source = getOperationSource(operation);
1392
1764
  if (source) {
1393
- sources.push({ field: `operations.${operationKey}.handler`, source });
1765
+ sources.push({
1766
+ field: `operations.${operationKey}.handler`,
1767
+ fileName: `${operationKey}.ts`,
1768
+ source,
1769
+ });
1394
1770
  }
1395
1771
  }
1396
- return sources.flatMap(({ field, source }) => {
1397
- const match = findLegacyChoiceUsage(source);
1772
+ return sources.flatMap(({ field, fileName, source }) => {
1773
+ const match = findLegacyChoiceUsage(fileName, source);
1398
1774
  if (!match)
1399
1775
  return [];
1400
1776
  return [
@@ -1506,9 +1882,15 @@ export function lintProviderWithInformation(provider, options = {}) {
1506
1882
  ...lintSelfHostedBrowserPatterns(provider, options),
1507
1883
  ...lintBrowserVersionLiterals(provider),
1508
1884
  ...lintUndeclaredThrownErrorCodes(provider),
1885
+ ...lintErrorCodeDeclarationConflicts(provider),
1886
+ ...lintErrorMessageLocalization(provider),
1509
1887
  ...lintLegacyChoiceUsage(provider),
1510
1888
  ...lintHandleDeclarations(provider),
1511
1889
  ];
1890
+ // Runtime boundary rules (process.env, node:fs/net/child_process, raw
1891
+ // fetch) also yield information entries for `@apifuse-allow` acknowledgements.
1892
+ const runtimeBoundary = lintRuntimeBoundary(getTypeScript(), provider, getOperationSource);
1893
+ diagnostics.push(...runtimeBoundary.diagnostics);
1512
1894
  if (provider.operations) {
1513
1895
  const authMode = provider.auth?.mode;
1514
1896
  for (const [operationKey, operation] of Object.entries(provider.operations)) {
@@ -1548,7 +1930,7 @@ export function lintProviderWithInformation(provider, options = {}) {
1548
1930
  }
1549
1931
  }
1550
1932
  if (!provider.operations) {
1551
- return applyPinnedWireFieldPaths(provider, diagnostics);
1933
+ return withInformation(applyPinnedWireFieldPaths(provider, diagnostics), runtimeBoundary.information);
1552
1934
  }
1553
1935
  diagnostics.push(...Object.entries(provider.operations).flatMap(([operationKey, operation]) => [
1554
1936
  ...lintOperation({
@@ -1567,5 +1949,10 @@ export function lintProviderWithInformation(provider, options = {}) {
1567
1949
  : `operations.${operationKey}`,
1568
1950
  message: `[${operationKey}] ${diagnostic.message}`,
1569
1951
  }))));
1570
- return applyPinnedWireFieldPaths(provider, diagnostics);
1952
+ return withInformation(applyPinnedWireFieldPaths(provider, diagnostics), runtimeBoundary.information);
1953
+ }
1954
+ function withInformation(result, information) {
1955
+ if (information.length === 0)
1956
+ return result;
1957
+ return { diagnostics: result.diagnostics, information: [...information, ...result.information] };
1571
1958
  }