@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.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 (165) hide show
  1. package/AUTHORING.md +14 -0
  2. package/CHANGELOG.md +4 -0
  3. package/bin/apifuse-check.ts +18 -58
  4. package/bin/apifuse-create.ts +1 -1
  5. package/bin/apifuse-dev.ts +27 -52
  6. package/bin/apifuse-pack-smoke.ts +22 -81
  7. package/bin/apifuse-pack-types.ts +266 -0
  8. package/bin/apifuse-perf.ts +45 -127
  9. package/bin/apifuse-record.ts +23 -61
  10. package/bin/apifuse-submit-check.ts +26 -21
  11. package/bin/apifuse.ts +1 -1
  12. package/bin/submit-check-xml.ts +1 -1
  13. package/dist/auth-turn/index.d.ts +1 -1
  14. package/dist/auth.d.ts +2 -2
  15. package/dist/auth.js +9 -18
  16. package/dist/ceremonies/index.d.ts +1 -1
  17. package/dist/ceremonies/index.js +8 -18
  18. package/dist/config/loader.d.ts +2 -2
  19. package/dist/config/loader.js +19 -42
  20. package/dist/contract-serialization.d.ts +2 -2
  21. package/dist/contract-serialization.js +3 -6
  22. package/dist/contract-types.d.ts +2 -2
  23. package/dist/contract.d.ts +3 -3
  24. package/dist/contract.js +4 -6
  25. package/dist/define.d.ts +8 -1
  26. package/dist/define.js +57 -115
  27. package/dist/dev.d.ts +1 -1
  28. package/dist/dev.js +1 -1
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.js +1 -3
  31. package/dist/i18n/catalog.d.ts +2 -2
  32. package/dist/i18n/catalog.js +4 -10
  33. package/dist/i18n/index.d.ts +2 -2
  34. package/dist/i18n/index.js +2 -2
  35. package/dist/i18n/keys.d.ts +2 -2
  36. package/dist/index.d.ts +43 -42
  37. package/dist/index.js +38 -37
  38. package/dist/lint.js +8 -15
  39. package/dist/provider.d.ts +11 -11
  40. package/dist/provider.js +9 -9
  41. package/dist/public-schema-field-lint.d.ts +1 -1
  42. package/dist/recipes/gov-api.js +1 -1
  43. package/dist/runtime/auth-flow.d.ts +1 -1
  44. package/dist/runtime/auth-flow.js +3 -3
  45. package/dist/runtime/browser.d.ts +1 -1
  46. package/dist/runtime/browser.js +15 -29
  47. package/dist/runtime/cache.d.ts +1 -1
  48. package/dist/runtime/cache.js +4 -8
  49. package/dist/runtime/choice.d.ts +1 -1
  50. package/dist/runtime/choice.js +12 -34
  51. package/dist/runtime/credential.d.ts +1 -1
  52. package/dist/runtime/credential.js +1 -1
  53. package/dist/runtime/env.d.ts +1 -1
  54. package/dist/runtime/executor.d.ts +1 -1
  55. package/dist/runtime/executor.js +2 -2
  56. package/dist/runtime/http.d.ts +2 -2
  57. package/dist/runtime/http.js +5 -5
  58. package/dist/runtime/insights.d.ts +1 -1
  59. package/dist/runtime/insights.js +6 -13
  60. package/dist/runtime/instrumentation.d.ts +2 -2
  61. package/dist/runtime/instrumentation.js +5 -15
  62. package/dist/runtime/keyring.js +1 -1
  63. package/dist/runtime/namespace.js +1 -1
  64. package/dist/runtime/otlp.d.ts +1 -1
  65. package/dist/runtime/perf.d.ts +1 -1
  66. package/dist/runtime/provider.d.ts +1 -1
  67. package/dist/runtime/provider.js +1 -2
  68. package/dist/runtime/proxy-errors.d.ts +1 -1
  69. package/dist/runtime/proxy-errors.js +3 -5
  70. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  71. package/dist/runtime/proxy-retry-policy.js +2 -2
  72. package/dist/runtime/proxy-telemetry.d.ts +1 -1
  73. package/dist/runtime/proxy-telemetry.js +16 -48
  74. package/dist/runtime/redis.d.ts +1 -1
  75. package/dist/runtime/redis.js +2 -4
  76. package/dist/runtime/request-options.d.ts +1 -1
  77. package/dist/runtime/state.d.ts +2 -2
  78. package/dist/runtime/state.js +3 -3
  79. package/dist/runtime/stealth.d.ts +2 -2
  80. package/dist/runtime/stealth.js +6 -6
  81. package/dist/runtime/stt.d.ts +1 -1
  82. package/dist/runtime/stt.js +11 -15
  83. package/dist/runtime/trace.d.ts +2 -2
  84. package/dist/runtime/trace.js +2 -4
  85. package/dist/runtime/waterfall.d.ts +1 -1
  86. package/dist/schema.d.ts +1 -1
  87. package/dist/schema.js +7 -15
  88. package/dist/serve.d.ts +1 -1
  89. package/dist/serve.js +1 -1
  90. package/dist/server/index.d.ts +7 -7
  91. package/dist/server/index.js +6 -6
  92. package/dist/server/self-test-redaction.d.ts +1 -1
  93. package/dist/server/self-test-redaction.js +1 -1
  94. package/dist/server/self-test.d.ts +3 -3
  95. package/dist/server/self-test.js +3 -3
  96. package/dist/server/serve.d.ts +3 -3
  97. package/dist/server/serve.js +36 -49
  98. package/dist/server/types.js +2 -7
  99. package/dist/stealth/profiles.d.ts +1 -1
  100. package/dist/stealth/profiles.js +5 -14
  101. package/dist/stream.d.ts +1 -1
  102. package/dist/testing/index.d.ts +2 -2
  103. package/dist/testing/index.js +2 -2
  104. package/dist/testing/run.d.ts +1 -1
  105. package/dist/testing/run.js +7 -14
  106. package/dist/types.d.ts +57 -0
  107. package/dist/user-input.d.ts +30 -0
  108. package/dist/user-input.js +66 -0
  109. package/package.json +6 -4
  110. package/src/auth-turn/index.ts +1 -1
  111. package/src/auth.ts +28 -86
  112. package/src/ceremonies/index.ts +24 -78
  113. package/src/config/loader.ts +58 -176
  114. package/src/contract-serialization.ts +5 -11
  115. package/src/contract-types.ts +2 -2
  116. package/src/contract.ts +12 -28
  117. package/src/define.ts +174 -495
  118. package/src/dev.ts +4 -9
  119. package/src/errors.ts +4 -11
  120. package/src/i18n/catalog.ts +10 -32
  121. package/src/i18n/index.ts +2 -2
  122. package/src/i18n/keys.ts +5 -11
  123. package/src/index.ts +44 -42
  124. package/src/lint.ts +89 -159
  125. package/src/provider.ts +12 -11
  126. package/src/public-schema-field-lint.ts +7 -33
  127. package/src/recipes/gov-api.ts +2 -5
  128. package/src/runtime/auth-flow.ts +5 -7
  129. package/src/runtime/browser.ts +64 -187
  130. package/src/runtime/cache.ts +20 -67
  131. package/src/runtime/choice.ts +55 -129
  132. package/src/runtime/credential.ts +2 -2
  133. package/src/runtime/env.ts +1 -1
  134. package/src/runtime/executor.ts +9 -19
  135. package/src/runtime/http.ts +7 -7
  136. package/src/runtime/insights.ts +15 -53
  137. package/src/runtime/instrumentation.ts +19 -58
  138. package/src/runtime/keyring.ts +7 -19
  139. package/src/runtime/namespace.ts +2 -7
  140. package/src/runtime/otlp.ts +12 -23
  141. package/src/runtime/perf.ts +1 -1
  142. package/src/runtime/provider.ts +4 -9
  143. package/src/runtime/proxy-errors.ts +17 -38
  144. package/src/runtime/proxy-retry-policy.ts +3 -3
  145. package/src/runtime/proxy-telemetry.ts +23 -72
  146. package/src/runtime/redis.ts +4 -12
  147. package/src/runtime/request-options.ts +4 -13
  148. package/src/runtime/state.ts +30 -109
  149. package/src/runtime/stealth.ts +8 -8
  150. package/src/runtime/stt.ts +38 -94
  151. package/src/runtime/trace.ts +14 -44
  152. package/src/runtime/waterfall.ts +5 -18
  153. package/src/schema.ts +23 -84
  154. package/src/serve.ts +1 -1
  155. package/src/server/index.ts +7 -7
  156. package/src/server/self-test-redaction.ts +2 -2
  157. package/src/server/self-test.ts +5 -5
  158. package/src/server/serve.ts +87 -242
  159. package/src/server/types.ts +6 -19
  160. package/src/stealth/profiles.ts +10 -26
  161. package/src/stream.ts +8 -19
  162. package/src/testing/index.ts +2 -2
  163. package/src/testing/run.ts +19 -67
  164. package/src/types.ts +58 -0
  165. package/src/user-input.ts +118 -0
@@ -1,5 +1,5 @@
1
- import { SDKError } from "../errors";
2
- import type { StealthPlatform, StealthProfile } from "../types";
1
+ import { SDKError } from "../errors.js";
2
+ import type { StealthPlatform, StealthProfile } from "../types.js";
3
3
 
4
4
  type StealthProfileDefinition = Omit<StealthProfile, "name" | "platform"> & {
5
5
  platform: StealthPlatform;
@@ -81,10 +81,7 @@ const FIREFOX_JA3 =
81
81
  const SAFARI_JA3 =
82
82
  "771,4865-4866-4867-49196-49195-52393-49200-49199-49188-49192-159-158-107-103-57-51-157-156-61-60-53-47-255,0-23-65281-10-11-16-5-13-18-51-45-43-27,29-23-24-25,0";
83
83
 
84
- function createProfile(
85
- name: string,
86
- definition: StealthProfileDefinition,
87
- ): StealthProfile {
84
+ function createProfile(name: string, definition: StealthProfileDefinition): StealthProfile {
88
85
  return {
89
86
  name,
90
87
  platform: definition.platform,
@@ -93,12 +90,8 @@ function createProfile(
93
90
  tlsClientIdentifier: definition.tlsClientIdentifier,
94
91
  ja3: definition.ja3,
95
92
  ja4: definition.ja4,
96
- h2Settings: definition.h2Settings
97
- ? { ...definition.h2Settings }
98
- : undefined,
99
- headerOrder: definition.headerOrder
100
- ? [...definition.headerOrder]
101
- : undefined,
93
+ h2Settings: definition.h2Settings ? { ...definition.h2Settings } : undefined,
94
+ headerOrder: definition.headerOrder ? [...definition.headerOrder] : undefined,
102
95
  };
103
96
  }
104
97
 
@@ -108,8 +101,7 @@ function extractBrowserMajorVersion(profile: StealthProfile): string {
108
101
  return chromeVersion;
109
102
  }
110
103
 
111
- const identifierVersion =
112
- profile.tlsClientIdentifier?.match(/(\d+)(?!.*\d)/)?.[1];
104
+ const identifierVersion = profile.tlsClientIdentifier?.match(/(\d+)(?!.*\d)/)?.[1];
113
105
  if (identifierVersion) {
114
106
  return identifierVersion;
115
107
  }
@@ -132,9 +124,7 @@ function toPlatformHeaderValue(platform: StealthPlatform): string {
132
124
  }
133
125
  }
134
126
 
135
- export function generateLayer2Headers(
136
- profile: StealthProfile,
137
- ): Record<string, string> {
127
+ export function generateLayer2Headers(profile: StealthProfile): Record<string, string> {
138
128
  const headers: Record<string, string> = {
139
129
  "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8",
140
130
  };
@@ -143,16 +133,13 @@ export function generateLayer2Headers(
143
133
  const majorVersion = extractBrowserMajorVersion(profile);
144
134
 
145
135
  if (identifier.startsWith("chrome_") || identifier.startsWith("edge_")) {
146
- const isEdge =
147
- identifier.startsWith("edge_") || /\bEdg\//.test(profile.userAgent);
136
+ const isEdge = identifier.startsWith("edge_") || /\bEdg\//.test(profile.userAgent);
148
137
  headers["Sec-Ch-Ua"] = isEdge
149
138
  ? `"Chromium";v="${majorVersion}", "Microsoft Edge";v="${majorVersion}", "Not)A;Brand";v="99"`
150
139
  : `"Chromium";v="${majorVersion}", "Google Chrome";v="${majorVersion}", "Not)A;Brand";v="99"`;
151
140
  headers["Sec-Ch-Ua-Platform"] = toPlatformHeaderValue(profile.platform);
152
141
  headers["Sec-Ch-Ua-Mobile"] =
153
- profile.platform === "android" || profile.platform === "ios"
154
- ? "?1"
155
- : "?0";
142
+ profile.platform === "android" || profile.platform === "ios" ? "?1" : "?0";
156
143
  }
157
144
 
158
145
  return headers;
@@ -301,8 +288,5 @@ export function getStealthProfile(name: string): StealthProfile {
301
288
  }
302
289
 
303
290
  export function listStealthProfiles(): string[] {
304
- return [
305
- ...Object.keys(STEALTH_PROFILES),
306
- ...Object.keys(STEALTH_PROFILE_ALIASES),
307
- ];
291
+ return [...Object.keys(STEALTH_PROFILES), ...Object.keys(STEALTH_PROFILE_ALIASES)];
308
292
  }
package/src/stream.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SseMessage } from "./types";
1
+ import type { SseMessage } from "./types.js";
2
2
 
3
3
  export interface SseEvent<TData = unknown> {
4
4
  event: string;
@@ -61,8 +61,7 @@ export function done<TData>(
61
61
 
62
62
  export function encodeSseEvent(input: SseEvent): string {
63
63
  const lines: string[] = [];
64
- if (input.id !== undefined)
65
- lines.push(`id: ${sseFieldValue(input.id, "id")}`);
64
+ if (input.id !== undefined) lines.push(`id: ${sseFieldValue(input.id, "id")}`);
66
65
  if (input.event) lines.push(`event: ${sseFieldValue(input.event, "event")}`);
67
66
  if (input.retry !== undefined) {
68
67
  if (!Number.isInteger(input.retry) || input.retry < 0) {
@@ -70,8 +69,7 @@ export function encodeSseEvent(input: SseEvent): string {
70
69
  }
71
70
  lines.push(`retry: ${input.retry}`);
72
71
  }
73
- const data =
74
- typeof input.data === "string" ? input.data : JSON.stringify(input.data);
72
+ const data = typeof input.data === "string" ? input.data : JSON.stringify(input.data);
75
73
  for (const line of data.split(/\r?\n/)) {
76
74
  lines.push(`data: ${line}`);
77
75
  }
@@ -85,9 +83,7 @@ function sseFieldValue(value: string, field: "event" | "id"): string {
85
83
  return value;
86
84
  }
87
85
 
88
- export async function* readableBytes(
89
- body: ReadableStream<Uint8Array>,
90
- ): AsyncIterable<Uint8Array> {
86
+ export async function* readableBytes(body: ReadableStream<Uint8Array>): AsyncIterable<Uint8Array> {
91
87
  const reader = body.getReader();
92
88
  try {
93
89
  for (;;) {
@@ -100,9 +96,7 @@ export async function* readableBytes(
100
96
  }
101
97
  }
102
98
 
103
- export async function* readableTextChunks(
104
- body: ReadableStream<Uint8Array>,
105
- ): AsyncIterable<string> {
99
+ export async function* readableTextChunks(body: ReadableStream<Uint8Array>): AsyncIterable<string> {
106
100
  const decoder = new TextDecoder();
107
101
  for await (const chunk of readableBytes(body)) {
108
102
  yield decoder.decode(chunk, { stream: true });
@@ -111,9 +105,7 @@ export async function* readableTextChunks(
111
105
  if (tail) yield tail;
112
106
  }
113
107
 
114
- export async function* readableLines(
115
- body: ReadableStream<Uint8Array>,
116
- ): AsyncIterable<string> {
108
+ export async function* readableLines(body: ReadableStream<Uint8Array>): AsyncIterable<string> {
117
109
  let buffer = "";
118
110
  for await (const chunk of readableTextChunks(body)) {
119
111
  buffer += chunk;
@@ -121,8 +113,7 @@ export async function* readableLines(
121
113
  const index = buffer.search(/\r?\n/);
122
114
  if (index < 0) break;
123
115
  const line = buffer.slice(0, index);
124
- const newlineLength =
125
- buffer[index] === "\r" && buffer[index + 1] === "\n" ? 2 : 1;
116
+ const newlineLength = buffer[index] === "\r" && buffer[index + 1] === "\n" ? 2 : 1;
126
117
  buffer = buffer.slice(index + newlineLength);
127
118
  yield line;
128
119
  }
@@ -150,9 +141,7 @@ function parseJson<T>(data: string): T {
150
141
  return JSON.parse(data);
151
142
  }
152
143
 
153
- export async function* parseSseStream(
154
- body: ReadableStream<Uint8Array>,
155
- ): AsyncIterable<SseMessage> {
144
+ export async function* parseSseStream(body: ReadableStream<Uint8Array>): AsyncIterable<SseMessage> {
156
145
  let eventName = "message";
157
146
  let dataLines: string[] = [];
158
147
  let id: string | undefined;
@@ -1,2 +1,2 @@
1
- export { describeTransform, snapshotTransform, toMatchShape } from "./helpers";
2
- export { runStandardTests } from "./run";
1
+ export { describeTransform, snapshotTransform, toMatchShape } from "./helpers.js";
2
+ export { runStandardTests } from "./run.js";
@@ -1,28 +1,23 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import { createProviderCache } from "../runtime/cache";
4
- import { createTestProviderChoiceContext } from "../runtime/choice";
5
- import { createMemoryProviderRuntimeState } from "../runtime/state";
6
- import { createUnsupportedSttClient } from "../runtime/stt";
7
- import { safeParseSchemaSync } from "../schema";
3
+ import { createProviderCache } from "../runtime/cache.js";
4
+ import { createTestProviderChoiceContext } from "../runtime/choice.js";
5
+ import { createMemoryProviderRuntimeState } from "../runtime/state.js";
6
+ import { createUnsupportedSttClient } from "../runtime/stt.js";
7
+ import { safeParseSchemaSync } from "../schema.js";
8
8
  import type {
9
9
  AuthMode,
10
10
  CredentialContext,
11
11
  HttpResponse,
12
12
  ProviderContext,
13
13
  ProviderDefinition,
14
- } from "../types";
14
+ } from "../types.js";
15
15
 
16
16
  // Mirrors CONNECTOR_ID_REGEX in ../define.ts, which defineProvider() enforces.
17
17
  // A single lowercase segment (no hyphen) is a valid id, so the trailing group
18
18
  // is optional (`*`), matching providers like `kakaomap`, `kstartup`, `triple`.
19
19
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
20
- const VALID_AUTH_MODES = [
21
- "none",
22
- "platform-managed",
23
- "credentials",
24
- "oauth2",
25
- ] as const;
20
+ const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
26
21
  const UPDATE_SNAPSHOT_ARGS = new Set(["-u", "--update-snapshots"]);
27
22
 
28
23
  export interface StandardTestsManifest {
@@ -118,9 +113,7 @@ function inferFixtureDir(providerId: string): string {
118
113
  .find((file) => file !== undefined);
119
114
 
120
115
  if (testFile) {
121
- const pathname = testFile.startsWith("file://")
122
- ? new URL(testFile).pathname
123
- : testFile;
116
+ const pathname = testFile.startsWith("file://") ? new URL(testFile).pathname : testFile;
124
117
  return `${pathname.replace(/\/[^/]+$/, "")}/../__fixtures__`;
125
118
  }
126
119
 
@@ -138,10 +131,7 @@ function jsonResponse(data: unknown): HttpResponse {
138
131
  json: async <_T = unknown>() => JSON.parse(JSON.stringify(data)),
139
132
  text: async () => body,
140
133
  arrayBuffer: async () =>
141
- bodyBytes.buffer.slice(
142
- bodyBytes.byteOffset,
143
- bodyBytes.byteOffset + bodyBytes.byteLength,
144
- ),
134
+ bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
145
135
  bytes: async () => bodyBytes.slice(0),
146
136
  };
147
137
  }
@@ -184,16 +174,14 @@ function createSnapshotContext(rawFixture: unknown): ProviderContext {
184
174
  engine: "playwright-stealth",
185
175
  newPage: async () => unsupported("ctx.browser.newPage"),
186
176
  rawPage: async () => unsupported("ctx.browser.rawPage"),
187
- withIsolatedContext: async () =>
188
- unsupported("ctx.browser.withIsolatedContext"),
177
+ withIsolatedContext: async () => unsupported("ctx.browser.withIsolatedContext"),
189
178
  solveChallenge: async () => unsupported("ctx.browser.solveChallenge"),
190
179
  },
191
180
  trace: {
192
181
  span: async (_name, fn) => fn(),
193
182
  },
194
183
  auth: {
195
- requestField: async (name) =>
196
- unsupported(`ctx.auth.requestField(${name})`),
184
+ requestField: async (name) => unsupported(`ctx.auth.requestField(${name})`),
197
185
  },
198
186
  stt: createUnsupportedSttClient(
199
187
  "Standard test snapshot context does not support ctx.stt.transcribe",
@@ -292,11 +280,7 @@ function parseSchemaFixture(
292
280
  operationName,
293
281
  fieldName,
294
282
  fixture,
295
- safeParseSchemaSync(
296
- schema,
297
- fixture,
298
- `operations.${operationName}.fixtures.${fieldName}`,
299
- ),
283
+ safeParseSchemaSync(schema, fixture, `operations.${operationName}.fixtures.${fieldName}`),
300
284
  );
301
285
  }
302
286
 
@@ -322,40 +306,20 @@ export function runStandardTests(
322
306
 
323
307
  for (const [operationName, op] of operations) {
324
308
  if (op.fixtures?.request !== undefined) {
325
- parseSchemaFixture(
326
- operationName,
327
- "request",
328
- op.input,
329
- op.fixtures.request,
330
- );
309
+ parseSchemaFixture(operationName, "request", op.input, op.fixtures.request);
331
310
  }
332
311
 
333
312
  if (op.fixtures?.response !== undefined) {
334
- parseSchemaFixture(
335
- operationName,
336
- "response",
337
- op.output,
338
- op.fixtures.response,
339
- );
313
+ parseSchemaFixture(operationName, "response", op.output, op.fixtures.response);
340
314
  }
341
315
 
342
316
  if (isFixtureEnvelope(rawFixture)) {
343
317
  if (rawFixture.request !== undefined) {
344
- parseSchemaFixture(
345
- operationName,
346
- "request",
347
- op.input,
348
- rawFixture.request,
349
- );
318
+ parseSchemaFixture(operationName, "request", op.input, rawFixture.request);
350
319
  }
351
320
 
352
321
  if (rawFixture.response !== undefined) {
353
- parseSchemaFixture(
354
- operationName,
355
- "response",
356
- op.output,
357
- rawFixture.response,
358
- );
322
+ parseSchemaFixture(operationName, "response", op.output, rawFixture.response);
359
323
  }
360
324
  }
361
325
  }
@@ -411,21 +375,11 @@ export function runStandardTests(
411
375
  it("operation schemas can parse fixture data", () => {
412
376
  for (const [operationName, op] of operations) {
413
377
  if (op.fixtures?.request !== undefined && op.input) {
414
- parseSchemaFixture(
415
- operationName,
416
- "request",
417
- op.input,
418
- op.fixtures.request,
419
- );
378
+ parseSchemaFixture(operationName, "request", op.input, op.fixtures.request);
420
379
  }
421
380
 
422
381
  if (op.fixtures?.response !== undefined && op.output) {
423
- parseSchemaFixture(
424
- operationName,
425
- "response",
426
- op.output,
427
- op.fixtures.response,
428
- );
382
+ parseSchemaFixture(operationName, "response", op.output, op.fixtures.response);
429
383
  }
430
384
 
431
385
  expect(operationName).toBeTruthy();
@@ -471,9 +425,7 @@ export function runStandardTests(
471
425
  await Bun.write(snapshotPath, serialized);
472
426
  }
473
427
 
474
- const expected: unknown = JSON.parse(
475
- await Bun.file(snapshotPath).text(),
476
- );
428
+ const expected: unknown = JSON.parse(await Bun.file(snapshotPath).text());
477
429
  expect(actual).toEqual(expected);
478
430
  });
479
431
  }
package/src/types.ts CHANGED
@@ -1759,6 +1759,19 @@ export interface OperationDefinition<
1759
1759
  TInput extends SchemaLike = SchemaLike,
1760
1760
  TOutput extends SchemaLike = SchemaLike,
1761
1761
  > {
1762
+ /**
1763
+ * Short English display title for the operation. The SDK passes it through
1764
+ * verbatim; the APIFuse registry derives the operation's en locale title
1765
+ * baseline from it (`docs.titleKey` remains available as an explicit key
1766
+ * override).
1767
+ */
1768
+ title?: string;
1769
+ /**
1770
+ * Raw English operation description. The SDK passes it through verbatim;
1771
+ * keyed descriptions via `descriptionKey` remain the primary flow, and
1772
+ * provider lint rules for raw prose descriptions still apply.
1773
+ */
1774
+ description?: string;
1762
1775
  descriptionKey?: ProviderLocaleKeyInput;
1763
1776
  docs?: OperationDocMeta;
1764
1777
  whenToUseKeys?: readonly ProviderLocaleKeyInput[];
@@ -1797,10 +1810,55 @@ export interface OperationDefinition<
1797
1810
  healthCheckUnsupported?: HealthCheckUnsupported;
1798
1811
  }
1799
1812
 
1813
+ /**
1814
+ * Author-declared deployment overrides accepted as the optional top-level
1815
+ * `deployment` key on `defineProvider()`.
1816
+ *
1817
+ * The SDK passes this object through VERBATIM onto the returned provider
1818
+ * definition — it is typed here but deliberately not deep-validated: the
1819
+ * APIFuse registry builder owns deployment validation and resolves omitted
1820
+ * fields against the runtime deployment profiles. The shape mirrors the
1821
+ * registry's `ProviderDeploymentOverrides` contract; every field is
1822
+ * optional. Note that the deployment `runtime` axis
1823
+ * (`shared`/`dedicated`/`browser`) is distinct from the provider execution
1824
+ * `runtime` (`standard`/`shared`/`browser`).
1825
+ */
1826
+ export interface ProviderDeploymentOverrides {
1827
+ runtime?: "shared" | "dedicated" | "browser";
1828
+ language?: "typescript" | "python";
1829
+ replicas?: number;
1830
+ hpa?: {
1831
+ enabled: boolean;
1832
+ minReplicas?: number;
1833
+ maxReplicas?: number;
1834
+ targetCPUUtilizationPercentage?: number;
1835
+ };
1836
+ resources?: {
1837
+ cpu: string;
1838
+ memory: string;
1839
+ };
1840
+ cache?: {
1841
+ redis?: {
1842
+ enabled: boolean;
1843
+ url?: string;
1844
+ };
1845
+ };
1846
+ network?: {
1847
+ additionalTcpPorts?: number[];
1848
+ };
1849
+ buildContext?: string;
1850
+ }
1851
+
1800
1852
  export interface ProviderDefinition {
1801
1853
  id: string;
1802
1854
  version: string;
1803
1855
  runtime: "standard" | "shared" | "browser";
1856
+ /**
1857
+ * Optional deployment overrides, passed through verbatim from
1858
+ * `defineProvider({ deployment })`. Validation and profile resolution are
1859
+ * owned by the APIFuse registry builder, not the SDK.
1860
+ */
1861
+ deployment?: ProviderDeploymentOverrides;
1804
1862
  allowedHosts?: string[];
1805
1863
  stealth?: {
1806
1864
  profile: string;
@@ -0,0 +1,118 @@
1
+ // Official cross-provider contract for user-input round-trips.
2
+ //
3
+ // Doctrine (born from the 2026-07-20 CatchTable reserve incident): an
4
+ // operation MUST NOT dead-end on a problem the end user can resolve by
5
+ // choosing among live options. Instead of throwing, the provider returns a
6
+ // SUCCESSFUL payload with `status: "needs_input"` that carries everything a
7
+ // consumer-side agent needs to relay the choice verbatim and retry:
8
+ //
9
+ // - `required_selections`: only the still-pending questions, each with
10
+ // human-readable `label`s and `valid_options` the agent shows the user
11
+ // as-is. The agent never chooses on the user's behalf.
12
+ // - `selected_options`: selections already settled, echoed so the follow-up
13
+ // call keeps them without the agent reconstructing anything.
14
+ // - a fresh provider-specific state token (e.g. `reservation_state`) minted
15
+ // at response time, so the retry never races an expired token.
16
+ //
17
+ // Keeping this success-shaped (instead of error `details`) is deliberate:
18
+ // consumer error-shaping layers routinely strip error metadata, and a model
19
+ // that only sees "error" narrates failure to the user. Complex recovery
20
+ // logic belongs to the system, not the model.
21
+ //
22
+ // Deliberately ABSENT: retry templates, next-action routing, or any other
23
+ // agent choreography. Provider payloads carry upstream-backed data only; how
24
+ // to phrase the ask and shape the retry call is the consumer's contract
25
+ // (apifuse#1030). The retry is fully determined by data: copy the echoed
26
+ // selected_options, add the user's new answers, resend with the fresh state.
27
+
28
+ export const NEEDS_INPUT_STATUS = "needs_input" as const;
29
+
30
+ export interface ProviderSelectionOption {
31
+ readonly selection_value: string;
32
+ readonly label: string;
33
+ readonly [extra: string]: unknown;
34
+ }
35
+
36
+ export interface ProviderRequiredSelection {
37
+ readonly selection_key: string;
38
+ readonly label: string;
39
+ readonly required: boolean;
40
+ readonly selection_type?: string;
41
+ readonly valid_options: readonly ProviderSelectionOption[];
42
+ readonly [extra: string]: unknown;
43
+ }
44
+
45
+ export interface ProviderSelectedOption {
46
+ readonly selection_key: string;
47
+ readonly selection_value: string;
48
+ /** Free-text answer for text-kind selections. */
49
+ readonly text?: string;
50
+ /** Quantity for quantity-kind selections. */
51
+ readonly quantity?: number;
52
+ }
53
+
54
+ export interface ProviderNeedsInputPayload {
55
+ readonly status: typeof NEEDS_INPUT_STATUS;
56
+ readonly required_selections: readonly ProviderRequiredSelection[];
57
+ readonly selected_options?: readonly ProviderSelectedOption[];
58
+ /** Provider-specific fresh state token(s), e.g. `reservation_state`. */
59
+ readonly [extra: string]: unknown;
60
+ }
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return typeof value === "object" && value !== null && !Array.isArray(value);
64
+ }
65
+
66
+ function isSelectionOption(value: unknown): value is ProviderSelectionOption {
67
+ return (
68
+ isRecord(value) &&
69
+ typeof value.selection_value === "string" &&
70
+ typeof value.label === "string"
71
+ );
72
+ }
73
+
74
+ function isRequiredSelection(
75
+ value: unknown,
76
+ ): value is ProviderRequiredSelection {
77
+ return (
78
+ isRecord(value) &&
79
+ typeof value.selection_key === "string" &&
80
+ typeof value.label === "string" &&
81
+ typeof value.required === "boolean" &&
82
+ Array.isArray(value.valid_options) &&
83
+ value.valid_options.every(isSelectionOption)
84
+ );
85
+ }
86
+
87
+ function isSelectedOption(value: unknown): value is ProviderSelectedOption {
88
+ return (
89
+ isRecord(value) &&
90
+ typeof value.selection_key === "string" &&
91
+ typeof value.selection_value === "string"
92
+ );
93
+ }
94
+
95
+ export function isProviderNeedsInputPayload(
96
+ value: unknown,
97
+ ): value is ProviderNeedsInputPayload {
98
+ if (!isRecord(value)) {
99
+ return false;
100
+ }
101
+ if (
102
+ value.status !== NEEDS_INPUT_STATUS ||
103
+ !Array.isArray(value.required_selections) ||
104
+ !value.required_selections.every(isRequiredSelection)
105
+ ) {
106
+ return false;
107
+ }
108
+ // A needs_input with nothing to ask AND nothing settled to echo is a
109
+ // no-op dead end — reject it so providers cannot ship it accidentally.
110
+ if (value.required_selections.length > 0) {
111
+ return true;
112
+ }
113
+ return (
114
+ Array.isArray(value.selected_options) &&
115
+ value.selected_options.length > 0 &&
116
+ value.selected_options.every(isSelectedOption)
117
+ );
118
+ }