@apifuse/provider-sdk 2.1.0-beta.3 → 2.1.0-beta.4

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 (60) hide show
  1. package/AUTHORING.md +163 -8
  2. package/CHANGELOG.md +8 -1
  3. package/README.md +17 -16
  4. package/SUBMISSION.md +4 -4
  5. package/bin/apifuse-dev.ts +12 -5
  6. package/bin/apifuse-pack-check.ts +9 -2
  7. package/bin/apifuse-pack-smoke.ts +127 -6
  8. package/bin/apifuse-perf.ts +19 -15
  9. package/bin/apifuse-record.ts +41 -53
  10. package/bin/apifuse-submit-check.ts +179 -7
  11. package/bin/apifuse.ts +1 -1
  12. package/package.json +17 -8
  13. package/src/choice-token.ts +164 -0
  14. package/src/cli/commands.ts +1 -3
  15. package/src/cli/create.ts +159 -50
  16. package/src/cli/templates/provider/README.md.tpl +24 -7
  17. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  18. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  19. package/src/cli/templates/provider/index.ts.tpl +5 -47
  20. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  21. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  22. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  23. package/src/cli/templates/provider/operations/ping.ts.tpl +23 -0
  24. package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -0
  25. package/src/cli/templates/provider/start.ts.tpl +1 -1
  26. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  27. package/src/config/loader.ts +1206 -9
  28. package/src/define.ts +1618 -104
  29. package/src/errors.ts +12 -0
  30. package/src/i18n/catalog.ts +121 -0
  31. package/src/i18n/index.ts +2 -0
  32. package/src/i18n/keys.ts +64 -0
  33. package/src/index.ts +149 -8
  34. package/src/lint.ts +297 -42
  35. package/src/observability.ts +41 -0
  36. package/src/provider.ts +60 -3
  37. package/src/public-schema-field-lint.ts +237 -0
  38. package/src/runtime/auth-flow.ts +7 -0
  39. package/src/runtime/browser.ts +77 -21
  40. package/src/runtime/cache.ts +582 -0
  41. package/src/runtime/executor.ts +13 -1
  42. package/src/runtime/http.ts +939 -195
  43. package/src/runtime/insights.ts +11 -11
  44. package/src/runtime/instrumentation.ts +12 -4
  45. package/src/runtime/key-derivation.ts +1 -1
  46. package/src/runtime/keyring.ts +4 -3
  47. package/src/runtime/proxy-errors.ts +132 -0
  48. package/src/runtime/proxy-telemetry.ts +253 -0
  49. package/src/runtime/request-options.ts +66 -0
  50. package/src/runtime/state.ts +76 -0
  51. package/src/runtime/stealth.ts +1145 -0
  52. package/src/runtime/stt.ts +629 -0
  53. package/src/schema.ts +363 -1
  54. package/src/server/serve.ts +816 -58
  55. package/src/server/types.ts +35 -0
  56. package/src/stream.ts +210 -0
  57. package/src/testing/run.ts +17 -4
  58. package/src/types.ts +869 -50
  59. package/src/runtime/tls.ts +0 -434
  60. package/src/types/playwright-stealth.d.ts +0 -9
package/AUTHORING.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ## Generator and runtime alignment
2
2
 
3
3
  - Canonical scaffolding command: `apifuse create`
4
- - Monorepo contributors should use `apifuse create <name> --preset monorepo`
4
+ - External bounty workspaces are one-provider repositories initialized with the standalone create flow. `--preset monorepo` is an internal APIFuse maintainer path only and must reject outside the private monorepo detected by `packages/provider-sdk/package.json`.
5
5
  - Standalone bounty contributors should use `bunx @apifuse/provider-sdk@beta create <name> --yes` until this release is promoted to `latest`
6
6
  - Provider server contract is:
7
7
  - dev default `3900`
@@ -47,7 +47,7 @@ description:
47
47
  - `description` — 150+ chars English (error-level rule)
48
48
  - Every Zod field in input AND output has `.describe()` including nested objects + array items (error-level rule)
49
49
  - `fixtures.request` + `fixtures.response` both present (error-level rule)
50
- - Exactly one of `healthCheck` or `healthCheckUnsupported` 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.
50
+ - 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.
51
51
 
52
52
  ### Factored operations
53
53
 
@@ -65,6 +65,158 @@ Use `defineOperation()` when an operation is large enough to live beside helper
65
65
  - `tags`: operation-level semantic tags for retrieval (e.g., `["weather", "korea", "realtime"]`)
66
66
  - `relatedOperations`: `{ alternatives?: string[] }` — links to fallback/sibling operations
67
67
 
68
+ ### STT runtime capability for audio OTP and short transcription
69
+
70
+ Providers that need speech-to-text should use the SDK runtime capability instead
71
+ of constructing a vendor client inside provider code. Declare STT at the provider
72
+ level, then call `ctx.stt` from operation handlers or auth-flow handlers.
73
+
74
+ <!-- @magic-start:sample -->
75
+ ```ts
76
+ export default defineProvider({
77
+ id: "example-provider",
78
+ // ...metadata, auth, operations, allowedHosts
79
+ stt: { mode: "required" },
80
+ operations: {
81
+ verifyAudioOtp: {
82
+ input: z.object({
83
+ audioBase64: z.string().describe("Base64-encoded short OTP audio"),
84
+ mediaType: z.string().optional().describe("Audio MIME type"),
85
+ }),
86
+ output: z.object({ code: z.string().describe("Verification code") }),
87
+ async handler(ctx, input) {
88
+ const transcript = await ctx.stt.transcribe({
89
+ audio: {
90
+ kind: "base64",
91
+ data: input.audioBase64,
92
+ mediaType: input.mediaType,
93
+ },
94
+ language: "ko-KR",
95
+ mode: "otp",
96
+ verificationCode: { codeLengths: [4, 6] },
97
+ });
98
+
99
+ const code =
100
+ transcript.verificationCode?.code ??
101
+ ctx.stt.extractVerificationCode(transcript.text, {
102
+ locale: "ko-KR",
103
+ codeLengths: [4, 6],
104
+ }).code;
105
+
106
+ return { code };
107
+ },
108
+ healthCheckUnsupported: {
109
+ reason: "Audio OTP transcription is cost-bearing and requires explicit smoke evidence.",
110
+ },
111
+ },
112
+ },
113
+ });
114
+ ```
115
+ <!-- @magic-end:sample -->
116
+
117
+ Best-practice rules:
118
+
119
+ - `stt: { mode: "required" }` is the production path for providers that depend
120
+ on STT; APIFuse provider manifests project STT credentials, model config, and
121
+ Cloudflare egress only for required STT. Use `mode: "optional"` only when STT
122
+ is a host/test override or truly best-effort capability that can remain
123
+ unavailable in production.
124
+ - Do not assume OTPs are always four digits. Configure accepted lengths, for
125
+ example `[4, 6]`, and keep the returned code as a string to preserve leading
126
+ zeros.
127
+ - Prompts are hints, not correctness guarantees. General transcription sends no
128
+ prompt by default. OTP mode may send a default digit-preserving hint. Use a
129
+ custom `initialPrompt` only with `promptPolicy: "custom-hint"`, and do not log
130
+ prompts, transcripts, raw audio, or OTP values.
131
+ - STT v1 accepts JSON-safe base64 audio only. Do not fetch arbitrary audio URLs
132
+ from provider code; URL input needs separate SSRF/private-network policy.
133
+ - Local and production wiring use the same env-backed runtime path. For the
134
+ Cloudflare Workers AI backend, set `APIFUSE__STT__BACKEND=cloudflare-workers-ai`,
135
+ `APIFUSE__STT__MODEL=@cf/openai/whisper-large-v3-turbo`,
136
+ `APIFUSE__CLOUDFLARE__ACCOUNT_ID`, and `APIFUSE__STT__CLOUDFLARE_API_TOKEN` in `.env.local` or the
137
+ provider workload environment. Do not deploy a Cloudflare Worker proxy for the
138
+ MVP; the SDK runtime calls Workers AI REST directly.
139
+ - Submission checks and health checks must not invoke live STT by default.
140
+ Provide explicit smoke evidence when a provider depends on audio OTP behavior.
141
+
142
+
143
+ ### Health journey DX for SMS/payment flows
144
+
145
+ Use `defineSmsOtpMatcher()` plus `defineHealthJourney()` when a real health signal requires an OTP ceremony and a safe handoff boundary. Keep matcher fields standards-backed: ISO 3166-1 alpha-2 `country`, BCP 47 `locale`, E.164 `phoneNumber` when present, ISO 8601 durations, and `nationalServiceCode` origins for local service senders. Do not add custom allowlist fields such as `senderAllowlist`; model the sender as an origin instead.
146
+
147
+ <!-- @magic-start:sample -->
148
+ ```ts
149
+ import {
150
+ defineHealthJourney,
151
+ defineProvider,
152
+ defineSmsOtpMatcher,
153
+ every,
154
+ } from "@apifuse/provider-sdk";
155
+
156
+ const phoneOtp = defineSmsOtpMatcher({
157
+ id: "phone-otp",
158
+ country: "KR",
159
+ locale: "ko-KR",
160
+ origins: [
161
+ {
162
+ kind: "nationalServiceCode",
163
+ country: "KR",
164
+ value: "16615270",
165
+ display: "1661-5270",
166
+ },
167
+ ],
168
+ code: { pattern: /인증번호는\s*\[([0-9]{4})\]/, capture: 1 },
169
+ maxAge: "PT5M",
170
+ waitTimeout: "PT2M30S",
171
+ clockSkew: "PT10S",
172
+ });
173
+
174
+ const paymentWebviewJourney = defineHealthJourney({
175
+ id: "sms-payment-webview",
176
+ schedule: every("8h", { jitter: "PT20M" }),
177
+ timeout: "PT5M",
178
+ cooldown: "PT8H",
179
+ requiredSecrets: [
180
+ "APIFUSE__HEALTH_MONITOR__PROVIDER_PHONE",
181
+ "APIFUSE__HEALTH_MONITOR__PROVIDER_PASSWORD",
182
+ "APIFUSE__HEALTH_MONITOR__PROVIDER_CANARY_ORDER_JSON",
183
+ ],
184
+ coversOperations: ["verify-phone", "confirm-phone", "place-order"],
185
+ smsMatchers: [phoneOtp],
186
+ steps: [
187
+ { id: "send-phone-otp", kind: "operation", operationId: "verify-phone" },
188
+ { id: "wait-phone-otp", kind: "smsOtp", usesSmsMatcher: "phone-otp" },
189
+ { id: "confirm-phone-otp", kind: "operation", operationId: "confirm-phone" },
190
+ {
191
+ id: "create-payment-webview",
192
+ kind: "operation",
193
+ operationId: "place-order",
194
+ safeBoundary: "paymentWebviewUrl",
195
+ },
196
+ ],
197
+ });
198
+
199
+ export default defineProvider({
200
+ id: "example-provider",
201
+ // ...metadata, auth, operations, allowedHosts
202
+ healthJourneys: [paymentWebviewJourney],
203
+ });
204
+ ```
205
+ <!-- @magic-end:sample -->
206
+
207
+ The journey runner supplies `ctx.gateway`, `ctx.sms.waitForOtp()`, `ctx.journal.sideEffect()`, `ctx.state`, and `ctx.event.operation()` to the optional journey `run` function. Provider authors should keep `run` small: call the covered operations in step order, stop at the declared safe boundary, and let the generated health metadata carry schedule, timeout, required secret, and SMS matcher information to the health monitor.
208
+
209
+ For authenticated journeys, open a fresh connection inside `run` with `ctx.gateway.connect({ input: { ... } })`, execute covered operations with the returned `connectionId`, and disconnect in a `finally` block. Do not require or store long-lived `HEALTH_MONITOR_*_CONNECTION_ID` secrets; those stale connection IDs can hide broken login ceremonies.
210
+
211
+ Use the runtime capabilities narrowly:
212
+
213
+ - `ctx.gateway.execute()` is the default path for operation health evidence; the runner records operation success/failure automatically.
214
+ - `ctx.journal.sideEffect()` wraps non-replayable provider mutations such as create/cancel/send operations.
215
+ - `ctx.state.namespace(name, policy)` stores bounded lifecycle memory and recovery cursors with TTL/quota/value-size policy. It is not a replacement for the side-effect journal.
216
+ - `ctx.event.operation()` records only synthetic operation outcomes proven by the journey, such as recovery/manual-review checks that are not direct gateway calls. The runtime rejects events for operations outside `coversOperations`.
217
+
218
+ Do not import `apps/health-monitor`, generated health artifacts, database repositories, schedulers, or recorders from provider code. If a journey needs provider-specific helper code, place it under the provider package (for example `providers/<id>/health-journeys/*`) and keep the SDK boundary generic.
219
+
68
220
  ### External bounty submission evidence
69
221
 
70
222
  External contributors are expected to submit standalone Provider source plus:
@@ -91,12 +243,15 @@ deployment projection checks, and release workflows.
91
243
  - Auth-flow debugging starts with `/auth/start`, continues with
92
244
  `/auth/continue`, and carries returned `contextPatch` values into the next
93
245
  request's `context`.
94
- - TLS/browser providers may require local runtime setup outside Provider code:
95
- use `bun pm untrusted`/`bun pm trust koffi` for blocked native TLS dependency
96
- scripts, `browser.engine: "playwright-stealth"` for TypeScript browser
97
- Providers (`nodriver` is Python-runtime only), `bunx playwright install
98
- chromium` for local Playwright browser assets, or
99
- `CDP_POOL_URL`/`APIFUSE_CDP_POOL_URL` for remote browser debugging.
246
+ - Stealth/browser providers may require local runtime setup outside Provider code:
247
+ keep access-sensitive operations on `ctx.stealth.fetch()` with an SDK stealth
248
+ `profile`; the TypeScript runtime uses `impit` behind that interface, so do
249
+ not add per-operation JA3, HTTP/2 SETTINGS, or pseudo-header tuning. `ctx.stealth`
250
+ supports Chrome/Firefox-style profiles; use `browser.engine:
251
+ "playwright-stealth"` for Safari-specific or real browser Providers
252
+ (`nodriver` is Python-runtime only); install local browser assets with
253
+ `bunx playwright install chromium`, or set
254
+ `APIFUSE__CDP_POOL__URL` for remote browser debugging.
100
255
 
101
256
  ### Running the pre-submission report
102
257
 
package/CHANGELOG.md CHANGED
@@ -1,7 +1,14 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.1.0-beta.4
4
+
5
+ - Align `apifuse create` with the bounty program topology: external contributors use the standalone one-provider-repository scaffold even when their assigned repo contains workspace-like files.
6
+ - Stop auto-detecting `providers/` directories as public monorepo scaffolds. `--preset monorepo` is now reserved for the private APIFuse monorepo where `packages/provider-sdk` is actually present.
7
+ - Remove public CLI/docs examples that present monorepo placement as a contributor workflow.
8
+
3
9
  ## 2.1.0-beta.3
4
10
 
11
+ - Replace the legacy TypeScript request transport with `ctx.stealth`, backed by `impit` browser-grade TLS/HTTP2 impersonation without Python runtime dependencies.
5
12
  - Add the public `apifuse submit-check` / `apifuse bounty-check` CLI for score-based pre-submission provider quality checks.
6
13
  - Ship `SUBMISSION.md` in the npm package so bounty contributors can follow the checklist without access to the private monorepo.
7
14
  - Include submit-check in generated provider validation scripts and packed-artifact smoke coverage.
@@ -12,7 +19,7 @@
12
19
  - Harden public bounty contributor DX with server-contract accurate README and generated Provider smoke examples.
13
20
  - Add packed-artifact regression checks so stale `connection: null` or missing `requestId` examples cannot ship again.
14
21
  - Extend clean-room packed SDK smoke coverage to boot the generated dev server and call `/health` plus `POST /v1/ping`.
15
- - Document credential, auth-flow, TLS, browser, and Bun trusted-dependency troubleshooting for SDK-only local development.
22
+ - Document credential, auth-flow, stealth, browser, and Bun trusted-dependency troubleshooting for SDK-only local development.
16
23
 
17
24
  ## 2.1.0-beta.1
18
25
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @apifuse/provider-sdk
2
2
 
3
- ApiFuse Provider SDK — build provider declarations and runtimes with one public SDK surface and one canonical CLI.
3
+ APIFuse Provider SDK — build provider declarations and runtimes with one public SDK surface and one canonical CLI.
4
4
 
5
5
  ## Install
6
6
 
@@ -29,13 +29,11 @@ The canonical `create` flow:
29
29
  3. runs baseline validation,
30
30
  4. prints the exact next local-dev command.
31
31
 
32
- ### Monorepo preset
32
+ ### Repository shape
33
33
 
34
- Inside the ApiFuse repository:
34
+ The Provider SDK is a public bounty-contributor tool first. External bounty workspaces are one-provider repositories initialized from the standalone create flow. The generated provider must be installable without private APIFuse monorepo access.
35
35
 
36
- ```bash
37
- apifuse create my-provider --preset monorepo
38
- ```
36
+ Do not use internal monorepo placement for bounty workspaces. Accepted provider work is imported into the private APIFuse monorepo later by maintainers or trusted automation.
39
37
 
40
38
  ## Provider server contract
41
39
 
@@ -71,7 +69,7 @@ curl -s -X POST http://localhost:3900/v1/ping \
71
69
  -d '{"requestId":"req_local_ping","input":{"value":"hello"},"headers":{}}'
72
70
  ```
73
71
 
74
- The operation request body is the same envelope used by the ApiFuse gateway:
72
+ The operation request body is the same envelope used by the APIFuse gateway:
75
73
 
76
74
  | Field | Required | Notes |
77
75
  |---|---:|---|
@@ -89,7 +87,7 @@ curl -s -X POST http://localhost:3900/v1/me \
89
87
  "requestId":"req_local_me",
90
88
  "input":{},
91
89
  "connection":{
92
- "id":"conn_local_debug",
90
+ "id":"af_con_local_debug",
93
91
  "mode":"credentials",
94
92
  "secrets":{"apiKey":"dev-only-secret"},
95
93
  "scopes":[],
@@ -125,14 +123,17 @@ the bad request path; provider/runtime failures include `code`, `message`, and
125
123
  - **Auth flows**: call `/auth/start`, then `/auth/continue` with the same
126
124
  `flowId`; preserve any returned `contextPatch` in the next local request's
127
125
  `context` object.
128
- - **TLS-sensitive providers**: if Bun reports blocked lifecycle scripts after
129
- install, run `bun pm untrusted`; when it lists trusted SDK native dependencies
130
- such as `koffi`, run `bun pm trust koffi` (or `bun pm trust`) before debugging
131
- `ctx.tls` failures.
126
+ - **Stealth-sensitive providers**: use `ctx.http` for normal JSON/REST calls and
127
+ `ctx.stealth.fetch()` when you need browser-like session or cookie control. `ctx.stealth.fetch()` uses the impit-backed browser stealth transport and accepts request
128
+ controls for `params`, `proxy`, `timeout`, `profile`, `throwOnHttpError`, and
129
+ `stealth.insecureSkipVerify`. Select an SDK stealth `profile` such as
130
+ `chrome-146`; do not tune JA3, HTTP/2 SETTINGS, or pseudo-header order in
131
+ provider code. Chrome/Firefox-style profiles are supported; use `ctx.browser`
132
+ when Safari-specific behavior is required.
132
133
  - **Browser providers**: for TypeScript Providers use `runtime: "browser"` plus
133
134
  `browser.engine: "playwright-stealth"`; `nodriver` is a Python-runtime path.
134
135
  Install local browser assets with `bunx playwright install chromium` when
135
- using the Playwright runtime, or set `CDP_POOL_URL`/`APIFUSE_CDP_POOL_URL`
136
+ using the Playwright runtime, or set `APIFUSE__CDP_POOL__URL`
136
137
  when debugging against a remote browser pool.
137
138
 
138
139
  ## Authoring ergonomics
@@ -202,7 +203,7 @@ apifuse perf <path> --operation <operation>
202
203
  ```
203
204
 
204
205
  `apifuse record` is for real upstream-backed operations that declare
205
- `upstream.baseUrl` and call the upstream through `ctx.http` or `ctx.tls`. The
206
+ `upstream.baseUrl` and call the upstream through `ctx.http` or `ctx.stealth`. The
206
207
  generated local-only `ping` operation intentionally has no upstream and should
207
208
  be replaced before recording fixtures.
208
209
 
@@ -214,7 +215,7 @@ Standalone providers include a pre-submission script:
214
215
  bun run submit-check
215
216
  ```
216
217
 
217
- This runs the public review-readiness evaluator and writes `submission-report.md`. The report contains provider metadata, a 100-point readiness score, hard blockers, warnings, checklist evidence, and remediation. Blockers override the score; fix them before posting bounty evidence. The command is offline-safe by default and does not execute arbitrary upstream calls. Add local smoke notes to your bounty issue after testing `/health` and `POST /v1/{operation}`. See [`SUBMISSION.md`](./SUBMISSION.md) for the full public-only bounty submission checklist shipped in the npm package.
218
+ This runs the public review-readiness evaluator and writes `submission-report.md`. The report contains provider metadata, a 100-point readiness score, hard blockers, warnings, checklist evidence, and remediation. Blockers override the score; fix them before posting bounty evidence. The command is offline-safe by default and does not execute arbitrary upstream calls. Add local smoke notes to your assigned workspace PR after testing `/health` and `POST /v1/{operation}`. See [`SUBMISSION.md`](./SUBMISSION.md) for the full public-only bounty submission checklist shipped in the npm package.
218
219
 
219
220
  ## Scope boundary
220
221
 
@@ -226,6 +227,6 @@ Generator v1 scaffolds **TypeScript providers only** for this redesign. Python g
226
227
  Provider cataloging, deployment enrollment, docs indexing, and runtime discovery are internal platform-registry responsibilities and are not part of the public `@apifuse/provider-sdk` contract.
227
228
 
228
229
  External bounty contributors should submit standalone Provider source plus
229
- `bun run check` / `bun run test` evidence. ApiFuse maintainers own monorepo
230
+ `bun run check` / `bun run test` evidence. APIFuse maintainers own monorepo
230
231
  import, registry generation, deployment projection checks, and release
231
232
  publishing.
package/SUBMISSION.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Provider bounty submission guide
2
2
 
3
- This guide is for public bounty contributors who only have access to the published `@apifuse/provider-sdk` package. You do **not** need the private ApiFuse monorepo to build or pre-check a Provider.
3
+ This guide is for public bounty contributors who only have access to the published `@apifuse/provider-sdk` package. You do **not** need the private APIFuse monorepo to build or pre-check a Provider.
4
4
 
5
- ## Public-only workflow
5
+ ## SDK-only workspace workflow
6
6
 
7
7
  ```bash
8
8
  bunx @apifuse/provider-sdk@beta create my-provider --yes
@@ -65,7 +65,7 @@ curl -s -X POST http://localhost:3900/v1/<operation> \
65
65
  bun run submit-check -- --smoke-note "GET /health and POST /v1/<operation> passed locally with redacted input."
66
66
  ```
67
67
 
68
- Never paste real credentials, personal data, account numbers, access tokens, cookies, or unredacted upstream responses into issue comments or reports.
68
+ Never paste real credentials, personal data, account numbers, access tokens, cookies, or unredacted upstream responses into workspace PR comments, chat, or reports.
69
69
 
70
70
  ## Submission evidence checklist
71
71
 
@@ -83,4 +83,4 @@ Include the following when you submit a Provider for review:
83
83
 
84
84
  ## Maintainer-owned follow-up
85
85
 
86
- After public evidence is submitted, ApiFuse maintainers import accepted standalone Provider work into the private monorepo and run internal registry, generated artifact, deployment projection, and CI checks. Public contributors are not expected to run those private checks.
86
+ After workspace evidence is submitted, APIFuse maintainers import accepted standalone Provider work into the private monorepo and run internal registry, generated artifact, deployment projection, and CI checks. External contributors are not expected to run those private checks.
@@ -2,16 +2,18 @@
2
2
 
3
3
  import { existsSync } from "node:fs";
4
4
  import { dirname, relative, resolve } from "node:path";
5
-
6
5
  import type { ProviderDefinition } from "../src";
7
6
  import {
8
7
  createBrowserClient,
9
8
  createCredentialContext,
10
9
  createEnvContext,
11
10
  createHttpClient,
12
- createTlsClient,
11
+ createProviderCache,
12
+ createStealthClient,
13
+ createSttClientFromEnv,
13
14
  ProviderError,
14
15
  } from "../src";
16
+ import { createUnsupportedProviderRuntimeState } from "../src/runtime/state";
15
17
  import { createTraceContext } from "../src/runtime/trace";
16
18
  import type { BrowserClient, ProviderContext } from "../src/types";
17
19
 
@@ -35,7 +37,7 @@ export async function main() {
35
37
  );
36
38
 
37
39
  const { startDevServer } = await import("../src/dev");
38
- const port = Number(process.env.PORT) || 3900;
40
+ const port = Number(process.env.APIFUSE__RUNTIME__PORT) || 3900;
39
41
 
40
42
  startDevServer(provider, { port });
41
43
 
@@ -85,8 +87,11 @@ export function createProviderContext(provider: ProviderDefinition): {
85
87
  })
86
88
  : createUnsupportedBrowserStub(),
87
89
  http: createHttpClient(),
90
+ cache: createProviderCache({ providerId: provider.id }),
91
+ state: createUnsupportedProviderRuntimeState(),
88
92
  trace: createTraceContext(),
89
- tls: createTlsClient("http://localhost"),
93
+ stealth: createStealthClient("http://localhost"),
94
+ stt: createSttClientFromEnv(provider.stt),
90
95
  };
91
96
 
92
97
  return { ctx };
@@ -129,7 +134,9 @@ function renderHotReloadCommand(providerPath: string, port: number): string {
129
134
  const devEntry = resolve(providerPath, "dev.ts");
130
135
  if (existsSync(devEntry)) {
131
136
  const relativeDevEntry = relative(process.cwd(), devEntry) || "dev.ts";
132
- const portPrefix = process.env.PORT ? `PORT=${port} ` : "";
137
+ const portPrefix = process.env.APIFUSE__RUNTIME__PORT
138
+ ? `APIFUSE__RUNTIME__PORT=${port} `
139
+ : "";
133
140
  return `${portPrefix}bun --hot ${relativeDevEntry}`;
134
141
  }
135
142
  return "rerun `apifuse dev` after edits (no dev.ts entrypoint found)";
@@ -38,6 +38,13 @@ const requiredPaths = [
38
38
  "src/cli/create.ts",
39
39
  "src/cli/templates/provider/index.ts.tpl",
40
40
  "src/cli/templates/provider/README.md.tpl",
41
+ "src/cli/templates/provider/meta.ts.tpl",
42
+ "src/cli/templates/provider/domain/README.md.tpl",
43
+ "src/cli/templates/provider/mappers/README.md.tpl",
44
+ "src/cli/templates/provider/operations/index.ts.tpl",
45
+ "src/cli/templates/provider/operations/ping.ts.tpl",
46
+ "src/cli/templates/provider/schemas/ping.ts.tpl",
47
+ "src/cli/templates/provider/upstream/README.md.tpl",
41
48
  ];
42
49
  const forbiddenMatches = filePaths.filter(
43
50
  (path) =>
@@ -113,9 +120,9 @@ function assertPublicSmokeDocs(label: string, content: string): void {
113
120
  );
114
121
  }
115
122
 
116
- if (!content.includes("bun pm untrusted")) {
123
+ if (!content.includes("impit")) {
117
124
  throw new Error(
118
- `${label} must include Bun trusted-dependency troubleshooting for TLS/browser bounties.`,
125
+ `${label} must include impit stealth runtime guidance for TLS/browser bounties.`,
119
126
  );
120
127
  }
121
128
 
@@ -39,17 +39,19 @@ const PING_RESPONSE_SCHEMA = z.object({
39
39
  error: z.unknown().optional(),
40
40
  });
41
41
 
42
- const KEEP_TEMP = process.env.APIFUSE_PACK_SMOKE_KEEP_TEMP === "1";
42
+ const KEEP_TEMP = process.env.APIFUSE__PACK_SMOKE__KEEP_TEMP === "1";
43
43
 
44
44
  const tempRoot = mkdtempSync(
45
45
  join(tmpdir(), "apifuse-provider-sdk-pack-smoke-"),
46
46
  );
47
47
  const packDir = join(tempRoot, "pack");
48
48
  const consumerDir = join(tempRoot, "consumer");
49
+ const externalWorkspaceDir = join(tempRoot, "external-workspace");
49
50
 
50
51
  try {
51
52
  mkdirSync(packDir, { recursive: true });
52
53
  mkdirSync(consumerDir, { recursive: true });
54
+ mkdirSync(join(externalWorkspaceDir, "providers"), { recursive: true });
53
55
 
54
56
  const packed = packSdk(packDir);
55
57
  const tarballPath = resolve(packDir, packed.filename);
@@ -97,6 +99,11 @@ try {
97
99
  run("bun", ["run", "test"], generatedProviderDir);
98
100
  assertGeneratedReadme(generatedProviderDir);
99
101
  await smokeGeneratedDevServer(generatedProviderDir);
102
+ assertExternalWorkspaceTopology(
103
+ cliBin,
104
+ externalWorkspaceDir,
105
+ tarballSpecifier,
106
+ );
100
107
 
101
108
  console.log(
102
109
  `Provider SDK packed-artifact smoke passed: ${tarballPath} -> ${generatedProviderDir}`,
@@ -109,6 +116,103 @@ try {
109
116
  }
110
117
  }
111
118
 
119
+ function assertExternalWorkspaceTopology(
120
+ cliBin: string,
121
+ externalWorkspaceDir: string,
122
+ tarballSpecifier: string,
123
+ ): void {
124
+ writeFileSync(
125
+ join(externalWorkspaceDir, "package.json"),
126
+ `${JSON.stringify(
127
+ {
128
+ private: true,
129
+ type: "module",
130
+ workspaces: ["providers/*"],
131
+ },
132
+ null,
133
+ 2,
134
+ )}\n`,
135
+ );
136
+
137
+ run(
138
+ "bun",
139
+ [
140
+ cliBin,
141
+ "create",
142
+ "external-workspace-smoke",
143
+ "--yes",
144
+ "--json",
145
+ "--sdk-specifier",
146
+ tarballSpecifier,
147
+ ],
148
+ externalWorkspaceDir,
149
+ );
150
+
151
+ const generatedProviderDir = join(
152
+ externalWorkspaceDir,
153
+ "external-workspace-smoke",
154
+ );
155
+ const forbiddenProviderDir = join(
156
+ externalWorkspaceDir,
157
+ "providers",
158
+ "external-workspace-smoke",
159
+ );
160
+ if (!existsSync(generatedProviderDir)) {
161
+ throw new Error(
162
+ "Public create must generate a one-provider repository at <name>/ even when providers/ exists.",
163
+ );
164
+ }
165
+ if (existsSync(forbiddenProviderDir)) {
166
+ throw new Error(
167
+ "Public create must not generate providers/<name>/ in external bounty workspaces.",
168
+ );
169
+ }
170
+
171
+ const packageJson = JSON.parse(
172
+ readFileSync(join(generatedProviderDir, "package.json"), "utf8"),
173
+ );
174
+ const sdkDependency = packageJson?.dependencies?.["@apifuse/provider-sdk"];
175
+ if (sdkDependency !== tarballSpecifier) {
176
+ throw new Error(
177
+ `Expected generated provider to depend on packed SDK ${tarballSpecifier}, got ${sdkDependency}`,
178
+ );
179
+ }
180
+ if (JSON.stringify(packageJson).includes("workspace:")) {
181
+ throw new Error(
182
+ "External bounty workspace scaffold must not contain workspace: dependencies.",
183
+ );
184
+ }
185
+
186
+ run("bun", ["install"], generatedProviderDir);
187
+ run("bun", ["run", "check"], generatedProviderDir);
188
+ run("bun", ["run", "submit-check"], generatedProviderDir);
189
+ run("bun", ["run", "test"], generatedProviderDir);
190
+
191
+ const monorepoAttempt = spawnSync(
192
+ "bun",
193
+ [cliBin, "create", "bad-monorepo-smoke", "--preset", "monorepo", "--yes"],
194
+ {
195
+ cwd: externalWorkspaceDir,
196
+ env: { ...process.env, APIFUSE__SDK__SPECIFIER: tarballSpecifier },
197
+ encoding: "utf8",
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ },
200
+ );
201
+ if (monorepoAttempt.status === 0) {
202
+ throw new Error(
203
+ "--preset monorepo must reject outside the private APIFuse monorepo.",
204
+ );
205
+ }
206
+ const rejectionOutput = `${monorepoAttempt.stdout}\n${monorepoAttempt.stderr}`;
207
+ if (
208
+ !rejectionOutput.includes(
209
+ "Monorepo preset is internal to the APIFuse repository",
210
+ )
211
+ ) {
212
+ throw new Error(`Unexpected monorepo rejection output: ${rejectionOutput}`);
213
+ }
214
+ }
215
+
112
216
  function packSdk(destination: string): { filename: string } {
113
217
  const raw = execFileSync(
114
218
  "npm",
@@ -162,9 +266,9 @@ function assertGeneratedReadme(providerDir: string): void {
162
266
  "Generated README is missing browser runtime troubleshooting guidance.",
163
267
  );
164
268
  }
165
- if (!readme.includes("bun pm untrusted")) {
269
+ if (!readme.includes("impit")) {
166
270
  throw new Error(
167
- "Generated README is missing Bun trusted-dependency troubleshooting guidance.",
271
+ "Generated README is missing impit stealth runtime guidance.",
168
272
  );
169
273
  }
170
274
  if (!readme.includes("bun run submit-check")) {
@@ -183,7 +287,8 @@ async function smokeGeneratedDevServer(providerDir: string): Promise<void> {
183
287
  const port = await getAvailablePort();
184
288
  const server = spawn("bun", ["run", "dev"], {
185
289
  cwd: providerDir,
186
- env: { ...process.env, PORT: String(port) },
290
+ env: { ...process.env, APIFUSE__RUNTIME__PORT: String(port) },
291
+ detached: process.platform !== "win32",
187
292
  stdio: ["ignore", "pipe", "pipe"],
188
293
  });
189
294
  let output = "";
@@ -286,11 +391,11 @@ async function stopServer(server: ChildProcess): Promise<void> {
286
391
  if (server.exitCode !== null) {
287
392
  return;
288
393
  }
289
- server.kill("SIGTERM");
394
+ killProcessTree(server, "SIGTERM");
290
395
  await new Promise<void>((resolvePromise) => {
291
396
  const timeout = setTimeout(() => {
292
397
  if (server.exitCode === null) {
293
- server.kill("SIGKILL");
398
+ killProcessTree(server, "SIGKILL");
294
399
  }
295
400
  resolvePromise();
296
401
  }, 2_000);
@@ -300,3 +405,19 @@ async function stopServer(server: ChildProcess): Promise<void> {
300
405
  });
301
406
  });
302
407
  }
408
+
409
+ function killProcessTree(server: ChildProcess, signal: NodeJS.Signals): void {
410
+ if (server.pid === undefined) {
411
+ return;
412
+ }
413
+
414
+ try {
415
+ if (process.platform === "win32") {
416
+ server.kill(signal);
417
+ return;
418
+ }
419
+ process.kill(-server.pid, signal);
420
+ } catch {
421
+ server.kill(signal);
422
+ }
423
+ }