@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.31

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 (342) hide show
  1. package/AUTHORING.md +493 -5
  2. package/CHANGELOG.md +131 -1
  3. package/README.md +52 -6
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +106 -62
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +63 -55
  8. package/bin/apifuse-pack-check.ts +22 -2
  9. package/bin/apifuse-pack-smoke.ts +78 -82
  10. package/bin/apifuse-pack-types.ts +583 -0
  11. package/bin/apifuse-perf.ts +59 -140
  12. package/bin/apifuse-record.ts +698 -113
  13. package/bin/apifuse-submit-check.ts +517 -44
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml.ts +1 -1
  18. package/dist/auth-turn/index.d.ts +4 -4
  19. package/dist/auth-turn/index.js +1 -1
  20. package/dist/auth.d.ts +16 -2
  21. package/dist/auth.js +76 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +117 -29
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/cli/templates/provider/README.md.tpl +4 -4
  32. package/dist/config/loader.d.ts +176 -17
  33. package/dist/config/loader.js +434 -161
  34. package/dist/contract-serialization.d.ts +2 -2
  35. package/dist/contract-serialization.js +7 -14
  36. package/dist/contract-types.d.ts +3 -2
  37. package/dist/contract.d.ts +3 -3
  38. package/dist/contract.js +6 -6
  39. package/dist/declaration-validation.d.ts +23 -0
  40. package/dist/declaration-validation.js +159 -0
  41. package/dist/define.d.ts +13 -1
  42. package/dist/define.js +391 -122
  43. package/dist/dev.d.ts +1 -1
  44. package/dist/dev.js +1 -1
  45. package/dist/error-resolution.d.ts +4 -0
  46. package/dist/error-resolution.js +123 -0
  47. package/dist/errors.d.ts +19 -1
  48. package/dist/errors.js +41 -3
  49. package/dist/fixture-sanitization.d.ts +26 -0
  50. package/dist/fixture-sanitization.js +216 -0
  51. package/dist/i18n/catalog.d.ts +2 -2
  52. package/dist/i18n/catalog.js +4 -10
  53. package/dist/i18n/index.d.ts +2 -2
  54. package/dist/i18n/index.js +2 -2
  55. package/dist/i18n/keys.d.ts +2 -2
  56. package/dist/index.d.ts +50 -42
  57. package/dist/index.js +41 -37
  58. package/dist/lint.d.ts +6 -1
  59. package/dist/lint.js +370 -18
  60. package/dist/native-address.d.ts +43 -0
  61. package/dist/native-address.js +281 -0
  62. package/dist/native-egress-policy.d.ts +31 -0
  63. package/dist/native-egress-policy.js +288 -0
  64. package/dist/observability.d.ts +5 -2
  65. package/dist/observability.js +48 -1
  66. package/dist/provider.d.ts +13 -11
  67. package/dist/provider.js +10 -9
  68. package/dist/public-schema-field-lint.d.ts +1 -1
  69. package/dist/recipes/gov-api.js +1 -1
  70. package/dist/runtime/auth-flow.d.ts +3 -1
  71. package/dist/runtime/auth-flow.js +8 -3
  72. package/dist/runtime/browser.d.ts +1 -1
  73. package/dist/runtime/browser.js +138 -40
  74. package/dist/runtime/cache.d.ts +2 -1
  75. package/dist/runtime/cache.js +173 -23
  76. package/dist/runtime/choice-wordlist.d.ts +9 -0
  77. package/dist/runtime/choice-wordlist.js +138 -0
  78. package/dist/runtime/choice.d.ts +14 -1
  79. package/dist/runtime/choice.js +566 -101
  80. package/dist/runtime/credential.d.ts +1 -1
  81. package/dist/runtime/credential.js +1 -1
  82. package/dist/runtime/env.d.ts +1 -1
  83. package/dist/runtime/executor.d.ts +1 -1
  84. package/dist/runtime/executor.js +25 -3
  85. package/dist/runtime/http.d.ts +3 -2
  86. package/dist/runtime/http.js +517 -55
  87. package/dist/runtime/insights.d.ts +1 -1
  88. package/dist/runtime/insights.js +6 -13
  89. package/dist/runtime/instrumentation.d.ts +2 -2
  90. package/dist/runtime/instrumentation.js +371 -23
  91. package/dist/runtime/keyring.js +1 -1
  92. package/dist/runtime/namespace.js +1 -1
  93. package/dist/runtime/native-network-errors.d.ts +33 -0
  94. package/dist/runtime/native-network-errors.js +69 -0
  95. package/dist/runtime/native-network.d.ts +96 -0
  96. package/dist/runtime/native-network.js +1232 -0
  97. package/dist/runtime/ocr.d.ts +29 -0
  98. package/dist/runtime/ocr.js +440 -0
  99. package/dist/runtime/otlp.d.ts +1 -1
  100. package/dist/runtime/perf.d.ts +1 -1
  101. package/dist/runtime/provider.d.ts +1 -1
  102. package/dist/runtime/provider.js +1 -2
  103. package/dist/runtime/proxy-errors.d.ts +1 -1
  104. package/dist/runtime/proxy-errors.js +9 -7
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  108. package/dist/runtime/proxy-retry-policy.js +2 -2
  109. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  110. package/dist/runtime/proxy-telemetry.js +58 -52
  111. package/dist/runtime/redirects.d.ts +29 -0
  112. package/dist/runtime/redirects.js +36 -0
  113. package/dist/runtime/redis.d.ts +1 -1
  114. package/dist/runtime/redis.js +5 -5
  115. package/dist/runtime/request-options.d.ts +68 -1
  116. package/dist/runtime/request-options.js +548 -0
  117. package/dist/runtime/resolver-config.d.ts +6 -0
  118. package/dist/runtime/resolver-config.js +6 -0
  119. package/dist/runtime/resolver-public.d.ts +1 -0
  120. package/dist/runtime/resolver-public.js +1 -0
  121. package/dist/runtime/resolver-shared.d.ts +3 -0
  122. package/dist/runtime/resolver-shared.js +12 -0
  123. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  124. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  125. package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
  126. package/dist/runtime/resolver-vendors/browser.js +282 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +83 -0
  132. package/dist/runtime/resolver-vendors/types.js +69 -0
  133. package/dist/runtime/resolver.d.ts +59 -0
  134. package/dist/runtime/resolver.js +705 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +5 -2
  138. package/dist/runtime/state.js +280 -74
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +30 -5
  142. package/dist/runtime/stealth.js +523 -259
  143. package/dist/runtime/stt.d.ts +1 -1
  144. package/dist/runtime/stt.js +12 -27
  145. package/dist/runtime/timeout.d.ts +5 -0
  146. package/dist/runtime/timeout.js +12 -0
  147. package/dist/runtime/trace.d.ts +2 -2
  148. package/dist/runtime/trace.js +2 -4
  149. package/dist/runtime/waterfall.d.ts +1 -1
  150. package/dist/schema.d.ts +1 -1
  151. package/dist/schema.js +7 -15
  152. package/dist/serve.d.ts +1 -1
  153. package/dist/serve.js +1 -1
  154. package/dist/server/index.d.ts +7 -7
  155. package/dist/server/index.js +6 -6
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test-redaction.d.ts +1 -1
  159. package/dist/server/self-test-redaction.js +1 -1
  160. package/dist/server/self-test.d.ts +117 -3
  161. package/dist/server/self-test.js +787 -151
  162. package/dist/server/serve-implementation.d.ts +210 -0
  163. package/dist/server/serve-implementation.js +2078 -0
  164. package/dist/server/serve.d.ts +1 -70
  165. package/dist/server/serve.js +1 -1143
  166. package/dist/server/types.d.ts +34 -9
  167. package/dist/server/types.js +8 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +1 -1
  207. package/dist/stealth/profiles.js +21 -21
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.d.ts +1 -1
  211. package/dist/stream.js +7 -1
  212. package/dist/testing/index.d.ts +3 -2
  213. package/dist/testing/index.js +3 -2
  214. package/dist/testing/run.d.ts +32 -2
  215. package/dist/testing/run.js +488 -28
  216. package/dist/types.d.ts +566 -19
  217. package/dist/types.js +1 -0
  218. package/dist/user-input.d.ts +30 -0
  219. package/dist/user-input.js +66 -0
  220. package/package.json +42 -7
  221. package/src/auth-turn/index.ts +2 -2
  222. package/src/auth.ts +146 -86
  223. package/src/ceremonies/index.ts +167 -92
  224. package/src/cli/commands.ts +10 -0
  225. package/src/cli/create.ts +42 -35
  226. package/src/cli/prompt-assets.ts +865 -0
  227. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  228. package/src/cli/templates/provider/README.md.tpl +4 -4
  229. package/src/config/loader.ts +667 -289
  230. package/src/contract-serialization.ts +10 -18
  231. package/src/contract-types.ts +3 -2
  232. package/src/contract.ts +14 -28
  233. package/src/declaration-validation.ts +202 -0
  234. package/src/define.ts +631 -495
  235. package/src/dev.ts +4 -9
  236. package/src/error-resolution.ts +128 -0
  237. package/src/errors.ts +56 -11
  238. package/src/fixture-sanitization.ts +247 -0
  239. package/src/i18n/catalog.ts +10 -32
  240. package/src/i18n/index.ts +2 -2
  241. package/src/i18n/keys.ts +5 -11
  242. package/src/index.ts +158 -44
  243. package/src/lint.ts +488 -154
  244. package/src/native-address.ts +340 -0
  245. package/src/native-egress-policy.ts +358 -0
  246. package/src/observability.ts +51 -1
  247. package/src/provider.ts +66 -11
  248. package/src/public-schema-field-lint.ts +7 -33
  249. package/src/recipes/gov-api.ts +2 -5
  250. package/src/runtime/auth-flow.ts +13 -7
  251. package/src/runtime/browser.ts +252 -207
  252. package/src/runtime/cache.ts +209 -81
  253. package/src/runtime/choice-wordlist.ts +145 -0
  254. package/src/runtime/choice.ts +758 -197
  255. package/src/runtime/credential.ts +2 -2
  256. package/src/runtime/env.ts +1 -1
  257. package/src/runtime/executor.ts +37 -19
  258. package/src/runtime/http.ts +645 -65
  259. package/src/runtime/insights.ts +15 -53
  260. package/src/runtime/instrumentation.ts +530 -67
  261. package/src/runtime/keyring.ts +7 -19
  262. package/src/runtime/namespace.ts +2 -7
  263. package/src/runtime/native-network-errors.ts +99 -0
  264. package/src/runtime/native-network.ts +1605 -0
  265. package/src/runtime/ocr.ts +523 -0
  266. package/src/runtime/otlp.ts +12 -23
  267. package/src/runtime/perf.ts +1 -1
  268. package/src/runtime/provider.ts +4 -9
  269. package/src/runtime/proxy-errors.ts +29 -42
  270. package/src/runtime/proxy-nodemaven.ts +221 -0
  271. package/src/runtime/proxy-retry-policy.ts +3 -3
  272. package/src/runtime/proxy-telemetry.ts +84 -77
  273. package/src/runtime/redirects.ts +66 -0
  274. package/src/runtime/redis.ts +10 -13
  275. package/src/runtime/request-options.ts +679 -9
  276. package/src/runtime/resolver-config.ts +6 -0
  277. package/src/runtime/resolver-public.ts +18 -0
  278. package/src/runtime/resolver-shared.ts +17 -0
  279. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  280. package/src/runtime/resolver-vendors/browser.ts +408 -0
  281. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  282. package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
  283. package/src/runtime/resolver-vendors/types.ts +173 -0
  284. package/src/runtime/resolver.ts +1060 -0
  285. package/src/runtime/secrets.ts +64 -0
  286. package/src/runtime/state.ts +399 -161
  287. package/src/runtime/stealth-cookies.ts +132 -0
  288. package/src/runtime/stealth.ts +681 -295
  289. package/src/runtime/stt.ts +39 -113
  290. package/src/runtime/timeout.ts +18 -0
  291. package/src/runtime/trace.ts +14 -44
  292. package/src/runtime/waterfall.ts +5 -18
  293. package/src/schema.ts +23 -84
  294. package/src/serve.ts +6 -1
  295. package/src/server/index.ts +30 -7
  296. package/src/server/self-test-input-tokens.ts +29 -14
  297. package/src/server/self-test-redaction.ts +2 -2
  298. package/src/server/self-test.ts +1030 -180
  299. package/src/server/serve-implementation.ts +3062 -0
  300. package/src/server/serve.ts +1 -1781
  301. package/src/server/types.ts +12 -13
  302. package/src/stateful/README.md +146 -0
  303. package/src/stateful/errors.ts +35 -0
  304. package/src/stateful/http-provider-event-emitter.ts +314 -0
  305. package/src/stateful/http-session-owner-registry.ts +306 -0
  306. package/src/stateful/index.ts +18 -0
  307. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  308. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  309. package/src/stateful/provider-event-pipeline.ts +61 -0
  310. package/src/stateful/provider-events.ts +462 -0
  311. package/src/stateful/session-key.ts +111 -0
  312. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  313. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  314. package/src/stateful/stateful-provider-adapter.ts +562 -0
  315. package/src/stateful/stateful-provider-observability.ts +261 -0
  316. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  317. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  318. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  319. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  320. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  321. package/src/stateful-signing.ts +46 -0
  322. package/src/stealth/profiles.ts +27 -33
  323. package/src/stream-evidence.ts +988 -0
  324. package/src/stream.ts +16 -20
  325. package/src/testing/index.ts +11 -2
  326. package/src/testing/run.ts +668 -74
  327. package/src/types.ts +665 -35
  328. package/src/user-input.ts +118 -0
  329. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  330. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  331. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  332. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  333. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  334. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  335. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  336. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  337. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  338. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  339. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  340. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  341. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  342. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/lint.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  import type { ZodType } from "zod";
2
2
 
3
- import { lintPublicSchemaFieldNames } from "./public-schema-field-lint";
4
3
  import {
5
- APIFUSE_DESCRIPTION_KEY_META_KEY,
6
- APIFUSE_SENSITIVE_META_KEY,
7
- } from "./schema";
4
+ SDK_RUNTIME_OWNED_ERROR_CODES,
5
+ SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES,
6
+ } from "./error-resolution.js";
7
+ import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
8
+ import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
8
9
 
9
10
  type AuthModeLike =
10
11
  | "none"
11
12
  | "platform-managed"
12
13
  | "credentials"
13
14
  | "oauth2"
15
+ | "oauth2_proxied"
14
16
  | "api-key";
15
17
 
16
18
  type ProviderAuthLike = {
@@ -25,9 +27,97 @@ type ProviderAuthLike = {
25
27
  exchange?: unknown;
26
28
  };
27
29
 
30
+ // Operations that perform an auth-lifecycle action belong on the single
31
+ // `auth.flow` interface, never on a provider operation:
32
+ // - entry (login / signin / authenticate) => auth.flow.start/continue
33
+ // - exit (logout / signout / disconnect) => auth.flow.abort
34
+ //
35
+ // Matching works on `-`/`_` separated segments rather than a raw substring or a
36
+ // leading anchor, so `shop-logout`, `shop_logout` and `user-sign-out-everywhere`
37
+ // are all recognised: a domain prefix does not make the operation any less of an
38
+ // auth-lifecycle action, and operation ids may use either separator.
39
+ //
40
+ // Vocabulary is split into two tiers because auth words collide with ordinary
41
+ // domain verbs. Measured against the live fleet plus synthetic domain ids:
42
+ // - `authorize-payment`, `revoke-invitation`, `unlink-record`,
43
+ // `disconnect-device` are domain actions that never touch the connection
44
+ // credential, so these verbs are NOT matched as segments;
45
+ // - the same verbs as a complete operation id (`authorize`, `revoke`) do
46
+ // refer to the credential itself, so they are matched only in that form.
47
+ // `exchange`, `callback`, `connect`, `session`, `token`, `credential`,
48
+ // `password` and `otp` stay out entirely for the same reason.
49
+ const AUTH_LIFECYCLE_SEGMENT_WORDS = new Set([
50
+ "login",
51
+ "logout",
52
+ "signin",
53
+ "signout",
54
+ "signup",
55
+ "authenticate",
56
+ "reauth",
57
+ "auth",
58
+ ]);
59
+
60
+ // Ambiguous as a prefix, unambiguous when they are the whole operation id.
61
+ const AUTH_LIFECYCLE_WHOLE_ID_WORDS = new Set([
62
+ "authorize",
63
+ "revoke",
64
+ "unlink",
65
+ "disconnect",
66
+ ]);
67
+
68
+ // A verb stem followed by a direction word across two segments: `sign-out`,
69
+ // `user_sign_up_flow`, and the spelled-out `log-in` / `shop-log-out` forms
70
+ // (their fused equivalents `login`/`logout` live in the segment set above).
71
+ // `sign` pairs match anywhere; `log` pairs match only at the END of the id,
72
+ // because mid-id `log` is the noun in domain phrases measured against real
73
+ // fleets (`audit-log-in-range`, `change-log-out-of-band` are reads of a log,
74
+ // while `shop-log-out` is a logout).
75
+ const AUTH_DIRECTION_PAIRS: ReadonlyMap<
76
+ string,
77
+ { directions: ReadonlySet<string>; endOnly: boolean }
78
+ > = new Map([
79
+ ["sign", { directions: new Set(["in", "out", "up"]), endOnly: false }],
80
+ ["log", { directions: new Set(["in", "out"]), endOnly: true }],
81
+ ]);
82
+
83
+ // Legacy anchored form kept for token-plumbing words whose bare use is only
84
+ // auth-related when it leads the operation id (`exchange-code`, `refresh`).
28
85
  const AUTH_OPERATION_ID_PATTERN =
29
86
  /^(?:auth[-_])?(?:login|exchange|continue|refresh|callback)(?:[-_]|$)/i;
30
87
 
88
+ function isAuthLifecycleOperationId(operationId: string, authMode: string): boolean {
89
+ const segments = operationId.toLowerCase().split(/[-_]+/).filter(Boolean);
90
+
91
+ if (segments.some((segment) => AUTH_LIFECYCLE_SEGMENT_WORDS.has(segment))) return true;
92
+
93
+ // A verb stem + direction spread across two segments (`sign-out`,
94
+ // `sign_up`, `shop-log-out`); see AUTH_DIRECTION_PAIRS for positioning.
95
+ if (
96
+ segments.some((segment, index) => {
97
+ const pair = AUTH_DIRECTION_PAIRS.get(segment);
98
+ if (pair === undefined || index + 1 >= segments.length) return false;
99
+ if (!pair.directions.has(segments[index + 1] as string)) return false;
100
+ return pair.endOnly ? index + 2 === segments.length : true;
101
+ })
102
+ ) {
103
+ return true;
104
+ }
105
+
106
+ if (segments.length === 1 && AUTH_LIFECYCLE_WHOLE_ID_WORDS.has(segments[0] as string)) {
107
+ return true;
108
+ }
109
+
110
+ // The legacy anchored pattern keeps its original scope. It matches ordinary
111
+ // domain ids such as `exchange-rates` and `refresh-catalog`, so extending it
112
+ // to `oauth2_proxied` would spread that behavior to providers it never
113
+ // applied to; proxied providers are covered by the segment tiers above.
114
+ if (authMode === "credentials" || authMode === "oauth2") {
115
+ return AUTH_OPERATION_ID_PATTERN.test(operationId);
116
+ }
117
+
118
+ return false;
119
+ }
120
+
31
121
  type ProviderContractMetaLike = {
32
122
  publicSchemaFieldNames?: "normalized";
33
123
  };
@@ -141,28 +231,19 @@ function hasReusableSecretKeys(keys: readonly string[] | undefined): boolean {
141
231
  }
142
232
 
143
233
  return keys.some((key) =>
144
- /(access_token|refresh_token|password|secret|cookie|session|token|api[_-]?key)/i.test(
145
- key,
146
- ),
234
+ /(access_token|refresh_token|password|secret|cookie|session|token|api[_-]?key)/i.test(key),
147
235
  );
148
236
  }
149
237
 
150
- function hasReusableReloginSecretKeys(
151
- keys: readonly string[] | undefined,
152
- ): boolean {
238
+ function hasReusableReloginSecretKeys(keys: readonly string[] | undefined): boolean {
153
239
  if (!keys) {
154
240
  return false;
155
241
  }
156
242
 
157
- return keys.some((key) =>
158
- /(password|passcode|secret|cookie|session)/i.test(key),
159
- );
243
+ return keys.some((key) => /(password|passcode|secret|cookie|session)/i.test(key));
160
244
  }
161
245
 
162
- function getAuthFlowSource(provider: {
163
- auth?: ProviderAuthLike;
164
- authFlowSource?: string;
165
- }): string {
246
+ function getAuthFlowSource(provider: { auth?: ProviderAuthLike; authFlowSource?: string }): string {
166
247
  if (provider.authFlowSource) {
167
248
  return provider.authFlowSource;
168
249
  }
@@ -176,10 +257,7 @@ function getAuthFlowSource(provider: {
176
257
  ];
177
258
 
178
259
  return parts
179
- .filter(
180
- (part): part is (...args: unknown[]) => unknown =>
181
- typeof part === "function",
182
- )
260
+ .filter((part): part is (...args: unknown[]) => unknown => typeof part === "function")
183
261
  .map((part) => part.toString())
184
262
  .join("\n");
185
263
  }
@@ -243,8 +321,7 @@ function lintAuthModel(provider: {
243
321
 
244
322
  if (
245
323
  hasReusableSecretKeys(credentialKeys) &&
246
- (!provider.credential?.storesReusableSecret ||
247
- !provider.credential.justification)
324
+ (!provider.credential?.storesReusableSecret || !provider.credential.justification)
248
325
  ) {
249
326
  diagnostics.push({
250
327
  rule: "credential-reusable-secret",
@@ -257,8 +334,7 @@ function lintAuthModel(provider: {
257
334
  if (
258
335
  typeof provider.auth?.flow?.refresh === "function" &&
259
336
  hasReusableReloginSecretKeys(credentialKeys) &&
260
- (!provider.credential?.storesReusableSecret ||
261
- !provider.credential.justification)
337
+ (!provider.credential?.storesReusableSecret || !provider.credential.justification)
262
338
  ) {
263
339
  diagnostics.push({
264
340
  rule: "auth-refresh-reusable-secret",
@@ -278,10 +354,7 @@ function lintAuthModel(provider: {
278
354
  }
279
355
 
280
356
  const authFlowSource = getAuthFlowSource(provider);
281
- if (
282
- authFlowSource.includes("ctx.context") &&
283
- (provider.context?.keys?.length ?? 0) === 0
284
- ) {
357
+ if (authFlowSource.includes("ctx.context") && (provider.context?.keys?.length ?? 0) === 0) {
285
358
  diagnostics.push({
286
359
  rule: "context-keys-required",
287
360
  level: "warn",
@@ -323,8 +396,7 @@ function isSchemaRecord(value: unknown): value is Record<string, SchemaLike> {
323
396
  }
324
397
 
325
398
  function getObjectShape(schema: SchemaLike): Record<string, SchemaLike> {
326
- const rawShape =
327
- typeof schema.shape === "function" ? schema.shape() : schema.shape;
399
+ const rawShape = typeof schema.shape === "function" ? schema.shape() : schema.shape;
328
400
  if (isSchemaRecord(rawShape)) {
329
401
  return rawShape;
330
402
  }
@@ -344,9 +416,7 @@ function getObjectShape(schema: SchemaLike): Record<string, SchemaLike> {
344
416
  return {};
345
417
  }
346
418
 
347
- function getChildSchemas(
348
- schema: SchemaLike,
349
- ): Array<{ key: string; schema: SchemaLike }> {
419
+ function getChildSchemas(schema: SchemaLike): Array<{ key: string; schema: SchemaLike }> {
350
420
  const seen = new Map<string, SchemaLike>();
351
421
  const def = getSchemaDef(schema);
352
422
 
@@ -450,10 +520,7 @@ function getSchemaMetadata(schema: SchemaLike): Record<string, unknown> {
450
520
  }
451
521
 
452
522
  function getSchemaDescriptionKey(schema: SchemaLike): string | undefined {
453
- const value = Reflect.get(
454
- getSchemaMetadata(schema),
455
- APIFUSE_DESCRIPTION_KEY_META_KEY,
456
- );
523
+ const value = Reflect.get(getSchemaMetadata(schema), APIFUSE_DESCRIPTION_KEY_META_KEY);
457
524
  return typeof value === "string" && value.length > 0 ? value : undefined;
458
525
  }
459
526
 
@@ -597,22 +664,14 @@ function collectSchemaDescriptionKeyDiagnostics(
597
664
  ? `${currentPath}[]`
598
665
  : `${currentPath}.${child.key}`;
599
666
  diagnostics.push(
600
- ...collectSchemaDescriptionKeyDiagnostics(
601
- child.schema,
602
- childPath,
603
- seen,
604
- !isStructuralNode,
605
- ),
667
+ ...collectSchemaDescriptionKeyDiagnostics(child.schema, childPath, seen, !isStructuralNode),
606
668
  );
607
669
  }
608
670
 
609
671
  return diagnostics;
610
672
  }
611
673
 
612
- function isComplexSchema(
613
- schema: unknown,
614
- seen = new Set<SchemaLike>(),
615
- ): boolean {
674
+ function isComplexSchema(schema: unknown, seen = new Set<SchemaLike>()): boolean {
616
675
  if (!isSchema(schema) || seen.has(schema)) {
617
676
  return false;
618
677
  }
@@ -624,10 +683,7 @@ function isComplexSchema(
624
683
  return childChildren.length > 0;
625
684
  });
626
685
 
627
- return (
628
- hasNestedComposite ||
629
- children.some(({ schema: child }) => isComplexSchema(child, seen))
630
- );
686
+ return hasNestedComposite || children.some(({ schema: child }) => isComplexSchema(child, seen));
631
687
  }
632
688
 
633
689
  function hasBidirectionalFixtures(fixtures: unknown): boolean {
@@ -638,16 +694,11 @@ function hasBidirectionalFixtures(fixtures: unknown): boolean {
638
694
  return "request" in fixtures && "response" in fixtures;
639
695
  }
640
696
 
641
- function getOperationSource(operation: {
642
- handler?: unknown;
643
- source?: string;
644
- }): string {
697
+ function getOperationSource(operation: { handler?: unknown; source?: string }): string {
645
698
  if (operation.source) {
646
699
  return operation.source;
647
700
  }
648
- return typeof operation.handler === "function"
649
- ? operation.handler.toString()
650
- : "";
701
+ return typeof operation.handler === "function" ? operation.handler.toString() : "";
651
702
  }
652
703
 
653
704
  function lintStealthTransportUsage(provider: {
@@ -660,22 +711,20 @@ function lintStealthTransportUsage(provider: {
660
711
  }
661
712
 
662
713
  const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
663
- return Object.entries(provider.operations).flatMap(
664
- ([operationKey, operation]) => {
665
- const source = getOperationSource(operation);
666
- if (!/\bctx\.stealth\b/.test(source)) {
667
- return [];
668
- }
669
- return [
670
- {
671
- rule: "stealth-config-required",
672
- level: "error" as const,
673
- field: `operations.${operationKey}`,
674
- message: `${providerLabel} operation "${operationKey}" uses ctx.stealth but provider.stealth is not declared.`,
675
- },
676
- ];
677
- },
678
- );
714
+ return Object.entries(provider.operations).flatMap(([operationKey, operation]) => {
715
+ const source = getOperationSource(operation);
716
+ if (!/\bctx\.stealth\b/.test(source)) {
717
+ return [];
718
+ }
719
+ return [
720
+ {
721
+ rule: "stealth-config-required",
722
+ level: "error" as const,
723
+ field: `operations.${operationKey}`,
724
+ message: `${providerLabel} operation "${operationKey}" uses ctx.stealth but provider.stealth is not declared.`,
725
+ },
726
+ ];
727
+ });
679
728
  }
680
729
 
681
730
  function lintCredentialWriteUsage(provider: {
@@ -685,24 +734,22 @@ function lintCredentialWriteUsage(provider: {
685
734
  return [];
686
735
  }
687
736
 
688
- return Object.entries(provider.operations).flatMap(
689
- ([operationKey, operation]) => {
690
- const source = getOperationSource(operation);
691
- if (!/\bctx\.credential\.(?:set|setMany)\s*\(/.test(source)) {
692
- return [];
693
- }
737
+ return Object.entries(provider.operations).flatMap(([operationKey, operation]) => {
738
+ const source = getOperationSource(operation);
739
+ if (!/\bctx\.credential\.(?:set|setMany)\s*\(/.test(source)) {
740
+ return [];
741
+ }
694
742
 
695
- return [
696
- {
697
- rule: "ctx-credential-write-forbidden-in-handler",
698
- level: "error" as const,
699
- field: `operations.${operationKey}.handler`,
700
- message:
701
- "Operation handlers must not mutate credentials; return refreshed credentials from auth.flow.refresh instead.",
702
- },
703
- ];
704
- },
705
- );
743
+ return [
744
+ {
745
+ rule: "ctx-credential-write-forbidden-in-handler",
746
+ level: "error" as const,
747
+ field: `operations.${operationKey}.handler`,
748
+ message:
749
+ "Operation handlers must not mutate credentials; return refreshed credentials from auth.flow.refresh instead.",
750
+ },
751
+ ];
752
+ });
706
753
  }
707
754
 
708
755
  function lintPlaywrightDirectImports(provider: {
@@ -724,9 +771,7 @@ function lintPlaywrightDirectImports(provider: {
724
771
  });
725
772
  }
726
773
 
727
- for (const [filePath, source] of Object.entries(
728
- provider.providerSourceFiles ?? {},
729
- )) {
774
+ for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
730
775
  if (!importPattern.test(source)) {
731
776
  continue;
732
777
  }
@@ -814,15 +859,11 @@ function lintSelfHostedBrowserPatterns(
814
859
  sources.push({ field: "auth.flow", source: provider.authFlowSource });
815
860
  }
816
861
 
817
- for (const [filePath, source] of Object.entries(
818
- provider.providerSourceFiles ?? {},
819
- )) {
862
+ for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
820
863
  sources.push({ field: `sourceFiles.${filePath}`, source });
821
864
  }
822
865
 
823
- for (const [operationKey, operation] of Object.entries(
824
- provider.operations ?? {},
825
- )) {
866
+ for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
826
867
  const source = getOperationSource(operation);
827
868
  if (source) {
828
869
  sources.push({
@@ -850,6 +891,303 @@ function lintSelfHostedBrowserPatterns(
850
891
  return diagnostics;
851
892
  }
852
893
 
894
+ const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(?:ProviderError|ValidationError)\s*\(/g;
895
+
896
+ const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
897
+
898
+ /**
899
+ * Skips a string literal starting at `startIndex` (which must point at the
900
+ * opening quote). Returns the index of the closing quote, or -1 when the
901
+ * literal is unterminated. Template literals handle nested `${...}`
902
+ * expressions, including strings inside them.
903
+ */
904
+ function skipStringLiteral(source: string, startIndex: number): number {
905
+ const quote = source[startIndex];
906
+ for (let index = startIndex + 1; index < source.length; index++) {
907
+ const char = source[index];
908
+ if (char === "\\") {
909
+ index++;
910
+ continue;
911
+ }
912
+ if (quote === "`" && char === "$" && source[index + 1] === "{") {
913
+ index = skipTemplateExpression(source, index + 2);
914
+ if (index < 0) {
915
+ return -1;
916
+ }
917
+ continue;
918
+ }
919
+ if (char === quote) {
920
+ return index;
921
+ }
922
+ if (quote !== "`" && char === "\n") {
923
+ return -1;
924
+ }
925
+ }
926
+ return -1;
927
+ }
928
+
929
+ function skipTemplateExpression(source: string, startIndex: number): number {
930
+ let depth = 1;
931
+ for (let index = startIndex; index < source.length; index++) {
932
+ const char = source[index];
933
+ if (char === '"' || char === "'" || char === "`") {
934
+ index = skipStringLiteral(source, index);
935
+ if (index < 0) {
936
+ return -1;
937
+ }
938
+ continue;
939
+ }
940
+ if (char === "{") {
941
+ depth++;
942
+ } else if (char === "}") {
943
+ depth--;
944
+ if (depth === 0) {
945
+ return index;
946
+ }
947
+ }
948
+ }
949
+ return -1;
950
+ }
951
+
952
+ /**
953
+ * Extracts the argument text of a call whose opening paren has already been
954
+ * consumed (`startIndex` points just past it). Returns undefined when the
955
+ * call never closes in this source, which the caller treats as "skip
956
+ * silently" — this scanner is conservative by design.
957
+ */
958
+ function extractBalancedCallArguments(source: string, startIndex: number): string | undefined {
959
+ let depth = 1;
960
+ for (let index = startIndex; index < source.length; index++) {
961
+ const char = source[index];
962
+ if (char === '"' || char === "'" || char === "`") {
963
+ index = skipStringLiteral(source, index);
964
+ if (index < 0) {
965
+ return undefined;
966
+ }
967
+ continue;
968
+ }
969
+ if (char === "/" && source[index + 1] === "/") {
970
+ const newline = source.indexOf("\n", index);
971
+ if (newline === -1) {
972
+ return undefined;
973
+ }
974
+ index = newline;
975
+ continue;
976
+ }
977
+ if (char === "/" && source[index + 1] === "*") {
978
+ const end = source.indexOf("*/", index + 2);
979
+ if (end === -1) {
980
+ return undefined;
981
+ }
982
+ index = end + 1;
983
+ continue;
984
+ }
985
+ if (char === "(") {
986
+ depth++;
987
+ } else if (char === ")") {
988
+ depth--;
989
+ if (depth === 0) {
990
+ return source.slice(startIndex, index);
991
+ }
992
+ }
993
+ }
994
+ return undefined;
995
+ }
996
+
997
+ /**
998
+ * Collects literal string values of top-level `code:` properties inside a
999
+ * ProviderError/ValidationError options object. Only plain `"..."` / `'...'`
1000
+ * literals at options-object depth count; computed codes (identifiers,
1001
+ * ternaries, template substitutions, concatenations, escapes) are skipped
1002
+ * silently so the rule never guesses.
1003
+ */
1004
+ function collectLiteralErrorCodeValues(args: string): string[] {
1005
+ const codes: string[] = [];
1006
+ let braceDepth = 0;
1007
+ let parenDepth = 0;
1008
+ let bracketDepth = 0;
1009
+ let previousSignificantChar = "";
1010
+ for (let index = 0; index < args.length; index++) {
1011
+ const char = args[index] ?? "";
1012
+ if (char === '"' || char === "'" || char === "`") {
1013
+ const end = skipStringLiteral(args, index);
1014
+ if (end < 0) {
1015
+ return codes;
1016
+ }
1017
+ index = end;
1018
+ previousSignificantChar = char;
1019
+ continue;
1020
+ }
1021
+ if (char === "/" && args[index + 1] === "/") {
1022
+ const newline = args.indexOf("\n", index);
1023
+ if (newline === -1) {
1024
+ return codes;
1025
+ }
1026
+ index = newline;
1027
+ continue;
1028
+ }
1029
+ if (char === "/" && args[index + 1] === "*") {
1030
+ const end = args.indexOf("*/", index + 2);
1031
+ if (end === -1) {
1032
+ return codes;
1033
+ }
1034
+ index = end + 1;
1035
+ continue;
1036
+ }
1037
+ if (/\s/.test(char)) {
1038
+ continue;
1039
+ }
1040
+ if (char === "{") {
1041
+ braceDepth++;
1042
+ } else if (char === "}") {
1043
+ braceDepth--;
1044
+ } else if (char === "(") {
1045
+ parenDepth++;
1046
+ } else if (char === ")") {
1047
+ parenDepth--;
1048
+ } else if (char === "[") {
1049
+ bracketDepth++;
1050
+ } else if (char === "]") {
1051
+ bracketDepth--;
1052
+ } else if (
1053
+ braceDepth === 1 &&
1054
+ parenDepth === 0 &&
1055
+ bracketDepth === 0 &&
1056
+ (previousSignificantChar === "{" || previousSignificantChar === ",") &&
1057
+ args.startsWith("code", index)
1058
+ ) {
1059
+ let cursor = index + "code".length;
1060
+ while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
1061
+ cursor++;
1062
+ }
1063
+ if (args[cursor] === ":") {
1064
+ cursor++;
1065
+ while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
1066
+ cursor++;
1067
+ }
1068
+ const quote = args[cursor];
1069
+ if (quote === '"' || quote === "'") {
1070
+ const end = skipStringLiteral(args, cursor);
1071
+ if (end > cursor) {
1072
+ const value = args.slice(cursor + 1, end);
1073
+ let after = end + 1;
1074
+ while (after < args.length && /\s/.test(args[after] ?? "")) {
1075
+ after++;
1076
+ }
1077
+ const nextChar = after < args.length ? (args[after] ?? "") : "";
1078
+ if (!value.includes("\\") && (nextChar === "," || nextChar === "}" || nextChar === "")) {
1079
+ codes.push(value);
1080
+ }
1081
+ index = end;
1082
+ previousSignificantChar = quote;
1083
+ continue;
1084
+ }
1085
+ return codes;
1086
+ }
1087
+ }
1088
+ }
1089
+ previousSignificantChar = char;
1090
+ }
1091
+ return codes;
1092
+ }
1093
+
1094
+ function collectLiteralThrownErrorCodes(source: string): string[] {
1095
+ const codes: string[] = [];
1096
+ THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = 0;
1097
+ for (
1098
+ let match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source);
1099
+ match;
1100
+ match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source)
1101
+ ) {
1102
+ const argsStart = match.index + match[0].length;
1103
+ const args = extractBalancedCallArguments(source, argsStart);
1104
+ if (args !== undefined) {
1105
+ codes.push(...collectLiteralErrorCodeValues(args));
1106
+ }
1107
+ THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = argsStart;
1108
+ }
1109
+ return codes;
1110
+ }
1111
+
1112
+ /**
1113
+ * Static counterpart of the runtime `unregistered_provider_error_code`
1114
+ * signal (honest-provider-error-contract Phase 3.5.5): flags
1115
+ * `new ProviderError(...)` / `new ValidationError(...)` constructions whose
1116
+ * literal `code` is neither SDK-registered (SDK_RUNTIME_OWNED_ERROR_CODES
1117
+ * plus the canonical status-mapped codes shared with serve.ts toStatusCode)
1118
+ * nor declared in any operation's docs.errorCodes. At runtime such a code
1119
+ * serves HTTP 500 and emits the signal; this rule surfaces it at check time.
1120
+ *
1121
+ * A throw site cannot be attributed to a specific operation statically —
1122
+ * providers routinely throw from helpers shared across operations — so this
1123
+ * rule matches against the provider-level union of declared codes. That is
1124
+ * the honest scope: it will not catch a code declared only on the "wrong"
1125
+ * operation, and it never claims per-operation attribution it cannot prove.
1126
+ * Only literal string codes are checked; computed/dynamic codes and test
1127
+ * sources are skipped silently. Warning level: the long tail of existing
1128
+ * providers converges gradually, so this must not fail `apifuse check`.
1129
+ */
1130
+ function lintUndeclaredThrownErrorCodes(provider: {
1131
+ authFlowSource?: string;
1132
+ providerSourceFiles?: Record<string, string>;
1133
+ operations?: Record<
1134
+ string,
1135
+ {
1136
+ handler?: unknown;
1137
+ source?: string;
1138
+ docs?: { errorCodes?: ReadonlyArray<{ code: string }> };
1139
+ }
1140
+ >;
1141
+ }): LintDiagnostic[] {
1142
+ const knownCodes = new Set<string>([
1143
+ ...SDK_RUNTIME_OWNED_ERROR_CODES,
1144
+ ...SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.keys(),
1145
+ ]);
1146
+ for (const operation of Object.values(provider.operations ?? {})) {
1147
+ for (const entry of operation.docs?.errorCodes ?? []) {
1148
+ if (typeof entry?.code === "string") {
1149
+ knownCodes.add(entry.code);
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ const sources: Array<{ field: string; source: string }> = [];
1155
+ const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
1156
+ ([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath),
1157
+ );
1158
+ if (sourceFiles.length > 0) {
1159
+ for (const [filePath, source] of sourceFiles) {
1160
+ sources.push({ field: `sourceFiles.${filePath}`, source });
1161
+ }
1162
+ } else {
1163
+ if (provider.authFlowSource) {
1164
+ sources.push({ field: "auth.flow", source: provider.authFlowSource });
1165
+ }
1166
+ for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
1167
+ const source = getOperationSource(operation);
1168
+ if (source) {
1169
+ sources.push({ field: `operations.${operationKey}.handler`, source });
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ const diagnostics: LintDiagnostic[] = [];
1175
+ for (const { field, source } of sources) {
1176
+ const undeclaredCodes = new Set(
1177
+ collectLiteralThrownErrorCodes(source).filter((code) => !knownCodes.has(code)),
1178
+ );
1179
+ for (const code of undeclaredCodes) {
1180
+ diagnostics.push({
1181
+ rule: "thrown-error-code-undeclared",
1182
+ level: "warn",
1183
+ field,
1184
+ message: `Thrown error code "${code}" (${field}) is neither SDK-registered nor declared in any operation's docs.errorCodes; at runtime it serves HTTP 500 and emits the unregistered_provider_error_code signal. Declare it in the owning operation's docs.errorCodes with status and retryable.`,
1185
+ });
1186
+ }
1187
+ }
1188
+ return diagnostics;
1189
+ }
1190
+
853
1191
  export function lintOperation(op: {
854
1192
  description?: string;
855
1193
  descriptionKey?: string;
@@ -865,16 +1203,14 @@ export function lintOperation(op: {
865
1203
  }): LintDiagnostic[] {
866
1204
  const diagnostics: LintDiagnostic[] = [];
867
1205
  const description = op.description ?? "";
868
- const hasDescriptionKey =
869
- typeof op.descriptionKey === "string" && op.descriptionKey.length > 0;
1206
+ const hasDescriptionKey = typeof op.descriptionKey === "string" && op.descriptionKey.length > 0;
870
1207
 
871
1208
  if (description.trim().length > 0 && !hasDescriptionKey) {
872
1209
  diagnostics.push({
873
1210
  rule: "operation-description-raw-prose",
874
1211
  level: "error",
875
1212
  field: "description",
876
- message:
877
- "Operation description must use descriptionKey instead of raw static prose.",
1213
+ message: "Operation description must use descriptionKey instead of raw static prose.",
878
1214
  });
879
1215
  }
880
1216
 
@@ -892,21 +1228,16 @@ export function lintOperation(op: {
892
1228
  rule: "operation-when-to-use-raw-prose",
893
1229
  level: "error",
894
1230
  field: "whenToUse",
895
- message:
896
- "Operation whenToUse must use whenToUseKeys instead of raw static prose.",
1231
+ message: "Operation whenToUse must use whenToUseKeys instead of raw static prose.",
897
1232
  });
898
1233
  }
899
1234
 
900
- if (
901
- (op.whenNotToUse?.length ?? 0) > 0 &&
902
- !(op.whenNotToUseKeys?.length ?? 0)
903
- ) {
1235
+ if ((op.whenNotToUse?.length ?? 0) > 0 && !(op.whenNotToUseKeys?.length ?? 0)) {
904
1236
  diagnostics.push({
905
1237
  rule: "operation-when-not-to-use-raw-prose",
906
1238
  level: "error",
907
1239
  field: "whenNotToUse",
908
- message:
909
- "Operation whenNotToUse must use whenNotToUseKeys instead of raw static prose.",
1240
+ message: "Operation whenNotToUse must use whenNotToUseKeys instead of raw static prose.",
910
1241
  });
911
1242
  }
912
1243
 
@@ -942,14 +1273,11 @@ export function lintOperation(op: {
942
1273
  rule: "complex-input-has-examples",
943
1274
  level: "warn",
944
1275
  field: "inputExamples",
945
- message:
946
- "Complex input schemas should provide at least 2 input examples.",
1276
+ message: "Complex input schemas should provide at least 2 input examples.",
947
1277
  });
948
1278
  }
949
1279
 
950
- for (const field of uniqueFields(
951
- collectUnmarkedSensitiveFields(op.input, "input"),
952
- )) {
1280
+ for (const field of uniqueFields(collectUnmarkedSensitiveFields(op.input, "input"))) {
953
1281
  diagnostics.push({
954
1282
  rule: "sensitive-field-unmarked",
955
1283
  level: "warn",
@@ -958,9 +1286,7 @@ export function lintOperation(op: {
958
1286
  });
959
1287
  }
960
1288
 
961
- for (const field of uniqueFields(
962
- collectUnmarkedSensitiveFields(op.output, "output"),
963
- )) {
1289
+ for (const field of uniqueFields(collectUnmarkedSensitiveFields(op.output, "output"))) {
964
1290
  diagnostics.push({
965
1291
  rule: "sensitive-field-unmarked",
966
1292
  level: "warn",
@@ -1004,6 +1330,7 @@ export function lintProvider(
1004
1330
  derivations?: Record<string, string>;
1005
1331
  handler?: unknown;
1006
1332
  source?: string;
1333
+ docs?: { errorCodes?: ReadonlyArray<{ code: string }> };
1007
1334
  }
1008
1335
  >;
1009
1336
  meta?: {
@@ -1021,18 +1348,26 @@ export function lintProvider(
1021
1348
  ...lintCredentialWriteUsage(provider),
1022
1349
  ...lintPlaywrightDirectImports(provider),
1023
1350
  ...lintSelfHostedBrowserPatterns(provider, options),
1351
+ ...lintUndeclaredThrownErrorCodes(provider),
1024
1352
  ];
1025
1353
 
1026
1354
  if (provider.operations) {
1027
1355
  const authMode = provider.auth?.mode;
1028
- if (authMode === "credentials" || authMode === "oauth2") {
1356
+ // Every authenticated mode owns an auth.flow; `oauth2_proxied` was
1357
+ // previously exempt, which let auth-lifecycle operations ship on
1358
+ // proxied providers unchecked.
1359
+ if (
1360
+ authMode === "credentials" ||
1361
+ authMode === "oauth2" ||
1362
+ authMode === "oauth2_proxied"
1363
+ ) {
1029
1364
  for (const operationKey of Object.keys(provider.operations)) {
1030
- if (AUTH_OPERATION_ID_PATTERN.test(operationKey)) {
1365
+ if (isAuthLifecycleOperationId(operationKey, authMode)) {
1031
1366
  diagnostics.push({
1032
1367
  rule: "auth-operation-unsupported",
1033
1368
  level: "error",
1034
1369
  field: `operations.${operationKey}`,
1035
- message: `Provider "${provider.id ?? "unknown"}" operation "${operationKey}" looks like a login/token/session exchange endpoint. Authenticated providers must expose login through the single auth.flow interface because Gateway persists only auth.flow complete turn data.credential as the connection credential. Move this logic into auth.flow.continue instead of a provider operation.`,
1370
+ message: `Provider "${provider.id ?? "unknown"}" operation "${operationKey}" performs an auth-lifecycle action (login, logout, token exchange or similar). Authenticated providers must expose the whole credential lifecycle through the single auth.flow interface because Gateway persists only auth.flow complete turn data.credential as the connection credential, and an operation that mutates the session outside that interface leaves the stored connection stale. Move sign-in logic into auth.flow.start/continue and sign-out/disconnect logic into auth.flow.abort (served by POST /auth/disconnect) instead of a provider operation.`,
1036
1371
  });
1037
1372
  }
1038
1373
  }
@@ -1044,36 +1379,35 @@ export function lintProvider(
1044
1379
  }
1045
1380
 
1046
1381
  diagnostics.push(
1047
- ...Object.entries(provider.operations).flatMap(
1048
- ([operationKey, operation]) =>
1049
- [
1050
- ...lintOperation({
1051
- description: operation.description ?? "",
1052
- descriptionKey: operation.descriptionKey,
1053
- whenToUse: operation.whenToUse,
1054
- whenToUseKeys: operation.whenToUseKeys,
1055
- whenNotToUse: operation.whenNotToUse,
1056
- whenNotToUseKeys: operation.whenNotToUseKeys,
1057
- input: operation.input,
1058
- output: operation.output,
1059
- fixtures: operation.fixtures,
1060
- inputExamples: operation.inputExamples,
1061
- derivations: operation.derivations,
1062
- }),
1063
- ...lintPublicSchemaFieldNames(
1064
- provider.id,
1065
- operationKey,
1066
- operation.input,
1067
- operation.output,
1068
- provider.meta?.contract?.publicSchemaFieldNames === "normalized",
1069
- ),
1070
- ].map((diagnostic) => ({
1071
- ...diagnostic,
1072
- field: diagnostic.field
1073
- ? `operations.${operationKey}.${diagnostic.field}`
1074
- : `operations.${operationKey}`,
1075
- message: `[${operationKey}] ${diagnostic.message}`,
1076
- })),
1382
+ ...Object.entries(provider.operations).flatMap(([operationKey, operation]) =>
1383
+ [
1384
+ ...lintOperation({
1385
+ description: operation.description ?? "",
1386
+ descriptionKey: operation.descriptionKey,
1387
+ whenToUse: operation.whenToUse,
1388
+ whenToUseKeys: operation.whenToUseKeys,
1389
+ whenNotToUse: operation.whenNotToUse,
1390
+ whenNotToUseKeys: operation.whenNotToUseKeys,
1391
+ input: operation.input,
1392
+ output: operation.output,
1393
+ fixtures: operation.fixtures,
1394
+ inputExamples: operation.inputExamples,
1395
+ derivations: operation.derivations,
1396
+ }),
1397
+ ...lintPublicSchemaFieldNames(
1398
+ provider.id,
1399
+ operationKey,
1400
+ operation.input,
1401
+ operation.output,
1402
+ provider.meta?.contract?.publicSchemaFieldNames === "normalized",
1403
+ ),
1404
+ ].map((diagnostic) => ({
1405
+ ...diagnostic,
1406
+ field: diagnostic.field
1407
+ ? `operations.${operationKey}.${diagnostic.field}`
1408
+ : `operations.${operationKey}`,
1409
+ message: `[${operationKey}] ${diagnostic.message}`,
1410
+ })),
1077
1411
  ),
1078
1412
  );
1079
1413