@supatype/cli 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +221 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/assets/supatype-logo-wordmark.ascii.txt +6 -0
  5. package/bin/dev-entry.ts +2 -0
  6. package/bin/supatype.js +5 -0
  7. package/dist/app/framework.d.ts +44 -0
  8. package/dist/app/framework.d.ts.map +1 -0
  9. package/dist/app/framework.js +200 -0
  10. package/dist/app/framework.js.map +1 -0
  11. package/dist/app/proxy-dev-app.d.ts +13 -0
  12. package/dist/app/proxy-dev-app.d.ts.map +1 -0
  13. package/dist/app/proxy-dev-app.js +54 -0
  14. package/dist/app/proxy-dev-app.js.map +1 -0
  15. package/dist/app-config.d.ts +7 -0
  16. package/dist/app-config.d.ts.map +1 -0
  17. package/dist/app-config.js +113 -0
  18. package/dist/app-config.js.map +1 -0
  19. package/dist/assets/supatype-logo-wordmark.ascii.txt +6 -0
  20. package/dist/augmentation-generator.d.ts +2 -0
  21. package/dist/augmentation-generator.d.ts.map +1 -0
  22. package/dist/augmentation-generator.js +111 -0
  23. package/dist/augmentation-generator.js.map +1 -0
  24. package/dist/binary-cache.d.ts +98 -0
  25. package/dist/binary-cache.d.ts.map +1 -0
  26. package/dist/binary-cache.js +687 -0
  27. package/dist/binary-cache.js.map +1 -0
  28. package/dist/cli.d.ts +2 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +61 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/commands/admin.d.ts +4 -0
  33. package/dist/commands/admin.d.ts.map +1 -0
  34. package/dist/commands/admin.js +271 -0
  35. package/dist/commands/admin.js.map +1 -0
  36. package/dist/commands/app.d.ts +3 -0
  37. package/dist/commands/app.d.ts.map +1 -0
  38. package/dist/commands/app.js +82 -0
  39. package/dist/commands/app.js.map +1 -0
  40. package/dist/commands/cache.d.ts +6 -0
  41. package/dist/commands/cache.d.ts.map +1 -0
  42. package/dist/commands/cache.js +105 -0
  43. package/dist/commands/cache.js.map +1 -0
  44. package/dist/commands/cloud.d.ts +23 -0
  45. package/dist/commands/cloud.d.ts.map +1 -0
  46. package/dist/commands/cloud.js +254 -0
  47. package/dist/commands/cloud.js.map +1 -0
  48. package/dist/commands/db.d.ts +8 -0
  49. package/dist/commands/db.d.ts.map +1 -0
  50. package/dist/commands/db.js +116 -0
  51. package/dist/commands/db.js.map +1 -0
  52. package/dist/commands/deploy-types.d.ts +14 -0
  53. package/dist/commands/deploy-types.d.ts.map +1 -0
  54. package/dist/commands/deploy-types.js +38 -0
  55. package/dist/commands/deploy-types.js.map +1 -0
  56. package/dist/commands/deploy.d.ts +15 -0
  57. package/dist/commands/deploy.d.ts.map +1 -0
  58. package/dist/commands/deploy.js +322 -0
  59. package/dist/commands/deploy.js.map +1 -0
  60. package/dist/commands/dev.d.ts +14 -0
  61. package/dist/commands/dev.d.ts.map +1 -0
  62. package/dist/commands/dev.js +806 -0
  63. package/dist/commands/dev.js.map +1 -0
  64. package/dist/commands/diff.d.ts +3 -0
  65. package/dist/commands/diff.d.ts.map +1 -0
  66. package/dist/commands/diff.js +54 -0
  67. package/dist/commands/diff.js.map +1 -0
  68. package/dist/commands/engine.d.ts +7 -0
  69. package/dist/commands/engine.d.ts.map +1 -0
  70. package/dist/commands/engine.js +27 -0
  71. package/dist/commands/engine.js.map +1 -0
  72. package/dist/commands/functions.d.ts +3 -0
  73. package/dist/commands/functions.d.ts.map +1 -0
  74. package/dist/commands/functions.js +749 -0
  75. package/dist/commands/functions.js.map +1 -0
  76. package/dist/commands/generate.d.ts +3 -0
  77. package/dist/commands/generate.d.ts.map +1 -0
  78. package/dist/commands/generate.js +38 -0
  79. package/dist/commands/generate.js.map +1 -0
  80. package/dist/commands/init.d.ts +7 -0
  81. package/dist/commands/init.d.ts.map +1 -0
  82. package/dist/commands/init.js +228 -0
  83. package/dist/commands/init.js.map +1 -0
  84. package/dist/commands/keys.d.ts +4 -0
  85. package/dist/commands/keys.d.ts.map +1 -0
  86. package/dist/commands/keys.js +57 -0
  87. package/dist/commands/keys.js.map +1 -0
  88. package/dist/commands/logs.d.ts +6 -0
  89. package/dist/commands/logs.d.ts.map +1 -0
  90. package/dist/commands/logs.js +52 -0
  91. package/dist/commands/logs.js.map +1 -0
  92. package/dist/commands/migrate-from-v1.d.ts +5 -0
  93. package/dist/commands/migrate-from-v1.d.ts.map +1 -0
  94. package/dist/commands/migrate-from-v1.js +125 -0
  95. package/dist/commands/migrate-from-v1.js.map +1 -0
  96. package/dist/commands/migrate.d.ts +3 -0
  97. package/dist/commands/migrate.d.ts.map +1 -0
  98. package/dist/commands/migrate.js +75 -0
  99. package/dist/commands/migrate.js.map +1 -0
  100. package/dist/commands/pg.d.ts +8 -0
  101. package/dist/commands/pg.d.ts.map +1 -0
  102. package/dist/commands/pg.js +102 -0
  103. package/dist/commands/pg.js.map +1 -0
  104. package/dist/commands/plugins.d.ts +3 -0
  105. package/dist/commands/plugins.d.ts.map +1 -0
  106. package/dist/commands/plugins.js +431 -0
  107. package/dist/commands/plugins.js.map +1 -0
  108. package/dist/commands/pull.d.ts +3 -0
  109. package/dist/commands/pull.d.ts.map +1 -0
  110. package/dist/commands/pull.js +12 -0
  111. package/dist/commands/pull.js.map +1 -0
  112. package/dist/commands/push.d.ts +3 -0
  113. package/dist/commands/push.d.ts.map +1 -0
  114. package/dist/commands/push.js +179 -0
  115. package/dist/commands/push.js.map +1 -0
  116. package/dist/commands/seed.d.ts +5 -0
  117. package/dist/commands/seed.d.ts.map +1 -0
  118. package/dist/commands/seed.js +55 -0
  119. package/dist/commands/seed.js.map +1 -0
  120. package/dist/commands/self-host.d.ts +9 -0
  121. package/dist/commands/self-host.d.ts.map +1 -0
  122. package/dist/commands/self-host.js +310 -0
  123. package/dist/commands/self-host.js.map +1 -0
  124. package/dist/commands/self-update.d.ts +9 -0
  125. package/dist/commands/self-update.d.ts.map +1 -0
  126. package/dist/commands/self-update.js +33 -0
  127. package/dist/commands/self-update.js.map +1 -0
  128. package/dist/commands/status.d.ts +6 -0
  129. package/dist/commands/status.d.ts.map +1 -0
  130. package/dist/commands/status.js +70 -0
  131. package/dist/commands/status.js.map +1 -0
  132. package/dist/commands/types.d.ts +3 -0
  133. package/dist/commands/types.d.ts.map +1 -0
  134. package/dist/commands/types.js +62 -0
  135. package/dist/commands/types.js.map +1 -0
  136. package/dist/commands/update.d.ts +7 -0
  137. package/dist/commands/update.d.ts.map +1 -0
  138. package/dist/commands/update.js +118 -0
  139. package/dist/commands/update.js.map +1 -0
  140. package/dist/components.d.ts +5 -0
  141. package/dist/components.d.ts.map +1 -0
  142. package/dist/components.js +3 -0
  143. package/dist/components.js.map +1 -0
  144. package/dist/config.d.ts +65 -0
  145. package/dist/config.d.ts.map +1 -0
  146. package/dist/config.js +134 -0
  147. package/dist/config.js.map +1 -0
  148. package/dist/dev-compose.d.ts +19 -0
  149. package/dist/dev-compose.d.ts.map +1 -0
  150. package/dist/dev-compose.js +468 -0
  151. package/dist/dev-compose.js.map +1 -0
  152. package/dist/dev-log-bus.d.ts +30 -0
  153. package/dist/dev-log-bus.d.ts.map +1 -0
  154. package/dist/dev-log-bus.js +87 -0
  155. package/dist/dev-log-bus.js.map +1 -0
  156. package/dist/dev-log-filter.d.ts +10 -0
  157. package/dist/dev-log-filter.d.ts.map +1 -0
  158. package/dist/dev-log-filter.js +36 -0
  159. package/dist/dev-log-filter.js.map +1 -0
  160. package/dist/dev-logo.d.ts +12 -0
  161. package/dist/dev-logo.d.ts.map +1 -0
  162. package/dist/dev-logo.js +57 -0
  163. package/dist/dev-logo.js.map +1 -0
  164. package/dist/dev-session.d.ts +26 -0
  165. package/dist/dev-session.d.ts.map +1 -0
  166. package/dist/dev-session.js +106 -0
  167. package/dist/dev-session.js.map +1 -0
  168. package/dist/dev-shutdown.d.ts +9 -0
  169. package/dist/dev-shutdown.d.ts.map +1 -0
  170. package/dist/dev-shutdown.js +50 -0
  171. package/dist/dev-shutdown.js.map +1 -0
  172. package/dist/dev-task-colors.d.ts +14 -0
  173. package/dist/dev-task-colors.d.ts.map +1 -0
  174. package/dist/dev-task-colors.js +44 -0
  175. package/dist/dev-task-colors.js.map +1 -0
  176. package/dist/dev-tui.d.ts +24 -0
  177. package/dist/dev-tui.d.ts.map +1 -0
  178. package/dist/dev-tui.js +188 -0
  179. package/dist/dev-tui.js.map +1 -0
  180. package/dist/diff-output.d.ts +4 -0
  181. package/dist/diff-output.d.ts.map +1 -0
  182. package/dist/diff-output.js +12 -0
  183. package/dist/diff-output.js.map +1 -0
  184. package/dist/docker-postgres.d.ts +57 -0
  185. package/dist/docker-postgres.d.ts.map +1 -0
  186. package/dist/docker-postgres.js +208 -0
  187. package/dist/docker-postgres.js.map +1 -0
  188. package/dist/engine-client.d.ts +69 -0
  189. package/dist/engine-client.d.ts.map +1 -0
  190. package/dist/engine-client.js +157 -0
  191. package/dist/engine-client.js.map +1 -0
  192. package/dist/engine-push-output.d.ts +16 -0
  193. package/dist/engine-push-output.d.ts.map +1 -0
  194. package/dist/engine-push-output.js +61 -0
  195. package/dist/engine-push-output.js.map +1 -0
  196. package/dist/ensure-binary.d.ts +7 -0
  197. package/dist/ensure-binary.d.ts.map +1 -0
  198. package/dist/ensure-binary.js +17 -0
  199. package/dist/ensure-binary.js.map +1 -0
  200. package/dist/functions-router-gen.d.ts +14 -0
  201. package/dist/functions-router-gen.d.ts.map +1 -0
  202. package/dist/functions-router-gen.js +199 -0
  203. package/dist/functions-router-gen.js.map +1 -0
  204. package/dist/index.d.ts +11 -0
  205. package/dist/index.d.ts.map +1 -0
  206. package/dist/index.js +9 -0
  207. package/dist/index.js.map +1 -0
  208. package/dist/jwt.d.ts +3 -0
  209. package/dist/jwt.d.ts.map +1 -0
  210. package/dist/jwt.js +13 -0
  211. package/dist/jwt.js.map +1 -0
  212. package/dist/kong-config.d.ts +25 -0
  213. package/dist/kong-config.d.ts.map +1 -0
  214. package/dist/kong-config.js +71 -0
  215. package/dist/kong-config.js.map +1 -0
  216. package/dist/local-gateway.d.ts +7 -0
  217. package/dist/local-gateway.d.ts.map +1 -0
  218. package/dist/local-gateway.js +9 -0
  219. package/dist/local-gateway.js.map +1 -0
  220. package/dist/local-storage.d.ts +8 -0
  221. package/dist/local-storage.d.ts.map +1 -0
  222. package/dist/local-storage.js +14 -0
  223. package/dist/local-storage.js.map +1 -0
  224. package/dist/pgbouncer-userlist.d.ts +5 -0
  225. package/dist/pgbouncer-userlist.d.ts.map +1 -0
  226. package/dist/pgbouncer-userlist.js +14 -0
  227. package/dist/pgbouncer-userlist.js.map +1 -0
  228. package/dist/postgres-ctl.d.ts +44 -0
  229. package/dist/postgres-ctl.d.ts.map +1 -0
  230. package/dist/postgres-ctl.js +137 -0
  231. package/dist/postgres-ctl.js.map +1 -0
  232. package/dist/process-manager.d.ts +49 -0
  233. package/dist/process-manager.d.ts.map +1 -0
  234. package/dist/process-manager.js +177 -0
  235. package/dist/process-manager.js.map +1 -0
  236. package/dist/project-config.d.ts +238 -0
  237. package/dist/project-config.d.ts.map +1 -0
  238. package/dist/project-config.js +159 -0
  239. package/dist/project-config.js.map +1 -0
  240. package/dist/pull-utils.d.ts +31 -0
  241. package/dist/pull-utils.d.ts.map +1 -0
  242. package/dist/pull-utils.js +77 -0
  243. package/dist/pull-utils.js.map +1 -0
  244. package/dist/release-pins.d.ts +7 -0
  245. package/dist/release-pins.d.ts.map +1 -0
  246. package/dist/release-pins.js +27 -0
  247. package/dist/release-pins.js.map +1 -0
  248. package/dist/release-public-key.d.ts +8 -0
  249. package/dist/release-public-key.d.ts.map +1 -0
  250. package/dist/release-public-key.js +13 -0
  251. package/dist/release-public-key.js.map +1 -0
  252. package/dist/restore-system-relation-targets.d.ts +3 -0
  253. package/dist/restore-system-relation-targets.d.ts.map +1 -0
  254. package/dist/restore-system-relation-targets.js +45 -0
  255. package/dist/restore-system-relation-targets.js.map +1 -0
  256. package/dist/runtime-routes.d.ts +34 -0
  257. package/dist/runtime-routes.d.ts.map +1 -0
  258. package/dist/runtime-routes.js +252 -0
  259. package/dist/runtime-routes.js.map +1 -0
  260. package/dist/schema-ast-v2.d.ts +127 -0
  261. package/dist/schema-ast-v2.d.ts.map +1 -0
  262. package/dist/schema-ast-v2.js +226 -0
  263. package/dist/schema-ast-v2.js.map +1 -0
  264. package/dist/scripts/postinstall.d.ts +11 -0
  265. package/dist/scripts/postinstall.d.ts.map +1 -0
  266. package/dist/scripts/postinstall.js +47 -0
  267. package/dist/scripts/postinstall.js.map +1 -0
  268. package/dist/seed.d.ts +8 -0
  269. package/dist/seed.d.ts.map +1 -0
  270. package/dist/seed.js +32 -0
  271. package/dist/seed.js.map +1 -0
  272. package/dist/self-host-compose.d.ts +43 -0
  273. package/dist/self-host-compose.d.ts.map +1 -0
  274. package/dist/self-host-compose.js +400 -0
  275. package/dist/self-host-compose.js.map +1 -0
  276. package/dist/storage-provision.d.ts +24 -0
  277. package/dist/storage-provision.d.ts.map +1 -0
  278. package/dist/storage-provision.js +44 -0
  279. package/dist/storage-provision.js.map +1 -0
  280. package/dist/studio-admin-roles.d.ts +7 -0
  281. package/dist/studio-admin-roles.d.ts.map +1 -0
  282. package/dist/studio-admin-roles.js +14 -0
  283. package/dist/studio-admin-roles.js.map +1 -0
  284. package/dist/studio-dev-server.d.ts +22 -0
  285. package/dist/studio-dev-server.d.ts.map +1 -0
  286. package/dist/studio-dev-server.js +28 -0
  287. package/dist/studio-dev-server.js.map +1 -0
  288. package/dist/supatype-eval-1781522769253.d.mts +2 -0
  289. package/dist/supatype-eval-1781522769253.d.mts.map +1 -0
  290. package/dist/supatype-eval-1781522769253.mjs +3 -0
  291. package/dist/supatype-eval-1781522769253.mjs.map +1 -0
  292. package/dist/systemd.d.ts +26 -0
  293. package/dist/systemd.d.ts.map +1 -0
  294. package/dist/systemd.js +102 -0
  295. package/dist/systemd.js.map +1 -0
  296. package/dist/tsx-runner.d.ts +18 -0
  297. package/dist/tsx-runner.d.ts.map +1 -0
  298. package/dist/tsx-runner.js +69 -0
  299. package/dist/tsx-runner.js.map +1 -0
  300. package/dist/type-extractor.d.ts +4 -0
  301. package/dist/type-extractor.d.ts.map +1 -0
  302. package/dist/type-extractor.js +1213 -0
  303. package/dist/type-extractor.js.map +1 -0
  304. package/dist/type-resolver.d.ts +33 -0
  305. package/dist/type-resolver.d.ts.map +1 -0
  306. package/dist/type-resolver.js +338 -0
  307. package/dist/type-resolver.js.map +1 -0
  308. package/package.json +41 -0
  309. package/releases/deno/VERSION +1 -0
  310. package/scripts/mirror-deno-release.sh +76 -0
  311. package/src/TYPE-RESOLUTION.md +294 -0
  312. package/src/app/framework.ts +249 -0
  313. package/src/app/proxy-dev-app.ts +68 -0
  314. package/src/app-config.ts +128 -0
  315. package/src/augmentation-generator.ts +126 -0
  316. package/src/binary-cache.ts +845 -0
  317. package/src/cli.ts +63 -0
  318. package/src/commands/admin.ts +372 -0
  319. package/src/commands/app.ts +97 -0
  320. package/src/commands/cache.ts +117 -0
  321. package/src/commands/cloud.ts +325 -0
  322. package/src/commands/db.ts +136 -0
  323. package/src/commands/deploy-types.ts +49 -0
  324. package/src/commands/deploy.ts +400 -0
  325. package/src/commands/dev.ts +1009 -0
  326. package/src/commands/diff.ts +63 -0
  327. package/src/commands/engine.ts +30 -0
  328. package/src/commands/functions.ts +901 -0
  329. package/src/commands/generate.ts +44 -0
  330. package/src/commands/init.ts +253 -0
  331. package/src/commands/keys.ts +66 -0
  332. package/src/commands/logs.ts +58 -0
  333. package/src/commands/migrate-from-v1.ts +131 -0
  334. package/src/commands/migrate.ts +87 -0
  335. package/src/commands/pg.ts +133 -0
  336. package/src/commands/plugins.ts +508 -0
  337. package/src/commands/pull.ts +17 -0
  338. package/src/commands/push.ts +226 -0
  339. package/src/commands/seed.ts +68 -0
  340. package/src/commands/self-host.ts +364 -0
  341. package/src/commands/self-update.ts +45 -0
  342. package/src/commands/status.ts +84 -0
  343. package/src/commands/types.ts +76 -0
  344. package/src/commands/update.ts +136 -0
  345. package/src/components.ts +6 -0
  346. package/src/config.ts +223 -0
  347. package/src/dev-compose.ts +583 -0
  348. package/src/dev-log-bus.ts +101 -0
  349. package/src/dev-log-filter.ts +32 -0
  350. package/src/dev-logo.ts +62 -0
  351. package/src/dev-session.ts +130 -0
  352. package/src/dev-shutdown.ts +54 -0
  353. package/src/dev-task-colors.ts +47 -0
  354. package/src/dev-tui.ts +232 -0
  355. package/src/diff-output.ts +12 -0
  356. package/src/docker-postgres.ts +295 -0
  357. package/src/engine-client.ts +236 -0
  358. package/src/engine-push-output.ts +71 -0
  359. package/src/ensure-binary.ts +28 -0
  360. package/src/functions-router-gen.ts +224 -0
  361. package/src/index.ts +11 -0
  362. package/src/jwt.ts +14 -0
  363. package/src/kong-config.ts +93 -0
  364. package/src/local-gateway.ts +9 -0
  365. package/src/local-storage.ts +14 -0
  366. package/src/pgbouncer-userlist.ts +15 -0
  367. package/src/postgres-ctl.ts +171 -0
  368. package/src/process-manager.ts +220 -0
  369. package/src/project-config.ts +388 -0
  370. package/src/pull-utils.ts +81 -0
  371. package/src/release-pins.ts +31 -0
  372. package/src/release-public-key.ts +12 -0
  373. package/src/restore-system-relation-targets.ts +45 -0
  374. package/src/runtime-routes.ts +291 -0
  375. package/src/schema-ast-v2.ts +324 -0
  376. package/src/scripts/postinstall.ts +51 -0
  377. package/src/seed.ts +43 -0
  378. package/src/self-host-compose.ts +452 -0
  379. package/src/storage-provision.ts +58 -0
  380. package/src/studio-admin-roles.ts +16 -0
  381. package/src/studio-dev-server.ts +53 -0
  382. package/src/supatype-eval-1781522769253.mts +1 -0
  383. package/src/systemd.ts +137 -0
  384. package/src/tsx-runner.ts +89 -0
  385. package/src/type-extractor.ts +1479 -0
  386. package/src/type-resolver.ts +457 -0
  387. package/tests/app-command.test.ts +54 -0
  388. package/tests/augmentation-generator.test.ts +59 -0
  389. package/tests/binary-cache-cloud-overrides.test.ts +123 -0
  390. package/tests/cached-artifact-format.test.ts +84 -0
  391. package/tests/cli-help.test.ts +133 -0
  392. package/tests/config.test.ts +252 -0
  393. package/tests/dev-ui.test.ts +139 -0
  394. package/tests/docker-postgres.test.ts +39 -0
  395. package/tests/engine-distribution.test.ts +418 -0
  396. package/tests/engine-push-output.test.ts +67 -0
  397. package/tests/ensure-binary.test.ts +59 -0
  398. package/tests/init.test.ts +127 -0
  399. package/tests/keys.test.ts +160 -0
  400. package/tests/migrate-from-v1.test.ts +29 -0
  401. package/tests/normalize-admin-config.test.ts +48 -0
  402. package/tests/pg-spawn-env.test.ts +18 -0
  403. package/tests/postgres-archive-tag.test.ts +9 -0
  404. package/tests/proxy-dev-app.test.ts +33 -0
  405. package/tests/pull-utils.test.ts +150 -0
  406. package/tests/release-pins.test.ts +28 -0
  407. package/tests/runtime-contract.test.ts +370 -0
  408. package/tests/seed-discover.test.ts +31 -0
  409. package/tests/studio-admin-roles.test.ts +27 -0
  410. package/tests/tsconfig.json +9 -0
  411. package/tests/tsx-runner.test.ts +66 -0
  412. package/tests/type-extractor.test.ts +985 -0
  413. package/tests/type-resolver.test.ts +59 -0
  414. package/tsconfig.json +10 -0
  415. package/tsconfig.tsbuildinfo +1 -0
  416. package/vitest.config.ts +12 -0
@@ -0,0 +1,845 @@
1
+ /**
2
+ * Binary cache — manages supatype component binaries.
3
+ *
4
+ * Components: engine, server, postgres, deno.
5
+ * Cache root: ~/.supatype/cache/{component}/{version}/
6
+ * Override path: config.overrides?.{component} (local build path).
7
+ *
8
+ * Security model:
9
+ * 1. Download checksums.sha256 + checksums.sha256.minisig from CDN.
10
+ * 2. Verify Ed25519 minisign signature on the checksum file using the
11
+ * embedded public key (SUPATYPE_RELEASE_PUBLIC_KEY).
12
+ * 3. Verify SHA256 of the downloaded binary against the signed checksum.
13
+ * Both checks are mandatory when SUPATYPE_RELEASE_PUBLIC_KEY is set.
14
+ */
15
+
16
+ import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto"
17
+ import {
18
+ closeSync,
19
+ copyFileSync,
20
+ createWriteStream,
21
+ existsSync,
22
+ mkdirSync,
23
+ openSync,
24
+ readFileSync,
25
+ readSync,
26
+ statSync,
27
+ unlinkSync,
28
+ writeFileSync,
29
+ } from "node:fs"
30
+ import { chmod } from "node:fs/promises"
31
+ import { homedir } from "node:os"
32
+ import { basename, join, resolve, isAbsolute } from "node:path"
33
+ import type { SupatypeProjectConfig } from "./project-config.js"
34
+ import { releasePublicKey } from "./release-public-key.js"
35
+
36
+ /**
37
+ * Set `versions.{engine|server|postgres|deno}: VERSION_PIN_LOCAL` to mean “use `overrides.*` only”
38
+ * without duplicating the path string (Phase 10.7). Requires the matching `overrides` entry.
39
+ */
40
+ export const VERSION_PIN_LOCAL = "local"
41
+
42
+ /** True if `overrides.engine` points at a local engine binary (contributor dev). */
43
+ export function hasEngineOverride(config: SupatypeProjectConfig): boolean {
44
+ const path = config.overrides?.engine
45
+ return typeof path === "string" && path.trim() !== ""
46
+ }
47
+
48
+ export function hasStudioOverride(config: SupatypeProjectConfig): boolean {
49
+ const path = config.overrides?.studio
50
+ return typeof path === "string" && path.trim() !== ""
51
+ }
52
+
53
+ /** True if `overrides` contains any non-empty string path (contributor local builds). */
54
+ export function hasMeaningfulOverrides(config: SupatypeProjectConfig): boolean {
55
+ const o = config.overrides
56
+ if (!o) return false
57
+ for (const v of Object.values(o)) {
58
+ if (typeof v === "string" && v.trim() !== "") return true
59
+ }
60
+ return false
61
+ }
62
+
63
+ /** Lines for a startup banner — non-empty override paths only. */
64
+ export function describeActiveOverrides(config: SupatypeProjectConfig): string[] {
65
+ const o = config.overrides
66
+ if (!o) return []
67
+ const lines: string[] = []
68
+ const add = (label: string, v: string | undefined) => {
69
+ if (typeof v === "string" && v.trim() !== "") {
70
+ lines.push(` ${label.padEnd(12)} → ${v.trim()}`)
71
+ }
72
+ }
73
+ add("engine", o.engine)
74
+ add("server", o.server)
75
+ add("postgres_dir", o.postgres_dir)
76
+ add("deno", o.deno)
77
+ add("studio", o.studio)
78
+ add("postgrest", o.postgrest)
79
+ return lines
80
+ }
81
+
82
+ /**
83
+ * True when this working tree is associated with a remote Supatype Cloud project:
84
+ * `project.ref`, `.supatype/cloud.json` (schema deploy link), or `.supatype/linked.json` (functions link).
85
+ */
86
+ export function isLinkedToCloudProject(cwd: string, config: SupatypeProjectConfig): boolean {
87
+ const ref = config.project.ref
88
+ if (typeof ref === "string" && ref.trim() !== "") return true
89
+
90
+ const linkedPath = join(cwd, ".supatype", "linked.json")
91
+ if (existsSync(linkedPath)) {
92
+ try {
93
+ const data = JSON.parse(readFileSync(linkedPath, "utf8")) as Record<string, unknown>
94
+ if (typeof data["ref"] === "string" && (data["ref"] as string).trim() !== "") return true
95
+ } catch { /* ignore */ }
96
+ }
97
+
98
+ const cloudPath = join(cwd, ".supatype", "cloud.json")
99
+ if (existsSync(cloudPath)) {
100
+ try {
101
+ const data = JSON.parse(readFileSync(cloudPath, "utf8")) as { projectSlug?: string }
102
+ if (typeof data.projectSlug === "string" && data.projectSlug.trim() !== "") return true
103
+ } catch { /* ignore */ }
104
+ }
105
+
106
+ return false
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Types
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export type { Component, ComponentVersions } from "./components.js"
114
+ export { BINARY_COMPONENTS } from "./components.js"
115
+ import { BINARY_COMPONENTS, type Component, type ComponentVersions } from "./components.js"
116
+
117
+ export interface PlatformId {
118
+ os: "linux" | "darwin" | "windows"
119
+ arch: "amd64" | "arm64"
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // CDN base URL + release signing public key
124
+ // ---------------------------------------------------------------------------
125
+
126
+ const CDN_BASE = "https://releases.supatype.com"
127
+
128
+ /** Postgres CDN archives use PG major in the basename (17.2 → `supatype-pg-17-…`). */
129
+ export function postgresArchiveTag(version: string): string {
130
+ return version.split(".")[0]!
131
+ }
132
+
133
+ /**
134
+ * Supatype release signing public key (minisign format).
135
+ * Generated with: minisign -G
136
+ * Rotate by: generating a new pair, updating this constant, and updating
137
+ * the MINISIGN_PRIVATE_KEY GitHub Actions secret.
138
+ *
139
+ * ⚠ PLACEHOLDER — replace with actual public key before first release.
140
+ * When empty, minisign verification is skipped with a warning (SHA256 only).
141
+ */
142
+ const SUPATYPE_RELEASE_PUBLIC_KEY = ""
143
+
144
+ // CDN path templates per component.
145
+ const CDN_PATHS: Record<Component, (version: string, platform: PlatformId) => string> = {
146
+ engine: (v, p) => `/engine/v${v}/supatype-engine-${p.os}-${p.arch}${p.os === "windows" ? ".exe" : ""}`,
147
+ server: (v, p) => `/server/v${v}/supatype-server-${p.os}-${p.arch}${p.os === "windows" ? ".exe" : ""}`,
148
+ postgres: (v, p) => `/postgres/v${v}/supatype-pg-${postgresArchiveTag(v)}-${p.os}-${p.arch}${p.os === "windows" ? ".zip" : ".tar.gz"}`,
149
+ deno: (v, p) => `/deno/v${v}/deno-${p.os}-${p.arch}${p.os === "windows" ? ".exe" : ""}`,
150
+ }
151
+
152
+ // Checksums file path (one per version directory, covers all platform binaries).
153
+ const checksumsDirPath = (component: Component, version: string): string =>
154
+ `/${component}/v${version}/checksums.sha256`
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Cache paths
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export function cacheRoot(): string {
161
+ return join(homedir(), ".supatype", "cache")
162
+ }
163
+
164
+ export function cachePath(component: Component, version: string): string {
165
+ return join(cacheRoot(), component, version)
166
+ }
167
+
168
+ export function cachedBinaryPath(component: Component, version: string, platform: PlatformId): string {
169
+ return join(cachePath(component, version), binaryName(component, version, platform))
170
+ }
171
+
172
+ function binaryName(component: Component, version: string, platform: PlatformId): string {
173
+ const win = platform.os === "windows"
174
+ switch (component) {
175
+ case "engine": return `supatype-engine-${platform.os}-${platform.arch}${win ? ".exe" : ""}`
176
+ case "server": return `supatype-server-${platform.os}-${platform.arch}${win ? ".exe" : ""}`
177
+ case "postgres": return `supatype-pg-${postgresArchiveTag(version)}-${platform.os}-${platform.arch}${win ? ".zip" : ".tar.gz"}`
178
+ case "deno": return `deno-${platform.os}-${platform.arch}${win ? ".exe" : ""}`
179
+ }
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Platform detection
184
+ // ---------------------------------------------------------------------------
185
+
186
+ export function currentPlatform(): PlatformId {
187
+ let os: PlatformId["os"]
188
+ if (process.platform === "darwin") os = "darwin"
189
+ else if (process.platform === "win32") os = "windows"
190
+ else os = "linux"
191
+
192
+ const rawArch = process.arch
193
+ let arch: PlatformId["arch"]
194
+ if (rawArch === "arm64") arch = "arm64"
195
+ else if (rawArch === "x64") arch = "amd64"
196
+ else throw new Error(`Unsupported architecture: ${rawArch}`)
197
+ return { os, arch }
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Override validation
202
+ // ---------------------------------------------------------------------------
203
+
204
+ /**
205
+ * Resolve the binary path for a component.
206
+ *
207
+ * Resolution order:
208
+ * 1. config.overrides?.[component] — local build path (must exist)
209
+ * 2. Cached binary at ~/.supatype/cache/{component}/{version}/
210
+ * 3. Throws — caller should call download() first.
211
+ *
212
+ * Hard error if any meaningful `overrides` entry is set while the project is linked to cloud
213
+ * (`project.ref`, `.supatype/cloud.json`, or `.supatype/linked.json`).
214
+ */
215
+ export async function resolveBinary(
216
+ component: Component,
217
+ config: SupatypeProjectConfig,
218
+ ): Promise<string> {
219
+ const cwd = process.cwd()
220
+ if (hasMeaningfulOverrides(config) && isLinkedToCloudProject(cwd, config)) {
221
+ throw new Error(
222
+ "[overrides] cannot be used while this project is linked to Supatype Cloud " +
223
+ "(project.ref, .supatype/cloud.json, or .supatype/linked.json).\n" +
224
+ "Remove overrides from supatype.config.ts / supatype.local.config.ts, or remove the cloud link files / clear project.ref.",
225
+ )
226
+ }
227
+
228
+ const overridePath = config.overrides?.[component === "postgres" ? "postgres_dir" : component]
229
+ const version = await resolveVersionFor(component, config)
230
+
231
+ if (version === VERSION_PIN_LOCAL && !overridePath) {
232
+ const key = component === "postgres" ? "postgres_dir" : component
233
+ throw new Error(
234
+ `[versions] versions.${component} is "${VERSION_PIN_LOCAL}" but overrides.${key} is not set. ` +
235
+ `Set overrides.${key} to your local build path, or pin a semver in versions.${component}.`,
236
+ )
237
+ }
238
+
239
+ if (overridePath) {
240
+ const normalised = normalisePlatformPath(overridePath)
241
+ let resolvedOverride = isAbsolute(normalised)
242
+ ? normalised
243
+ : resolve(process.cwd(), normalised)
244
+
245
+ if (process.platform === "win32" && !/\.\w+$/.test(resolvedOverride) && !existsSync(resolvedOverride)) {
246
+ const withExe = resolvedOverride + ".exe"
247
+ if (existsSync(withExe)) resolvedOverride = withExe
248
+ }
249
+
250
+ // On Windows, CreateProcess automatically appends .exe to extensionless paths.
251
+ // If the override binary exists without .exe, copy it to path.exe so it
252
+ // spawns correctly (and takes precedence over any stale .exe at that path).
253
+ if (process.platform === "win32" && !/\.\w+$/.test(resolvedOverride) && existsSync(resolvedOverride)) {
254
+ const withExe = resolvedOverride + ".exe"
255
+ const srcStat = statSync(resolvedOverride)
256
+ const dstStat = existsSync(withExe) ? statSync(withExe) : null
257
+ if (!dstStat || dstStat.size !== srcStat.size || dstStat.mtimeMs < srcStat.mtimeMs) {
258
+ copyFileSync(resolvedOverride, withExe)
259
+ }
260
+ resolvedOverride = withExe
261
+ }
262
+
263
+ if (!existsSync(resolvedOverride)) {
264
+ throw new Error(`[overrides] ${component} path does not exist: ${resolvedOverride}`)
265
+ }
266
+
267
+ const stat = statSync(resolvedOverride)
268
+ if (!stat.isFile() && !stat.isDirectory()) {
269
+ throw new Error(`[overrides] ${component} path is not a file or directory: ${resolvedOverride}`)
270
+ }
271
+
272
+ return resolvedOverride
273
+ }
274
+
275
+ const platform = currentPlatform()
276
+ const binPath = cachedBinaryPath(component, version, platform)
277
+
278
+ if (existsSync(binPath)) return binPath
279
+
280
+ throw new Error(`${component} v${version} not found in cache. Run: supatype update`)
281
+ }
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Download + verify
285
+ // ---------------------------------------------------------------------------
286
+
287
+ /**
288
+ * Download a component binary to the cache.
289
+ *
290
+ * Verification order:
291
+ * 1. Fetch checksums.sha256 + checksums.sha256.minisig from CDN.
292
+ * 2. If SUPATYPE_RELEASE_PUBLIC_KEY is set: verify minisign signature.
293
+ * 3. Verify SHA256 of downloaded binary against signed checksum.
294
+ */
295
+ /** Download if missing or invalid; return cached path for the given platform. */
296
+ export async function ensureCachedBinary(
297
+ component: Component,
298
+ version: string,
299
+ platform: PlatformId,
300
+ ): Promise<string> {
301
+ return download(component, version, platform)
302
+ }
303
+
304
+ export async function download(
305
+ component: Component,
306
+ version: string,
307
+ platform: PlatformId,
308
+ ): Promise<string> {
309
+ if (version === VERSION_PIN_LOCAL) {
310
+ throw new Error(
311
+ `cannot download CDN binary when version is "${VERSION_PIN_LOCAL}" — set overrides.${component === "postgres" ? "postgres_dir" : component} or pin a semver`,
312
+ )
313
+ }
314
+
315
+ const dir = cachePath(component, version)
316
+ mkdirSync(dir, { recursive: true })
317
+
318
+ const name = binaryName(component, version, platform)
319
+ const destPath = join(dir, name)
320
+
321
+ if (existsSync(destPath)) {
322
+ if (cachedArtifactLooksValid(component, destPath)) {
323
+ console.log(`[supatype] ${component} v${version} already cached.`)
324
+ return destPath
325
+ }
326
+ console.warn(
327
+ `[supatype] ${component} v${version} cache invalid — re-downloading (${destPath}).`,
328
+ )
329
+ unlinkSync(destPath)
330
+ }
331
+
332
+ const binaryUrl = `${CDN_BASE}${CDN_PATHS[component](version, platform)}`
333
+ const checksumsUrl = `${CDN_BASE}${checksumsDirPath(component, version)}`
334
+ const minisigUrl = `${checksumsUrl}.minisig`
335
+
336
+ console.log(`[supatype] Downloading ${component} v${version} (${platform.os}/${platform.arch})...`)
337
+
338
+ // ── Fetch checksums + optional minisig ────────────────────────────────────
339
+ const expectedChecksum = await withRetry(() =>
340
+ fetchChecksums(checksumsUrl, minisigUrl, name),
341
+ )
342
+
343
+ // ── Stream-download binary with progress ─────────────────────────────────
344
+ const tmpPath = destPath + ".tmp"
345
+ try {
346
+ await withRetry(() => streamToFileWithProgress(binaryUrl, tmpPath))
347
+
348
+ // ── Verify SHA256 ────────────────────────────────────────────────────────
349
+ await verifyChecksum(tmpPath, expectedChecksum, component)
350
+
351
+ writeFileSync(destPath, readFileSync(tmpPath))
352
+
353
+ assertArtifactFormat(component, destPath, platform)
354
+ if (process.platform !== "win32" && EXECUTABLE_COMPONENTS.has(component)) {
355
+ await chmod(destPath, 0o755)
356
+ }
357
+ } finally {
358
+ if (existsSync(tmpPath)) {
359
+ try { require("node:fs").unlinkSync(tmpPath) } catch { /* ignore */ }
360
+ }
361
+ }
362
+
363
+ return destPath
364
+ }
365
+
366
+ /**
367
+ * Fetch checksums.sha256, optionally verify its minisign signature, and
368
+ * return the expected SHA256 for `binaryFilename`.
369
+ */
370
+ async function fetchChecksums(
371
+ checksumsUrl: string,
372
+ minisigUrl: string,
373
+ binaryFilename: string,
374
+ ): Promise<string> {
375
+ const csResp = await fetch(checksumsUrl)
376
+ if (!csResp.ok) {
377
+ throw new Error(`Failed to fetch checksums from ${checksumsUrl}: HTTP ${csResp.status}`)
378
+ }
379
+ const checksumsText = await csResp.text()
380
+
381
+ const pubKey = releasePublicKey()
382
+ if (pubKey) {
383
+ // Minisign signature is required when a public key is embedded.
384
+ const sigResp = await fetch(minisigUrl)
385
+ if (!sigResp.ok) {
386
+ throw new Error(
387
+ `Failed to fetch checksum signature from ${minisigUrl}: HTTP ${sigResp.status}\n` +
388
+ "Cannot verify release integrity. Aborting download.",
389
+ )
390
+ }
391
+ const sigText = await sigResp.text()
392
+ verifyMinisign(Buffer.from(checksumsText, "utf8"), sigText, pubKey)
393
+ } else {
394
+ console.warn(
395
+ "[supatype] \u26a0 Minisign public key not configured — " +
396
+ "skipping signature verification (SHA256 only).",
397
+ )
398
+ }
399
+
400
+ return extractChecksum(checksumsText, binaryFilename)
401
+ }
402
+
403
+ // ---------------------------------------------------------------------------
404
+ // Minisign signature verification (pure Node.js, no external deps)
405
+ // ---------------------------------------------------------------------------
406
+
407
+ /**
408
+ * Ed25519 SPKI DER prefix — wraps a raw 32-byte public key into the
409
+ * SubjectPublicKeyInfo structure that Node.js crypto.createPublicKey expects.
410
+ *
411
+ * Breakdown:
412
+ * 30 2a SEQUENCE (42 bytes)
413
+ * 30 05 SEQUENCE (5 bytes)
414
+ * 06 03 OID (3 bytes)
415
+ * 2b 65 70 OID value: 1.3.101.112 (id-Ed25519)
416
+ * 03 21 BIT STRING (33 bytes)
417
+ * 00 0 unused bits
418
+ * <32 bytes Ed25519 public key>
419
+ */
420
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex")
421
+
422
+ /**
423
+ * Verify a minisign signature (Ed25519 legacy mode, algorithm bytes "Ed").
424
+ * Throws if verification fails.
425
+ */
426
+ function verifyMinisign(fileBytes: Buffer, sigFileContent: string, pubKeyStr: string): void {
427
+ // Parse public key: [2 algo][8 keyId][32 ed25519 key]
428
+ const pkLines = pubKeyStr.trim().split("\n")
429
+ const pkBytes = Buffer.from(pkLines[pkLines.length - 1]!.trim(), "base64")
430
+ if (pkBytes.length < 42) throw new Error("Invalid minisign public key")
431
+ const pkKeyId = pkBytes.subarray(2, 10)
432
+ const pkEd25519 = pkBytes.subarray(10, 42)
433
+
434
+ // Parse signature file:
435
+ // line 0: untrusted comment
436
+ // line 1: base64 sig bytes — [2 algo][8 keyId][64 Ed25519 sig]
437
+ // line 2: trusted comment
438
+ // line 3: base64 global sig (over sig bytes + trusted comment)
439
+ const sigLines = sigFileContent.trim().split("\n")
440
+ if (sigLines.length < 4) throw new Error("Malformed minisign signature file")
441
+ const sigBytes = Buffer.from(sigLines[1]!.trim(), "base64")
442
+ if (sigBytes.length < 74) throw new Error("Invalid minisign signature length")
443
+
444
+ const algo = sigBytes.subarray(0, 2)
445
+ const sigKeyId = sigBytes.subarray(2, 10)
446
+ const signature = sigBytes.subarray(10, 74)
447
+
448
+ // Only Ed25519 legacy mode ("Ed" = 0x45, 0x64) is supported.
449
+ // Hashed mode ("ED") requires BLAKE2b prehashing — not implemented.
450
+ if (algo[0] !== 0x45 || algo[1] !== 0x64) {
451
+ throw new Error(
452
+ "Unsupported minisign algorithm — only Ed25519 legacy mode supported.\n" +
453
+ `Got: 0x${algo[0]?.toString(16)}${algo[1]?.toString(16)}`,
454
+ )
455
+ }
456
+
457
+ if (!sigKeyId.equals(pkKeyId)) {
458
+ throw new Error(
459
+ "Minisign key ID mismatch — signature was produced with a different key.\n" +
460
+ "This could indicate a compromised release. Do not proceed.",
461
+ )
462
+ }
463
+
464
+ const spkiDer = Buffer.concat([ED25519_SPKI_PREFIX, pkEd25519])
465
+ const keyObject = createPublicKey({ key: spkiDer, format: "der", type: "spki" })
466
+
467
+ const valid = cryptoVerify(null, fileBytes, keyObject, signature)
468
+ if (!valid) {
469
+ throw new Error(
470
+ "Minisign signature verification FAILED — the checksum file may have been tampered with.\n" +
471
+ "This could indicate a supply chain attack. Aborting download.",
472
+ )
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Extract the SHA256 hash for `filename` from a checksums.sha256 file.
478
+ * Format: `<hash> <filename>` (sha256sum output, two spaces).
479
+ */
480
+ function extractChecksum(checksumsText: string, filename: string): string {
481
+ const target = basename(filename)
482
+ for (const line of checksumsText.split("\n")) {
483
+ const parts = line.trim().split(/\s+/)
484
+ if (parts.length >= 2 && parts[1] === target) {
485
+ return parts[0]!
486
+ }
487
+ }
488
+ throw new Error(
489
+ `Checksum not found for "${target}" in checksums.sha256.\n` +
490
+ "The checksums file may be from a different release.",
491
+ )
492
+ }
493
+
494
+ // ---------------------------------------------------------------------------
495
+ // Streaming download with progress bar
496
+ // ---------------------------------------------------------------------------
497
+
498
+ async function streamToFileWithProgress(url: string, destPath: string): Promise<void> {
499
+ const resp = await fetch(url)
500
+ if (!resp.ok) throw new Error(`Failed to download from ${url}: HTTP ${resp.status}`)
501
+ if (!resp.body) throw new Error("Response body is null")
502
+
503
+ const totalStr = resp.headers.get("content-length")
504
+ const total = totalStr ? parseInt(totalStr, 10) : null
505
+ let downloaded = 0
506
+
507
+ const file = createWriteStream(destPath)
508
+ const reader = resp.body.getReader()
509
+
510
+ try {
511
+ while (true) {
512
+ const { done, value } = await reader.read()
513
+ if (done) break
514
+
515
+ await new Promise<void>((res, rej) => {
516
+ file.write(value, (err) => (err ? rej(err) : res()))
517
+ })
518
+ downloaded += value.length
519
+
520
+ if (total && process.stdout.isTTY) {
521
+ const pct = Math.min(100, Math.floor((downloaded / total) * 100))
522
+ const filled = Math.floor(pct / 5)
523
+ const bar = "=".repeat(filled).padEnd(20)
524
+ process.stdout.write(
525
+ `\r [${bar}] ${pct}% ${(downloaded / 1_000_000).toFixed(1)} / ${(total / 1_000_000).toFixed(1)} MB`,
526
+ )
527
+ }
528
+ }
529
+
530
+ if (total && process.stdout.isTTY) process.stdout.write("\n")
531
+
532
+ await new Promise<void>((res, rej) => {
533
+ file.end((err?: Error | null) => (err ? rej(err) : res()))
534
+ })
535
+ } catch (err) {
536
+ file.destroy()
537
+ throw err
538
+ }
539
+ }
540
+
541
+ // ---------------------------------------------------------------------------
542
+ // SHA256 verification
543
+ // ---------------------------------------------------------------------------
544
+
545
+ const EXECUTABLE_COMPONENTS = new Set<Component>(["engine", "server", "deno"])
546
+
547
+ /** True when a cached file matches expected format for the current platform. */
548
+ function cachedArtifactLooksValid(component: Component, filePath: string): boolean {
549
+ try {
550
+ const st = statSync(filePath)
551
+ if (!st.isFile() || st.size < 64) return false
552
+ assertArtifactFormat(component, filePath, currentPlatform())
553
+ return true
554
+ } catch {
555
+ return false
556
+ }
557
+ }
558
+
559
+ /** Confirm a downloaded/cached artifact matches the expected CDN format (tests, CI). */
560
+ export function validateArtifactFormat(
561
+ component: Component,
562
+ filePath: string,
563
+ platform: PlatformId,
564
+ ): void {
565
+ assertArtifactFormat(component, filePath, platform)
566
+ }
567
+
568
+ /**
569
+ * Per-component CDN artifact shapes:
570
+ * engine, server, deno — native executable (ELF / Mach-O / PE)
571
+ * postgres (unix) — .tar.gz (gzip)
572
+ * postgres (windows) — .zip
573
+ */
574
+ function assertArtifactFormat(
575
+ component: Component,
576
+ filePath: string,
577
+ platform: PlatformId,
578
+ ): void {
579
+ if (component === "postgres") {
580
+ if (platform.os === "windows") assertZipArchive(filePath)
581
+ else assertGzipArchive(filePath)
582
+ return
583
+ }
584
+ if (EXECUTABLE_COMPONENTS.has(component)) {
585
+ assertNativeExecutable(filePath, component, platform)
586
+ return
587
+ }
588
+ }
589
+
590
+ /** Reject HTML/error pages or corrupt postgres .tar.gz on CDN. */
591
+ function assertGzipArchive(filePath: string): void {
592
+ const fd = openSync(filePath, "r")
593
+ try {
594
+ const magic = Buffer.alloc(2)
595
+ readSync(fd, magic, 0, 2, 0)
596
+ if (magic[0] !== 0x1f || magic[1] !== 0x8b) {
597
+ throw new Error(
598
+ "Downloaded postgres file is not a gzip archive (bad magic bytes). " +
599
+ "The CDN object may be corrupt or cached HTML — delete ~/.supatype/cache and retry.",
600
+ )
601
+ }
602
+ } finally {
603
+ closeSync(fd)
604
+ }
605
+ }
606
+
607
+ /** Reject corrupt postgres .zip on CDN (Windows bundles). */
608
+ function assertZipArchive(filePath: string): void {
609
+ const fd = openSync(filePath, "r")
610
+ try {
611
+ const magic = Buffer.alloc(4)
612
+ readSync(fd, magic, 0, 4, 0)
613
+ if (magic[0] !== 0x50 || magic[1] !== 0x4b) {
614
+ throw new Error(
615
+ "Downloaded postgres file is not a zip archive (bad magic bytes). " +
616
+ "The CDN object may be corrupt or cached HTML — delete ~/.supatype/cache and retry.",
617
+ )
618
+ }
619
+ } finally {
620
+ closeSync(fd)
621
+ }
622
+ }
623
+
624
+ /** Reject HTML/error pages, Go c-archives, or wrong-OS executables on CDN. */
625
+ function assertNativeExecutable(
626
+ filePath: string,
627
+ component: Component,
628
+ platform: PlatformId,
629
+ ): void {
630
+ const fd = openSync(filePath, "r")
631
+ try {
632
+ const magic = Buffer.alloc(4)
633
+ readSync(fd, magic, 0, 4, 0)
634
+ const goCArchive =
635
+ magic[0] === 0x21 && magic[1] === 0x3c && magic[2] === 0x61 && magic[3] === 0x72
636
+ if (goCArchive) {
637
+ throw new Error(
638
+ `Downloaded ${component} file is a Go static archive (c-archive), not an executable. ` +
639
+ "The CDN object may be from a bad release build — delete ~/.supatype/cache and retry.",
640
+ )
641
+ }
642
+ if (platform.os === "windows") {
643
+ if (magic[0] !== 0x4d || magic[1] !== 0x5a) {
644
+ throw new Error(
645
+ `Downloaded ${component} file is not a Windows PE executable (bad magic bytes). ` +
646
+ "The CDN object may be corrupt or cached HTML — delete ~/.supatype/cache and retry.",
647
+ )
648
+ }
649
+ return
650
+ }
651
+ if (platform.os === "linux") {
652
+ const elf =
653
+ magic[0] === 0x7f && magic[1] === 0x45 && magic[2] === 0x4c && magic[3] === 0x46
654
+ if (!elf) {
655
+ throw new Error(
656
+ `Downloaded ${component} file is not an ELF executable (bad magic bytes). ` +
657
+ "The CDN object may be corrupt or cached HTML — delete ~/.supatype/cache and retry.",
658
+ )
659
+ }
660
+ return
661
+ }
662
+ const macho =
663
+ magic.readUInt32BE(0) === 0xfe_ed_fa_ce ||
664
+ magic.readUInt32BE(0) === 0xfe_ed_fa_cf ||
665
+ magic.readUInt32LE(0) === 0xfe_ed_fa_ce ||
666
+ magic.readUInt32LE(0) === 0xfe_ed_fa_cf ||
667
+ magic.readUInt32BE(0) === 0xca_fe_ba_be
668
+ if (!macho) {
669
+ throw new Error(
670
+ `Downloaded ${component} file is not a Mach-O executable (bad magic bytes). ` +
671
+ "The CDN object may be corrupt or cached HTML — delete ~/.supatype/cache and retry.",
672
+ )
673
+ }
674
+ } finally {
675
+ closeSync(fd)
676
+ }
677
+ }
678
+
679
+ async function verifyChecksum(filePath: string, expected: string, component: Component): Promise<void> {
680
+ const data = readFileSync(filePath)
681
+ const actual = createHash("sha256").update(data).digest("hex")
682
+ if (actual !== expected) {
683
+ throw new Error(
684
+ `Checksum mismatch for ${component}.\n` +
685
+ ` Expected: ${expected}\n` +
686
+ ` Got: ${actual}`,
687
+ )
688
+ }
689
+ }
690
+
691
+ // ---------------------------------------------------------------------------
692
+ // Retry with exponential backoff
693
+ // ---------------------------------------------------------------------------
694
+
695
+ async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
696
+ for (let i = 1; i <= attempts; i++) {
697
+ try {
698
+ return await fn()
699
+ } catch (err) {
700
+ if (i === attempts) throw err
701
+ const delay = Math.pow(3, i - 1) * 1_000 // 1 s, 3 s, 9 s
702
+ console.error(
703
+ `[supatype] Download attempt ${i}/${attempts} failed: ${(err as Error).message}. ` +
704
+ `Retrying in ${delay / 1_000}s...`,
705
+ )
706
+ await new Promise((r) => setTimeout(r, delay))
707
+ }
708
+ }
709
+ throw new Error("unreachable")
710
+ }
711
+
712
+ // ---------------------------------------------------------------------------
713
+ // Helpers
714
+ // ---------------------------------------------------------------------------
715
+
716
+ /**
717
+ * On Windows, Git Bash represents paths as /c/Users/... — convert to C:\Users\...
718
+ */
719
+ export function normalisePlatformPath(p: string): string {
720
+ let result = p
721
+ if (process.platform === "win32" && /^\/[a-zA-Z]\//.test(result)) {
722
+ result = result
723
+ .replace(/^\/([a-zA-Z])\//, (_, drive: string) => `${drive.toUpperCase()}:\\`)
724
+ .replace(/\//g, "\\")
725
+ }
726
+ if (process.platform === "win32" && !/\.\w+$/.test(result) && !existsSync(result)) {
727
+ const withExe = result + ".exe"
728
+ if (existsSync(withExe)) return withExe
729
+ }
730
+ return result
731
+ }
732
+
733
+ export function pinnedVersion(component: Component, config: SupatypeProjectConfig): string | undefined {
734
+ const version = config.versions?.[component]
735
+ if (typeof version !== "string") return undefined
736
+ const trimmed = version.trim()
737
+ return trimmed === "" ? undefined : trimmed
738
+ }
739
+
740
+ /** Pinned version from config, or latest from CDN when unset. */
741
+ export async function resolveVersionFor(
742
+ component: Component,
743
+ config: SupatypeProjectConfig,
744
+ ): Promise<string> {
745
+ const pinned = pinnedVersion(component, config)
746
+ if (pinned) return pinned
747
+ return fetchLatestVersion(component)
748
+ }
749
+
750
+ /** @deprecated Prefer {@link pinnedVersion} or {@link resolveVersionFor}. */
751
+ export function versionFor(component: Component, config: SupatypeProjectConfig): string {
752
+ const version = pinnedVersion(component, config)
753
+ if (!version) {
754
+ throw new Error(
755
+ `[supatype] versions.${component} is not pinned in supatype.config.ts (omit versions to use latest)`,
756
+ )
757
+ }
758
+ return version
759
+ }
760
+
761
+ // ---------------------------------------------------------------------------
762
+ // Latest version resolution from CDN
763
+ // ---------------------------------------------------------------------------
764
+
765
+ /**
766
+ * Fetch the latest available version for a component.
767
+ * Each component directory on the CDN exposes `latest.json` → `{"version":"x.y.z"}`.
768
+ */
769
+ export async function fetchLatestVersion(component: Component): Promise<string> {
770
+ const url = `${CDN_BASE}/${component}/latest.json`
771
+ const resp = await fetch(url)
772
+ if (!resp.ok) {
773
+ throw new Error(`Failed to fetch latest version for ${component} from ${url}: HTTP ${resp.status}`)
774
+ }
775
+ const data = await resp.json() as { version?: unknown }
776
+ if (typeof data.version !== "string" || data.version.trim() === "") {
777
+ throw new Error(`Invalid latest.json for ${component}: missing "version" field`)
778
+ }
779
+ return data.version.trim()
780
+ }
781
+
782
+ /** Fetch the latest version for all components concurrently. */
783
+ export async function fetchAllLatestVersions(): Promise<Record<Component, string>> {
784
+ const results = await Promise.all(
785
+ BINARY_COMPONENTS.map(async (c) => [c, await fetchLatestVersion(c)] as const),
786
+ )
787
+ return Object.fromEntries(results) as Record<Component, string>
788
+ }
789
+
790
+ // ---------------------------------------------------------------------------
791
+ // Download all components (used by postinstall + supatype update)
792
+ // ---------------------------------------------------------------------------
793
+
794
+ /**
795
+ * Download all component binaries for the current platform.
796
+ * Skips components that are already cached.
797
+ * Fails gracefully when graceful=true (suitable for postinstall).
798
+ */
799
+ /**
800
+ * Verify all cached binaries for the current platform (used by integration CI).
801
+ * Throws if any cached component is missing or fails format checks.
802
+ */
803
+ export function verifyCachedBinaries(versions: Partial<ComponentVersions> | undefined): void {
804
+ if (!versions) {
805
+ throw new Error("[supatype] verifyCachedBinaries requires pinned versions")
806
+ }
807
+ const platform = currentPlatform()
808
+ for (const component of BINARY_COMPONENTS) {
809
+ const version = versions[component]
810
+ if (typeof version !== "string" || version.trim() === "") {
811
+ throw new Error(`[supatype] versions.${component} must be set`)
812
+ }
813
+ const destPath = join(cachePath(component, version), binaryName(component, version, platform))
814
+ if (!cachedArtifactLooksValid(component, destPath)) {
815
+ throw new Error(
816
+ `[supatype] Cached ${component} v${version} is missing or invalid at ${destPath}. ` +
817
+ "Run: supatype update (or delete ~/.supatype/cache and retry).",
818
+ )
819
+ }
820
+ }
821
+ }
822
+
823
+ export async function downloadAll(
824
+ versions: Partial<ComponentVersions> | undefined,
825
+ graceful = false,
826
+ ): Promise<void> {
827
+ const platform = currentPlatform()
828
+ const components: Component[] = [...BINARY_COMPONENTS]
829
+ const latest = await fetchAllLatestVersions()
830
+
831
+ for (const component of components) {
832
+ const version = versions?.[component] ?? latest[component]
833
+ if (version === VERSION_PIN_LOCAL) continue
834
+ try {
835
+ await download(component, version, platform)
836
+ } catch (err) {
837
+ const msg = `[supatype] Failed to download ${component}: ${(err as Error).message}`
838
+ if (graceful) {
839
+ console.error(msg)
840
+ } else {
841
+ throw new Error(msg)
842
+ }
843
+ }
844
+ }
845
+ }