@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/AUTHORING.md CHANGED
@@ -136,6 +136,75 @@ Hosted runtime settings are engine-owned as well: every `APIFUSE__CDP_POOL__*`
136
136
  variable, the Cloudflare OCR/STT tokens and account identifier,
137
137
  `APIFUSE__OCR__API_KEY`, and `APIFUSE__CACHE__KEY_PEPPER`. `defineProvider`
138
138
  rejects them in `secrets`; the engine reads them from its own environment.
139
+ Provider code must not probe those names either — to learn whether OCR is
140
+ configured, read `ctx.ocr.available` (see the OCR section below).
141
+
142
+ ### Engine mode
143
+
144
+ Provider processes attach their capability bindings through an engine. Today the
145
+ default is the in-process engine; ADR-0011 replaces it with the authenticated
146
+ remote engine in a phased migration, and `serve` logs every boot as
147
+ `provider_engine_mode` (`deprecated: true` for in-process). You do not need to
148
+ change anything now.
149
+
150
+ A deployment names the mode with the engine-owned `APIFUSE__ENGINE__MODE`
151
+ variable (`in-process` or `remote`; trimmed and case-folded, blank means unset);
152
+ hosts may pass `engineMode` to `serve`/`createServerApp`/`startDevServer`
153
+ instead. The manifest wins when the two disagree, so provider code cannot
154
+ silently downgrade what the deployment asked for; the disagreement is reported in
155
+ the boot event's `warnings`. An unrecognized value is warned about and ignored —
156
+ it never crashes the process. `APIFUSE__ENGINE__MODE` cannot be declared in
157
+ `secrets` (`APIFUSE__ENGINE__*` is engine-owned).
158
+
159
+ `remote` has no client in this release: the server attaches an engine that fails
160
+ every request with `PROVIDER_ENGINE_MODE_UNSUPPORTED` and reports `503` on
161
+ `/readyz`. It never falls back to in-process. `apifuse dev` and `apifuse record`
162
+ refuse to start instead.
163
+
164
+ `GET /health` is liveness only and stays engine-blind; `GET /readyz` reports the
165
+ resolved engine attachment and is what readiness/startup probes should use — but
166
+ only for a provider already pinned to an SDK release that serves the route.
167
+ Earlier pins answer `404`, and a failing readiness/startup probe is not
168
+ self-healing, so manifests move to `/readyz` after the pin wave, never before.
169
+
170
+ ### Deployment intent
171
+
172
+ Deployment intent (runtime profile, resources, HPA, Redis, extra TCP ports) has
173
+ exactly one authored home: the optional `deployment` key inside
174
+ `defineProvider({...})`. The SDK passes it through verbatim; the APIFuse
175
+ registry resolves omitted fields from the runtime profile and rejects anything
176
+ it cannot resolve.
177
+
178
+ - Declare only what differs from the profile. Profile defaults:
179
+ `runtime: "shared"`, `language: "typescript"`, `replicas: 1`,
180
+ `hpa: { enabled: true, minReplicas: 1, maxReplicas: 1, targetCPUUtilizationPercentage: 70 }`,
181
+ resources `25m/128Mi` (shared) or `200m/256Mi` (browser), `buildContext: "."`.
182
+ Most providers therefore declare no `deployment` key at all.
183
+ - The deployment `runtime` axis (`shared | dedicated | browser`) is distinct
184
+ from the execution `runtime` (`standard | shared | browser`) declared next to
185
+ it.
186
+ - Do not author a standalone `deploy.ts`, a `deployment` extra on a spread
187
+ default export (`export default { ...provider, deployment }`), or a
188
+ re-exported config module. A repository that still carries `deploy.ts`
189
+ migrates with `apifuse migrate-deployment .`: it hoists the file's non-default
190
+ values into the declaration, proves the result resolves identically, and
191
+ deletes the file. The `/deploy.ts` CODEOWNERS entry stays until the platform
192
+ stops honoring a re-added legacy file (`--drop-codeowners-lock` then). A
193
+ refusal names the manual step.
194
+ - `apifuse check` warns during the migration window
195
+ (`deployment/legacy-deploy-file`, `deployment/spread-export`,
196
+ `deployment/redundant-default`); the rules become errors once no repository
197
+ carries the legacy file.
198
+
199
+ ```ts
200
+ const buildProvider = defineProvider({
201
+ id: "example",
202
+ version: "1.0.0",
203
+ runtime: "standard",
204
+ deployment: { cache: { redis: { enabled: true } } },
205
+ // ...
206
+ });
207
+ ```
139
208
 
140
209
  ### Factored operations
141
210
 
@@ -388,6 +457,40 @@ Best-practice rules:
388
457
  - Submission checks and health checks must not invoke live STT by default.
389
458
  Provide explicit smoke evidence when a provider depends on audio OTP behavior.
390
459
 
460
+ ### OCR runtime capability: availability is the SDK's verdict
461
+
462
+ Declare `ocr: { mode: "required" | "optional" }` and call `ctx.ocr` from
463
+ operation handlers or auth-flow handlers. Whether a working backend is wired
464
+ in is decided by the SDK, never by provider code: `ctx.ocr.available` is
465
+ `true` when `createOcrClientFromEnv` found a configured backend and `false`
466
+ for the unsupported/error client that undeclared or misconfigured providers
467
+ receive. Branch on that flag for behavior that depends on OCR being present
468
+ (for example, whether a CAPTCHA field is required from the user), and keep the
469
+ `OCR_UNAVAILABLE` / `UNSUPPORTED_OCR_BACKEND` error fallback for the call
470
+ itself: `available: true` promises a configured backend, not a successful
471
+ upstream call.
472
+
473
+ ```ts
474
+ async start(ctx) {
475
+ const image = await fetchCaptcha(ctx);
476
+ return ctx.auth.nextForm({
477
+ turnId: "credentials",
478
+ fields: {
479
+ captchaText: { type: "string", required: !ctx.ocr.available, /* ... */ },
480
+ },
481
+ data: { captchaImage: image, ocrConfigured: ctx.ocr.available },
482
+ });
483
+ }
484
+ ```
485
+
486
+ Do not re-implement the predicate by reading `APIFUSE__OCR__*` or
487
+ `APIFUSE__CLOUDFLARE__ACCOUNT_ID`: those names are engine-owned, the
488
+ runtime-boundary lint reports the read, and a drift between the copy and the
489
+ SDK's own selection logic downgrades every login to the manual path silently.
490
+
491
+ A custom `OcrContext` override (`serve({ ocr })`, `createFlowContext({ ocr })`,
492
+ or a hand-written test double) must set `available` explicitly as a plain data
493
+ property; the SDK's own clients do, and `bindOcrTelemetry` preserves it.
391
494
 
392
495
  ### Health journey DX for SMS/payment flows
393
496
 
@@ -536,18 +639,20 @@ operation:
536
639
 
537
640
  ```ts
538
641
  errorCodes: [{
539
- code: "UPSTREAM_SCHEMA_ERROR",
540
- status: 502,
541
- retryable: true,
542
- description: "The upstream response no longer matches its schema.",
642
+ code: "ITEM_SOLD_OUT",
643
+ status: 409,
644
+ retryable: false,
645
+ description: "The upstream sold the item before checkout completed.",
543
646
  }],
544
647
  handler: async () => {
545
- throw new ProviderError("Upstream schema changed", {
546
- code: "UPSTREAM_SCHEMA_ERROR",
547
- });
648
+ throw new ProviderError("Item sold out", { code: "ITEM_SOLD_OUT" });
548
649
  },
549
650
  ```
550
651
 
652
+ Declare only codes the SDK does not already register. `UPSTREAM_SCHEMA_ERROR`,
653
+ `UPSTREAM_AUTH_ERROR`, and `INVALID_REQUEST` are registered below, so throwing
654
+ them needs no declaration at all.
655
+
551
656
  `defineProvider` accepts only statuses the server can emit: 400, 401, 404, 429,
552
657
  500, 502, 503, and 504. Invalid declared statuses fail provider definition,
553
658
  not a live request. Status selection uses this order:
@@ -567,10 +672,10 @@ The registered mappings are:
567
672
  | Error code or fallback | HTTP status |
568
673
  | --- | ---: |
569
674
  | `AUTH_REQUIRED`, `reauth_required` | 401 |
570
- | `MISSING_SECRET` | 400 |
675
+ | `MISSING_SECRET`, `UPSTREAM_AUTH_ERROR`, `INVALID_REQUEST` | 400 |
571
676
  | `NOT_FOUND`, `not_found`, `NO_DATA` | 404 |
572
677
  | `RATE_LIMITED`, `UPSTREAM_RATE_LIMIT`, `LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR` | 429 |
573
- | `UPSTREAM_ERROR`, `BLOCKED` | 502 |
678
+ | `UPSTREAM_ERROR`, `BLOCKED`, `UPSTREAM_SCHEMA_ERROR` | 502 |
574
679
  | `STT_UNAVAILABLE`, `UNSUPPORTED_STT_BACKEND`, `STATEFUL_FORWARDING_REPLAY_CACHE_FULL` | 503 |
575
680
  | Unregistered input `ValidationError` code | 400 |
576
681
  | Other unregistered `ProviderError` code | 500 |
@@ -587,6 +692,154 @@ Throw the domain `ProviderError` directly. Subclassing or wrapping it as a
587
692
  domain code's `status` instead. Genuine `TransportError` values remain
588
693
  SDK-owned and keep their 502/504 mapping.
589
694
 
695
+ #### Three fleet-consensus codes are registered
696
+
697
+ `UPSTREAM_AUTH_ERROR` (400), `UPSTREAM_SCHEMA_ERROR` (502), and
698
+ `INVALID_REQUEST` (400) are registered mappings. All three are non-retryable.
699
+
700
+ - `UPSTREAM_AUTH_ERROR` is 400, not 401 or 502, because it means the upstream
701
+ refused a *platform-managed* service key. The caller holds no credential, so
702
+ 401 ("re-authenticate") asks for something the caller cannot do, and 502
703
+ ("the upstream is sick") promises a recovery that will never arrive. It is a
704
+ deployment/config defect, exactly like `MISSING_SECRET`.
705
+ - `UPSTREAM_SCHEMA_ERROR` is non-retryable: a retry returns the same payload
706
+ the provider already could not normalize.
707
+ - `INVALID_REQUEST` is the spelling for caller-side bad input. `INVALID_INPUT`,
708
+ `VALIDATION_ERROR`, and `BAD_REQUEST` are not registered.
709
+
710
+ Like `UPSTREAM_ERROR` and `BLOCKED`, these are thrown by provider code rather
711
+ than by the SDK, so they are registered mappings and not SDK-owned codes: an
712
+ existing operation declaration still wins at runtime (step 2 above). Registering
713
+ them therefore never rewrites a status you already declared. What it does do is
714
+ let you delete the declaration — and when your declaration disagrees with the
715
+ registered mapping, `apifuse check` reports it rather than leaving the fleet
716
+ answering one code two ways:
717
+
718
+ - `error-code-status-conflicts-sdk` — declared `status` differs from the
719
+ registered status.
720
+ - `error-code-retryable-conflicts-sdk` — declared `retryable` differs from the
721
+ registered retryability.
722
+
723
+ Both are warnings. Drop the conflicting field so the SDK supplies the value, or,
724
+ if your operation genuinely means something else, throw a distinct code that
725
+ says so.
726
+
727
+ ### Localized error messages (`messageKey` / `fixKey`)
728
+
729
+ The positional `ProviderError` message stays **English**. It is what
730
+ `error.message`, cause frames, the `provider_request_failed` log, OTLP
731
+ attributes and tests see. Only two fields of the response body are localized:
732
+ `error.message` and `error.fix`. Nothing else — not `code`, not `details`, not
733
+ `errorCodes[].description`, not the `X-ApiFuse-Error-Observability` header.
734
+
735
+ Attach locale keys instead of parsing `Accept-Language` yourself:
736
+
737
+ ```ts
738
+ // locales/en.json
739
+ { "errors": { "upstreamSchema": {
740
+ "message": "{provider} returned an unexpected response.",
741
+ "fix": "Retry in a few minutes."
742
+ } } }
743
+ ```
744
+
745
+ ```ts
746
+ throw new ProviderError("The culture data service returned an unexpected response.", {
747
+ code: "UPSTREAM_SCHEMA_ERROR",
748
+ messageKey: "errors.upstreamSchema.message",
749
+ fixKey: "errors.upstreamSchema.fix",
750
+ params: { provider: "Culture Data" },
751
+ });
752
+ ```
753
+
754
+ Keys can also live on the declaration, which localizes **every** site that
755
+ throws that code without touching the throw sites:
756
+
757
+ ```ts
758
+ errorCodes: [{
759
+ code: "UPSTREAM_SCHEMA_ERROR",
760
+ description: "The upstream response no longer matches its schema.",
761
+ messageKey: "errors.upstreamSchema.message",
762
+ fixKey: "errors.upstreamSchema.fix",
763
+ }],
764
+ ```
765
+
766
+ `status` and `retryable` are omitted because `UPSTREAM_SCHEMA_ERROR` is a
767
+ registered code: the SDK already supplies 502 and non-retryable, and declaring
768
+ a different value trips `error-code-status-conflicts-sdk` /
769
+ `error-code-retryable-conflicts-sdk`. A declaration that carries only keys is
770
+ still worth writing — it localizes every site that throws the code.
771
+
772
+ Resolution order per field, evaluated at serve time:
773
+
774
+ 1. the throw site's `messageKey` / `fixKey`;
775
+ 2. the matching `errorCodes[]` entry's `messageKey` / `fixKey`;
776
+ 3. the derived `errors.<code>.message` / `errors.<code>.fix` (the code is used
777
+ verbatim as a catalog segment, so `errors.UPSTREAM_SCHEMA_ERROR.message`
778
+ works with no declaration at all);
779
+ 4. the English literal passed to the constructor.
780
+
781
+ Each candidate is resolved in the caller's locale first and then in `en`, so a
782
+ more specific key always beats a more specific locale. A missing key, a
783
+ malformed key, a non-string catalog value or an absent catalog is a miss, never
784
+ a throw: the caller always gets text. Steps 2 and 3 do not apply to SDK-owned
785
+ failures (transport, Zod, stateful deadline, SDK runtime codes) — a provider
786
+ catalog must not be able to relabel `Request timed out`.
787
+
788
+ **Key grammar.** A locale key is dot-separated segments. Every segment is
789
+ camelCase (`upstreamSchema`, `whenToUse`) or an array index (`0`), and the
790
+ first segment is always a camelCase namespace. The one exception is the segment
791
+ directly under `errors`, which may also be the error code spelled the way it is
792
+ thrown — `errors.UPSTREAM_SCHEMA_ERROR.message`, `errors.BLOCKED.fix`,
793
+ `errors.reauth_required.message`. Both spellings work, so
794
+ `errors.upstreamSchema.message` and `errors.UPSTREAM_SCHEMA_ERROR.message` are
795
+ equally legal keys; write the code form when you want the key to match the
796
+ derived path in step 3. Nothing else loosens: a space, a dot inside a segment,
797
+ an empty segment, a mixed-case or hyphenated code, and a leading digit are all
798
+ still malformed.
799
+
800
+ Locale negotiation reads `Accept-Language` from the request envelope's
801
+ `headers` map first (the gateway stamps the flow's locale there) and then from
802
+ the HTTP header, honours quality values, matches on the primary subtag, and
803
+ supports `en`, `ko`, `ja` with `en` as the default. A request with no
804
+ `Accept-Language` therefore keeps serving the `en` catalog value, so existing
805
+ contract fixtures do not churn.
806
+
807
+ **Interpolation.** `{name}` placeholders, `{{` and `}}` for literal braces.
808
+ `params` accepts strings and finite numbers only. A placeholder with no usable
809
+ param is left verbatim so the authoring gap is visible rather than silently
810
+ deleting text.
811
+
812
+ **Params are untrusted.** Upstream error text routinely ends up in `params`, so
813
+ every string value is scrubbed before substitution: secrets and e-mail
814
+ addresses redacted, control/bidi/newline characters encoded or collapsed,
815
+ `<`, `>` and backticks removed, HTML-entity ampersands (`&lt;`) neutralized
816
+ while ordinary ampersands survive, and the value capped at 200 characters.
817
+ Substituted text is never rescanned, so a value containing `{other}` cannot
818
+ trigger a second interpolation round. Prefer `details` when you need to relay
819
+ an upstream body verbatim; `params` is for short, bounded values.
820
+
821
+ **Catalog loading.** Catalogs are read once when the server is built (from the
822
+ provider directory's `locales/{en,ko,ja}.json`, loading only the locales that
823
+ exist). Pass `serve(provider, { localeCatalogs })` to bundle them instead. A
824
+ catalog that exists but cannot be parsed logs one
825
+ `provider_locale_catalogs_unavailable` warn at boot and every field falls back
826
+ to its English literal.
827
+
828
+ **Lint.** `apifuse check` reports:
829
+
830
+ - `error-locale-key-missing` / `error-locale-key-malformed` (**error**) — a
831
+ literal `messageKey`/`fixKey`, on a throw site or on an `errorCodes[]` entry,
832
+ that `locales/en.json` does not resolve to non-empty text, or that is not a
833
+ legal locale dot path (see **Key grammar** above).
834
+ - `thrown-error-message-not-localized` (**warn**) — a throw site whose declared
835
+ code has no key anywhere and no `errors.<code>.message` in `en`, so it is
836
+ served untranslated. Warning during the migration; promoted to error per
837
+ provider as each wave lands.
838
+
839
+ `ko`/`ja` coverage needs no new rule: `en` is the baseline, so the existing
840
+ catalog parity and placeholder checks already fail an `errors.*` key that is
841
+ missing or untranslated in `ko`/`ja`.
842
+
590
843
  ### Declared secrets are SDK-enforced
591
844
 
592
845
  Environment/secret presence validation is single-sourced in the SDK. Declare
@@ -640,6 +893,69 @@ Note the asymmetry: the gate treats whitespace-only values as missing, but
640
893
  `ctx.env.get()` still returns the raw value to handlers — trim at the point of
641
894
  use if the upstream is whitespace-sensitive.
642
895
 
896
+ ### Runtime boundary: no `process.env`, `node:fs`, `child_process`, or raw `fetch()`
897
+
898
+ Provider runtime code reaches the outside world through the context only:
899
+ `ctx.http` / `ctx.stealth` for HTTP, `ctx.browser` for browser work,
900
+ `ctx.env.get()` / `ctx.credential` for configuration and secrets, `ctx.files` /
901
+ `ctx.cache` / `ctx.state` for data that must outlive a request. Ambient Node
902
+ and Bun globals bypass `allowedHosts`, proxy policy, retries, redaction, the
903
+ secret presence gate, and telemetry. `apifuse check` reports them (**warn** in
904
+ this release; the level is raised once the fleet is clean):
905
+
906
+ - `process-env-direct-read` — `process.env.X`, `process.env["X"]`, a bare
907
+ `process.env` object use, `Bun.env`. The `APIFUSE__RUNTIME__*` bootstrap
908
+ family (`APIFUSE__RUNTIME__PORT`, `…POD_ID`, `…POD_ENDPOINT`) is exempt
909
+ everywhere; every other value is declared in `defineProvider` under
910
+ `secrets: [{ name, required }]` and read with `ctx.env.get(name)`. The names
911
+ visible to `ctx.env.get` are exactly the declared `secrets[].name` (minus
912
+ engine-owned names; an `oauth2_proxied` auth flow additionally sees
913
+ `APIFUSE__AUTH_PROXY__URL`). `env: true` (or `env: {}`) is the capability
914
+ declaration that turns the `ctx.env` binding on — a type *and* runtime gate,
915
+ since touching `ctx.env` without it throws `PROVIDER_CAPABILITY_UNDECLARED`
916
+ — and does not widen the readable names. Test/E2E toggles are not secrets
917
+ and cannot move to `ctx.env`: replace them with test doubles (inject
918
+ `ctx.env` / `ctx.http`) or recorded fixtures.
919
+ - `node-runtime-module-import` — value imports of `fs`, `fs/promises`, `net`,
920
+ `tls`, `dgram`, `http`, `https`, `http2`, `child_process` (with or without
921
+ `node:`), and `Bun.spawn` / `Bun.spawnSync` / ``Bun.$` ` `` / `Bun.file` /
922
+ `Bun.write`. Type-only imports (`import type`, or every binding inline `type`) are ignored; `node:path`, `node:crypto`,
923
+ `node:url` are fine.
924
+ - `direct-fetch-call` — the global `fetch()` (also `globalThis.fetch`).
925
+ `ctx.stealth.fetch()` never matches, nor does a call to a `fetch` binding
926
+ declared in an enclosing scope (parameter, variable, import, catch clause).
927
+ The check is lexical: a wrapper that takes `fetch` as a parameter does not
928
+ hide a global `fetch()` in another function of the same file.
929
+
930
+ Scope is runtime source only: tests, recorded fixtures, `.d.ts`, the root
931
+ `dev.ts` / `start.ts` / `deploy.ts` entrypoints, and the `scripts/`, `tools/`,
932
+ `bin/` directories at the provider root are not scanned. Keep fixture
933
+ recorders, smoke scripts, and other operator tooling under `scripts/`, and do
934
+ not import from there in request-path modules.
935
+
936
+ Acknowledge a deliberate exception on the finding line or the comment-only
937
+ line above it with `// @apifuse-allow <rule>: <reason>`; the finding then
938
+ appears as an `INFO` audit line instead of a warning.
939
+
940
+ ```ts
941
+ // Before: request-path module reads the ambient environment and the disk
942
+ import { mkdir, writeFile } from "node:fs/promises";
943
+ const serviceKey = process.env.EXAMPLE_SERVICE_KEY;
944
+ if (process.env.EXAMPLE_DIAG_TAP_DIR) {
945
+ await writeFile(`${process.env.EXAMPLE_DIAG_TAP_DIR}/booking.json`, JSON.stringify(response));
946
+ }
947
+
948
+ // After: the credential is a declared secret read through the context —
949
+ // defineProvider({
950
+ // env: true, // turns the ctx.env capability on (type + runtime gate)
951
+ // secrets: [{ name: "APIFUSE__PROVIDER__EXAMPLE__SERVICE_KEY", required: true }], // the readable names
952
+ // })
953
+ // — and the ad-hoc diagnostic tap is deleted: a toggle that is not a secret has
954
+ // no place in ctx.env; observability goes through ctx.trace instead.
955
+ const serviceKey = ctx.env.get("APIFUSE__PROVIDER__EXAMPLE__SERVICE_KEY");
956
+ const response = await ctx.trace.span("booking.confirm", () => confirmBooking(ctx, serviceKey));
957
+ ```
958
+
643
959
  ### Credentials forced into query parameters
644
960
 
645
961
  Prefer an authorization header or request body whenever the upstream supports
@@ -678,6 +994,48 @@ redacted. If a login flow must consume a rotated credential from `Location`,
678
994
  inspect it inside `stopWhen`; that callback receives the real hop while callback
679
995
  failures are sanitized before propagation.
680
996
 
997
+ ### Request-bound proof headers
998
+
999
+ When an upstream requires a single-use, request-bound header (DPoP proofs,
1000
+ HTTP message signatures, HMAC nonces), pass `headers` as a factory instead of a
1001
+ record. `ctx.http` calls it once per issued attempt with the resolved URL and
1002
+ method, so a managed retry sends a fresh proof rather than replaying the first
1003
+ one, and the request keeps proxy failover and the transient retry taxonomy:
1004
+
1005
+ ```ts
1006
+ const response = await ctx.http.get(requestUrl, {
1007
+ headers: async ({ url, method }) => {
1008
+ // RFC 9449 §4.2: `htu` is the target URI without query and fragment.
1009
+ const htu = new URL(url);
1010
+ htu.search = "";
1011
+ htu.hash = "";
1012
+ return {
1013
+ ...commonHeaders,
1014
+ dpop: await createDpopProof(htu.toString(), method),
1015
+ };
1016
+ },
1017
+ });
1018
+ ```
1019
+
1020
+ `url` is the exact URL the attempt is issued against, including `params` and
1021
+ `sensitiveParams`. Bind to it instead of a URL you rebuild, but strip the query
1022
+ and fragment for fields that are defined without them (DPoP `htu`), and never
1023
+ log it or copy it verbatim into a signed field a verifier logs.
1024
+
1025
+ Keep the factory fast and local. It runs after proxy resolution, so an already
1026
+ allocated egress endpoint and its lease are held while it is awaited; a remote
1027
+ signing round trip there can outlive the lease TTL. Mint locally, and set
1028
+ `timeout` on the call so a hung signer cannot hold the endpoint indefinitely.
1029
+
1030
+ The factory is not invoked for redirect hops (they reuse the attempt's
1031
+ headers), for skipped duplicate proxy offsets, or for an attempt whose proxy
1032
+ allocation failed — those build no request, so `attempt` counts only the
1033
+ requests that are actually issued. A factory that throws fails the request with
1034
+ the non-retryable `TransportError` code `http_header_factory_failed`, which is
1035
+ classified as a provider-side fault (`provider_error`), not an upstream
1036
+ failure. `ctx.stealth` has its own `headers` option and does not accept a
1037
+ factory.
1038
+
681
1039
  ### Public local debugging checklist
682
1040
 
683
1041
  - Operation smoke requests use the provider server envelope:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.64
4
+
5
+ - Release candidate for main commit 0505a938340d08db3ea3e2302cab5f6b5dd2b4e5.
6
+
7
+ ## 2.2.0-beta.63
8
+
9
+ - Release candidate for main commit a9b85acc5af4c40ec59774b0d1e864e7ed21828e.
10
+
3
11
  ## 2.2.0-beta.62
4
12
 
5
13
  - Release candidate for main commit 98c623f799268b6d13ce59844b2b33ed78dcd0c3.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vooy Korea, Inc. (주식회사 부이코리아)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -78,6 +78,7 @@ The operation request body is the same envelope used by the APIFuse gateway:
78
78
  | `headers` | no | Extra caller headers to expose through `ctx.request.headers`. |
79
79
  | `connectionId` | no | Connection identity only; it does not include credentials. The gateway sends it for `optional` connection mode, and only when the caller supplied a connection that passed authorization. Exposed as `ctx.request.connectionId`. |
80
80
  | `connection` | no | Credential-bearing connection data. The gateway sends it for `required` connection mode; omit it for no-auth/public (`none` mode) operations. For local debugging, pass an object with `id`, `mode`, `secrets`, `metadata`, and `externalRef`. Do not pass `null`. |
81
+ | `tenantId` | no | Gateway-asserted principal scope: the organization id for customer subjects, the service-account id for platform service accounts. Sent for every connection mode, including `none`; absent on direct calls and self-test. Exposed as `ctx.request.tenantId` (`undefined` when absent or empty). Caller headers never populate it. |
81
82
 
82
83
  The gateway sends only `connection` for `required` mode, only `connectionId`
83
84
  for `optional` mode (and nothing when the caller passed no connection), and
@@ -15,6 +15,7 @@ import {
15
15
  DECLARATION_INVALID_CODE,
16
16
  validateFailClosedDeclaration,
17
17
  } from "../src/declaration-validation.js";
18
+ import { redundantDeploymentDefaults } from "../src/cli/deployment-intent.js";
18
19
  import { isProviderError } from "../src/errors.js";
19
20
  import type { ProviderDefinition } from "../src/index.js";
20
21
  import { lintProviderWithInformation, type ProviderLintMode } from "../src/lint.js";
@@ -140,6 +141,7 @@ export async function runChecks(
140
141
  }
141
142
  const provider = assertProviderDefinition(providerModule?.default);
142
143
  const providerSourceFiles = collectProviderSourceFiles(providerRoot);
144
+ const localeCatalogEn = readEnglishLocaleCatalog(providerRoot);
143
145
 
144
146
  return [
145
147
  checkIndex(indexPath, provider),
@@ -147,8 +149,9 @@ export async function runChecks(
147
149
  checkOperations(provider),
148
150
  checkFixtures(provider),
149
151
  checkSchemas(provider),
150
- checkAuthoringLint(provider, providerSourceFiles, options.lintMode),
152
+ checkAuthoringLint(provider, providerSourceFiles, localeCatalogEn, options.lintMode),
151
153
  checkProviderMetadata(provider),
154
+ await checkDeploymentIntent(providerRoot, provider),
152
155
  checkDockerfile(dockerfilePath),
153
156
  checkPackageJson(packageJsonPath),
154
157
  checkProviderJson(providerJsonPath, packageJsonPath),
@@ -367,9 +370,30 @@ function checkSchemas(provider: ProviderDefinition | undefined): CheckResult {
367
370
  };
368
371
  }
369
372
 
373
+ /**
374
+ * Reads `locales/en.json` so the error-localization lint rules can resolve
375
+ * declared `messageKey`/`fixKey` values. An absent or unreadable catalog is
376
+ * reported as an empty catalog rather than as "no catalog": a provider that
377
+ * references locale keys without shipping `en.json` must still fail the
378
+ * missing-key rule instead of silently skipping it.
379
+ */
380
+ function readEnglishLocaleCatalog(providerRoot: string): Record<string, unknown> {
381
+ const catalogPath = resolve(providerRoot, "locales", "en.json");
382
+ if (!existsSync(catalogPath)) return {};
383
+ try {
384
+ const parsed: unknown = JSON.parse(readFileSync(catalogPath, "utf8"));
385
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
386
+ ? (parsed as Record<string, unknown>)
387
+ : {};
388
+ } catch {
389
+ return {};
390
+ }
391
+ }
392
+
370
393
  function checkAuthoringLint(
371
394
  provider: ProviderDefinition | undefined,
372
395
  providerSourceFiles: Record<string, string>,
396
+ localeCatalogEn: Record<string, unknown>,
373
397
  lintMode: ProviderLintMode = "official",
374
398
  ): CheckResult {
375
399
  if (!provider) {
@@ -380,7 +404,7 @@ function checkAuthoringLint(
380
404
  }
381
405
 
382
406
  const { diagnostics, information } = lintProviderWithInformation(
383
- { ...provider, providerSourceFiles },
407
+ { ...provider, providerSourceFiles, localeCatalogEn },
384
408
  { mode: lintMode },
385
409
  );
386
410
  const errors = diagnostics.filter((diagnostic) => diagnostic.level === "error");
@@ -448,6 +472,64 @@ function checkProviderMetadata(provider: ProviderDefinition | undefined): CheckR
448
472
  };
449
473
  }
450
474
 
475
+ export const DEPLOYMENT_INTENT_CHECK_MESSAGE =
476
+ "Deployment intent lives only in defineProvider({ deployment }) without restating profile defaults";
477
+ export const LEGACY_DEPLOY_FILE_RULE = "deployment/legacy-deploy-file";
478
+ export const REDUNDANT_DEPLOYMENT_DEFAULT_RULE = "deployment/redundant-default";
479
+ export const SPREAD_EXPORT_DEPLOYMENT_RULE = "deployment/spread-export";
480
+
481
+ /**
482
+ * Deployment intent has one authored home: the `deployment` key on
483
+ * `defineProvider()`, carrying only the fields that differ from the runtime
484
+ * profile. A standalone `deploy.ts` is a legacy surface and a field that
485
+ * restates a profile default is noise the platform derives anyway. Both are
486
+ * reported as warnings while the fleet migrates (`apifuse
487
+ * migrate-deployment`); they become errors once no repository carries the
488
+ * legacy file.
489
+ */
490
+ async function checkDeploymentIntent(
491
+ providerRoot: string,
492
+ provider: ProviderDefinition | undefined,
493
+ ): Promise<CheckResult> {
494
+ const details: string[] = [];
495
+ if (existsSync(resolve(providerRoot, "deploy.ts"))) {
496
+ details.push(
497
+ `WARN ${LEGACY_DEPLOY_FILE_RULE}: deploy.ts is a legacy deployment surface; run \`apifuse migrate-deployment .\` to move its non-default values into defineProvider({ deployment }) and delete the file.`,
498
+ );
499
+ }
500
+ // A `deployment` extra on `export default { ...provider, deployment }` is
501
+ // indistinguishable from a declaration key once the module is loaded, so
502
+ // inspect the source. The inspector needs the TypeScript compiler (a
503
+ // devDependency of provider repositories); without it this rule is skipped.
504
+ const indexPath = resolve(providerRoot, "index.ts");
505
+ if (existsSync(indexPath)) {
506
+ try {
507
+ const { findSpreadExportDeploymentProperty } = await import(
508
+ "../src/cli/migrate-deployment-intent.js"
509
+ );
510
+ const spread = findSpreadExportDeploymentProperty(readFileSync(indexPath, "utf8"), indexPath);
511
+ if (spread !== undefined) {
512
+ details.push(
513
+ `WARN ${SPREAD_EXPORT_DEPLOYMENT_RULE}: index.ts:${spread.line} attaches \`deployment\` to a spread default export; the only authored surface is the \`deployment\` key inside defineProvider({...}). Run \`apifuse migrate-deployment .\` to hoist it.`,
514
+ );
515
+ }
516
+ } catch {
517
+ // TypeScript unavailable: the source-shape rule cannot run here.
518
+ }
519
+ }
520
+ if (provider?.deployment !== undefined) {
521
+ for (const redundant of redundantDeploymentDefaults(provider.deployment)) {
522
+ details.push(`WARN ${REDUNDANT_DEPLOYMENT_DEFAULT_RULE}: ${redundant.message}`);
523
+ }
524
+ }
525
+ return {
526
+ message: DEPLOYMENT_INTENT_CHECK_MESSAGE,
527
+ // Warning-only during the fleet migration window.
528
+ passed: true,
529
+ details,
530
+ };
531
+ }
532
+
451
533
  function checkDockerfile(dockerfilePath: string): CheckResult {
452
534
  return {
453
535
  message: "Dockerfile exists",