@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.30

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 (342) hide show
  1. package/AUTHORING.md +493 -5
  2. package/CHANGELOG.md +127 -1
  3. package/README.md +52 -6
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +106 -62
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +63 -55
  8. package/bin/apifuse-pack-check.ts +22 -2
  9. package/bin/apifuse-pack-smoke.ts +78 -82
  10. package/bin/apifuse-pack-types.ts +583 -0
  11. package/bin/apifuse-perf.ts +59 -140
  12. package/bin/apifuse-record.ts +698 -113
  13. package/bin/apifuse-submit-check.ts +517 -44
  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.ts +1 -1
  18. package/dist/auth-turn/index.d.ts +4 -4
  19. package/dist/auth-turn/index.js +1 -1
  20. package/dist/auth.d.ts +16 -2
  21. package/dist/auth.js +76 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +117 -29
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/cli/templates/provider/README.md.tpl +4 -4
  32. package/dist/config/loader.d.ts +176 -17
  33. package/dist/config/loader.js +434 -161
  34. package/dist/contract-serialization.d.ts +2 -2
  35. package/dist/contract-serialization.js +7 -14
  36. package/dist/contract-types.d.ts +3 -2
  37. package/dist/contract.d.ts +3 -3
  38. package/dist/contract.js +6 -6
  39. package/dist/declaration-validation.d.ts +23 -0
  40. package/dist/declaration-validation.js +159 -0
  41. package/dist/define.d.ts +13 -1
  42. package/dist/define.js +391 -122
  43. package/dist/dev.d.ts +1 -1
  44. package/dist/dev.js +1 -1
  45. package/dist/error-resolution.d.ts +4 -0
  46. package/dist/error-resolution.js +122 -0
  47. package/dist/errors.d.ts +19 -1
  48. package/dist/errors.js +41 -3
  49. package/dist/fixture-sanitization.d.ts +26 -0
  50. package/dist/fixture-sanitization.js +216 -0
  51. package/dist/i18n/catalog.d.ts +2 -2
  52. package/dist/i18n/catalog.js +4 -10
  53. package/dist/i18n/index.d.ts +2 -2
  54. package/dist/i18n/index.js +2 -2
  55. package/dist/i18n/keys.d.ts +2 -2
  56. package/dist/index.d.ts +50 -42
  57. package/dist/index.js +41 -37
  58. package/dist/lint.d.ts +6 -1
  59. package/dist/lint.js +370 -18
  60. package/dist/native-address.d.ts +43 -0
  61. package/dist/native-address.js +281 -0
  62. package/dist/native-egress-policy.d.ts +31 -0
  63. package/dist/native-egress-policy.js +288 -0
  64. package/dist/observability.d.ts +5 -2
  65. package/dist/observability.js +48 -1
  66. package/dist/provider.d.ts +13 -11
  67. package/dist/provider.js +10 -9
  68. package/dist/public-schema-field-lint.d.ts +1 -1
  69. package/dist/recipes/gov-api.js +1 -1
  70. package/dist/runtime/auth-flow.d.ts +3 -1
  71. package/dist/runtime/auth-flow.js +8 -3
  72. package/dist/runtime/browser.d.ts +1 -1
  73. package/dist/runtime/browser.js +138 -40
  74. package/dist/runtime/cache.d.ts +2 -1
  75. package/dist/runtime/cache.js +173 -23
  76. package/dist/runtime/choice-wordlist.d.ts +9 -0
  77. package/dist/runtime/choice-wordlist.js +138 -0
  78. package/dist/runtime/choice.d.ts +13 -1
  79. package/dist/runtime/choice.js +514 -124
  80. package/dist/runtime/credential.d.ts +1 -1
  81. package/dist/runtime/credential.js +1 -1
  82. package/dist/runtime/env.d.ts +1 -1
  83. package/dist/runtime/executor.d.ts +1 -1
  84. package/dist/runtime/executor.js +25 -3
  85. package/dist/runtime/http.d.ts +3 -2
  86. package/dist/runtime/http.js +517 -55
  87. package/dist/runtime/insights.d.ts +1 -1
  88. package/dist/runtime/insights.js +6 -13
  89. package/dist/runtime/instrumentation.d.ts +2 -2
  90. package/dist/runtime/instrumentation.js +371 -23
  91. package/dist/runtime/keyring.js +1 -1
  92. package/dist/runtime/namespace.js +1 -1
  93. package/dist/runtime/native-network-errors.d.ts +33 -0
  94. package/dist/runtime/native-network-errors.js +69 -0
  95. package/dist/runtime/native-network.d.ts +96 -0
  96. package/dist/runtime/native-network.js +1232 -0
  97. package/dist/runtime/ocr.d.ts +29 -0
  98. package/dist/runtime/ocr.js +440 -0
  99. package/dist/runtime/otlp.d.ts +1 -1
  100. package/dist/runtime/perf.d.ts +1 -1
  101. package/dist/runtime/provider.d.ts +1 -1
  102. package/dist/runtime/provider.js +1 -2
  103. package/dist/runtime/proxy-errors.d.ts +1 -1
  104. package/dist/runtime/proxy-errors.js +9 -7
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  108. package/dist/runtime/proxy-retry-policy.js +2 -2
  109. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  110. package/dist/runtime/proxy-telemetry.js +58 -52
  111. package/dist/runtime/redirects.d.ts +29 -0
  112. package/dist/runtime/redirects.js +36 -0
  113. package/dist/runtime/redis.d.ts +1 -1
  114. package/dist/runtime/redis.js +5 -5
  115. package/dist/runtime/request-options.d.ts +68 -1
  116. package/dist/runtime/request-options.js +548 -0
  117. package/dist/runtime/resolver-config.d.ts +6 -0
  118. package/dist/runtime/resolver-config.js +6 -0
  119. package/dist/runtime/resolver-public.d.ts +1 -0
  120. package/dist/runtime/resolver-public.js +1 -0
  121. package/dist/runtime/resolver-shared.d.ts +3 -0
  122. package/dist/runtime/resolver-shared.js +12 -0
  123. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  124. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  125. package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
  126. package/dist/runtime/resolver-vendors/browser.js +282 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +83 -0
  132. package/dist/runtime/resolver-vendors/types.js +69 -0
  133. package/dist/runtime/resolver.d.ts +59 -0
  134. package/dist/runtime/resolver.js +705 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +5 -2
  138. package/dist/runtime/state.js +280 -74
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +30 -5
  142. package/dist/runtime/stealth.js +523 -259
  143. package/dist/runtime/stt.d.ts +1 -1
  144. package/dist/runtime/stt.js +12 -27
  145. package/dist/runtime/timeout.d.ts +5 -0
  146. package/dist/runtime/timeout.js +12 -0
  147. package/dist/runtime/trace.d.ts +2 -2
  148. package/dist/runtime/trace.js +2 -4
  149. package/dist/runtime/waterfall.d.ts +1 -1
  150. package/dist/schema.d.ts +1 -1
  151. package/dist/schema.js +7 -15
  152. package/dist/serve.d.ts +1 -1
  153. package/dist/serve.js +1 -1
  154. package/dist/server/index.d.ts +7 -7
  155. package/dist/server/index.js +6 -6
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test-redaction.d.ts +1 -1
  159. package/dist/server/self-test-redaction.js +1 -1
  160. package/dist/server/self-test.d.ts +117 -3
  161. package/dist/server/self-test.js +787 -151
  162. package/dist/server/serve-implementation.d.ts +210 -0
  163. package/dist/server/serve-implementation.js +2077 -0
  164. package/dist/server/serve.d.ts +1 -70
  165. package/dist/server/serve.js +1 -1143
  166. package/dist/server/types.d.ts +34 -9
  167. package/dist/server/types.js +8 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +1 -1
  207. package/dist/stealth/profiles.js +21 -21
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.d.ts +1 -1
  211. package/dist/stream.js +7 -1
  212. package/dist/testing/index.d.ts +3 -2
  213. package/dist/testing/index.js +3 -2
  214. package/dist/testing/run.d.ts +32 -2
  215. package/dist/testing/run.js +488 -28
  216. package/dist/types.d.ts +566 -19
  217. package/dist/types.js +1 -0
  218. package/dist/user-input.d.ts +30 -0
  219. package/dist/user-input.js +66 -0
  220. package/package.json +42 -7
  221. package/src/auth-turn/index.ts +2 -2
  222. package/src/auth.ts +146 -86
  223. package/src/ceremonies/index.ts +167 -92
  224. package/src/cli/commands.ts +10 -0
  225. package/src/cli/create.ts +42 -35
  226. package/src/cli/prompt-assets.ts +865 -0
  227. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  228. package/src/cli/templates/provider/README.md.tpl +4 -4
  229. package/src/config/loader.ts +667 -289
  230. package/src/contract-serialization.ts +10 -18
  231. package/src/contract-types.ts +3 -2
  232. package/src/contract.ts +14 -28
  233. package/src/declaration-validation.ts +202 -0
  234. package/src/define.ts +631 -495
  235. package/src/dev.ts +4 -9
  236. package/src/error-resolution.ts +127 -0
  237. package/src/errors.ts +56 -11
  238. package/src/fixture-sanitization.ts +247 -0
  239. package/src/i18n/catalog.ts +10 -32
  240. package/src/i18n/index.ts +2 -2
  241. package/src/i18n/keys.ts +5 -11
  242. package/src/index.ts +157 -44
  243. package/src/lint.ts +488 -154
  244. package/src/native-address.ts +340 -0
  245. package/src/native-egress-policy.ts +358 -0
  246. package/src/observability.ts +51 -1
  247. package/src/provider.ts +65 -11
  248. package/src/public-schema-field-lint.ts +7 -33
  249. package/src/recipes/gov-api.ts +2 -5
  250. package/src/runtime/auth-flow.ts +13 -7
  251. package/src/runtime/browser.ts +252 -207
  252. package/src/runtime/cache.ts +209 -81
  253. package/src/runtime/choice-wordlist.ts +145 -0
  254. package/src/runtime/choice.ts +685 -216
  255. package/src/runtime/credential.ts +2 -2
  256. package/src/runtime/env.ts +1 -1
  257. package/src/runtime/executor.ts +37 -19
  258. package/src/runtime/http.ts +645 -65
  259. package/src/runtime/insights.ts +15 -53
  260. package/src/runtime/instrumentation.ts +530 -67
  261. package/src/runtime/keyring.ts +7 -19
  262. package/src/runtime/namespace.ts +2 -7
  263. package/src/runtime/native-network-errors.ts +99 -0
  264. package/src/runtime/native-network.ts +1605 -0
  265. package/src/runtime/ocr.ts +523 -0
  266. package/src/runtime/otlp.ts +12 -23
  267. package/src/runtime/perf.ts +1 -1
  268. package/src/runtime/provider.ts +4 -9
  269. package/src/runtime/proxy-errors.ts +29 -42
  270. package/src/runtime/proxy-nodemaven.ts +221 -0
  271. package/src/runtime/proxy-retry-policy.ts +3 -3
  272. package/src/runtime/proxy-telemetry.ts +84 -77
  273. package/src/runtime/redirects.ts +66 -0
  274. package/src/runtime/redis.ts +10 -13
  275. package/src/runtime/request-options.ts +679 -9
  276. package/src/runtime/resolver-config.ts +6 -0
  277. package/src/runtime/resolver-public.ts +18 -0
  278. package/src/runtime/resolver-shared.ts +17 -0
  279. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  280. package/src/runtime/resolver-vendors/browser.ts +408 -0
  281. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  282. package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
  283. package/src/runtime/resolver-vendors/types.ts +173 -0
  284. package/src/runtime/resolver.ts +1060 -0
  285. package/src/runtime/secrets.ts +64 -0
  286. package/src/runtime/state.ts +399 -161
  287. package/src/runtime/stealth-cookies.ts +132 -0
  288. package/src/runtime/stealth.ts +681 -295
  289. package/src/runtime/stt.ts +39 -113
  290. package/src/runtime/timeout.ts +18 -0
  291. package/src/runtime/trace.ts +14 -44
  292. package/src/runtime/waterfall.ts +5 -18
  293. package/src/schema.ts +23 -84
  294. package/src/serve.ts +6 -1
  295. package/src/server/index.ts +30 -7
  296. package/src/server/self-test-input-tokens.ts +29 -14
  297. package/src/server/self-test-redaction.ts +2 -2
  298. package/src/server/self-test.ts +1030 -180
  299. package/src/server/serve-implementation.ts +3060 -0
  300. package/src/server/serve.ts +1 -1781
  301. package/src/server/types.ts +12 -13
  302. package/src/stateful/README.md +146 -0
  303. package/src/stateful/errors.ts +35 -0
  304. package/src/stateful/http-provider-event-emitter.ts +314 -0
  305. package/src/stateful/http-session-owner-registry.ts +306 -0
  306. package/src/stateful/index.ts +18 -0
  307. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  308. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  309. package/src/stateful/provider-event-pipeline.ts +61 -0
  310. package/src/stateful/provider-events.ts +462 -0
  311. package/src/stateful/session-key.ts +111 -0
  312. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  313. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  314. package/src/stateful/stateful-provider-adapter.ts +562 -0
  315. package/src/stateful/stateful-provider-observability.ts +261 -0
  316. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  317. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  318. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  319. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  320. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  321. package/src/stateful-signing.ts +46 -0
  322. package/src/stealth/profiles.ts +27 -33
  323. package/src/stream-evidence.ts +988 -0
  324. package/src/stream.ts +16 -20
  325. package/src/testing/index.ts +11 -2
  326. package/src/testing/run.ts +668 -74
  327. package/src/types.ts +665 -35
  328. package/src/user-input.ts +118 -0
  329. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  330. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  331. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  332. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  333. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  334. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  335. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  336. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  337. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  338. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  339. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  340. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  341. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  342. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,9 +1,13 @@
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 { createUnsupportedOcrClient } from "../runtime/ocr.js";
6
+ import { createUnsupportedSttClient } from "../runtime/stt.js";
7
+ import { createNativeEgressAuthorization, NativeNetworkError, snapshotNativeConnectInput, snapshotNativeGrantInput, } from "../runtime/native-network.js";
8
+ import { safeParseSchemaSync } from "../schema.js";
9
+ import { requestPathForFixture } from "../fixture-sanitization.js";
10
+ import { findStreamCaptureGroup, replayStreamEvidence } from "../stream-evidence.js";
7
11
  // Mirrors CONNECTOR_ID_REGEX in ../define.ts, which defineProvider() enforces.
8
12
  // A single lowercase segment (no hyphen) is a valid id, so the trailing group
9
13
  // is optional (`*`), matching providers like `kakaomap`, `kstartup`, `triple`.
@@ -13,8 +17,10 @@ const VALID_AUTH_MODES = [
13
17
  "platform-managed",
14
18
  "credentials",
15
19
  "oauth2",
20
+ "oauth2_proxied",
16
21
  ];
17
22
  const UPDATE_SNAPSHOT_ARGS = new Set(["-u", "--update-snapshots"]);
23
+ const snapshotCaptureStates = new WeakMap();
18
24
  function isFixtureEnvelope(value) {
19
25
  return (value !== null &&
20
26
  typeof value === "object" &&
@@ -60,9 +66,7 @@ function inferFixtureDir(providerId) {
60
66
  .map((line) => line.match(/\(?((?:file:\/\/)?[^():]+\.test\.ts)/)?.[1])
61
67
  .find((file) => file !== undefined);
62
68
  if (testFile) {
63
- const pathname = testFile.startsWith("file://")
64
- ? new URL(testFile).pathname
65
- : testFile;
69
+ const pathname = testFile.startsWith("file://") ? new URL(testFile).pathname : testFile;
66
70
  return `${pathname.replace(/\/[^/]+$/, "")}/../__fixtures__`;
67
71
  }
68
72
  return `providers/${providerId}/__fixtures__`;
@@ -81,10 +85,354 @@ function jsonResponse(data) {
81
85
  bytes: async () => bodyBytes.slice(0),
82
86
  };
83
87
  }
88
+ function headersToRecord(headers) {
89
+ return Object.fromEntries(headers.entries());
90
+ }
91
+ async function normalizeUpstreamResponse(response) {
92
+ if (response instanceof Response) {
93
+ const bytes = new Uint8Array(await response.arrayBuffer());
94
+ const text = new TextDecoder().decode(bytes);
95
+ const headers = headersToRecord(response.headers);
96
+ let data = text;
97
+ if (response.headers.get("content-type")?.includes("application/json")) {
98
+ data = text.length > 0 ? JSON.parse(text) : null;
99
+ }
100
+ return { status: response.status, headers, data, text, bytes };
101
+ }
102
+ const headers = { ...(response.headers ?? {}) };
103
+ const body = response.body ?? null;
104
+ let bytes;
105
+ let text;
106
+ let data;
107
+ if (body instanceof Uint8Array) {
108
+ bytes = body.slice(0);
109
+ text = new TextDecoder().decode(bytes);
110
+ data = text;
111
+ }
112
+ else if (body instanceof ArrayBuffer) {
113
+ bytes = new Uint8Array(body.slice(0));
114
+ text = new TextDecoder().decode(bytes);
115
+ data = text;
116
+ }
117
+ else if (typeof body === "string") {
118
+ text = body;
119
+ bytes = new TextEncoder().encode(text);
120
+ data = body;
121
+ }
122
+ else {
123
+ text = JSON.stringify(body);
124
+ bytes = new TextEncoder().encode(text);
125
+ data = body;
126
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
127
+ headers["content-type"] = "application/json";
128
+ }
129
+ }
130
+ return { status: response.status ?? 200, headers, data, text, bytes };
131
+ }
132
+ function toHttpResponse(response) {
133
+ return {
134
+ status: response.status,
135
+ ok: response.status >= 200 && response.status < 300,
136
+ headers: response.headers,
137
+ data: response.data,
138
+ json: async () => JSON.parse(response.text),
139
+ text: async () => response.text,
140
+ arrayBuffer: async () => response.bytes.slice(0).buffer,
141
+ bytes: async () => response.bytes.slice(0),
142
+ };
143
+ }
144
+ const emptyCookieJar = {
145
+ get: () => undefined,
146
+ getAll: () => ({}),
147
+ toString: () => "",
148
+ };
149
+ function emptyCookieStore() {
150
+ return {
151
+ version: 1,
152
+ jar: { cookies: [] },
153
+ };
154
+ }
155
+ function toStealthResponse(response, url) {
156
+ return {
157
+ status: response.status,
158
+ ok: response.status >= 200 && response.status < 300,
159
+ url,
160
+ redirected: false,
161
+ headers: response.headers,
162
+ rawHeaders: Object.entries(response.headers),
163
+ body: response.text,
164
+ cookies: emptyCookieJar,
165
+ json: async () => JSON.parse(response.text),
166
+ arrayBuffer: async () => response.bytes.slice(0).buffer,
167
+ bytes: async () => response.bytes.slice(0),
168
+ };
169
+ }
170
+ function streamFromBytes(bytes) {
171
+ return new ReadableStream({
172
+ start(controller) {
173
+ controller.enqueue(bytes.slice(0));
174
+ controller.close();
175
+ },
176
+ });
177
+ }
178
+ async function* singleBytes(bytes) {
179
+ yield bytes.slice(0);
180
+ }
181
+ async function* singleText(textValue) {
182
+ yield textValue;
183
+ }
184
+ function createUpstreamContext(provider, operationName, upstreamStub) {
185
+ const credential = {
186
+ mode: "none",
187
+ get: () => undefined,
188
+ getAll: () => ({}),
189
+ getAccessToken: () => undefined,
190
+ getScopes: () => [],
191
+ };
192
+ const request = { headers: {} };
193
+ const state = createMemoryProviderRuntimeState();
194
+ const nativeEgress = provider.native
195
+ ? createNativeEgressAuthorization({ egress: provider.native.network })
196
+ : undefined;
197
+ const requireNativeEgress = () => {
198
+ if (!nativeEgress)
199
+ throw new NativeNetworkError("Native egress authorization is unavailable", "native_egress_policy_invalid");
200
+ return nativeEgress;
201
+ };
202
+ const dispatch = async (call) => {
203
+ const canned = await upstreamStub({ operationName, ...call });
204
+ if (canned === undefined) {
205
+ throw new Error(`Unmatched upstream call for operation "${operationName}": ${call.transport}.${call.method}${call.url ? ` ${call.url}` : ""}. Add a canned response to upstreamStub; live network passthrough is disabled.`);
206
+ }
207
+ return normalizeUpstreamResponse(canned);
208
+ };
209
+ const httpCall = async (method, url, body, options) => toHttpResponse(await dispatch({ transport: "http", method, url, body, options }));
210
+ const stealthCall = async (url, options) => toStealthResponse(await dispatch({
211
+ transport: "stealth",
212
+ method: options?.method?.toUpperCase() ?? "GET",
213
+ url,
214
+ body: options?.body,
215
+ options,
216
+ }), url);
217
+ const createBrowserPage = () => {
218
+ let currentUrl = "about:blank";
219
+ let currentResponse;
220
+ const browserAction = async (method, body) => {
221
+ currentResponse = await dispatch({
222
+ transport: "browser",
223
+ method,
224
+ url: currentUrl,
225
+ body,
226
+ });
227
+ return currentResponse;
228
+ };
229
+ const page = {
230
+ id: `standard-test-${operationName}`,
231
+ url: async () => currentUrl,
232
+ title: async () => currentResponse?.text.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] ?? "",
233
+ content: async () => currentResponse?.text ?? "",
234
+ evaluate: async (fn) => (await browserAction("evaluate", typeof fn === "string" ? fn : String(fn))).data,
235
+ locator: (selector) => ({
236
+ click: async () => {
237
+ await browserAction("locator.click", { selector });
238
+ },
239
+ fill: async (textValue) => {
240
+ await browserAction("locator.fill", { selector, text: textValue });
241
+ },
242
+ textContent: async () => {
243
+ const value = (await browserAction("locator.textContent", { selector })).data;
244
+ return value === null || value === undefined ? null : String(value);
245
+ },
246
+ waitFor: async (options) => {
247
+ await browserAction("locator.waitFor", { selector, options });
248
+ },
249
+ }),
250
+ close: async () => { },
251
+ cookies: async () => (await browserAction("cookies")).data,
252
+ fill: async (selector, textValue) => {
253
+ await browserAction("fill", { selector, text: textValue });
254
+ },
255
+ goto: async (url) => {
256
+ currentUrl = url;
257
+ await browserAction("goto");
258
+ },
259
+ screenshot: async (options) => Buffer.from((await browserAction("screenshot", options)).bytes),
260
+ click: async (selector) => {
261
+ await browserAction("click", { selector });
262
+ },
263
+ type: async (selector, textValue) => {
264
+ await browserAction("type", { selector, text: textValue });
265
+ },
266
+ waitForSelector: async (selector, options) => {
267
+ await browserAction("waitForSelector", { selector, options });
268
+ },
269
+ frames: async () => [page],
270
+ withResourcePolicy: async (_policy, run) => run(),
271
+ };
272
+ return page;
273
+ };
274
+ const createStealthSession = () => ({
275
+ fetch: stealthCall,
276
+ cookies: {
277
+ ...emptyCookieJar,
278
+ has: () => false,
279
+ setFromCookieStrings: () => { },
280
+ toHeader: () => "",
281
+ snapshot: () => ({}),
282
+ restore: () => { },
283
+ serialize: emptyCookieStore,
284
+ deserialize: () => { },
285
+ clear: () => { },
286
+ },
287
+ redirects: {
288
+ run: async (options) => {
289
+ const final = await stealthCall(options.url, options);
290
+ return {
291
+ final,
292
+ hops: [],
293
+ reason: "completed",
294
+ cookies: {},
295
+ cookieStore: emptyCookieStore(),
296
+ };
297
+ },
298
+ },
299
+ close: () => { },
300
+ });
301
+ return {
302
+ env: { get: () => undefined },
303
+ credential,
304
+ request,
305
+ http: {
306
+ request: (url, options) => httpCall(options?.method?.toUpperCase() ?? "GET", url, options?.body, options),
307
+ get: (url, options) => httpCall("GET", url, undefined, options),
308
+ post: (url, body, options) => httpCall("POST", url, body, options),
309
+ put: (url, body, options) => httpCall("PUT", url, body, options),
310
+ delete: (url, options) => httpCall("DELETE", url, undefined, options),
311
+ stream: async (url, options) => {
312
+ const response = await dispatch({
313
+ transport: "http",
314
+ method: options?.method?.toUpperCase() ?? "GET",
315
+ url,
316
+ body: options?.body,
317
+ options,
318
+ });
319
+ return {
320
+ status: response.status,
321
+ ok: response.status >= 200 && response.status < 300,
322
+ headers: response.headers,
323
+ body: streamFromBytes(response.bytes),
324
+ bytes: () => singleBytes(response.bytes),
325
+ textChunks: () => singleText(response.text),
326
+ lines: () => singleText(response.text),
327
+ };
328
+ },
329
+ sse: async (url, options) => {
330
+ const response = await dispatch({
331
+ transport: "http",
332
+ method: options?.method?.toUpperCase() ?? "GET",
333
+ url,
334
+ body: options?.body,
335
+ options,
336
+ });
337
+ async function* messages() {
338
+ for (const block of response.text.split(/\r?\n\r?\n/)) {
339
+ const data = block
340
+ .split(/\r?\n/)
341
+ .filter((line) => line.startsWith("data:"))
342
+ .map((line) => line.slice(5).trimStart())
343
+ .join("\n");
344
+ if (!data)
345
+ continue;
346
+ yield { event: "message", data, json: () => JSON.parse(data) };
347
+ }
348
+ }
349
+ return messages();
350
+ },
351
+ },
352
+ cache: createProviderCache({ providerId: `standard-test-${operationName}` }),
353
+ state,
354
+ stealth: {
355
+ fetch: stealthCall,
356
+ createSession: createStealthSession,
357
+ },
358
+ browser: {
359
+ engine: "playwright-stealth",
360
+ newPage: async () => createBrowserPage(),
361
+ rawPage: async () => createBrowserPage(),
362
+ withIsolatedContext: async (handler) => handler(createBrowserPage()),
363
+ solveChallenge: async (challenge) => (await dispatch({
364
+ transport: "browser",
365
+ method: "solveChallenge",
366
+ body: challenge,
367
+ })).data,
368
+ },
369
+ ...(provider.native
370
+ ? {
371
+ native: {
372
+ network: {
373
+ connectTcp: async (options) => {
374
+ const request = snapshotNativeConnectInput(options);
375
+ requireNativeEgress().assertConnect(request, "disabled");
376
+ return createNativeConnection(await dispatch({
377
+ transport: "native",
378
+ method: "connectTcp",
379
+ url: `tcp://${request.host}:${request.port}`,
380
+ options: request,
381
+ }), dispatch, `tcp://${request.host}:${request.port}`);
382
+ },
383
+ connectTls: async (options) => {
384
+ const request = snapshotNativeConnectInput(options);
385
+ requireNativeEgress().assertConnect(request, "required");
386
+ return createNativeConnection(await dispatch({
387
+ transport: "native",
388
+ method: "connectTls",
389
+ url: `tls://${request.host}:${request.port}`,
390
+ options: request,
391
+ }), dispatch, `tls://${request.host}:${request.port}`);
392
+ },
393
+ grantTcpEgress: (input) => {
394
+ return requireNativeEgress().grant(snapshotNativeGrantInput(input));
395
+ },
396
+ },
397
+ },
398
+ }
399
+ : {}),
400
+ trace: { span: async (_name, fn) => fn() },
401
+ auth: { requestField: async (name) => unsupported(`ctx.auth.requestField(${name})`) },
402
+ ocr: createUnsupportedOcrClient("Standard test upstream context does not support ctx.ocr.recognize"),
403
+ stt: createUnsupportedSttClient("Standard test upstream context does not support ctx.stt.transcribe"),
404
+ resolver: {
405
+ solve: async () => unsupported("ctx.resolver.solve"),
406
+ },
407
+ choice: createTestProviderChoiceContext({
408
+ providerId: `standard-test-${operationName}`,
409
+ request,
410
+ credential,
411
+ state,
412
+ }),
413
+ };
414
+ }
415
+ function createNativeConnection(initialResponse, dispatch, url) {
416
+ let unread = initialResponse.bytes.slice(0);
417
+ return {
418
+ read: async () => {
419
+ if (unread.byteLength === 0)
420
+ return null;
421
+ const bytes = unread;
422
+ unread = new Uint8Array();
423
+ return bytes;
424
+ },
425
+ write: async (body) => {
426
+ const response = await dispatch({ transport: "native", method: "write", url, body });
427
+ unread = response.bytes.slice(0);
428
+ },
429
+ close: async () => { },
430
+ };
431
+ }
84
432
  function unsupported(name) {
85
433
  throw new Error(`Standard test snapshot context does not support ${name}`);
86
434
  }
87
- function createSnapshotContext(rawFixture) {
435
+ export function createSnapshotContext(rawFixture) {
88
436
  const credential = {
89
437
  mode: "none",
90
438
  get: () => undefined,
@@ -94,17 +442,52 @@ function createSnapshotContext(rawFixture) {
94
442
  };
95
443
  const request = { headers: {} };
96
444
  const state = createMemoryProviderRuntimeState();
97
- return {
445
+ const streamCaptureGroup = findStreamCaptureGroup(rawFixture);
446
+ let nextCaptureItem = 0;
447
+ const replayJsonResponse = () => {
448
+ if (!streamCaptureGroup)
449
+ return jsonResponse(rawFixture);
450
+ const item = streamCaptureGroup.items[nextCaptureItem];
451
+ if (!item) {
452
+ throw new Error(`Stream fixture exhausted: no recorded response exists for ordinary HTTP call ${nextCaptureItem + 1}.`);
453
+ }
454
+ if (item.kind !== "response") {
455
+ throw new Error(`Stream fixture call-order mismatch: expected a stream call at position ${nextCaptureItem + 1}, received an ordinary HTTP call.`);
456
+ }
457
+ nextCaptureItem += 1;
458
+ return jsonResponse(item.value);
459
+ };
460
+ const context = {
98
461
  env: { get: () => undefined },
99
462
  credential,
100
463
  request,
101
464
  http: {
102
- request: async () => jsonResponse(rawFixture),
103
- get: async () => jsonResponse(rawFixture),
104
- post: async () => jsonResponse(rawFixture),
105
- put: async () => jsonResponse(rawFixture),
106
- delete: async () => jsonResponse(rawFixture),
107
- stream: async () => unsupported("ctx.http.stream"),
465
+ request: async () => replayJsonResponse(),
466
+ get: async () => replayJsonResponse(),
467
+ post: async () => replayJsonResponse(),
468
+ put: async () => replayJsonResponse(),
469
+ delete: async () => replayJsonResponse(),
470
+ stream: async (...args) => {
471
+ if (!streamCaptureGroup)
472
+ return unsupported("ctx.http.stream");
473
+ const item = streamCaptureGroup.items[nextCaptureItem];
474
+ if (!item) {
475
+ throw new Error(`Stream fixture exhausted: no recorded evidence exists for stream call ${nextCaptureItem + 1}.`);
476
+ }
477
+ if (item.kind !== "stream") {
478
+ throw new Error(`Stream fixture call-order mismatch: expected an ordinary HTTP call at position ${nextCaptureItem + 1}, received a stream call.`);
479
+ }
480
+ if (item.evidence.request) {
481
+ const expected = item.evidence.request;
482
+ const actualMethod = (args[1]?.method ?? "GET").toUpperCase();
483
+ const actualPath = replayRequestPath(args[0], expected.path);
484
+ if (actualMethod !== expected.method || actualPath !== expected.path) {
485
+ throw new Error(`Stream fixture request mismatch: expected ${expected.method} ${expected.path}, received ${actualMethod} ${actualPath}.`);
486
+ }
487
+ }
488
+ nextCaptureItem += 1;
489
+ return replayStreamEvidence(item.evidence);
490
+ },
108
491
  sse: async () => unsupported("ctx.http.sse"),
109
492
  },
110
493
  cache: createProviderCache({ providerId: "standard-test" }),
@@ -126,7 +509,11 @@ function createSnapshotContext(rawFixture) {
126
509
  auth: {
127
510
  requestField: async (name) => unsupported(`ctx.auth.requestField(${name})`),
128
511
  },
512
+ ocr: createUnsupportedOcrClient("Standard test snapshot context does not support ctx.ocr.recognize"),
129
513
  stt: createUnsupportedSttClient("Standard test snapshot context does not support ctx.stt.transcribe"),
514
+ resolver: {
515
+ solve: async () => unsupported("ctx.resolver.solve"),
516
+ },
130
517
  choice: createTestProviderChoiceContext({
131
518
  providerId: "standard-test",
132
519
  request,
@@ -134,15 +521,39 @@ function createSnapshotContext(rawFixture) {
134
521
  state,
135
522
  }),
136
523
  };
524
+ snapshotCaptureStates.set(context, {
525
+ assertConsumed() {
526
+ if (streamCaptureGroup && nextCaptureItem !== streamCaptureGroup.items.length) {
527
+ throw new Error(`Stream fixture has ${streamCaptureGroup.items.length - nextCaptureItem} unconsumed capture item${streamCaptureGroup.items.length - nextCaptureItem === 1 ? "" : "s"} after handler completion.`);
528
+ }
529
+ },
530
+ consumed: () => nextCaptureItem,
531
+ });
532
+ return context;
533
+ }
534
+ function replayRequestPath(requestUrl, expectedPath) {
535
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(requestUrl) || requestUrl.startsWith("/")) {
536
+ return requestPathForFixture(requestUrl);
537
+ }
538
+ return requestPathForFixture(new URL(requestUrl, `https://fixture.invalid${expectedPath}`).toString());
137
539
  }
138
540
  async function transformSnapshotOutput(provider, rawFixture) {
139
541
  const entries = Object.entries(provider.operations);
140
- const context = createSnapshotContext(rawFixture);
542
+ const captureStates = [];
141
543
  const outputs = await Promise.all(entries.map(async ([operationName, operation]) => {
544
+ const context = createSnapshotContext(rawFixture);
142
545
  const request = operation.fixtures?.request ?? {};
143
546
  const output = await operation.handler(context, request);
547
+ const captureState = snapshotCaptureStates.get(context);
548
+ if (captureState)
549
+ captureStates.push(captureState);
550
+ if (captureState?.consumed())
551
+ captureState.assertConsumed();
144
552
  return [operationName, output];
145
553
  }));
554
+ if (findStreamCaptureGroup(rawFixture) && !captureStates.some((state) => state.consumed() > 0)) {
555
+ captureStates[0]?.assertConsumed();
556
+ }
146
557
  if (outputs.length === 1) {
147
558
  return outputs[0]?.[1];
148
559
  }
@@ -161,7 +572,7 @@ function formatJsonDiff(current, expected) {
161
572
  const expectedLine = expectedLines[index];
162
573
  if (currentLine === expectedLine) {
163
574
  if (currentLine !== undefined) {
164
- lines.push(` ${currentLine}`);
575
+ lines.push(` ${currentLine}`);
165
576
  }
166
577
  continue;
167
578
  }
@@ -187,16 +598,51 @@ function expectSchemaFixture(operationName, fieldName, fixture, result) {
187
598
  function parseSchemaFixture(operationName, fieldName, schema, fixture) {
188
599
  expectSchemaFixture(operationName, fieldName, fixture, safeParseSchemaSync(schema, fixture, `operations.${operationName}.fixtures.${fieldName}`));
189
600
  }
190
- /**
191
- * Run standard SDK tests for a provider in one line.
192
- *
193
- * Usage:
194
- * import { myProvider } from "../index";
195
- * import { runStandardTests } from "@apifuse/provider-sdk/testing";
196
- * runStandardTests(myProvider, rawFixture, manifest, { snapshot: true });
197
- */
198
- export function runStandardTests(provider, rawFixture, manifest, options = {}) {
601
+ async function materializeHandlerOutput(output) {
602
+ if (!(output instanceof Response))
603
+ return output;
604
+ const contentType = output.headers.get("content-type") ?? "";
605
+ if (contentType.includes("application/json"))
606
+ return output.json();
607
+ return output.text();
608
+ }
609
+ /** Internal execution seam exported for focused SDK tests; use runStandardTests as public API. */
610
+ export async function executeStandardTestHandler(provider, operationName, upstreamStub) {
611
+ const operation = provider.operations[operationName];
612
+ if (!operation)
613
+ throw new Error(`Unknown operation "${operationName}".`);
614
+ if (operation.fixtures?.request === undefined) {
615
+ throw new Error(`Operation "${operationName}" has no fixtures.request for handler E2E execution.`);
616
+ }
617
+ const context = createUpstreamContext(provider, operationName, upstreamStub);
618
+ const output = await materializeHandlerOutput(await operation.handler(context, operation.fixtures.request));
619
+ const result = safeParseSchemaSync(operation.output, output, `operations.${operationName}.handler.output`);
620
+ if (!result.success) {
621
+ throw new Error([
622
+ `Handler output for operation "${operationName}" failed schema validation.`,
623
+ formatJsonDiff({ valid: false, value: output, error: result.error }, { valid: true, value: output }),
624
+ ].join("\n"));
625
+ }
626
+ return output;
627
+ }
628
+ function isOptionsShortcut(value) {
629
+ return (value !== null &&
630
+ typeof value === "object" &&
631
+ !Array.isArray(value) &&
632
+ Object.hasOwn(value, "upstreamStub"));
633
+ }
634
+ export function runStandardTests(provider, rawFixtureOrOptions, manifest, legacyOptions) {
635
+ const shortcut = manifest === undefined && legacyOptions === undefined && isOptionsShortcut(rawFixtureOrOptions);
636
+ const rawFixture = shortcut ? undefined : rawFixtureOrOptions;
637
+ const options = shortcut ? rawFixtureOrOptions : (legacyOptions ?? {});
199
638
  const operations = Object.entries(provider.operations);
639
+ const warnings = options.upstreamStub
640
+ ? operations
641
+ .filter(([, operation]) => operation.fixtures?.request === undefined)
642
+ .map(([operationName]) => `[provider-sdk] Operation "${provider.id}.${operationName}" has no fixtures.request, so runStandardTests cannot invoke its handler E2E.`)
643
+ : operations.map(([operationName]) => `[provider-sdk] Operation "${provider.id}.${operationName}" has no handler E2E coverage in runStandardTests; configure upstreamStub to invoke the real handler.`);
644
+ for (const warning of warnings)
645
+ console.warn(warning);
200
646
  const assertFixtureValidation = () => {
201
647
  expect(rawFixture).toBeDefined();
202
648
  expect(isJsonCompatible(rawFixture)).toBe(true);
@@ -296,12 +742,26 @@ export function runStandardTests(provider, rawFixture, manifest, options = {}) {
296
742
  const actual = await transformSnapshotOutput(provider, rawFixture);
297
743
  const serialized = stableStringify(actual);
298
744
  const snapshotFile = Bun.file(snapshotPath);
299
- if (shouldUpdateSnapshots() || !(await snapshotFile.exists())) {
745
+ const snapshotExists = await snapshotFile.exists();
746
+ if (!snapshotExists && options.requireSnapshot && !shouldUpdateSnapshots()) {
747
+ throw new Error(`Required golden snapshot is missing: ${snapshotPath}. Regenerate it with bun test --update-snapshots.`);
748
+ }
749
+ if (shouldUpdateSnapshots() || !snapshotExists) {
300
750
  await Bun.write(snapshotPath, serialized);
301
751
  }
302
752
  const expected = JSON.parse(await Bun.file(snapshotPath).text());
303
753
  expect(actual).toEqual(expected);
304
754
  });
305
755
  }
756
+ if (options.upstreamStub) {
757
+ for (const [operationName, operation] of operations) {
758
+ if (operation.fixtures?.request === undefined)
759
+ continue;
760
+ it(`invokes the real ${operationName} handler with canned upstream responses`, async () => {
761
+ await executeStandardTestHandler(provider, operationName, options.upstreamStub);
762
+ });
763
+ }
764
+ }
306
765
  });
766
+ return { warnings };
307
767
  }