@apifuse/provider-sdk 2.2.0-beta.61 → 2.2.0-beta.63

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 (258) hide show
  1. package/AUTHORING.md +268 -9
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +1 -0
  4. package/bin/apifuse-check.ts +84 -2
  5. package/bin/apifuse-dev.ts +11 -31
  6. package/bin/apifuse-migrate-deployment.ts +200 -0
  7. package/bin/apifuse-pack-check.ts +11 -4
  8. package/bin/apifuse-pack-types.ts +191 -4
  9. package/bin/apifuse-record.ts +44 -32
  10. package/bin/apifuse-test.ts +3 -1
  11. package/dist/cli/commands.d.ts +1 -1
  12. package/dist/cli/commands.js +11 -0
  13. package/dist/cli/deployment-intent.d.ts +178 -0
  14. package/dist/cli/deployment-intent.js +277 -0
  15. package/dist/cli/migrate-deployment-intent.d.ts +132 -0
  16. package/dist/cli/migrate-deployment-intent.js +1552 -0
  17. package/dist/cli/migrate-provider-shape.d.ts +5 -1
  18. package/dist/cli/migrate-provider-shape.js +4 -2
  19. package/dist/cli/resolver-runtime.d.ts +21 -0
  20. package/dist/cli/resolver-runtime.js +31 -0
  21. package/dist/define.d.ts +3 -0
  22. package/dist/define.js +5 -0
  23. package/dist/dev.d.ts +3 -1
  24. package/dist/dev.js +6 -1
  25. package/dist/engine.d.ts +35 -1
  26. package/dist/engine.js +21 -2
  27. package/dist/error-resolution.d.ts +1 -0
  28. package/dist/error-resolution.js +54 -0
  29. package/dist/errors.d.ts +39 -1
  30. package/dist/errors.js +50 -0
  31. package/dist/handle-meta.d.ts +18 -2
  32. package/dist/handle-meta.js +30 -2
  33. package/dist/handle.d.ts +33 -12
  34. package/dist/handle.js +69 -24
  35. package/dist/i18n/error-messages.d.ts +86 -0
  36. package/dist/i18n/error-messages.js +192 -0
  37. package/dist/i18n/index.d.ts +1 -0
  38. package/dist/i18n/index.js +1 -0
  39. package/dist/i18n/keys.d.ts +12 -0
  40. package/dist/i18n/keys.js +76 -5
  41. package/dist/index.d.ts +10 -2
  42. package/dist/index.js +9 -1
  43. package/dist/lint.d.ts +10 -0
  44. package/dist/lint.js +440 -57
  45. package/dist/provider.d.ts +7 -5
  46. package/dist/provider.js +4 -2
  47. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  48. package/dist/runtime/browser-telemetry-binding.js +83 -0
  49. package/dist/runtime/browser-telemetry.d.ts +76 -0
  50. package/dist/runtime/browser-telemetry.js +165 -0
  51. package/dist/runtime/browser.d.ts +3 -0
  52. package/dist/runtime/browser.js +208 -47
  53. package/dist/runtime/cache-telemetry.d.ts +71 -0
  54. package/dist/runtime/cache-telemetry.js +177 -0
  55. package/dist/runtime/cache.js +67 -7
  56. package/dist/runtime/capability-response.d.ts +3 -0
  57. package/dist/runtime/capability-response.js +56 -0
  58. package/dist/runtime/capability-telemetry.d.ts +46 -0
  59. package/dist/runtime/capability-telemetry.js +68 -0
  60. package/dist/runtime/diagnostic-env.d.ts +3 -0
  61. package/dist/runtime/diagnostic-env.js +10 -1
  62. package/dist/runtime/engine-mode.d.ts +74 -0
  63. package/dist/runtime/engine-mode.js +152 -0
  64. package/dist/runtime/handle.js +15 -10
  65. package/dist/runtime/http-telemetry-binding.d.ts +10 -0
  66. package/dist/runtime/http-telemetry-binding.js +152 -0
  67. package/dist/runtime/http-telemetry-guard.d.ts +16 -0
  68. package/dist/runtime/http-telemetry-guard.js +202 -0
  69. package/dist/runtime/http-telemetry.d.ts +104 -0
  70. package/dist/runtime/http-telemetry.js +190 -0
  71. package/dist/runtime/http.d.ts +31 -0
  72. package/dist/runtime/http.js +356 -174
  73. package/dist/runtime/instrumentation.js +92 -13
  74. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  75. package/dist/runtime/ocr-telemetry.js +192 -0
  76. package/dist/runtime/ocr.js +30 -8
  77. package/dist/runtime/request-telemetry.d.ts +2 -0
  78. package/dist/runtime/request-telemetry.js +2 -1
  79. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  80. package/dist/runtime/resolver-runtime-options.js +33 -0
  81. package/dist/runtime/resolver-shared.d.ts +3 -2
  82. package/dist/runtime/resolver-shared.js +4 -4
  83. package/dist/runtime/resolver-telemetry.d.ts +1 -0
  84. package/dist/runtime/resolver-telemetry.js +1 -1
  85. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  86. package/dist/runtime/resolver-vendors/browser.js +46 -0
  87. package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
  88. package/dist/runtime/resolver-vendors/hosts.js +35 -0
  89. package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
  90. package/dist/runtime/resolver-vendors/types.d.ts +5 -4
  91. package/dist/runtime/resolver.d.ts +4 -1
  92. package/dist/runtime/resolver.js +46 -13
  93. package/dist/runtime/state-telemetry.d.ts +79 -0
  94. package/dist/runtime/state-telemetry.js +194 -0
  95. package/dist/runtime/state.d.ts +3 -0
  96. package/dist/runtime/state.js +26 -7
  97. package/dist/runtime/stealth-cookies.d.ts +5 -1
  98. package/dist/runtime/stealth-cookies.js +21 -1
  99. package/dist/runtime/stealth-owned-headers.d.ts +26 -0
  100. package/dist/runtime/stealth-owned-headers.js +37 -0
  101. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  102. package/dist/runtime/stealth-telemetry.js +179 -0
  103. package/dist/runtime/stealth.d.ts +4 -1
  104. package/dist/runtime/stealth.js +228 -85
  105. package/dist/runtime/stt-telemetry.d.ts +77 -0
  106. package/dist/runtime/stt-telemetry.js +176 -0
  107. package/dist/runtime/stt.js +15 -6
  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 +9 -0
  111. package/dist/server/index.js +5 -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 +53 -2
  118. package/dist/server/serve-implementation.js +369 -123
  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 +94 -1
  125. package/package.json +6 -1
  126. package/src/cli/commands.ts +14 -0
  127. package/src/cli/deployment-intent.ts +364 -0
  128. package/src/cli/migrate-deployment-intent.ts +1908 -0
  129. package/src/cli/migrate-provider-shape.ts +10 -4
  130. package/src/cli/resolver-runtime.ts +72 -0
  131. package/src/define.ts +13 -0
  132. package/src/dev.ts +9 -2
  133. package/src/engine.ts +38 -2
  134. package/src/error-resolution.ts +58 -0
  135. package/src/errors.ts +77 -1
  136. package/src/handle-meta.ts +39 -4
  137. package/src/handle.ts +121 -38
  138. package/src/i18n/error-messages.ts +255 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/keys.ts +80 -6
  141. package/src/index.ts +91 -0
  142. package/src/lint.ts +552 -57
  143. package/src/provider.ts +20 -0
  144. package/src/runtime/browser-telemetry-binding.ts +92 -0
  145. package/src/runtime/browser-telemetry.ts +289 -0
  146. package/src/runtime/browser.ts +249 -65
  147. package/src/runtime/cache-telemetry.ts +247 -0
  148. package/src/runtime/cache.ts +68 -17
  149. package/src/runtime/capability-response.ts +53 -0
  150. package/src/runtime/capability-telemetry.ts +101 -0
  151. package/src/runtime/diagnostic-env.ts +11 -1
  152. package/src/runtime/engine-mode.ts +233 -0
  153. package/src/runtime/handle.ts +20 -10
  154. package/src/runtime/http-telemetry-binding.ts +162 -0
  155. package/src/runtime/http-telemetry-guard.ts +208 -0
  156. package/src/runtime/http-telemetry.ts +292 -0
  157. package/src/runtime/http.ts +457 -204
  158. package/src/runtime/instrumentation.ts +105 -16
  159. package/src/runtime/ocr-telemetry.ts +308 -0
  160. package/src/runtime/ocr.ts +109 -74
  161. package/src/runtime/request-telemetry.ts +2 -1
  162. package/src/runtime/resolver-runtime-options.ts +50 -0
  163. package/src/runtime/resolver-shared.ts +9 -4
  164. package/src/runtime/resolver-telemetry.ts +1 -1
  165. package/src/runtime/resolver-vendors/browser.ts +65 -7
  166. package/src/runtime/resolver-vendors/hosts.ts +43 -0
  167. package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
  168. package/src/runtime/resolver-vendors/types.ts +5 -4
  169. package/src/runtime/resolver.ts +64 -21
  170. package/src/runtime/state-telemetry.ts +298 -0
  171. package/src/runtime/state.ts +46 -45
  172. package/src/runtime/stealth-cookies.ts +24 -1
  173. package/src/runtime/stealth-owned-headers.ts +41 -0
  174. package/src/runtime/stealth-telemetry.ts +304 -0
  175. package/src/runtime/stealth.ts +355 -150
  176. package/src/runtime/stt-telemetry.ts +282 -0
  177. package/src/runtime/stt.ts +93 -71
  178. package/src/server/error-stack-frames.ts +214 -0
  179. package/src/server/index.ts +66 -0
  180. package/src/server/sdk-version.ts +22 -0
  181. package/src/server/self-test.ts +1 -21
  182. package/src/server/sensitive-values.ts +14 -0
  183. package/src/server/serve-implementation.ts +601 -157
  184. package/src/server/types.ts +7 -0
  185. package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
  186. package/src/stateful/stateful-provider-runtime-context.ts +4 -0
  187. package/src/types.ts +99 -1
  188. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
  189. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
  190. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
  191. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
  192. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
  193. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
  194. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
  195. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
  196. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
  197. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
  198. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
  199. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
  200. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
  201. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
  202. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
  203. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
  204. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
  205. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
  206. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
  207. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
  208. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
  209. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
  210. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
  211. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
  212. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
  213. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
  214. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
  215. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
  216. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
  217. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
  218. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
  219. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
  220. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
  221. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
  222. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
  246. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
  247. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
  248. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
  249. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
  250. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
  251. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
  252. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
  253. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
  254. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
  255. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
  256. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
  257. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
  258. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
package/dist/lint.js CHANGED
@@ -1,7 +1,10 @@
1
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, isHandleFieldMeta, } from "./handle-meta.js";
2
+ import { SDK_CANONICAL_ERROR_CODE_RETRYABILITY, 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";
4
+ import { PROVIDER_ERROR_CATALOG_NAMESPACE } from "./i18n/error-messages.js";
5
+ import { getProviderLocaleSegments, isProviderLocaleKey, } from "./i18n/keys.js";
4
6
  import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
7
+ import { isStealthOwnedHeaderName, SDK_OWNED_CHROME_HEADER_PREFIX, } from "./runtime/stealth-owned-headers.js";
5
8
  import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
6
9
  const requireModule = createRequire(import.meta.url);
7
10
  // `typeof import(...)` keeps the type without emitting a static import: the
@@ -750,7 +753,14 @@ function lintSelfHostedBrowserPatterns(provider, options) {
750
753
  }
751
754
  return diagnostics;
752
755
  }
753
- const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(?:ProviderError|ValidationError)\s*\(/g;
756
+ // Captures the constructor name so each rule can pick its own scope: the
757
+ // undeclared-code rule keeps its historical ProviderError/ValidationError
758
+ // surface, while the localization rules also cover AuthError.
759
+ const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(ProviderError|AuthError|ValidationError)\s*\(/g;
760
+ const UNDECLARED_CODE_ERROR_CLASSES = new Set([
761
+ "ProviderError",
762
+ "ValidationError",
763
+ ]);
754
764
  const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
755
765
  const RECORDED_FIXTURE_SOURCE_FILE_PATTERN = /(?:^|\/)__fixtures__(?:\/|$)|(?:^|\/)__tests__\/fixtures(?:\/|$)/;
756
766
  const JAVASCRIPT_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
@@ -758,6 +768,11 @@ const VERSIONED_PROFILE_LITERAL_PATTERN = /\b(?:chrome|chromium|firefox|safari|e
758
768
  const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
759
769
  const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
760
770
  const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
771
+ // Calls that write a header entry as (name, value): Headers/Map `set` and
772
+ // `append`, Node's `setHeader`. Predicates, replacements, and logging calls
773
+ // take string arguments too and must not count as header writes.
774
+ const HEADER_SETTER_CALLEE_PATTERN = /^(?:set|append|setHeader|addHeader)$/;
775
+ const OWNED_CLIENT_HINT_HEADER_PREFIX = "sec-ch-ua";
761
776
  function staticStringText(node) {
762
777
  if (!node)
763
778
  return undefined;
@@ -778,11 +793,83 @@ function staticPropertyName(node) {
778
793
  function isSecChUaHeaderName(value) {
779
794
  return value?.toLowerCase() === "sec-ch-ua";
780
795
  }
796
+ /**
797
+ * True for the code paths that reach the stealth transport: `ctx.stealth`,
798
+ * `ctx["stealth"]`, and `const { stealth } = ctx`. Read from the AST so a
799
+ * comment or string that mentions ctx.stealth does not scope a ctx.http file
800
+ * into this rule.
801
+ */
802
+ function isStealthContextAccess(node) {
803
+ const ts = getTypeScript();
804
+ if (ts.isPropertyAccessExpression(node)) {
805
+ return (ts.isIdentifier(node.expression) &&
806
+ node.expression.text === "ctx" &&
807
+ node.name.text === "stealth");
808
+ }
809
+ if (ts.isElementAccessExpression(node)) {
810
+ return (ts.isIdentifier(node.expression) &&
811
+ node.expression.text === "ctx" &&
812
+ staticStringText(node.argumentExpression) === "stealth");
813
+ }
814
+ if (ts.isBindingElement(node)) {
815
+ const key = node.propertyName ?? node.name;
816
+ if (!ts.isIdentifier(key) || key.text !== "stealth")
817
+ return false;
818
+ const declaration = node.parent?.parent;
819
+ return (declaration !== undefined &&
820
+ ts.isVariableDeclaration(declaration) &&
821
+ declaration.initializer !== undefined &&
822
+ ts.isIdentifier(declaration.initializer) &&
823
+ declaration.initializer.text === "ctx");
824
+ }
825
+ return false;
826
+ }
827
+ function isStaticUndefined(node) {
828
+ if (!node)
829
+ return false;
830
+ const ts = getTypeScript();
831
+ return (ts.isIdentifier(node) && node.text === "undefined") || ts.isVoidExpression(node);
832
+ }
833
+ function calleeName(node) {
834
+ const ts = getTypeScript();
835
+ if (ts.isPropertyAccessExpression(node.expression))
836
+ return node.expression.name.text;
837
+ if (ts.isIdentifier(node.expression))
838
+ return node.expression.text;
839
+ return undefined;
840
+ }
841
+ /**
842
+ * Header names this rule reports in ctx.stealth files: the Sec-Fetch-* and
843
+ * client-hint (sec-ch-ua*) families, restricted to what the stealth runtime
844
+ * really rejects (isStealthOwnedHeaderName). Deliberately narrower than the
845
+ * runtime rejection: host, connection, and accept-encoding are left alone
846
+ * because ctx.http callers set them legitimately in the same files, and
847
+ * user-agent only reaches a finding through the "user-agent" kind, which
848
+ * matches a versioned literal value rather than the name — a user-agent read
849
+ * from a variable is rejected by the runtime and stays silent here. Widening
850
+ * the set needs a per-file transport scope and a suppression path first.
851
+ */
852
+ function isReportedOwnedHeaderName(value) {
853
+ if (value === undefined)
854
+ return false;
855
+ const name = value.toLowerCase();
856
+ if (!isStealthOwnedHeaderName(name))
857
+ return false;
858
+ if (name.startsWith(SDK_OWNED_CHROME_HEADER_PREFIX)) {
859
+ return name.length > SDK_OWNED_CHROME_HEADER_PREFIX.length;
860
+ }
861
+ return name.startsWith(OWNED_CLIENT_HINT_HEADER_PREFIX);
862
+ }
781
863
  function collectBrowserVersionLiteralFindings(source) {
782
864
  const ts = getTypeScript();
783
865
  const sourceFile = ts.createSourceFile("provider-source.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
784
- const findings = [];
866
+ let findings = [];
785
867
  const seen = new Set();
868
+ // Owned-header entries only count once the file is known to reach
869
+ // ctx.stealth, which may appear after the entry; collect them and decide
870
+ // after the walk.
871
+ let stealthAccess = false;
872
+ const ownedHeaderEntries = [];
786
873
  const addFinding = (kind, literal, position) => {
787
874
  const key = `${kind}:${position}:${literal}`;
788
875
  if (seen.has(key))
@@ -805,23 +892,73 @@ function collectBrowserVersionLiteralFindings(source) {
805
892
  addFinding("sec-ch-ua", "sec-ch-ua", node.getStart(sourceFile));
806
893
  }
807
894
  };
895
+ // A header entry `name: value` in any of the shapes below. In a ctx.stealth
896
+ // file an owned name is the finding (the runtime rejects it whatever the
897
+ // value); elsewhere only a versioned sec-ch-ua value is. A statically
898
+ // undefined value is exempt only in the "record" shapes, the ones the
899
+ // runtime drops before the ownership check (`normalizedHeaderEntries`):
900
+ // `Headers.set(name, undefined)` stringifies to "undefined" and still
901
+ // throws, so it stays a finding.
902
+ const inspectHeaderEntry = (nameNode, name, valueNode, ownedHeaderShape) => {
903
+ if (name === undefined)
904
+ return;
905
+ if (ownedHeaderShape !== false &&
906
+ isReportedOwnedHeaderName(name) &&
907
+ !(ownedHeaderShape === "record" && isStaticUndefined(valueNode))) {
908
+ ownedHeaderEntries.push({
909
+ name,
910
+ position: nameNode.getStart(sourceFile),
911
+ valuePosition: valueNode?.getStart(sourceFile),
912
+ });
913
+ }
914
+ if (isSecChUaHeaderName(name))
915
+ inspectSecChUaValue(valueNode);
916
+ };
808
917
  const visit = (node) => {
809
918
  const text = staticStringText(node);
810
919
  if (text !== undefined)
811
920
  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]);
921
+ if (isStealthContextAccess(node))
922
+ stealthAccess = true;
923
+ if (ts.isPropertyAssignment(node)) {
924
+ inspectHeaderEntry(node.name, staticPropertyName(node.name), node.initializer, "record");
925
+ }
926
+ if (ts.isCallExpression(node) && node.arguments[0]) {
927
+ // Only `headers.set(name, value)`-style calls write a header;
928
+ // `name.startsWith("sec-fetch-")` or `log("sec-fetch-dest", x)` do not.
929
+ inspectHeaderEntry(node.arguments[0], staticStringText(node.arguments[0]), node.arguments[1], node.arguments.length >= 2 && HEADER_SETTER_CALLEE_PATTERN.test(calleeName(node) ?? "")
930
+ ? "entry"
931
+ : false);
932
+ }
933
+ if (ts.isArrayLiteralExpression(node) && node.elements[0]) {
934
+ // Only a `[name, value]` tuple is a header. A name list such as
935
+ // `new Set(["sec-ch-ua-mobile", "sec-ch-ua-platform"])` is not: it is
936
+ // the argument of a constructor, or its second element is itself an
937
+ // owned name.
938
+ const second = node.elements[1];
939
+ inspectHeaderEntry(node.elements[0], staticStringText(node.elements[0]), second, node.elements.length === 2 &&
940
+ !ts.isNewExpression(node.parent) &&
941
+ !isStealthOwnedHeaderName(staticStringText(second) ?? "")
942
+ ? "entry"
943
+ : false);
944
+ }
945
+ if (ts.isBinaryExpression(node) &&
946
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
947
+ ts.isElementAccessExpression(node.left)) {
948
+ const nameNode = node.left.argumentExpression;
949
+ inspectHeaderEntry(nameNode, staticStringText(nameNode), node.right, "record");
821
950
  }
822
951
  ts.forEachChild(node, visit);
823
952
  };
824
953
  visit(sourceFile);
954
+ if (stealthAccess) {
955
+ // The owned name is the finding; do not also report its sec-ch-ua value.
956
+ const shadowedValues = new Set(ownedHeaderEntries.map((entry) => entry.valuePosition));
957
+ findings = findings.filter((finding) => !(finding.kind === "sec-ch-ua" && shadowedValues.has(finding.position)));
958
+ for (const entry of ownedHeaderEntries) {
959
+ addFinding("owned-header", entry.name, entry.position);
960
+ }
961
+ }
825
962
  return findings.sort((left, right) => left.position - right.position);
826
963
  }
827
964
  function browserVersionLiteralMessage(finding) {
@@ -832,6 +969,8 @@ function browserVersionLiteralMessage(finding) {
832
969
  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
970
  case "sec-ch-ua":
834
971
  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.';
972
+ case "owned-header":
973
+ 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
974
  }
836
975
  }
837
976
  function lintBrowserVersionLiterals(provider) {
@@ -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,14 +1565,15 @@ function readHandleKindDeclarations(value) {
1204
1565
  return (typeof record.name === "string" &&
1205
1566
  (record.type === "cursor" || record.type === "draft") &&
1206
1567
  typeof record.fieldName === "string" &&
1207
- (record.issuedBy === undefined || typeof record.issuedBy === "string"));
1568
+ (record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy)));
1208
1569
  });
1209
1570
  }
1210
1571
  /**
1211
1572
  * ADR-0012 handle rules: every `x-apifuse-handle` schema field must belong to
1212
1573
  * a declared kind under its declared property key; every declared kind must be
1213
- * both issued (some output) and accepted (some input); `issuedBy` must name an
1214
- * operation whose output carries the field; and `ctx.handle` needs `state`.
1574
+ * both issued (some output) and accepted (some input); every `issuedBy` entry
1575
+ * must be an operation key and at least one of them must output the field; and
1576
+ * `ctx.handle` needs `state`.
1215
1577
  */
1216
1578
  function lintHandleDeclarations(provider) {
1217
1579
  const diagnostics = [];
@@ -1292,20 +1654,39 @@ function lintHandleDeclarations(provider) {
1292
1654
  }
1293
1655
  if (kind.issuedBy === undefined)
1294
1656
  continue;
1295
- if (!Object.hasOwn(operations, kind.issuedBy)) {
1657
+ const issuers = handleIssuerList(kind.issuedBy);
1658
+ const known = issuers.filter((operationKey) => Object.hasOwn(operations, operationKey));
1659
+ for (const operationKey of issuers) {
1660
+ if (known.includes(operationKey))
1661
+ continue;
1296
1662
  diagnostics.push({
1297
1663
  rule: "handle-issued-by",
1298
1664
  level: "error",
1299
1665
  field,
1300
- message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${kind.issuedBy}", which is not an operation key.`,
1666
+ message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${operationKey}", which is not an operation key.`,
1301
1667
  });
1302
1668
  }
1303
- else if (!outputKindsByOperation.get(kind.issuedBy)?.has(kind.name)) {
1669
+ if (known.length === 0)
1670
+ continue;
1671
+ const issuing = known.filter((operationKey) => outputKindsByOperation.get(operationKey)?.has(kind.name));
1672
+ if (issuing.length > 0)
1673
+ continue;
1674
+ if (known.length === 1) {
1675
+ diagnostics.push({
1676
+ rule: "handle-issued-by",
1677
+ level: "error",
1678
+ field,
1679
+ message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind.fieldName}" handle field.`,
1680
+ });
1681
+ }
1682
+ else {
1304
1683
  diagnostics.push({
1305
1684
  rule: "handle-issued-by",
1306
1685
  level: "error",
1307
1686
  field,
1308
- message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${kind.issuedBy}", but that operation's output has no "${kind.fieldName}" handle field.`,
1687
+ message: `${providerLabel} handle kind "${kind.name}" declares issuedBy ${known
1688
+ .map((operationKey) => `"${operationKey}"`)
1689
+ .join(", ")}, but none of those operations' outputs has a "${kind.fieldName}" handle field. At least one listed issuer must output it.`,
1309
1690
  });
1310
1691
  }
1311
1692
  }
@@ -1486,6 +1867,8 @@ export function lintProviderWithInformation(provider, options = {}) {
1486
1867
  ...lintSelfHostedBrowserPatterns(provider, options),
1487
1868
  ...lintBrowserVersionLiterals(provider),
1488
1869
  ...lintUndeclaredThrownErrorCodes(provider),
1870
+ ...lintErrorCodeDeclarationConflicts(provider),
1871
+ ...lintErrorMessageLocalization(provider),
1489
1872
  ...lintLegacyChoiceUsage(provider),
1490
1873
  ...lintHandleDeclarations(provider),
1491
1874
  ];