@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.10

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 (263) hide show
  1. package/AUTHORING.md +240 -0
  2. package/CHANGELOG.md +93 -0
  3. package/README.md +26 -10
  4. package/SUBMISSION.md +11 -12
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +36 -0
  9. package/bin/apifuse-pack-smoke.ts +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +2177 -353
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  20. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  21. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  22. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  23. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  24. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  25. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  26. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  27. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  28. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  29. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  30. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  31. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  32. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  33. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  34. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  35. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  36. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  37. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  38. package/dist/auth-turn/index.d.ts +195 -0
  39. package/dist/auth-turn/index.js +133 -0
  40. package/dist/auth.d.ts +76 -0
  41. package/dist/auth.js +427 -0
  42. package/dist/ceremonies/index.d.ts +1 -1
  43. package/dist/ceremonies/index.js +14 -48
  44. package/dist/cli/commands.d.ts +1 -1
  45. package/dist/cli/commands.js +8 -0
  46. package/dist/cli/create.d.ts +3 -0
  47. package/dist/cli/create.js +47 -33
  48. package/dist/cli/prompt-assets.d.ts +80 -0
  49. package/dist/cli/prompt-assets.js +743 -0
  50. package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  51. package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  52. package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  53. package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  54. package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  55. package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  56. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  57. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  58. package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
  59. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  60. package/dist/cli/templates/provider/README.md.tpl +163 -0
  61. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  62. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  63. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  64. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  65. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  66. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  67. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  68. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  69. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  70. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  71. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  72. package/dist/config/loader.d.ts +149 -8
  73. package/dist/config/loader.js +378 -89
  74. package/dist/contract-serialization.d.ts +2 -2
  75. package/dist/contract-serialization.js +3 -6
  76. package/dist/contract-types.d.ts +2 -2
  77. package/dist/contract.d.ts +3 -3
  78. package/dist/contract.js +5 -6
  79. package/dist/define.d.ts +13 -1
  80. package/dist/define.js +245 -178
  81. package/dist/dev.d.ts +1 -1
  82. package/dist/dev.js +1 -1
  83. package/dist/errors.d.ts +4 -1
  84. package/dist/errors.js +48 -0
  85. package/dist/i18n/catalog.d.ts +2 -2
  86. package/dist/i18n/catalog.js +4 -10
  87. package/dist/i18n/index.d.ts +2 -2
  88. package/dist/i18n/index.js +2 -2
  89. package/dist/i18n/keys.d.ts +2 -2
  90. package/dist/index.d.ts +44 -41
  91. package/dist/index.js +39 -36
  92. package/dist/lint.d.ts +1 -0
  93. package/dist/lint.js +35 -15
  94. package/dist/provider.d.ts +11 -9
  95. package/dist/provider.js +9 -8
  96. package/dist/public-schema-field-lint.d.ts +1 -1
  97. package/dist/recipes/gov-api.js +1 -1
  98. package/dist/runtime/auth-flow.d.ts +1 -1
  99. package/dist/runtime/auth-flow.js +4 -2
  100. package/dist/runtime/browser.d.ts +1 -1
  101. package/dist/runtime/browser.js +214 -25
  102. package/dist/runtime/cache.d.ts +1 -1
  103. package/dist/runtime/cache.js +4 -8
  104. package/dist/runtime/choice.d.ts +1 -1
  105. package/dist/runtime/choice.js +31 -35
  106. package/dist/runtime/credential.d.ts +1 -1
  107. package/dist/runtime/credential.js +1 -1
  108. package/dist/runtime/env.d.ts +1 -1
  109. package/dist/runtime/executor.d.ts +1 -1
  110. package/dist/runtime/executor.js +15 -3
  111. package/dist/runtime/http.d.ts +2 -2
  112. package/dist/runtime/http.js +160 -344
  113. package/dist/runtime/insights.d.ts +1 -1
  114. package/dist/runtime/insights.js +6 -13
  115. package/dist/runtime/instrumentation.d.ts +2 -2
  116. package/dist/runtime/instrumentation.js +56 -19
  117. package/dist/runtime/keyring.js +1 -1
  118. package/dist/runtime/namespace.js +1 -1
  119. package/dist/runtime/otlp.d.ts +1 -1
  120. package/dist/runtime/perf.d.ts +1 -1
  121. package/dist/runtime/provider.d.ts +1 -1
  122. package/dist/runtime/provider.js +1 -2
  123. package/dist/runtime/proxy-errors.d.ts +1 -1
  124. package/dist/runtime/proxy-errors.js +9 -7
  125. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  126. package/dist/runtime/proxy-nodemaven.js +128 -0
  127. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  128. package/dist/runtime/proxy-retry-policy.js +326 -0
  129. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  130. package/dist/runtime/proxy-telemetry.js +55 -52
  131. package/dist/runtime/redis.d.ts +1 -1
  132. package/dist/runtime/redis.js +2 -4
  133. package/dist/runtime/request-options.d.ts +1 -1
  134. package/dist/runtime/secrets.d.ts +27 -0
  135. package/dist/runtime/secrets.js +51 -0
  136. package/dist/runtime/state.d.ts +2 -2
  137. package/dist/runtime/state.js +15 -4
  138. package/dist/runtime/stealth.d.ts +7 -4
  139. package/dist/runtime/stealth.js +257 -215
  140. package/dist/runtime/stt.d.ts +1 -1
  141. package/dist/runtime/stt.js +11 -15
  142. package/dist/runtime/trace.d.ts +2 -2
  143. package/dist/runtime/trace.js +2 -4
  144. package/dist/runtime/waterfall.d.ts +1 -1
  145. package/dist/schema.d.ts +1 -1
  146. package/dist/schema.js +7 -15
  147. package/dist/serve.d.ts +1 -1
  148. package/dist/serve.js +1 -1
  149. package/dist/server/index.d.ts +7 -3
  150. package/dist/server/index.js +6 -2
  151. package/dist/server/self-test-input-tokens.d.ts +1 -0
  152. package/dist/server/self-test-input-tokens.js +37 -0
  153. package/dist/server/self-test-redaction.d.ts +20 -0
  154. package/dist/server/self-test-redaction.js +70 -0
  155. package/dist/server/self-test-token.d.ts +30 -0
  156. package/dist/server/self-test-token.js +50 -0
  157. package/dist/server/self-test.d.ts +199 -0
  158. package/dist/server/self-test.js +1113 -0
  159. package/dist/server/serve.d.ts +14 -3
  160. package/dist/server/serve.js +135 -64
  161. package/dist/server/types.d.ts +10 -9
  162. package/dist/server/types.js +3 -7
  163. package/dist/stealth/profiles.d.ts +1 -1
  164. package/dist/stealth/profiles.js +5 -14
  165. package/dist/stream.d.ts +1 -1
  166. package/dist/testing/index.d.ts +2 -2
  167. package/dist/testing/index.js +2 -2
  168. package/dist/testing/run.d.ts +1 -1
  169. package/dist/testing/run.js +12 -15
  170. package/dist/types.d.ts +237 -1
  171. package/dist/user-input.d.ts +30 -0
  172. package/dist/user-input.js +66 -0
  173. package/package.json +16 -5
  174. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  175. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  176. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  177. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  178. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  179. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  180. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  181. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  182. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  183. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  184. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  185. package/src/auth-turn/fixtures/valid/form.json +14 -0
  186. package/src/auth-turn/fixtures/valid/message.json +13 -0
  187. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  188. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  189. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  190. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  191. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  192. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  193. package/src/auth-turn/index.ts +177 -0
  194. package/src/auth.ts +728 -0
  195. package/src/ceremonies/index.ts +33 -121
  196. package/src/cli/commands.ts +10 -0
  197. package/src/cli/create.ts +69 -99
  198. package/src/cli/prompt-assets.ts +865 -0
  199. package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  200. package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  201. package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  202. package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  203. package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  204. package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  205. package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
  206. package/src/cli/templates/provider/README.md.tpl +7 -4
  207. package/src/config/loader.ts +543 -208
  208. package/src/contract-serialization.ts +5 -11
  209. package/src/contract-types.ts +2 -2
  210. package/src/contract.ts +13 -28
  211. package/src/define.ts +397 -528
  212. package/src/dev.ts +4 -9
  213. package/src/errors.ts +58 -5
  214. package/src/i18n/catalog.ts +10 -32
  215. package/src/i18n/index.ts +2 -2
  216. package/src/i18n/keys.ts +5 -11
  217. package/src/index.ts +64 -41
  218. package/src/lint.ts +122 -159
  219. package/src/provider.ts +40 -9
  220. package/src/public-schema-field-lint.ts +7 -33
  221. package/src/recipes/gov-api.ts +2 -5
  222. package/src/runtime/auth-flow.ts +6 -6
  223. package/src/runtime/browser.ts +320 -151
  224. package/src/runtime/cache.ts +20 -67
  225. package/src/runtime/choice.ts +79 -132
  226. package/src/runtime/credential.ts +2 -2
  227. package/src/runtime/env.ts +1 -1
  228. package/src/runtime/executor.ts +23 -20
  229. package/src/runtime/http.ts +216 -539
  230. package/src/runtime/insights.ts +15 -53
  231. package/src/runtime/instrumentation.ts +78 -65
  232. package/src/runtime/keyring.ts +7 -19
  233. package/src/runtime/namespace.ts +2 -7
  234. package/src/runtime/otlp.ts +12 -23
  235. package/src/runtime/perf.ts +1 -1
  236. package/src/runtime/provider.ts +4 -9
  237. package/src/runtime/proxy-errors.ts +29 -42
  238. package/src/runtime/proxy-nodemaven.ts +178 -0
  239. package/src/runtime/proxy-retry-policy.ts +469 -0
  240. package/src/runtime/proxy-telemetry.ts +79 -77
  241. package/src/runtime/redis.ts +4 -12
  242. package/src/runtime/request-options.ts +4 -13
  243. package/src/runtime/secrets.ts +64 -0
  244. package/src/runtime/state.ts +41 -110
  245. package/src/runtime/stealth.ts +331 -369
  246. package/src/runtime/stt.ts +38 -94
  247. package/src/runtime/trace.ts +14 -44
  248. package/src/runtime/waterfall.ts +5 -18
  249. package/src/schema.ts +23 -84
  250. package/src/serve.ts +1 -1
  251. package/src/server/index.ts +44 -3
  252. package/src/server/self-test-input-tokens.ts +46 -0
  253. package/src/server/self-test-redaction.ts +97 -0
  254. package/src/server/self-test-token.ts +70 -0
  255. package/src/server/self-test.ts +1450 -0
  256. package/src/server/serve.ts +206 -216
  257. package/src/server/types.ts +7 -19
  258. package/src/stealth/profiles.ts +10 -26
  259. package/src/stream.ts +8 -19
  260. package/src/testing/index.ts +2 -2
  261. package/src/testing/run.ts +24 -64
  262. package/src/types.ts +274 -1
  263. package/src/user-input.ts +118 -0
@@ -1,19 +1,14 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { createProviderCache } from "../runtime/cache";
3
- import { createTestProviderChoiceContext } from "../runtime/choice";
4
- import { createMemoryProviderRuntimeState } from "../runtime/state";
5
- import { createUnsupportedSttClient } from "../runtime/stt";
6
- import { safeParseSchemaSync } from "../schema";
2
+ import { createProviderCache } from "../runtime/cache.js";
3
+ import { createTestProviderChoiceContext } from "../runtime/choice.js";
4
+ import { createMemoryProviderRuntimeState } from "../runtime/state.js";
5
+ import { createUnsupportedSttClient } from "../runtime/stt.js";
6
+ import { safeParseSchemaSync } from "../schema.js";
7
7
  // Mirrors CONNECTOR_ID_REGEX in ../define.ts, which defineProvider() enforces.
8
8
  // A single lowercase segment (no hyphen) is a valid id, so the trailing group
9
9
  // is optional (`*`), matching providers like `kakaomap`, `kstartup`, `triple`.
10
10
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
11
- const VALID_AUTH_MODES = [
12
- "none",
13
- "platform-managed",
14
- "credentials",
15
- "oauth2",
16
- ];
11
+ const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"];
17
12
  const UPDATE_SNAPSHOT_ARGS = new Set(["-u", "--update-snapshots"]);
18
13
  function isFixtureEnvelope(value) {
19
14
  return (value !== null &&
@@ -60,21 +55,23 @@ function inferFixtureDir(providerId) {
60
55
  .map((line) => line.match(/\(?((?:file:\/\/)?[^():]+\.test\.ts)/)?.[1])
61
56
  .find((file) => file !== undefined);
62
57
  if (testFile) {
63
- const pathname = testFile.startsWith("file://")
64
- ? new URL(testFile).pathname
65
- : testFile;
58
+ const pathname = testFile.startsWith("file://") ? new URL(testFile).pathname : testFile;
66
59
  return `${pathname.replace(/\/[^/]+$/, "")}/../__fixtures__`;
67
60
  }
68
61
  return `providers/${providerId}/__fixtures__`;
69
62
  }
70
63
  function jsonResponse(data) {
64
+ const body = JSON.stringify(data);
65
+ const bodyBytes = new TextEncoder().encode(body);
71
66
  return {
72
67
  status: 200,
73
68
  ok: true,
74
69
  headers: {},
75
70
  data,
76
71
  json: async () => JSON.parse(JSON.stringify(data)),
77
- text: async () => JSON.stringify(data),
72
+ text: async () => body,
73
+ arrayBuffer: async () => bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
74
+ bytes: async () => bodyBytes.slice(0),
78
75
  };
79
76
  }
80
77
  function unsupported(name) {
package/dist/types.d.ts CHANGED
@@ -268,8 +268,16 @@ export interface HealthJourneySchedule {
268
268
  kind: "interval";
269
269
  /** ISO 8601 duration, for example PT8H. */
270
270
  interval: Iso8601Duration;
271
+ randomize?: HealthScheduleRandomization;
271
272
  jitter?: Iso8601Duration;
272
273
  }
274
+ export type HealthScheduleRandomization = {
275
+ mode: "centered";
276
+ maxOffset: Iso8601Duration;
277
+ } | {
278
+ mode: "delayed";
279
+ maxDelay: Iso8601Duration;
280
+ };
273
281
  export interface HealthJourneyStep {
274
282
  id: string;
275
283
  description?: string;
@@ -511,6 +519,9 @@ export interface HealthCheckCase<TInput = unknown, TOutput = unknown> {
511
519
  export interface HealthCheckSuite<TInput = unknown, TOutput = unknown> {
512
520
  /** Polling interval for the suite. All cases share this cadence. */
513
521
  interval: ProbeInterval;
522
+ schedule?: {
523
+ randomize?: HealthScheduleRandomization;
524
+ };
514
525
  /** Per-case timeout in milliseconds. Default: 30000. */
515
526
  timeoutMs?: number;
516
527
  /** Default degradation threshold for cases in this suite. Default: runtime threshold. */
@@ -583,6 +594,13 @@ export interface ProviderHealthMonitorConfig {
583
594
  */
584
595
  serviceAccount?: string;
585
596
  }
597
+ /**
598
+ * New name for `ProviderHealthMonitorConfig` as part of the health dependency
599
+ * inversion (`healthMonitor` → `healthProbe`). Both provider-level fields are
600
+ * accepted transitionally with identical validation; declaring both is a
601
+ * `ValidationError`.
602
+ */
603
+ export type ProviderHealthProbeConfig = ProviderHealthMonitorConfig;
586
604
  export interface HealthMonitorProbeOverride {
587
605
  /** Optional runtime interval override as a positive `ms`-style duration string. */
588
606
  interval?: ProbeInterval;
@@ -632,7 +650,25 @@ export type ConnectionMode = AuthMode;
632
650
  export type ProviderReviewed = "first-party" | "community" | "staging";
633
651
  export type ProviderAccessVisibility = "public" | "early_access";
634
652
  export type ProviderProxyMode = "disabled" | "optional" | "required";
635
- export type ProviderProxyProvider = "smartproxy" | "decodo" | "custom";
653
+ /**
654
+ * Proxy egress vendors. These are FOUR DISTINCT services — do not conflate them
655
+ * (a common mistake because the names collide with a well-known rebrand):
656
+ *
657
+ * - `smartproxy` — **api.smartproxy.org**, a residential proxy with an IP
658
+ * *extraction/allocation* API (app_key → a pool of raw `ip:port` CONNECT
659
+ * endpoints). This is our own vendor. It is NOT the company formerly named
660
+ * "Smartproxy". Credentials: `APIFUSE__PROXY__SMARTPROXY_APP_KEY`.
661
+ * - `nodemaven` — **gate.nodemaven.com**, a *gateway* proxy with static
662
+ * credentials; geo/session encoded in the username, no allocation API.
663
+ * - `decodo` — **decodo.com**, the *gateway* proxy that was named "Smartproxy"
664
+ * (smartproxy.com) before its 2025 rebrand to Decodo. Sticky sessions via
665
+ * username params. A different company from `smartproxy` above.
666
+ * **@deprecated** — unused; no managed adapter. Use `smartproxy`/`nodemaven`,
667
+ * or the `APIFUSE__PROXY__URL` bring-your-own escape hatch.
668
+ * - `custom` — **@deprecated** bring-your-own static proxy URL marker. The
669
+ * `APIFUSE__PROXY__URL` env still works without declaring this value.
670
+ */
671
+ export type ProviderProxyProvider = "smartproxy" | "nodemaven" | "decodo" | "custom";
636
672
  export type ProviderProxySessionAffinity = "request" | "operation" | "auth-flow" | "connection";
637
673
  export interface ProviderProxyPolicy {
638
674
  /**
@@ -640,7 +676,18 @@ export interface ProviderProxyPolicy {
640
676
  * certificate verification, and vendor allocator endpoints are SDK-owned.
641
677
  */
642
678
  mode: ProviderProxyMode;
679
+ /**
680
+ * @deprecated Use `providers: [...]` to declare an ordered vendor fallback
681
+ * chain. A single-element `providers` list is equivalent to this field.
682
+ */
643
683
  provider?: ProviderProxyProvider;
684
+ /**
685
+ * Ordered proxy-vendor fallback chain. The SDK tries each vendor in order and
686
+ * fails over to the next when a vendor lacks credentials or its allocation /
687
+ * transport is exhausted. When omitted, `provider` (or the platform default)
688
+ * is used as a single-vendor chain.
689
+ */
690
+ providers?: ProviderProxyProvider[];
644
691
  geo?: {
645
692
  /** ISO 3166-1 alpha-2 country code, for example KR or US. */
646
693
  country?: Iso3166Alpha2CountryCode;
@@ -805,6 +852,7 @@ export type HttpMethod = "HEAD" | "head" | "GET" | "get" | "POST" | "post" | "PU
805
852
  export interface StealthFetchOptions extends RequestOptions {
806
853
  method?: HttpMethod;
807
854
  body?: string | Buffer;
855
+ redirect?: "follow" | "manual" | "error";
808
856
  /**
809
857
  * Offsets policy-managed proxy pool selection for caller-managed retries.
810
858
  * Use when a request receives an upstream challenge page rather than a
@@ -829,9 +877,19 @@ export interface CookieJar {
829
877
  toString(): string;
830
878
  find?(predicate: (cookie: string) => boolean): string | undefined;
831
879
  }
880
+ export interface StealthSessionCookies extends CookieJar {
881
+ has(name: string): boolean;
882
+ setFromCookieStrings(cookieStrings: readonly string[]): void;
883
+ toHeader(): string;
884
+ snapshot(): Record<string, string>;
885
+ restore(cookies: Record<string, string>): void;
886
+ clear(): void;
887
+ }
832
888
  export interface DeclarativeStealthResponse {
833
889
  status: number;
834
890
  ok: boolean;
891
+ url?: string;
892
+ redirected?: boolean;
835
893
  headers: Record<string, string>;
836
894
  rawHeaders: [string, string][];
837
895
  body: string;
@@ -851,8 +909,30 @@ export type RequestWithMethodOptions = RequestOptions & {
851
909
  method?: string;
852
910
  body?: unknown;
853
911
  };
912
+ export interface StealthRedirectHop {
913
+ url: string;
914
+ status: number;
915
+ method: string;
916
+ location?: string;
917
+ nextUrl?: string;
918
+ }
919
+ export interface StealthRedirectRunOptions extends Omit<StealthFetchOptions, "redirect"> {
920
+ url: string;
921
+ maxHops?: number;
922
+ stopWhen?: (hop: StealthRedirectHop) => boolean | Promise<boolean>;
923
+ }
924
+ export interface StealthRedirectRunResult {
925
+ final: StealthResponse;
926
+ hops: StealthRedirectHop[];
927
+ reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
928
+ cookies: Record<string, string>;
929
+ }
854
930
  export interface StealthSession {
855
931
  fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
932
+ cookies: StealthSessionCookies;
933
+ redirects: {
934
+ run(options: StealthRedirectRunOptions): Promise<StealthRedirectRunResult>;
935
+ };
856
936
  close(): void;
857
937
  }
858
938
  export interface ApiFuseResponse<T> {
@@ -873,6 +953,8 @@ export interface HttpResponse<T = unknown> {
873
953
  data: T;
874
954
  json<U = T>(): Promise<U>;
875
955
  text(): Promise<string>;
956
+ arrayBuffer(): Promise<ArrayBuffer>;
957
+ bytes(): Promise<Uint8Array>;
876
958
  }
877
959
  export interface HttpStreamResponse {
878
960
  status: number;
@@ -983,6 +1065,32 @@ export interface BrowserFrame {
983
1065
  evaluate<T>(fn: string | (() => T)): Promise<T>;
984
1066
  locator(selector: string): BrowserLocator;
985
1067
  }
1068
+ export type BrowserResourceMethod = "GET" | "HEAD";
1069
+ export type BrowserResourceRequest = {
1070
+ readonly url: string;
1071
+ readonly method: BrowserResourceMethod;
1072
+ readonly resourceType?: string;
1073
+ readonly headers: Readonly<Record<string, string>>;
1074
+ };
1075
+ export type BrowserResourceBody = Buffer | Uint8Array | ArrayBuffer | string;
1076
+ export type BrowserResourceDecision = {
1077
+ readonly action: "fulfill";
1078
+ readonly status?: number;
1079
+ readonly headers?: Readonly<Record<string, string>>;
1080
+ readonly body?: BrowserResourceBody;
1081
+ } | {
1082
+ readonly action: "block";
1083
+ readonly reason?: string;
1084
+ };
1085
+ export type BrowserResourceRoute = {
1086
+ readonly match: string | RegExp | ((request: BrowserResourceRequest) => boolean);
1087
+ readonly handle: (request: BrowserResourceRequest) => Promise<BrowserResourceDecision> | BrowserResourceDecision;
1088
+ };
1089
+ export type BrowserResourcePolicy = {
1090
+ readonly defaultAction?: "block";
1091
+ readonly allowedMethods?: readonly BrowserResourceMethod[];
1092
+ readonly routes: readonly BrowserResourceRoute[];
1093
+ };
986
1094
  export interface BrowserPage extends BrowserFrame {
987
1095
  close(): Promise<void>;
988
1096
  fill(selector: string, text: string): Promise<void>;
@@ -997,6 +1105,7 @@ export interface BrowserPage extends BrowserFrame {
997
1105
  timeout?: number;
998
1106
  }): Promise<void>;
999
1107
  frames(): Promise<BrowserFrame[]>;
1108
+ withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T>;
1000
1109
  }
1001
1110
  export type BrowserChallengeRequest = {
1002
1111
  type: "recaptcha";
@@ -1137,6 +1246,73 @@ export interface ContextScratchpad {
1137
1246
  toJSON(): Record<string, unknown>;
1138
1247
  }
1139
1248
  export type FlowContextStore = ContextScratchpad;
1249
+ export type AuthSafeJson = string | number | boolean | null | readonly AuthSafeJson[] | {
1250
+ readonly [key: string]: AuthSafeJson;
1251
+ };
1252
+ export type AuthSafeData = {
1253
+ readonly [key: string]: AuthSafeJson;
1254
+ };
1255
+ export type AuthAbortRetry = "never" | "retry" | "after_user_action";
1256
+ export type AuthAbortData = Record<string, unknown> & {
1257
+ readonly code: string;
1258
+ readonly message?: string;
1259
+ readonly retry?: AuthAbortRetry;
1260
+ readonly actionHint?: AuthSafeJson;
1261
+ readonly fieldErrors?: {
1262
+ readonly [field: string]: string;
1263
+ };
1264
+ readonly details?: AuthSafeData;
1265
+ };
1266
+ export interface AuthFlowTerminalContext {
1267
+ readonly signal?: AbortSignal;
1268
+ readonly deadline?: string;
1269
+ complete<TCredential extends Record<string, string>>(options: {
1270
+ readonly credential: TCredential;
1271
+ readonly metadata?: AuthSafeData;
1272
+ readonly data?: AuthSafeData;
1273
+ readonly turnId?: string;
1274
+ readonly expiresAt?: string;
1275
+ }): AuthTurn;
1276
+ abort(options: {
1277
+ readonly code: string;
1278
+ readonly message?: string;
1279
+ readonly retry?: AuthAbortRetry;
1280
+ readonly actionHint?: AuthSafeJson;
1281
+ readonly fieldErrors?: {
1282
+ readonly [field: string]: string;
1283
+ };
1284
+ readonly data?: AuthSafeData;
1285
+ readonly turnId?: string;
1286
+ readonly expiresAt?: string;
1287
+ }): AuthTurn;
1288
+ nextForm(options: {
1289
+ readonly hintKey?: ProviderLocaleKeyInput;
1290
+ readonly data?: AuthSafeData;
1291
+ readonly turnId?: string;
1292
+ readonly expiresAt?: string;
1293
+ readonly timing?: AuthTurn["timing"];
1294
+ } & ({
1295
+ readonly fields: Record<string, {
1296
+ readonly type?: "string" | "email" | "password" | "otp";
1297
+ readonly labelKey?: ProviderLocaleKeyInput;
1298
+ readonly descriptionKey?: ProviderLocaleKeyInput;
1299
+ readonly placeholderKey?: ProviderLocaleKeyInput;
1300
+ readonly required?: boolean;
1301
+ readonly sensitive?: boolean;
1302
+ }>;
1303
+ readonly expectedInput?: never;
1304
+ } | {
1305
+ readonly expectedInput: Record<string, unknown>;
1306
+ readonly fields?: never;
1307
+ })): AuthTurn;
1308
+ nextPoll(options?: {
1309
+ readonly hintKey?: ProviderLocaleKeyInput;
1310
+ readonly data?: AuthSafeData;
1311
+ readonly turnId?: string;
1312
+ readonly expiresAt?: string;
1313
+ readonly timing?: AuthTurn["timing"];
1314
+ }): AuthTurn;
1315
+ }
1140
1316
  export interface FlowContext {
1141
1317
  connectionId?: string;
1142
1318
  externalRef?: string;
@@ -1148,6 +1324,7 @@ export interface FlowContext {
1148
1324
  credential?: CredentialContext;
1149
1325
  context: ContextScratchpad;
1150
1326
  stt: SttContext;
1327
+ auth: AuthFlowTerminalContext;
1151
1328
  }
1152
1329
  export interface AuthTurn {
1153
1330
  kind: string;
@@ -1270,6 +1447,19 @@ export interface OperationContractMetadata {
1270
1447
  deprecation?: OperationDeprecationMetadata;
1271
1448
  }
1272
1449
  export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOutput extends SchemaLike = SchemaLike> {
1450
+ /**
1451
+ * Short English display title for the operation. The SDK passes it through
1452
+ * verbatim; the APIFuse registry derives the operation's en locale title
1453
+ * baseline from it (`docs.titleKey` remains available as an explicit key
1454
+ * override).
1455
+ */
1456
+ title?: string;
1457
+ /**
1458
+ * Raw English operation description. The SDK passes it through verbatim;
1459
+ * keyed descriptions via `descriptionKey` remain the primary flow, and
1460
+ * provider lint rules for raw prose descriptions still apply.
1461
+ */
1462
+ description?: string;
1273
1463
  descriptionKey?: ProviderLocaleKeyInput;
1274
1464
  docs?: OperationDocMeta;
1275
1465
  whenToUseKeys?: readonly ProviderLocaleKeyInput[];
@@ -1299,10 +1489,54 @@ export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOu
1299
1489
  healthCheck?: HealthCheckSuite<InferSchemaOutput<TInput>, InferSchemaOutput<TOutput>>;
1300
1490
  healthCheckUnsupported?: HealthCheckUnsupported;
1301
1491
  }
1492
+ /**
1493
+ * Author-declared deployment overrides accepted as the optional top-level
1494
+ * `deployment` key on `defineProvider()`.
1495
+ *
1496
+ * The SDK passes this object through VERBATIM onto the returned provider
1497
+ * definition — it is typed here but deliberately not deep-validated: the
1498
+ * APIFuse registry builder owns deployment validation and resolves omitted
1499
+ * fields against the runtime deployment profiles. The shape mirrors the
1500
+ * registry's `ProviderDeploymentOverrides` contract; every field is
1501
+ * optional. Note that the deployment `runtime` axis
1502
+ * (`shared`/`dedicated`/`browser`) is distinct from the provider execution
1503
+ * `runtime` (`standard`/`shared`/`browser`).
1504
+ */
1505
+ export interface ProviderDeploymentOverrides {
1506
+ runtime?: "shared" | "dedicated" | "browser";
1507
+ language?: "typescript" | "python";
1508
+ replicas?: number;
1509
+ hpa?: {
1510
+ enabled: boolean;
1511
+ minReplicas?: number;
1512
+ maxReplicas?: number;
1513
+ targetCPUUtilizationPercentage?: number;
1514
+ };
1515
+ resources?: {
1516
+ cpu: string;
1517
+ memory: string;
1518
+ };
1519
+ cache?: {
1520
+ redis?: {
1521
+ enabled: boolean;
1522
+ url?: string;
1523
+ };
1524
+ };
1525
+ network?: {
1526
+ additionalTcpPorts?: number[];
1527
+ };
1528
+ buildContext?: string;
1529
+ }
1302
1530
  export interface ProviderDefinition {
1303
1531
  id: string;
1304
1532
  version: string;
1305
1533
  runtime: "standard" | "shared" | "browser";
1534
+ /**
1535
+ * Optional deployment overrides, passed through verbatim from
1536
+ * `defineProvider({ deployment })`. Validation and profile resolution are
1537
+ * owned by the APIFuse registry builder, not the SDK.
1538
+ */
1539
+ deployment?: ProviderDeploymentOverrides;
1306
1540
  allowedHosts?: string[];
1307
1541
  stealth?: {
1308
1542
  profile: string;
@@ -1322,5 +1556,7 @@ export interface ProviderDefinition {
1322
1556
  meta: ProviderMeta;
1323
1557
  operations: Record<string, OperationDefinition<SchemaLike, SchemaLike>>;
1324
1558
  healthMonitor?: ProviderHealthMonitorConfig;
1559
+ /** Transitional alias for `healthMonitor`; `defineProvider` mirrors both. */
1560
+ healthProbe?: ProviderHealthProbeConfig;
1325
1561
  healthJourneys?: readonly HealthJourneyDefinition[];
1326
1562
  }
@@ -0,0 +1,30 @@
1
+ export declare const NEEDS_INPUT_STATUS: "needs_input";
2
+ export interface ProviderSelectionOption {
3
+ readonly selection_value: string;
4
+ readonly label: string;
5
+ readonly [extra: string]: unknown;
6
+ }
7
+ export interface ProviderRequiredSelection {
8
+ readonly selection_key: string;
9
+ readonly label: string;
10
+ readonly required: boolean;
11
+ readonly selection_type?: string;
12
+ readonly valid_options: readonly ProviderSelectionOption[];
13
+ readonly [extra: string]: unknown;
14
+ }
15
+ export interface ProviderSelectedOption {
16
+ readonly selection_key: string;
17
+ readonly selection_value: string;
18
+ /** Free-text answer for text-kind selections. */
19
+ readonly text?: string;
20
+ /** Quantity for quantity-kind selections. */
21
+ readonly quantity?: number;
22
+ }
23
+ export interface ProviderNeedsInputPayload {
24
+ readonly status: typeof NEEDS_INPUT_STATUS;
25
+ readonly required_selections: readonly ProviderRequiredSelection[];
26
+ readonly selected_options?: readonly ProviderSelectedOption[];
27
+ /** Provider-specific fresh state token(s), e.g. `reservation_state`. */
28
+ readonly [extra: string]: unknown;
29
+ }
30
+ export declare function isProviderNeedsInputPayload(value: unknown): value is ProviderNeedsInputPayload;
@@ -0,0 +1,66 @@
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
+ export const NEEDS_INPUT_STATUS = "needs_input";
28
+ function isRecord(value) {
29
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30
+ }
31
+ function isSelectionOption(value) {
32
+ return (isRecord(value) &&
33
+ typeof value.selection_value === "string" &&
34
+ typeof value.label === "string");
35
+ }
36
+ function isRequiredSelection(value) {
37
+ return (isRecord(value) &&
38
+ typeof value.selection_key === "string" &&
39
+ typeof value.label === "string" &&
40
+ typeof value.required === "boolean" &&
41
+ Array.isArray(value.valid_options) &&
42
+ value.valid_options.every(isSelectionOption));
43
+ }
44
+ function isSelectedOption(value) {
45
+ return (isRecord(value) &&
46
+ typeof value.selection_key === "string" &&
47
+ typeof value.selection_value === "string");
48
+ }
49
+ export function isProviderNeedsInputPayload(value) {
50
+ if (!isRecord(value)) {
51
+ return false;
52
+ }
53
+ if (value.status !== NEEDS_INPUT_STATUS ||
54
+ !Array.isArray(value.required_selections) ||
55
+ !value.required_selections.every(isRequiredSelection)) {
56
+ return false;
57
+ }
58
+ // A needs_input with nothing to ask AND nothing settled to echo is a
59
+ // no-op dead end — reject it so providers cannot ship it accidentally.
60
+ if (value.required_selections.length > 0) {
61
+ return true;
62
+ }
63
+ return (Array.isArray(value.selected_options) &&
64
+ value.selected_options.length > 0 &&
65
+ value.selected_options.every(isSelectedOption));
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
+ "version": "2.2.0-beta.10",
2
3
  "name": "@apifuse/provider-sdk",
3
- "version": "2.1.0-beta.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "APIFuse Provider SDK — Build providers with zero architectural constraints",
@@ -50,6 +50,12 @@
50
50
  "import": "./dist/contract.js",
51
51
  "default": "./dist/contract.js"
52
52
  },
53
+ "./auth-turn": {
54
+ "types": "./dist/auth-turn/index.d.ts",
55
+ "import": "./dist/auth-turn/index.js",
56
+ "default": "./dist/auth-turn/index.js"
57
+ },
58
+ "./auth-turn/auth-turn.v1.schema.json": "./dist/auth-turn/auth-turn.v1.schema.json",
53
59
  "./server": {
54
60
  "types": "./dist/server/index.d.ts",
55
61
  "import": "./dist/server/index.js",
@@ -69,25 +75,30 @@
69
75
  "scripts": {
70
76
  "lint": "biome lint .",
71
77
  "lint:fix": "biome lint --write",
78
+ "lint:deprecated": "bun run scripts/lint-deprecated-usage.ts",
72
79
  "format": "biome format --write",
73
- "type-check": "tsgo --noEmit",
80
+ "type-check": "tsc --noEmit",
74
81
  "test": "bun test",
75
- "check": "bun run lint && bun run type-check && bun run build",
82
+ "check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run build",
76
83
  "pack:check": "bun run build && bun bin/apifuse-pack-check.ts",
77
84
  "pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
85
+ "pack:types": "bun run build && bun bin/apifuse-pack-types.ts",
78
86
  "release:guard": "bun scripts/guard-release-pr.ts",
79
87
  "format:check": "biome format .",
80
- "build": "tsgo -p tsconfig.build.json"
88
+ "build": "tsc -p tsconfig.build.json && rm -rf dist/cli/templates && cp -R src/cli/templates dist/cli/templates && rm -rf dist/auth-turn/fixtures && cp -R src/auth-turn/fixtures dist/auth-turn/fixtures && cp src/auth-turn/auth-turn.v1.schema.json dist/auth-turn/auth-turn.v1.schema.json"
81
89
  },
82
90
  "devDependencies": {
91
+ "@arethetypeswrong/cli": "^0.18.5",
83
92
  "@biomejs/biome": "^2.5.0",
84
93
  "@types/bun": "latest",
85
94
  "@types/node": "^25.9.3",
86
- "@typescript/native-preview": "7.0.0-dev.20260419.1"
95
+ "typescript": "6.0.3"
87
96
  },
88
97
  "dependencies": {
89
98
  "@clack/prompts": "^1.5.1",
99
+ "@rgrove/parse-xml": "4.2.2",
90
100
  "@types/ms": "^2.1.0",
101
+ "acorn": "^8.17.0",
91
102
  "ajv": "^8.17",
92
103
  "hono": "^4.12.25",
93
104
  "impit": "0.14.1",
@@ -0,0 +1,89 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://apifuse.com/contracts/auth-turn/v1",
4
+ "title": "APIFuse AuthTurn envelope v1",
5
+ "description": "Protocol message exchanged during auth.flow ceremonies. kind is an open string; known kinds are tooling metadata, not a wire constraint.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["kind", "turnId"],
9
+ "properties": {
10
+ "kind": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "Open turn kind. Known kinds are listed in the TURN_KINDS registry; unknown kinds remain valid on the wire."
14
+ },
15
+ "turnId": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "description": "Provider-scoped identifier of this turn."
19
+ },
20
+ "expiresAt": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "description": "Turn expiry timestamp as an ISO 8601 / RFC 3339 string."
24
+ },
25
+ "data": {
26
+ "type": "object",
27
+ "additionalProperties": true,
28
+ "description": "Kind-specific payload. Terminal kinds carry the payloads described in $defs."
29
+ },
30
+ "expectedInput": {
31
+ "type": "object",
32
+ "additionalProperties": true,
33
+ "description": "JSON Schema describing the input expected next."
34
+ },
35
+ "hint": {
36
+ "type": "string",
37
+ "description": "Deprecated but load-bearing human-readable hint materialized from provider locale catalogs."
38
+ },
39
+ "hintKey": {
40
+ "type": "string",
41
+ "description": "Provider locale catalog key for the turn hint."
42
+ },
43
+ "timing": {
44
+ "type": "object",
45
+ "additionalProperties": false,
46
+ "description": "Client pacing guidance for poll-style turns.",
47
+ "properties": {
48
+ "suggestedPollIntervalMs": {
49
+ "type": "number",
50
+ "minimum": 1
51
+ },
52
+ "maxWaitMs": {
53
+ "type": "number",
54
+ "minimum": 1
55
+ }
56
+ }
57
+ }
58
+ },
59
+ "$defs": {
60
+ "completeTurnData": {
61
+ "title": "Terminal payload for kind \"complete\"",
62
+ "description": "data payload of a complete turn. The gateway extracts data.credential for persistence; complete turns are never echoed to browsers.",
63
+ "type": "object",
64
+ "additionalProperties": true,
65
+ "required": ["credential"],
66
+ "properties": {
67
+ "credential": {
68
+ "type": "object",
69
+ "additionalProperties": true
70
+ },
71
+ "metadata": {
72
+ "type": "object",
73
+ "additionalProperties": true
74
+ }
75
+ }
76
+ },
77
+ "abortTurnData": {
78
+ "title": "Terminal payload for kind \"abort\"",
79
+ "description": "data payload of an abort turn. code, when present, is the machine-readable abort reason.",
80
+ "type": "object",
81
+ "additionalProperties": true,
82
+ "properties": {
83
+ "code": {
84
+ "type": "string"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "kind": "",
3
+ "turnId": "fixture.invalid.empty-kind"
4
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "kind": "challenge",
3
+ "turnId": "fixture.invalid.expires-at-not-string",
4
+ "expiresAt": 1767225600000
5
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "turnId": "fixture.invalid.missing-kind"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "kind": "form"
3
+ }