@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/src/lint.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { createRequire } from "node:module";
2
-
3
1
  import type { ZodType } from "zod";
4
2
 
5
3
  import {
4
+ SDK_CANONICAL_ERROR_CODE_RETRYABILITY,
6
5
  SDK_RUNTIME_OWNED_ERROR_CODES,
7
6
  SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES,
8
7
  } from "./error-resolution.js";
@@ -11,24 +10,27 @@ import {
11
10
  type HandleFieldMeta,
12
11
  type HandleKindDeclaration,
13
12
  handleFieldDescription,
13
+ handleFieldNameFor,
14
+ handleHasDirectionalFieldNames,
14
15
  handleIssuerList,
15
- isHandleIssuedBy,
16
16
  isHandleFieldMeta,
17
+ isHandleIssuedBy,
17
18
  } from "./handle-meta.js";
19
+ import { PROVIDER_ERROR_CATALOG_NAMESPACE } from "./i18n/error-messages.js";
20
+ import {
21
+ getProviderLocaleSegments,
22
+ isProviderLocaleKey,
23
+ type ProviderLocaleCatalog,
24
+ } from "./i18n/keys.js";
18
25
  import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
26
+ import {
27
+ isStealthOwnedHeaderName,
28
+ SDK_OWNED_CHROME_HEADER_PREFIX,
29
+ } from "./runtime/stealth-owned-headers.js";
30
+ import { lintRuntimeBoundary, scriptKindForSourceFile } from "./runtime-boundary-lint.js";
19
31
  import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
20
32
  import type { AuthMode, OperationApprovalPolicy, OperationRiskClass } from "./types.js";
21
-
22
- const requireModule = createRequire(import.meta.url);
23
- // `typeof import(...)` keeps the type without emitting a static import: the
24
- // typescript package is a CLI-only dependency and src/lint.ts is production
25
- // runtime, which the typescript-import-boundary test enforces.
26
- let typeScriptModule: typeof import("typescript") | undefined;
27
-
28
- function getTypeScript(): typeof import("typescript") {
29
- typeScriptModule ??= requireModule("typescript") as typeof import("typescript");
30
- return typeScriptModule;
31
- }
33
+ import { getTypeScript } from "./typescript-module.js";
32
34
 
33
35
  type AuthModeLike =
34
36
  | "none"
@@ -191,12 +193,17 @@ type ProviderLintOptions = {
191
193
  mode?: ProviderLintMode;
192
194
  };
193
195
 
196
+ /**
197
+ * A non-blocking audit line: a finding the author acknowledged with
198
+ * `// @apifuse-allow <rule>: <reason>`, kept visible in `apifuse check` output.
199
+ */
194
200
  export interface ProviderLintInformation {
195
201
  rule: string;
196
202
  message: string;
197
203
  field?: string;
198
204
  }
199
205
 
206
+ /** Diagnostics plus the acknowledged findings that accompany them. */
200
207
  export interface ProviderLintResult {
201
208
  diagnostics: LintDiagnostic[];
202
209
  information: ProviderLintInformation[];
@@ -1014,7 +1021,14 @@ function lintSelfHostedBrowserPatterns(
1014
1021
  return diagnostics;
1015
1022
  }
1016
1023
 
1017
- const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(?:ProviderError|ValidationError)\s*\(/g;
1024
+ // Captures the constructor name so each rule can pick its own scope: the
1025
+ // undeclared-code rule keeps its historical ProviderError/ValidationError
1026
+ // surface, while the localization rules also cover AuthError.
1027
+ const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(ProviderError|AuthError|ValidationError)\s*\(/g;
1028
+ const UNDECLARED_CODE_ERROR_CLASSES: ReadonlySet<string> = new Set([
1029
+ "ProviderError",
1030
+ "ValidationError",
1031
+ ]);
1018
1032
 
1019
1033
  const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
1020
1034
  const RECORDED_FIXTURE_SOURCE_FILE_PATTERN =
@@ -1025,8 +1039,13 @@ const VERSIONED_PROFILE_LITERAL_PATTERN =
1025
1039
  const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
1026
1040
  const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
1027
1041
  const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
1042
+ // Calls that write a header entry as (name, value): Headers/Map `set` and
1043
+ // `append`, Node's `setHeader`. Predicates, replacements, and logging calls
1044
+ // take string arguments too and must not count as header writes.
1045
+ const HEADER_SETTER_CALLEE_PATTERN = /^(?:set|append|setHeader|addHeader)$/;
1046
+ const OWNED_CLIENT_HINT_HEADER_PREFIX = "sec-ch-ua";
1028
1047
 
1029
- type BrowserVersionLiteralKind = "profile" | "user-agent" | "sec-ch-ua";
1048
+ type BrowserVersionLiteralKind = "profile" | "user-agent" | "sec-ch-ua" | "owned-header";
1030
1049
 
1031
1050
  type BrowserVersionLiteralFinding = {
1032
1051
  kind: BrowserVersionLiteralKind;
@@ -1054,17 +1073,96 @@ function isSecChUaHeaderName(value: string | undefined): boolean {
1054
1073
  return value?.toLowerCase() === "sec-ch-ua";
1055
1074
  }
1056
1075
 
1057
- function collectBrowserVersionLiteralFindings(source: string): BrowserVersionLiteralFinding[] {
1076
+ /**
1077
+ * True for the code paths that reach the stealth transport: `ctx.stealth`,
1078
+ * `ctx["stealth"]`, and `const { stealth } = ctx`. Read from the AST so a
1079
+ * comment or string that mentions ctx.stealth does not scope a ctx.http file
1080
+ * into this rule.
1081
+ */
1082
+ function isStealthContextAccess(node: import("typescript").Node): boolean {
1083
+ const ts = getTypeScript();
1084
+ if (ts.isPropertyAccessExpression(node)) {
1085
+ return (
1086
+ ts.isIdentifier(node.expression) &&
1087
+ node.expression.text === "ctx" &&
1088
+ node.name.text === "stealth"
1089
+ );
1090
+ }
1091
+ if (ts.isElementAccessExpression(node)) {
1092
+ return (
1093
+ ts.isIdentifier(node.expression) &&
1094
+ node.expression.text === "ctx" &&
1095
+ staticStringText(node.argumentExpression) === "stealth"
1096
+ );
1097
+ }
1098
+ if (ts.isBindingElement(node)) {
1099
+ const key = node.propertyName ?? node.name;
1100
+ if (!ts.isIdentifier(key) || key.text !== "stealth") return false;
1101
+ const declaration = node.parent?.parent;
1102
+ return (
1103
+ declaration !== undefined &&
1104
+ ts.isVariableDeclaration(declaration) &&
1105
+ declaration.initializer !== undefined &&
1106
+ ts.isIdentifier(declaration.initializer) &&
1107
+ declaration.initializer.text === "ctx"
1108
+ );
1109
+ }
1110
+ return false;
1111
+ }
1112
+
1113
+ function isStaticUndefined(node: import("typescript").Node | undefined): boolean {
1114
+ if (!node) return false;
1115
+ const ts = getTypeScript();
1116
+ return (ts.isIdentifier(node) && node.text === "undefined") || ts.isVoidExpression(node);
1117
+ }
1118
+
1119
+ function calleeName(node: import("typescript").CallExpression): string | undefined {
1120
+ const ts = getTypeScript();
1121
+ if (ts.isPropertyAccessExpression(node.expression)) return node.expression.name.text;
1122
+ if (ts.isIdentifier(node.expression)) return node.expression.text;
1123
+ return undefined;
1124
+ }
1125
+
1126
+ /**
1127
+ * Header names this rule reports in ctx.stealth files: the Sec-Fetch-* and
1128
+ * client-hint (sec-ch-ua*) families, restricted to what the stealth runtime
1129
+ * really rejects (isStealthOwnedHeaderName). Deliberately narrower than the
1130
+ * runtime rejection: host, connection, and accept-encoding are left alone
1131
+ * because ctx.http callers set them legitimately in the same files, and
1132
+ * user-agent only reaches a finding through the "user-agent" kind, which
1133
+ * matches a versioned literal value rather than the name — a user-agent read
1134
+ * from a variable is rejected by the runtime and stays silent here. Widening
1135
+ * the set needs a per-file transport scope and a suppression path first.
1136
+ */
1137
+ function isReportedOwnedHeaderName(value: string | undefined): boolean {
1138
+ if (value === undefined) return false;
1139
+ const name = value.toLowerCase();
1140
+ if (!isStealthOwnedHeaderName(name)) return false;
1141
+ if (name.startsWith(SDK_OWNED_CHROME_HEADER_PREFIX)) {
1142
+ return name.length > SDK_OWNED_CHROME_HEADER_PREFIX.length;
1143
+ }
1144
+ return name.startsWith(OWNED_CLIENT_HINT_HEADER_PREFIX);
1145
+ }
1146
+
1147
+ function collectBrowserVersionLiteralFindings(
1148
+ fileName: string,
1149
+ source: string,
1150
+ ): BrowserVersionLiteralFinding[] {
1058
1151
  const ts = getTypeScript();
1059
1152
  const sourceFile = ts.createSourceFile(
1060
- "provider-source.ts",
1153
+ fileName,
1061
1154
  source,
1062
1155
  ts.ScriptTarget.Latest,
1063
1156
  true,
1064
- ts.ScriptKind.TSX,
1157
+ scriptKindForSourceFile(ts, fileName),
1065
1158
  );
1066
- const findings: BrowserVersionLiteralFinding[] = [];
1159
+ let findings: BrowserVersionLiteralFinding[] = [];
1067
1160
  const seen = new Set<string>();
1161
+ // Owned-header entries only count once the file is known to reach
1162
+ // ctx.stealth, which may appear after the entry; collect them and decide
1163
+ // after the walk.
1164
+ let stealthAccess = false;
1165
+ const ownedHeaderEntries: Array<{ name: string; position: number; valuePosition?: number }> = [];
1068
1166
 
1069
1167
  const addFinding = (kind: BrowserVersionLiteralKind, literal: string, position: number) => {
1070
1168
  const key = `${kind}:${position}:${literal}`;
@@ -1090,29 +1188,97 @@ function collectBrowserVersionLiteralFindings(source: string): BrowserVersionLit
1090
1188
  }
1091
1189
  };
1092
1190
 
1191
+ // A header entry `name: value` in any of the shapes below. In a ctx.stealth
1192
+ // file an owned name is the finding (the runtime rejects it whatever the
1193
+ // value); elsewhere only a versioned sec-ch-ua value is. A statically
1194
+ // undefined value is exempt only in the "record" shapes, the ones the
1195
+ // runtime drops before the ownership check (`normalizedHeaderEntries`):
1196
+ // `Headers.set(name, undefined)` stringifies to "undefined" and still
1197
+ // throws, so it stays a finding.
1198
+ const inspectHeaderEntry = (
1199
+ nameNode: import("typescript").Node,
1200
+ name: string | undefined,
1201
+ valueNode: import("typescript").Node | undefined,
1202
+ ownedHeaderShape: false | "record" | "entry",
1203
+ ) => {
1204
+ if (name === undefined) return;
1205
+ if (
1206
+ ownedHeaderShape !== false &&
1207
+ isReportedOwnedHeaderName(name) &&
1208
+ !(ownedHeaderShape === "record" && isStaticUndefined(valueNode))
1209
+ ) {
1210
+ ownedHeaderEntries.push({
1211
+ name,
1212
+ position: nameNode.getStart(sourceFile),
1213
+ valuePosition: valueNode?.getStart(sourceFile),
1214
+ });
1215
+ }
1216
+ if (isSecChUaHeaderName(name)) inspectSecChUaValue(valueNode);
1217
+ };
1218
+
1093
1219
  const visit = (node: import("typescript").Node) => {
1094
1220
  const text = staticStringText(node);
1095
1221
  if (text !== undefined) inspectLiteral(text, node.getStart(sourceFile));
1222
+ if (isStealthContextAccess(node)) stealthAccess = true;
1096
1223
 
1097
- if (ts.isPropertyAssignment(node) && isSecChUaHeaderName(staticPropertyName(node.name))) {
1098
- inspectSecChUaValue(node.initializer);
1224
+ if (ts.isPropertyAssignment(node)) {
1225
+ inspectHeaderEntry(node.name, staticPropertyName(node.name), node.initializer, "record");
1099
1226
  }
1100
1227
 
1101
- if (ts.isCallExpression(node) && isSecChUaHeaderName(staticStringText(node.arguments[0]))) {
1102
- inspectSecChUaValue(node.arguments[1]);
1228
+ if (ts.isCallExpression(node) && node.arguments[0]) {
1229
+ // Only `headers.set(name, value)`-style calls write a header;
1230
+ // `name.startsWith("sec-fetch-")` or `log("sec-fetch-dest", x)` do not.
1231
+ inspectHeaderEntry(
1232
+ node.arguments[0],
1233
+ staticStringText(node.arguments[0]),
1234
+ node.arguments[1],
1235
+ node.arguments.length >= 2 && HEADER_SETTER_CALLEE_PATTERN.test(calleeName(node) ?? "")
1236
+ ? "entry"
1237
+ : false,
1238
+ );
1239
+ }
1240
+
1241
+ if (ts.isArrayLiteralExpression(node) && node.elements[0]) {
1242
+ // Only a `[name, value]` tuple is a header. A name list such as
1243
+ // `new Set(["sec-ch-ua-mobile", "sec-ch-ua-platform"])` is not: it is
1244
+ // the argument of a constructor, or its second element is itself an
1245
+ // owned name.
1246
+ const second = node.elements[1];
1247
+ inspectHeaderEntry(
1248
+ node.elements[0],
1249
+ staticStringText(node.elements[0]),
1250
+ second,
1251
+ node.elements.length === 2 &&
1252
+ !ts.isNewExpression(node.parent) &&
1253
+ !isStealthOwnedHeaderName(staticStringText(second) ?? "")
1254
+ ? "entry"
1255
+ : false,
1256
+ );
1103
1257
  }
1104
1258
 
1105
1259
  if (
1106
- ts.isArrayLiteralExpression(node) &&
1107
- isSecChUaHeaderName(staticStringText(node.elements[0]))
1260
+ ts.isBinaryExpression(node) &&
1261
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
1262
+ ts.isElementAccessExpression(node.left)
1108
1263
  ) {
1109
- inspectSecChUaValue(node.elements[1]);
1264
+ const nameNode = node.left.argumentExpression;
1265
+ inspectHeaderEntry(nameNode, staticStringText(nameNode), node.right, "record");
1110
1266
  }
1111
1267
 
1112
1268
  ts.forEachChild(node, visit);
1113
1269
  };
1114
1270
 
1115
1271
  visit(sourceFile);
1272
+ if (stealthAccess) {
1273
+ // The owned name is the finding; do not also report its sec-ch-ua value.
1274
+ const shadowedValues = new Set(ownedHeaderEntries.map((entry) => entry.valuePosition));
1275
+ findings = findings.filter(
1276
+ (finding) => !(finding.kind === "sec-ch-ua" && shadowedValues.has(finding.position)),
1277
+ );
1278
+ for (const entry of ownedHeaderEntries) {
1279
+ addFinding("owned-header", entry.name, entry.position);
1280
+ }
1281
+ }
1116
1282
  return findings.sort((left, right) => left.position - right.position);
1117
1283
  }
1118
1284
 
@@ -1124,11 +1290,13 @@ function browserVersionLiteralMessage(finding: BrowserVersionLiteralFinding): st
1124
1290
  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.`;
1125
1291
  case "sec-ch-ua":
1126
1292
  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.';
1293
+ case "owned-header":
1294
+ 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.`;
1127
1295
  }
1128
1296
  }
1129
1297
 
1130
1298
  function lintBrowserVersionLiterals(provider: ProviderSourceLike): LintDiagnostic[] {
1131
- const sources: Array<{ field: string; source: string }> = [];
1299
+ const sources: Array<{ field: string; fileName: string; source: string }> = [];
1132
1300
  const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
1133
1301
  ([filePath]) =>
1134
1302
  JAVASCRIPT_SOURCE_FILE_PATTERN.test(filePath) &&
@@ -1137,19 +1305,28 @@ function lintBrowserVersionLiterals(provider: ProviderSourceLike): LintDiagnosti
1137
1305
  );
1138
1306
  if (sourceFiles.length > 0) {
1139
1307
  for (const [filePath, source] of sourceFiles) {
1140
- sources.push({ field: `sourceFiles.${filePath}`, source });
1308
+ sources.push({ field: `sourceFiles.${filePath}`, fileName: filePath, source });
1141
1309
  }
1142
1310
  } else {
1143
1311
  if (provider.authFlowSource)
1144
- sources.push({ field: "auth.flow", source: provider.authFlowSource });
1312
+ sources.push({
1313
+ field: "auth.flow",
1314
+ fileName: "auth-flow.ts",
1315
+ source: provider.authFlowSource,
1316
+ });
1145
1317
  for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1146
1318
  const source = getOperationSource(operation);
1147
- if (source) sources.push({ field: `operations.${operationKey}.handler`, source });
1319
+ if (source)
1320
+ sources.push({
1321
+ field: `operations.${operationKey}.handler`,
1322
+ fileName: `${operationKey}.ts`,
1323
+ source,
1324
+ });
1148
1325
  }
1149
1326
  }
1150
1327
 
1151
- return sources.flatMap(({ field, source }) =>
1152
- collectBrowserVersionLiteralFindings(source).map((finding) => ({
1328
+ return sources.flatMap(({ field, fileName, source }) =>
1329
+ collectBrowserVersionLiteralFindings(fileName, source).map((finding) => ({
1153
1330
  rule: "browser-version-literal",
1154
1331
  level: "error" as const,
1155
1332
  field,
@@ -1258,14 +1435,17 @@ function extractBalancedCallArguments(source: string, startIndex: number): strin
1258
1435
  }
1259
1436
 
1260
1437
  /**
1261
- * Collects literal string values of top-level `code:` properties inside a
1262
- * ProviderError/ValidationError options object. Only plain `"..."` / `'...'`
1263
- * literals at options-object depth count; computed codes (identifiers,
1438
+ * Collects literal string values of the named top-level properties inside a
1439
+ * ProviderError/AuthError/ValidationError options object. Only plain `"..."` /
1440
+ * `'...'` literals at options-object depth count; computed values (identifiers,
1264
1441
  * ternaries, template substitutions, concatenations, escapes) are skipped
1265
- * silently so the rule never guesses.
1442
+ * silently so the rules never guess. The first literal wins per property name.
1266
1443
  */
1267
- function collectLiteralErrorCodeValues(args: string): string[] {
1268
- const codes: string[] = [];
1444
+ function collectLiteralErrorOptionValues(
1445
+ args: string,
1446
+ optionNames: readonly string[],
1447
+ ): Record<string, string> {
1448
+ const collected: Record<string, string> = {};
1269
1449
  let braceDepth = 0;
1270
1450
  let parenDepth = 0;
1271
1451
  let bracketDepth = 0;
@@ -1275,7 +1455,7 @@ function collectLiteralErrorCodeValues(args: string): string[] {
1275
1455
  if (char === '"' || char === "'" || char === "`") {
1276
1456
  const end = skipStringLiteral(args, index);
1277
1457
  if (end < 0) {
1278
- return codes;
1458
+ return collected;
1279
1459
  }
1280
1460
  index = end;
1281
1461
  previousSignificantChar = char;
@@ -1284,7 +1464,7 @@ function collectLiteralErrorCodeValues(args: string): string[] {
1284
1464
  if (char === "/" && args[index + 1] === "/") {
1285
1465
  const newline = args.indexOf("\n", index);
1286
1466
  if (newline === -1) {
1287
- return codes;
1467
+ return collected;
1288
1468
  }
1289
1469
  index = newline;
1290
1470
  continue;
@@ -1292,7 +1472,7 @@ function collectLiteralErrorCodeValues(args: string): string[] {
1292
1472
  if (char === "/" && args[index + 1] === "*") {
1293
1473
  const end = args.indexOf("*/", index + 2);
1294
1474
  if (end === -1) {
1295
- return codes;
1475
+ return collected;
1296
1476
  }
1297
1477
  index = end + 1;
1298
1478
  continue;
@@ -1316,10 +1496,14 @@ function collectLiteralErrorCodeValues(args: string): string[] {
1316
1496
  braceDepth === 1 &&
1317
1497
  parenDepth === 0 &&
1318
1498
  bracketDepth === 0 &&
1319
- (previousSignificantChar === "{" || previousSignificantChar === ",") &&
1320
- args.startsWith("code", index)
1499
+ (previousSignificantChar === "{" || previousSignificantChar === ",")
1321
1500
  ) {
1322
- let cursor = index + "code".length;
1501
+ const optionName = optionNames.find((name) => startsWithOptionName(args, index, name));
1502
+ if (optionName === undefined) {
1503
+ previousSignificantChar = char;
1504
+ continue;
1505
+ }
1506
+ let cursor = index + optionName.length;
1323
1507
  while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
1324
1508
  cursor++;
1325
1509
  }
@@ -1342,23 +1526,41 @@ function collectLiteralErrorCodeValues(args: string): string[] {
1342
1526
  !value.includes("\\") &&
1343
1527
  (nextChar === "," || nextChar === "}" || nextChar === "")
1344
1528
  ) {
1345
- codes.push(value);
1529
+ collected[optionName] ??= value;
1346
1530
  }
1347
1531
  index = end;
1348
1532
  previousSignificantChar = quote;
1349
1533
  continue;
1350
1534
  }
1351
- return codes;
1535
+ return collected;
1352
1536
  }
1353
1537
  }
1354
1538
  }
1355
1539
  previousSignificantChar = char;
1356
1540
  }
1357
- return codes;
1541
+ return collected;
1542
+ }
1543
+
1544
+ /** True when `name` starts at `index` and is not the prefix of a longer identifier. */
1545
+ function startsWithOptionName(args: string, index: number, name: string): boolean {
1546
+ if (!args.startsWith(name, index)) return false;
1547
+ const next = args[index + name.length] ?? "";
1548
+ return !/[A-Za-z0-9_$]/.test(next);
1358
1549
  }
1359
1550
 
1360
- function collectLiteralThrownErrorCodes(source: string): string[] {
1361
- const codes: string[] = [];
1551
+ /** One `new ProviderError|AuthError|ValidationError(...)` construction found by source scan. */
1552
+ type ThrownErrorConstructionSite = {
1553
+ /** Constructor name, e.g. `ProviderError`. */
1554
+ readonly errorClass: string;
1555
+ readonly code?: string;
1556
+ readonly messageKey?: string;
1557
+ readonly fixKey?: string;
1558
+ };
1559
+
1560
+ const THROWN_ERROR_LOCALE_OPTION_NAMES = ["code", "messageKey", "fixKey"] as const;
1561
+
1562
+ function collectThrownErrorConstructionSites(source: string): ThrownErrorConstructionSite[] {
1563
+ const sites: ThrownErrorConstructionSite[] = [];
1362
1564
  THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = 0;
1363
1565
  for (
1364
1566
  let match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source);
@@ -1368,11 +1570,52 @@ function collectLiteralThrownErrorCodes(source: string): string[] {
1368
1570
  const argsStart = match.index + match[0].length;
1369
1571
  const args = extractBalancedCallArguments(source, argsStart);
1370
1572
  if (args !== undefined) {
1371
- codes.push(...collectLiteralErrorCodeValues(args));
1573
+ const values = collectLiteralErrorOptionValues(args, THROWN_ERROR_LOCALE_OPTION_NAMES);
1574
+ sites.push({
1575
+ errorClass: match[1] ?? "",
1576
+ ...(values.code === undefined ? {} : { code: values.code }),
1577
+ ...(values.messageKey === undefined ? {} : { messageKey: values.messageKey }),
1578
+ ...(values.fixKey === undefined ? {} : { fixKey: values.fixKey }),
1579
+ });
1372
1580
  }
1373
1581
  THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = argsStart;
1374
1582
  }
1375
- return codes;
1583
+ return sites;
1584
+ }
1585
+
1586
+ type ThrowSiteLintSource = { readonly field: string; readonly source: string };
1587
+
1588
+ /**
1589
+ * The provider sources a throw-site rule scans, with the diagnostic field path
1590
+ * each one reports under. Prefers the CLI-collected source files (whole-repo
1591
+ * coverage) and falls back to the handler/auth-flow function text available
1592
+ * when `lintProvider` is called without them. Test sources are excluded.
1593
+ */
1594
+ function collectThrowSiteLintSources(provider: {
1595
+ authFlowSource?: string;
1596
+ providerSourceFiles?: Record<string, string>;
1597
+ operations?: Record<string, { handler?: unknown; source?: string }>;
1598
+ }): ThrowSiteLintSource[] {
1599
+ const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
1600
+ ([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath),
1601
+ );
1602
+ if (sourceFiles.length > 0) {
1603
+ return sourceFiles.map(([filePath, source]) => ({
1604
+ field: `sourceFiles.${filePath}`,
1605
+ source,
1606
+ }));
1607
+ }
1608
+ const sources: ThrowSiteLintSource[] = [];
1609
+ if (provider.authFlowSource) {
1610
+ sources.push({ field: "auth.flow", source: provider.authFlowSource });
1611
+ }
1612
+ for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1613
+ const source = getOperationSource(operation);
1614
+ if (source) {
1615
+ sources.push({ field: `operations.${operationKey}.handler`, source });
1616
+ }
1617
+ }
1618
+ return sources;
1376
1619
  }
1377
1620
 
1378
1621
  /**
@@ -1417,30 +1660,15 @@ function lintUndeclaredThrownErrorCodes(provider: {
1417
1660
  }
1418
1661
  }
1419
1662
 
1420
- const sources: Array<{ field: string; source: string }> = [];
1421
- const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
1422
- ([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath),
1423
- );
1424
- if (sourceFiles.length > 0) {
1425
- for (const [filePath, source] of sourceFiles) {
1426
- sources.push({ field: `sourceFiles.${filePath}`, source });
1427
- }
1428
- } else {
1429
- if (provider.authFlowSource) {
1430
- sources.push({ field: "auth.flow", source: provider.authFlowSource });
1431
- }
1432
- for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1433
- const source = getOperationSource(operation);
1434
- if (source) {
1435
- sources.push({ field: `operations.${operationKey}.handler`, source });
1436
- }
1437
- }
1438
- }
1663
+ const sources = collectThrowSiteLintSources(provider);
1439
1664
 
1440
1665
  const diagnostics: LintDiagnostic[] = [];
1441
1666
  for (const { field, source } of sources) {
1442
1667
  const undeclaredCodes = new Set(
1443
- collectLiteralThrownErrorCodes(source).filter((code) => !knownCodes.has(code)),
1668
+ collectThrownErrorConstructionSites(source)
1669
+ .filter((site) => UNDECLARED_CODE_ERROR_CLASSES.has(site.errorClass))
1670
+ .flatMap((site) => (site.code === undefined ? [] : [site.code]))
1671
+ .filter((code) => !knownCodes.has(code)),
1444
1672
  );
1445
1673
  for (const code of undeclaredCodes) {
1446
1674
  diagnostics.push({
@@ -1454,6 +1682,245 @@ function lintUndeclaredThrownErrorCodes(provider: {
1454
1682
  return diagnostics;
1455
1683
  }
1456
1684
 
1685
+ /**
1686
+ * Flags an operation `errorCodes` entry whose declared `status` or `retryable`
1687
+ * contradicts the SDK's canonical resolution for that code.
1688
+ *
1689
+ * For a code the SDK status-maps but does not runtime-own (UPSTREAM_ERROR,
1690
+ * BLOCKED, and the fleet-consensus codes registered alongside them), the
1691
+ * declaration still wins at runtime — `toStatusCode` reads the declaration
1692
+ * before the canonical map. So registering a code never silently rewrites an
1693
+ * existing declaration's status, and it must not: that would change a served
1694
+ * status behind the author's back. What it does create is a fleet that answers
1695
+ * one code two ways. This rule is where that divergence surfaces, by name, with
1696
+ * the canonical value to converge on.
1697
+ *
1698
+ * Codes the SDK runtime-owns are handled separately: `defineProvider` already
1699
+ * warns there, because for those the declaration really is ignored.
1700
+ *
1701
+ * Warning level, deliberately: the divergent providers are live and converge on
1702
+ * their own schedule, so this must not fail `apifuse check`.
1703
+ */
1704
+ function lintErrorCodeDeclarationConflicts(provider: {
1705
+ id?: string;
1706
+ operations?: Record<
1707
+ string,
1708
+ {
1709
+ errorCodes?: ReadonlyArray<{ code: string; status?: number; retryable?: boolean }>;
1710
+ }
1711
+ >;
1712
+ }): LintDiagnostic[] {
1713
+ const diagnostics: LintDiagnostic[] = [];
1714
+ const providerId = provider.id ?? "unknown";
1715
+ for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1716
+ for (const [index, entry] of (operation.errorCodes ?? []).entries()) {
1717
+ if (typeof entry?.code !== "string") continue;
1718
+ // SDK-owned codes ignore the declaration outright; defineProvider owns
1719
+ // that message so authors are not told two different things.
1720
+ if (SDK_RUNTIME_OWNED_ERROR_CODES.has(entry.code)) continue;
1721
+
1722
+ const field = `operations.${operationKey}.errorCodes[${index}]`;
1723
+ const canonicalStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(entry.code);
1724
+ if (canonicalStatus !== undefined && entry.status !== undefined) {
1725
+ if (entry.status !== canonicalStatus) {
1726
+ diagnostics.push({
1727
+ rule: "error-code-status-conflicts-sdk",
1728
+ level: "warn",
1729
+ field: `${field}.status`,
1730
+ 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.`,
1731
+ });
1732
+ }
1733
+ }
1734
+
1735
+ const canonicalRetryable = SDK_CANONICAL_ERROR_CODE_RETRYABILITY.get(entry.code);
1736
+ if (
1737
+ canonicalRetryable !== undefined &&
1738
+ entry.retryable !== undefined &&
1739
+ entry.retryable !== canonicalRetryable
1740
+ ) {
1741
+ diagnostics.push({
1742
+ rule: "error-code-retryable-conflicts-sdk",
1743
+ level: "warn",
1744
+ field: `${field}.retryable`,
1745
+ 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.`,
1746
+ });
1747
+ }
1748
+ }
1749
+ }
1750
+ return diagnostics;
1751
+ }
1752
+
1753
+ /** Declared `errorCodes[]` entry shape the localization rules read. */
1754
+ type OperationErrorCodeLike = {
1755
+ code: string;
1756
+ messageKey?: string;
1757
+ fixKey?: string;
1758
+ };
1759
+
1760
+ type ErrorLocaleKeyStatus = "ok" | "missing" | "malformed";
1761
+
1762
+ function errorLocaleKeyStatus(catalog: ProviderLocaleCatalog, key: string): ErrorLocaleKeyStatus {
1763
+ if (!isProviderLocaleKey(key)) return "malformed";
1764
+ const value = getProviderLocaleSegments(catalog, key.split("."));
1765
+ return typeof value === "string" && value.trim().length > 0 ? "ok" : "missing";
1766
+ }
1767
+
1768
+ function hasDerivedErrorCatalogText(
1769
+ catalog: ProviderLocaleCatalog,
1770
+ code: string,
1771
+ field: "message" | "fix",
1772
+ ): boolean {
1773
+ const value = getProviderLocaleSegments(catalog, [PROVIDER_ERROR_CATALOG_NAMESPACE, code, field]);
1774
+ return typeof value === "string" && value.trim().length > 0;
1775
+ }
1776
+
1777
+ /**
1778
+ * True when a thrown code reaches the caller as a real provider error whose
1779
+ * text the declared/derived catalog path can actually reach.
1780
+ *
1781
+ * A code qualifies either because the provider declares it in an operation's
1782
+ * `errorCodes`, or because the SDK registers a canonical status for it. The
1783
+ * second arm matters because registering a code is precisely what lets a
1784
+ * provider *delete* the declaration — AUTHORING tells it to for
1785
+ * `UPSTREAM_AUTH_ERROR`, `UPSTREAM_SCHEMA_ERROR` and `INVALID_REQUEST` — and an
1786
+ * undeclared throw of a registered code is no longer the HTTP 500 that
1787
+ * `thrown-error-code-undeclared` reports. Without this arm the two rules leave
1788
+ * a silent hole exactly where the fleet is being told to converge: a 502
1789
+ * `UPSTREAM_SCHEMA_ERROR` served untranslated with nothing warning about it.
1790
+ *
1791
+ * Codes the SDK runtime-owns are excluded: `sdkOwnsErrorResolution` skips the
1792
+ * declaration and derived candidates for them at serve time, so the catalog
1793
+ * entry this rule asks for would never take effect.
1794
+ */
1795
+ function isCatalogReachableThrownCode(code: string, declaredCodes: ReadonlySet<string>): boolean {
1796
+ if (declaredCodes.has(code)) return true;
1797
+ return (
1798
+ !SDK_RUNTIME_OWNED_ERROR_CODES.has(code) && SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.has(code)
1799
+ );
1800
+ }
1801
+
1802
+ /**
1803
+ * Authoring rules for localized provider error text (see
1804
+ * `ProviderErrorOptions.messageKey`).
1805
+ *
1806
+ * - `error-locale-key-missing` / `error-locale-key-malformed` (**error**): a
1807
+ * literal `messageKey`/`fixKey`, whether on a throw site or on an
1808
+ * `errorCodes[]` declaration, that the provider's `en` catalog does not
1809
+ * resolve to non-empty text, or that is not a legal locale dot path. At
1810
+ * runtime such a key is silently skipped and the caller gets the English
1811
+ * literal, so nothing fails loudly without this rule.
1812
+ * - `thrown-error-message-not-localized` (**warn**): a throw site whose literal
1813
+ * `code` is declared in the provider's `errorCodes` — or SDK-registered, and
1814
+ * so servable without a declaration — but that carries no `messageKey`, no
1815
+ * declaration-level `messageKey`, and no derived `errors.<code>.message` in
1816
+ * `en`. Such an error is served untranslated to every caller. Warning while
1817
+ * the ~2,000 existing sites migrate; it is promoted to error per provider as
1818
+ * each migration wave lands.
1819
+ *
1820
+ * Both rules are skipped entirely when the caller did not supply
1821
+ * `localeCatalogEn`: without the catalog they could only guess.
1822
+ */
1823
+ function lintErrorMessageLocalization(provider: {
1824
+ authFlowSource?: string;
1825
+ providerSourceFiles?: Record<string, string>;
1826
+ localeCatalogEn?: ProviderLocaleCatalog;
1827
+ operations?: Record<
1828
+ string,
1829
+ {
1830
+ handler?: unknown;
1831
+ source?: string;
1832
+ errorCodes?: ReadonlyArray<OperationErrorCodeLike>;
1833
+ }
1834
+ >;
1835
+ }): LintDiagnostic[] {
1836
+ const catalog = provider.localeCatalogEn;
1837
+ if (!catalog) return [];
1838
+
1839
+ const diagnostics: LintDiagnostic[] = [];
1840
+ const declaredCodes = new Set<string>();
1841
+ const codesWithDeclaredMessageKey = new Set<string>();
1842
+
1843
+ for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1844
+ for (const entry of operation.errorCodes ?? []) {
1845
+ if (typeof entry?.code !== "string") continue;
1846
+ declaredCodes.add(entry.code);
1847
+ for (const keyField of ["messageKey", "fixKey"] as const) {
1848
+ const key = entry[keyField];
1849
+ if (typeof key !== "string") continue;
1850
+ if (keyField === "messageKey") codesWithDeclaredMessageKey.add(entry.code);
1851
+ const status = errorLocaleKeyStatus(catalog, key);
1852
+ if (status === "ok") continue;
1853
+ diagnostics.push(
1854
+ errorLocaleKeyDiagnostic(
1855
+ status,
1856
+ `operations.${operationKey}.errorCodes`,
1857
+ key,
1858
+ `errorCodes entry "${entry.code}" of operation "${operationKey}"`,
1859
+ ),
1860
+ );
1861
+ }
1862
+ }
1863
+ }
1864
+
1865
+ for (const { field, source } of collectThrowSiteLintSources(provider)) {
1866
+ const reportedKeys = new Set<string>();
1867
+ const reportedCodes = new Set<string>();
1868
+ for (const site of collectThrownErrorConstructionSites(source)) {
1869
+ for (const key of [site.messageKey, site.fixKey]) {
1870
+ if (key === undefined || reportedKeys.has(key)) continue;
1871
+ const status = errorLocaleKeyStatus(catalog, key);
1872
+ if (status === "ok") continue;
1873
+ reportedKeys.add(key);
1874
+ diagnostics.push(
1875
+ errorLocaleKeyDiagnostic(status, field, key, `a thrown error in ${field}`),
1876
+ );
1877
+ }
1878
+ const code = site.code;
1879
+ if (code === undefined || reportedCodes.has(code)) continue;
1880
+ if (site.messageKey !== undefined) continue;
1881
+ if (!isCatalogReachableThrownCode(code, declaredCodes)) continue;
1882
+ if (codesWithDeclaredMessageKey.has(code)) continue;
1883
+ if (hasDerivedErrorCatalogText(catalog, code, "message")) continue;
1884
+ reportedCodes.add(code);
1885
+ const origin = declaredCodes.has(code)
1886
+ ? `declared code "${code}"`
1887
+ : `SDK-registered code "${code}"`;
1888
+ const declarationClause = declaredCodes.has(code)
1889
+ ? "its errorCodes declaration has none, and"
1890
+ : "no errorCodes declaration supplies one, and";
1891
+ diagnostics.push({
1892
+ rule: "thrown-error-message-not-localized",
1893
+ level: "warn",
1894
+ field,
1895
+ 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).`,
1896
+ });
1897
+ }
1898
+ }
1899
+
1900
+ return diagnostics;
1901
+ }
1902
+
1903
+ function errorLocaleKeyDiagnostic(
1904
+ status: Exclude<ErrorLocaleKeyStatus, "ok">,
1905
+ field: string,
1906
+ key: string,
1907
+ owner: string,
1908
+ ): LintDiagnostic {
1909
+ return status === "malformed"
1910
+ ? {
1911
+ rule: "error-locale-key-malformed",
1912
+ level: "error",
1913
+ field,
1914
+ 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.`,
1915
+ }
1916
+ : {
1917
+ rule: "error-locale-key-missing",
1918
+ level: "error",
1919
+ field,
1920
+ 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.`,
1921
+ };
1922
+ }
1923
+
1457
1924
  type HandleFieldOccurrence = {
1458
1925
  /** Diagnostic path, e.g. `output.offers[].waiting_token`. */
1459
1926
  path: string;
@@ -1536,6 +2003,7 @@ function readHandleKindDeclarations(value: unknown): HandleKindDeclaration[] {
1536
2003
  typeof record.name === "string" &&
1537
2004
  (record.type === "cursor" || record.type === "draft") &&
1538
2005
  typeof record.fieldName === "string" &&
2006
+ (record.outputFieldName === undefined || typeof record.outputFieldName === "string") &&
1539
2007
  (record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy))
1540
2008
  );
1541
2009
  });
@@ -1581,7 +2049,13 @@ function lintHandleDeclarations(provider: {
1581
2049
  for (const { side, fields } of positions) {
1582
2050
  for (const occurrence of fields) {
1583
2051
  const field = `operations.${operationKey}.${occurrence.path}`;
1584
- const { kind, fieldName } = occurrence.meta;
2052
+ const { kind } = occurrence.meta;
2053
+ // A kind may name one handle differently in each direction (the canon's
2054
+ // `cursor` in, `next_cursor` out). The rule is still exact: each side
2055
+ // must use the key declared for THAT side, so the handle's identity
2056
+ // stays readable from the property key and the round trip has exactly
2057
+ // one edge — read `output`, send back under `input`.
2058
+ const expected = handleFieldNameFor(occurrence.meta, side);
1585
2059
 
1586
2060
  if (!kindByName.has(kind)) {
1587
2061
  diagnostics.push({
@@ -1592,12 +2066,16 @@ function lintHandleDeclarations(provider: {
1592
2066
  });
1593
2067
  }
1594
2068
 
1595
- if (occurrence.key !== fieldName) {
2069
+ if (occurrence.key !== expected) {
2070
+ const directional = handleHasDirectionalFieldNames(occurrence.meta);
2071
+ const advice = directional
2072
+ ? `Use the kind's ${side} fieldName on every ${side} schema; its ${side === "input" ? "output" : "input"} fieldName is "${handleFieldNameFor(occurrence.meta, side === "input" ? "output" : "input")}".`
2073
+ : "Use the same key in every schema so the LLM sees one name.";
1596
2074
  diagnostics.push({
1597
2075
  rule: "handle-field-name",
1598
2076
  level: "error",
1599
2077
  field,
1600
- 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.`,
2078
+ message: `${providerLabel} operation "${operationKey}" ${side} declares handle kind "${kind}" under property "${occurrence.key}" but the kind's ${directional ? `${side} ` : ""}fieldName is "${expected}". ${advice}`,
1601
2079
  });
1602
2080
  }
1603
2081
 
@@ -1626,7 +2104,7 @@ function lintHandleDeclarations(provider: {
1626
2104
  rule: "handle-field-parity",
1627
2105
  level: "error",
1628
2106
  field,
1629
- 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.`,
2107
+ 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.`,
1630
2108
  });
1631
2109
  }
1632
2110
  if (!accepted) {
@@ -1634,7 +2112,7 @@ function lintHandleDeclarations(provider: {
1634
2112
  rule: "handle-field-parity",
1635
2113
  level: "error",
1636
2114
  field,
1637
- 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.`,
2115
+ 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.`,
1638
2116
  });
1639
2117
  }
1640
2118
 
@@ -1660,7 +2138,7 @@ function lintHandleDeclarations(provider: {
1660
2138
  rule: "handle-issued-by",
1661
2139
  level: "error",
1662
2140
  field,
1663
- message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind.fieldName}" handle field.`,
2141
+ message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${handleFieldNameFor(kind, "output")}" handle field.`,
1664
2142
  });
1665
2143
  } else {
1666
2144
  diagnostics.push({
@@ -1671,7 +2149,7 @@ function lintHandleDeclarations(provider: {
1671
2149
  .map((operationKey) => `"${operationKey}"`)
1672
2150
  .join(
1673
2151
  ", ",
1674
- )}, but none of those operations' outputs has a "${kind.fieldName}" handle field. At least one listed issuer must output it.`,
2152
+ )}, but none of those operations' outputs has a "${handleFieldNameFor(kind, "output")}" handle field. At least one listed issuer must output it.`,
1675
2153
  });
1676
2154
  }
1677
2155
  }
@@ -1703,7 +2181,7 @@ const LEGACY_CHOICE_IDENTIFIERS = new Set([
1703
2181
  * the provider) while code inside `${}` interpolations still does. Falls back
1704
2182
  * to a raw-source regex when TypeScript is not installed.
1705
2183
  */
1706
- function findLegacyChoiceUsage(source: string): string | undefined {
2184
+ function findLegacyChoiceUsage(fileName: string, source: string): string | undefined {
1707
2185
  let ts: typeof import("typescript");
1708
2186
  try {
1709
2187
  ts = getTypeScript();
@@ -1711,11 +2189,11 @@ function findLegacyChoiceUsage(source: string): string | undefined {
1711
2189
  return LEGACY_CHOICE_USAGE_PATTERN.exec(source)?.[0];
1712
2190
  }
1713
2191
  const file = ts.createSourceFile(
1714
- "provider.ts",
2192
+ fileName,
1715
2193
  source,
1716
2194
  ts.ScriptTarget.Latest,
1717
2195
  false,
1718
- ts.ScriptKind.TSX,
2196
+ scriptKindForSourceFile(ts, fileName),
1719
2197
  );
1720
2198
  let found: string | undefined;
1721
2199
  const visit = (node: import("typescript").Node): void => {
@@ -1740,23 +2218,27 @@ function findLegacyChoiceUsage(source: string): string | undefined {
1740
2218
  }
1741
2219
 
1742
2220
  function lintLegacyChoiceUsage(provider: ProviderSourceLike): LintDiagnostic[] {
1743
- const sources: Array<{ field: string; source: string }> = [];
2221
+ const sources: Array<{ field: string; fileName: string; source: string }> = [];
1744
2222
 
1745
2223
  if (provider.authFlowSource) {
1746
- sources.push({ field: "auth.flow", source: provider.authFlowSource });
2224
+ sources.push({ field: "auth.flow", fileName: "auth-flow.ts", source: provider.authFlowSource });
1747
2225
  }
1748
2226
  for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
1749
- sources.push({ field: `sourceFiles.${filePath}`, source });
2227
+ sources.push({ field: `sourceFiles.${filePath}`, fileName: filePath, source });
1750
2228
  }
1751
2229
  for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1752
2230
  const source = getOperationSource(operation);
1753
2231
  if (source) {
1754
- sources.push({ field: `operations.${operationKey}.handler`, source });
2232
+ sources.push({
2233
+ field: `operations.${operationKey}.handler`,
2234
+ fileName: `${operationKey}.ts`,
2235
+ source,
2236
+ });
1755
2237
  }
1756
2238
  }
1757
2239
 
1758
- return sources.flatMap(({ field, source }) => {
1759
- const match = findLegacyChoiceUsage(source);
2240
+ return sources.flatMap(({ field, fileName, source }) => {
2241
+ const match = findLegacyChoiceUsage(fileName, source);
1760
2242
  if (!match) return [];
1761
2243
  return [
1762
2244
  {
@@ -1901,6 +2383,11 @@ export function lintProvider(
1901
2383
  };
1902
2384
  authFlowSource?: string;
1903
2385
  providerSourceFiles?: Record<string, string>;
2386
+ /**
2387
+ * The provider's `locales/en.json`, injected by `apifuse check`. Error
2388
+ * message localization rules are skipped when it is absent.
2389
+ */
2390
+ localeCatalogEn?: ProviderLocaleCatalog;
1904
2391
  operations?: Record<
1905
2392
  string,
1906
2393
  {
@@ -1915,7 +2402,13 @@ export function lintProvider(
1915
2402
  fixtures?: unknown;
1916
2403
  handler?: unknown;
1917
2404
  source?: string;
1918
- errorCodes?: ReadonlyArray<{ code: string }>;
2405
+ errorCodes?: ReadonlyArray<{
2406
+ code: string;
2407
+ status?: number;
2408
+ retryable?: boolean;
2409
+ messageKey?: string;
2410
+ fixKey?: string;
2411
+ }>;
1919
2412
  }
1920
2413
  >;
1921
2414
  meta?: {
@@ -1946,9 +2439,15 @@ export function lintProviderWithInformation(
1946
2439
  ...lintSelfHostedBrowserPatterns(provider, options),
1947
2440
  ...lintBrowserVersionLiterals(provider),
1948
2441
  ...lintUndeclaredThrownErrorCodes(provider),
2442
+ ...lintErrorCodeDeclarationConflicts(provider),
2443
+ ...lintErrorMessageLocalization(provider),
1949
2444
  ...lintLegacyChoiceUsage(provider),
1950
2445
  ...lintHandleDeclarations(provider),
1951
2446
  ];
2447
+ // Runtime boundary rules (process.env, node:fs/net/child_process, raw
2448
+ // fetch) also yield information entries for `@apifuse-allow` acknowledgements.
2449
+ const runtimeBoundary = lintRuntimeBoundary(getTypeScript(), provider, getOperationSource);
2450
+ diagnostics.push(...runtimeBoundary.diagnostics);
1952
2451
 
1953
2452
  if (provider.operations) {
1954
2453
  const authMode = provider.auth?.mode;
@@ -1992,7 +2491,10 @@ export function lintProviderWithInformation(
1992
2491
  }
1993
2492
 
1994
2493
  if (!provider.operations) {
1995
- return applyPinnedWireFieldPaths(provider, diagnostics);
2494
+ return withInformation(
2495
+ applyPinnedWireFieldPaths(provider, diagnostics),
2496
+ runtimeBoundary.information,
2497
+ );
1996
2498
  }
1997
2499
 
1998
2500
  diagnostics.push(
@@ -2023,5 +2525,16 @@ export function lintProviderWithInformation(
2023
2525
  ),
2024
2526
  );
2025
2527
 
2026
- return applyPinnedWireFieldPaths(provider, diagnostics);
2528
+ return withInformation(
2529
+ applyPinnedWireFieldPaths(provider, diagnostics),
2530
+ runtimeBoundary.information,
2531
+ );
2532
+ }
2533
+
2534
+ function withInformation(
2535
+ result: ProviderLintResult,
2536
+ information: readonly ProviderLintInformation[],
2537
+ ): ProviderLintResult {
2538
+ if (information.length === 0) return result;
2539
+ return { diagnostics: result.diagnostics, information: [...information, ...result.information] };
2027
2540
  }