@apifuse/provider-sdk 2.2.0-beta.48 → 2.2.0-beta.49

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 (108) hide show
  1. package/AUTHORING.md +91 -36
  2. package/CHANGELOG.md +4 -0
  3. package/README.md +11 -9
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-dev.ts +24 -13
  6. package/bin/apifuse-migrate-operation-declaration.ts +55 -0
  7. package/bin/apifuse-pack-smoke.ts +1 -1
  8. package/bin/apifuse-pack-types.ts +2 -1
  9. package/bin/apifuse-record.ts +30 -16
  10. package/bin/apifuse-submit-check.ts +20 -35
  11. package/dist/cli/commands.d.ts +1 -1
  12. package/dist/cli/commands.js +11 -0
  13. package/dist/cli/migrate-operation-declaration.d.ts +59 -0
  14. package/dist/cli/migrate-operation-declaration.js +1178 -0
  15. package/dist/cli/templates/provider/README.md.tpl +3 -3
  16. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -0
  17. package/dist/config/loader.d.ts +2 -0
  18. package/dist/config/loader.js +18 -7
  19. package/dist/contract-types.d.ts +11 -5
  20. package/dist/contract.js +21 -10
  21. package/dist/define.d.ts +25 -22
  22. package/dist/define.js +49 -75
  23. package/dist/dev.d.ts +3 -0
  24. package/dist/dev.js +1 -1
  25. package/dist/engine.d.ts +78 -0
  26. package/dist/engine.js +133 -0
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.js +2 -1
  29. package/dist/lint.d.ts +7 -15
  30. package/dist/lint.js +45 -70
  31. package/dist/provider.d.ts +3 -1
  32. package/dist/provider.js +1 -0
  33. package/dist/runtime/chrome149-header-order.d.ts +58 -0
  34. package/dist/runtime/chrome149-header-order.js +289 -0
  35. package/dist/runtime/env.js +12 -0
  36. package/dist/runtime/executor.d.ts +2 -1
  37. package/dist/runtime/executor.js +3 -36
  38. package/dist/runtime/insights.js +2 -2
  39. package/dist/runtime/otlp.d.ts +71 -2
  40. package/dist/runtime/otlp.js +397 -16
  41. package/dist/runtime/resolver-vendors/capsolver.js +3 -3
  42. package/dist/runtime/resolver.js +3 -3
  43. package/dist/runtime/stealth.d.ts +13 -4
  44. package/dist/runtime/stealth.js +362 -85
  45. package/dist/runtime/trace-config.js +2 -1
  46. package/dist/runtime/trace.d.ts +5 -0
  47. package/dist/runtime/trace.js +43 -10
  48. package/dist/server/self-test.d.ts +1 -3
  49. package/dist/server/self-test.js +2 -12
  50. package/dist/server/serve-implementation.d.ts +6 -1
  51. package/dist/server/serve-implementation.js +55 -40
  52. package/dist/server/trace-output.d.ts +3 -1
  53. package/dist/server/trace-output.js +61 -2
  54. package/dist/stealth/profiles.d.ts +9 -8
  55. package/dist/stealth/profiles.js +123 -286
  56. package/dist/types.d.ts +111 -108
  57. package/package.json +2 -1
  58. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
  59. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
  60. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
  61. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
  62. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
  63. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
  64. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
  65. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
  66. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
  67. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
  68. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
  69. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
  70. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
  71. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
  72. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
  73. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
  74. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
  75. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
  76. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
  77. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
  78. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
  79. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
  80. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
  81. package/src/cli/commands.ts +13 -0
  82. package/src/cli/migrate-operation-declaration.ts +1654 -0
  83. package/src/cli/templates/provider/README.md.tpl +3 -3
  84. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -0
  85. package/src/config/loader.ts +31 -6
  86. package/src/contract-types.ts +11 -5
  87. package/src/contract.ts +21 -10
  88. package/src/define.ts +107 -119
  89. package/src/dev.ts +4 -1
  90. package/src/engine.ts +279 -0
  91. package/src/index.ts +13 -5
  92. package/src/lint.ts +58 -92
  93. package/src/provider.ts +25 -3
  94. package/src/runtime/chrome149-header-order.ts +330 -0
  95. package/src/runtime/env.ts +13 -0
  96. package/src/runtime/executor.ts +7 -40
  97. package/src/runtime/insights.ts +2 -2
  98. package/src/runtime/otlp.ts +467 -21
  99. package/src/runtime/resolver-vendors/capsolver.ts +4 -3
  100. package/src/runtime/resolver.ts +3 -3
  101. package/src/runtime/stealth.ts +435 -103
  102. package/src/runtime/trace-config.ts +3 -2
  103. package/src/runtime/trace.ts +57 -17
  104. package/src/server/self-test.ts +2 -9
  105. package/src/server/serve-implementation.ts +89 -72
  106. package/src/server/trace-output.ts +99 -2
  107. package/src/stealth/profiles.ts +169 -327
  108. package/src/types.ts +109 -137
package/AUTHORING.md CHANGED
@@ -73,45 +73,105 @@ payment state tokens in the platform monorepo.
73
73
 
74
74
  ### Description template
75
75
 
76
- Every operation `description` MUST be at least 150 characters and follow this structure:
76
+ Every operation declares a `descriptionKey`. Its English locale value should
77
+ follow this structure:
77
78
 
78
79
  ```
79
80
  <What the tool does in one sentence>. Use when <specific scenarios>. Do NOT use for <counter-scenarios; point to alternatives>. Returns <key output fields>. <Important caveats: rate limits, auth, freshness>.
80
81
  ```
81
82
 
82
- Example:
83
+ Example declaration and locale entry:
83
84
  ```ts
84
- description:
85
- "Retrieves KMA ultra-short-term weather observation for a given grid coordinate in South Korea, " +
86
- "including temperature, humidity, wind speed, precipitation, and sky condition. " +
87
- "Use when the user asks about current or hourly weather at a specific Korean location. " +
88
- "Do NOT use for forecasts beyond 2 days — use kma_mid_forecast instead. " +
89
- "Returns hourly data in KST timezone; null values indicate data unavailable. " +
90
- "Rate-limited to 1000 calls/day on the free tier.",
85
+ descriptionKey: "operations.realtimeWeather.description",
86
+ ```
87
+
88
+ ```json
89
+ {
90
+ "operations": {
91
+ "realtimeWeather": {
92
+ "description": "Retrieves KMA ultra-short-term weather observations for a South Korean grid coordinate. Use when the user asks about current or hourly weather. Do NOT use for forecasts beyond two days; use the mid-range forecast operation instead. Returns hourly KST data; null values mean unavailable data."
93
+ }
94
+ }
95
+ }
91
96
  ```
92
97
 
93
98
  ### Language policy
94
99
 
95
- - **Structural text**: English (operation `description`, Zod `.describe()`, `whenToUse`, `whenNotToUse`, `derivations`, `inputExamples.scenario/rationale`).
96
- - **Values only**: native language (fixtures payloads, `inputExamples[].input` values like "대방동", "KRW-BTC", entity catalog entries).
100
+ - **Structural text**: locale keys (`descriptionKey`, the other `*Key`/`*Keys`
101
+ fields, schema `.describeKey()`, and `examples[].scenarioKey`/`rationaleKey`).
102
+ - **Values only**: native language (fixtures payloads and `examples[].input`
103
+ values like "대방동", "KRW-BTC", entity catalog entries).
97
104
 
98
105
  ### Required per operation
99
106
 
100
- - `description` — 150+ chars English (error-level rule)
101
- - Every Zod field in input AND output has `.describe()` including nested objects + array items (error-level rule)
107
+ - `riskClass` — authored as `read`, `write`, `destructive`, or `external-send`
108
+ - `descriptionKey` backed by every required provider locale catalog
109
+ - `connectionMode` — explicit for `credentials`, `oauth2`, and `oauth2_proxied` providers
110
+ - Every Zod field in input AND output has `.describeKey()` including nested objects + array items (error-level rule)
102
111
  - `fixtures.request` + `fixtures.response` both present (error-level rule)
103
112
  - Exactly one of `healthCheck`, `healthCheckUnsupported`, or `healthJourneys[].coversOperations` coverage per operation. Prefer `healthCheck` for safe read-only upstream probes; use `healthCheckUnsupported` only with a specific reason for destructive, paid, credential-sensitive, flaky, or otherwise unsafe probes. Use a provider-level health journey when a destructive or credential-sensitive flow can be proven safely only as a multi-step boundary test, such as stopping at a payment WebView URL.
104
113
 
114
+ ### Capability declarations
115
+
116
+ Declare each capability used by provider operations. Capabilities without
117
+ configuration use a bare object, for example `http: {}`, `choice: {}`, or
118
+ `cache: {}`. `trace` is ambient and must not be declared: every operation
119
+ context receives it. `allowedHosts`, `proxy`, `secrets`, and `context` declare
120
+ policy or metadata only; they do not create context members and are not
121
+ capability bindings.
122
+
123
+ Declare `runtimeTarget: "vanilla"` for portable provider business logic. Use
124
+ `runtimeTarget: "engine"` only for an approved session-bearing provider that
125
+ must remain engine-resident. A vanilla target cannot declare `native`.
126
+
127
+ Proxy vendor application keys, usernames, and passwords are engine-owned. Do
128
+ not list `APIFUSE__PROXY__SMARTPROXY_APP_KEY`,
129
+ `APIFUSE__PROXY__NODEMAVEN_USERNAME`, or
130
+ `APIFUSE__PROXY__NODEMAVEN_PASSWORD` in provider `secrets`; `proxy` contains
131
+ policy intent only.
132
+
105
133
  ### Factored operations
106
134
 
107
135
  `defineProvider(declaration)` returns the builder that accepts `operations`, so
108
136
  inline handlers are typed only after capability declarations are fixed. Export
109
- `ProviderContextOf<typeof buildProvider>` once from the provider entry point.
110
- Separate operation files import that provider context and call
111
- `defineOperation<ProviderContext>()({...})`; helpers should accept the one SDK
112
- client they use rather than the provider context. Zod and Standard Schema v1
113
- schemas retain input/output inference. Invalid configs name the offending field,
114
- such as `auth.mode` or `operations.<id>.fixtures.response`.
137
+ one declaration-derived context alias from the provider entry point:
138
+
139
+ ```ts
140
+ import {
141
+ defineProvider,
142
+ type ProviderContext,
143
+ type ProviderDeclaration,
144
+ } from "@apifuse/provider-sdk/provider";
145
+
146
+ const declaration = {
147
+ // ...id, version, runtime, meta
148
+ http: {},
149
+ } as const satisfies ProviderDeclaration;
150
+
151
+ export type Ctx = ProviderContext<typeof declaration>;
152
+ const buildProvider = defineProvider(declaration);
153
+ ```
154
+
155
+ Separate operation files need one provider-local type import and no generic
156
+ handler signature:
157
+
158
+ ```ts
159
+ import { defineOperation } from "@apifuse/provider-sdk/provider";
160
+ import type { Ctx } from "../index.js";
161
+
162
+ export const search = defineOperation<Ctx>()({
163
+ // ...input, output, fixtures, health check
164
+ async handler(ctx, input) {
165
+ return fetchSearch(ctx.http, input);
166
+ },
167
+ });
168
+ ```
169
+
170
+ `ProviderContextOf<typeof buildProvider>` remains an equivalent convenience
171
+ alias. Helpers should accept the one SDK client they use rather than the
172
+ provider context. Zod and Standard Schema v1 schemas retain input/output
173
+ inference. Invalid configs name the offending field, such as `auth.mode` or
174
+ `operations.<id>.fixtures.response`.
115
175
 
116
176
  ### Replay-safe fixtures
117
177
 
@@ -221,15 +281,11 @@ healthCheck: {
221
281
  ```
222
282
  <!-- @magic-end:sample -->
223
283
 
224
- ### Strongly recommended (warn-level rules)
225
-
226
- - `description` includes "use" AND "when" phrasing
227
- - `inputExamples` with 2+ scenarios for complex input (nested objects, enums, format-sensitive strings)
228
- - `derivations` for parameters not directly visible in the user query (e.g., `gridX` derived from geocoding)
229
-
230
284
  ### Optional but valuable
231
285
 
232
- - `annotations`: `{ readOnly, destructive, idempotent, openWorld, rateLimit }` agentic safety signals
286
+ - `examples`: usage examples with locale-keyed `scenarioKey`, an `input`, and optional `rationaleKey`
287
+ - `approval`: only when it deliberately differs from the `riskClass` default (`read → never`, `write → risk-based`, otherwise `always`)
288
+ - `titleKey`, `summaryKey`, `markdownKey`, `whenToUseKeys`, `whenNotToUseKeys`, and `normalizationNotesKeys`: locale-keyed operation prose
233
289
  - `tags`: operation-level semantic tags for retrieval (e.g., `["weather", "korea", "realtime"]`)
234
290
  - `relatedOperations`: `{ alternatives?: string[] }` — links to fallback/sibling operations
235
291
 
@@ -248,6 +304,7 @@ export default defineProvider({
248
304
  })({
249
305
  operations: {
250
306
  verifyAudioOtp: {
307
+ riskClass: "read",
251
308
  input: z.object({
252
309
  audioBase64: z.string().describe("Base64-encoded short OTP audio"),
253
310
  mediaType: z.string().optional().describe("Audio MIME type"),
@@ -422,7 +479,7 @@ Provider-server failures use a stable public envelope:
422
479
  `retryable` is always present on responses emitted by the current SDK. Set
423
480
  `retryable` in the `ProviderError` options when the provider knows the answer;
424
481
  an explicit `true` or `false` wins over the matching operation declaration and
425
- SDK derivation. When it is omitted, `operations.<id>.docs.errorCodes[].retryable`
482
+ SDK derivation. When it is omitted, `operations.<id>.errorCodes[].retryable`
426
483
  is used for a matching provider-owned code, followed by SDK derivation (which
427
484
  defaults ordinary `ProviderError` values to `false`). During stateful rolling
428
485
  upgrades, the forwarding client also accepts an older owner response that omits
@@ -447,19 +504,17 @@ Treat this header as telemetry, not as provider-controlled public error detail.
447
504
  Its category, taxonomy version, retryability, and optional upstream status match
448
505
  the structured `provider_request_failed` log event.
449
506
 
450
- Declare provider-owned operation failures next to their documentation. The
507
+ Declare provider-owned operation failures directly on the operation. The
451
508
  server builds a lookup once at startup and applies it to failures from that
452
509
  operation:
453
510
 
454
511
  ```ts
455
- docs: {
456
- errorCodes: [{
457
- code: "UPSTREAM_SCHEMA_ERROR",
458
- status: 502,
459
- retryable: true,
460
- description: "The upstream response no longer matches its schema.",
461
- }],
462
- },
512
+ errorCodes: [{
513
+ code: "UPSTREAM_SCHEMA_ERROR",
514
+ status: 502,
515
+ retryable: true,
516
+ description: "The upstream response no longer matches its schema.",
517
+ }],
463
518
  handler: async () => {
464
519
  throw new ProviderError("Upstream schema changed", {
465
520
  code: "UPSTREAM_SCHEMA_ERROR",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.49
4
+
5
+ - Release candidate for main commit 694b145f65c2c81e298c2516dde221ce19cb52da.
6
+
3
7
  ## 2.2.0-beta.48
4
8
 
5
9
  - Release candidate for main commit ba6636fa8dd53a35af3091ae6efd797f5ef3d836.
package/README.md CHANGED
@@ -138,13 +138,14 @@ the bad request path; provider/runtime failures include `code`, `message`, and
138
138
  - **Stealth-sensitive providers**: use `ctx.http` for normal JSON/REST calls and
139
139
  `ctx.stealth.fetch()` when you need browser-like session or cookie control.
140
140
  `ctx.stealth.fetch()` uses the `wreq-js`-backed browser stealth transport and
141
- accepts request controls for `params`, `sensitiveParams`, `proxy`, `timeout`, `profile`,
141
+ accepts request controls for `params`, `sensitiveParams`, `proxy`, `timeout`,
142
142
  `maxBodyBytes`, `redirect`, `throwOnHttpError`, and
143
143
  `stealth.insecureSkipVerify`. For login
144
144
  flows that must inspect intermediate `Location`/`Set-Cookie` headers, create
145
145
  a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
146
146
  inspect accumulated cookies through `session.cookies`. Select an SDK stealth
147
- intent-based `profile` such as `chrome-desktop`; do not pin a browser version
147
+ identity with structured `stealth: { browser, os }` options; omitted `os`
148
+ explicitly defaults to `macos`. Do not pin a browser version
148
149
  or tune JA3, HTTP/2 SETTINGS, or
149
150
  pseudo-header order in provider code. Chrome, Firefox, and Safari profiles
150
151
  are supported; use `ctx.browser` when the provider needs browser execution.
@@ -219,6 +220,7 @@ const buildProvider = defineProvider({
219
220
  export type ProviderContext = ProviderContextOf<typeof buildProvider>
220
221
 
221
222
  const search = defineOperation<ProviderContext>()({
223
+ riskClass: "read",
222
224
  input: z.object({ q: z.string().describe("Search query") }),
223
225
  output: z.object({ count: z.number().describe("Result count") }),
224
226
  async handler(ctx, input) {
@@ -266,17 +268,17 @@ healthCheck: {
266
268
  }
267
269
  ```
268
270
 
269
- ### Operation annotations
271
+ ### Flat operation metadata
270
272
 
271
- Operations declare non-functional metadata via `annotations`:
273
+ Operations declare access, safety, and execution metadata directly on the
274
+ operation. `riskClass` is required and is the safety source of truth:
272
275
 
273
276
  | Field | Type | Notes |
274
277
  |---|---|---|
275
- | `readOnly` | `boolean` | Operation has no side effects (safe to test in production). |
276
- | `destructive` | `boolean` | Operation modifies/deletes state. |
277
- | `idempotent` | `boolean` | Safe to retry without duplicate side effects. |
278
- | `openWorld` | `boolean` | Callable without authentication. |
279
- | `rateLimit` | `{ calls, window }` | Per-operation rate hint. `window` is `"minute"\|"hour"\|"day"`. |
278
+ | `riskClass` | `"read" \| "write" \| "destructive" \| "external-send"` | Required safety classification. |
279
+ | `approval` | `"never" \| "risk-based" \| "always"` | Optional override; omit when it matches the risk-class default. |
280
+ | `connectionMode` | `"none" \| "optional" \| "required"` | Access requirement. Required by lint for credential-bearing providers. |
281
+ | `connectionExternalRefParam` | `string` | Public argument used to resolve a Connection. |
280
282
  | `timeoutMs` | `number` | Per-operation upstream timeout (1–60000 ms). Omit to inherit the gateway global default. |
281
283
 
282
284
  `defineProvider()` validates `timeoutMs` is an integer in `[1, 60000]` and throws `ValidationError` otherwise. The gateway applies the value via `context.WithTimeout` on every proxied call and clamps defensively to the same bound.
package/SUBMISSION.md CHANGED
@@ -28,7 +28,7 @@ The score is a triage aid, not a payout guarantee. Maintainers still review corr
28
28
  | Category | Points | Examples |
29
29
  |---|---:|---|
30
30
  | Definition & metadata | 15 | `defineProvider`, package, Dockerfile, SDK structural checks |
31
- | Operations & schemas | 15 | strong descriptions, annotations, input/output schemas |
31
+ | Operations & schemas | 15 | locale-keyed descriptions, explicit risk classes, input/output schemas |
32
32
  | Fixtures & tests | 15 | bidirectional fixtures that parse against schemas |
33
33
  | Health coverage | 15 | real `healthCheck` or specific `healthCheckUnsupported.reason` |
34
34
  | Runtime/local smoke | 10 | `/health` and at least one `POST /v1/{operation}` note |
@@ -6,13 +6,17 @@ import {
6
6
  createCredentialContext,
7
7
  createEnvContext,
8
8
  createHttpClient,
9
+ createInProcessProviderEngine,
9
10
  createOcrClientFromEnv,
10
11
  createProviderCache,
11
12
  createProviderChoiceContext,
13
+ createProviderEnvironment,
12
14
  createUnsupportedResolverClient,
13
15
  createSttClientFromEnv,
14
16
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
17
+ readEngineProxyCredentials,
15
18
  type ProviderDefinition,
19
+ type ProviderEngineBindingCandidates,
16
20
  ProviderError,
17
21
  type ProviderProxyPolicy,
18
22
  } from "../src/index.js";
@@ -79,19 +83,20 @@ export async function main() {
79
83
  export function createProviderContext(provider: ProviderDefinition): {
80
84
  ctx: ProviderContext;
81
85
  } {
82
- const env = createEnvContext([
83
- ...(provider.secrets?.map((secret) => secret.name) ?? []),
84
- PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
85
- ]);
86
+ const providerEnvironment = createProviderEnvironment(
87
+ process.env,
88
+ provider.secrets?.map((secret) => secret.name) ?? [],
89
+ );
90
+ const providerEnv = { get: (key: string) => providerEnvironment[key] };
91
+ const engineEnv = createEnvContext([PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV]);
92
+ const engineCredentials = readEngineProxyCredentials();
86
93
  const credential = createCredentialContext();
87
94
  const state = createMemoryProviderRuntimeState();
88
95
  const cache = createProviderCache({ providerId: provider.id });
89
96
  const proxyPolicy = resolveNativeProxyPolicy(provider);
90
- const stealthProfile = provider.stealth?.profile
91
- ? getStealthProfile(provider.stealth.profile)
92
- : undefined;
93
- const ctx: ProviderContext = {
94
- env,
97
+ const stealthProfile = provider.stealth ? getStealthProfile(provider.stealth) : undefined;
98
+ const candidates: ProviderEngineBindingCandidates = {
99
+ env: providerEnv,
95
100
  credential,
96
101
  auth: createUnsupportedAuthStub(),
97
102
  browser:
@@ -105,11 +110,13 @@ export function createProviderContext(provider: ProviderDefinition): {
105
110
  cache,
106
111
  state,
107
112
  trace: createTraceContext(),
108
- stealth: createStealthClient("http://localhost"),
113
+ stealth: createStealthClient("http://localhost", {
114
+ ...(provider.stealth ? { stealth: provider.stealth } : {}),
115
+ }),
109
116
  ocr: createOcrClientFromEnv(provider.ocr),
110
117
  stt: createSttClientFromEnv(provider.stt),
111
118
  resolver: provider.resolver
112
- ? createResolverClientFromEnv(provider.resolver, undefined, {
119
+ ? createResolverClientFromEnv(provider.resolver, engineCredentials, {
113
120
  allowedHosts: provider.allowedHosts,
114
121
  cache,
115
122
  ...(proxyPolicy
@@ -125,11 +132,15 @@ export function createProviderContext(provider: ProviderDefinition): {
125
132
  : createUnsupportedResolverClient("Provider does not declare resolver capability"),
126
133
  choice: createProviderChoiceContext({
127
134
  providerId: provider.id,
128
- env,
135
+ env: engineEnv,
129
136
  credential,
130
137
  state,
131
138
  }),
132
- } satisfies Omit<ProviderContext, "native"> as unknown as ProviderContext;
139
+ };
140
+ const ctx = createInProcessProviderEngine().attach({
141
+ provider,
142
+ bindings: candidates,
143
+ }) as ProviderContext;
133
144
 
134
145
  return { ctx };
135
146
  }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * `apifuse migrate-operation-declaration [path] [--check] [--json]`
5
+ *
6
+ * Rewrites ADR-0009's legacy nested operation declarations to the flat
7
+ * authoring contract. The run is repository-atomic: any declaration the
8
+ * codemod cannot prove leaves every source file untouched and exits 2.
9
+ */
10
+
11
+ import { resolve } from "node:path";
12
+
13
+ import { migrateOperationDeclarationRepository } from "../src/cli/migrate-operation-declaration.js";
14
+
15
+ export async function main(): Promise<number> {
16
+ const args = process.argv.slice(3);
17
+ const check = args.includes("--check");
18
+ const json = args.includes("--json");
19
+ const positional = args.filter((argument) => !argument.startsWith("--"));
20
+ const providerRoot = resolve(positional[0] ?? ".");
21
+ const result = migrateOperationDeclarationRepository(providerRoot, { check });
22
+
23
+ if (json) {
24
+ console.log(
25
+ JSON.stringify({
26
+ schemaVersion: 1,
27
+ command: "migrate-operation-declaration",
28
+ ...result,
29
+ }),
30
+ );
31
+ } else if (result.status === "refused") {
32
+ console.error(
33
+ `migrate-operation-declaration: refused ${result.refusals.length} declaration(s); no files were written.`,
34
+ );
35
+ for (const item of result.refusals) {
36
+ console.error(` ${item.file} [${item.operationKey}] ${item.reason}: ${item.detail}`);
37
+ }
38
+ } else if (result.status === "unchanged") {
39
+ console.log("migrate-operation-declaration: already flat; nothing to do.");
40
+ } else {
41
+ const verb = result.status === "would-migrate" ? "would migrate" : "migrated";
42
+ const sidecar = result.sidecar === undefined ? "" : ` and ${result.sidecar}`;
43
+ console.log(
44
+ `migrate-operation-declaration: ${verb} ${result.operationCount} operation(s) across ${result.changedFiles.length} file(s)${sidecar}.`,
45
+ );
46
+ }
47
+
48
+ const exitCode = result.status === "refused" ? 2 : 0;
49
+ process.exitCode = exitCode;
50
+ return exitCode;
51
+ }
52
+
53
+ if (import.meta.main) {
54
+ process.exit(await main());
55
+ }
@@ -236,7 +236,7 @@ function smokePackedStealthNative(consumerDir: string): void {
236
236
  "const address = server.address();",
237
237
  'if (!address || typeof address === "string") throw new Error("Local server has no TCP address");',
238
238
  'const baseUrl = "http://127.0.0.1:" + address.port;',
239
- 'const session = createStealthClient(baseUrl).createSession({ profile: "safari-17" });',
239
+ 'const session = createStealthClient(baseUrl).createSession({ stealth: { browser: "safari", os: "macos" } });',
240
240
  "try {",
241
241
  ' const response = await session.fetch("/native");',
242
242
  ' if (response.body !== "packed native stealth ok") throw new Error("Unexpected stealth body: " + response.body);',
@@ -80,6 +80,7 @@ const NEGATIVE_CONTROLS = [
80
80
  "})({",
81
81
  "\toperations: {",
82
82
  "\t\tprobe: {",
83
+ '\t\t\triskClass: "read",',
83
84
  "\t\t\tinput: z.object({}),",
84
85
  "\t\t\toutput: z.object({ ok: z.boolean() }),",
85
86
  "\t\t\thandler: async () => ({ ok: true }),",
@@ -436,7 +437,7 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
436
437
  'export const cookieSolution: ChallengeSolution = { form: "cookies", cookies: { cf_clearance: "clearance" }, userAgent: "fixture-agent" };',
437
438
  "export const invalidation = invalidateResolverSolution(providerContext.resolver, awsWafChallenge, cookieSolution);",
438
439
  'export const resolverConfig: ProviderResolverConfig = { vendors: ["browser", "capsolver"], kinds: ["cloudflare_interstitial", "turnstile"] };',
439
- 'export const resolverProvider = defineProvider({ id: "pack-types-resolver", version: "1.0.0", runtime: "standard", resolver: resolverConfig, meta: { displayName: "Pack Types Resolver", descriptionKey: "meta.description", category: "test" } })({ operations: { probe: { input: z.object({}), output: z.object({ ok: z.boolean() }), handler: async () => ({ ok: true }), healthCheckUnsupported: { reason: "type fixture" } } } });',
440
+ 'export const resolverProvider = defineProvider({ id: "pack-types-resolver", version: "1.0.0", runtime: "standard", resolver: resolverConfig, meta: { displayName: "Pack Types Resolver", descriptionKey: "meta.description", category: "test" } })({ operations: { probe: { riskClass: "read", input: z.object({}), output: z.object({ ok: z.boolean() }), handler: async () => ({ ok: true }), healthCheckUnsupported: { reason: "type fixture" } } } });',
440
441
  "export const resolverContext: ResolverContext = { solve: async () => tokenSolution };",
441
442
  'export const browserCookie: BrowserCookie = { name: "persistent-id", value: "persistent-token", domain: "example.com", path: "/", expires: 1786698176, httpOnly: true, secure: true };',
442
443
  'const browserPage = undefined as unknown as Awaited<ReturnType<ProviderContext["browser"]["newPage"]>>;',
@@ -8,8 +8,10 @@ import { pathToFileURL } from "node:url";
8
8
  import {
9
9
  createBypassProviderCache,
10
10
  createHttpClient,
11
+ createInProcessProviderEngine,
11
12
  createOcrClientFromEnv,
12
13
  createProviderChoiceContext,
14
+ createProviderEnvironment,
13
15
  createSttClientFromEnv,
14
16
  createUnsupportedResolverClient,
15
17
  executeOperation,
@@ -18,7 +20,9 @@ import {
18
20
  type HttpStreamResponse,
19
21
  type ProviderContext,
20
22
  type ProviderDefinition,
23
+ type ProviderEngineBindingCandidates,
21
24
  ProviderError,
25
+ readEngineProxyCredentials,
22
26
  type ProviderProxyPolicy,
23
27
  type RequestOptions,
24
28
  type StealthClient,
@@ -449,7 +453,7 @@ export function createCaptureContext(
449
453
  let capturedSse: { order: number; method: string; path: string } | undefined;
450
454
  const sensitiveParamNames = new Set<string>();
451
455
  const sensitiveParamValues = new Set<string>();
452
- const captureSensitiveParams = (url: string, options?: RequestOptions) => {
456
+ const captureSensitiveParams = (url: string, options?: SensitiveRequestOptions) => {
453
457
  captureSensitiveRequestValues(url, options, sensitiveParamNames, sensitiveParamValues);
454
458
  };
455
459
  const getCapturedSensitiveParams = (): CapturedSensitiveParams => ({
@@ -503,15 +507,21 @@ export function createCaptureContext(
503
507
  },
504
508
  });
505
509
  const stealth = proxyStealthClient(
506
- createStealthClient(baseUrl),
510
+ createStealthClient(baseUrl, {
511
+ ...(provider.stealth ? { stealth: provider.stealth } : {}),
512
+ }),
507
513
  captureSensitiveParams,
508
514
  (order, response) => retainRawCapture(order, normalizeCapturedStealthResponse(response)),
509
515
  reserveCaptureOrder,
510
516
  );
511
517
 
512
- const env = {
513
- get: (key: string) => process.env[key],
514
- };
518
+ const providerEnvironment = createProviderEnvironment(
519
+ process.env,
520
+ provider.secrets?.map((secret) => secret.name) ?? [],
521
+ );
522
+ const env = { get: (key: string) => providerEnvironment[key] };
523
+ const engineEnv = { get: (key: string) => process.env[key] };
524
+ const engineCredentials = readEngineProxyCredentials();
515
525
  const credential = {
516
526
  mode: "none" as const,
517
527
  get: () => undefined,
@@ -522,10 +532,8 @@ export function createCaptureContext(
522
532
  const state = createMemoryProviderRuntimeState();
523
533
  const cache = createBypassProviderCache({ providerId: provider.id });
524
534
  const proxyPolicy = resolveNativeProxyPolicy(provider);
525
- const stealthProfile = provider.stealth?.profile
526
- ? getStealthProfile(provider.stealth.profile)
527
- : undefined;
528
- const ctx: ProviderContext = {
535
+ const stealthProfile = provider.stealth ? getStealthProfile(provider.stealth) : undefined;
536
+ const candidates: ProviderEngineBindingCandidates = {
529
537
  env,
530
538
  credential,
531
539
  request: { headers: {} },
@@ -560,7 +568,7 @@ export function createCaptureContext(
560
568
  ocr: createOcrClientFromEnv(provider.ocr),
561
569
  stt: createSttClientFromEnv(provider.stt),
562
570
  resolver: provider.resolver
563
- ? createResolverClientFromEnv(provider.resolver, undefined, {
571
+ ? createResolverClientFromEnv(provider.resolver, engineCredentials, {
564
572
  allowedHosts: provider.allowedHosts,
565
573
  cache,
566
574
  ...(proxyPolicy
@@ -576,12 +584,16 @@ export function createCaptureContext(
576
584
  : createUnsupportedResolverClient("Provider does not declare resolver capability"),
577
585
  choice: createProviderChoiceContext({
578
586
  providerId: provider.id,
579
- env,
587
+ env: engineEnv,
580
588
  request: { headers: {} },
581
589
  credential,
582
590
  state,
583
591
  }),
584
- } satisfies Omit<ProviderContext, "native"> as unknown as ProviderContext;
592
+ };
593
+ const ctx = createInProcessProviderEngine().attach({
594
+ provider,
595
+ bindings: candidates,
596
+ }) as ProviderContext;
585
597
 
586
598
  return {
587
599
  ctx,
@@ -636,9 +648,11 @@ type CapturedSensitiveParams = {
636
648
  values: readonly string[];
637
649
  };
638
650
 
651
+ type SensitiveRequestOptions = Pick<RequestOptions, "params" | "sensitiveParams">;
652
+
639
653
  function captureSensitiveRequestValues(
640
654
  url: string,
641
- options: RequestOptions | undefined,
655
+ options: SensitiveRequestOptions | undefined,
642
656
  names: Set<string>,
643
657
  values: Set<string>,
644
658
  ): void {
@@ -683,7 +697,7 @@ function captureSensitiveRequestValues(
683
697
  }
684
698
  }
685
699
 
686
- function snapshotRequestOptions<T extends RequestOptions>(options: T): T {
700
+ function snapshotRequestOptions<T extends SensitiveRequestOptions>(options: T): T {
687
701
  return {
688
702
  ...options,
689
703
  ...(options.params
@@ -778,7 +792,7 @@ type StealthSession = ReturnType<StealthClient["createSession"]>;
778
792
 
779
793
  function proxyStealthClient(
780
794
  client: StealthClient,
781
- onSensitiveParams: (url: string, options?: RequestOptions) => void,
795
+ onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
782
796
  onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
783
797
  reserveOrder: () => number,
784
798
  ): StealthClient {
@@ -804,7 +818,7 @@ function proxyStealthClient(
804
818
 
805
819
  function proxyStealthSession(
806
820
  session: StealthSession,
807
- onSensitiveParams: (url: string, options?: RequestOptions) => void,
821
+ onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
808
822
  onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
809
823
  reserveOrder: () => number,
810
824
  ): StealthSession {
@@ -2947,13 +2947,17 @@ function collectProviderRequiredLocaleKeys(provider: ProviderDefinition): string
2947
2947
 
2948
2948
  for (const operation of Object.values(provider.operations)) {
2949
2949
  addLocaleKeys(keys, [
2950
+ operation.titleKey,
2950
2951
  operation.descriptionKey,
2951
- operation.docs?.titleKey,
2952
- operation.docs?.descriptionKey,
2953
- operation.docs?.summaryKey,
2954
- operation.docs?.markdownKey,
2952
+ operation.summaryKey,
2953
+ operation.markdownKey,
2955
2954
  ...(operation.whenToUseKeys ?? []),
2956
2955
  ...(operation.whenNotToUseKeys ?? []),
2956
+ ...(operation.normalizationNotesKeys ?? []),
2957
+ ...(operation.examples ?? []).flatMap((example) => [
2958
+ example.scenarioKey,
2959
+ example.rationaleKey,
2960
+ ]),
2957
2961
  ...collectSchemaDescriptionKeys(operation.input),
2958
2962
  ...collectSchemaDescriptionKeys(operation.output),
2959
2963
  ]);
@@ -3020,8 +3024,8 @@ function scoreOperationMetadata(provider: ProviderDefinition): SubmitCheck {
3020
3024
  return true;
3021
3025
  })
3022
3026
  .map(([operationId]) => operationId);
3023
- const missingAnnotations = operations
3024
- .filter(([, operation]) => !operation.annotations)
3027
+ const missingRiskClass = operations
3028
+ .filter(([, operation]) => !operation.riskClass)
3025
3029
  .map(([operationId]) => operationId);
3026
3030
 
3027
3031
  if (weakDescriptions.length > 0) {
@@ -3033,30 +3037,30 @@ function scoreOperationMetadata(provider: ProviderDefinition): SubmitCheck {
3033
3037
  points: 0,
3034
3038
  maxPoints: CATEGORY_MAX_POINTS.operations,
3035
3039
  message: "One or more operations have weak descriptions.",
3036
- remediation: `For ${weakDescriptions.join(", ")}, add an operation \`descriptionKey\` backed by \`locales/en.json\` and \`locales/ko.json\`, or add a 150+ character \`description\` explaining when to use it, when not to use it, outputs, and caveats.`,
3040
+ remediation: `For ${weakDescriptions.join(", ")}, add an operation \`descriptionKey\` backed by \`locales/en.json\` and \`locales/ko.json\`.`,
3037
3041
  evidence: weakDescriptions,
3038
3042
  };
3039
3043
  }
3040
3044
 
3041
- const points = missingAnnotations.length > 0 ? 11 : CATEGORY_MAX_POINTS.operations;
3045
+ const points = missingRiskClass.length > 0 ? 11 : CATEGORY_MAX_POINTS.operations;
3042
3046
  return {
3043
3047
  id: "operation-metadata",
3044
3048
  category: "operations",
3045
- level: missingAnnotations.length > 0 ? "warn" : "info",
3046
- status: missingAnnotations.length > 0 ? "warn" : "pass",
3049
+ level: missingRiskClass.length > 0 ? "warn" : "info",
3050
+ status: missingRiskClass.length > 0 ? "warn" : "pass",
3047
3051
  points,
3048
3052
  maxPoints: CATEGORY_MAX_POINTS.operations,
3049
3053
  message:
3050
- missingAnnotations.length > 0
3051
- ? "Operations are described, but some are missing safety annotations."
3054
+ missingRiskClass.length > 0
3055
+ ? "Operations are described, but some are missing a safety classification."
3052
3056
  : "Operation descriptions and metadata are review-ready.",
3053
3057
  remediation:
3054
- missingAnnotations.length > 0
3055
- ? `For ${missingAnnotations.join(", ")}, add \`annotations\` with the applicable safety fields, such as \`readOnly\`, \`destructive\`, \`idempotent\`, \`openWorld\`, \`rateLimit\`, or \`timeoutMs\`.`
3058
+ missingRiskClass.length > 0
3059
+ ? `For ${missingRiskClass.join(", ")}, declare the operation's required \`riskClass\` as \`read\`, \`write\`, \`destructive\`, or \`external-send\`.`
3056
3060
  : undefined,
3057
3061
  evidence:
3058
- missingAnnotations.length > 0
3059
- ? missingAnnotations.map((operationId) => `${operationId}: missing annotations`)
3062
+ missingRiskClass.length > 0
3063
+ ? missingRiskClass.map((operationId) => `${operationId}: missing riskClass`)
3060
3064
  : operations.map(([operationId]) => operationId),
3061
3065
  };
3062
3066
  }
@@ -5332,25 +5336,6 @@ function scoreAuthSafety(provider: ProviderDefinition): SubmitCheck {
5332
5336
  };
5333
5337
  }
5334
5338
 
5335
- if (authMode === "none") {
5336
- const securedOperations = Object.entries(provider.operations).filter(
5337
- ([, operation]) => operation.annotations?.openWorld === false,
5338
- );
5339
- if (securedOperations.length > 0) {
5340
- return {
5341
- id: "auth-safety",
5342
- category: "auth",
5343
- level: "warn",
5344
- status: "warn",
5345
- points: 7,
5346
- maxPoints: CATEGORY_MAX_POINTS.auth,
5347
- message: "Provider is no-auth but at least one operation is not marked openWorld.",
5348
- remediation: `Either set \`auth.mode\` to the upstream auth model, or mark these public no-auth operations with \`annotations.openWorld: true\`: ${securedOperations.map(([operationId]) => operationId).join(", ")}.`,
5349
- evidence: securedOperations.map(([operationId]) => operationId),
5350
- };
5351
- }
5352
- }
5353
-
5354
5339
  return pass(
5355
5340
  "auth-safety",
5356
5341
  "auth",