@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,8 +1,109 @@
1
1
  import { createRequire } from "node:module";
2
- import { ProviderError } from "../errors";
2
+ import { ProviderError } from "../errors.js";
3
3
  const require = createRequire(import.meta.url);
4
4
  const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
5
5
  const SELECTOR_POLL_INTERVAL_MS = 100;
6
+ const RESOURCE_POLICY_ROUTE_PATTERN = "**/*";
7
+ const DEFAULT_RESOURCE_METHODS = ["GET", "HEAD"];
8
+ function toResourceBody(body) {
9
+ if (body === undefined || typeof body === "string" || Buffer.isBuffer(body)) {
10
+ return body;
11
+ }
12
+ if (body instanceof ArrayBuffer) {
13
+ return Buffer.from(new Uint8Array(body));
14
+ }
15
+ return Buffer.from(body);
16
+ }
17
+ function isResourceMethod(method) {
18
+ return method === "GET" || method === "HEAD";
19
+ }
20
+ async function toResourceRequest(request) {
21
+ const method = request.method().toUpperCase();
22
+ if (!isResourceMethod(method)) {
23
+ return null;
24
+ }
25
+ return {
26
+ headers: await request.allHeaders(),
27
+ method,
28
+ resourceType: request.resourceType(),
29
+ url: request.url(),
30
+ };
31
+ }
32
+ function toCdpResourceRequest(params) {
33
+ if (!isRecord(params)) {
34
+ return null;
35
+ }
36
+ const requestId = params.requestId;
37
+ const rawRequest = params.request;
38
+ if (typeof requestId !== "string" || !isRecord(rawRequest)) {
39
+ return null;
40
+ }
41
+ const url = rawRequest.url;
42
+ const method = String(rawRequest.method ?? "").toUpperCase();
43
+ if (typeof url !== "string" || !isResourceMethod(method)) {
44
+ return null;
45
+ }
46
+ return {
47
+ requestId,
48
+ request: {
49
+ headers: toCdpResourceHeaders(rawRequest.headers),
50
+ method,
51
+ resourceType: typeof params.resourceType === "string" ? params.resourceType : undefined,
52
+ url,
53
+ },
54
+ };
55
+ }
56
+ function getCdpPausedRequestId(params) {
57
+ if (!isRecord(params) || typeof params.requestId !== "string") {
58
+ return null;
59
+ }
60
+ return params.requestId;
61
+ }
62
+ function toCdpResourceHeaders(value) {
63
+ if (!isRecord(value)) {
64
+ return {};
65
+ }
66
+ const headers = {};
67
+ for (const [name, headerValue] of Object.entries(value)) {
68
+ if (typeof headerValue === "string") {
69
+ headers[name] = headerValue;
70
+ }
71
+ }
72
+ return headers;
73
+ }
74
+ function matchesResourceRoute(match, request) {
75
+ if (typeof match === "string") {
76
+ return request.url === match;
77
+ }
78
+ if (match instanceof RegExp) {
79
+ return match.test(request.url);
80
+ }
81
+ return match(request);
82
+ }
83
+ function toCdpFulfillParams(requestId, decision) {
84
+ const body = toResourceBody(decision.body);
85
+ return {
86
+ ...(body === undefined ? {} : { body: Buffer.from(body).toString("base64") }),
87
+ ...(decision.headers === undefined
88
+ ? {}
89
+ : {
90
+ responseHeaders: Object.entries(decision.headers).map(([name, value]) => ({
91
+ name,
92
+ value,
93
+ })),
94
+ }),
95
+ requestId,
96
+ responseCode: decision.status ?? 200,
97
+ };
98
+ }
99
+ async function fulfillResourceRoute(route, decision) {
100
+ const body = toResourceBody(decision.body);
101
+ await route.fulfill({
102
+ ...(body === undefined ? {} : { body }),
103
+ ...(decision.headers === undefined ? {} : { headers: decision.headers }),
104
+ status: decision.status ?? 200,
105
+ });
106
+ }
6
107
  function getDefaultCdpPoolUrl(env = process.env) {
7
108
  return env.APIFUSE__CDP_POOL__URL;
8
109
  }
@@ -256,6 +357,38 @@ class PlaywrightBrowserPage {
256
357
  async close() {
257
358
  await this.page.close();
258
359
  }
360
+ async withResourcePolicy(policy, run) {
361
+ const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
362
+ const handler = async (route) => {
363
+ const request = await toResourceRequest(route.request());
364
+ if (!request || !allowedMethods.has(request.method)) {
365
+ await route.abort("blockedbyclient");
366
+ return;
367
+ }
368
+ for (const resourceRoute of policy.routes) {
369
+ if (!matchesResourceRoute(resourceRoute.match, request)) {
370
+ continue;
371
+ }
372
+ const decision = await resourceRoute.handle(request);
373
+ switch (decision.action) {
374
+ case "fulfill":
375
+ await fulfillResourceRoute(route, decision);
376
+ return;
377
+ case "block":
378
+ await route.abort("blockedbyclient");
379
+ return;
380
+ }
381
+ }
382
+ await route.abort("blockedbyclient");
383
+ };
384
+ await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
385
+ try {
386
+ return await run();
387
+ }
388
+ finally {
389
+ await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
390
+ }
391
+ }
259
392
  }
260
393
  class PlaywrightBrowserClient {
261
394
  options;
@@ -442,16 +575,13 @@ function parsePoolAcquireResponse(value) {
442
575
  code: "BROWSER_RUNTIME_UNSUPPORTED",
443
576
  });
444
577
  }
445
- if (value.browserContextId !== undefined &&
446
- typeof value.browserContextId !== "string") {
578
+ if (value.browserContextId !== undefined && typeof value.browserContextId !== "string") {
447
579
  throw new ProviderError("CDP Pool returned an invalid acquire response", {
448
580
  code: "BROWSER_RUNTIME_UNSUPPORTED",
449
581
  });
450
582
  }
451
583
  return {
452
- ...(value.browserContextId
453
- ? { browserContextId: value.browserContextId }
454
- : {}),
584
+ ...(value.browserContextId ? { browserContextId: value.browserContextId } : {}),
455
585
  pageId: value.pageId,
456
586
  wsEndpoint: value.wsEndpoint,
457
587
  };
@@ -473,9 +603,7 @@ function parseCdpFrameTreeNode(value) {
473
603
  frame: {
474
604
  id: frameId,
475
605
  name: typeof value.frame.name === "string" ? value.frame.name : undefined,
476
- parentId: typeof value.frame.parentId === "string"
477
- ? value.frame.parentId
478
- : undefined,
606
+ parentId: typeof value.frame.parentId === "string" ? value.frame.parentId : undefined,
479
607
  url: typeof value.frame.url === "string" ? value.frame.url : undefined,
480
608
  },
481
609
  ...(childFrames ? { childFrames } : {}),
@@ -488,9 +616,7 @@ function getCdpExecutionContext(params) {
488
616
  const contextId = params.context.id;
489
617
  const auxData = params.context.auxData;
490
618
  return {
491
- frameId: isRecord(auxData) && typeof auxData.frameId === "string"
492
- ? auxData.frameId
493
- : undefined,
619
+ frameId: isRecord(auxData) && typeof auxData.frameId === "string" ? auxData.frameId : undefined,
494
620
  id: typeof contextId === "number" ? contextId : undefined,
495
621
  };
496
622
  }
@@ -641,8 +767,7 @@ class CdpPoolBrowserPage {
641
767
  returnByValue: true,
642
768
  });
643
769
  if (result.exceptionDetails) {
644
- throw new Error(String(result.exceptionDetails.text ??
645
- "Browser evaluation failed"));
770
+ throw new Error(String(result.exceptionDetails.text ?? "Browser evaluation failed"));
646
771
  }
647
772
  return result.result?.value;
648
773
  }
@@ -727,9 +852,7 @@ class CdpPoolBrowserPage {
727
852
  this.closed = true;
728
853
  try {
729
854
  await this.release({
730
- ...(this.browserContextId
731
- ? { browserContextId: this.browserContextId }
732
- : {}),
855
+ ...(this.browserContextId ? { browserContextId: this.browserContextId } : {}),
733
856
  pageId: this.pageId,
734
857
  });
735
858
  }
@@ -737,6 +860,77 @@ class CdpPoolBrowserPage {
737
860
  await this.pageClient.close();
738
861
  }
739
862
  }
863
+ async withResourcePolicy(policy, run) {
864
+ const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
865
+ const handlePausedRequest = (params) => {
866
+ void this.handleResourcePolicyPausedRequest(params, policy, allowedMethods);
867
+ };
868
+ const unsubscribe = this.pageClient.on("Fetch.requestPaused", handlePausedRequest);
869
+ try {
870
+ await this.pageClient.send("Fetch.enable", {
871
+ patterns: [{ requestStage: "Request", urlPattern: "*" }],
872
+ });
873
+ }
874
+ catch (error) {
875
+ unsubscribe();
876
+ throw new ProviderError("CDP browser target does not support BrowserPage.withResourcePolicy()", {
877
+ cause: error instanceof Error ? error : undefined,
878
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
879
+ fix: "Use a Chromium CDP target with the Fetch domain enabled, or use the local Playwright browser runtime.",
880
+ });
881
+ }
882
+ try {
883
+ return await run();
884
+ }
885
+ finally {
886
+ unsubscribe();
887
+ await this.pageClient.send("Fetch.disable");
888
+ }
889
+ }
890
+ async handleResourcePolicyPausedRequest(params, policy, allowedMethods) {
891
+ const requestId = getCdpPausedRequestId(params);
892
+ if (requestId === null) {
893
+ return;
894
+ }
895
+ try {
896
+ const parsed = toCdpResourceRequest(params);
897
+ if (!parsed || !allowedMethods.has(parsed.request.method)) {
898
+ await this.failCdpResourceRequest(requestId);
899
+ return;
900
+ }
901
+ for (const resourceRoute of policy.routes) {
902
+ if (!matchesResourceRoute(resourceRoute.match, parsed.request)) {
903
+ continue;
904
+ }
905
+ const decision = await resourceRoute.handle(parsed.request);
906
+ switch (decision.action) {
907
+ case "fulfill":
908
+ await this.pageClient.send("Fetch.fulfillRequest", toCdpFulfillParams(parsed.requestId, decision));
909
+ return;
910
+ case "block":
911
+ await this.failCdpResourceRequest(parsed.requestId);
912
+ return;
913
+ }
914
+ }
915
+ await this.failCdpResourceRequest(parsed.requestId);
916
+ }
917
+ catch {
918
+ await this.failCdpResourceRequest(requestId);
919
+ }
920
+ }
921
+ async failCdpResourceRequest(requestId) {
922
+ try {
923
+ await this.pageClient.send("Fetch.failRequest", {
924
+ errorReason: "BlockedByClient",
925
+ requestId,
926
+ });
927
+ }
928
+ catch (error) {
929
+ if (error instanceof Error) {
930
+ return;
931
+ }
932
+ }
933
+ }
740
934
  async initialize() {
741
935
  if (this.initialized) {
742
936
  return;
@@ -799,12 +993,8 @@ class CdpPoolBrowserClient {
799
993
  }
800
994
  async acquirePage(options) {
801
995
  const acquireResult = parsePoolAcquireResponse(await this.poolClient.send("acquire", {
802
- ...(this.allowedHosts.length > 0
803
- ? { allowedHosts: this.allowedHosts }
804
- : {}),
805
- ...(options?.isolatedContext
806
- ? { isolationMode: "browserContext" }
807
- : {}),
996
+ ...(this.allowedHosts.length > 0 ? { allowedHosts: this.allowedHosts } : {}),
997
+ ...(options?.isolatedContext ? { isolationMode: "browserContext" } : {}),
808
998
  }));
809
999
  const pageClient = new JsonRpcWebSocketClient(acquireResult.wsEndpoint);
810
1000
  const page = new CdpPoolBrowserPage(acquireResult.pageId, acquireResult.browserContextId, pageClient, async (request) => {
@@ -1020,8 +1210,7 @@ async function solveRecaptchaChallenge(page, request) {
1020
1210
  async function findRecaptchaFrame(frames, siteKey) {
1021
1211
  for (const frame of frames) {
1022
1212
  const url = await frame.url();
1023
- const matchesRecaptcha = url.includes("google.com/recaptcha") ||
1024
- url.includes("recaptcha.net/recaptcha");
1213
+ const matchesRecaptcha = url.includes("google.com/recaptcha") || url.includes("recaptcha.net/recaptcha");
1025
1214
  const matchesSiteKey = !siteKey || url.includes(siteKey);
1026
1215
  if (matchesRecaptcha && matchesSiteKey) {
1027
1216
  return frame;
@@ -1,4 +1,4 @@
1
- import type { ProviderCache } from "../types";
1
+ import type { ProviderCache } from "../types.js";
2
2
  export type ProviderCacheOptions = {
3
3
  providerId: string;
4
4
  redisUrl?: string;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { providerCacheRedisUrlFromEnv } from "../config/loader";
3
- import { createProviderRedisClient, ensureRedisReady, withRedisTimeout, } from "./redis";
2
+ import { providerCacheRedisUrlFromEnv } from "../config/loader.js";
3
+ import { createProviderRedisClient, ensureRedisReady, withRedisTimeout, } from "./redis.js";
4
4
  const DEFAULT_PREFIX = "apifuse:provider-cache:v1";
5
5
  const DEFAULT_MEMORY_MAX_ENTRIES = 1_000;
6
6
  const DEFAULT_REDIS_TIMEOUT_MS = 150;
@@ -71,10 +71,7 @@ function normalizeKeyPart(value, extra) {
71
71
  return value;
72
72
  }
73
73
  function stableHash(value) {
74
- return createHash("sha256")
75
- .update(JSON.stringify(value))
76
- .digest("hex")
77
- .slice(0, 32);
74
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 32);
78
75
  }
79
76
  function jitteredTtlMs(ttlMs, jitterPct) {
80
77
  if (!jitterPct || jitterPct <= 0)
@@ -282,8 +279,7 @@ export function createProviderCache(options) {
282
279
  async delete(key) {
283
280
  backend.memory.delete(key);
284
281
  const redis = backend.redis;
285
- if (!redis ||
286
- !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
282
+ if (!redis || !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
287
283
  return;
288
284
  }
289
285
  await withRedisFallback(() => redis.del(key));
@@ -1,4 +1,4 @@
1
- import type { CredentialContext, EnvContext, ProviderChoiceContext, ProviderRequestContext, ProviderRuntimeState } from "../types";
1
+ import type { CredentialContext, EnvContext, ProviderChoiceContext, ProviderRequestContext, ProviderRuntimeState } from "../types.js";
2
2
  export declare const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
3
3
  export type CreateProviderChoiceContextOptions = {
4
4
  readonly providerId: string;
@@ -1,6 +1,6 @@
1
1
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
2
- import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "../choice-token";
3
- import { ProviderError } from "../errors";
2
+ import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "../choice-token.js";
3
+ import { isProviderError, ProviderError } from "../errors.js";
4
4
  export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
5
5
  const PRIMARY_CHOICE_TOKEN_KID = "v1";
6
6
  const MANAGED_CHOICE_TOKEN_VERSION = 1;
@@ -53,7 +53,7 @@ export function createProviderChoiceContext(options) {
53
53
  });
54
54
  }
55
55
  function parse(parseOptions) {
56
- const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature,] = parseManagedChoiceTokenParts(parseOptions.token);
56
+ const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] = parseManagedChoiceTokenParts(parseOptions.token);
57
57
  if (actualPrefix !== parseOptions.prefix ||
58
58
  tokenKid !== kid ||
59
59
  !encodedIv ||
@@ -68,13 +68,7 @@ export function createProviderChoiceContext(options) {
68
68
  purpose: parseOptions.purpose,
69
69
  kid: tokenKid,
70
70
  });
71
- const signedBody = [
72
- parseOptions.prefix,
73
- tokenKid,
74
- encodedIv,
75
- encryptedPayload,
76
- authTag,
77
- ].join(".");
71
+ const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(".");
78
72
  assertManagedChoiceSignature({
79
73
  signedBody,
80
74
  signature,
@@ -117,13 +111,11 @@ export function createProviderChoiceContext(options) {
117
111
  export function createTestProviderChoiceContext(options) {
118
112
  return createProviderChoiceContext({
119
113
  ...options,
120
- masterSecret: options.masterSecret ??
121
- "apifuse-test-provider-runtime-choice-token-master-secret",
114
+ masterSecret: options.masterSecret ?? "apifuse-test-provider-runtime-choice-token-master-secret",
122
115
  });
123
116
  }
124
117
  function resolveChoiceMasterSecret(options) {
125
- const configured = options.masterSecret ??
126
- options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
118
+ const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
127
119
  const trimmed = configured?.trim();
128
120
  if (trimmed)
129
121
  return trimmed;
@@ -165,13 +157,7 @@ function encryptManagedChoiceToken(options) {
165
157
  ]).toString("base64url");
166
158
  const authTag = cipher.getAuthTag().toString("base64url");
167
159
  const encodedIv = iv.toString("base64url");
168
- const signedBody = [
169
- options.prefix,
170
- options.kid,
171
- encodedIv,
172
- encryptedPayload,
173
- authTag,
174
- ].join(".");
160
+ const signedBody = [options.prefix, options.kid, encodedIv, encryptedPayload, authTag].join(".");
175
161
  const signature = createHmac("sha256", options.keys.signing)
176
162
  .update(signedBody)
177
163
  .digest("base64url");
@@ -223,7 +209,25 @@ async function parseServerStoredChoice(options) {
223
209
  storage,
224
210
  contextState: options.contextState,
225
211
  });
226
- const record = await namespace.get(optionsStateKey(options.handle.state_id));
212
+ // Reading a server-stored choice back deserializes a persisted value. A
213
+ // corrupt/undecodable value would otherwise surface as a raw JSON.parse
214
+ // SyntaxError (or another unexpected throwable) that escapes the choice error
215
+ // taxonomy, gets masked as internal_error 500, and is treated as retryable by
216
+ // the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
217
+ // Convert any non-branded throwable into a branded invalid_payload so it maps
218
+ // to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
219
+ // ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
220
+ // their category/retryable semantics are preserved.
221
+ let record;
222
+ try {
223
+ record = await namespace.get(optionsStateKey(options.handle.state_id));
224
+ }
225
+ catch (error) {
226
+ if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
227
+ throw error;
228
+ }
229
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload could not be decoded.");
230
+ }
227
231
  if (!record) {
228
232
  throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload is missing.");
229
233
  }
@@ -345,9 +349,7 @@ function isManagedChoiceEnvelope(value) {
345
349
  typeof value.issued_at_ms === "number" &&
346
350
  "ttl_ms" in value &&
347
351
  typeof value.ttl_ms === "number" &&
348
- (!("binding" in value) ||
349
- value.binding === undefined ||
350
- isChoiceBinding(value.binding)));
352
+ (!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)));
351
353
  }
352
354
  function isChoicePayload(value) {
353
355
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -355,29 +357,23 @@ function isChoicePayload(value) {
355
357
  function isChoiceBinding(value) {
356
358
  if (!value || typeof value !== "object" || Array.isArray(value))
357
359
  return false;
358
- return ((!("connection_hash" in value) ||
359
- typeof value.connection_hash === "string") &&
360
+ return ((!("connection_hash" in value) || typeof value.connection_hash === "string") &&
360
361
  (!("credential_hash" in value) || typeof value.credential_hash === "string"));
361
362
  }
362
363
  function assertManagedChoiceEnvelope(envelope, options) {
363
- if (envelope.provider_id !== options.providerId ||
364
- envelope.purpose !== options.purpose) {
364
+ if (envelope.provider_id !== options.providerId || envelope.purpose !== options.purpose) {
365
365
  throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token payload is invalid.");
366
366
  }
367
367
  assertFreshProviderChoiceIssuedAt(envelope.issued_at_ms, {
368
368
  // Clamp to the issuer's embedded TTL so a caller-supplied value cannot
369
369
  // silently extend token validity past the deadline the issuer intended.
370
- ttlMs: options.ttlMs != null
371
- ? Math.min(options.ttlMs, envelope.ttl_ms)
372
- : envelope.ttl_ms,
370
+ ttlMs: options.ttlMs != null ? Math.min(options.ttlMs, envelope.ttl_ms) : envelope.ttl_ms,
373
371
  nowMs: options.nowMs,
374
372
  futureToleranceMs: options.futureToleranceMs,
375
373
  });
376
374
  }
377
375
  function createChoiceBinding(options) {
378
- const connectionHash = options.options?.connection
379
- ? hashRequiredConnection(options)
380
- : undefined;
376
+ const connectionHash = options.options?.connection ? hashRequiredConnection(options) : undefined;
381
377
  const credentialHash = options.options?.credentialKeys?.length
382
378
  ? hashCredentialKeys(options)
383
379
  : undefined;
@@ -1,4 +1,4 @@
1
- import type { AuthMode, CredentialContext } from "../types";
1
+ import type { AuthMode, CredentialContext } from "../types.js";
2
2
  export interface CreateCredentialContextOptions {
3
3
  allowedKeys?: string[];
4
4
  mode?: AuthMode;
@@ -1,4 +1,4 @@
1
- import { CredentialModeError } from "../errors";
1
+ import { CredentialModeError } from "../errors.js";
2
2
  function getAllowedKeys(allowedKeys, values) {
3
3
  if (allowedKeys) {
4
4
  return allowedKeys;
@@ -1,2 +1,2 @@
1
- import type { EnvContext } from "../types";
1
+ import type { EnvContext } from "../types.js";
2
2
  export declare function createEnvContext(allowedKeys?: string[]): EnvContext;
@@ -1,4 +1,4 @@
1
- import type { ProviderContext, ProviderDefinition } from "../types";
1
+ import type { ProviderContext, ProviderDefinition } from "../types.js";
2
2
  export declare function isStreamingOperation(provider: ProviderDefinition, operationId: string): boolean;
3
3
  /**
4
4
  * Execute a provider operation by calling its handler.
@@ -1,5 +1,6 @@
1
- import { ProviderError, SessionExpiredError } from "../errors";
2
- import { parseSchema } from "../schema";
1
+ import { isSessionExpiredError, ProviderError, SessionExpiredError } from "../errors.js";
2
+ import { parseSchema } from "../schema.js";
3
+ import { assertRequiredSecretsPresent } from "./secrets.js";
3
4
  export function isStreamingOperation(provider, operationId) {
4
5
  const kind = provider.operations[operationId]?.transport?.kind ?? "json";
5
6
  return kind !== "json";
@@ -23,6 +24,12 @@ export async function executeOperation(provider, operationId, ctx, input, _optio
23
24
  fix: `Valid operations: ${Object.keys(provider.operations).join(", ")}`,
24
25
  });
25
26
  }
27
+ // SDK-owned secret presence gate (single source of truth): declared
28
+ // `required: true` secrets are validated here, before input parsing and the
29
+ // handler, so every invocation path (serve /v1, self-test probes, perf,
30
+ // record) fails with the same structured MISSING_SECRET error instead of a
31
+ // handler-specific crash. Providers must not re-check presence locally.
32
+ assertRequiredSecretsPresent(provider, ctx.env);
26
33
  const validatedInput = await parseSchema(operation.input, input, `operations.${operationId}.input`);
27
34
  const execute = () => ctx.trace.span(`handler:${operationId}`, () => Promise.resolve(operation.handler(ctx, validatedInput)));
28
35
  let result;
@@ -39,7 +46,12 @@ export async function executeOperation(provider, operationId, ctx, input, _optio
39
46
  // operation is safe to re-drive after refresh, which we signal by marking
40
47
  // the surfaced error retryable; non-idempotent operations (the default)
41
48
  // stay non-retryable so they are not auto-re-driven. See design.md §4.3 D3.
42
- if (error instanceof SessionExpiredError && operation.retryOnAuthRefresh) {
49
+ // Use the branded guard, not `instanceof`: a handler loaded through a
50
+ // duplicate/published SDK module can throw a correctly branded
51
+ // SessionExpiredError whose constructor identity differs from this
52
+ // executor's, which `instanceof` would miss — dropping the retryable
53
+ // upgrade and stranding an operation that opted into auth refresh.
54
+ if (isSessionExpiredError(error) && operation.retryOnAuthRefresh) {
43
55
  throw new SessionExpiredError(error.message, { retryable: true });
44
56
  }
45
57
  throw error;
@@ -1,5 +1,5 @@
1
- import type { ProxyResolutionOptions } from "../config/loader";
2
- import type { HttpClient, HttpRetrySummary } from "../types";
1
+ import type { ProxyResolutionOptions } from "../config/loader.js";
2
+ import type { HttpClient, HttpRetrySummary } from "../types.js";
3
3
  export type HttpClientOptions = ProxyResolutionOptions & {
4
4
  warn?: (message: string) => void;
5
5
  userAgent?: string;