dedalus-cli 0.0.0-stage → 0.7.0

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 (684) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +296 -2
  3. package/SKILL.md +73 -0
  4. package/api.md +340 -0
  5. package/dist/cjs/bin.d.ts +3 -0
  6. package/dist/cjs/bin.d.ts.map +1 -0
  7. package/dist/cjs/bin.js +7 -0
  8. package/dist/cjs/bin.js.map +1 -0
  9. package/dist/cjs/cli/completions.d.ts +6 -0
  10. package/dist/cjs/cli/completions.d.ts.map +1 -0
  11. package/dist/cjs/cli/completions.js +11 -0
  12. package/dist/cjs/cli/completions.js.map +1 -0
  13. package/dist/cjs/cli/credentials.d.ts +169 -0
  14. package/dist/cjs/cli/credentials.d.ts.map +1 -0
  15. package/dist/cjs/cli/credentials.js +630 -0
  16. package/dist/cjs/cli/credentials.js.map +1 -0
  17. package/dist/cjs/cli/keychain.d.ts +34 -0
  18. package/dist/cjs/cli/keychain.d.ts.map +1 -0
  19. package/dist/cjs/cli/keychain.js +246 -0
  20. package/dist/cjs/cli/keychain.js.map +1 -0
  21. package/dist/cjs/cli/list.d.ts +23 -0
  22. package/dist/cjs/cli/list.d.ts.map +1 -0
  23. package/dist/cjs/cli/list.js +32 -0
  24. package/dist/cjs/cli/list.js.map +1 -0
  25. package/dist/cjs/cli/login.d.ts +137 -0
  26. package/dist/cjs/cli/login.d.ts.map +1 -0
  27. package/dist/cjs/cli/login.js +2060 -0
  28. package/dist/cjs/cli/login.js.map +1 -0
  29. package/dist/cjs/cli/runtime.d.ts +98 -0
  30. package/dist/cjs/cli/runtime.d.ts.map +1 -0
  31. package/dist/cjs/cli/runtime.js +1356 -0
  32. package/dist/cjs/cli/runtime.js.map +1 -0
  33. package/dist/cjs/cli/toon.d.ts +6 -0
  34. package/dist/cjs/cli/toon.d.ts.map +1 -0
  35. package/dist/cjs/cli/toon.js +165 -0
  36. package/dist/cjs/cli/toon.js.map +1 -0
  37. package/dist/cjs/cli/upgrade.d.ts +28 -0
  38. package/dist/cjs/cli/upgrade.d.ts.map +1 -0
  39. package/dist/cjs/cli/upgrade.js +1191 -0
  40. package/dist/cjs/cli/upgrade.js.map +1 -0
  41. package/dist/cjs/commands/index.d.ts +47 -0
  42. package/dist/cjs/commands/index.d.ts.map +1 -0
  43. package/dist/cjs/commands/index.js +1202 -0
  44. package/dist/cjs/commands/index.js.map +1 -0
  45. package/dist/cjs/custom/browser-routing.d.ts +4 -0
  46. package/dist/cjs/custom/browser-routing.d.ts.map +1 -0
  47. package/dist/cjs/custom/browser-routing.js +51 -0
  48. package/dist/cjs/custom/browser-routing.js.map +1 -0
  49. package/dist/cjs/custom/images.d.ts +6 -0
  50. package/dist/cjs/custom/images.d.ts.map +1 -0
  51. package/dist/cjs/custom/images.js +74 -0
  52. package/dist/cjs/custom/images.js.map +1 -0
  53. package/dist/cjs/custom/machines.d.ts +20 -0
  54. package/dist/cjs/custom/machines.d.ts.map +1 -0
  55. package/dist/cjs/custom/machines.js +84 -0
  56. package/dist/cjs/custom/machines.js.map +1 -0
  57. package/dist/cjs/custom/ssh-picker.d.ts +19 -0
  58. package/dist/cjs/custom/ssh-picker.d.ts.map +1 -0
  59. package/dist/cjs/custom/ssh-picker.js +157 -0
  60. package/dist/cjs/custom/ssh-picker.js.map +1 -0
  61. package/dist/cjs/custom/ssh.d.ts +18 -0
  62. package/dist/cjs/custom/ssh.d.ts.map +1 -0
  63. package/dist/cjs/custom/ssh.js +152 -0
  64. package/dist/cjs/custom/ssh.js.map +1 -0
  65. package/dist/cjs/feedback/bundle.d.ts +41 -0
  66. package/dist/cjs/feedback/bundle.d.ts.map +1 -0
  67. package/dist/cjs/feedback/bundle.js +88 -0
  68. package/dist/cjs/feedback/bundle.js.map +1 -0
  69. package/dist/cjs/feedback/command.d.ts +6 -0
  70. package/dist/cjs/feedback/command.d.ts.map +1 -0
  71. package/dist/cjs/feedback/command.js +100 -0
  72. package/dist/cjs/feedback/command.js.map +1 -0
  73. package/dist/cjs/feedback/diagnostics.d.ts +30 -0
  74. package/dist/cjs/feedback/diagnostics.d.ts.map +1 -0
  75. package/dist/cjs/feedback/diagnostics.js +278 -0
  76. package/dist/cjs/feedback/diagnostics.js.map +1 -0
  77. package/dist/cjs/index.d.ts +4 -0
  78. package/dist/cjs/index.d.ts.map +1 -0
  79. package/dist/cjs/index.js +11 -0
  80. package/dist/cjs/index.js.map +1 -0
  81. package/dist/cjs/package.json +3 -0
  82. package/dist/cjs/sdk/api-promise.d.ts +31 -0
  83. package/dist/cjs/sdk/api-promise.d.ts.map +1 -0
  84. package/dist/cjs/sdk/api-promise.js +73 -0
  85. package/dist/cjs/sdk/api-promise.js.map +1 -0
  86. package/dist/cjs/sdk/client.d.ts +267 -0
  87. package/dist/cjs/sdk/client.d.ts.map +1 -0
  88. package/dist/cjs/sdk/client.js +965 -0
  89. package/dist/cjs/sdk/client.js.map +1 -0
  90. package/dist/cjs/sdk/core/api-promise.d.ts +3 -0
  91. package/dist/cjs/sdk/core/api-promise.d.ts.map +1 -0
  92. package/dist/cjs/sdk/core/api-promise.js +8 -0
  93. package/dist/cjs/sdk/core/api-promise.js.map +1 -0
  94. package/dist/cjs/sdk/core/error.d.ts +46 -0
  95. package/dist/cjs/sdk/core/error.d.ts.map +1 -0
  96. package/dist/cjs/sdk/core/error.js +135 -0
  97. package/dist/cjs/sdk/core/error.js.map +1 -0
  98. package/dist/cjs/sdk/core/pagination.d.ts +38 -0
  99. package/dist/cjs/sdk/core/pagination.d.ts.map +1 -0
  100. package/dist/cjs/sdk/core/pagination.js +98 -0
  101. package/dist/cjs/sdk/core/pagination.js.map +1 -0
  102. package/dist/cjs/sdk/core/resource.d.ts +2 -0
  103. package/dist/cjs/sdk/core/resource.d.ts.map +1 -0
  104. package/dist/cjs/sdk/core/resource.js +7 -0
  105. package/dist/cjs/sdk/core/resource.js.map +1 -0
  106. package/dist/cjs/sdk/core/uploads.d.ts +3 -0
  107. package/dist/cjs/sdk/core/uploads.d.ts.map +1 -0
  108. package/dist/cjs/sdk/core/uploads.js +7 -0
  109. package/dist/cjs/sdk/core/uploads.js.map +1 -0
  110. package/dist/cjs/sdk/error.d.ts +3 -0
  111. package/dist/cjs/sdk/error.d.ts.map +1 -0
  112. package/dist/cjs/sdk/error.js +20 -0
  113. package/dist/cjs/sdk/error.js.map +1 -0
  114. package/dist/cjs/sdk/helpers/images.d.ts +31 -0
  115. package/dist/cjs/sdk/helpers/images.d.ts.map +1 -0
  116. package/dist/cjs/sdk/helpers/images.js +195 -0
  117. package/dist/cjs/sdk/helpers/images.js.map +1 -0
  118. package/dist/cjs/sdk/index.d.ts +7 -0
  119. package/dist/cjs/sdk/index.d.ts.map +1 -0
  120. package/dist/cjs/sdk/index.js +29 -0
  121. package/dist/cjs/sdk/index.js.map +1 -0
  122. package/dist/cjs/sdk/internal/builtin-types.d.ts +73 -0
  123. package/dist/cjs/sdk/internal/builtin-types.d.ts.map +1 -0
  124. package/dist/cjs/sdk/internal/builtin-types.js +4 -0
  125. package/dist/cjs/sdk/internal/builtin-types.js.map +1 -0
  126. package/dist/cjs/sdk/internal/detect-platform.d.ts +15 -0
  127. package/dist/cjs/sdk/internal/detect-platform.d.ts.map +1 -0
  128. package/dist/cjs/sdk/internal/detect-platform.js +185 -0
  129. package/dist/cjs/sdk/internal/detect-platform.js.map +1 -0
  130. package/dist/cjs/sdk/internal/errors.d.ts +3 -0
  131. package/dist/cjs/sdk/internal/errors.d.ts.map +1 -0
  132. package/dist/cjs/sdk/internal/errors.js +41 -0
  133. package/dist/cjs/sdk/internal/errors.js.map +1 -0
  134. package/dist/cjs/sdk/internal/headers.d.ts +43 -0
  135. package/dist/cjs/sdk/internal/headers.d.ts.map +1 -0
  136. package/dist/cjs/sdk/internal/headers.js +142 -0
  137. package/dist/cjs/sdk/internal/headers.js.map +1 -0
  138. package/dist/cjs/sdk/internal/parse.d.ts +3 -0
  139. package/dist/cjs/sdk/internal/parse.d.ts.map +1 -0
  140. package/dist/cjs/sdk/internal/parse.js +7 -0
  141. package/dist/cjs/sdk/internal/parse.js.map +1 -0
  142. package/dist/cjs/sdk/internal/qs/formats.d.ts +7 -0
  143. package/dist/cjs/sdk/internal/qs/formats.d.ts.map +1 -0
  144. package/dist/cjs/sdk/internal/qs/formats.js +13 -0
  145. package/dist/cjs/sdk/internal/qs/formats.js.map +1 -0
  146. package/dist/cjs/sdk/internal/qs/index.d.ts +10 -0
  147. package/dist/cjs/sdk/internal/qs/index.d.ts.map +1 -0
  148. package/dist/cjs/sdk/internal/qs/index.js +14 -0
  149. package/dist/cjs/sdk/internal/qs/index.js.map +1 -0
  150. package/dist/cjs/sdk/internal/qs/stringify.d.ts +3 -0
  151. package/dist/cjs/sdk/internal/qs/stringify.d.ts.map +1 -0
  152. package/dist/cjs/sdk/internal/qs/stringify.js +278 -0
  153. package/dist/cjs/sdk/internal/qs/stringify.js.map +1 -0
  154. package/dist/cjs/sdk/internal/qs/types.d.ts +57 -0
  155. package/dist/cjs/sdk/internal/qs/types.d.ts.map +1 -0
  156. package/dist/cjs/sdk/internal/qs/types.js +3 -0
  157. package/dist/cjs/sdk/internal/qs/types.js.map +1 -0
  158. package/dist/cjs/sdk/internal/qs/utils.d.ts +15 -0
  159. package/dist/cjs/sdk/internal/qs/utils.d.ts.map +1 -0
  160. package/dist/cjs/sdk/internal/qs/utils.js +230 -0
  161. package/dist/cjs/sdk/internal/qs/utils.js.map +1 -0
  162. package/dist/cjs/sdk/internal/request-options.d.ts +33 -0
  163. package/dist/cjs/sdk/internal/request-options.d.ts.map +1 -0
  164. package/dist/cjs/sdk/internal/request-options.js +15 -0
  165. package/dist/cjs/sdk/internal/request-options.js.map +1 -0
  166. package/dist/cjs/sdk/internal/shim-types.d.ts +17 -0
  167. package/dist/cjs/sdk/internal/shim-types.d.ts.map +1 -0
  168. package/dist/cjs/sdk/internal/shim-types.js +4 -0
  169. package/dist/cjs/sdk/internal/shim-types.js.map +1 -0
  170. package/dist/cjs/sdk/internal/shims.d.ts +26 -0
  171. package/dist/cjs/sdk/internal/shims.d.ts.map +1 -0
  172. package/dist/cjs/sdk/internal/shims.js +92 -0
  173. package/dist/cjs/sdk/internal/shims.js.map +1 -0
  174. package/dist/cjs/sdk/internal/to-file.d.ts +45 -0
  175. package/dist/cjs/sdk/internal/to-file.d.ts.map +1 -0
  176. package/dist/cjs/sdk/internal/to-file.js +91 -0
  177. package/dist/cjs/sdk/internal/to-file.js.map +1 -0
  178. package/dist/cjs/sdk/internal/types.d.ts +63 -0
  179. package/dist/cjs/sdk/internal/types.d.ts.map +1 -0
  180. package/dist/cjs/sdk/internal/types.js +4 -0
  181. package/dist/cjs/sdk/internal/types.js.map +1 -0
  182. package/dist/cjs/sdk/internal/uploads.d.ts +42 -0
  183. package/dist/cjs/sdk/internal/uploads.d.ts.map +1 -0
  184. package/dist/cjs/sdk/internal/uploads.js +158 -0
  185. package/dist/cjs/sdk/internal/uploads.js.map +1 -0
  186. package/dist/cjs/sdk/internal/utils/base64.d.ts +3 -0
  187. package/dist/cjs/sdk/internal/utils/base64.d.ts.map +1 -0
  188. package/dist/cjs/sdk/internal/utils/base64.js +38 -0
  189. package/dist/cjs/sdk/internal/utils/base64.js.map +1 -0
  190. package/dist/cjs/sdk/internal/utils/body.d.ts +40 -0
  191. package/dist/cjs/sdk/internal/utils/body.d.ts.map +1 -0
  192. package/dist/cjs/sdk/internal/utils/body.js +148 -0
  193. package/dist/cjs/sdk/internal/utils/body.js.map +1 -0
  194. package/dist/cjs/sdk/internal/utils/bytes.d.ts +4 -0
  195. package/dist/cjs/sdk/internal/utils/bytes.d.ts.map +1 -0
  196. package/dist/cjs/sdk/internal/utils/bytes.js +31 -0
  197. package/dist/cjs/sdk/internal/utils/bytes.js.map +1 -0
  198. package/dist/cjs/sdk/internal/utils/env.d.ts +9 -0
  199. package/dist/cjs/sdk/internal/utils/env.d.ts.map +1 -0
  200. package/dist/cjs/sdk/internal/utils/env.js +22 -0
  201. package/dist/cjs/sdk/internal/utils/env.js.map +1 -0
  202. package/dist/cjs/sdk/internal/utils/log.d.ts +52 -0
  203. package/dist/cjs/sdk/internal/utils/log.d.ts.map +1 -0
  204. package/dist/cjs/sdk/internal/utils/log.js +413 -0
  205. package/dist/cjs/sdk/internal/utils/log.js.map +1 -0
  206. package/dist/cjs/sdk/internal/utils/path.d.ts +41 -0
  207. package/dist/cjs/sdk/internal/utils/path.d.ts.map +1 -0
  208. package/dist/cjs/sdk/internal/utils/path.js +115 -0
  209. package/dist/cjs/sdk/internal/utils/path.js.map +1 -0
  210. package/dist/cjs/sdk/internal/utils/query-string.d.ts +21 -0
  211. package/dist/cjs/sdk/internal/utils/query-string.d.ts.map +1 -0
  212. package/dist/cjs/sdk/internal/utils/query-string.js +128 -0
  213. package/dist/cjs/sdk/internal/utils/query-string.js.map +1 -0
  214. package/dist/cjs/sdk/internal/utils/query.d.ts +2 -0
  215. package/dist/cjs/sdk/internal/utils/query.d.ts.map +1 -0
  216. package/dist/cjs/sdk/internal/utils/query.js +12 -0
  217. package/dist/cjs/sdk/internal/utils/query.js.map +1 -0
  218. package/dist/cjs/sdk/internal/utils/sleep.d.ts +2 -0
  219. package/dist/cjs/sdk/internal/utils/sleep.d.ts.map +1 -0
  220. package/dist/cjs/sdk/internal/utils/sleep.js +7 -0
  221. package/dist/cjs/sdk/internal/utils/sleep.js.map +1 -0
  222. package/dist/cjs/sdk/internal/utils/uuid.d.ts +5 -0
  223. package/dist/cjs/sdk/internal/utils/uuid.d.ts.map +1 -0
  224. package/dist/cjs/sdk/internal/utils/uuid.js +19 -0
  225. package/dist/cjs/sdk/internal/utils/uuid.js.map +1 -0
  226. package/dist/cjs/sdk/internal/utils/values.d.ts +18 -0
  227. package/dist/cjs/sdk/internal/utils/values.d.ts.map +1 -0
  228. package/dist/cjs/sdk/internal/utils/values.js +112 -0
  229. package/dist/cjs/sdk/internal/utils/values.js.map +1 -0
  230. package/dist/cjs/sdk/internal/utils.d.ts +8 -0
  231. package/dist/cjs/sdk/internal/utils.d.ts.map +1 -0
  232. package/dist/cjs/sdk/internal/utils.js +25 -0
  233. package/dist/cjs/sdk/internal/utils.js.map +1 -0
  234. package/dist/cjs/sdk/pagination.d.ts +3 -0
  235. package/dist/cjs/sdk/pagination.d.ts.map +1 -0
  236. package/dist/cjs/sdk/pagination.js +20 -0
  237. package/dist/cjs/sdk/pagination.js.map +1 -0
  238. package/dist/cjs/sdk/resource.d.ts +6 -0
  239. package/dist/cjs/sdk/resource.d.ts.map +1 -0
  240. package/dist/cjs/sdk/resource.js +12 -0
  241. package/dist/cjs/sdk/resource.js.map +1 -0
  242. package/dist/cjs/sdk/resources/images/images.d.ts +202 -0
  243. package/dist/cjs/sdk/resources/images/images.d.ts.map +1 -0
  244. package/dist/cjs/sdk/resources/images/images.js +139 -0
  245. package/dist/cjs/sdk/resources/images/images.js.map +1 -0
  246. package/dist/cjs/sdk/resources/images/index.d.ts +5 -0
  247. package/dist/cjs/sdk/resources/images/index.d.ts.map +1 -0
  248. package/dist/cjs/sdk/resources/images/index.js +9 -0
  249. package/dist/cjs/sdk/resources/images/index.js.map +1 -0
  250. package/dist/cjs/sdk/resources/images/uploads.d.ts +50 -0
  251. package/dist/cjs/sdk/resources/images/uploads.d.ts.map +1 -0
  252. package/dist/cjs/sdk/resources/images/uploads.js +28 -0
  253. package/dist/cjs/sdk/resources/images/uploads.js.map +1 -0
  254. package/dist/cjs/sdk/resources/images.d.ts +2 -0
  255. package/dist/cjs/sdk/resources/images.d.ts.map +1 -0
  256. package/dist/cjs/sdk/resources/images.js +19 -0
  257. package/dist/cjs/sdk/resources/images.js.map +1 -0
  258. package/dist/cjs/sdk/resources/index.d.ts +6 -0
  259. package/dist/cjs/sdk/resources/index.d.ts.map +1 -0
  260. package/dist/cjs/sdk/resources/index.js +11 -0
  261. package/dist/cjs/sdk/resources/index.js.map +1 -0
  262. package/dist/cjs/sdk/resources/machines/autoresizing.d.ts +115 -0
  263. package/dist/cjs/sdk/resources/machines/autoresizing.d.ts.map +1 -0
  264. package/dist/cjs/sdk/resources/machines/autoresizing.js +46 -0
  265. package/dist/cjs/sdk/resources/machines/autoresizing.js.map +1 -0
  266. package/dist/cjs/sdk/resources/machines/executions/executions.d.ts +341 -0
  267. package/dist/cjs/sdk/resources/machines/executions/executions.d.ts.map +1 -0
  268. package/dist/cjs/sdk/resources/machines/executions/executions.js +161 -0
  269. package/dist/cjs/sdk/resources/machines/executions/executions.js.map +1 -0
  270. package/dist/cjs/sdk/resources/machines/executions/index.d.ts +5 -0
  271. package/dist/cjs/sdk/resources/machines/executions/index.d.ts.map +1 -0
  272. package/dist/cjs/sdk/resources/machines/executions/index.js +9 -0
  273. package/dist/cjs/sdk/resources/machines/executions/index.js.map +1 -0
  274. package/dist/cjs/sdk/resources/machines/executions/logs.d.ts +103 -0
  275. package/dist/cjs/sdk/resources/machines/executions/logs.d.ts.map +1 -0
  276. package/dist/cjs/sdk/resources/machines/executions/logs.js +48 -0
  277. package/dist/cjs/sdk/resources/machines/executions/logs.js.map +1 -0
  278. package/dist/cjs/sdk/resources/machines/executions.d.ts +2 -0
  279. package/dist/cjs/sdk/resources/machines/executions.d.ts.map +1 -0
  280. package/dist/cjs/sdk/resources/machines/executions.js +19 -0
  281. package/dist/cjs/sdk/resources/machines/executions.js.map +1 -0
  282. package/dist/cjs/sdk/resources/machines/index.d.ts +9 -0
  283. package/dist/cjs/sdk/resources/machines/index.d.ts.map +1 -0
  284. package/dist/cjs/sdk/resources/machines/index.js +13 -0
  285. package/dist/cjs/sdk/resources/machines/index.js.map +1 -0
  286. package/dist/cjs/sdk/resources/machines/machines.d.ts +496 -0
  287. package/dist/cjs/sdk/resources/machines/machines.d.ts.map +1 -0
  288. package/dist/cjs/sdk/resources/machines/machines.js +220 -0
  289. package/dist/cjs/sdk/resources/machines/machines.js.map +1 -0
  290. package/dist/cjs/sdk/resources/machines/ssh.d.ts +175 -0
  291. package/dist/cjs/sdk/resources/machines/ssh.d.ts.map +1 -0
  292. package/dist/cjs/sdk/resources/machines/ssh.js +89 -0
  293. package/dist/cjs/sdk/resources/machines/ssh.js.map +1 -0
  294. package/dist/cjs/sdk/resources/machines.d.ts +2 -0
  295. package/dist/cjs/sdk/resources/machines.d.ts.map +1 -0
  296. package/dist/cjs/sdk/resources/machines.js +19 -0
  297. package/dist/cjs/sdk/resources/machines.js.map +1 -0
  298. package/dist/cjs/sdk/resources/organization/autoresizing.d.ts +86 -0
  299. package/dist/cjs/sdk/resources/organization/autoresizing.d.ts.map +1 -0
  300. package/dist/cjs/sdk/resources/organization/autoresizing.js +38 -0
  301. package/dist/cjs/sdk/resources/organization/autoresizing.js.map +1 -0
  302. package/dist/cjs/sdk/resources/organization/index.d.ts +4 -0
  303. package/dist/cjs/sdk/resources/organization/index.d.ts.map +1 -0
  304. package/dist/cjs/sdk/resources/organization/index.js +9 -0
  305. package/dist/cjs/sdk/resources/organization/index.js.map +1 -0
  306. package/dist/cjs/sdk/resources/organization/organization.d.ts +10 -0
  307. package/dist/cjs/sdk/resources/organization/organization.d.ts.map +1 -0
  308. package/dist/cjs/sdk/resources/organization/organization.js +46 -0
  309. package/dist/cjs/sdk/resources/organization/organization.js.map +1 -0
  310. package/dist/cjs/sdk/resources/organization.d.ts +2 -0
  311. package/dist/cjs/sdk/resources/organization.d.ts.map +1 -0
  312. package/dist/cjs/sdk/resources/organization.js +19 -0
  313. package/dist/cjs/sdk/resources/organization.js.map +1 -0
  314. package/dist/cjs/sdk/resources.d.ts +3 -0
  315. package/dist/cjs/sdk/resources.d.ts.map +1 -0
  316. package/dist/cjs/sdk/resources.js +9 -0
  317. package/dist/cjs/sdk/resources.js.map +1 -0
  318. package/dist/cjs/sdk/uploads.d.ts +2 -0
  319. package/dist/cjs/sdk/uploads.d.ts.map +1 -0
  320. package/dist/cjs/sdk/uploads.js +7 -0
  321. package/dist/cjs/sdk/uploads.js.map +1 -0
  322. package/dist/cjs/sdk/version.d.ts +2 -0
  323. package/dist/cjs/sdk/version.d.ts.map +1 -0
  324. package/dist/cjs/sdk/version.js +6 -0
  325. package/dist/cjs/sdk/version.js.map +1 -0
  326. package/dist/esm/bin.d.ts +3 -0
  327. package/dist/esm/bin.d.ts.map +1 -0
  328. package/dist/esm/bin.js +5 -0
  329. package/dist/esm/bin.js.map +1 -0
  330. package/dist/esm/cli/completions.d.ts +6 -0
  331. package/dist/esm/cli/completions.d.ts.map +1 -0
  332. package/dist/esm/cli/completions.js +8 -0
  333. package/dist/esm/cli/completions.js.map +1 -0
  334. package/dist/esm/cli/credentials.d.ts +169 -0
  335. package/dist/esm/cli/credentials.d.ts.map +1 -0
  336. package/dist/esm/cli/credentials.js +614 -0
  337. package/dist/esm/cli/credentials.js.map +1 -0
  338. package/dist/esm/cli/keychain.d.ts +34 -0
  339. package/dist/esm/cli/keychain.d.ts.map +1 -0
  340. package/dist/esm/cli/keychain.js +237 -0
  341. package/dist/esm/cli/keychain.js.map +1 -0
  342. package/dist/esm/cli/list.d.ts +23 -0
  343. package/dist/esm/cli/list.d.ts.map +1 -0
  344. package/dist/esm/cli/list.js +27 -0
  345. package/dist/esm/cli/list.js.map +1 -0
  346. package/dist/esm/cli/login.d.ts +137 -0
  347. package/dist/esm/cli/login.d.ts.map +1 -0
  348. package/dist/esm/cli/login.js +2053 -0
  349. package/dist/esm/cli/login.js.map +1 -0
  350. package/dist/esm/cli/runtime.d.ts +98 -0
  351. package/dist/esm/cli/runtime.d.ts.map +1 -0
  352. package/dist/esm/cli/runtime.js +1345 -0
  353. package/dist/esm/cli/runtime.js.map +1 -0
  354. package/dist/esm/cli/toon.d.ts +6 -0
  355. package/dist/esm/cli/toon.d.ts.map +1 -0
  356. package/dist/esm/cli/toon.js +161 -0
  357. package/dist/esm/cli/toon.js.map +1 -0
  358. package/dist/esm/cli/upgrade.d.ts +28 -0
  359. package/dist/esm/cli/upgrade.d.ts.map +1 -0
  360. package/dist/esm/cli/upgrade.js +1186 -0
  361. package/dist/esm/cli/upgrade.js.map +1 -0
  362. package/dist/esm/commands/index.d.ts +47 -0
  363. package/dist/esm/commands/index.d.ts.map +1 -0
  364. package/dist/esm/commands/index.js +1195 -0
  365. package/dist/esm/commands/index.js.map +1 -0
  366. package/dist/esm/custom/browser-routing.d.ts +4 -0
  367. package/dist/esm/custom/browser-routing.d.ts.map +1 -0
  368. package/dist/esm/custom/browser-routing.js +47 -0
  369. package/dist/esm/custom/browser-routing.js.map +1 -0
  370. package/dist/esm/custom/images.d.ts +6 -0
  371. package/dist/esm/custom/images.d.ts.map +1 -0
  372. package/dist/esm/custom/images.js +68 -0
  373. package/dist/esm/custom/images.js.map +1 -0
  374. package/dist/esm/custom/machines.d.ts +20 -0
  375. package/dist/esm/custom/machines.d.ts.map +1 -0
  376. package/dist/esm/custom/machines.js +76 -0
  377. package/dist/esm/custom/machines.js.map +1 -0
  378. package/dist/esm/custom/ssh-picker.d.ts +19 -0
  379. package/dist/esm/custom/ssh-picker.d.ts.map +1 -0
  380. package/dist/esm/custom/ssh-picker.js +149 -0
  381. package/dist/esm/custom/ssh-picker.js.map +1 -0
  382. package/dist/esm/custom/ssh.d.ts +18 -0
  383. package/dist/esm/custom/ssh.d.ts.map +1 -0
  384. package/dist/esm/custom/ssh.js +147 -0
  385. package/dist/esm/custom/ssh.js.map +1 -0
  386. package/dist/esm/feedback/bundle.d.ts +41 -0
  387. package/dist/esm/feedback/bundle.d.ts.map +1 -0
  388. package/dist/esm/feedback/bundle.js +83 -0
  389. package/dist/esm/feedback/bundle.js.map +1 -0
  390. package/dist/esm/feedback/command.d.ts +6 -0
  391. package/dist/esm/feedback/command.d.ts.map +1 -0
  392. package/dist/esm/feedback/command.js +92 -0
  393. package/dist/esm/feedback/command.js.map +1 -0
  394. package/dist/esm/feedback/diagnostics.d.ts +30 -0
  395. package/dist/esm/feedback/diagnostics.d.ts.map +1 -0
  396. package/dist/esm/feedback/diagnostics.js +270 -0
  397. package/dist/esm/feedback/diagnostics.js.map +1 -0
  398. package/dist/esm/index.d.ts +4 -0
  399. package/dist/esm/index.d.ts.map +1 -0
  400. package/dist/esm/index.js +7 -0
  401. package/dist/esm/index.js.map +1 -0
  402. package/dist/esm/sdk/api-promise.d.ts +31 -0
  403. package/dist/esm/sdk/api-promise.d.ts.map +1 -0
  404. package/dist/esm/sdk/api-promise.js +68 -0
  405. package/dist/esm/sdk/api-promise.js.map +1 -0
  406. package/dist/esm/sdk/client.d.ts +267 -0
  407. package/dist/esm/sdk/client.d.ts.map +1 -0
  408. package/dist/esm/sdk/client.js +928 -0
  409. package/dist/esm/sdk/client.js.map +1 -0
  410. package/dist/esm/sdk/core/api-promise.d.ts +3 -0
  411. package/dist/esm/sdk/core/api-promise.d.ts.map +1 -0
  412. package/dist/esm/sdk/core/api-promise.js +3 -0
  413. package/dist/esm/sdk/core/api-promise.js.map +1 -0
  414. package/dist/esm/sdk/core/error.d.ts +46 -0
  415. package/dist/esm/sdk/core/error.d.ts.map +1 -0
  416. package/dist/esm/sdk/core/error.js +119 -0
  417. package/dist/esm/sdk/core/error.js.map +1 -0
  418. package/dist/esm/sdk/core/pagination.d.ts +38 -0
  419. package/dist/esm/sdk/core/pagination.d.ts.map +1 -0
  420. package/dist/esm/sdk/core/pagination.js +92 -0
  421. package/dist/esm/sdk/core/pagination.js.map +1 -0
  422. package/dist/esm/sdk/core/resource.d.ts +2 -0
  423. package/dist/esm/sdk/core/resource.d.ts.map +1 -0
  424. package/dist/esm/sdk/core/resource.js +3 -0
  425. package/dist/esm/sdk/core/resource.js.map +1 -0
  426. package/dist/esm/sdk/core/uploads.d.ts +3 -0
  427. package/dist/esm/sdk/core/uploads.d.ts.map +1 -0
  428. package/dist/esm/sdk/core/uploads.js +3 -0
  429. package/dist/esm/sdk/core/uploads.js.map +1 -0
  430. package/dist/esm/sdk/error.d.ts +3 -0
  431. package/dist/esm/sdk/error.d.ts.map +1 -0
  432. package/dist/esm/sdk/error.js +4 -0
  433. package/dist/esm/sdk/error.js.map +1 -0
  434. package/dist/esm/sdk/helpers/images.d.ts +31 -0
  435. package/dist/esm/sdk/helpers/images.d.ts.map +1 -0
  436. package/dist/esm/sdk/helpers/images.js +189 -0
  437. package/dist/esm/sdk/helpers/images.js.map +1 -0
  438. package/dist/esm/sdk/index.d.ts +7 -0
  439. package/dist/esm/sdk/index.d.ts.map +1 -0
  440. package/dist/esm/sdk/index.js +8 -0
  441. package/dist/esm/sdk/index.js.map +1 -0
  442. package/dist/esm/sdk/internal/builtin-types.d.ts +73 -0
  443. package/dist/esm/sdk/internal/builtin-types.d.ts.map +1 -0
  444. package/dist/esm/sdk/internal/builtin-types.js +3 -0
  445. package/dist/esm/sdk/internal/builtin-types.js.map +1 -0
  446. package/dist/esm/sdk/internal/detect-platform.d.ts +15 -0
  447. package/dist/esm/sdk/internal/detect-platform.d.ts.map +1 -0
  448. package/dist/esm/sdk/internal/detect-platform.js +180 -0
  449. package/dist/esm/sdk/internal/detect-platform.js.map +1 -0
  450. package/dist/esm/sdk/internal/errors.d.ts +3 -0
  451. package/dist/esm/sdk/internal/errors.d.ts.map +1 -0
  452. package/dist/esm/sdk/internal/errors.js +36 -0
  453. package/dist/esm/sdk/internal/errors.js.map +1 -0
  454. package/dist/esm/sdk/internal/headers.d.ts +43 -0
  455. package/dist/esm/sdk/internal/headers.d.ts.map +1 -0
  456. package/dist/esm/sdk/internal/headers.js +133 -0
  457. package/dist/esm/sdk/internal/headers.js.map +1 -0
  458. package/dist/esm/sdk/internal/parse.d.ts +3 -0
  459. package/dist/esm/sdk/internal/parse.d.ts.map +1 -0
  460. package/dist/esm/sdk/internal/parse.js +3 -0
  461. package/dist/esm/sdk/internal/parse.js.map +1 -0
  462. package/dist/esm/sdk/internal/qs/formats.d.ts +7 -0
  463. package/dist/esm/sdk/internal/qs/formats.d.ts.map +1 -0
  464. package/dist/esm/sdk/internal/qs/formats.js +9 -0
  465. package/dist/esm/sdk/internal/qs/formats.js.map +1 -0
  466. package/dist/esm/sdk/internal/qs/index.d.ts +10 -0
  467. package/dist/esm/sdk/internal/qs/index.d.ts.map +1 -0
  468. package/dist/esm/sdk/internal/qs/index.js +10 -0
  469. package/dist/esm/sdk/internal/qs/index.js.map +1 -0
  470. package/dist/esm/sdk/internal/qs/stringify.d.ts +3 -0
  471. package/dist/esm/sdk/internal/qs/stringify.d.ts.map +1 -0
  472. package/dist/esm/sdk/internal/qs/stringify.js +275 -0
  473. package/dist/esm/sdk/internal/qs/stringify.js.map +1 -0
  474. package/dist/esm/sdk/internal/qs/types.d.ts +57 -0
  475. package/dist/esm/sdk/internal/qs/types.d.ts.map +1 -0
  476. package/dist/esm/sdk/internal/qs/types.js +2 -0
  477. package/dist/esm/sdk/internal/qs/types.js.map +1 -0
  478. package/dist/esm/sdk/internal/qs/utils.d.ts +15 -0
  479. package/dist/esm/sdk/internal/qs/utils.d.ts.map +1 -0
  480. package/dist/esm/sdk/internal/qs/utils.js +217 -0
  481. package/dist/esm/sdk/internal/qs/utils.js.map +1 -0
  482. package/dist/esm/sdk/internal/request-options.d.ts +33 -0
  483. package/dist/esm/sdk/internal/request-options.d.ts.map +1 -0
  484. package/dist/esm/sdk/internal/request-options.js +11 -0
  485. package/dist/esm/sdk/internal/request-options.js.map +1 -0
  486. package/dist/esm/sdk/internal/shim-types.d.ts +17 -0
  487. package/dist/esm/sdk/internal/shim-types.d.ts.map +1 -0
  488. package/dist/esm/sdk/internal/shim-types.js +3 -0
  489. package/dist/esm/sdk/internal/shim-types.js.map +1 -0
  490. package/dist/esm/sdk/internal/shims.d.ts +26 -0
  491. package/dist/esm/sdk/internal/shims.d.ts.map +1 -0
  492. package/dist/esm/sdk/internal/shims.js +85 -0
  493. package/dist/esm/sdk/internal/shims.js.map +1 -0
  494. package/dist/esm/sdk/internal/to-file.d.ts +45 -0
  495. package/dist/esm/sdk/internal/to-file.d.ts.map +1 -0
  496. package/dist/esm/sdk/internal/to-file.js +88 -0
  497. package/dist/esm/sdk/internal/to-file.js.map +1 -0
  498. package/dist/esm/sdk/internal/types.d.ts +63 -0
  499. package/dist/esm/sdk/internal/types.d.ts.map +1 -0
  500. package/dist/esm/sdk/internal/types.js +3 -0
  501. package/dist/esm/sdk/internal/types.js.map +1 -0
  502. package/dist/esm/sdk/internal/uploads.d.ts +42 -0
  503. package/dist/esm/sdk/internal/uploads.d.ts.map +1 -0
  504. package/dist/esm/sdk/internal/uploads.js +148 -0
  505. package/dist/esm/sdk/internal/uploads.js.map +1 -0
  506. package/dist/esm/sdk/internal/utils/base64.d.ts +3 -0
  507. package/dist/esm/sdk/internal/utils/base64.d.ts.map +1 -0
  508. package/dist/esm/sdk/internal/utils/base64.js +33 -0
  509. package/dist/esm/sdk/internal/utils/base64.js.map +1 -0
  510. package/dist/esm/sdk/internal/utils/body.d.ts +40 -0
  511. package/dist/esm/sdk/internal/utils/body.d.ts.map +1 -0
  512. package/dist/esm/sdk/internal/utils/body.js +140 -0
  513. package/dist/esm/sdk/internal/utils/body.js.map +1 -0
  514. package/dist/esm/sdk/internal/utils/bytes.d.ts +4 -0
  515. package/dist/esm/sdk/internal/utils/bytes.d.ts.map +1 -0
  516. package/dist/esm/sdk/internal/utils/bytes.js +26 -0
  517. package/dist/esm/sdk/internal/utils/bytes.js.map +1 -0
  518. package/dist/esm/sdk/internal/utils/env.d.ts +9 -0
  519. package/dist/esm/sdk/internal/utils/env.d.ts.map +1 -0
  520. package/dist/esm/sdk/internal/utils/env.js +18 -0
  521. package/dist/esm/sdk/internal/utils/env.js.map +1 -0
  522. package/dist/esm/sdk/internal/utils/log.d.ts +52 -0
  523. package/dist/esm/sdk/internal/utils/log.d.ts.map +1 -0
  524. package/dist/esm/sdk/internal/utils/log.js +405 -0
  525. package/dist/esm/sdk/internal/utils/log.js.map +1 -0
  526. package/dist/esm/sdk/internal/utils/path.d.ts +41 -0
  527. package/dist/esm/sdk/internal/utils/path.d.ts.map +1 -0
  528. package/dist/esm/sdk/internal/utils/path.js +108 -0
  529. package/dist/esm/sdk/internal/utils/path.js.map +1 -0
  530. package/dist/esm/sdk/internal/utils/query-string.d.ts +21 -0
  531. package/dist/esm/sdk/internal/utils/query-string.d.ts.map +1 -0
  532. package/dist/esm/sdk/internal/utils/query-string.js +125 -0
  533. package/dist/esm/sdk/internal/utils/query-string.js.map +1 -0
  534. package/dist/esm/sdk/internal/utils/query.d.ts +2 -0
  535. package/dist/esm/sdk/internal/utils/query.d.ts.map +1 -0
  536. package/dist/esm/sdk/internal/utils/query.js +9 -0
  537. package/dist/esm/sdk/internal/utils/query.js.map +1 -0
  538. package/dist/esm/sdk/internal/utils/sleep.d.ts +2 -0
  539. package/dist/esm/sdk/internal/utils/sleep.d.ts.map +1 -0
  540. package/dist/esm/sdk/internal/utils/sleep.js +3 -0
  541. package/dist/esm/sdk/internal/utils/sleep.js.map +1 -0
  542. package/dist/esm/sdk/internal/utils/uuid.d.ts +5 -0
  543. package/dist/esm/sdk/internal/utils/uuid.d.ts.map +1 -0
  544. package/dist/esm/sdk/internal/utils/uuid.js +15 -0
  545. package/dist/esm/sdk/internal/utils/uuid.js.map +1 -0
  546. package/dist/esm/sdk/internal/utils/values.d.ts +18 -0
  547. package/dist/esm/sdk/internal/utils/values.d.ts.map +1 -0
  548. package/dist/esm/sdk/internal/utils/values.js +94 -0
  549. package/dist/esm/sdk/internal/utils/values.js.map +1 -0
  550. package/dist/esm/sdk/internal/utils.d.ts +8 -0
  551. package/dist/esm/sdk/internal/utils.d.ts.map +1 -0
  552. package/dist/esm/sdk/internal/utils.js +9 -0
  553. package/dist/esm/sdk/internal/utils.js.map +1 -0
  554. package/dist/esm/sdk/pagination.d.ts +3 -0
  555. package/dist/esm/sdk/pagination.d.ts.map +1 -0
  556. package/dist/esm/sdk/pagination.js +4 -0
  557. package/dist/esm/sdk/pagination.js.map +1 -0
  558. package/dist/esm/sdk/resource.d.ts +6 -0
  559. package/dist/esm/sdk/resource.d.ts.map +1 -0
  560. package/dist/esm/sdk/resource.js +8 -0
  561. package/dist/esm/sdk/resource.js.map +1 -0
  562. package/dist/esm/sdk/resources/images/images.d.ts +202 -0
  563. package/dist/esm/sdk/resources/images/images.d.ts.map +1 -0
  564. package/dist/esm/sdk/resources/images/images.js +102 -0
  565. package/dist/esm/sdk/resources/images/images.js.map +1 -0
  566. package/dist/esm/sdk/resources/images/index.d.ts +5 -0
  567. package/dist/esm/sdk/resources/images/index.d.ts.map +1 -0
  568. package/dist/esm/sdk/resources/images/index.js +4 -0
  569. package/dist/esm/sdk/resources/images/index.js.map +1 -0
  570. package/dist/esm/sdk/resources/images/uploads.d.ts +50 -0
  571. package/dist/esm/sdk/resources/images/uploads.d.ts.map +1 -0
  572. package/dist/esm/sdk/resources/images/uploads.js +24 -0
  573. package/dist/esm/sdk/resources/images/uploads.js.map +1 -0
  574. package/dist/esm/sdk/resources/images.d.ts +2 -0
  575. package/dist/esm/sdk/resources/images.d.ts.map +1 -0
  576. package/dist/esm/sdk/resources/images.js +3 -0
  577. package/dist/esm/sdk/resources/images.js.map +1 -0
  578. package/dist/esm/sdk/resources/index.d.ts +6 -0
  579. package/dist/esm/sdk/resources/index.d.ts.map +1 -0
  580. package/dist/esm/sdk/resources/index.js +5 -0
  581. package/dist/esm/sdk/resources/index.js.map +1 -0
  582. package/dist/esm/sdk/resources/machines/autoresizing.d.ts +115 -0
  583. package/dist/esm/sdk/resources/machines/autoresizing.d.ts.map +1 -0
  584. package/dist/esm/sdk/resources/machines/autoresizing.js +42 -0
  585. package/dist/esm/sdk/resources/machines/autoresizing.js.map +1 -0
  586. package/dist/esm/sdk/resources/machines/executions/executions.d.ts +341 -0
  587. package/dist/esm/sdk/resources/machines/executions/executions.d.ts.map +1 -0
  588. package/dist/esm/sdk/resources/machines/executions/executions.js +124 -0
  589. package/dist/esm/sdk/resources/machines/executions/executions.js.map +1 -0
  590. package/dist/esm/sdk/resources/machines/executions/index.d.ts +5 -0
  591. package/dist/esm/sdk/resources/machines/executions/index.d.ts.map +1 -0
  592. package/dist/esm/sdk/resources/machines/executions/index.js +4 -0
  593. package/dist/esm/sdk/resources/machines/executions/index.js.map +1 -0
  594. package/dist/esm/sdk/resources/machines/executions/logs.d.ts +103 -0
  595. package/dist/esm/sdk/resources/machines/executions/logs.d.ts.map +1 -0
  596. package/dist/esm/sdk/resources/machines/executions/logs.js +44 -0
  597. package/dist/esm/sdk/resources/machines/executions/logs.js.map +1 -0
  598. package/dist/esm/sdk/resources/machines/executions.d.ts +2 -0
  599. package/dist/esm/sdk/resources/machines/executions.d.ts.map +1 -0
  600. package/dist/esm/sdk/resources/machines/executions.js +3 -0
  601. package/dist/esm/sdk/resources/machines/executions.js.map +1 -0
  602. package/dist/esm/sdk/resources/machines/index.d.ts +9 -0
  603. package/dist/esm/sdk/resources/machines/index.d.ts.map +1 -0
  604. package/dist/esm/sdk/resources/machines/index.js +6 -0
  605. package/dist/esm/sdk/resources/machines/index.js.map +1 -0
  606. package/dist/esm/sdk/resources/machines/machines.d.ts +496 -0
  607. package/dist/esm/sdk/resources/machines/machines.d.ts.map +1 -0
  608. package/dist/esm/sdk/resources/machines/machines.js +183 -0
  609. package/dist/esm/sdk/resources/machines/machines.js.map +1 -0
  610. package/dist/esm/sdk/resources/machines/ssh.d.ts +175 -0
  611. package/dist/esm/sdk/resources/machines/ssh.d.ts.map +1 -0
  612. package/dist/esm/sdk/resources/machines/ssh.js +85 -0
  613. package/dist/esm/sdk/resources/machines/ssh.js.map +1 -0
  614. package/dist/esm/sdk/resources/machines.d.ts +2 -0
  615. package/dist/esm/sdk/resources/machines.d.ts.map +1 -0
  616. package/dist/esm/sdk/resources/machines.js +3 -0
  617. package/dist/esm/sdk/resources/machines.js.map +1 -0
  618. package/dist/esm/sdk/resources/organization/autoresizing.d.ts +86 -0
  619. package/dist/esm/sdk/resources/organization/autoresizing.d.ts.map +1 -0
  620. package/dist/esm/sdk/resources/organization/autoresizing.js +34 -0
  621. package/dist/esm/sdk/resources/organization/autoresizing.js.map +1 -0
  622. package/dist/esm/sdk/resources/organization/index.d.ts +4 -0
  623. package/dist/esm/sdk/resources/organization/index.d.ts.map +1 -0
  624. package/dist/esm/sdk/resources/organization/index.js +4 -0
  625. package/dist/esm/sdk/resources/organization/index.js.map +1 -0
  626. package/dist/esm/sdk/resources/organization/organization.d.ts +10 -0
  627. package/dist/esm/sdk/resources/organization/organization.d.ts.map +1 -0
  628. package/dist/esm/sdk/resources/organization/organization.js +9 -0
  629. package/dist/esm/sdk/resources/organization/organization.js.map +1 -0
  630. package/dist/esm/sdk/resources/organization.d.ts +2 -0
  631. package/dist/esm/sdk/resources/organization.d.ts.map +1 -0
  632. package/dist/esm/sdk/resources/organization.js +3 -0
  633. package/dist/esm/sdk/resources/organization.js.map +1 -0
  634. package/dist/esm/sdk/resources.d.ts +3 -0
  635. package/dist/esm/sdk/resources.d.ts.map +1 -0
  636. package/dist/esm/sdk/resources.js +3 -0
  637. package/dist/esm/sdk/resources.js.map +1 -0
  638. package/dist/esm/sdk/uploads.d.ts +2 -0
  639. package/dist/esm/sdk/uploads.d.ts.map +1 -0
  640. package/dist/esm/sdk/uploads.js +3 -0
  641. package/dist/esm/sdk/uploads.js.map +1 -0
  642. package/dist/esm/sdk/version.d.ts +2 -0
  643. package/dist/esm/sdk/version.d.ts.map +1 -0
  644. package/dist/esm/sdk/version.js +3 -0
  645. package/dist/esm/sdk/version.js.map +1 -0
  646. package/man/dedalus-completion.1 +35 -0
  647. package/man/dedalus-images-create.1 +126 -0
  648. package/man/dedalus-images-delete.1 +102 -0
  649. package/man/dedalus-images-import.1 +34 -0
  650. package/man/dedalus-images-list.1 +105 -0
  651. package/man/dedalus-images-retrieve.1 +102 -0
  652. package/man/dedalus-images-update.1 +108 -0
  653. package/man/dedalus-images-uploads-create.1 +112 -0
  654. package/man/dedalus-login.1 +47 -0
  655. package/man/dedalus-logout.1 +28 -0
  656. package/man/dedalus-machines-autoresizing-retrieve.1 +104 -0
  657. package/man/dedalus-machines-autoresizing-update.1 +116 -0
  658. package/man/dedalus-machines-create.1 +119 -0
  659. package/man/dedalus-machines-delete.1 +107 -0
  660. package/man/dedalus-machines-executions-create.1 +120 -0
  661. package/man/dedalus-machines-executions-delete.1 +108 -0
  662. package/man/dedalus-machines-executions-events.1 +118 -0
  663. package/man/dedalus-machines-executions-list.1 +114 -0
  664. package/man/dedalus-machines-executions-logs-create-token.1 +108 -0
  665. package/man/dedalus-machines-executions-logs-retrieve.1 +108 -0
  666. package/man/dedalus-machines-executions-output.1 +108 -0
  667. package/man/dedalus-machines-executions-retrieve.1 +108 -0
  668. package/man/dedalus-machines-list.1 +108 -0
  669. package/man/dedalus-machines-reboot.1 +107 -0
  670. package/man/dedalus-machines-resize.1 +110 -0
  671. package/man/dedalus-machines-retrieve.1 +104 -0
  672. package/man/dedalus-machines-sleep.1 +104 -0
  673. package/man/dedalus-machines-ssh-create.1 +108 -0
  674. package/man/dedalus-machines-ssh-delete.1 +108 -0
  675. package/man/dedalus-machines-ssh-list.1 +114 -0
  676. package/man/dedalus-machines-ssh-retrieve.1 +108 -0
  677. package/man/dedalus-machines-update.1 +110 -0
  678. package/man/dedalus-machines-wake.1 +104 -0
  679. package/man/dedalus-organization-autoresizing-retrieve.1 +99 -0
  680. package/man/dedalus-organization-autoresizing-update.1 +111 -0
  681. package/man/dedalus-uninstall.1 +23 -0
  682. package/man/dedalus-upgrade.1 +32 -0
  683. package/man/dedalus.1 +222 -0
  684. package/package.json +88 -4
@@ -0,0 +1,2053 @@
1
+ // File generated from our OpenAPI spec by Scalar. See README.md for details.
2
+ import { spawn } from 'node:child_process';
3
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
4
+ import { createServer } from 'node:http';
5
+ import { createInterface } from 'node:readline';
6
+ import { stderr as processStderr, stdin as processStdin } from 'node:process';
7
+ import { clearAll, deleteProfile, profileKey, readProfile, refreshProfile, storeDescription, warn, writeProfile, } from './credentials.js';
8
+ import { windowsPowerShell } from './keychain.js';
9
+ import { clip } from './list.js';
10
+ /** How long the browser flow waits for the redirect before giving the terminal back. */
11
+ const BROWSER_FLOW_TIMEOUT_MS = 300000;
12
+ /**
13
+ * How long the browser flow waits for its answered pages to finish sending before closing the
14
+ * listener. Loopback delivers a page in milliseconds; this only bounds a browser that stops reading.
15
+ */
16
+ const RESPONSE_FLUSH_TIMEOUT_MS = 2000;
17
+ /** The one path the loopback listener answers as a redirect; it is what `redirect_uri` registers. */
18
+ const REDIRECT_PATH = '/callback';
19
+ /**
20
+ * Ceiling on how long the device grant polls, whatever `expires_in` claims.
21
+ *
22
+ * RFC 8628 leaves the lifetime to the server, and the server is named by the OpenAPI document. This
23
+ * is the same bound the browser grant has, so neither interactive sign-in can be made to wait
24
+ * indefinitely by an endpoint that answers `authorization_pending` and a very large expiry.
25
+ */
26
+ const DEVICE_FLOW_TIMEOUT_MS = 300000;
27
+ /**
28
+ * How long a token request may take, and how much of its answer is read.
29
+ *
30
+ * Both bounds exist because the endpoint comes from the OpenAPI document. `fetch` has no default
31
+ * timeout, and a refresh runs before every command that has no flag or environment credential, so an
32
+ * endpoint that accepts the connection and never answers would hang the whole CLI with nothing on
33
+ * screen. The size cap keeps an endless body from being buffered into memory for a response whose
34
+ * useful part is a few hundred bytes.
35
+ */
36
+ const TOKEN_REQUEST_TIMEOUT_MS = 30000;
37
+ const TOKEN_RESPONSE_LIMIT = 65536;
38
+ /**
39
+ * Deadline for each token revocation `logout` sends, and for the metadata read `login` makes only to
40
+ * learn where those go.
41
+ *
42
+ * Short because the sign-out has already happened by then — the local copy is gone — so the user is
43
+ * waiting only on a courtesy to the provider, and an endpoint that never answers must not hold the
44
+ * terminal for the full token-request deadline, once per token. The sign-in read is optional in the
45
+ * same way: its grant has succeeded, and the token is not saved until the read returns.
46
+ */
47
+ const REVOCATION_TIMEOUT_MS = 5000;
48
+ /** Refresh this far ahead of expiry so a token cannot lapse between the check and the request. */
49
+ const REFRESH_LEEWAY_MS = 60000;
50
+ /**
51
+ * Deadline for the refresh that runs ahead of an ordinary command, rather than the full one above.
52
+ *
53
+ * That refresh is not what the user asked for — it is preparation for a request they are waiting on
54
+ * — and its failure is swallowed, so the whole of its cost is delay they cannot see a reason for. An
55
+ * unreachable refresh endpoint would otherwise add `TOKEN_REQUEST_TIMEOUT_MS` to *every* invocation
56
+ * for the life of the stored entry. Giving up sooner costs a refresh that a slow provider might
57
+ * still have answered; the command then goes out with the credential it already had, and a 401 says
58
+ * to sign in again. An interactive `login` keeps the full deadline, because there the exchange is
59
+ * the thing the user is waiting for.
60
+ */
61
+ const SILENT_REFRESH_TIMEOUT_MS = 5000;
62
+ /**
63
+ * How far past its `exp` an ID token is still accepted, to absorb clock drift between this machine
64
+ * and the provider.
65
+ *
66
+ * OpenID Connect Core leaves the allowance to the client. Five minutes is the customary figure: a
67
+ * token the provider minted seconds ago is not refused because a laptop's clock runs slow, and a
68
+ * token that genuinely expired is still refused long before it could be useful to a replay.
69
+ */
70
+ const ID_TOKEN_CLOCK_SKEW_MS = 300000;
71
+ /** Lifetime assumed for a token whose endpoint did not state one. See {@link oauthMetadata}. */
72
+ const ASSUMED_TOKEN_LIFETIME_S = 3600;
73
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
74
+ /**
75
+ * Shortest device-code poll interval a server can ask for.
76
+ *
77
+ * Low enough that a local authorization server still feels immediate, high enough that one asking
78
+ * for zero cannot turn the wait for authorization into a request flood.
79
+ */
80
+ const DEVICE_POLL_FLOOR_MS = 200;
81
+ /** The grant type RFC 8628 registers, sent on each poll and looked for in server metadata. */
82
+ const DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
83
+ const storeLocation = (auth) => ({
84
+ storeName: auth.storeName,
85
+ storeEnv: auth.storeEnv,
86
+ backend: auth.backend,
87
+ });
88
+ /** The client option keys a method fills, which for HTTP Basic is the username and password pair. */
89
+ const methodKeys = (method) => method.kind === 'basic' ? [method.usernameKey, method.passwordKey] : [method.clientKey];
90
+ /**
91
+ * Names what a request still needs when this sign-in cannot satisfy any alternative by itself.
92
+ *
93
+ * An OpenAPI `security` list is OR-of-ANDs, so an alternative can require two schemes at once — an
94
+ * API key *and* a bearer token. `login` captures one credential per run, which leaves that document
95
+ * signed in only halfway, and saying nothing would send the user off to collect a 401 whose hint
96
+ * tells them to run the command they have just run.
97
+ *
98
+ * Read back from the store rather than from what was captured, so a credential an earlier `login`
99
+ * already left behind counts towards the requirement instead of being asked for a second time.
100
+ *
101
+ * The alternative closest to complete is the one named: it is the cheapest way out for the user,
102
+ * and listing every unmet alternative would describe choices the API treats as interchangeable.
103
+ *
104
+ * `loginCommand` names the environment this `login` selected, when it selected one. The credential
105
+ * just saved is filed under that environment's URL, so a hint naming the bare command would file
106
+ * the next one under the default environment instead, and the pair would never meet. A sign-in made
107
+ * with `--base-url` selects no environment and gets the bare command, as the request hints do.
108
+ */
109
+ const outstandingRequirement = (auth, location, key, loginCommand) => {
110
+ if (auth.requirements.length === 0)
111
+ return undefined;
112
+ // The store *and* the environment, because that is what the request path counts. `applyStoredCredentials`
113
+ // treats a non-blank environment variable as an explicit value and never consults the store for
114
+ // that key, so a user who had exported one half of an AND alternative would otherwise be sent to
115
+ // obtain a credential their very next command was going to send anyway.
116
+ const held = new Set(Object.keys(readProfile(location, key).credentials ?? {}));
117
+ for (const [clientKey, name] of Object.entries(auth.envByKey)) {
118
+ if ((process.env[name] ?? '').trim())
119
+ held.add(clientKey);
120
+ }
121
+ if (auth.requirements.some((keys) => keys.every((candidate) => held.has(candidate))))
122
+ return undefined;
123
+ const missing = auth.requirements
124
+ .map((keys) => keys.filter((candidate) => !held.has(candidate)))
125
+ .filter((keys) => keys.length > 0)
126
+ .sort((left, right) => left.length - right.length)[0];
127
+ if (!missing || missing.length === 0)
128
+ return undefined;
129
+ // A key with no method behind it cannot be asked for, so it is described rather than commanded.
130
+ // Deduplicated by method, because HTTP Basic fills two keys in one sitting and would otherwise be
131
+ // offered twice for the one sign-in that satisfies both.
132
+ const names = new Set();
133
+ const runnable = [];
134
+ for (const candidate of missing) {
135
+ const method = auth.methods.find((entry) => methodKeys(entry).includes(candidate));
136
+ if (!method || names.has(method.name))
137
+ continue;
138
+ names.add(method.name);
139
+ runnable.push(method);
140
+ }
141
+ if (runnable.length === 0)
142
+ return 'Requests here also need another credential this CLI cannot obtain.';
143
+ const commands = runnable.map((method) => loginCommand + ' --flow ' + method.name);
144
+ return ('Requests here also need ' +
145
+ (runnable.length === 1 ? 'one more credential' : String(runnable.length) + ' more credentials') +
146
+ '. Run ' +
147
+ commands.join(', then ') +
148
+ '.');
149
+ };
150
+ /**
151
+ * Runs an interactive sign-in and saves the result, returning the line to print.
152
+ *
153
+ * Every prompt is written to standard error, so the confirmation this returns is the only thing on
154
+ * standard output and `login` stays usable in a pipeline.
155
+ *
156
+ * `loginCommand` is the sign-in command for the environment this one selected (the bare command
157
+ * when none was); it names the next step when the API needs a further credential.
158
+ */
159
+ export const runLogin = async (auth, baseUrl, flow, loginCommand) => {
160
+ const method = await chooseMethod(auth, flow);
161
+ const location = storeLocation(auth);
162
+ const key = profileKey(baseUrl);
163
+ const captured = await captureCredentials(method, baseUrl, auth.logo);
164
+ // Where it landed is reported by the write itself rather than predicted, so under `auto` the
165
+ // line a user reads names the store the credential actually went into. The write merges this
166
+ // captured result inside its lock, because a browser grant can sit for minutes before it arrives.
167
+ const backend = writeProfile(location, key, captured);
168
+ const line = 'Signed in to ' +
169
+ // The base URL can be the document's own `servers[0].url` when no flag and no environment
170
+ // variable named one, which makes it spec-derived text on its way to a terminal.
171
+ safeText(baseUrl || 'the API') +
172
+ '. Credentials saved to ' +
173
+ storeDescription(location, backend) +
174
+ '.';
175
+ const outstanding = outstandingRequirement(auth, location, key, loginCommand);
176
+ return outstanding ? line + '\n' + outstanding : line;
177
+ };
178
+ /**
179
+ * Forgets stored credentials and revokes their OAuth tokens, returning the line to print.
180
+ *
181
+ * The local copy goes first, under the store lock, and only then is the provider asked to revoke.
182
+ * That order is what makes an interrupted or offline `logout` safe: whatever happens on the
183
+ * network, nothing is left on disk, and the refresh path's conditional write will not bring a
184
+ * deleted profile back. Revocation is best effort — a failure is a warning, never a failed logout,
185
+ * because the user asked to be signed out here and that part has already succeeded.
186
+ */
187
+ export const runLogout = async (auth, baseUrl, all) => {
188
+ const location = storeLocation(auth);
189
+ // Only an OAuth sign-in stores anything revocable, so a CLI without one skips reading what a
190
+ // profile held and signs out exactly as it did before revocation existed.
191
+ const revocable = auth.methods.some((method) => method.kind === 'oauth');
192
+ if (all) {
193
+ const { removed, failure } = clearAll(location, revocable);
194
+ // Every profile's revocation runs at once, so a host that never answers costs its own timeout
195
+ // rather than one per host. Warnings are raised afterwards, in store order, so the output does
196
+ // not depend on which server answered first.
197
+ const outcomes = await Promise.all(removed.map(({ key, held }) => {
198
+ const base = profileBase(auth, key);
199
+ return revokeHeld(auth, base, base, held);
200
+ }));
201
+ for (const message of outcomes.flat())
202
+ warn(message);
203
+ if (failure)
204
+ throw new Error(failure);
205
+ return 'Signed out everywhere.';
206
+ }
207
+ const key = profileKey(baseUrl);
208
+ const { outcome, held } = deleteProfile(location, key, revocable);
209
+ // Named without any `user:password@` it carries, in the warnings and the result line alike, so
210
+ // the two still name the host the same way.
211
+ const shown = withoutCredentials(baseUrl);
212
+ for (const message of await revokeHeld(auth, baseUrl, shown, held))
213
+ warn(message);
214
+ if (outcome === 'removed')
215
+ return 'Signed out of ' + safeText(shown || 'the API') + '.';
216
+ return 'No stored credentials for ' + safeText(shown || 'the API') + '.';
217
+ };
218
+ /**
219
+ * The base URL a profile key stands for, for `logout --all`, which knows a profile only by its key.
220
+ *
221
+ * A profile with no stored revocation endpoint falls back to the configured one, resolved against
222
+ * the base URL as sign-in resolves its endpoints. The key drops a trailing slash, which moves where
223
+ * a path-relative endpoint lands (`oauth/revoke` beside `https://api.example.com/v1/` is under
224
+ * `/v1/`, beside the key it is not), so a base URL this run knows that files under this key is
225
+ * used when there is one: the base-URL environment variable if it is still set, the configured
226
+ * default, or an environment URL. The variable comes first because sign-in prefers it to the
227
+ * default, which matters when the two differ only in a trailing slash and so share a key. Only a
228
+ * sign-in under a `--base-url` or a variable since unset falls back to the key.
229
+ *
230
+ * Credentials in the base URL are dropped, as `profileKey` drops them: the result is printed in
231
+ * warnings, and an endpoint resolved against it would carry them into a request `fetch` refuses.
232
+ */
233
+ const profileBase = (auth, key) => {
234
+ if (key === 'default')
235
+ return '';
236
+ const known = [
237
+ (auth.baseUrlEnv ? process.env[auth.baseUrlEnv] : undefined)?.trim(),
238
+ auth.defaultBaseUrl,
239
+ ...(auth.environments ?? []).map((environment) => environment.url),
240
+ ];
241
+ const base = known.find((url) => url && profileKey(url) === key);
242
+ return base ? withoutCredentials(base) : key;
243
+ };
244
+ /** A URL with any `user:password@` removed, or the value unchanged when it does not parse. */
245
+ const withoutCredentials = (raw) => {
246
+ try {
247
+ const url = new URL(raw);
248
+ // Rewritten only when there is something to drop: serializing adds a trailing slash to a bare
249
+ // origin, which would change how every warning names the host.
250
+ if (!url.username && !url.password)
251
+ return raw;
252
+ url.username = '';
253
+ url.password = '';
254
+ return url.toString();
255
+ }
256
+ catch {
257
+ return raw;
258
+ }
259
+ };
260
+ /**
261
+ * The revocation endpoint to keep for a token, or `undefined` for a local-only sign-out.
262
+ *
263
+ * Each candidate is resolved against `base` exactly as the token endpoint is, and the first one on
264
+ * the same origin as the endpoint that issued the token wins. `revocationUrl` is one value for
265
+ * every OAuth scheme in the CLI, so without that check a token from one provider would be posted
266
+ * to another's revocation endpoint. The discovered endpoint is held to the same rule: every
267
+ * provider that advertises one serves it beside its token endpoint, and a sign-out that stays
268
+ * local is a smaller loss than a token sent somewhere that did not issue it.
269
+ */
270
+ const revocationEndpoint = (candidates, tokenUrl, base) => {
271
+ const issuer = tokenUrl ? urlOrigin(tokenUrl, base) : undefined;
272
+ if (!issuer)
273
+ return undefined;
274
+ for (const candidate of candidates) {
275
+ const resolved = storedEndpoint(candidate, base);
276
+ if (resolved && urlOrigin(resolved, '') === issuer)
277
+ return resolved;
278
+ }
279
+ return undefined;
280
+ };
281
+ /** A URL's origin, or `undefined` when it cannot be parsed or has none (an opaque `null` origin). */
282
+ const urlOrigin = (raw, base) => {
283
+ try {
284
+ const origin = new URL(raw, base || undefined).origin;
285
+ return origin === 'null' ? undefined : origin;
286
+ }
287
+ catch {
288
+ return undefined;
289
+ }
290
+ };
291
+ /**
292
+ * Revokes what one removed profile held, returning the warnings to raise.
293
+ *
294
+ * Each warning names the base URL, because under `logout --all` that is the only way to tell which
295
+ * sign-in may still be live at its provider. `label` is the one the success line prints for a
296
+ * single `logout`, so the two lines name the host the same way. `base` is what a fallback
297
+ * endpoint resolves against: the base URL a single `logout` was given, or {@link profileBase}.
298
+ */
299
+ const revokeHeld = async (auth, base, label, held) => {
300
+ const where = 'Signed out of ' + safeText(label || 'the API') + ' locally, but ';
301
+ if (held.unreadable) {
302
+ // Said only where this build could have revoked something: a configured endpoint, or a
303
+ // provider whose published metadata (OpenID Connect discovery or RFC 8414) may name one.
304
+ // Anywhere else revocation never happens, and the keyring warning the removal already raised
305
+ // covers what is left to say.
306
+ const couldRevoke = auth.methods.some((method) => method.kind === 'oauth' &&
307
+ (method.revocationUrl !== undefined ||
308
+ method.grant === 'openIdConnect' ||
309
+ 'metadataUrl' in method ||
310
+ ('revocationMetadataUrl' in method && method.revocationMetadataUrl !== undefined)));
311
+ if (!couldRevoke)
312
+ return [];
313
+ return [
314
+ where +
315
+ 'the saved credentials could not be read back, so no OAuth token in them was revoked at the provider.',
316
+ ];
317
+ }
318
+ return revokeProfile(auth, base, held.profile, where);
319
+ };
320
+ /**
321
+ * Revokes every OAuth token one removed profile held (RFC 7009), returning the warnings to raise.
322
+ *
323
+ * API keys and Basic credentials have nothing to revoke, so only entries with OAuth metadata are
324
+ * considered. The endpoint is the one stored at sign-in; a profile that stored none falls back to
325
+ * the one this build is configured with (see {@link fallbackRevocation}). With neither, the
326
+ * sign-out stays local, as it always was. `where` opens every warning.
327
+ */
328
+ const revokeProfile = async (auth, base, profile, where) => {
329
+ const messages = [];
330
+ for (const [clientKey, meta] of Object.entries(profile.oauth ?? {})) {
331
+ // Same guard as `storedCredentials`: the store is parsed without checking the shape beneath.
332
+ if (!meta || typeof meta !== 'object')
333
+ continue;
334
+ const endpoint = meta.revocationUrl ?? fallbackRevocation(auth, clientKey, meta, base);
335
+ if (!endpoint)
336
+ continue;
337
+ const accessToken = profile.credentials?.[clientKey];
338
+ // Refresh token first: on most servers revoking it ends the whole grant, access tokens included,
339
+ // so it is the one that matters if only one call gets through.
340
+ const tokens = [
341
+ ...(meta.refreshToken ? [{ token: meta.refreshToken, hint: 'refresh_token' }] : []),
342
+ ...(accessToken ? [{ token: accessToken, hint: 'access_token' }] : []),
343
+ ];
344
+ if (tokens.length === 0)
345
+ continue;
346
+ let url;
347
+ try {
348
+ // Checked here rather than at sign-in: this is the request that carries a token, and failing
349
+ // a login over an endpoint only `logout` uses would cost the user the sign-in itself.
350
+ url = requireSecureUrl(endpoint, base, 'revocation endpoint');
351
+ }
352
+ catch (error) {
353
+ messages.push(where + 'the token was not revoked. ' + errorMessage(error));
354
+ continue;
355
+ }
356
+ try {
357
+ let refreshRevoked = false;
358
+ for (const { token, hint } of tokens) {
359
+ await revokeToken(url, token, hint, meta.clientId, refreshRevoked);
360
+ if (hint === 'refresh_token')
361
+ refreshRevoked = true;
362
+ }
363
+ }
364
+ catch (error) {
365
+ // One failure ends this profile's revocation rather than trying the next token: an endpoint
366
+ // that has just timed out or refused is unlikely to do otherwise a second later, and each try
367
+ // costs the user up to `REVOCATION_TIMEOUT_MS`.
368
+ messages.push(where + safeText(url.host) + ' could not revoke the token. ' + errorMessage(error));
369
+ }
370
+ }
371
+ return messages;
372
+ };
373
+ /**
374
+ * The configured revocation endpoint for a profile that stored none, or `undefined` to stay local.
375
+ *
376
+ * Reached by every profile that stored none: one saved before revocation existed, one whose
377
+ * sign-in had nothing configured — so a `revocationUrl` a later build adds still reaches it — and
378
+ * one whose sign-in rejected the configured endpoint. That last needs no mark of its own: the
379
+ * checks below are sign-in's rule, so the endpoint it rejected is rejected here too, and only a
380
+ * build whose configuration was since corrected revokes it.
381
+ *
382
+ * The endpoint, resolved against `base`, has to pass two origin checks. One is against the
383
+ * profile's saved refresh endpoint, the only endpoint an older profile recorded. The other is
384
+ * sign-in's own rule against the token endpoint, so a build never revokes an old sign-in somewhere
385
+ * it would refuse for a new one — a provider can serve refresh from another host than the one
386
+ * that issues tokens. A profile does not record which grant produced it, and every grant of one
387
+ * scheme shares its key, so the token endpoint of each of those grants has to pass. An OpenID
388
+ * Connect grant, or a device grant reading RFC 8414 metadata, learns its token endpoint at sign-in
389
+ * and saved that same URL as the refresh endpoint, so the first check covers it.
390
+ */
391
+ const fallbackRevocation = (auth, clientKey, meta, base) => {
392
+ const grants = auth.methods.filter((method) => method.kind === 'oauth' && method.clientKey === clientKey);
393
+ // One configured value covers every OAuth method, so any grant that carries it names it.
394
+ const configured = grants.find((method) => method.revocationUrl)?.revocationUrl;
395
+ const endpoint = revocationEndpoint([configured], meta.refreshUrl ?? meta.tokenUrl, base);
396
+ if (!endpoint)
397
+ return undefined;
398
+ // OpenID Connect and device sign-in from metadata learn their token endpoint at sign-in and saved
399
+ // it as the refresh endpoint, which the check above already covers.
400
+ const issuedHere = grants.every((method) => !('tokenUrl' in method) || revocationEndpoint([configured], method.tokenUrl, base) !== undefined);
401
+ return issuedHere ? endpoint : undefined;
402
+ };
403
+ /**
404
+ * Sends one RFC 7009 revocation request, throwing a printable reason when it did not succeed.
405
+ *
406
+ * Only the client id goes with the token: the CLI keeps no client secret, so a provider that
407
+ * requires confidential-client authentication here — typical for tokens from the client-credentials
408
+ * grant — refuses, and that surfaces as the warning rather than as anything the CLI could fix.
409
+ */
410
+ const revokeToken = async (url, token, hint, clientId, refreshRevoked) => {
411
+ const { response, text } = await postForToken(url, { token, token_type_hint: hint, ...(clientId ? { client_id: clientId } : {}) }, REVOCATION_TIMEOUT_MS, 'revocation endpoint');
412
+ // Section 2.2: a 200 is the answer for an invalid or already-revoked token too, so success says
413
+ // nothing about the token's prior state and needs nothing checked beyond the status.
414
+ if (response.ok)
415
+ return;
416
+ const payload = parseJson(text);
417
+ // Section 2.2.1 lets a server that only revokes refresh tokens answer this for an access token.
418
+ // When this call already revoked the refresh token, that ended the grant and there is nothing to
419
+ // report. With no refresh token (a client-credentials grant never gets one) the access token is
420
+ // the whole grant and is still live, so the refusal is a failure like any other.
421
+ if (hint === 'access_token' && refreshRevoked && payload?.['error'] === 'unsupported_token_type')
422
+ return;
423
+ throw new Error('The revocation endpoint rejected the request' + tokenErrorDetail(payload, response.status));
424
+ };
425
+ /** The message of a caught value, which every throw in this module makes an `Error` with safe text. */
426
+ const errorMessage = (error) => (error instanceof Error ? error.message : String(error));
427
+ /**
428
+ * Stored credentials for one base URL, keyed by client-option name.
429
+ *
430
+ * An OAuth access token at or near its expiry is refreshed in place first, so a CLI left alone
431
+ * overnight keeps working without a second `login`. A refresh that fails is swallowed: the stale
432
+ * token still goes out, and the 401 that follows carries the runtime's own sign-in hint, which
433
+ * says more than a refresh error would.
434
+ *
435
+ * Refresh exchanges may overlap, but their results merge into the current profile under the same
436
+ * lock as interactive sign-in. A server that rotates refresh tokens can still reject one of two
437
+ * simultaneous exchanges; that is recoverable with one `login` and never loses another flow's key.
438
+ */
439
+ export const storedCredentials = async (auth, baseUrl) => {
440
+ const location = storeLocation(auth);
441
+ const key = profileKey(baseUrl);
442
+ const profile = readProfile(location, key);
443
+ const credentials = { ...(profile.credentials ?? {}) };
444
+ for (const [clientKey, meta] of Object.entries(profile.oauth ?? {})) {
445
+ // `readProfile` checks that the stored value parses and is an object, but not the shape beneath
446
+ // it, so an entry this CLI did not write can still be `null` or a scalar. Reading `expiresAt` off
447
+ // one would throw on the request path of *every* command — the exact failure the corrupt-store
448
+ // handling exists to avoid, and worse than it, since a bad OAuth entry would also take out the
449
+ // commands that need no credential at all. Skipping it falls back to `login`, as a missing
450
+ // entry does.
451
+ if (!meta || typeof meta !== 'object')
452
+ continue;
453
+ // Older generated CLIs stored this as `tokenUrl`; retaining that fallback makes their saved
454
+ // credentials refreshable after an upgrade while new entries preserve a distinct refresh URL.
455
+ const refreshUrl = meta.refreshUrl ?? meta.tokenUrl;
456
+ if (!needsRefresh(meta) || !meta.refreshToken || !refreshUrl)
457
+ continue;
458
+ // The target API is read from this CLI's own definition for the key, not from the stored entry,
459
+ // which never held it: it is configuration rather than something a sign-in produced, so the
460
+ // refresh follows the CLI the user is running now. A key belongs to exactly one security scheme
461
+ // (the emitter gives each scheme its own option name), and every OAuth method of that scheme
462
+ // carries the scheme's one value, so the first is as good as any.
463
+ const target = auth.methods.find((method) => method.kind === 'oauth' && method.clientKey === clientKey);
464
+ try {
465
+ const token = await exchangeToken(refreshUrl, baseUrl, {
466
+ grant_type: 'refresh_token',
467
+ refresh_token: meta.refreshToken,
468
+ ...(meta.clientId ? { client_id: meta.clientId } : {}),
469
+ ...(target?.kind === 'oauth' ? targetParams(target) : {}),
470
+ }, SILENT_REFRESH_TIMEOUT_MS);
471
+ credentials[clientKey] = token.accessToken;
472
+ // Conditional, not `writeProfile`: a `logout` that landed while the exchange was in flight
473
+ // must not be undone by the token it produced. See `refreshProfile`.
474
+ refreshProfile(location, key, {
475
+ credentials: { [clientKey]: token.accessToken },
476
+ oauth: {
477
+ [clientKey]: oauthMetadata(token, refreshUrl, meta.clientId, meta.refreshToken, meta.revocationUrl),
478
+ },
479
+ });
480
+ }
481
+ catch {
482
+ // Deliberately silent; see this function's doc comment.
483
+ }
484
+ }
485
+ return credentials;
486
+ };
487
+ const needsRefresh = (meta) => meta.expiresAt !== undefined && meta.expiresAt - Date.now() < REFRESH_LEEWAY_MS;
488
+ /**
489
+ * Picks the sign-in method to run.
490
+ *
491
+ * A named flow is matched exactly; otherwise a single method runs unprompted and several are put to
492
+ * the user. The picker needs a terminal, so a non-interactive shell is told to name a flow instead
493
+ * of being left waiting on stdin that will never arrive.
494
+ */
495
+ const chooseMethod = async (auth, flow) => {
496
+ const names = auth.methods.map((method) => method.name);
497
+ if (flow !== undefined) {
498
+ const chosen = auth.methods.find((method) => method.name === flow);
499
+ if (!chosen)
500
+ throw new UsageError("Unknown sign-in flow '" + flow + "'. Available: " + names.join(', ') + '.');
501
+ return chosen;
502
+ }
503
+ const first = auth.methods[0];
504
+ if (!first)
505
+ throw new Error('This CLI has no sign-in flow.');
506
+ if (auth.methods.length === 1)
507
+ return first;
508
+ const index = await promptChoice('How would you like to sign in?', auth.methods.map((method) => ({ label: method.label, hint: method.name })));
509
+ // The picker only ever resolves an index it drew, so `first` stands in for a state that cannot
510
+ // occur rather than an error message describing one.
511
+ return auth.methods[index] ?? first;
512
+ };
513
+ const captureCredentials = async (method, baseUrl, logo) => {
514
+ if (method.kind === 'token') {
515
+ const value = await promptSecret(method.prompt);
516
+ if (!value)
517
+ throw new UsageError('No value entered; nothing was saved.');
518
+ return { credentials: { [method.clientKey]: value } };
519
+ }
520
+ if (method.kind === 'basic') {
521
+ const username = await promptLine('Username: ');
522
+ if (!username)
523
+ throw new UsageError('No username entered; nothing was saved.');
524
+ const password = await promptSecret('Password: ');
525
+ if (!password)
526
+ throw new UsageError('No password entered; nothing was saved.');
527
+ return { credentials: { [method.usernameKey]: username, [method.passwordKey]: password } };
528
+ }
529
+ const { token, clientId, refreshUrl, revocationUrl } = await runOauthFlow(method, baseUrl, logo);
530
+ return {
531
+ credentials: { [method.clientKey]: token.accessToken },
532
+ oauth: { [method.clientKey]: oauthMetadata(token, refreshUrl, clientId, undefined, revocationUrl) },
533
+ };
534
+ };
535
+ const oauthMetadata = (token, refreshUrl, clientId, previousRefreshToken, revocationUrl) => {
536
+ // A refresh response may legitimately omit `refresh_token`, which means "keep using the one you
537
+ // have" rather than "you no longer have one"; dropping it would turn every refresh into the last.
538
+ const refreshToken = token.refreshToken ?? previousRefreshToken;
539
+ // `expires_in` is OPTIONAL in RFC 6749 §5.1, and an absent one used to mean no `expiresAt`, which
540
+ // `needsRefresh` reads as "never expires" — so a provider that omits it left the refresh token
541
+ // stored and never used, and the user re-ran the whole flow on every expiry. When there is
542
+ // something to refresh with, an unstated lifetime is assumed to be the hour nearly every provider
543
+ // issues; refreshing earlier than necessary costs one request, never refreshing costs the feature.
544
+ const lifetime = token.expiresIn ?? (refreshToken ? ASSUMED_TOKEN_LIFETIME_S : undefined);
545
+ return {
546
+ ...(refreshToken ? { refreshToken } : {}),
547
+ ...(lifetime === undefined ? {} : { expiresAt: Date.now() + lifetime * 1000 }),
548
+ refreshUrl,
549
+ ...(clientId ? { clientId } : {}),
550
+ // Carried through every refresh too, so a token renewed in place is still revoked where it was issued.
551
+ ...(revocationUrl ? { revocationUrl } : {}),
552
+ };
553
+ };
554
+ /**
555
+ * Runs one OAuth grant and picks the revocation endpoint to store beside its token.
556
+ *
557
+ * OpenID Connect picks its own, since only discovery names its token endpoint; every other grant is
558
+ * decided here, after the grant succeeded. The configured endpoint comes first, then whatever the
559
+ * scheme's RFC 8414 metadata advertises — read during a device sign-in that took its endpoints from
560
+ * there, otherwise read now by {@link metadataRevocation} — and both are held to the origin of the
561
+ * token endpoint that issued the token.
562
+ */
563
+ const runOauthFlow = async (method, baseUrl, logo) => {
564
+ if (method.grant === 'openIdConnect')
565
+ return openIdConnectFlow(method, baseUrl, logo);
566
+ const result = await runTokenGrant(method, baseUrl, logo);
567
+ // A device sign-in from metadata names no token endpoint up front; the one it discovered is the
568
+ // refresh endpoint it returned.
569
+ const tokenUrl = 'tokenUrl' in method ? method.tokenUrl : result.refreshUrl;
570
+ const discovered = result.revocationUrl
571
+ ? { revocationUrl: result.revocationUrl }
572
+ : await metadataRevocation(method, tokenUrl, baseUrl);
573
+ return {
574
+ ...result,
575
+ revocationUrl: signInRevocation(method.revocationUrl, discovered, tokenUrl, baseUrl),
576
+ };
577
+ };
578
+ /**
579
+ * The revocation endpoint a scheme's RFC 8414 metadata advertises, read once a grant has succeeded.
580
+ *
581
+ * Read only when the configured endpoint does not already qualify, so a sign-in the config covers
582
+ * costs no round trip. It passes the same issuer checks as a device sign-in from that metadata.
583
+ * Unlike that sign-in, nothing but revocation depends on it, so a failed read never fails the
584
+ * sign-in: it warns, since `logout` will then only forget the token locally, and the user should
585
+ * hear so at the one point they are watching — {@link signInRevocation} says so, with the reason
586
+ * returned here, in the one warning it raises. For the same reason it gets the revocation request's
587
+ * short deadline rather than a token request's: the grant has already succeeded, its token is not
588
+ * saved until this returns, and a provider that never answers must not hold that up.
589
+ */
590
+ const metadataRevocation = async (method, tokenUrl, baseUrl) => {
591
+ if (!('revocationMetadataUrl' in method) || !method.revocationMetadataUrl)
592
+ return {};
593
+ if (revocationEndpoint([method.revocationUrl], tokenUrl, baseUrl) !== undefined)
594
+ return {};
595
+ try {
596
+ const { payload, issuerUrl } = await readAuthorizationServerMetadata({ metadataUrl: method.revocationMetadataUrl, issuer: method.issuer }, baseUrl, REVOCATION_TIMEOUT_MS);
597
+ return { revocationUrl: advertisedRevocation(payload, issuerUrl) };
598
+ }
599
+ catch (error) {
600
+ return { failure: errorMessage(error) };
601
+ }
602
+ };
603
+ /**
604
+ * A metadata document's `revocation_endpoint` (RFC 8414 section 2), resolved against its verified
605
+ * issuer, or `undefined` when it names none.
606
+ *
607
+ * Optional, and only `logout` uses it, so a missing or odd one is left out rather than failing the
608
+ * sign-in, and its HTTPS check waits for the revocation itself.
609
+ */
610
+ const advertisedRevocation = (payload, issuerUrl) => {
611
+ const advertised = payload['revocation_endpoint'];
612
+ return typeof advertised === 'string' && advertised
613
+ ? storedEndpoint(advertised, issuerUrl.toString())
614
+ : undefined;
615
+ };
616
+ /** Runs a grant whose token endpoint the document names, returning the token and how to refresh it. */
617
+ const runTokenGrant = async (method, baseUrl, logo) => {
618
+ if (method.grant === 'authorizationCode') {
619
+ const result = await authorizationCodeFlow(method, baseUrl, logo);
620
+ return { ...result, refreshUrl: method.refreshUrl };
621
+ }
622
+ if (method.grant === 'deviceAuthorization')
623
+ return deviceAuthorizationFlow(method, baseUrl);
624
+ if (method.grant === 'clientCredentials') {
625
+ const clientId = method.clientId ?? (await promptLine('Client id: '));
626
+ if (!clientId)
627
+ throw new UsageError('No client id entered; nothing was saved.');
628
+ const clientSecret = await promptSecret('Client secret: ');
629
+ if (!clientSecret)
630
+ throw new UsageError('No client secret entered; nothing was saved.');
631
+ const token = await exchangeToken(method.tokenUrl, baseUrl, {
632
+ grant_type: 'client_credentials',
633
+ client_id: clientId,
634
+ client_secret: clientSecret,
635
+ ...scopeParam(method.scopes),
636
+ ...targetParams(method),
637
+ });
638
+ return { token, clientId, refreshUrl: method.refreshUrl };
639
+ }
640
+ // Asked for the same way the client-credentials grant above asks, so a provider that requires a
641
+ // client id on this grant — which a public client's does — can be signed into without the value
642
+ // having to be baked into the generated source. It stays optional, unlike there: RFC 6749 lets a
643
+ // client that authenticates some other way leave it out, so a blank answer omits the parameter
644
+ // rather than sending an empty one, and the prompt says which it is.
645
+ const clientId = method.clientId ?? (await promptLine('Client id (optional): '));
646
+ const username = await promptLine('Username: ');
647
+ if (!username)
648
+ throw new UsageError('No username entered; nothing was saved.');
649
+ const password = await promptSecret('Password: ');
650
+ if (!password)
651
+ throw new UsageError('No password entered; nothing was saved.');
652
+ const token = await exchangeToken(method.tokenUrl, baseUrl, {
653
+ grant_type: 'password',
654
+ username,
655
+ password,
656
+ ...(clientId ? { client_id: clientId } : {}),
657
+ ...scopeParam(method.scopes),
658
+ ...targetParams(method),
659
+ });
660
+ // Persisted so the refresh exchange can send the same client id the grant was made with; a blank
661
+ // answer stores nothing, which is what an absent one already did.
662
+ return { token, clientId: clientId || undefined, refreshUrl: method.refreshUrl };
663
+ };
664
+ /**
665
+ * Resolves an endpoint against the base URL it applies to, for storing beside the token.
666
+ *
667
+ * Deliberately not `requireSecureUrl`: nothing is sent to a revocation endpoint at sign-in, so a
668
+ * bad one must not cost the user the sign-in. `logout` checks it before a token goes out. An
669
+ * unparseable value yields `undefined`, so it is never stored; a configured one is reported at
670
+ * sign-in by {@link signInRevocation}, and a discovered one is optional and simply left out.
671
+ */
672
+ const storedEndpoint = (raw, base) => {
673
+ if (!raw)
674
+ return undefined;
675
+ try {
676
+ const url = new URL(raw, base || undefined);
677
+ // A relative endpoint inherits credentials from a `https://user:pass@host/` base. Kept, they
678
+ // would sit in the store beside the token and make `fetch` refuse every revocation request.
679
+ url.username = '';
680
+ url.password = '';
681
+ return url.toString();
682
+ }
683
+ catch {
684
+ return undefined;
685
+ }
686
+ };
687
+ /**
688
+ * The revocation endpoint to store with a token that sign-in has just obtained.
689
+ *
690
+ * Runs only once the grant succeeded, because it can warn, and a failed `login` drains warnings
691
+ * too. It warns when the configured endpoint cannot be used — unparseable, or off the token
692
+ * endpoint's origin — and nothing else qualified either, since `logout` will then only forget
693
+ * this sign-in locally, and the user should hear so while the configuration can still be fixed.
694
+ *
695
+ * It also warns when the provider's metadata could not be read for one, with nothing configured
696
+ * or alongside the configured endpoint's own reason, so both causes reach the user in one warning:
697
+ * each alone would point at only half of why `logout` will not revoke.
698
+ *
699
+ * Nothing stored means `logout` falls back to the configured endpoint under this same rule (see
700
+ * {@link fallbackRevocation}), so a rejected endpoint stays rejected there.
701
+ */
702
+ const signInRevocation = (configured, discovered, tokenUrl, baseUrl) => {
703
+ const chosen = revocationEndpoint([configured, discovered.revocationUrl], tokenUrl, baseUrl);
704
+ if (chosen !== undefined)
705
+ return chosen;
706
+ const metadataReason = discovered.failure ? ' ' + discovered.failure : '';
707
+ if (!configured) {
708
+ if (discovered.failure)
709
+ warn('Logout will not revoke this sign-in at the provider.' + metadataReason);
710
+ return undefined;
711
+ }
712
+ const revocationOrigin = urlOrigin(configured, baseUrl);
713
+ const issuerOrigin = urlOrigin(tokenUrl, baseUrl);
714
+ warn((revocationOrigin === undefined || issuerOrigin === undefined
715
+ ? 'The configured revocation endpoint is not a usable URL, so logout will not revoke this sign-in at the provider.'
716
+ : 'The configured revocation endpoint ' +
717
+ safeText(revocationOrigin) +
718
+ " is not on the token endpoint's origin " +
719
+ safeText(issuerOrigin) +
720
+ ', so logout will not revoke this sign-in at the provider.') + metadataReason);
721
+ return undefined;
722
+ };
723
+ /**
724
+ * Runs the Device Authorization Grant without needing a loopback listener or a local browser.
725
+ *
726
+ * The device code is never printed: it is the credential that the token endpoint accepts. The
727
+ * user code, by contrast, is deliberately meant to be entered in a browser and is shown beside the
728
+ * verification URL. Polling follows RFC 8628's ‘authorization_pending’ and ‘slow_down’ signals
729
+ * instead of treating them as failed sign-ins.
730
+ */
731
+ const deviceAuthorizationFlow = async (method, baseUrl) => {
732
+ // Discovered before the client id is asked for, so a provider that cannot run this grant fails
733
+ // the sign-in before the user has been asked to type anything.
734
+ const endpoints = 'metadataUrl' in method ? await discoverDeviceEndpoints(method, baseUrl) : method;
735
+ const clientId = method.clientId ?? (await promptLine('Client id: '));
736
+ if (!clientId)
737
+ throw new UsageError('No client id entered; nothing was saved.');
738
+ const device = await requestDeviceAuthorization(endpoints.deviceAuthorizationUrl, baseUrl, {
739
+ client_id: clientId,
740
+ ...scopeParam(method.scopes),
741
+ ...targetParams(method),
742
+ });
743
+ processStderr.write('To sign in, visit:\n\n ' +
744
+ device.verificationUrl +
745
+ (device.userCode ? '\n\nThen enter code: ' + safeText(device.userCode) : '') +
746
+ '\n\nWaiting for authorization...\n');
747
+ return {
748
+ token: await pollDeviceToken(endpoints.tokenUrl, baseUrl, { client_id: clientId, ...targetParams(method) }, device),
749
+ clientId,
750
+ refreshUrl: endpoints.refreshUrl,
751
+ // From the metadata the endpoints came from, so revocation costs no second read of it.
752
+ ...('revocationUrl' in endpoints && endpoints.revocationUrl
753
+ ? { revocationUrl: endpoints.revocationUrl }
754
+ : {}),
755
+ };
756
+ };
757
+ /**
758
+ * Reads the device grant's endpoints from the authorization server's RFC 8414 metadata.
759
+ *
760
+ * Both come back absolute, resolved against the verified issuer, so the polls and every later
761
+ * refresh go to the provider that published them rather than to the API host.
762
+ */
763
+ const discoverDeviceEndpoints = async (method, baseUrl) => {
764
+ const label = AUTHORIZATION_SERVER_METADATA;
765
+ const { payload, issuerUrl } = await readAuthorizationServerMetadata(method, baseUrl);
766
+ // RFC 8414 defaults an omitted `grant_types_supported` to the code and implicit grants, but
767
+ // providers that do run this grant routinely leave the list out, so only a list that is present
768
+ // and leaves the grant out is taken as a refusal. The endpoint below is the signal that counts.
769
+ const grants = payload['grant_types_supported'];
770
+ if (Array.isArray(grants) && !grants.includes(DEVICE_CODE_GRANT)) {
771
+ throw new Error('The ' + label + ' does not list the device authorization grant as supported.');
772
+ }
773
+ const deviceAuthorizationUrl = payload['device_authorization_endpoint'];
774
+ const tokenUrl = payload['token_endpoint'];
775
+ if (typeof deviceAuthorizationUrl !== 'string' || !deviceAuthorizationUrl) {
776
+ throw new Error('The ' + label + ' names no device authorization endpoint.');
777
+ }
778
+ if (typeof tokenUrl !== 'string' || !tokenUrl)
779
+ throw new Error('The ' + label + ' names no token endpoint.');
780
+ const token = requireSecureUrl(tokenUrl, issuerUrl.toString(), 'token endpoint').toString();
781
+ const revocationUrl = advertisedRevocation(payload, issuerUrl);
782
+ return {
783
+ deviceAuthorizationUrl: requireSecureUrl(deviceAuthorizationUrl, issuerUrl.toString(), 'device authorization endpoint').toString(),
784
+ tokenUrl: token,
785
+ refreshUrl: token,
786
+ ...(revocationUrl ? { revocationUrl } : {}),
787
+ };
788
+ };
789
+ const AUTHORIZATION_SERVER_METADATA = 'authorization server metadata';
790
+ /**
791
+ * Reads a scheme's RFC 8414 metadata and holds it to its issuer: the one its URL was built from
792
+ * (section 3.3), and the configured issuer when there is one.
793
+ *
794
+ * Shared by device sign-in, which takes its endpoints from the document, and by revocation, which
795
+ * takes only `revocation_endpoint`, so the two cannot disagree about which documents to trust.
796
+ */
797
+ const readAuthorizationServerMetadata = async (method, baseUrl, timeoutMs) => {
798
+ const label = AUTHORIZATION_SERVER_METADATA;
799
+ const { payload, issuer, issuerUrl } = await readServerMetadata(method.metadataUrl, baseUrl, { document: label, issuer: 'authorization server issuer' }, metadataIssuer, timeoutMs);
800
+ if (method.issuer !== undefined && !sameIssuer(issuer, method.issuer)) {
801
+ throw new Error('The ' +
802
+ label +
803
+ ' is issued by ' +
804
+ safeText(issuer) +
805
+ ', not the configured issuer ' +
806
+ safeText(method.issuer) +
807
+ '.');
808
+ }
809
+ return { payload, issuerUrl };
810
+ };
811
+ /**
812
+ * Discovers OpenID Connect endpoints, then uses the same PKCE flow as a declared authorization code
813
+ * grant — holding the redirect's `iss` and the ID token to the issuer discovery verified.
814
+ */
815
+ const openIdConnectFlow = async (method, baseUrl, logo) => {
816
+ const endpoints = await discoverOpenIdConnect(method.discoveryUrl, baseUrl);
817
+ // A configured issuer is the publisher saying which provider this CLI signs in with. Discovery
818
+ // only proves the document agrees with the URL it came from, and that URL is spec-derived, so a
819
+ // document that points somewhere else is refused here rather than trusted for the rest of the flow.
820
+ if (method.issuer !== undefined && !sameIssuer(endpoints.issuer, method.issuer)) {
821
+ throw new Error('The OpenID Connect discovery document is issued by ' +
822
+ safeText(endpoints.issuer) +
823
+ ', not the configured issuer ' +
824
+ safeText(method.issuer) +
825
+ '.');
826
+ }
827
+ // Same randomness as `state`. `state` ties the redirect to this process; the nonce ties the ID
828
+ // token to it, so a token minted for some other sign-in cannot be replayed into this one.
829
+ const nonce = base64Url(randomBytes(16));
830
+ const result = await authorizationCodeFlow({
831
+ ...method,
832
+ grant: 'authorizationCode',
833
+ authorizationUrl: endpoints.authorizationUrl,
834
+ tokenUrl: endpoints.tokenUrl,
835
+ refreshUrl: endpoints.tokenUrl,
836
+ issuer: endpoints.issuer,
837
+ }, baseUrl, logo, {
838
+ // Discovery always yields an issuer to compare a received `iss` with, but only a configured
839
+ // issuer or the provider's own advertisement makes a *missing* one a failure: RFC 9207 lets a
840
+ // server that never promised the parameter leave it out.
841
+ issRequired: method.issuer !== undefined || endpoints.issParameterSupported,
842
+ nonce,
843
+ // Run inside the code flow, while the browser is still waiting on its page, so a refused ID
844
+ // token is what the browser is told about too. It also runs before `login` writes anything:
845
+ // a token that fails is not stored, and neither is the access token that arrived beside it.
846
+ verify: (token) => {
847
+ if (token.idToken !== undefined) {
848
+ verifyIdToken(token.idToken, { issuer: endpoints.issuer, clientId: method.clientId, nonce });
849
+ }
850
+ },
851
+ });
852
+ // A configured endpoint wins over the discovered one when it sits beside the token endpoint
853
+ // discovery named; the discovered one was already resolved against the issuer.
854
+ const revocationUrl = signInRevocation(method.revocationUrl, { revocationUrl: endpoints.revocationUrl }, endpoints.tokenUrl, baseUrl);
855
+ return { ...result, refreshUrl: endpoints.tokenUrl, revocationUrl };
856
+ };
857
+ /**
858
+ * Checks the claims of an ID token the token endpoint returned, per OpenID Connect Core 3.1.3.7.
859
+ *
860
+ * The signature is deliberately not verified. Section 3.1.3.7 lets TLS server validation stand in
861
+ * for it when the token is received directly from the token endpoint — which is the only way this
862
+ * flow receives one: `exchangeToken` refuses a plain-HTTP endpoint off loopback and follows no
863
+ * redirects. Verifying it anyway would mean fetching and caching the provider's JWKS and
864
+ * implementing each signing algorithm, in a runtime that ships with no dependencies. The claims are
865
+ * what bind the token to this sign-in, and those are checked in full.
866
+ */
867
+ const verifyIdToken = (idToken, expected) => {
868
+ const claims = idTokenClaims(idToken);
869
+ const issuer = claims['iss'];
870
+ if (typeof issuer !== 'string' || !sameIssuer(issuer, expected.issuer)) {
871
+ throw new Error('The ID token was issued by ' +
872
+ (typeof issuer === 'string' ? safeText(issuer) || 'an empty issuer' : 'no issuer') +
873
+ ', but this sign-in expected ' +
874
+ safeText(expected.issuer) +
875
+ '.');
876
+ }
877
+ const audience = claims['aud'];
878
+ const audiences = typeof audience === 'string' ? [audience] : Array.isArray(audience) ? audience : [];
879
+ if (!audiences.includes(expected.clientId)) {
880
+ throw new Error('The ID token is not addressed to this client (' + safeText(expected.clientId) + ').');
881
+ }
882
+ // With more than one audience, `azp` names the party the token was actually issued to; without
883
+ // it, a token minted for another client that merely lists this one could be passed off as ours.
884
+ // A present `azp` is held to the client id even for a single audience, as the spec recommends.
885
+ const party = claims['azp'];
886
+ if ((audiences.length > 1 || party !== undefined) && party !== expected.clientId) {
887
+ throw new Error(party === undefined
888
+ ? 'The ID token lists several audiences but names no authorized party (azp).'
889
+ : 'The ID token was issued to another authorized party (azp) than this client.');
890
+ }
891
+ const expiry = claims['exp'];
892
+ if (typeof expiry !== 'number' || !Number.isFinite(expiry)) {
893
+ throw new Error('The ID token has no usable expiry.');
894
+ }
895
+ if (expiry * 1000 + ID_TOKEN_CLOCK_SKEW_MS <= Date.now()) {
896
+ throw new Error('The ID token has expired.');
897
+ }
898
+ const nonce = claims['nonce'];
899
+ if (typeof nonce !== 'string' || !sameToken(nonce, expected.nonce)) {
900
+ throw new Error('The ID token does not carry the nonce this sign-in sent, so it may belong to another sign-in.');
901
+ }
902
+ };
903
+ /**
904
+ * Decodes the payload of a compact-serialized JWT into its claims, refusing anything malformed.
905
+ *
906
+ * Strict about the alphabet because Node's base64url decoder is not: it skips characters it does
907
+ * not recognise, so a mangled segment would otherwise decode into *something* rather than fail.
908
+ */
909
+ const idTokenClaims = (idToken) => {
910
+ const malformed = new Error('The token endpoint returned a malformed ID token.');
911
+ const segments = idToken.split('.');
912
+ const payload = segments[1];
913
+ if (segments.length !== 3 || !payload || !/^[A-Za-z0-9_-]+$/u.test(payload))
914
+ throw malformed;
915
+ let claims;
916
+ try {
917
+ claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
918
+ }
919
+ catch {
920
+ throw malformed;
921
+ }
922
+ if (!claims || typeof claims !== 'object' || Array.isArray(claims))
923
+ throw malformed;
924
+ return claims;
925
+ };
926
+ /** The `scope` parameter, left off entirely when there is nothing to request. */
927
+ const scopeParam = (scopes) => scopes.length > 0 ? { scope: scopes.join(' ') } : {};
928
+ /**
929
+ * The parameters naming which API a token is for, spread into every request a grant makes.
930
+ *
931
+ * Every one — the authorization URL, the device authorization request, each token-endpoint call and
932
+ * the refresh — because providers differ in where they read it: some bind it to the authorization
933
+ * and check the token request against it, others look only at the token endpoint, and a request
934
+ * without it can come back with a token for a different API. Kept in one place so no grant can be
935
+ * written without it. Both are absent unless configured, which leaves an unconfigured CLI's
936
+ * requests exactly as they were.
937
+ */
938
+ const targetParams = (target) => ({
939
+ ...(target.resource && target.resource.length > 0 ? { resource: target.resource } : {}),
940
+ ...(target.audience ? { audience: target.audience } : {}),
941
+ });
942
+ /**
943
+ * Writes grant fields into a query or form, one entry per value.
944
+ *
945
+ * Each key is cleared first, so a field replaces whatever the authorization URL already carried
946
+ * under that name — the same outcome `searchParams.set` gives a single value — while a list still
947
+ * repeats its key.
948
+ */
949
+ const writeParams = (params, fields) => {
950
+ for (const [key, value] of Object.entries(fields)) {
951
+ params.delete(key);
952
+ for (const entry of typeof value === 'string' ? [value] : value)
953
+ params.append(key, entry);
954
+ }
955
+ return params;
956
+ };
957
+ /**
958
+ * Starts a device grant and validates the response before anything is printed to the terminal.
959
+ *
960
+ * `fields` is the whole request body — the client id, the scope and the target API — so the caller
961
+ * that knows the method decides what the grant asks for.
962
+ */
963
+ const requestDeviceAuthorization = async (endpoint, baseUrl, fields) => {
964
+ const url = requireSecureUrl(endpoint, baseUrl, 'device authorization endpoint');
965
+ const { response, text } = await postForToken(url, fields);
966
+ const payload = parseJson(text);
967
+ if (!response.ok) {
968
+ throw new Error('The device authorization endpoint rejected the request' + tokenErrorDetail(payload, response.status));
969
+ }
970
+ const deviceCode = payload?.['device_code'];
971
+ const verificationUri = payload?.['verification_uri_complete'] ?? payload?.['verification_uri'];
972
+ const userCode = payload?.['user_code'];
973
+ const expiresIn = payload?.['expires_in'];
974
+ const interval = payload?.['interval'];
975
+ if (typeof deviceCode !== 'string' ||
976
+ !deviceCode ||
977
+ typeof verificationUri !== 'string' ||
978
+ !verificationUri) {
979
+ throw new Error('The device authorization endpoint returned an incomplete response.');
980
+ }
981
+ if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {
982
+ throw new Error('The device authorization endpoint returned no usable expiry.');
983
+ }
984
+ const verificationUrl = requireSecureUrl(verificationUri, baseUrl, 'device verification URL').toString();
985
+ return {
986
+ deviceCode,
987
+ verificationUrl,
988
+ ...(typeof userCode === 'string' && userCode ? { userCode } : {}),
989
+ // Capped at this CLI's own ceiling rather than taken as given. `expires_in` comes from a
990
+ // spec-derived endpoint, so a hostile or simply wrong one answering `1e12` would make the only
991
+ // loop guard in `pollDeviceToken` unreachable and leave `login` waiting with no way out but
992
+ // Ctrl-C — the browser grant is bounded by `BROWSER_FLOW_TIMEOUT_MS` and this is its equal.
993
+ expiresAt: Date.now() + Math.min(expiresIn * 1000, DEVICE_FLOW_TIMEOUT_MS),
994
+ // RFC 8628 defaults to five seconds. A server may shorten it — a local authorization server
995
+ // answering instantly is the case that matters — but never below `DEVICE_POLL_FLOOR_MS`, and a
996
+ // negative or non-finite value never shortens it at all. Honouring a literal zero polled with no
997
+ // delay for the whole of `expires_in`: a request flood against someone else's token endpoint and
998
+ // a pinned core here, for a grant that answers `authorization_pending` until the user acts.
999
+ intervalMs: typeof interval === 'number' && Number.isFinite(interval) && interval >= 0
1000
+ ? Math.max(interval * 1000, DEVICE_POLL_FLOOR_MS)
1001
+ : 5000,
1002
+ };
1003
+ };
1004
+ /**
1005
+ * Polls the token endpoint until the user authorizes, declines, or the device code expires.
1006
+ *
1007
+ * `fields` carries the client id and the target API, sent on every poll beside the device code.
1008
+ */
1009
+ const pollDeviceToken = async (endpoint, baseUrl, fields, device) => {
1010
+ const url = requireSecureUrl(endpoint, baseUrl, 'token endpoint');
1011
+ let intervalMs = device.intervalMs;
1012
+ while (Date.now() < device.expiresAt) {
1013
+ await waitFor(Math.min(intervalMs, Math.max(0, device.expiresAt - Date.now())));
1014
+ if (Date.now() >= device.expiresAt)
1015
+ break;
1016
+ const { response, text } = await postForToken(url, {
1017
+ grant_type: DEVICE_CODE_GRANT,
1018
+ device_code: device.deviceCode,
1019
+ ...fields,
1020
+ });
1021
+ const payload = parseJson(text);
1022
+ if (response.ok)
1023
+ return tokenResponse(payload);
1024
+ const error = payload?.['error'];
1025
+ if (error === 'authorization_pending')
1026
+ continue;
1027
+ if (error === 'slow_down') {
1028
+ intervalMs += 5000;
1029
+ continue;
1030
+ }
1031
+ if (error === 'expired_token')
1032
+ throw new Error('The device code expired before authorization completed.');
1033
+ if (error === 'access_denied')
1034
+ throw new Error('The device authorization request was denied.');
1035
+ throw new Error('The token endpoint rejected the device code' + tokenErrorDetail(payload, response.status));
1036
+ }
1037
+ throw new Error('The device code expired before authorization completed.');
1038
+ };
1039
+ /** Waits between device-code polls without blocking the event loop that owns terminal I/O. */
1040
+ const waitFor = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
1041
+ /** The suffix OpenID Connect Discovery appends to an issuer to reach its configuration document. */
1042
+ const OPENID_CONFIGURATION_PATH = '/.well-known/openid-configuration';
1043
+ /**
1044
+ * The issuer a discovery URL was built from, per OpenID Connect Discovery section 4.
1045
+ *
1046
+ * The spec builds the URL by appending the well-known path to the issuer, so removing it again is
1047
+ * what the returned `issuer` has to match. A URL spelled any other way — a document naming a
1048
+ * configuration endpoint that is not derived from its issuer at all — yields nothing, and the
1049
+ * caller falls back to comparing origins, which still pins the answer to the host that was asked.
1050
+ */
1051
+ const discoveryIssuer = (url) => url.pathname.endsWith(OPENID_CONFIGURATION_PATH)
1052
+ ? url.origin + url.pathname.slice(0, -OPENID_CONFIGURATION_PATH.length)
1053
+ : undefined;
1054
+ /** Compares two issuer spellings, where a trailing slash is the one difference that is not one. */
1055
+ const sameIssuer = (left, right) => left.replace(/\/+$/u, '') === right.replace(/\/+$/u, '');
1056
+ /** The well-known name RFC 8414 registers for an OAuth authorization server's metadata. */
1057
+ const OAUTH_METADATA_PATH = '/.well-known/oauth-authorization-server';
1058
+ /**
1059
+ * The issuer an RFC 8414 metadata URL was built from, or `undefined` when its spelling does not say.
1060
+ *
1061
+ * RFC 8414 section 3.1 *inserts* the well-known path between the host and the issuer's own path
1062
+ * (`https://as.example/.well-known/oauth-authorization-server/tenant` for the issuer
1063
+ * `https://as.example/tenant`), and section 5 uses the same insertion for an OpenID Connect
1064
+ * configuration. OpenID Connect Discovery appends instead, so that spelling is accepted as well.
1065
+ */
1066
+ const metadataIssuer = (url) => {
1067
+ for (const wellKnown of [OAUTH_METADATA_PATH, OPENID_CONFIGURATION_PATH]) {
1068
+ if (url.pathname === wellKnown || url.pathname.startsWith(wellKnown + '/')) {
1069
+ return url.origin + url.pathname.slice(wellKnown.length);
1070
+ }
1071
+ }
1072
+ return discoveryIssuer(url);
1073
+ };
1074
+ /**
1075
+ * Fetches an authorization server's published metadata and verifies who published it.
1076
+ *
1077
+ * Both OpenID Connect Discovery (section 4.3) and RFC 8414 (section 3.3) require the document's
1078
+ * `issuer` to be the one its URL was built from. It is the check that keeps a provider from naming
1079
+ * somebody else's endpoints: without it the sign-in sends the user, and then the code or device
1080
+ * grant it obtains, wherever an untrusted document says. A URL whose spelling names no issuer is
1081
+ * held to its origin instead, which still pins the answer to the host that was asked.
1082
+ */
1083
+ const readServerMetadata = async (metadataUrl, baseUrl, labels, issuerFor, timeoutMs) => {
1084
+ const url = requireSecureUrl(metadataUrl, baseUrl, labels.document);
1085
+ const { response, text } = await getJson(url, labels.document, timeoutMs);
1086
+ const payload = parseJson(text);
1087
+ if (!response.ok) {
1088
+ throw new Error('The ' + labels.document + ' could not be read' + tokenErrorDetail(payload, response.status));
1089
+ }
1090
+ const issuer = payload?.['issuer'];
1091
+ if (!payload || typeof issuer !== 'string' || !issuer) {
1092
+ throw new Error('The ' + labels.document + ' named no issuer.');
1093
+ }
1094
+ const expected = issuerFor(url);
1095
+ const issuerUrl = requireSecureUrl(issuer, '', labels.issuer);
1096
+ const matches = expected === undefined ? issuerUrl.origin === url.origin : sameIssuer(issuer, expected);
1097
+ if (!matches) {
1098
+ throw new Error('The ' +
1099
+ labels.document +
1100
+ ' is issued by ' +
1101
+ safeText(issuer) +
1102
+ ', which is not the provider it was fetched from.');
1103
+ }
1104
+ return { payload, issuer, issuerUrl };
1105
+ };
1106
+ /**
1107
+ * Fetches the OpenID Provider configuration and keeps what the browser flow needs: the two
1108
+ * endpoints PKCE uses, the issuer the document was verified to come from, and whether the provider
1109
+ * promises RFC 9207's `iss` on its redirects — plus the revocation endpoint `logout` uses when
1110
+ * the provider advertises one.
1111
+ */
1112
+ const discoverOpenIdConnect = async (discoveryUrl, baseUrl) => {
1113
+ // Held to the appended spelling alone, which is the derivation the compiler's
1114
+ // `Config/OidcIssuerMismatch` check mirrors at generation time.
1115
+ const { payload, issuer, issuerUrl } = await readServerMetadata(discoveryUrl, baseUrl, { document: 'OpenID Connect discovery document', issuer: 'OpenID Connect issuer' }, discoveryIssuer);
1116
+ const authorizationUrl = payload['authorization_endpoint'];
1117
+ const tokenUrl = payload['token_endpoint'];
1118
+ if (typeof authorizationUrl !== 'string' || typeof tokenUrl !== 'string') {
1119
+ throw new Error('The OpenID Connect discovery document returned no usable authorization and token endpoints.');
1120
+ }
1121
+ // Resolved against the issuer, never the API base URL: a relative `/authorize` belongs to the
1122
+ // provider that answered, and resolving it against the API would point the browser — and the code
1123
+ // exchange after it — at the API host instead.
1124
+ const revocationUrl = advertisedRevocation(payload, issuerUrl);
1125
+ return {
1126
+ authorizationUrl: requireSecureUrl(authorizationUrl, issuerUrl.toString(), 'OpenID Connect authorization endpoint').toString(),
1127
+ tokenUrl: requireSecureUrl(tokenUrl, issuerUrl.toString(), 'OpenID Connect token endpoint').toString(),
1128
+ ...(revocationUrl ? { revocationUrl } : {}),
1129
+ issuer,
1130
+ issParameterSupported: payload?.['authorization_response_iss_parameter_supported'] === true,
1131
+ };
1132
+ };
1133
+ /**
1134
+ * The browser sign-in: PKCE authorization code against a loopback redirect (RFC 8252).
1135
+ *
1136
+ * The CLI is a public client, so it holds no secret; PKCE is what stops an intercepted code from
1137
+ * being redeemed by anything but this process. The redirect listens on 127.0.0.1 rather than a
1138
+ * public interface, and the authorization URL is printed before the browser is opened so a headless
1139
+ * or remote shell can still complete the flow by hand.
1140
+ *
1141
+ * `options.issRequired` makes an `iss`-less redirect a failure; it defaults to whether an issuer
1142
+ * is known at all, which for this grant means one was configured. `options.nonce` is sent on the
1143
+ * authorize request, and only OpenID Connect sends one, since only it returns an ID token to hold
1144
+ * the nonce to. `options.verify` checks the exchanged token and throws to refuse it.
1145
+ *
1146
+ * The browser's request for the redirect is answered only after the code exchange and `verify`,
1147
+ * so it never shows "Signed in" for a token this flow goes on to refuse. (Saving the credential
1148
+ * comes later, in `runLogin`; a store that then refuses the write is reported in the terminal.)
1149
+ * The wait is bounded by the token request's own timeout. Afterwards no redirect is held any more,
1150
+ * and the listener is closed only once every answered response has finished sending — or its
1151
+ * browser has gone — or `RESPONSE_FLUSH_TIMEOUT_MS` has passed.
1152
+ */
1153
+ const authorizationCodeFlow = async (method, baseUrl, logo, options = {}) => {
1154
+ const clientId = method.clientId;
1155
+ if (!clientId || !method.authorizationUrl)
1156
+ throw new Error('This flow needs a configured OAuth client id.');
1157
+ const authorizeUrl = requireSecureUrl(method.authorizationUrl, baseUrl, 'authorization endpoint');
1158
+ const verifier = base64Url(randomBytes(32));
1159
+ const challenge = base64Url(createHash('sha256').update(verifier).digest());
1160
+ const state = base64Url(randomBytes(16));
1161
+ const server = createServer();
1162
+ // Bound first, and only then given its request handler. Registering the handler earlier meant a
1163
+ // bind failure (a configured `redirectPort` already in use) rejected two promises while only
1164
+ // `listen`'s was ever awaited, and the orphan took the process down with an unhandled rejection —
1165
+ // losing the friendly message and the auth exit status. Nothing can reach the port in between:
1166
+ // the browser has not been opened yet.
1167
+ // A configured port that is already taken is the one bind failure a user can act on, and Node's
1168
+ // raw `EADDRINUSE` names neither the setting that chose it nor the alternative. Port 0 lets the
1169
+ // OS pick, so it cannot reach this.
1170
+ const port = await listen(server, method.redirectPort ?? 0).catch((error) => {
1171
+ const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
1172
+ if (code === 'EADDRINUSE' && method.redirectPort) {
1173
+ throw new Error('Port ' +
1174
+ String(method.redirectPort) +
1175
+ ' is already in use, so the sign-in redirect could not be received. Free it, or remove the configured redirect port to let the operating system choose one.');
1176
+ }
1177
+ throw error;
1178
+ });
1179
+ const listener = { answered: [], closing: false };
1180
+ const redirect = awaitRedirect(server, state, REDIRECT_PATH, logo, { expected: method.issuer, required: options.issRequired ?? method.issuer !== undefined }, listener);
1181
+ const redirectUri = 'http://127.0.0.1:' + String(port) + REDIRECT_PATH;
1182
+ try {
1183
+ authorizeUrl.searchParams.set('response_type', 'code');
1184
+ authorizeUrl.searchParams.set('client_id', clientId);
1185
+ authorizeUrl.searchParams.set('redirect_uri', redirectUri);
1186
+ authorizeUrl.searchParams.set('state', state);
1187
+ authorizeUrl.searchParams.set('code_challenge', challenge);
1188
+ authorizeUrl.searchParams.set('code_challenge_method', 'S256');
1189
+ if (options.nonce !== undefined)
1190
+ authorizeUrl.searchParams.set('nonce', options.nonce);
1191
+ if (method.scopes.length > 0)
1192
+ authorizeUrl.searchParams.set('scope', method.scopes.join(' '));
1193
+ writeParams(authorizeUrl.searchParams, targetParams(method));
1194
+ const target = authorizeUrl.toString();
1195
+ processStderr.write('Opening your browser to sign in. If it does not open, visit:\n\n ' + target + '\n\n');
1196
+ openBrowser(target);
1197
+ const { code, respond } = await withTimeout(redirect, BROWSER_FLOW_TIMEOUT_MS, 'Timed out waiting for the browser redirect.');
1198
+ // `respond` answers the held browser request once and ignores later calls, so the `finally`
1199
+ // below can answer it with a failure on any path that got here without answering it.
1200
+ try {
1201
+ let token;
1202
+ try {
1203
+ token = await exchangeToken(method.tokenUrl, baseUrl, {
1204
+ grant_type: 'authorization_code',
1205
+ code,
1206
+ redirect_uri: redirectUri,
1207
+ client_id: clientId,
1208
+ code_verifier: verifier,
1209
+ ...targetParams(method),
1210
+ });
1211
+ }
1212
+ catch (error) {
1213
+ respond('Sign-in failed', 'Your provider did not issue a credential. Return to your terminal for details.', false);
1214
+ throw error;
1215
+ }
1216
+ options.verify?.(token);
1217
+ respond('Signed in', 'You can close this window and return to your terminal.', true);
1218
+ return { token, clientId };
1219
+ }
1220
+ finally {
1221
+ respond('Sign-in could not be verified', 'The credential your provider issued did not match this sign-in. Return to your terminal for details.', false);
1222
+ }
1223
+ }
1224
+ finally {
1225
+ // From here on a redirect is answered at once with the neutral page and never held, so nothing
1226
+ // that arrives during the wait below is left for nobody to answer — a redirect landing after the
1227
+ // browser wait timed out included.
1228
+ listener.closing = true;
1229
+ // Answered pages are waited for before any socket is destroyed: `closeAllConnections` cancels
1230
+ // writes still queued on a socket, and a page carrying a large publisher logo takes more than one
1231
+ // write to send, so closing straight after `end()` could leave the browser a blank or truncated
1232
+ // page. Bounded, so a browser that stops reading cannot hold the terminal.
1233
+ //
1234
+ // `server.close()` comes after the wait, not before it: since Node 19 it also closes every
1235
+ // connection with no request in progress, and a connection whose response has been ended but not
1236
+ // yet flushed counts as one — so closing first cut off exactly the pages this wait protects.
1237
+ await drainAnswered(listener.answered, RESPONSE_FLUSH_TIMEOUT_MS);
1238
+ server.close();
1239
+ // Belt and braces with the `connection: close` above: a connection opened but never used (a
1240
+ // browser preconnecting to the redirect host) is not covered by that header and would keep the
1241
+ // listener, and so the process, alive.
1242
+ server.closeAllConnections();
1243
+ }
1244
+ };
1245
+ /**
1246
+ * Waits, up to `ms` in all, for every answered response — including any answered while waiting.
1247
+ *
1248
+ * Re-reads the list after each batch because the listener keeps accepting until it is closed, and a
1249
+ * response written during the wait is just as liable to be cut off as one written before it.
1250
+ */
1251
+ const drainAnswered = async (answered, ms) => {
1252
+ const deadline = Date.now() + ms;
1253
+ let waited = 0;
1254
+ while (waited < answered.length && Date.now() < deadline) {
1255
+ const batch = answered.slice(waited);
1256
+ waited = answered.length;
1257
+ await settledWithin(Promise.all(batch), deadline - Date.now());
1258
+ }
1259
+ };
1260
+ /**
1261
+ * Resolves once `promise` settles or `ms` have passed, whichever is first, and never rejects.
1262
+ *
1263
+ * The timer is cleared when the promise wins, so a finished wait leaves nothing pending that would
1264
+ * keep the process alive.
1265
+ */
1266
+ const settledWithin = (promise, ms) => new Promise((resolve) => {
1267
+ const timer = setTimeout(resolve, ms);
1268
+ const done = () => {
1269
+ clearTimeout(timer);
1270
+ resolve();
1271
+ };
1272
+ promise.then(done, done);
1273
+ });
1274
+ /**
1275
+ * The style for the one page a generated CLI ever puts in front of a browser.
1276
+ *
1277
+ * Laid out after the Scalar dashboard's own sign-in confirmation: the publisher's logo centred a
1278
+ * little above the fold, then a one-line heading with its icon, then a muted line of detail — no
1279
+ * card, no accent colour. Every colour is a token on `:root` so the palette can be restyled in one
1280
+ * block.
1281
+ *
1282
+ * The font stack names Inter first and then falls back to the system UI face. Naming it is free
1283
+ * where it is already installed and costs nothing where it is not — what it must never do is
1284
+ * *fetch* it, for the same reason the rest of this page is inline.
1285
+ */
1286
+ const PAGE_STYLE =
1287
+ // Values taken from the Scalar dashboard's own theme: `--scalar-background-1` and
1288
+ // `--scalar-color-1/2`, per mode.
1289
+ //
1290
+ // `light-dark()` rather than a `prefers-color-scheme` block, so each token states its light and
1291
+ // dark value together and the palette can be read as pairs. It resolves against the
1292
+ // `color-scheme` declared on the same rule, which is why that comes first.
1293
+ //
1294
+ // Where the function is not understood (before Firefox 120, Chrome 123, Safari 17.5) every token
1295
+ // is invalid at computed-value time and the properties using them fall back to their initial
1296
+ // values — black on white. Plain, still legible, and still saying the sign-in worked, which is the
1297
+ // page's whole job.
1298
+ ':root{color-scheme:light dark;' +
1299
+ '--bg:light-dark(#fff,#0f0f0f);' +
1300
+ '--fg:light-dark(#1b1b1b,#e7e7e7);' +
1301
+ '--muted:light-dark(#757575,#a4a4a4)}' +
1302
+ '*{box-sizing:border-box}' +
1303
+ // The dashboard's auth layout: content starts 15% of the way down the viewport in a 380px column.
1304
+ 'body{margin:0;padding:15vh 48px 48px;background:var(--bg);color:var(--fg);' +
1305
+ // Mirrors `--scalar-font`, which also leads with Inter and falls back to the system UI face.
1306
+ 'font:14px/1.5 Inter,ui-sans-serif,system-ui,sans-serif;-webkit-font-smoothing:antialiased}' +
1307
+ 'main{max-width:380px;margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center}' +
1308
+ // 32px tall like the dashboard's mark, but width left to the image: a publisher's logo is as
1309
+ // often a wordmark as a square icon. The 16px margin plus the 8px gap is the dashboard's 24px.
1310
+ '.logo{display:block;height:32px;max-width:100%;margin-bottom:16px;object-fit:contain}' +
1311
+ // `text-lg` at regular weight, as the dashboard sets it, with a 20px icon beside it.
1312
+ 'h1{margin:0;display:flex;align-items:center;justify-content:center;gap:8px;font-size:16px;font-weight:400;line-height:24px}' +
1313
+ 'h1 svg{flex:none;width:20px;height:20px}' +
1314
+ // `text-sm` in `--scalar-color-2`.
1315
+ 'p{margin:0;color:var(--muted);font-size:13px;line-height:20px}';
1316
+ /**
1317
+ * The page's two marks, as Phosphor's light-weight `sign-in` and `warning` glyphs on a 256 grid —
1318
+ * the icons the dashboard's own confirmation and failure screens use.
1319
+ *
1320
+ * Phosphor Icons, Copyright (c) 2020 Phosphor Icons, MIT License (https://github.com/phosphor-icons/core).
1321
+ */
1322
+ const SIGNED_IN_ICON = 'M140.24,132.24l-40,40a6,6,0,0,1-8.48-8.48L121.51,134H24a6,6,0,0,1,0-12h97.51L91.76,92.24a6,6,0,0,1,8.48-8.48l40,40A6,6,0,0,1,140.24,132.24ZM200,34H136a6,6,0,0,0,0,12h58V210H136a6,6,0,0,0,0,12h64a6,6,0,0,0,6-6V40A6,6,0,0,0,200,34Z';
1323
+ const WARNING_ICON = 'M235.07,189.09,147.61,37.22h0a22.75,22.75,0,0,0-39.22,0L20.93,189.09a21.53,21.53,0,0,0,0,21.72A22.35,22.35,0,0,0,40.55,222h174.9a22.35,22.35,0,0,0,19.6-11.19A21.53,21.53,0,0,0,235.07,189.09ZM224.66,204.8a10.46,10.46,0,0,1-9.21,5.2H40.55a10.46,10.46,0,0,1-9.21-5.2,9.51,9.51,0,0,1,0-9.72L118.79,43.21a10.75,10.75,0,0,1,18.42,0l87.46,151.87A9.51,9.51,0,0,1,224.66,204.8ZM122,144V104a6,6,0,0,1,12,0v40a6,6,0,0,1-12,0Zm16,36a10,10,0,1,1-10-10A10,10,0,0,1,138,180Z';
1324
+ /**
1325
+ * Escapes a value for a double-quoted HTML attribute.
1326
+ *
1327
+ * The logo arrives through the generator's own filter, which admits only an `https` URL or an image
1328
+ * `data:` URI, but an SVG `data:` URI can legally carry quotes and angle brackets. Escaping here is
1329
+ * lossless — the parser decodes the entities back before the URL is read — so the attribute cannot
1330
+ * be closed early whatever the filter let through.
1331
+ */
1332
+ const escapeAttribute = (value) => value.replace(/&/gu, '&amp;').replace(/"/gu, '&quot;').replace(/</gu, '&lt;').replace(/>/gu, '&gt;');
1333
+ /**
1334
+ * The tab icon: the publisher's logo when one is configured, otherwise the page's own mark.
1335
+ *
1336
+ * Always declared, so the browser never requests `/favicon.ico` from the loopback listener — which
1337
+ * would otherwise answer a 404 to a request it only receives because the page declared no icon.
1338
+ *
1339
+ * The fallback is deliberately a status mark and not a vendor logo: this page is served by every
1340
+ * generated CLI, so a default icon identifying the SDK's *generator* would put one company's
1341
+ * branding on another's sign-in.
1342
+ *
1343
+ * The fallback's colours are a green and a red that read against a light and a dark tab strip
1344
+ * alike; `light-dark()` is no use here because the SVG is a separate document with no
1345
+ * `color-scheme` of its own. Only `#`, `<` and `>` are escaped, which is all a data URI in an
1346
+ * attribute needs, and the SVG quotes its attributes with apostrophes so the `href` can keep the
1347
+ * double quotes.
1348
+ */
1349
+ const favicon = (icon, ok, logo) => {
1350
+ if (logo)
1351
+ return '<link rel="icon" href="' + escapeAttribute(logo) + '">';
1352
+ const svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='" +
1353
+ (ok ? '#00b648' : '#dc1b19') +
1354
+ "'><path d='" +
1355
+ icon +
1356
+ "'/></svg>";
1357
+ return ('<link rel="icon" href="data:image/svg+xml,' +
1358
+ svg.replace(/#/gu, '%23').replace(/</gu, '%3C').replace(/>/gu, '%3E') +
1359
+ '">');
1360
+ };
1361
+ /**
1362
+ * Renders the redirect page.
1363
+ *
1364
+ * Inlined down to the icon, with nothing fetched but a configured logo: the listener answers on
1365
+ * loopback, where the machine may well have no route out at all, and a callback page that reached
1366
+ * for a stylesheet or a font would also tell whoever served it that a sign-in had just happened.
1367
+ * The logo is the one exception, because it is the publisher's own and the publisher's provider has
1368
+ * just seen the sign-in anyway. It is sent with no referrer, so the host serving it never sees the
1369
+ * loopback URL, and a `data:` URI keeps even that request from happening. A logo that fails to
1370
+ * load has an empty `alt`, so it leaves nothing on the page rather than a broken-image box.
1371
+ *
1372
+ * Nothing the authorization server sent is interpolated here. The provider's own `error` goes to
1373
+ * the terminal through `safeText` instead, which keeps untrusted text out of markup entirely rather
1374
+ * than relying on this function to escape it.
1375
+ */
1376
+ const redirectPage = (heading, detail, ok, logo) => {
1377
+ const icon = ok ? SIGNED_IN_ICON : WARNING_ICON;
1378
+ return ('<!doctype html><html lang="en"><meta charset="utf-8">' +
1379
+ '<meta name="viewport" content="width=device-width,initial-scale=1">' +
1380
+ '<meta name="referrer" content="no-referrer">' +
1381
+ favicon(icon, ok, logo) +
1382
+ '<title>' +
1383
+ heading +
1384
+ '</title><style>' +
1385
+ PAGE_STYLE +
1386
+ '</style><main>' +
1387
+ (logo ? '<img class="logo" src="' + escapeAttribute(logo) + '" alt="">' : '') +
1388
+ '<h1><svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="' +
1389
+ icon +
1390
+ '"/></svg>' +
1391
+ heading +
1392
+ '</h1><p>' +
1393
+ detail +
1394
+ '</p></main>');
1395
+ };
1396
+ /**
1397
+ * Resolves with the authorization code the redirect carries, holding the browser's request open.
1398
+ *
1399
+ * A redirect that fails a check here is answered at once. One that passes is left waiting, and the
1400
+ * caller answers it through `respond` once the rest of the sign-in has succeeded or failed.
1401
+ *
1402
+ * Anything that is not the redirect — a browser probing `/favicon.ico`, a stray request to the
1403
+ * port — is answered 404 and ignored, so it cannot resolve or reject the wait. The `state` is
1404
+ * compared in constant time and rejected on mismatch, which is what ties the redirect back to the
1405
+ * request this process started; the `iss` is then held to `issuer` (see {@link redirectIssuerProblem}).
1406
+ */
1407
+ const awaitRedirect = (server, state, path, logo, issuer, listener) => new Promise((resolve, reject) => {
1408
+ // Set once a redirect has resolved or rejected this wait. A later redirect — the user reloading
1409
+ // the tab while the first is still held — can change nothing, but it still deserves an answer:
1410
+ // left unanswered it would hang until the listener closed and then show an empty-response error.
1411
+ let settled = false;
1412
+ server.on('request', (request, response) => {
1413
+ const url = new URL(request.url ?? '/', 'http://127.0.0.1');
1414
+ const params = url.searchParams;
1415
+ // Listened for as soon as the request arrives, not when it is answered: a held redirect whose
1416
+ // browser gives up while the code is exchanged (a reload, a closed tab) has already fired its
1417
+ // `'close'` by the time it is answered, and a listener added then would never hear it —
1418
+ // leaving the flow to wait out `RESPONSE_FLUSH_TIMEOUT_MS` for a page nobody will read.
1419
+ const closed = new Promise((done) => response.once('close', () => done()));
1420
+ const send = (status, type, body) => {
1421
+ // `connection: close` rather than the default keep-alive: `server.close()` waits for open
1422
+ // connections, and a browser holding one would leave the CLI running after it had signed in.
1423
+ response.writeHead(status, { 'content-type': type + '; charset=utf-8', connection: 'close' });
1424
+ listener.answered.push(closed);
1425
+ response.end(body);
1426
+ };
1427
+ const finish = (status, heading, detail, ok) => {
1428
+ send(status, 'text/html', redirectPage(heading, detail, ok, logo));
1429
+ };
1430
+ // The path is part of what was registered as `redirect_uri`, so anything else is not the
1431
+ // redirect however it is decorated. Checked alongside the parameters rather than instead of
1432
+ // them: both are cheap, and together they keep this handler from reading a request that was
1433
+ // never the provider's as though it were.
1434
+ if (url.pathname !== path || (!params.has('code') && !params.has('error'))) {
1435
+ // Plain text, and deliberately not the page above: this answers a probe rather than a
1436
+ // person — a browser asking for `/favicon.ico`, or anything else that finds the port.
1437
+ send(404, 'text/plain', 'Not found.\n');
1438
+ return;
1439
+ }
1440
+ if (settled || listener.closing) {
1441
+ finish(409, 'Return to your terminal', 'This sign-in has already been received. Your terminal shows how it ended.', false);
1442
+ return;
1443
+ }
1444
+ settled = true;
1445
+ // scalar-sdk-generator:custom-code oauth-callback-singletons:start
1446
+ // Reject ambiguous callbacks before selecting any security-sensitive value.
1447
+ if (['state', 'iss', 'code', 'error'].some((name) => params.getAll(name).length > 1)) {
1448
+ finish(400, 'Sign-in could not be verified', 'This response contains repeated parameters. Return to your terminal and try again.', false);
1449
+ reject(new Error('The browser redirect contains repeated OAuth parameters.'));
1450
+ return;
1451
+ }
1452
+ // scalar-sdk-generator:custom-code oauth-callback-singletons:end
1453
+ if (!sameToken(params.get('state') ?? '', state)) {
1454
+ // Ends the sign-in rather than waiting for a better redirect. A request that reaches here is
1455
+ // on the registered path and carries a `code` or an `error`, which makes it the provider's
1456
+ // redirect rather than a stray probe — the path check above is what turns those away — so a
1457
+ // `state` that does not match is tampering or a stale tab, and neither is worth leaving the
1458
+ // user watching a terminal for `BROWSER_FLOW_TIMEOUT_MS` on the chance that a second,
1459
+ // better redirect arrives.
1460
+ finish(400, 'Sign-in could not be verified', 'This page did not match the sign-in your terminal started. Return to your terminal and try again.', false);
1461
+ reject(new Error('The browser redirect did not match this sign-in attempt.'));
1462
+ return;
1463
+ }
1464
+ // Checked before the `error` branch as well as the code: RFC 9207 puts `iss` on error
1465
+ // responses too, and an error page from a server that is not the expected one is no more its
1466
+ // provider's word than a code is.
1467
+ const issuerProblem = redirectIssuerProblem(params.get('iss'), issuer);
1468
+ if (issuerProblem) {
1469
+ finish(400, 'Sign-in could not be verified', 'This response did not come from the provider your terminal expected. Return to your terminal and try again.', false);
1470
+ reject(new Error(issuerProblem));
1471
+ return;
1472
+ }
1473
+ const failure = params.get('error');
1474
+ if (failure) {
1475
+ finish(400, 'Sign-in failed', 'Your provider declined the request. Return to your terminal for details.', false);
1476
+ reject(new Error('Authorization failed: ' + safeText(failure) + '.'));
1477
+ return;
1478
+ }
1479
+ const code = params.get('code');
1480
+ if (!code) {
1481
+ finish(400, 'Sign-in failed', 'The redirect carried no authorization code. Return to your terminal and try again.', false);
1482
+ reject(new Error('The browser redirect carried no authorization code.'));
1483
+ return;
1484
+ }
1485
+ let responded = false;
1486
+ resolve({
1487
+ code,
1488
+ respond: (heading, detail, ok) => {
1489
+ if (responded)
1490
+ return;
1491
+ responded = true;
1492
+ finish(ok ? 200 : 400, heading, detail, ok);
1493
+ },
1494
+ });
1495
+ });
1496
+ server.on('error', reject);
1497
+ });
1498
+ /**
1499
+ * Why a redirect's `iss` parameter (RFC 9207) is unacceptable, or `undefined` when it is fine.
1500
+ *
1501
+ * This is the mix-up-attack defence. `state` proves the redirect answers this process's request,
1502
+ * but not which authorization server sent it: a CLI that trusts more than one provider can have its
1503
+ * browser steered through an attacker's, and would then redeem the code that server handed back at
1504
+ * the honest provider's token endpoint — or hand an honest code to the attacker's. Naming the issuer
1505
+ * on the redirect, and refusing one that is not the server this sign-in began with, closes that
1506
+ * before the code is exchanged anywhere.
1507
+ *
1508
+ * With no expected issuer — a plain OAuth scheme and none configured — a received `iss` is
1509
+ * ignored: there is nothing to compare it with, so it can neither confirm nor refute the sender,
1510
+ * and refusing it would break every provider that sends one to clients that never asked.
1511
+ */
1512
+ const redirectIssuerProblem = (received, issuer) => {
1513
+ if (issuer.expected === undefined)
1514
+ return undefined;
1515
+ if (received === null) {
1516
+ return issuer.required
1517
+ ? 'The browser redirect did not say which provider sent it (no iss parameter), but this sign-in expected ' +
1518
+ safeText(issuer.expected) +
1519
+ '.'
1520
+ : undefined;
1521
+ }
1522
+ if (sameIssuer(received, issuer.expected))
1523
+ return undefined;
1524
+ return ('The browser redirect was sent by ' +
1525
+ (safeText(received) || 'an empty issuer') +
1526
+ ', but this sign-in expected ' +
1527
+ safeText(issuer.expected) +
1528
+ '.');
1529
+ };
1530
+ /** Starts the loopback listener, resolving with the port the OS actually bound. */
1531
+ const listen = (server, port) => new Promise((resolve, reject) => {
1532
+ server.once('error', reject);
1533
+ // 127.0.0.1 rather than every interface: the redirect is for this machine's browser alone.
1534
+ server.listen(port, '127.0.0.1', () => {
1535
+ const address = server.address();
1536
+ if (address && typeof address === 'object')
1537
+ resolve(address.port);
1538
+ else
1539
+ reject(new Error('Could not open a local port for the sign-in redirect.'));
1540
+ });
1541
+ });
1542
+ /**
1543
+ * Opens a URL in the user's browser, best effort.
1544
+ *
1545
+ * A missing opener is not an error: the URL was printed first precisely so the flow still completes
1546
+ * when this does nothing.
1547
+ *
1548
+ * The URL never reaches a shell. On Unix that is automatic — `open` and `xdg-open` are spawned
1549
+ * directly. Windows is the trap: `cmd /c start` would make cmd itself the shell, and Node quotes an
1550
+ * argument only when it contains a space, tab or quote, so every `&` in an authorization URL would
1551
+ * arrive at cmd as a command separator. That breaks the flow even on a benign document (the browser
1552
+ * opens on the first query parameter alone, with no `state` or `code_challenge`) and runs arbitrary
1553
+ * programs on a hostile one, since `&` survives URL normalization inside the path. So Windows gets
1554
+ * PowerShell with the URL in the environment, where nothing parses it as a command line at all.
1555
+ */
1556
+ const openBrowser = (url) => {
1557
+ const windows = process.platform === 'win32';
1558
+ const [file, ...args] = windows
1559
+ ? [windowsPowerShell(), '-NoProfile', '-NonInteractive', '-Command', '-']
1560
+ : process.platform === 'darwin'
1561
+ ? ['open', url]
1562
+ : ['xdg-open', url];
1563
+ try {
1564
+ const child = spawn(file, args, {
1565
+ stdio: windows ? ['pipe', 'ignore', 'ignore'] : 'ignore',
1566
+ detached: !windows,
1567
+ ...(windows ? { env: { ...process.env, SCALAR_BROWSER_URL: url } } : {}),
1568
+ });
1569
+ child.on('error', () => { });
1570
+ if (windows) {
1571
+ // `-FilePath` is explicit so a URL is never read as a PowerShell parameter, and the value comes
1572
+ // from the environment so it is not part of any command line.
1573
+ child.stdin?.end('Start-Process -FilePath $env:SCALAR_BROWSER_URL\n');
1574
+ }
1575
+ child.unref();
1576
+ }
1577
+ catch {
1578
+ // Ignored; see this function's doc comment.
1579
+ }
1580
+ };
1581
+ /**
1582
+ * Exchanges a grant for an access token.
1583
+ *
1584
+ * The endpoint comes from the OpenAPI document, which is untrusted input, so it is resolved against
1585
+ * the base URL and required to be HTTPS before a secret is put in the request body. Only the
1586
+ * standard `error`/`error_description` fields of a failure response are reported: a server that
1587
+ * echoed the submitted secret back would otherwise have it printed to stderr and into any CI log.
1588
+ */
1589
+ const exchangeToken = async (tokenUrl, baseUrl, body, timeoutMs = TOKEN_REQUEST_TIMEOUT_MS) => {
1590
+ const url = requireSecureUrl(tokenUrl, baseUrl, 'token endpoint');
1591
+ const { response, text } = await postForToken(url, body, timeoutMs);
1592
+ const payload = parseJson(text);
1593
+ if (!response.ok)
1594
+ throw new Error('The token endpoint rejected the request' + tokenErrorDetail(payload, response.status));
1595
+ return tokenResponse(payload);
1596
+ };
1597
+ /** Validates the standard OAuth token response fields before a token reaches the credential store. */
1598
+ const tokenResponse = (payload) => {
1599
+ const accessToken = payload?.['access_token'];
1600
+ if (typeof accessToken !== 'string' || !accessToken) {
1601
+ throw new Error('The token endpoint returned no access token.');
1602
+ }
1603
+ const refreshToken = payload?.['refresh_token'];
1604
+ const expiresIn = payload?.['expires_in'];
1605
+ const idToken = payload?.['id_token'];
1606
+ return {
1607
+ accessToken,
1608
+ ...(typeof refreshToken === 'string' && refreshToken ? { refreshToken } : {}),
1609
+ // Kept as sent, even when empty or not a string, so the OIDC flow refuses a malformed one rather
1610
+ // than treating it as absent.
1611
+ ...(idToken === undefined || idToken === null ? {} : { idToken: String(idToken) }),
1612
+ // A lifetime at or below zero is not a lifetime. Stored as one it makes `needsRefresh` true
1613
+ // forever, so every command pays a full refresh round trip ahead of the request it wanted —
1614
+ // on a token that is in fact perfectly good. The device grant already applies this floor to
1615
+ // its own `expires_in`; treating an unusable value as unstated puts the two in step.
1616
+ ...(typeof expiresIn === 'number' && Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresIn } : {}),
1617
+ };
1618
+ };
1619
+ /** Fetches a bounded JSON document under the same timeout policy used for token endpoints. */
1620
+ const getJson = async (url, label, timeoutMs = TOKEN_REQUEST_TIMEOUT_MS) => {
1621
+ try {
1622
+ const response = await fetch(url, {
1623
+ headers: { accept: 'application/json' },
1624
+ // Same reasoning as `postForToken`, for the document that *names* the endpoints rather than
1625
+ // the exchange itself: only the URL handed to `fetch` was checked, so a spec-derived
1626
+ // discovery endpoint that redirects would have its answer — the authorization and token URLs
1627
+ // this whole grant is then pointed at — supplied by whichever origin it forwarded to.
1628
+ redirect: 'error',
1629
+ signal: AbortSignal.timeout(timeoutMs),
1630
+ });
1631
+ return { response, text: await readBounded(response) };
1632
+ }
1633
+ catch (error) {
1634
+ const name = error instanceof Error ? error.name : '';
1635
+ throw new Error(name === 'TimeoutError' || name === 'AbortError'
1636
+ ? 'The ' + label + ' did not answer within ' + String(timeoutMs / 1000) + ' seconds.'
1637
+ : 'Could not reach the ' + label + '.');
1638
+ }
1639
+ };
1640
+ /**
1641
+ * Posts the grant and reads the answer, both under one deadline.
1642
+ *
1643
+ * The read is inside the same `try` as the request because the deadline covers it too: a server
1644
+ * that answers its headers and then stalls aborts here, and without this the user would see a raw
1645
+ * `The operation was aborted due to timeout` instead of a sentence naming the endpoint.
1646
+ *
1647
+ * The endpoint is spec-derived, so its own error text is never repeated beyond whether the wait ran
1648
+ * out: a `fetch` failure can carry the request, and the request carries the secret.
1649
+ *
1650
+ * `label` names the endpoint in those two messages, so a revocation that times out does not
1651
+ * claim to have been a token request.
1652
+ */
1653
+ const postForToken = async (url, body, timeoutMs = TOKEN_REQUEST_TIMEOUT_MS, label = 'token endpoint') => {
1654
+ try {
1655
+ const response = await fetch(url, {
1656
+ method: 'POST',
1657
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
1658
+ body: writeParams(new URLSearchParams(), body).toString(),
1659
+ // A 307/308 preserves the POST body. Do not let a spec-derived endpoint forward a refresh
1660
+ // token, authorization code, or device code to a different (possibly insecure) origin.
1661
+ redirect: 'error',
1662
+ signal: AbortSignal.timeout(timeoutMs),
1663
+ });
1664
+ return { response, text: await readBounded(response) };
1665
+ }
1666
+ catch (error) {
1667
+ // A deadline reached during the request is a `TimeoutError`; one reached while the body is
1668
+ // still arriving surfaces as the abort itself.
1669
+ const name = error instanceof Error ? error.name : '';
1670
+ throw new Error(name === 'TimeoutError' || name === 'AbortError'
1671
+ ? 'The ' + label + ' did not answer within ' + String(timeoutMs / 1000) + ' seconds.'
1672
+ : 'Could not reach the ' + label + '.');
1673
+ }
1674
+ };
1675
+ /**
1676
+ * Reads at most {@link TOKEN_RESPONSE_LIMIT} characters of a response body.
1677
+ *
1678
+ * `response.text()` would buffer whatever the endpoint chooses to send before anything could cap
1679
+ * it, so the stream is read in pieces and cancelled once there is more than enough to parse.
1680
+ */
1681
+ const readBounded = async (response) => {
1682
+ const body = response.body;
1683
+ if (!body)
1684
+ return '';
1685
+ const reader = body.getReader();
1686
+ const decoder = new TextDecoder();
1687
+ let text = '';
1688
+ try {
1689
+ while (text.length < TOKEN_RESPONSE_LIMIT) {
1690
+ const { done, value } = await reader.read();
1691
+ if (done)
1692
+ break;
1693
+ text += decoder.decode(value, { stream: true });
1694
+ }
1695
+ }
1696
+ finally {
1697
+ // Nothing is read after this point, and an abandoned body keeps the socket open.
1698
+ void reader.cancel().catch(() => { });
1699
+ }
1700
+ return text.slice(0, TOKEN_RESPONSE_LIMIT);
1701
+ };
1702
+ const parseJson = (text) => {
1703
+ try {
1704
+ const parsed = JSON.parse(text);
1705
+ return parsed && typeof parsed === 'object' ? parsed : undefined;
1706
+ }
1707
+ catch {
1708
+ return undefined;
1709
+ }
1710
+ };
1711
+ const tokenErrorDetail = (payload, status) => {
1712
+ const code = payload?.['error'];
1713
+ const description = payload?.['error_description'];
1714
+ const parts = [
1715
+ typeof code === 'string' ? code : undefined,
1716
+ typeof description === 'string' ? description : undefined,
1717
+ ]
1718
+ .filter((part) => !!part)
1719
+ .map(safeText);
1720
+ return parts.length > 0 ? ' (' + parts.join(': ') + ').' : ' with status ' + String(status) + '.';
1721
+ };
1722
+ /**
1723
+ * Resolves a spec-derived endpoint and refuses to send credentials to it in the clear.
1724
+ *
1725
+ * HTTPS is required, with the usual carve-out for loopback so a local development authorization
1726
+ * server still works. Without this, a document naming an `http://` token endpoint would have the
1727
+ * CLI post a client secret unencrypted to whatever host it named.
1728
+ */
1729
+ const requireSecureUrl = (raw, baseUrl, label) => {
1730
+ let url;
1731
+ try {
1732
+ url = new URL(raw, baseUrl || undefined);
1733
+ }
1734
+ catch {
1735
+ throw new Error('The ' + label + ' is not a usable URL.');
1736
+ }
1737
+ if (url.protocol === 'https:')
1738
+ return url;
1739
+ if (url.protocol === 'http:' && LOOPBACK_HOSTS.has(url.hostname))
1740
+ return url;
1741
+ throw new Error('Refusing to send credentials to a non-HTTPS ' +
1742
+ label +
1743
+ ' (' +
1744
+ safeText(url.protocol + '//' + url.host) +
1745
+ ').');
1746
+ };
1747
+ /**
1748
+ * Compares two opaque tokens without leaking their contents through timing.
1749
+ *
1750
+ * Lengths are compared first because `timingSafeEqual` throws on a mismatch; both values here are
1751
+ * fixed-length base64url, so an unequal length is already a mismatch.
1752
+ */
1753
+ const sameToken = (received, expected) => {
1754
+ const a = Buffer.from(received);
1755
+ const b = Buffer.from(expected);
1756
+ return a.length === b.length && timingSafeEqual(a, b);
1757
+ };
1758
+ const base64Url = (bytes) => bytes.toString('base64url');
1759
+ /**
1760
+ * Strips anything from a network-supplied string that a terminal would act on.
1761
+ *
1762
+ * An OAuth error message reaches stderr verbatim, and both the redirect query and the token
1763
+ * endpoint's response body are written by whoever the document points at, so control characters
1764
+ * (escape sequences that reposition the cursor or recolour the line) are removed and the result is
1765
+ * capped rather than printed as received.
1766
+ */
1767
+ const safeText = (value) => {
1768
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping them is the point.
1769
+ const stripped = value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, ' ').trim();
1770
+ return stripped.length > 200 ? stripped.slice(0, 200) + '...' : stripped;
1771
+ };
1772
+ const withTimeout = (promise, ms, message) => new Promise((resolve, reject) => {
1773
+ const timer = setTimeout(() => reject(new Error(message)), ms);
1774
+ promise.then((value) => {
1775
+ clearTimeout(timer);
1776
+ resolve(value);
1777
+ }, (error) => {
1778
+ clearTimeout(timer);
1779
+ reject(error instanceof Error ? error : new Error(String(error)));
1780
+ });
1781
+ });
1782
+ /**
1783
+ * A failure the user can fix by invoking the command differently, reported as a usage error.
1784
+ *
1785
+ * Distinguished from an authentication failure so a script can tell "you typed the wrong flow name"
1786
+ * (retrying will not help) from "the credential was rejected" (a fresh sign-in might).
1787
+ */
1788
+ export class UsageError extends Error {
1789
+ }
1790
+ const NO_TERMINAL = 'Sign-in needs an interactive terminal. Run it from a terminal, pick a flow that needs no prompt with --flow <name> (see --help), or set the credential through its flag or environment variable instead.';
1791
+ /** Puts the terminal cursor back, for the picker's own exit paths and for a signal that skips them. */
1792
+ const restoreCursor = () => {
1793
+ processStderr.write('\u001b[?25h');
1794
+ };
1795
+ /**
1796
+ * Puts a list of choices to the user and resolves the index of the one chosen.
1797
+ *
1798
+ * A highlight moved with the arrow keys rather than a number typed at a prompt, so the row being
1799
+ * chosen is the row being read. Only the highlighted row carries its `--flow` value, which is what
1800
+ * keeps a nine-line list from spelling out nine of them; the value is still on screen for whichever
1801
+ * flow the user is about to pick, which is the one they would want to write down.
1802
+ *
1803
+ * Raw mode is what delivers a keystroke without Enter behind it, and is required rather than fallen
1804
+ * back from, exactly as `promptSecret` requires it: a terminal Node reports as a TTY always offers
1805
+ * it, so the alternative was a branch that could not be reached to be tested. A digit still jumps to
1806
+ * its row, so the keys that answered the numbered prompt this replaces still answer this one.
1807
+ *
1808
+ * Drawn on stderr, like every other prompt, so stdout stays machine-readable.
1809
+ */
1810
+ const promptChoice = async (heading, choices) => {
1811
+ if (!processStdin.isTTY || typeof processStdin.setRawMode !== 'function') {
1812
+ throw new UsageError(NO_TERMINAL);
1813
+ }
1814
+ return new Promise((resolve, reject) => {
1815
+ let active = 0;
1816
+ // How many rows the last draw wrote, which is how far back up the cursor has to go to redraw
1817
+ // over them. Without it every keystroke would leave another copy of the list in the scrollback.
1818
+ let drawn = 0;
1819
+ const width = () => Number(processStderr.columns) || 80;
1820
+ /**
1821
+ * The slice of choices to draw, keeping the highlight inside it.
1822
+ *
1823
+ * A list taller than the terminal would scroll the region the redraw is about to move back over,
1824
+ * putting every subsequent frame in the wrong place, so a long list is windowed instead.
1825
+ */
1826
+ const window = () => {
1827
+ const room = Math.max(1, (Number(processStderr.rows) || 24) - 3);
1828
+ if (choices.length <= room)
1829
+ return choices;
1830
+ const start = Math.min(Math.max(0, active - (room >> 1)), choices.length - room);
1831
+ return choices.slice(start, start + room);
1832
+ };
1833
+ const render = () => {
1834
+ if (drawn > 0)
1835
+ processStderr.write('\u001b[' + String(drawn) + 'A');
1836
+ const columns = width();
1837
+ const shown = window();
1838
+ const lines = [clip(heading, columns)];
1839
+ for (const choice of shown) {
1840
+ const selected = choice === choices[active];
1841
+ const head = (selected ? '> ' : ' ') + choice.label;
1842
+ const tail = ' (--flow ' + choice.hint + ')';
1843
+ const plain = clip(head + tail, columns);
1844
+ // The flow name is kept on every row rather than only the highlighted one, because labels
1845
+ // are not unique: an API key in a header, in a query parameter and in a cookie all read as
1846
+ // `Enter your API key`, and the flow name is the only thing telling those rows apart. It is
1847
+ // dimmed so the labels still read as the list, and dropped entirely from a row too narrow
1848
+ // to hold it, where clipping has already eaten the closing bracket.
1849
+ const row = plain === head + tail ? head + '\u001b[2m' + tail + '\u001b[22m' : plain;
1850
+ lines.push(selected ? '\u001b[36m' + row + '\u001b[39m' : row);
1851
+ }
1852
+ const position = shown.length < choices.length ? String(active + 1) + '/' + String(choices.length) + ', ' : '';
1853
+ lines.push(clip(' ' + position + 'arrows to move, enter to choose', columns));
1854
+ // Each row is cleared to its end before being rewritten: a shorter line would otherwise leave
1855
+ // the tail of whatever longer one occupied that row in the frame before.
1856
+ processStderr.write(lines.map((line) => '\u001b[2K' + line).join('\n') + '\n');
1857
+ drawn = lines.length;
1858
+ };
1859
+ const move = (delta) => {
1860
+ active = (active + delta + choices.length) % choices.length;
1861
+ render();
1862
+ };
1863
+ const jump = (index) => {
1864
+ if (index < 0 || index >= choices.length)
1865
+ return;
1866
+ active = index;
1867
+ render();
1868
+ };
1869
+ const onData = (chunk) => {
1870
+ let index = 0;
1871
+ while (index < chunk.length) {
1872
+ const character = chunk[index] ?? '';
1873
+ if (character === '\u001b') {
1874
+ // An arrow is ESC [ A or ESC O A. The sequence is scanned to its final byte so that no
1875
+ // part of it is read as a keystroke in its own right — the `[` of an Up arrow would
1876
+ // otherwise be an ordinary printable character arriving between two frames.
1877
+ let cursor = index + 1;
1878
+ if (chunk[cursor] === '[' || chunk[cursor] === 'O') {
1879
+ cursor += 1;
1880
+ // A CSI sequence ends at its final byte, anything in the range `@` to `~`.
1881
+ while (cursor < chunk.length) {
1882
+ const byte = chunk[cursor] ?? '';
1883
+ if (byte >= '@' && byte <= '~')
1884
+ break;
1885
+ cursor += 1;
1886
+ }
1887
+ const final = chunk[cursor] ?? '';
1888
+ if (final === 'A')
1889
+ move(-1);
1890
+ else if (final === 'B')
1891
+ move(1);
1892
+ else if (final === 'H')
1893
+ jump(0);
1894
+ else if (final === 'F')
1895
+ jump(choices.length - 1);
1896
+ index = cursor + 1;
1897
+ continue;
1898
+ }
1899
+ index = cursor;
1900
+ continue;
1901
+ }
1902
+ index += 1;
1903
+ if (character === '\r' || character === '\n') {
1904
+ finish();
1905
+ return;
1906
+ }
1907
+ // Ctrl-C and Ctrl-D: raw mode has taken the terminal's own handling away, so the cancel has
1908
+ // to be honoured here or the picker could never be escaped.
1909
+ if (character === '\u0003' || character === '\u0004') {
1910
+ finish(new UsageError('Cancelled.'));
1911
+ return;
1912
+ }
1913
+ // Ctrl-P and Ctrl-N beside the vim keys, so the three habits a terminal user might arrive
1914
+ // with all work without anything on screen having to offer them.
1915
+ if (character === 'k' || character === '\u0010')
1916
+ move(-1);
1917
+ else if (character === 'j' || character === '\u000e')
1918
+ move(1);
1919
+ else if (character >= '1' && character <= '9')
1920
+ jump(Number(character) - 1);
1921
+ }
1922
+ };
1923
+ const finish = (error) => {
1924
+ processStdin.off('data', onData);
1925
+ processStdin.setRawMode(false);
1926
+ processStdin.pause();
1927
+ // The list is wound back over and replaced by the single line saying what was chosen: a menu
1928
+ // is worth reading while it is being answered and is clutter in the scrollback afterwards.
1929
+ if (drawn > 0)
1930
+ processStderr.write('\u001b[' + String(drawn) + 'A\u001b[0J');
1931
+ process.off('exit', restoreCursor);
1932
+ restoreCursor();
1933
+ if (error) {
1934
+ reject(error);
1935
+ return;
1936
+ }
1937
+ const chosen = choices[active];
1938
+ processStderr.write(heading + ' ' + (chosen?.label ?? '') + ' (--flow ' + (chosen?.hint ?? '') + ')\n');
1939
+ resolve(active);
1940
+ };
1941
+ // Hidden for the duration: the cursor would otherwise sit at the end of the last row drawn,
1942
+ // blinking somewhere that has nothing to do with the highlight. Restoring it is registered with
1943
+ // the hide rather than left to `finish`, because a signal that ends the process while the menu
1944
+ // is up reaches neither the key handler nor the rejection paths, and an invisible cursor
1945
+ // outlives the CLI it belonged to — the user's next shell prompt is where they find out.
1946
+ process.on('exit', restoreCursor);
1947
+ processStderr.write('\u001b[?25l');
1948
+ processStdin.setRawMode(true);
1949
+ processStdin.resume();
1950
+ processStdin.setEncoding('utf8');
1951
+ processStdin.on('data', onData);
1952
+ render();
1953
+ });
1954
+ };
1955
+ /** Reads one visible line from the terminal. Prompts go to stderr so stdout stays machine-readable. */
1956
+ const promptLine = (label) => new Promise((resolve, reject) => {
1957
+ if (!processStdin.isTTY) {
1958
+ reject(new UsageError(NO_TERMINAL));
1959
+ return;
1960
+ }
1961
+ const rl = createInterface({ input: processStdin, output: processStderr });
1962
+ let answered = false;
1963
+ // Closing the interface emits 'close' synchronously, so the guard is what keeps the end-of-input
1964
+ // fallback from resolving ahead of the answer that was just typed.
1965
+ rl.on('close', () => {
1966
+ if (!answered)
1967
+ resolve('');
1968
+ });
1969
+ rl.question(label, (answer) => {
1970
+ answered = true;
1971
+ resolve(answer.trim());
1972
+ rl.close();
1973
+ });
1974
+ });
1975
+ /**
1976
+ * Reads one secret from the terminal without echoing it.
1977
+ *
1978
+ * Raw mode is entered directly rather than muting a readline interface: readline exposes no
1979
+ * supported way to suppress its echo, and the credential would otherwise be left on screen and in
1980
+ * any recorded terminal session. Nothing typed here is written back, so there is no cursor to
1981
+ * manage — only the terminating newline is emitted.
1982
+ */
1983
+ const promptSecret = (label) => new Promise((resolve, reject) => {
1984
+ if (!processStdin.isTTY || typeof processStdin.setRawMode !== 'function') {
1985
+ reject(new UsageError(NO_TERMINAL));
1986
+ return;
1987
+ }
1988
+ processStderr.write(label);
1989
+ let value = '';
1990
+ // Where an escape sequence has got to: 0 outside one, 1 just after ESC, 2 inside CSI/SS3
1991
+ // parameters. Dropping the ESC alone is not enough — an Up arrow is ESC [ A, and the `[` and
1992
+ // `A` are ordinary printable bytes that would otherwise land in the middle of a credential the
1993
+ // user cannot see to correct.
1994
+ let escape = 0;
1995
+ const onData = (chunk) => {
1996
+ for (const character of chunk) {
1997
+ if (escape === 1) {
1998
+ escape = character === '[' || character === 'O' ? 2 : 0;
1999
+ continue;
2000
+ }
2001
+ if (escape === 2) {
2002
+ // A CSI sequence ends at its final byte, anything in the range `@` to `~`.
2003
+ if (character >= '@' && character <= '~')
2004
+ escape = 0;
2005
+ continue;
2006
+ }
2007
+ if (character === '\u001b') {
2008
+ escape = 1;
2009
+ continue;
2010
+ }
2011
+ if (character === '\r' || character === '\n') {
2012
+ finish();
2013
+ return;
2014
+ }
2015
+ // Ctrl-C and Ctrl-D: raw mode has taken the terminal's own handling away, so the cancel has
2016
+ // to be honoured here or the prompt could never be escaped.
2017
+ if (character === '\u0003') {
2018
+ finish(new UsageError('Cancelled.'));
2019
+ return;
2020
+ }
2021
+ if (character === '\u0004') {
2022
+ if (value)
2023
+ finish();
2024
+ else
2025
+ finish(new UsageError('Cancelled.'));
2026
+ return;
2027
+ }
2028
+ if (character === '\u007f' || character === '\b') {
2029
+ value = value.slice(0, -1);
2030
+ continue;
2031
+ }
2032
+ // Any other control byte is dropped for the same reason.
2033
+ if (character < ' ')
2034
+ continue;
2035
+ value += character;
2036
+ }
2037
+ };
2038
+ const finish = (error) => {
2039
+ processStdin.off('data', onData);
2040
+ processStdin.setRawMode(false);
2041
+ processStdin.pause();
2042
+ processStderr.write('\n');
2043
+ if (error)
2044
+ reject(error);
2045
+ else
2046
+ resolve(value);
2047
+ };
2048
+ processStdin.setRawMode(true);
2049
+ processStdin.resume();
2050
+ processStdin.setEncoding('utf8');
2051
+ processStdin.on('data', onData);
2052
+ });
2053
+ //# sourceMappingURL=login.js.map