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

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.
package/AUTHORING.md CHANGED
@@ -53,6 +53,30 @@ description:
53
53
 
54
54
  Use `defineOperation()` when an operation is large enough to live beside helper functions or in a separate module. It preserves the same type inference as inline `defineProvider()` operations and can be placed directly in the provider `operations` map. `defineProvider()` accepts Zod and Standard Schema v1-compatible schemas. If config validation fails, the SDK names the field to fix, for example `runtime`, `auth.mode`, `operations.<id>.handler`, or `operations.<id>.fixtures.response`.
55
55
 
56
+ ### Health assertion context
57
+
58
+ `healthCheck.cases[].assertions` receives a `HealthCheckAssertionContext` with
59
+ `data`, `status`, `durationMs`, and optional `meta`. `data` is typed from the
60
+ operation output schema, so assertions should inspect normalized output instead
61
+ of reaching into transport internals.
62
+
63
+ <!-- @magic-start:sample -->
64
+ ```ts
65
+ healthCheck: {
66
+ interval: "5m",
67
+ cases: [{
68
+ name: "lookup baseline",
69
+ input: { q: "btc" },
70
+ assertions: ({ data, status, durationMs }) => {
71
+ if (status !== 200 || data.results.length === 0 || durationMs > 3000) {
72
+ return { status: "degraded", label: "lookup baseline changed" };
73
+ }
74
+ },
75
+ }],
76
+ }
77
+ ```
78
+ <!-- @magic-end:sample -->
79
+
56
80
  ### Strongly recommended (warn-level rules)
57
81
 
58
82
  - `description` includes "use" AND "when" phrasing
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.1.0-beta.5
4
+
5
+ - Republish the bounty workspace DX hardening that accepts generated readonly metadata and factored `defineOperation()` maps during standalone TypeScript checks.
6
+ - Ensure new bounty workspaces can install the public SDK version that matches their generated scaffold and pass `bun run check` immediately after bootstrap.
7
+
3
8
  ## 2.1.0-beta.4
4
9
 
5
10
  - 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.
package/README.md CHANGED
@@ -149,6 +149,9 @@ const search = defineOperation({
149
149
  async handler(ctx, input) {
150
150
  return { count: input.q.length }
151
151
  },
152
+ healthCheckUnsupported: {
153
+ reason: "Example operation only; replace with a real upstream probe.",
154
+ },
152
155
  })
153
156
 
154
157
  export default defineProvider({
@@ -174,6 +177,24 @@ Every operation must declare exactly one of:
174
177
  The generated `ping` operation uses `healthCheckUnsupported` only because it is
175
178
  a local scaffold check, not a real upstream API probe.
176
179
 
180
+ `healthCheck.cases[].assertions` receives `{ data, status, durationMs, meta }`.
181
+ `data` is the operation output parsed by the declared output schema:
182
+
183
+ ```ts
184
+ healthCheck: {
185
+ interval: "5m",
186
+ cases: [{
187
+ name: "search responds",
188
+ input: { q: "weather" },
189
+ assertions: ({ data, status, durationMs }) => {
190
+ if (status !== 200 || data.count < 1 || durationMs > 3000) {
191
+ return { status: "degraded", label: "unexpected search baseline" }
192
+ }
193
+ },
194
+ }],
195
+ }
196
+ ```
197
+
177
198
  ### Operation annotations
178
199
 
179
200
  Operations declare non-functional metadata via `annotations`:
@@ -195,11 +216,11 @@ Operations declare non-functional metadata via `annotations`:
195
216
  apifuse create <name>
196
217
  apifuse dev [path]
197
218
  apifuse check [path]
198
- apifuse record [path] --operation <operation> --params '{"value":"hello"}'
219
+ apifuse record providers/airkorea --operation realtime --params '{"stationName":"종로구"}'
199
220
  apifuse test [path]
200
221
  apifuse submit-check [path] --tier bronze --markdown submission-report.md
201
222
  apifuse bounty-check [path]
202
- apifuse perf <path> --operation <operation>
223
+ apifuse perf providers/airkorea --operation realtime --params '{"stationName":"종로구"}'
203
224
  ```
204
225
 
205
226
  `apifuse record` is for real upstream-backed operations that declare
@@ -43,6 +43,7 @@ type CliArgs = {
43
43
  exportPath?: string;
44
44
  flame: boolean;
45
45
  operation: string;
46
+ params?: string;
46
47
  runs: number;
47
48
  warmup: number;
48
49
  };
@@ -81,6 +82,21 @@ const DEFAULT_RUNS = 10;
81
82
  const DEFAULT_WARMUP = 2;
82
83
  const DEFAULT_CONCURRENCY = 1;
83
84
  const BAR_WIDTH = 20;
85
+ const HELP_TEXT = `Usage: apifuse perf <provider-path> --operation <operation> [options]
86
+
87
+ Options:
88
+ --operation, -o <name> operation to profile (required)
89
+ --params, -p <json> JSON input template; falls back to fixtures.request or {}
90
+ --runs, -n <number> number of runs (default: 10)
91
+ --warmup <number> warmup runs (default: 2)
92
+ --concurrency, -c <n> concurrent requests (default: 1)
93
+ --compare-proxy run with proxy on/off and compare
94
+ --export <path> export results to JSON file
95
+ --flame generate flamegraph SVG
96
+ --help, -h show this help
97
+
98
+ Example:
99
+ apifuse perf providers/airkorea --operation realtime --params '{"stationName":"jongno"}' --runs 5`;
84
100
 
85
101
  export async function main() {
86
102
  try {
@@ -94,7 +110,11 @@ export async function main() {
94
110
  const inputSchema = getOperationSchema(provider, operation, "input");
95
111
  const outputSchema = getOperationSchema(provider, operation, "output");
96
112
  const fixtureReplay = await loadFixtureReplay(providerDirectory);
97
- const inputTemplate = resolveInputTemplate(provider, inputSchema);
113
+ const inputTemplate = resolveInputTemplate(
114
+ provider,
115
+ inputSchema,
116
+ args.params,
117
+ );
98
118
 
99
119
  const directSuite = await runProfileSuite({
100
120
  args,
@@ -184,6 +204,7 @@ function parseArgs(argv: string[]): CliArgs {
184
204
  let compareProxy = false;
185
205
  let exportPath: string | undefined;
186
206
  let flame = false;
207
+ let params: string | undefined;
187
208
 
188
209
  for (let index = 0; index < argv.length; index += 1) {
189
210
  const arg = argv[index];
@@ -201,6 +222,11 @@ function parseArgs(argv: string[]): CliArgs {
201
222
  continue;
202
223
  }
203
224
 
225
+ if (arg === "--help" || arg === "-h") {
226
+ console.log(HELP_TEXT);
227
+ process.exit(0);
228
+ }
229
+
204
230
  if (arg === "--compare-proxy") {
205
231
  compareProxy = true;
206
232
  continue;
@@ -222,6 +248,22 @@ function parseArgs(argv: string[]): CliArgs {
222
248
  continue;
223
249
  }
224
250
 
251
+ if (arg === "--params" || arg === "-p") {
252
+ params = requireArgValue(argv, index, arg);
253
+ index += 1;
254
+ continue;
255
+ }
256
+
257
+ if (arg.startsWith("--params=")) {
258
+ params = arg.slice("--params=".length);
259
+ continue;
260
+ }
261
+
262
+ if (arg.startsWith("-p=")) {
263
+ params = arg.slice("-p=".length);
264
+ continue;
265
+ }
266
+
225
267
  if (arg.startsWith("-o=")) {
226
268
  operation = arg.slice("-o=".length);
227
269
  continue;
@@ -294,20 +336,7 @@ function parseArgs(argv: string[]): CliArgs {
294
336
  }
295
337
 
296
338
  if (!providerPath || !operation) {
297
- throw new Error(
298
- [
299
- "Usage: apifuse perf <provider-path> [options]",
300
- "",
301
- "Options:",
302
- " --operation, -o <name> operation to profile (required)",
303
- " --runs, -n <number> number of runs (default: 10)",
304
- " --warmup <number> warmup runs (default: 2)",
305
- " --concurrency, -c <n> concurrent requests (default: 1)",
306
- " --compare-proxy run with proxy on/off and compare",
307
- " --export <path> export results to JSON file",
308
- " --flame generate flamegraph SVG",
309
- ].join("\n"),
310
- );
339
+ throw new Error(HELP_TEXT);
311
340
  }
312
341
 
313
342
  return {
@@ -317,6 +346,7 @@ function parseArgs(argv: string[]): CliArgs {
317
346
  exportPath,
318
347
  flame,
319
348
  operation,
349
+ params,
320
350
  runs,
321
351
  warmup,
322
352
  };
@@ -411,7 +441,18 @@ function isSchema(value: unknown): value is { parse(input: unknown): unknown } {
411
441
  function resolveInputTemplate(
412
442
  provider: ProviderDefinition,
413
443
  inputSchema: { parse(input: unknown): unknown },
444
+ params: string | undefined,
414
445
  ): unknown {
446
+ if (params !== undefined) {
447
+ try {
448
+ return inputSchema.parse(JSON.parse(params));
449
+ } catch (error) {
450
+ throw new Error(
451
+ `Failed to parse --params JSON or validate input: ${error instanceof Error ? error.message : String(error)}`,
452
+ );
453
+ }
454
+ }
455
+
415
456
  const firstOp = Object.values(provider.operations)[0];
416
457
  if (firstOp?.fixtures?.request !== undefined) {
417
458
  return firstOp.fixtures.request;
@@ -1095,7 +1136,7 @@ function cloneValue<T>(value: T): T {
1095
1136
 
1096
1137
  function handleCliError(error: unknown): never {
1097
1138
  const message = error instanceof Error ? error.message : String(error);
1098
- console.error(message);
1139
+ console.error(`[apifuse perf] ${message}`);
1099
1140
  process.exit(1);
1100
1141
  }
1101
1142
 
@@ -15,7 +15,10 @@ import {
15
15
  type HttpClient,
16
16
  type ProviderContext,
17
17
  type ProviderDefinition,
18
+ ProviderError,
18
19
  type StealthClient,
20
+ TransportError,
21
+ ValidationError,
19
22
  } from "../src";
20
23
 
21
24
  type CliArgs = {
@@ -30,56 +33,77 @@ type ProviderRuntime = ProviderDefinition;
30
33
 
31
34
  type MutableRecord = Record<string, unknown>;
32
35
 
33
- export async function main() {
34
- const args = parseArgs(normalizeArgs(process.argv.slice(2)));
35
- const location = resolveProviderLocation(args.providerPath);
36
- const provider = await loadProvider(location.rootDir);
37
- const operationName = resolveOperationName(provider, args.operation);
38
- const operation = provider.operations[operationName];
39
- const parsedParams = parseParams(operation, args.params);
40
-
41
- const capture = createCaptureContext(
42
- provider,
43
- resolveOperationBaseUrl(provider, operationName),
44
- );
36
+ const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params '<json>'
45
37
 
46
- console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
38
+ Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
47
39
 
48
- const result = await executeOperation(
49
- provider,
50
- operationName,
51
- capture.ctx,
52
- parsedParams,
53
- );
54
- const captured = capture.getCapturedRaw();
40
+ Options:
41
+ --operation, -o <name> operation to call
42
+ --params, -p <json> JSON input passed to the operation (default: {})
43
+ --append append to an existing array fixture
44
+ --sanitize redact common token/header fields (default)
45
+ --no-sanitize write the captured upstream payload as-is
46
+ --help, -h show this help
55
47
 
56
- if (captured === undefined) {
57
- throw new Error(
58
- `No upstream response was captured for ${provider.id}.${operationName}.`,
48
+ Example:
49
+ apifuse record providers/airkorea --operation realtime --params '{"stationName":"jongno"}'`;
50
+
51
+ export async function main() {
52
+ try {
53
+ const args = parseArgs(normalizeArgs(process.argv.slice(2)));
54
+ const location = resolveProviderLocation(args.providerPath);
55
+ const provider = await loadProvider(location.rootDir);
56
+ const operationName = resolveOperationName(provider, args.operation);
57
+ const operation = provider.operations[operationName];
58
+ const parsedParams = parseParams(operation, args.params);
59
+
60
+ const capture = createCaptureContext(
61
+ provider,
62
+ resolveOperationBaseUrl(provider, operationName),
59
63
  );
60
- }
61
64
 
62
- const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
63
- const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
64
- const nextPayload = await prepareFixturePayload(
65
- fixturePath,
66
- rawPayload,
67
- args.append,
68
- );
65
+ console.log(
66
+ `[apifuse record] Calling ${operationName} on ${provider.id}...`,
67
+ );
69
68
 
70
- await mkdir(dirname(fixturePath), { recursive: true });
71
- await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
69
+ const result = await executeOperation(
70
+ provider,
71
+ operationName,
72
+ capture.ctx,
73
+ parsedParams,
74
+ );
75
+ const captured = capture.getCapturedRaw();
72
76
 
73
- console.log(
74
- `[apifuse record] Captured response (${formatBytes(
75
- Buffer.byteLength(JSON.stringify(rawPayload)),
76
- )})`,
77
- );
78
- console.log(
79
- `[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
80
- );
77
+ if (captured === undefined) {
78
+ throw new Error(
79
+ `No upstream response was captured for ${provider.id}.${operationName}.`,
80
+ );
81
+ }
82
+
83
+ const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
84
+ const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
85
+ const nextPayload = await prepareFixturePayload(
86
+ fixturePath,
87
+ rawPayload,
88
+ args.append,
89
+ );
81
90
 
82
- void result;
91
+ await mkdir(dirname(fixturePath), { recursive: true });
92
+ await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
93
+
94
+ console.log(
95
+ `[apifuse record] Captured response (${formatBytes(
96
+ Buffer.byteLength(JSON.stringify(rawPayload)),
97
+ )})`,
98
+ );
99
+ console.log(
100
+ `[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
101
+ );
102
+
103
+ void result;
104
+ } catch (error) {
105
+ handleCliError(error);
106
+ }
83
107
  }
84
108
 
85
109
  function normalizeArgs(argv: string[]): string[] {
@@ -107,6 +131,11 @@ function parseArgs(argv: string[]): CliArgs {
107
131
  continue;
108
132
  }
109
133
 
134
+ if (arg === "--help" || arg === "-h") {
135
+ console.log(HELP_TEXT);
136
+ process.exit(0);
137
+ }
138
+
110
139
  if (arg.startsWith("--operation=")) {
111
140
  operation = arg.slice("--operation=".length);
112
141
  continue;
@@ -158,6 +187,43 @@ function parseArgs(argv: string[]): CliArgs {
158
187
  return { append, providerPath, operation, params, sanitize };
159
188
  }
160
189
 
190
+ function handleCliError(error: unknown): never {
191
+ const message = formatCliError(error);
192
+ console.error(`[apifuse record] ${message}`);
193
+ process.exit(1);
194
+ }
195
+
196
+ function formatCliError(error: unknown): string {
197
+ if (error instanceof TransportError) {
198
+ return [
199
+ error.message,
200
+ error.upstreamStatus ? `status=${error.upstreamStatus}` : undefined,
201
+ error.options?.retryable !== undefined
202
+ ? `retryable=${String(error.options.retryable)}`
203
+ : undefined,
204
+ error.fix ? `fix=${error.fix}` : undefined,
205
+ ]
206
+ .filter(Boolean)
207
+ .join(" ");
208
+ }
209
+
210
+ if (error instanceof ProviderError || error instanceof ValidationError) {
211
+ return [
212
+ error.message,
213
+ error.code ? `code=${error.code}` : undefined,
214
+ error.fix,
215
+ ]
216
+ .filter(Boolean)
217
+ .join(" ");
218
+ }
219
+
220
+ if (error instanceof Error) {
221
+ return error.message;
222
+ }
223
+
224
+ return String(error);
225
+ }
226
+
161
227
  function resolveProviderLocation(inputPath?: string) {
162
228
  const originalInput = inputPath ?? process.cwd();
163
229
  const resolvedInput = resolve(process.cwd(), originalInput);
@@ -240,6 +240,7 @@ export async function buildSubmitCheckReport(
240
240
  checks.push(scoreAuthSafety(provider));
241
241
  checks.push(scoreSmokeEvidence(args.smokeNote));
242
242
  checks.push(...scoreProviderDocs(providerRoot));
243
+ checks.push(scoreRepositoryDx(providerRoot));
243
244
  checks.push(scoreSecrets(providerRoot));
244
245
  } else {
245
246
  checks.push(
@@ -287,6 +288,69 @@ export async function buildSubmitCheckReport(
287
288
  };
288
289
  }
289
290
 
291
+ function scoreRepositoryDx(providerRoot: string): SubmitCheck {
292
+ const missing: string[] = [];
293
+ if (!existsSync(resolve(providerRoot, ".gitignore"))) {
294
+ missing.push(".gitignore");
295
+ }
296
+
297
+ const packageJsonPath = resolve(providerRoot, "package.json");
298
+ const packageScripts = readPackageScripts(packageJsonPath);
299
+ if (typeof packageScripts?.["type-check"] !== "string") {
300
+ missing.push("package.json scripts.type-check");
301
+ }
302
+ if (!checkScriptRunsTypeCheck(packageScripts?.check)) {
303
+ missing.push("package.json scripts.check includes type-check");
304
+ }
305
+
306
+ if (missing.length === 0) {
307
+ return pass(
308
+ "repository-dx",
309
+ "docs",
310
+ "Repository includes generated-provider DX guardrails.",
311
+ 0,
312
+ );
313
+ }
314
+
315
+ return {
316
+ id: "repository-dx",
317
+ category: "docs",
318
+ level: "warn",
319
+ status: "warn",
320
+ points: 0,
321
+ maxPoints: 0,
322
+ message: `Generated repository DX guardrails are missing: ${missing.join(", ")}.`,
323
+ remediation:
324
+ "Regenerate with the current `apifuse create` template or add .gitignore plus `type-check: tsc --noEmit` and include it from `check`.",
325
+ evidence: missing,
326
+ };
327
+ }
328
+
329
+ function readPackageScripts(
330
+ packageJsonPath: string,
331
+ ): Record<string, unknown> | undefined {
332
+ if (!existsSync(packageJsonPath)) {
333
+ return undefined;
334
+ }
335
+
336
+ try {
337
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
338
+ if (!isRecord(packageJson) || !isRecord(packageJson.scripts)) {
339
+ return undefined;
340
+ }
341
+ return packageJson.scripts;
342
+ } catch {
343
+ return undefined;
344
+ }
345
+ }
346
+
347
+ function checkScriptRunsTypeCheck(checkScript: unknown): boolean {
348
+ return (
349
+ typeof checkScript === "string" &&
350
+ /(?:^|&&|;)\s*bun\s+run\s+type-check(?:\s|$)/.test(checkScript)
351
+ );
352
+ }
353
+
290
354
  async function safeRunChecks(providerRoot: string): Promise<CheckResult[]> {
291
355
  try {
292
356
  return await runChecks(providerRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apifuse/provider-sdk",
3
- "version": "2.1.0-beta.4",
3
+ "version": "2.1.0-beta.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "APIFuse Provider SDK — Build providers with zero architectural constraints",
@@ -72,7 +72,6 @@ export const COMMAND_MANIFEST: Record<
72
72
  usage:
73
73
  'apifuse record [path] --operation <operation> --params \'{"value":"hello"}\'',
74
74
  examples: [
75
- 'apifuse record . --operation ping --params \'{"value":"hello"}\'',
76
75
  'apifuse record providers/airkorea --operation realtime --params \'{"stationName":"종로구"}\'',
77
76
  ],
78
77
  modulePath: "./apifuse-record",
@@ -88,10 +87,10 @@ export const COMMAND_MANIFEST: Record<
88
87
  name: "perf",
89
88
  summary:
90
89
  "Profile a provider operation and export latency/trace diagnostics.",
91
- usage: "apifuse perf <path> --operation <operation> [options]",
90
+ usage:
91
+ "apifuse perf <path> --operation <operation> [--params '<json>'] [options]",
92
92
  examples: [
93
- "apifuse perf . --operation ping",
94
- "apifuse perf providers/airkorea --operation realtime --runs 5",
93
+ 'apifuse perf providers/airkorea --operation realtime --params \'{"stationName":"종로구"}\' --runs 5',
95
94
  ],
96
95
  modulePath: "./apifuse-perf",
97
96
  },
package/src/cli/create.ts CHANGED
@@ -474,6 +474,14 @@ export async function buildProviderCreatePlan(
474
474
  };
475
475
 
476
476
  const files: ProviderPlanFile[] = [
477
+ {
478
+ path: resolve(providerRoot, ".dockerignore"),
479
+ content: await renderTemplate(".dockerignore.tpl", {}),
480
+ },
481
+ {
482
+ path: resolve(providerRoot, ".gitignore"),
483
+ content: await renderTemplate(".gitignore.tpl", {}),
484
+ },
477
485
  {
478
486
  path: resolve(providerRoot, "index.ts"),
479
487
  content: await renderTemplate("index.ts.tpl", templateValues),
@@ -574,6 +582,7 @@ export async function buildProviderCreatePlan(
574
582
  providerRoot,
575
583
  validationCommands: [
576
584
  "bun run check",
585
+ "bun run type-check",
577
586
  "bun run submit-check",
578
587
  "bun run test",
579
588
  ],
@@ -605,7 +614,8 @@ function renderPackageJson(input: {
605
614
  main: "./index.ts",
606
615
  scripts: {
607
616
  dev: "apifuse dev .",
608
- check: "apifuse check .",
617
+ check: "apifuse check . && bun run type-check",
618
+ "type-check": "tsc --noEmit",
609
619
  "submit-check":
610
620
  "apifuse submit-check . --markdown submission-report.md",
611
621
  test: "apifuse test .",
@@ -618,6 +628,7 @@ function renderPackageJson(input: {
618
628
  },
619
629
  devDependencies: {
620
630
  "@types/bun": "latest",
631
+ typescript: "^6.0.3",
621
632
  },
622
633
  },
623
634
  null,
@@ -636,6 +647,7 @@ function renderTsconfig(): string {
636
647
  noEmit: true,
637
648
  skipLibCheck: true,
638
649
  resolveJsonModule: true,
650
+ types: ["bun"],
639
651
  },
640
652
  include: ["**/*.ts"],
641
653
  exclude: ["node_modules"],
@@ -952,6 +964,14 @@ function printResult(
952
964
  console.log(`Validation: (cd ${plan.providerRoot} && ${command})`);
953
965
  }
954
966
  console.log(`Next local dev: ${plan.nextDevCommand}`);
967
+ console.log(
968
+ "Submission evidence: run `bun run submit-check`, save the generated report, and note `/health` plus `POST /v1/{operation}` smoke results.",
969
+ );
970
+ if (plan.files.some((file) => file.content.includes('runtime: "browser"'))) {
971
+ console.log(
972
+ "Browser runtime: run `bunx playwright install chromium` locally or set `APIFUSE__CDP_POOL__URL` before browser-backed smoke tests.",
973
+ );
974
+ }
955
975
  }
956
976
 
957
977
  function escapeTemplate(value: string): string {
@@ -0,0 +1,22 @@
1
+ node_modules/
2
+ .git/
3
+ .github/
4
+
5
+ # Environment and local secrets
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Local reports and generated artifacts
11
+ submission-report.md
12
+ coverage/
13
+ dist/
14
+ .cache/
15
+ .turbo/
16
+ .bun/
17
+ *.tsbuildinfo
18
+
19
+ # OS/editor junk
20
+ .DS_Store
21
+ Thumbs.db
22
+ *.swp
@@ -0,0 +1,22 @@
1
+ node_modules/
2
+
3
+ # Environment and local secrets
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+
8
+ # Build, coverage, cache, and local runtime artifacts
9
+ coverage/
10
+ dist/
11
+ .cache/
12
+ .turbo/
13
+ .bun/
14
+ *.tsbuildinfo
15
+
16
+ # Bounty submission output
17
+ submission-report.md
18
+
19
+ # OS/editor junk
20
+ .DS_Store
21
+ Thumbs.db
22
+ *.swp
@@ -140,3 +140,21 @@ Every operation must declare exactly one of:
140
140
 
141
141
  The generated `ping` operation uses `healthCheckUnsupported` only because it is
142
142
  a local scaffold check, not a real upstream API probe.
143
+
144
+ `healthCheck.cases[].assertions` receives `{ data, status, durationMs, meta }`.
145
+ `data` is the parsed operation output. Use this shape in real operations:
146
+
147
+ ```ts
148
+ healthCheck: {
149
+ interval: "5m",
150
+ cases: [{
151
+ name: "lookup baseline",
152
+ input: { q: "btc" },
153
+ assertions: ({ data, status, durationMs }) => {
154
+ if (status !== 200 || data.results.length === 0 || durationMs > 3000) {
155
+ return { status: "degraded", label: "lookup baseline changed" };
156
+ }
157
+ },
158
+ }],
159
+ }
160
+ ```
package/src/define.ts CHANGED
@@ -181,7 +181,7 @@ type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> =
181
181
  infer TInput,
182
182
  infer TOutput
183
183
  >
184
- ? OperationConfig<TInput, TOutput>
184
+ ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
185
185
  : never;
186
186
  };
187
187
  type StreamOperationConfig<
@@ -255,13 +255,13 @@ export interface ProviderConfig<
255
255
  displayNameKey?: string;
256
256
  descriptionKey: string;
257
257
  category: string;
258
- tags?: string[];
258
+ tags?: readonly string[];
259
259
  icon?: string;
260
260
  docTitleKey?: string;
261
261
  docDescriptionKey?: string;
262
262
  docSummaryKey?: string;
263
263
  docMarkdownKey?: string;
264
- normalizationNotesKeys?: string[];
264
+ normalizationNotesKeys?: readonly string[];
265
265
  environment?: "staging";
266
266
  purpose?: string;
267
267
  purposeKey?: string;
package/src/lint.ts CHANGED
@@ -54,7 +54,7 @@ export interface LintDiagnostic {
54
54
 
55
55
  function lintAllowedHosts(
56
56
  providerId: string | undefined,
57
- allowedHosts: string[] | undefined,
57
+ allowedHosts: readonly string[] | undefined,
58
58
  ): LintDiagnostic[] {
59
59
  const prefix = providerId ? `Provider "${providerId}"` : "Provider";
60
60
 
@@ -114,7 +114,7 @@ function lintReviewed(
114
114
  ];
115
115
  }
116
116
 
117
- function hasReusableSecretKeys(keys: string[] | undefined): boolean {
117
+ function hasReusableSecretKeys(keys: readonly string[] | undefined): boolean {
118
118
  if (!keys) {
119
119
  return false;
120
120
  }
@@ -154,12 +154,12 @@ function lintAuthModel(provider: {
154
154
  id?: string;
155
155
  auth?: ProviderAuthLike;
156
156
  credential?: {
157
- keys?: string[];
157
+ keys?: readonly string[];
158
158
  storesReusableSecret?: boolean;
159
159
  justification?: string;
160
160
  };
161
161
  context?: {
162
- keys?: string[];
162
+ keys?: readonly string[];
163
163
  };
164
164
  authFlowSource?: string;
165
165
  }): LintDiagnostic[] {
@@ -624,14 +624,14 @@ function lintStealthTransportUsage(provider: {
624
624
  export function lintOperation(op: {
625
625
  description?: string;
626
626
  descriptionKey?: string;
627
- whenToUse?: string[];
628
- whenToUseKeys?: string[];
629
- whenNotToUse?: string[];
630
- whenNotToUseKeys?: string[];
627
+ whenToUse?: readonly string[];
628
+ whenToUseKeys?: readonly string[];
629
+ whenNotToUse?: readonly string[];
630
+ whenNotToUseKeys?: readonly string[];
631
631
  input: unknown;
632
632
  output: unknown;
633
633
  fixtures?: unknown;
634
- inputExamples?: unknown[];
634
+ inputExamples?: readonly unknown[];
635
635
  derivations?: Record<string, string>;
636
636
  }): LintDiagnostic[] {
637
637
  const diagnostics: LintDiagnostic[] = [];
@@ -745,16 +745,16 @@ export function lintOperation(op: {
745
745
 
746
746
  export function lintProvider(provider: {
747
747
  id?: string;
748
- allowedHosts?: string[];
748
+ allowedHosts?: readonly string[];
749
749
  stealth?: unknown;
750
750
  auth?: ProviderAuthLike;
751
751
  credential?: {
752
- keys?: string[];
752
+ keys?: readonly string[];
753
753
  storesReusableSecret?: boolean;
754
754
  justification?: string;
755
755
  };
756
756
  context?: {
757
- keys?: string[];
757
+ keys?: readonly string[];
758
758
  };
759
759
  authFlowSource?: string;
760
760
  operations?: Record<
@@ -762,14 +762,14 @@ export function lintProvider(provider: {
762
762
  {
763
763
  description?: string;
764
764
  descriptionKey?: string;
765
- whenToUse?: string[];
766
- whenToUseKeys?: string[];
767
- whenNotToUse?: string[];
768
- whenNotToUseKeys?: string[];
765
+ whenToUse?: readonly string[];
766
+ whenToUseKeys?: readonly string[];
767
+ whenNotToUse?: readonly string[];
768
+ whenNotToUseKeys?: readonly string[];
769
769
  input: unknown;
770
770
  output: unknown;
771
771
  fixtures?: unknown;
772
- inputExamples?: unknown[];
772
+ inputExamples?: readonly unknown[];
773
773
  derivations?: Record<string, string>;
774
774
  handler?: unknown;
775
775
  source?: string;
@@ -64,7 +64,7 @@ type InternalTraceContext = TraceContext & {
64
64
  function buildOTLPExportOptions(
65
65
  config?: TraceConfig,
66
66
  ): OTLPExportOptions | undefined {
67
- if (!config || config.exporter !== "otlp") {
67
+ if (config?.exporter !== "otlp") {
68
68
  return undefined;
69
69
  }
70
70
 
package/src/types.ts CHANGED
@@ -798,15 +798,15 @@ export interface ProviderPublicProfile {
798
798
  longDescriptionKey?: ProviderLocaleKeyInput;
799
799
  logo?: ProviderLogoProfile;
800
800
  category?: string;
801
- tags?: string[];
802
- capabilityKeys?: ProviderLocaleKeyInput[];
803
- examplePromptKeys?: ProviderLocaleKeyInput[];
801
+ tags?: readonly string[];
802
+ capabilityKeys?: readonly ProviderLocaleKeyInput[];
803
+ examplePromptKeys?: readonly ProviderLocaleKeyInput[];
804
804
  setupSummaryKey?: ProviderLocaleKeyInput;
805
805
  connectionMode?: ProviderPublicConnectionMode;
806
- requirementKeys?: ProviderLocaleKeyInput[];
807
- limitationKeys?: ProviderLocaleKeyInput[];
806
+ requirementKeys?: readonly ProviderLocaleKeyInput[];
807
+ limitationKeys?: readonly ProviderLocaleKeyInput[];
808
808
  availability?: {
809
- regions?: string[];
809
+ regions?: readonly string[];
810
810
  supportLevel?: ProviderSupportLevel;
811
811
  };
812
812
  /**
@@ -823,13 +823,13 @@ export interface ProviderMeta {
823
823
  displayNameKey?: ProviderLocaleKeyInput;
824
824
  descriptionKey: ProviderLocaleKeyInput;
825
825
  category: string;
826
- tags?: string[];
826
+ tags?: readonly string[];
827
827
  icon?: string;
828
828
  docTitleKey?: ProviderLocaleKeyInput;
829
829
  docDescriptionKey?: ProviderLocaleKeyInput;
830
830
  docSummaryKey?: ProviderLocaleKeyInput;
831
831
  docMarkdownKey?: ProviderLocaleKeyInput;
832
- normalizationNotesKeys?: ProviderLocaleKeyInput[];
832
+ normalizationNotesKeys?: readonly ProviderLocaleKeyInput[];
833
833
  environment?: "staging";
834
834
  purpose?: string;
835
835
  purposeKey?: ProviderLocaleKeyInput;
@@ -1373,13 +1373,13 @@ export interface OperationDefinition<
1373
1373
  > {
1374
1374
  descriptionKey?: ProviderLocaleKeyInput;
1375
1375
  docs?: OperationDocMeta;
1376
- whenToUseKeys?: ProviderLocaleKeyInput[];
1377
- whenNotToUseKeys?: ProviderLocaleKeyInput[];
1376
+ whenToUseKeys?: readonly ProviderLocaleKeyInput[];
1377
+ whenNotToUseKeys?: readonly ProviderLocaleKeyInput[];
1378
1378
  derivations?: Record<string, string>;
1379
- inputExamples?: OperationInputExample[];
1379
+ inputExamples?: readonly OperationInputExample[];
1380
1380
  annotations?: OperationAnnotations;
1381
1381
  contract?: OperationContractMetadata;
1382
- tags?: string[];
1382
+ tags?: readonly string[];
1383
1383
  relatedOperations?: OperationRelationships;
1384
1384
  toolRouter?: OperationToolRouterMetadata;
1385
1385
  observability?: OperationObservabilityConfig;
@@ -1396,6 +1396,10 @@ export interface OperationDefinition<
1396
1396
  request: InferSchemaOutput<TInput>;
1397
1397
  response: InferSchemaOutput<TOutput>;
1398
1398
  };
1399
+ upstream?: {
1400
+ baseUrl?: string;
1401
+ proxy?: boolean | ProviderProxyPolicy;
1402
+ };
1399
1403
  hints?: Record<string, string>;
1400
1404
  healthCheck?: HealthCheckSuite<
1401
1405
  InferSchemaOutput<TInput>,