open-multi-agent-kit 0.90.7 → 0.90.9

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 (331) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +17 -10
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/commands/doctor-provider-cli.d.ts +33 -0
  7. package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
  8. package/dist/commands/doctor-provider-cli.js +215 -0
  9. package/dist/commands/doctor-provider-cli.js.map +1 -0
  10. package/dist/commands/doctor-provider-transport.d.ts +35 -0
  11. package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
  12. package/dist/commands/doctor-provider-transport.js +185 -0
  13. package/dist/commands/doctor-provider-transport.js.map +1 -0
  14. package/dist/commands/doctor-provider.d.ts +121 -7
  15. package/dist/commands/doctor-provider.d.ts.map +1 -1
  16. package/dist/commands/doctor-provider.js +1295 -102
  17. package/dist/commands/doctor-provider.js.map +1 -1
  18. package/dist/commands/session-doctor-cli.d.ts +43 -0
  19. package/dist/commands/session-doctor-cli.d.ts.map +1 -0
  20. package/dist/commands/session-doctor-cli.js +494 -0
  21. package/dist/commands/session-doctor-cli.js.map +1 -0
  22. package/dist/commands/verify-ci.d.ts +21 -0
  23. package/dist/commands/verify-ci.d.ts.map +1 -0
  24. package/dist/commands/verify-ci.js +66 -0
  25. package/dist/commands/verify-ci.js.map +1 -0
  26. package/dist/core/agent-session-services.d.ts +3 -0
  27. package/dist/core/agent-session-services.d.ts.map +1 -1
  28. package/dist/core/agent-session-services.js +3 -0
  29. package/dist/core/agent-session-services.js.map +1 -1
  30. package/dist/core/agent-session.d.ts +116 -2
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +1015 -71
  33. package/dist/core/agent-session.js.map +1 -1
  34. package/dist/core/agent-tool-settings.d.ts +35 -0
  35. package/dist/core/agent-tool-settings.d.ts.map +1 -0
  36. package/dist/core/agent-tool-settings.js +155 -0
  37. package/dist/core/agent-tool-settings.js.map +1 -0
  38. package/dist/core/atomic-session-file.d.ts +14 -0
  39. package/dist/core/atomic-session-file.d.ts.map +1 -0
  40. package/dist/core/atomic-session-file.js +104 -0
  41. package/dist/core/atomic-session-file.js.map +1 -0
  42. package/dist/core/compaction/compaction.d.ts +6 -0
  43. package/dist/core/compaction/compaction.d.ts.map +1 -1
  44. package/dist/core/compaction/compaction.js +12 -1
  45. package/dist/core/compaction/compaction.js.map +1 -1
  46. package/dist/core/compaction/hysteresis.d.ts +27 -0
  47. package/dist/core/compaction/hysteresis.d.ts.map +1 -0
  48. package/dist/core/compaction/hysteresis.js +84 -0
  49. package/dist/core/compaction/hysteresis.js.map +1 -0
  50. package/dist/core/compaction/index.d.ts +2 -0
  51. package/dist/core/compaction/index.d.ts.map +1 -1
  52. package/dist/core/compaction/index.js +2 -0
  53. package/dist/core/compaction/index.js.map +1 -1
  54. package/dist/core/compaction/transaction.d.ts +172 -0
  55. package/dist/core/compaction/transaction.d.ts.map +1 -0
  56. package/dist/core/compaction/transaction.js +824 -0
  57. package/dist/core/compaction/transaction.js.map +1 -0
  58. package/dist/core/context-budget-governor-v2.d.ts +2 -1
  59. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  60. package/dist/core/context-budget-governor-v2.js +2 -1
  61. package/dist/core/context-budget-governor-v2.js.map +1 -1
  62. package/dist/core/context-budget-headroom.d.ts.map +1 -1
  63. package/dist/core/context-budget-headroom.js +5 -1
  64. package/dist/core/context-budget-headroom.js.map +1 -1
  65. package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
  66. package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
  67. package/dist/core/context-budget-reserved-tokens.js +188 -0
  68. package/dist/core/context-budget-reserved-tokens.js.map +1 -0
  69. package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
  70. package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
  71. package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
  72. package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
  73. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  74. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  75. package/dist/core/context-budget-v2-cache-keys.js +6 -0
  76. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  77. package/dist/core/context-budget-v2-cache-provider.d.ts +8 -2
  78. package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
  79. package/dist/core/context-budget-v2-cache-provider.js +28 -5
  80. package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
  81. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  82. package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
  83. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  84. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  85. package/dist/core/context-budget-v2-planner.js +1 -0
  86. package/dist/core/context-budget-v2-planner.js.map +1 -1
  87. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  88. package/dist/core/context-budget-v2-selection.js +9 -0
  89. package/dist/core/context-budget-v2-selection.js.map +1 -1
  90. package/dist/core/context-budget-v2-types.d.ts +4 -0
  91. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  92. package/dist/core/context-budget-v2-types.js.map +1 -1
  93. package/dist/core/durable-file-directory.d.ts +4 -0
  94. package/dist/core/durable-file-directory.d.ts.map +1 -0
  95. package/dist/core/durable-file-directory.js +76 -0
  96. package/dist/core/durable-file-directory.js.map +1 -0
  97. package/dist/core/durable-file-identity.d.ts +26 -0
  98. package/dist/core/durable-file-identity.d.ts.map +1 -0
  99. package/dist/core/durable-file-identity.js +118 -0
  100. package/dist/core/durable-file-identity.js.map +1 -0
  101. package/dist/core/durable-file-io.d.ts +29 -0
  102. package/dist/core/durable-file-io.d.ts.map +1 -0
  103. package/dist/core/durable-file-io.js +179 -0
  104. package/dist/core/durable-file-io.js.map +1 -0
  105. package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
  106. package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
  107. package/dist/core/durable-file-lock-cleanup.js +39 -0
  108. package/dist/core/durable-file-lock-cleanup.js.map +1 -0
  109. package/dist/core/durable-file-lock-owner.d.ts +18 -0
  110. package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
  111. package/dist/core/durable-file-lock-owner.js +78 -0
  112. package/dist/core/durable-file-lock-owner.js.map +1 -0
  113. package/dist/core/durable-file-lock-storage.d.ts +35 -0
  114. package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
  115. package/dist/core/durable-file-lock-storage.js +263 -0
  116. package/dist/core/durable-file-lock-storage.js.map +1 -0
  117. package/dist/core/durable-file-mode.d.ts +8 -0
  118. package/dist/core/durable-file-mode.d.ts.map +1 -0
  119. package/dist/core/durable-file-mode.js +41 -0
  120. package/dist/core/durable-file-mode.js.map +1 -0
  121. package/dist/core/extensions/loader.d.ts +12 -0
  122. package/dist/core/extensions/loader.d.ts.map +1 -1
  123. package/dist/core/extensions/loader.js +50 -37
  124. package/dist/core/extensions/loader.js.map +1 -1
  125. package/dist/core/extensions/types.d.ts +2 -0
  126. package/dist/core/extensions/types.d.ts.map +1 -1
  127. package/dist/core/extensions/types.js.map +1 -1
  128. package/dist/core/index.d.ts +6 -2
  129. package/dist/core/index.d.ts.map +1 -1
  130. package/dist/core/index.js +5 -0
  131. package/dist/core/index.js.map +1 -1
  132. package/dist/core/loop-gates.d.ts.map +1 -1
  133. package/dist/core/loop-gates.js +25 -1
  134. package/dist/core/loop-gates.js.map +1 -1
  135. package/dist/core/model-resolver.d.ts.map +1 -1
  136. package/dist/core/model-resolver.js +1 -1
  137. package/dist/core/model-resolver.js.map +1 -1
  138. package/dist/core/pi-package-intake.d.ts +1 -0
  139. package/dist/core/pi-package-intake.d.ts.map +1 -1
  140. package/dist/core/pi-package-intake.js +7 -0
  141. package/dist/core/pi-package-intake.js.map +1 -1
  142. package/dist/core/redaction.d.ts +8 -0
  143. package/dist/core/redaction.d.ts.map +1 -0
  144. package/dist/core/redaction.js +31 -0
  145. package/dist/core/redaction.js.map +1 -0
  146. package/dist/core/resource-loader.d.ts.map +1 -1
  147. package/dist/core/resource-loader.js +36 -0
  148. package/dist/core/resource-loader.js.map +1 -1
  149. package/dist/core/run-journal-store.d.ts +53 -0
  150. package/dist/core/run-journal-store.d.ts.map +1 -0
  151. package/dist/core/run-journal-store.js +278 -0
  152. package/dist/core/run-journal-store.js.map +1 -0
  153. package/dist/core/run-journal.d.ts +172 -0
  154. package/dist/core/run-journal.d.ts.map +1 -0
  155. package/dist/core/run-journal.js +1053 -0
  156. package/dist/core/run-journal.js.map +1 -0
  157. package/dist/core/sdk.d.ts +7 -0
  158. package/dist/core/sdk.d.ts.map +1 -1
  159. package/dist/core/sdk.js +94 -3
  160. package/dist/core/sdk.js.map +1 -1
  161. package/dist/core/session-doctor-plan.d.ts +128 -0
  162. package/dist/core/session-doctor-plan.d.ts.map +1 -0
  163. package/dist/core/session-doctor-plan.js +833 -0
  164. package/dist/core/session-doctor-plan.js.map +1 -0
  165. package/dist/core/session-file-compaction.d.ts +6 -0
  166. package/dist/core/session-file-compaction.d.ts.map +1 -0
  167. package/dist/core/session-file-compaction.js +169 -0
  168. package/dist/core/session-file-compaction.js.map +1 -0
  169. package/dist/core/session-file-head.d.ts +10 -0
  170. package/dist/core/session-file-head.d.ts.map +1 -0
  171. package/dist/core/session-file-head.js +78 -0
  172. package/dist/core/session-file-head.js.map +1 -0
  173. package/dist/core/session-file-persistence.d.ts +30 -0
  174. package/dist/core/session-file-persistence.d.ts.map +1 -0
  175. package/dist/core/session-file-persistence.js +106 -0
  176. package/dist/core/session-file-persistence.js.map +1 -0
  177. package/dist/core/session-integrity.d.ts +43 -0
  178. package/dist/core/session-integrity.d.ts.map +1 -0
  179. package/dist/core/session-integrity.js +420 -0
  180. package/dist/core/session-integrity.js.map +1 -0
  181. package/dist/core/session-manager.d.ts +24 -4
  182. package/dist/core/session-manager.d.ts.map +1 -1
  183. package/dist/core/session-manager.js +242 -125
  184. package/dist/core/session-manager.js.map +1 -1
  185. package/dist/core/session-owner-lease.d.ts +22 -0
  186. package/dist/core/session-owner-lease.d.ts.map +1 -0
  187. package/dist/core/session-owner-lease.js +176 -0
  188. package/dist/core/session-owner-lease.js.map +1 -0
  189. package/dist/core/session-path-policy.d.ts +132 -0
  190. package/dist/core/session-path-policy.d.ts.map +1 -0
  191. package/dist/core/session-path-policy.js +780 -0
  192. package/dist/core/session-path-policy.js.map +1 -0
  193. package/dist/core/session-repair-plan.d.ts +49 -0
  194. package/dist/core/session-repair-plan.d.ts.map +1 -0
  195. package/dist/core/session-repair-plan.js +202 -0
  196. package/dist/core/session-repair-plan.js.map +1 -0
  197. package/dist/core/session-termination.d.ts +100 -0
  198. package/dist/core/session-termination.d.ts.map +1 -0
  199. package/dist/core/session-termination.js +403 -0
  200. package/dist/core/session-termination.js.map +1 -0
  201. package/dist/core/settings-manager.d.ts +32 -0
  202. package/dist/core/settings-manager.d.ts.map +1 -1
  203. package/dist/core/settings-manager.js +52 -5
  204. package/dist/core/settings-manager.js.map +1 -1
  205. package/dist/core/system-prompt.d.ts.map +1 -1
  206. package/dist/core/system-prompt.js +1 -1
  207. package/dist/core/system-prompt.js.map +1 -1
  208. package/dist/core/verified-bash-adapter.d.ts +36 -0
  209. package/dist/core/verified-bash-adapter.d.ts.map +1 -0
  210. package/dist/core/verified-bash-adapter.js +90 -0
  211. package/dist/core/verified-bash-adapter.js.map +1 -0
  212. package/dist/guardrails/command-redaction.d.ts +32 -0
  213. package/dist/guardrails/command-redaction.d.ts.map +1 -0
  214. package/dist/guardrails/command-redaction.js +776 -0
  215. package/dist/guardrails/command-redaction.js.map +1 -0
  216. package/dist/guardrails/evidence-attestation.d.ts +31 -0
  217. package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
  218. package/dist/guardrails/evidence-attestation.js +178 -0
  219. package/dist/guardrails/evidence-attestation.js.map +1 -0
  220. package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
  221. package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
  222. package/dist/guardrails/evidence-receipt-store.js +446 -0
  223. package/dist/guardrails/evidence-receipt-store.js.map +1 -0
  224. package/dist/guardrails/evidence-receipt.d.ts +58 -0
  225. package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
  226. package/dist/guardrails/evidence-receipt.js +389 -0
  227. package/dist/guardrails/evidence-receipt.js.map +1 -0
  228. package/dist/guardrails/evidence-system.d.ts +40 -9
  229. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  230. package/dist/guardrails/evidence-system.js +379 -83
  231. package/dist/guardrails/evidence-system.js.map +1 -1
  232. package/dist/guardrails/replay-ledger-io.d.ts +6 -0
  233. package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
  234. package/dist/guardrails/replay-ledger-io.js +42 -0
  235. package/dist/guardrails/replay-ledger-io.js.map +1 -0
  236. package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
  237. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
  238. package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
  239. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
  240. package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
  241. package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
  242. package/dist/guardrails/replay-ledger-lock.js +135 -0
  243. package/dist/guardrails/replay-ledger-lock.js.map +1 -0
  244. package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
  245. package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
  246. package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
  247. package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
  248. package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
  249. package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
  250. package/dist/guardrails/replay-ledger-quarantine.js +19 -0
  251. package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
  252. package/dist/guardrails/replay-ledger-store.d.ts +32 -0
  253. package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
  254. package/dist/guardrails/replay-ledger-store.js +211 -0
  255. package/dist/guardrails/replay-ledger-store.js.map +1 -0
  256. package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
  257. package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
  258. package/dist/guardrails/shell-command-name-glob.js +233 -0
  259. package/dist/guardrails/shell-command-name-glob.js.map +1 -0
  260. package/dist/guardrails/shell-command-option.d.ts +7 -0
  261. package/dist/guardrails/shell-command-option.d.ts.map +1 -0
  262. package/dist/guardrails/shell-command-option.js +24 -0
  263. package/dist/guardrails/shell-command-option.js.map +1 -0
  264. package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
  265. package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
  266. package/dist/guardrails/shell-command-tokenizer.js +145 -0
  267. package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
  268. package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
  269. package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
  270. package/dist/guardrails/shell-command-wrapper.js +42 -0
  271. package/dist/guardrails/shell-command-wrapper.js.map +1 -0
  272. package/dist/guardrails/verified-executor.d.ts +65 -0
  273. package/dist/guardrails/verified-executor.d.ts.map +1 -0
  274. package/dist/guardrails/verified-executor.js +171 -0
  275. package/dist/guardrails/verified-executor.js.map +1 -0
  276. package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
  277. package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
  278. package/dist/guardrails/workspace-fingerprint.js +634 -0
  279. package/dist/guardrails/workspace-fingerprint.js.map +1 -0
  280. package/dist/index.d.ts +15 -3
  281. package/dist/index.d.ts.map +1 -1
  282. package/dist/index.js +13 -1
  283. package/dist/index.js.map +1 -1
  284. package/dist/main.d.ts.map +1 -1
  285. package/dist/main.js +12 -0
  286. package/dist/main.js.map +1 -1
  287. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  288. package/dist/modes/interactive/components/control-panel-layout.js +1 -1
  289. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  290. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  291. package/dist/modes/interactive/components/footer.js +1 -1
  292. package/dist/modes/interactive/components/footer.js.map +1 -1
  293. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  294. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  295. package/dist/modes/interactive/interactive-mode.js +59 -9
  296. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  297. package/dist/modes/print-mode.d.ts.map +1 -1
  298. package/dist/modes/print-mode.js +43 -2
  299. package/dist/modes/print-mode.js.map +1 -1
  300. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  301. package/dist/modes/rpc/rpc-mode.js +23 -5
  302. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  303. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  304. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  305. package/dist/modes/rpc/rpc-types.js.map +1 -1
  306. package/dist/types/evidence.d.ts +215 -1
  307. package/dist/types/evidence.d.ts.map +1 -1
  308. package/dist/types/evidence.js.map +1 -1
  309. package/dist/verify-ci.d.ts +3 -0
  310. package/dist/verify-ci.d.ts.map +1 -0
  311. package/dist/verify-ci.js +30 -0
  312. package/dist/verify-ci.js.map +1 -0
  313. package/docs/compaction.md +44 -4
  314. package/docs/extensions.md +2 -0
  315. package/docs/providers.md +6 -0
  316. package/docs/sdk.md +238 -1
  317. package/docs/settings.md +67 -4
  318. package/docs/usage.md +38 -0
  319. package/examples/extensions/correctness-wall/README.md +6 -6
  320. package/examples/extensions/correctness-wall/index.ts +37 -11
  321. package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
  322. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  323. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  324. package/examples/extensions/gondolin/package-lock.json +2 -2
  325. package/examples/extensions/gondolin/package.json +1 -1
  326. package/examples/extensions/sandbox/package-lock.json +2 -2
  327. package/examples/extensions/sandbox/package.json +1 -1
  328. package/examples/extensions/with-deps/package-lock.json +4 -4
  329. package/examples/extensions/with-deps/package.json +1 -1
  330. package/npm-shrinkwrap.json +12 -12
  331. package/package.json +4 -4
@@ -0,0 +1,215 @@
1
+ /**
2
+ * CLI entry for the provider doctor.
3
+ *
4
+ * Canonical form: `omk provider doctor <provider-id>` with `--level`, `--model`,
5
+ * `--timeout`, and the Level-2 opt-in `--probe-model`. The legacy flag form
6
+ * `omk --doctor-provider <provider-id>` (with `--doctor-level`/`--doctor-model`/
7
+ * `--doctor-timeout`) remains a supported alias.
8
+ *
9
+ * Prints exactly one sanitized JSON document to stdout and reports a stable exit code:
10
+ * 0 = diagnosis ok, 1 = diagnosis failed, 2 = usage error. Usage errors never echo
11
+ * argument values, so mistyped invocations cannot leak credentials.
12
+ */
13
+ import { join } from "node:path";
14
+ import { AuthStorage } from "../core/auth-storage.js";
15
+ import { ModelRegistry } from "../core/model-registry.js";
16
+ import { canonicalProviderEndpointUrl, diagnoseProvider, getAgentDir, } from "./doctor-provider.js";
17
+ import { createProviderDoctorTransport } from "./doctor-provider-transport.js";
18
+ export const DOCTOR_PROVIDER_FLAG = "--doctor-provider";
19
+ const USAGE = `Usage: omk provider doctor <provider-id> [--level <0|1>] [--model <model-id>] [--timeout <ms>] [--probe-model <model-id>] (legacy alias: omk ${DOCTOR_PROVIDER_FLAG} <provider-id>)`;
20
+ const HELP = [
21
+ "Provider doctor: diagnose one provider's configuration and endpoint reachability.",
22
+ "",
23
+ USAGE,
24
+ "",
25
+ " --level <0|1> 0 = static checks only (default); 1 = adds GET-only reachability probes",
26
+ " --model <model-id> validate a specific model against the provider",
27
+ " --timeout <ms> network probe timeout in milliseconds",
28
+ " --probe-model <model-id> Level 2 (opt-in): one minimal-token generative POST probe;",
29
+ " may incur provider costs and sets costWarning in the output",
30
+ "",
31
+ "Levels 0 and 1 are non-generative and GET-only; no tool-call probe is ever sent.",
32
+ "Output is one sanitized JSON document (credentials are never printed).",
33
+ "Exit codes: 0 = ok, 1 = diagnosis failed, 2 = usage error.",
34
+ "Legacy flags --doctor-level/--doctor-model/--doctor-timeout are aliases of --level/--model/--timeout.",
35
+ ].join("\n");
36
+ function parseDoctorProviderArgs(args) {
37
+ const canonical = args[0] === "provider" && args[1] === "doctor";
38
+ if (!canonical && !args.includes(DOCTOR_PROVIDER_FLAG))
39
+ return { kind: "absent" };
40
+ const error = (message) => ({ kind: "error", message });
41
+ let providerId;
42
+ let level = 0;
43
+ let modelId;
44
+ let timeoutMs;
45
+ let probeModelId;
46
+ let help = false;
47
+ const rest = canonical ? args.slice(2) : args;
48
+ for (let index = 0; index < rest.length; index++) {
49
+ const arg = rest[index];
50
+ const takeValue = () => {
51
+ const value = rest[index + 1];
52
+ if (value === undefined || value.startsWith("--"))
53
+ return undefined;
54
+ index++;
55
+ return value;
56
+ };
57
+ if (!canonical && arg === DOCTOR_PROVIDER_FLAG) {
58
+ providerId = takeValue();
59
+ if (!providerId)
60
+ return error(`${DOCTOR_PROVIDER_FLAG} requires a provider id`);
61
+ }
62
+ else if (arg === "--help" || arg === "-h") {
63
+ help = true;
64
+ }
65
+ else if (arg === "--doctor-level" || arg === "--level") {
66
+ const value = takeValue();
67
+ if (value !== "0" && value !== "1") {
68
+ return error("--level/--doctor-level must be 0 or 1; Level 2 runs only via --probe-model");
69
+ }
70
+ level = Number(value);
71
+ }
72
+ else if (arg === "--doctor-model" || arg === "--model") {
73
+ modelId = takeValue();
74
+ if (!modelId)
75
+ return error("--model/--doctor-model requires a model id");
76
+ }
77
+ else if (arg === "--doctor-timeout" || arg === "--timeout") {
78
+ const value = takeValue();
79
+ const parsedTimeout = value === undefined ? Number.NaN : Number(value);
80
+ if (!Number.isInteger(parsedTimeout) || parsedTimeout <= 0) {
81
+ return error("--timeout/--doctor-timeout requires a positive integer of milliseconds");
82
+ }
83
+ timeoutMs = parsedTimeout;
84
+ }
85
+ else if (arg === "--probe-model") {
86
+ probeModelId = takeValue();
87
+ if (!probeModelId)
88
+ return error("--probe-model requires a model id");
89
+ }
90
+ else if (canonical && providerId === undefined && !arg.startsWith("-")) {
91
+ providerId = arg;
92
+ }
93
+ else {
94
+ // Never echo the offending argument: it may carry a credential value.
95
+ return error("unexpected extra arguments in provider doctor mode");
96
+ }
97
+ }
98
+ if (help)
99
+ return { kind: "help" };
100
+ if (!providerId) {
101
+ return error(canonical ? "provider doctor requires a provider id" : `${DOCTOR_PROVIDER_FLAG} requires a provider id`);
102
+ }
103
+ if (modelId !== undefined && probeModelId !== undefined && modelId !== probeModelId) {
104
+ return error("--model and --probe-model must name the same model");
105
+ }
106
+ // Level 2 is reachable only through the explicit --probe-model opt-in.
107
+ if (probeModelId !== undefined)
108
+ level = 2;
109
+ return { kind: "ok", parsed: { providerId, level, modelId, timeoutMs, probeModelId } };
110
+ }
111
+ const STRIPPED_REQUEST_HEADERS = new Set([
112
+ "connection",
113
+ "content-length",
114
+ "expect",
115
+ "host",
116
+ "keep-alive",
117
+ "proxy-authenticate",
118
+ "proxy-authorization",
119
+ "proxy-connection",
120
+ "te",
121
+ "trailer",
122
+ "transfer-encoding",
123
+ "upgrade",
124
+ ]);
125
+ const SINGLE_VALUE_CREDENTIAL_HEADERS = new Set(["authorization", "cf-aig-authorization"]);
126
+ function safeResolvedHeaders(resolvedHeaders, apiKey, providerId) {
127
+ const headers = new Headers();
128
+ const names = new Set();
129
+ for (const [name, value] of Object.entries(resolvedHeaders ?? {})) {
130
+ const normalizedName = name.toLowerCase();
131
+ if (STRIPPED_REQUEST_HEADERS.has(normalizedName))
132
+ continue;
133
+ if (SINGLE_VALUE_CREDENTIAL_HEADERS.has(normalizedName) && names.has(normalizedName)) {
134
+ throw new Error("provider auth headers conflict");
135
+ }
136
+ names.add(normalizedName);
137
+ headers.set(name, value);
138
+ }
139
+ if (apiKey) {
140
+ const authorizationName = providerId === "cloudflare-ai-gateway" ? "cf-aig-authorization" : "authorization";
141
+ if (!headers.has(authorizationName))
142
+ headers.set(authorizationName, `Bearer ${apiKey}`);
143
+ }
144
+ return headers;
145
+ }
146
+ function createProductionAuthHeaderResolver(agentDir) {
147
+ let registry;
148
+ return async (target) => {
149
+ registry ??= ModelRegistry.create(AuthStorage.create(join(agentDir, "auth.json")), join(agentDir, "models.json"));
150
+ const modelId = target.endpoint.modelId;
151
+ const model = modelId ? registry.find(target.providerId, modelId) : undefined;
152
+ if (!model ||
153
+ canonicalProviderEndpointUrl(model.baseUrl) !== canonicalProviderEndpointUrl(target.endpoint.baseUrl) ||
154
+ model.api !== target.endpoint.api) {
155
+ throw new Error("provider auth model unavailable");
156
+ }
157
+ const resolved = await registry.getApiKeyAndHeaders(model);
158
+ if (!resolved.ok)
159
+ throw new Error("provider auth unavailable");
160
+ const headers = safeResolvedHeaders(resolved.headers, resolved.apiKey, target.providerId);
161
+ if (target.auth.present && !resolved.apiKey && [...headers].length === 0) {
162
+ throw new Error("provider auth unavailable");
163
+ }
164
+ return headers;
165
+ };
166
+ }
167
+ /**
168
+ * Runs the provider doctor when argv is `provider doctor ...` or contains `--doctor-provider`.
169
+ * Returns `{ handled: false }` untouched argv so the regular CLI flow continues.
170
+ */
171
+ export async function runDoctorProviderCli(args, overrides = {}) {
172
+ const parsed = parseDoctorProviderArgs(args);
173
+ if (parsed.kind === "absent")
174
+ return { handled: false, exitCode: 0 };
175
+ const writeLine = overrides.writeLine ?? ((line) => console.log(line));
176
+ if (parsed.kind === "help") {
177
+ writeLine(HELP);
178
+ return { handled: true, exitCode: 0 };
179
+ }
180
+ if (parsed.kind === "error") {
181
+ writeLine(JSON.stringify({
182
+ status: "fail",
183
+ error: { category: "config", code: "cli-usage", message: `${parsed.message}. ${USAGE}` },
184
+ }, null, 2));
185
+ return { handled: true, exitCode: 2 };
186
+ }
187
+ const { providerId, level, modelId, timeoutMs, probeModelId } = parsed.parsed;
188
+ const transport = overrides.transport ?? (level >= 1 ? createProviderDoctorTransport() : undefined);
189
+ const agentDir = overrides.agentDir ?? getAgentDir();
190
+ const resolveAuthHeaders = level === 2 ? (overrides.resolveAuthHeaders ?? createProductionAuthHeaderResolver(agentDir)) : undefined;
191
+ try {
192
+ const result = await diagnoseProvider(providerId, {
193
+ level,
194
+ modelId,
195
+ probeModelId,
196
+ timeoutMs,
197
+ agentDir,
198
+ kimiConfigPath: overrides.kimiConfigPath,
199
+ transport,
200
+ resolveAuthHeaders,
201
+ });
202
+ writeLine(JSON.stringify(result, null, 2));
203
+ return { handled: true, exitCode: result.status === "ok" ? 0 : 1, result };
204
+ }
205
+ catch {
206
+ // diagnoseProvider maps every failure internally; this is fail-closed insurance
207
+ // with a fixed message so nothing sensitive can surface.
208
+ writeLine(JSON.stringify({
209
+ status: "fail",
210
+ error: { category: "unknown", code: "unexpected-response", message: "Provider doctor failed" },
211
+ }, null, 2));
212
+ return { handled: true, exitCode: 1 };
213
+ }
214
+ }
215
+ //# sourceMappingURL=doctor-provider-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-provider-cli.js","sourceRoot":"","sources":["../../src/commands/doctor-provider-cli.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * CLI entry for the provider doctor.\n *\n * Canonical form: `omk provider doctor <provider-id>` with `--level`, `--model`,\n * `--timeout`, and the Level-2 opt-in `--probe-model`. The legacy flag form\n * `omk --doctor-provider <provider-id>` (with `--doctor-level`/`--doctor-model`/\n * `--doctor-timeout`) remains a supported alias.\n *\n * Prints exactly one sanitized JSON document to stdout and reports a stable exit code:\n * 0 = diagnosis ok, 1 = diagnosis failed, 2 = usage error. Usage errors never echo\n * argument values, so mistyped invocations cannot leak credentials.\n */\n\nimport { join } from \"node:path\";\nimport { AuthStorage } from \"../core/auth-storage.ts\";\nimport { ModelRegistry } from \"../core/model-registry.ts\";\nimport {\n\tcanonicalProviderEndpointUrl,\n\tdiagnoseProvider,\n\tgetAgentDir,\n\ttype ProviderDoctorAuthHeaderResolver,\n\ttype ProviderDoctorLevel,\n\ttype ProviderDoctorResult,\n\ttype ProviderDoctorTransport,\n} from \"./doctor-provider.ts\";\nimport { createProviderDoctorTransport } from \"./doctor-provider-transport.ts\";\n\nexport const DOCTOR_PROVIDER_FLAG = \"--doctor-provider\";\nconst USAGE = `Usage: omk provider doctor <provider-id> [--level <0|1>] [--model <model-id>] [--timeout <ms>] [--probe-model <model-id>] (legacy alias: omk ${DOCTOR_PROVIDER_FLAG} <provider-id>)`;\nconst HELP = [\n\t\"Provider doctor: diagnose one provider's configuration and endpoint reachability.\",\n\t\"\",\n\tUSAGE,\n\t\"\",\n\t\" --level <0|1> 0 = static checks only (default); 1 = adds GET-only reachability probes\",\n\t\" --model <model-id> validate a specific model against the provider\",\n\t\" --timeout <ms> network probe timeout in milliseconds\",\n\t\" --probe-model <model-id> Level 2 (opt-in): one minimal-token generative POST probe;\",\n\t\" may incur provider costs and sets costWarning in the output\",\n\t\"\",\n\t\"Levels 0 and 1 are non-generative and GET-only; no tool-call probe is ever sent.\",\n\t\"Output is one sanitized JSON document (credentials are never printed).\",\n\t\"Exit codes: 0 = ok, 1 = diagnosis failed, 2 = usage error.\",\n\t\"Legacy flags --doctor-level/--doctor-model/--doctor-timeout are aliases of --level/--model/--timeout.\",\n].join(\"\\n\");\n\nexport interface DoctorProviderCliOverrides {\n\tagentDir?: string;\n\tkimiConfigPath?: string;\n\ttransport?: ProviderDoctorTransport;\n\t/** Level-2 test seam; production uses AuthStorage + ModelRegistry lazily inside request dispatch. */\n\tresolveAuthHeaders?: ProviderDoctorAuthHeaderResolver;\n\twriteLine?: (line: string) => void;\n}\n\nexport interface DoctorProviderCliOutcome {\n\thandled: boolean;\n\texitCode: number;\n\tresult?: ProviderDoctorResult;\n}\n\ninterface ParsedDoctorArgs {\n\tproviderId: string;\n\tlevel: ProviderDoctorLevel;\n\tmodelId?: string;\n\ttimeoutMs?: number;\n\tprobeModelId?: string;\n}\n\ntype ParseOutcome =\n\t| { kind: \"absent\" }\n\t| { kind: \"help\" }\n\t| { kind: \"error\"; message: string }\n\t| { kind: \"ok\"; parsed: ParsedDoctorArgs };\n\nfunction parseDoctorProviderArgs(args: readonly string[]): ParseOutcome {\n\tconst canonical = args[0] === \"provider\" && args[1] === \"doctor\";\n\tif (!canonical && !args.includes(DOCTOR_PROVIDER_FLAG)) return { kind: \"absent\" };\n\tconst error = (message: string): ParseOutcome => ({ kind: \"error\", message });\n\n\tlet providerId: string | undefined;\n\tlet level: ProviderDoctorLevel = 0;\n\tlet modelId: string | undefined;\n\tlet timeoutMs: number | undefined;\n\tlet probeModelId: string | undefined;\n\tlet help = false;\n\tconst rest = canonical ? args.slice(2) : args;\n\tfor (let index = 0; index < rest.length; index++) {\n\t\tconst arg = rest[index];\n\t\tconst takeValue = (): string | undefined => {\n\t\t\tconst value = rest[index + 1];\n\t\t\tif (value === undefined || value.startsWith(\"--\")) return undefined;\n\t\t\tindex++;\n\t\t\treturn value;\n\t\t};\n\t\tif (!canonical && arg === DOCTOR_PROVIDER_FLAG) {\n\t\t\tproviderId = takeValue();\n\t\t\tif (!providerId) return error(`${DOCTOR_PROVIDER_FLAG} requires a provider id`);\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\thelp = true;\n\t\t} else if (arg === \"--doctor-level\" || arg === \"--level\") {\n\t\t\tconst value = takeValue();\n\t\t\tif (value !== \"0\" && value !== \"1\") {\n\t\t\t\treturn error(\"--level/--doctor-level must be 0 or 1; Level 2 runs only via --probe-model\");\n\t\t\t}\n\t\t\tlevel = Number(value) as ProviderDoctorLevel;\n\t\t} else if (arg === \"--doctor-model\" || arg === \"--model\") {\n\t\t\tmodelId = takeValue();\n\t\t\tif (!modelId) return error(\"--model/--doctor-model requires a model id\");\n\t\t} else if (arg === \"--doctor-timeout\" || arg === \"--timeout\") {\n\t\t\tconst value = takeValue();\n\t\t\tconst parsedTimeout = value === undefined ? Number.NaN : Number(value);\n\t\t\tif (!Number.isInteger(parsedTimeout) || parsedTimeout <= 0) {\n\t\t\t\treturn error(\"--timeout/--doctor-timeout requires a positive integer of milliseconds\");\n\t\t\t}\n\t\t\ttimeoutMs = parsedTimeout;\n\t\t} else if (arg === \"--probe-model\") {\n\t\t\tprobeModelId = takeValue();\n\t\t\tif (!probeModelId) return error(\"--probe-model requires a model id\");\n\t\t} else if (canonical && providerId === undefined && !arg.startsWith(\"-\")) {\n\t\t\tproviderId = arg;\n\t\t} else {\n\t\t\t// Never echo the offending argument: it may carry a credential value.\n\t\t\treturn error(\"unexpected extra arguments in provider doctor mode\");\n\t\t}\n\t}\n\tif (help) return { kind: \"help\" };\n\tif (!providerId) {\n\t\treturn error(\n\t\t\tcanonical ? \"provider doctor requires a provider id\" : `${DOCTOR_PROVIDER_FLAG} requires a provider id`,\n\t\t);\n\t}\n\tif (modelId !== undefined && probeModelId !== undefined && modelId !== probeModelId) {\n\t\treturn error(\"--model and --probe-model must name the same model\");\n\t}\n\t// Level 2 is reachable only through the explicit --probe-model opt-in.\n\tif (probeModelId !== undefined) level = 2;\n\treturn { kind: \"ok\", parsed: { providerId, level, modelId, timeoutMs, probeModelId } };\n}\n\nconst STRIPPED_REQUEST_HEADERS = new Set([\n\t\"connection\",\n\t\"content-length\",\n\t\"expect\",\n\t\"host\",\n\t\"keep-alive\",\n\t\"proxy-authenticate\",\n\t\"proxy-authorization\",\n\t\"proxy-connection\",\n\t\"te\",\n\t\"trailer\",\n\t\"transfer-encoding\",\n\t\"upgrade\",\n]);\nconst SINGLE_VALUE_CREDENTIAL_HEADERS = new Set([\"authorization\", \"cf-aig-authorization\"]);\n\nfunction safeResolvedHeaders(\n\tresolvedHeaders: Record<string, string> | undefined,\n\tapiKey: string | undefined,\n\tproviderId: string,\n): Headers {\n\tconst headers = new Headers();\n\tconst names = new Set<string>();\n\tfor (const [name, value] of Object.entries(resolvedHeaders ?? {})) {\n\t\tconst normalizedName = name.toLowerCase();\n\t\tif (STRIPPED_REQUEST_HEADERS.has(normalizedName)) continue;\n\t\tif (SINGLE_VALUE_CREDENTIAL_HEADERS.has(normalizedName) && names.has(normalizedName)) {\n\t\t\tthrow new Error(\"provider auth headers conflict\");\n\t\t}\n\t\tnames.add(normalizedName);\n\t\theaders.set(name, value);\n\t}\n\tif (apiKey) {\n\t\tconst authorizationName = providerId === \"cloudflare-ai-gateway\" ? \"cf-aig-authorization\" : \"authorization\";\n\t\tif (!headers.has(authorizationName)) headers.set(authorizationName, `Bearer ${apiKey}`);\n\t}\n\treturn headers;\n}\n\nfunction createProductionAuthHeaderResolver(agentDir: string): ProviderDoctorAuthHeaderResolver {\n\tlet registry: ModelRegistry | undefined;\n\treturn async (target) => {\n\t\tregistry ??= ModelRegistry.create(AuthStorage.create(join(agentDir, \"auth.json\")), join(agentDir, \"models.json\"));\n\t\tconst modelId = target.endpoint.modelId;\n\t\tconst model = modelId ? registry.find(target.providerId, modelId) : undefined;\n\t\tif (\n\t\t\t!model ||\n\t\t\tcanonicalProviderEndpointUrl(model.baseUrl) !== canonicalProviderEndpointUrl(target.endpoint.baseUrl) ||\n\t\t\tmodel.api !== target.endpoint.api\n\t\t) {\n\t\t\tthrow new Error(\"provider auth model unavailable\");\n\t\t}\n\n\t\tconst resolved = await registry.getApiKeyAndHeaders(model);\n\t\tif (!resolved.ok) throw new Error(\"provider auth unavailable\");\n\t\tconst headers = safeResolvedHeaders(resolved.headers, resolved.apiKey, target.providerId);\n\t\tif (target.auth.present && !resolved.apiKey && [...headers].length === 0) {\n\t\t\tthrow new Error(\"provider auth unavailable\");\n\t\t}\n\t\treturn headers;\n\t};\n}\n\n/**\n * Runs the provider doctor when argv is `provider doctor ...` or contains `--doctor-provider`.\n * Returns `{ handled: false }` untouched argv so the regular CLI flow continues.\n */\nexport async function runDoctorProviderCli(\n\targs: readonly string[],\n\toverrides: DoctorProviderCliOverrides = {},\n): Promise<DoctorProviderCliOutcome> {\n\tconst parsed = parseDoctorProviderArgs(args);\n\tif (parsed.kind === \"absent\") return { handled: false, exitCode: 0 };\n\n\tconst writeLine = overrides.writeLine ?? ((line: string) => console.log(line));\n\tif (parsed.kind === \"help\") {\n\t\twriteLine(HELP);\n\t\treturn { handled: true, exitCode: 0 };\n\t}\n\tif (parsed.kind === \"error\") {\n\t\twriteLine(\n\t\t\tJSON.stringify(\n\t\t\t\t{\n\t\t\t\t\tstatus: \"fail\",\n\t\t\t\t\terror: { category: \"config\", code: \"cli-usage\", message: `${parsed.message}. ${USAGE}` },\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t),\n\t\t);\n\t\treturn { handled: true, exitCode: 2 };\n\t}\n\n\tconst { providerId, level, modelId, timeoutMs, probeModelId } = parsed.parsed;\n\tconst transport = overrides.transport ?? (level >= 1 ? createProviderDoctorTransport() : undefined);\n\tconst agentDir = overrides.agentDir ?? getAgentDir();\n\tconst resolveAuthHeaders =\n\t\tlevel === 2 ? (overrides.resolveAuthHeaders ?? createProductionAuthHeaderResolver(agentDir)) : undefined;\n\ttry {\n\t\tconst result = await diagnoseProvider(providerId, {\n\t\t\tlevel,\n\t\t\tmodelId,\n\t\t\tprobeModelId,\n\t\t\ttimeoutMs,\n\t\t\tagentDir,\n\t\t\tkimiConfigPath: overrides.kimiConfigPath,\n\t\t\ttransport,\n\t\t\tresolveAuthHeaders,\n\t\t});\n\t\twriteLine(JSON.stringify(result, null, 2));\n\t\treturn { handled: true, exitCode: result.status === \"ok\" ? 0 : 1, result };\n\t} catch {\n\t\t// diagnoseProvider maps every failure internally; this is fail-closed insurance\n\t\t// with a fixed message so nothing sensitive can surface.\n\t\twriteLine(\n\t\t\tJSON.stringify(\n\t\t\t\t{\n\t\t\t\t\tstatus: \"fail\",\n\t\t\t\t\terror: { category: \"unknown\", code: \"unexpected-response\", message: \"Provider doctor failed\" },\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t),\n\t\t);\n\t\treturn { handled: true, exitCode: 1 };\n\t}\n}\n"]}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * First-party Level-1/Level-2 transport for the provider doctor.
3
+ *
4
+ * Safety properties:
5
+ * - GET, plus bounded JSON POST for the opt-in Level-2 model probe; redirects are
6
+ * never followed (the raw 3xx status is returned).
7
+ * - POST bodies are capped, must be JSON, and are never echoed anywhere.
8
+ * - The response is dropped as soon as the status line arrives: no body, headers,
9
+ * or error details ever leave this module, and thrown errors use fixed messages.
10
+ * - Loopback-only policies require every resolved address to be loopback.
11
+ * - Public policies validate every resolved address against the doctor blocklist and
12
+ * pin the socket to one validated address; the hostname is never re-resolved
13
+ * (DNS-rebinding safe) and TLS identity is still checked against the original hostname.
14
+ */
15
+ import { type Socket } from "node:net";
16
+ import { type ProviderDoctorTransport } from "./doctor-provider.ts";
17
+ /** Resolves a hostname to every address it maps to. */
18
+ export type ProviderDoctorLookup = (hostname: string) => Promise<readonly string[]>;
19
+ /** The validated connection target handed to the test-only socket seam. */
20
+ export interface ProviderDoctorPinnedConnection {
21
+ address: string;
22
+ port: number;
23
+ hostname: string;
24
+ }
25
+ export interface ProviderDoctorTransportOptions {
26
+ /** DNS resolver returning every address for a hostname. Defaults to node:dns lookup({ all: true }). */
27
+ lookup?: ProviderDoctorLookup;
28
+ /**
29
+ * Test-only seam (plain http) that builds the socket for a validated, pinned address.
30
+ * Production requests always dial the pinned address directly.
31
+ */
32
+ createConnection?: (pinned: ProviderDoctorPinnedConnection) => Socket;
33
+ }
34
+ export declare function createProviderDoctorTransport(options?: ProviderDoctorTransportOptions): ProviderDoctorTransport;
35
+ //# sourceMappingURL=doctor-provider-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-provider-transport.d.ts","sourceRoot":"","sources":["../../src/commands/doctor-provider-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,EAAQ,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAKN,KAAK,uBAAuB,EAG5B,MAAM,sBAAsB,CAAC;AAE9B,uDAAuD;AACvD,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;AAEpF,2EAA2E;AAC3E,MAAM,WAAW,8BAA8B;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC9C,uGAAuG;IACvG,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,8BAA8B,KAAK,MAAM,CAAC;CACtE;AA2DD,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,8BAAmC,GAAG,uBAAuB,CA+GnH","sourcesContent":["/**\n * First-party Level-1/Level-2 transport for the provider doctor.\n *\n * Safety properties:\n * - GET, plus bounded JSON POST for the opt-in Level-2 model probe; redirects are\n * never followed (the raw 3xx status is returned).\n * - POST bodies are capped, must be JSON, and are never echoed anywhere.\n * - The response is dropped as soon as the status line arrives: no body, headers,\n * or error details ever leave this module, and thrown errors use fixed messages.\n * - Loopback-only policies require every resolved address to be loopback.\n * - Public policies validate every resolved address against the doctor blocklist and\n * pin the socket to one validated address; the hostname is never re-resolved\n * (DNS-rebinding safe) and TLS identity is still checked against the original hostname.\n */\n\nimport { lookup as dnsLookup } from \"node:dns/promises\";\nimport { request as httpRequest, type RequestOptions } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { isIP, type Socket } from \"node:net\";\nimport { checkServerIdentity } from \"node:tls\";\nimport {\n\tisBlockedPublicAddress,\n\tisLoopbackAddress,\n\tnormalizedUrlHostname,\n\tProviderDoctorAuthMaterializationError,\n\ttype ProviderDoctorTransport,\n\ttype ProviderDoctorTransportRequest,\n\ttype ProviderDoctorTransportResponse,\n} from \"./doctor-provider.ts\";\n\n/** Resolves a hostname to every address it maps to. */\nexport type ProviderDoctorLookup = (hostname: string) => Promise<readonly string[]>;\n\n/** The validated connection target handed to the test-only socket seam. */\nexport interface ProviderDoctorPinnedConnection {\n\taddress: string;\n\tport: number;\n\thostname: string;\n}\n\nexport interface ProviderDoctorTransportOptions {\n\t/** DNS resolver returning every address for a hostname. Defaults to node:dns lookup({ all: true }). */\n\tlookup?: ProviderDoctorLookup;\n\t/**\n\t * Test-only seam (plain http) that builds the socket for a validated, pinned address.\n\t * Production requests always dial the pinned address directly.\n\t */\n\tcreateConnection?: (pinned: ProviderDoctorPinnedConnection) => Socket;\n}\n\nconst SCHEME_UNSUPPORTED = \"provider doctor transport rejected an unsupported URL scheme\";\nconst METHOD_UNSUPPORTED = \"provider doctor transport only performs GET and bounded POST requests\";\nconst REQUEST_SHAPE_INVALID = \"provider doctor transport rejected an invalid request shape\";\nconst MAX_POST_BODY_BYTES = 4_096;\nconst RESOLUTION_FAILED = \"provider doctor transport could not resolve the endpoint hostname\";\nconst RESOLUTION_BLOCKED = \"provider doctor transport blocked hostname re-resolution\";\nconst ADDRESS_BLOCKED = \"provider doctor transport blocked a resolved address by policy\";\nconst REQUEST_FAILED = \"provider doctor transport request failed\";\nconst REQUEST_ABORTED = \"provider doctor transport request was aborted\";\nconst STRIPPED_REQUEST_HEADERS = new Set([\n\t\"connection\",\n\t\"content-length\",\n\t\"expect\",\n\t\"host\",\n\t\"keep-alive\",\n\t\"proxy-authenticate\",\n\t\"proxy-authorization\",\n\t\"proxy-connection\",\n\t\"te\",\n\t\"trailer\",\n\t\"transfer-encoding\",\n\t\"upgrade\",\n]);\nconst SINGLE_VALUE_CREDENTIAL_HEADERS = new Set([\"authorization\", \"cf-aig-authorization\"]);\nconst ADDITIONAL_AUTHORIZATION_VALUE = /,\\s*[!#$%&'*+.^_`|~0-9A-Za-z-]+\\s+/;\n\nconst defaultLookup: ProviderDoctorLookup = async (hostname) => {\n\tconst results = await dnsLookup(hostname, { all: true, verbatim: true });\n\treturn results.map((result) => result.address);\n};\n\nasync function resolveAddresses(hostname: string, lookup: ProviderDoctorLookup): Promise<readonly string[]> {\n\tif (isIP(hostname) !== 0) return [hostname];\n\tlet addresses: readonly string[];\n\ttry {\n\t\taddresses = await lookup(hostname);\n\t} catch {\n\t\tthrow new Error(RESOLUTION_FAILED);\n\t}\n\tif (addresses.length === 0) throw new Error(RESOLUTION_FAILED);\n\treturn addresses;\n}\n\nfunction assertRequestActive(signal: AbortSignal): void {\n\tif (signal.aborted) throw new Error(REQUEST_ABORTED);\n}\n\nfunction assertAddressesAllowed(\n\taddresses: readonly string[],\n\tpolicy: ProviderDoctorTransportRequest[\"addressPolicy\"],\n): void {\n\tfor (const address of addresses) {\n\t\tconst allowed = policy.kind === \"loopback-only\" ? isLoopbackAddress(address) : !isBlockedPublicAddress(address);\n\t\tif (!allowed) throw new Error(ADDRESS_BLOCKED);\n\t}\n}\n\nexport function createProviderDoctorTransport(options: ProviderDoctorTransportOptions = {}): ProviderDoctorTransport {\n\tconst lookup = options.lookup ?? defaultLookup;\n\n\treturn {\n\t\tpinsResolvedAddress: true,\n\t\tasync request(request: ProviderDoctorTransportRequest): Promise<ProviderDoctorTransportResponse> {\n\t\t\tassertRequestActive(request.signal);\n\t\t\tconst isPost = request.method === \"POST\";\n\t\t\tif (request.method !== \"GET\" && !isPost) throw new Error(METHOD_UNSUPPORTED);\n\t\t\tif (!isPost && request.body !== undefined) throw new Error(REQUEST_SHAPE_INVALID);\n\t\t\tif (isPost) {\n\t\t\t\tif (typeof request.body !== \"string\" || request.contentType !== \"application/json\") {\n\t\t\t\t\tthrow new Error(REQUEST_SHAPE_INVALID);\n\t\t\t\t}\n\t\t\t\tif (Buffer.byteLength(request.body, \"utf-8\") > MAX_POST_BODY_BYTES) {\n\t\t\t\t\tthrow new Error(REQUEST_SHAPE_INVALID);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst url = request.url;\n\t\t\tconst isHttps = url.protocol === \"https:\";\n\t\t\tif (!isHttps && url.protocol !== \"http:\") throw new Error(SCHEME_UNSUPPORTED);\n\n\t\t\tconst hostname = normalizedUrlHostname(url);\n\t\t\tconst addresses = await resolveAddresses(hostname, lookup);\n\t\t\tassertAddressesAllowed(addresses, request.addressPolicy);\n\t\t\tassertRequestActive(request.signal);\n\t\t\tconst pinned = addresses[0];\n\t\t\tconst port = url.port.length > 0 ? Number(url.port) : isHttps ? 443 : 80;\n\n\t\t\tlet materializedHeaders: Headers;\n\t\t\ttry {\n\t\t\t\tmaterializedHeaders = await request.createHeaders();\n\t\t\t} catch {\n\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t}\n\t\t\tassertRequestActive(request.signal);\n\t\t\tconst headers: Record<string, string> = {};\n\t\t\tfor (const [name, value] of materializedHeaders) {\n\t\t\t\tconst normalizedName = name.toLowerCase();\n\t\t\t\tif (STRIPPED_REQUEST_HEADERS.has(normalizedName)) continue;\n\t\t\t\tif (SINGLE_VALUE_CREDENTIAL_HEADERS.has(normalizedName) && ADDITIONAL_AUTHORIZATION_VALUE.test(value)) {\n\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t}\n\t\t\t\theaders[normalizedName] = value;\n\t\t\t}\n\t\t\theaders.host = url.host;\n\t\t\tif (isPost) {\n\t\t\t\theaders[\"content-type\"] = \"application/json\";\n\t\t\t\theaders[\"content-length\"] = String(Buffer.byteLength(request.body as string, \"utf-8\"));\n\t\t\t}\n\n\t\t\tconst base: RequestOptions = {\n\t\t\t\tmethod: request.method,\n\t\t\t\tpath: `${url.pathname}${url.search}`,\n\t\t\t\theaders,\n\t\t\t\tsignal: request.signal,\n\t\t\t\tsetHost: false,\n\t\t\t};\n\t\t\tconst seam = options.createConnection;\n\t\t\tconst requestOptions: RequestOptions = seam\n\t\t\t\t? { ...base, createConnection: () => seam({ address: pinned, port, hostname }) }\n\t\t\t\t: {\n\t\t\t\t\t\t...base,\n\t\t\t\t\t\thost: pinned,\n\t\t\t\t\t\tport,\n\t\t\t\t\t\tagent: false,\n\t\t\t\t\t\t// Fail closed if anything below ever tries to resolve a name again.\n\t\t\t\t\t\tlookup: (_hostname, lookupOptions, callback) => {\n\t\t\t\t\t\t\tconst done = typeof lookupOptions === \"function\" ? lookupOptions : callback;\n\t\t\t\t\t\t\t(done as (error: Error) => void)(new Error(RESOLUTION_BLOCKED));\n\t\t\t\t\t\t},\n\t\t\t\t\t\t...(isHttps\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tservername: isIP(hostname) === 0 ? hostname : \"\",\n\t\t\t\t\t\t\t\t\tcheckServerIdentity: (\n\t\t\t\t\t\t\t\t\t\t_host: string,\n\t\t\t\t\t\t\t\t\t\tcertificate: Parameters<typeof checkServerIdentity>[1],\n\t\t\t\t\t\t\t\t\t) => checkServerIdentity(hostname, certificate),\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t};\n\n\t\t\tassertRequestActive(request.signal);\n\t\t\treturn new Promise<ProviderDoctorTransportResponse>((resolve, reject) => {\n\t\t\t\tlet settled = false;\n\t\t\t\tconst settle = (action: () => void): void => {\n\t\t\t\t\tif (settled) return;\n\t\t\t\t\tsettled = true;\n\t\t\t\t\taction();\n\t\t\t\t};\n\t\t\t\tif (request.signal.aborted) {\n\t\t\t\t\tsettle(() => reject(new Error(REQUEST_ABORTED)));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst clientRequest = (isHttps ? httpsRequest : httpRequest)(requestOptions, (response) => {\n\t\t\t\t\tconst status = response.statusCode ?? 0;\n\t\t\t\t\t// Drop the response immediately: the doctor only needs the status,\n\t\t\t\t\t// so no body is ever read or buffered (bounded by construction).\n\t\t\t\t\tresponse.destroy();\n\t\t\t\t\tsettle(() => resolve({ status }));\n\t\t\t\t});\n\t\t\t\tclientRequest.on(\"error\", () => settle(() => reject(new Error(REQUEST_FAILED))));\n\t\t\t\tif (request.signal.aborted) {\n\t\t\t\t\tclientRequest.destroy();\n\t\t\t\t\tsettle(() => reject(new Error(REQUEST_ABORTED)));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tclientRequest.end(isPost ? (request.body as string) : undefined);\n\t\t\t});\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * First-party Level-1/Level-2 transport for the provider doctor.
3
+ *
4
+ * Safety properties:
5
+ * - GET, plus bounded JSON POST for the opt-in Level-2 model probe; redirects are
6
+ * never followed (the raw 3xx status is returned).
7
+ * - POST bodies are capped, must be JSON, and are never echoed anywhere.
8
+ * - The response is dropped as soon as the status line arrives: no body, headers,
9
+ * or error details ever leave this module, and thrown errors use fixed messages.
10
+ * - Loopback-only policies require every resolved address to be loopback.
11
+ * - Public policies validate every resolved address against the doctor blocklist and
12
+ * pin the socket to one validated address; the hostname is never re-resolved
13
+ * (DNS-rebinding safe) and TLS identity is still checked against the original hostname.
14
+ */
15
+ import { lookup as dnsLookup } from "node:dns/promises";
16
+ import { request as httpRequest } from "node:http";
17
+ import { request as httpsRequest } from "node:https";
18
+ import { isIP } from "node:net";
19
+ import { checkServerIdentity } from "node:tls";
20
+ import { isBlockedPublicAddress, isLoopbackAddress, normalizedUrlHostname, ProviderDoctorAuthMaterializationError, } from "./doctor-provider.js";
21
+ const SCHEME_UNSUPPORTED = "provider doctor transport rejected an unsupported URL scheme";
22
+ const METHOD_UNSUPPORTED = "provider doctor transport only performs GET and bounded POST requests";
23
+ const REQUEST_SHAPE_INVALID = "provider doctor transport rejected an invalid request shape";
24
+ const MAX_POST_BODY_BYTES = 4_096;
25
+ const RESOLUTION_FAILED = "provider doctor transport could not resolve the endpoint hostname";
26
+ const RESOLUTION_BLOCKED = "provider doctor transport blocked hostname re-resolution";
27
+ const ADDRESS_BLOCKED = "provider doctor transport blocked a resolved address by policy";
28
+ const REQUEST_FAILED = "provider doctor transport request failed";
29
+ const REQUEST_ABORTED = "provider doctor transport request was aborted";
30
+ const STRIPPED_REQUEST_HEADERS = new Set([
31
+ "connection",
32
+ "content-length",
33
+ "expect",
34
+ "host",
35
+ "keep-alive",
36
+ "proxy-authenticate",
37
+ "proxy-authorization",
38
+ "proxy-connection",
39
+ "te",
40
+ "trailer",
41
+ "transfer-encoding",
42
+ "upgrade",
43
+ ]);
44
+ const SINGLE_VALUE_CREDENTIAL_HEADERS = new Set(["authorization", "cf-aig-authorization"]);
45
+ const ADDITIONAL_AUTHORIZATION_VALUE = /,\s*[!#$%&'*+.^_`|~0-9A-Za-z-]+\s+/;
46
+ const defaultLookup = async (hostname) => {
47
+ const results = await dnsLookup(hostname, { all: true, verbatim: true });
48
+ return results.map((result) => result.address);
49
+ };
50
+ async function resolveAddresses(hostname, lookup) {
51
+ if (isIP(hostname) !== 0)
52
+ return [hostname];
53
+ let addresses;
54
+ try {
55
+ addresses = await lookup(hostname);
56
+ }
57
+ catch {
58
+ throw new Error(RESOLUTION_FAILED);
59
+ }
60
+ if (addresses.length === 0)
61
+ throw new Error(RESOLUTION_FAILED);
62
+ return addresses;
63
+ }
64
+ function assertRequestActive(signal) {
65
+ if (signal.aborted)
66
+ throw new Error(REQUEST_ABORTED);
67
+ }
68
+ function assertAddressesAllowed(addresses, policy) {
69
+ for (const address of addresses) {
70
+ const allowed = policy.kind === "loopback-only" ? isLoopbackAddress(address) : !isBlockedPublicAddress(address);
71
+ if (!allowed)
72
+ throw new Error(ADDRESS_BLOCKED);
73
+ }
74
+ }
75
+ export function createProviderDoctorTransport(options = {}) {
76
+ const lookup = options.lookup ?? defaultLookup;
77
+ return {
78
+ pinsResolvedAddress: true,
79
+ async request(request) {
80
+ assertRequestActive(request.signal);
81
+ const isPost = request.method === "POST";
82
+ if (request.method !== "GET" && !isPost)
83
+ throw new Error(METHOD_UNSUPPORTED);
84
+ if (!isPost && request.body !== undefined)
85
+ throw new Error(REQUEST_SHAPE_INVALID);
86
+ if (isPost) {
87
+ if (typeof request.body !== "string" || request.contentType !== "application/json") {
88
+ throw new Error(REQUEST_SHAPE_INVALID);
89
+ }
90
+ if (Buffer.byteLength(request.body, "utf-8") > MAX_POST_BODY_BYTES) {
91
+ throw new Error(REQUEST_SHAPE_INVALID);
92
+ }
93
+ }
94
+ const url = request.url;
95
+ const isHttps = url.protocol === "https:";
96
+ if (!isHttps && url.protocol !== "http:")
97
+ throw new Error(SCHEME_UNSUPPORTED);
98
+ const hostname = normalizedUrlHostname(url);
99
+ const addresses = await resolveAddresses(hostname, lookup);
100
+ assertAddressesAllowed(addresses, request.addressPolicy);
101
+ assertRequestActive(request.signal);
102
+ const pinned = addresses[0];
103
+ const port = url.port.length > 0 ? Number(url.port) : isHttps ? 443 : 80;
104
+ let materializedHeaders;
105
+ try {
106
+ materializedHeaders = await request.createHeaders();
107
+ }
108
+ catch {
109
+ throw new ProviderDoctorAuthMaterializationError();
110
+ }
111
+ assertRequestActive(request.signal);
112
+ const headers = {};
113
+ for (const [name, value] of materializedHeaders) {
114
+ const normalizedName = name.toLowerCase();
115
+ if (STRIPPED_REQUEST_HEADERS.has(normalizedName))
116
+ continue;
117
+ if (SINGLE_VALUE_CREDENTIAL_HEADERS.has(normalizedName) && ADDITIONAL_AUTHORIZATION_VALUE.test(value)) {
118
+ throw new ProviderDoctorAuthMaterializationError();
119
+ }
120
+ headers[normalizedName] = value;
121
+ }
122
+ headers.host = url.host;
123
+ if (isPost) {
124
+ headers["content-type"] = "application/json";
125
+ headers["content-length"] = String(Buffer.byteLength(request.body, "utf-8"));
126
+ }
127
+ const base = {
128
+ method: request.method,
129
+ path: `${url.pathname}${url.search}`,
130
+ headers,
131
+ signal: request.signal,
132
+ setHost: false,
133
+ };
134
+ const seam = options.createConnection;
135
+ const requestOptions = seam
136
+ ? { ...base, createConnection: () => seam({ address: pinned, port, hostname }) }
137
+ : {
138
+ ...base,
139
+ host: pinned,
140
+ port,
141
+ agent: false,
142
+ // Fail closed if anything below ever tries to resolve a name again.
143
+ lookup: (_hostname, lookupOptions, callback) => {
144
+ const done = typeof lookupOptions === "function" ? lookupOptions : callback;
145
+ done(new Error(RESOLUTION_BLOCKED));
146
+ },
147
+ ...(isHttps
148
+ ? {
149
+ servername: isIP(hostname) === 0 ? hostname : "",
150
+ checkServerIdentity: (_host, certificate) => checkServerIdentity(hostname, certificate),
151
+ }
152
+ : {}),
153
+ };
154
+ assertRequestActive(request.signal);
155
+ return new Promise((resolve, reject) => {
156
+ let settled = false;
157
+ const settle = (action) => {
158
+ if (settled)
159
+ return;
160
+ settled = true;
161
+ action();
162
+ };
163
+ if (request.signal.aborted) {
164
+ settle(() => reject(new Error(REQUEST_ABORTED)));
165
+ return;
166
+ }
167
+ const clientRequest = (isHttps ? httpsRequest : httpRequest)(requestOptions, (response) => {
168
+ const status = response.statusCode ?? 0;
169
+ // Drop the response immediately: the doctor only needs the status,
170
+ // so no body is ever read or buffered (bounded by construction).
171
+ response.destroy();
172
+ settle(() => resolve({ status }));
173
+ });
174
+ clientRequest.on("error", () => settle(() => reject(new Error(REQUEST_FAILED))));
175
+ if (request.signal.aborted) {
176
+ clientRequest.destroy();
177
+ settle(() => reject(new Error(REQUEST_ABORTED)));
178
+ return;
179
+ }
180
+ clientRequest.end(isPost ? request.body : undefined);
181
+ });
182
+ },
183
+ };
184
+ }
185
+ //# sourceMappingURL=doctor-provider-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-provider-transport.js","sourceRoot":"","sources":["../../src/commands/doctor-provider-transport.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * First-party Level-1/Level-2 transport for the provider doctor.\n *\n * Safety properties:\n * - GET, plus bounded JSON POST for the opt-in Level-2 model probe; redirects are\n * never followed (the raw 3xx status is returned).\n * - POST bodies are capped, must be JSON, and are never echoed anywhere.\n * - The response is dropped as soon as the status line arrives: no body, headers,\n * or error details ever leave this module, and thrown errors use fixed messages.\n * - Loopback-only policies require every resolved address to be loopback.\n * - Public policies validate every resolved address against the doctor blocklist and\n * pin the socket to one validated address; the hostname is never re-resolved\n * (DNS-rebinding safe) and TLS identity is still checked against the original hostname.\n */\n\nimport { lookup as dnsLookup } from \"node:dns/promises\";\nimport { request as httpRequest, type RequestOptions } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { isIP, type Socket } from \"node:net\";\nimport { checkServerIdentity } from \"node:tls\";\nimport {\n\tisBlockedPublicAddress,\n\tisLoopbackAddress,\n\tnormalizedUrlHostname,\n\tProviderDoctorAuthMaterializationError,\n\ttype ProviderDoctorTransport,\n\ttype ProviderDoctorTransportRequest,\n\ttype ProviderDoctorTransportResponse,\n} from \"./doctor-provider.ts\";\n\n/** Resolves a hostname to every address it maps to. */\nexport type ProviderDoctorLookup = (hostname: string) => Promise<readonly string[]>;\n\n/** The validated connection target handed to the test-only socket seam. */\nexport interface ProviderDoctorPinnedConnection {\n\taddress: string;\n\tport: number;\n\thostname: string;\n}\n\nexport interface ProviderDoctorTransportOptions {\n\t/** DNS resolver returning every address for a hostname. Defaults to node:dns lookup({ all: true }). */\n\tlookup?: ProviderDoctorLookup;\n\t/**\n\t * Test-only seam (plain http) that builds the socket for a validated, pinned address.\n\t * Production requests always dial the pinned address directly.\n\t */\n\tcreateConnection?: (pinned: ProviderDoctorPinnedConnection) => Socket;\n}\n\nconst SCHEME_UNSUPPORTED = \"provider doctor transport rejected an unsupported URL scheme\";\nconst METHOD_UNSUPPORTED = \"provider doctor transport only performs GET and bounded POST requests\";\nconst REQUEST_SHAPE_INVALID = \"provider doctor transport rejected an invalid request shape\";\nconst MAX_POST_BODY_BYTES = 4_096;\nconst RESOLUTION_FAILED = \"provider doctor transport could not resolve the endpoint hostname\";\nconst RESOLUTION_BLOCKED = \"provider doctor transport blocked hostname re-resolution\";\nconst ADDRESS_BLOCKED = \"provider doctor transport blocked a resolved address by policy\";\nconst REQUEST_FAILED = \"provider doctor transport request failed\";\nconst REQUEST_ABORTED = \"provider doctor transport request was aborted\";\nconst STRIPPED_REQUEST_HEADERS = new Set([\n\t\"connection\",\n\t\"content-length\",\n\t\"expect\",\n\t\"host\",\n\t\"keep-alive\",\n\t\"proxy-authenticate\",\n\t\"proxy-authorization\",\n\t\"proxy-connection\",\n\t\"te\",\n\t\"trailer\",\n\t\"transfer-encoding\",\n\t\"upgrade\",\n]);\nconst SINGLE_VALUE_CREDENTIAL_HEADERS = new Set([\"authorization\", \"cf-aig-authorization\"]);\nconst ADDITIONAL_AUTHORIZATION_VALUE = /,\\s*[!#$%&'*+.^_`|~0-9A-Za-z-]+\\s+/;\n\nconst defaultLookup: ProviderDoctorLookup = async (hostname) => {\n\tconst results = await dnsLookup(hostname, { all: true, verbatim: true });\n\treturn results.map((result) => result.address);\n};\n\nasync function resolveAddresses(hostname: string, lookup: ProviderDoctorLookup): Promise<readonly string[]> {\n\tif (isIP(hostname) !== 0) return [hostname];\n\tlet addresses: readonly string[];\n\ttry {\n\t\taddresses = await lookup(hostname);\n\t} catch {\n\t\tthrow new Error(RESOLUTION_FAILED);\n\t}\n\tif (addresses.length === 0) throw new Error(RESOLUTION_FAILED);\n\treturn addresses;\n}\n\nfunction assertRequestActive(signal: AbortSignal): void {\n\tif (signal.aborted) throw new Error(REQUEST_ABORTED);\n}\n\nfunction assertAddressesAllowed(\n\taddresses: readonly string[],\n\tpolicy: ProviderDoctorTransportRequest[\"addressPolicy\"],\n): void {\n\tfor (const address of addresses) {\n\t\tconst allowed = policy.kind === \"loopback-only\" ? isLoopbackAddress(address) : !isBlockedPublicAddress(address);\n\t\tif (!allowed) throw new Error(ADDRESS_BLOCKED);\n\t}\n}\n\nexport function createProviderDoctorTransport(options: ProviderDoctorTransportOptions = {}): ProviderDoctorTransport {\n\tconst lookup = options.lookup ?? defaultLookup;\n\n\treturn {\n\t\tpinsResolvedAddress: true,\n\t\tasync request(request: ProviderDoctorTransportRequest): Promise<ProviderDoctorTransportResponse> {\n\t\t\tassertRequestActive(request.signal);\n\t\t\tconst isPost = request.method === \"POST\";\n\t\t\tif (request.method !== \"GET\" && !isPost) throw new Error(METHOD_UNSUPPORTED);\n\t\t\tif (!isPost && request.body !== undefined) throw new Error(REQUEST_SHAPE_INVALID);\n\t\t\tif (isPost) {\n\t\t\t\tif (typeof request.body !== \"string\" || request.contentType !== \"application/json\") {\n\t\t\t\t\tthrow new Error(REQUEST_SHAPE_INVALID);\n\t\t\t\t}\n\t\t\t\tif (Buffer.byteLength(request.body, \"utf-8\") > MAX_POST_BODY_BYTES) {\n\t\t\t\t\tthrow new Error(REQUEST_SHAPE_INVALID);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst url = request.url;\n\t\t\tconst isHttps = url.protocol === \"https:\";\n\t\t\tif (!isHttps && url.protocol !== \"http:\") throw new Error(SCHEME_UNSUPPORTED);\n\n\t\t\tconst hostname = normalizedUrlHostname(url);\n\t\t\tconst addresses = await resolveAddresses(hostname, lookup);\n\t\t\tassertAddressesAllowed(addresses, request.addressPolicy);\n\t\t\tassertRequestActive(request.signal);\n\t\t\tconst pinned = addresses[0];\n\t\t\tconst port = url.port.length > 0 ? Number(url.port) : isHttps ? 443 : 80;\n\n\t\t\tlet materializedHeaders: Headers;\n\t\t\ttry {\n\t\t\t\tmaterializedHeaders = await request.createHeaders();\n\t\t\t} catch {\n\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t}\n\t\t\tassertRequestActive(request.signal);\n\t\t\tconst headers: Record<string, string> = {};\n\t\t\tfor (const [name, value] of materializedHeaders) {\n\t\t\t\tconst normalizedName = name.toLowerCase();\n\t\t\t\tif (STRIPPED_REQUEST_HEADERS.has(normalizedName)) continue;\n\t\t\t\tif (SINGLE_VALUE_CREDENTIAL_HEADERS.has(normalizedName) && ADDITIONAL_AUTHORIZATION_VALUE.test(value)) {\n\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t}\n\t\t\t\theaders[normalizedName] = value;\n\t\t\t}\n\t\t\theaders.host = url.host;\n\t\t\tif (isPost) {\n\t\t\t\theaders[\"content-type\"] = \"application/json\";\n\t\t\t\theaders[\"content-length\"] = String(Buffer.byteLength(request.body as string, \"utf-8\"));\n\t\t\t}\n\n\t\t\tconst base: RequestOptions = {\n\t\t\t\tmethod: request.method,\n\t\t\t\tpath: `${url.pathname}${url.search}`,\n\t\t\t\theaders,\n\t\t\t\tsignal: request.signal,\n\t\t\t\tsetHost: false,\n\t\t\t};\n\t\t\tconst seam = options.createConnection;\n\t\t\tconst requestOptions: RequestOptions = seam\n\t\t\t\t? { ...base, createConnection: () => seam({ address: pinned, port, hostname }) }\n\t\t\t\t: {\n\t\t\t\t\t\t...base,\n\t\t\t\t\t\thost: pinned,\n\t\t\t\t\t\tport,\n\t\t\t\t\t\tagent: false,\n\t\t\t\t\t\t// Fail closed if anything below ever tries to resolve a name again.\n\t\t\t\t\t\tlookup: (_hostname, lookupOptions, callback) => {\n\t\t\t\t\t\t\tconst done = typeof lookupOptions === \"function\" ? lookupOptions : callback;\n\t\t\t\t\t\t\t(done as (error: Error) => void)(new Error(RESOLUTION_BLOCKED));\n\t\t\t\t\t\t},\n\t\t\t\t\t\t...(isHttps\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tservername: isIP(hostname) === 0 ? hostname : \"\",\n\t\t\t\t\t\t\t\t\tcheckServerIdentity: (\n\t\t\t\t\t\t\t\t\t\t_host: string,\n\t\t\t\t\t\t\t\t\t\tcertificate: Parameters<typeof checkServerIdentity>[1],\n\t\t\t\t\t\t\t\t\t) => checkServerIdentity(hostname, certificate),\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t};\n\n\t\t\tassertRequestActive(request.signal);\n\t\t\treturn new Promise<ProviderDoctorTransportResponse>((resolve, reject) => {\n\t\t\t\tlet settled = false;\n\t\t\t\tconst settle = (action: () => void): void => {\n\t\t\t\t\tif (settled) return;\n\t\t\t\t\tsettled = true;\n\t\t\t\t\taction();\n\t\t\t\t};\n\t\t\t\tif (request.signal.aborted) {\n\t\t\t\t\tsettle(() => reject(new Error(REQUEST_ABORTED)));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst clientRequest = (isHttps ? httpsRequest : httpRequest)(requestOptions, (response) => {\n\t\t\t\t\tconst status = response.statusCode ?? 0;\n\t\t\t\t\t// Drop the response immediately: the doctor only needs the status,\n\t\t\t\t\t// so no body is ever read or buffered (bounded by construction).\n\t\t\t\t\tresponse.destroy();\n\t\t\t\t\tsettle(() => resolve({ status }));\n\t\t\t\t});\n\t\t\t\tclientRequest.on(\"error\", () => settle(() => reject(new Error(REQUEST_FAILED))));\n\t\t\t\tif (request.signal.aborted) {\n\t\t\t\t\tclientRequest.destroy();\n\t\t\t\t\tsettle(() => reject(new Error(REQUEST_ABORTED)));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tclientRequest.end(isPost ? (request.body as string) : undefined);\n\t\t\t});\n\t\t},\n\t};\n}\n"]}