voxflow 1.15.0 → 1.15.2

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 (453) hide show
  1. package/README.md +34 -0
  2. package/bin/voxflow.js +27 -0
  3. package/dist/index.js +1 -1
  4. package/dist/remotion-bundle/02a2fb2eb80bc7bf.woff2 +0 -0
  5. package/dist/remotion-bundle/052ca5351e5e06ba.woff2 +0 -0
  6. package/dist/remotion-bundle/05853dd28f4019cb.woff2 +0 -0
  7. package/dist/remotion-bundle/072ead3737f7c0d0.woff2 +0 -0
  8. package/dist/remotion-bundle/07d4248613c86a2e.woff2 +0 -0
  9. package/dist/remotion-bundle/0884a5c2d1d2d99b.woff2 +0 -0
  10. package/dist/remotion-bundle/0b0e185b2752095e.woff2 +0 -0
  11. package/dist/remotion-bundle/0e66c11bde067d91.woff2 +0 -0
  12. package/dist/remotion-bundle/0f7794cfba2c5d21.woff2 +0 -0
  13. package/dist/remotion-bundle/0fdbae5a4365783a.woff2 +0 -0
  14. package/dist/remotion-bundle/112.bundle.js +11 -0
  15. package/dist/remotion-bundle/112.bundle.js.map +1 -0
  16. package/dist/remotion-bundle/113.bundle.js +11 -0
  17. package/dist/remotion-bundle/113.bundle.js.map +1 -0
  18. package/dist/remotion-bundle/119cae0c4c16f7ed.woff2 +0 -0
  19. package/dist/remotion-bundle/14725f649fd1e78c.woff2 +0 -0
  20. package/dist/remotion-bundle/14abe9e3f95f7888.woff2 +0 -0
  21. package/dist/remotion-bundle/163.bundle.js +14678 -0
  22. package/dist/remotion-bundle/163.bundle.js.map +1 -0
  23. package/dist/remotion-bundle/1808c54072bf6d14.woff2 +0 -0
  24. package/dist/remotion-bundle/18948bec3e3012fe.woff2 +0 -0
  25. package/dist/remotion-bundle/1a661c60d0fc84fc.woff2 +0 -0
  26. package/dist/remotion-bundle/1af94941e1bc7e1e.woff2 +0 -0
  27. package/dist/remotion-bundle/1bee0219595f606c.woff2 +0 -0
  28. package/dist/remotion-bundle/1bfd5da7ce9d4ec4.woff2 +0 -0
  29. package/dist/remotion-bundle/1c158d56f1884f3f.woff2 +0 -0
  30. package/dist/remotion-bundle/1cf5e88e667610eb.woff2 +0 -0
  31. package/dist/remotion-bundle/1d431bd10f53c481.woff2 +0 -0
  32. package/dist/remotion-bundle/1d701a81a7670db2.woff2 +0 -0
  33. package/dist/remotion-bundle/1da0fecad4240f16.woff2 +0 -0
  34. package/dist/remotion-bundle/1ed14d3d0c5c63fe.woff2 +0 -0
  35. package/dist/remotion-bundle/1edfecf40e586f53.woff2 +0 -0
  36. package/dist/remotion-bundle/1f479711bc34b054.woff +0 -0
  37. package/dist/remotion-bundle/1f86e54a0ff5fcd1.woff2 +0 -0
  38. package/dist/remotion-bundle/2043ea87d9aabd11.woff2 +0 -0
  39. package/dist/remotion-bundle/20563c39ee8a0e40.woff2 +0 -0
  40. package/dist/remotion-bundle/20c231590fd12c44.woff2 +0 -0
  41. package/dist/remotion-bundle/20ce61713f754c07.woff2 +0 -0
  42. package/dist/remotion-bundle/21eb9306fce24bb1.woff2 +0 -0
  43. package/dist/remotion-bundle/244bf71c0cc851af.woff2 +0 -0
  44. package/dist/remotion-bundle/274d4cfc02bffbcb.woff2 +0 -0
  45. package/dist/remotion-bundle/275.bundle.js +21 -0
  46. package/dist/remotion-bundle/275.bundle.js.map +1 -0
  47. package/dist/remotion-bundle/2958f540b39513dc.woff2 +0 -0
  48. package/dist/remotion-bundle/2a168b98fd97722e.woff2 +0 -0
  49. package/dist/remotion-bundle/2d1f6373937ab55f.woff2 +0 -0
  50. package/dist/remotion-bundle/2d213ae47ff6daa9.woff2 +0 -0
  51. package/dist/remotion-bundle/2e4b1f04fcd05047.woff2 +0 -0
  52. package/dist/remotion-bundle/304170d98f4c4563.woff2 +0 -0
  53. package/dist/remotion-bundle/30d02e136e7a5642.woff2 +0 -0
  54. package/dist/remotion-bundle/3135562b52a714cd.woff2 +0 -0
  55. package/dist/remotion-bundle/313713af2c8144e9.woff2 +0 -0
  56. package/dist/remotion-bundle/325fa4108d2285b9.woff2 +0 -0
  57. package/dist/remotion-bundle/338e927ed3345e0c.woff2 +0 -0
  58. package/dist/remotion-bundle/35fc6b190365bc17.woff2 +0 -0
  59. package/dist/remotion-bundle/37a51f1122d4efc5.woff2 +0 -0
  60. package/dist/remotion-bundle/39a4d63e02736f5e.woff2 +0 -0
  61. package/dist/remotion-bundle/3a00e0d62dfc4171.woff2 +0 -0
  62. package/dist/remotion-bundle/3a6955e6561affe1.woff2 +0 -0
  63. package/dist/remotion-bundle/3c573945aef49b89.woff2 +0 -0
  64. package/dist/remotion-bundle/3cdbfbfa23b516a5.woff2 +0 -0
  65. package/dist/remotion-bundle/3e42f85a9e64ca8a.woff2 +0 -0
  66. package/dist/remotion-bundle/3e83eaf1ec859415.woff2 +0 -0
  67. package/dist/remotion-bundle/3f3c8c90de1250ee.woff2 +0 -0
  68. package/dist/remotion-bundle/434.bundle.js +205 -0
  69. package/dist/remotion-bundle/434.bundle.js.map +1 -0
  70. package/dist/remotion-bundle/44ffc6ca4d781692.woff2 +0 -0
  71. package/dist/remotion-bundle/4670d9c4580b09eb.woff2 +0 -0
  72. package/dist/remotion-bundle/479756881b302824.woff2 +0 -0
  73. package/dist/remotion-bundle/481b82134bfa9c82.woff2 +0 -0
  74. package/dist/remotion-bundle/48d27029626f4328.woff2 +0 -0
  75. package/dist/remotion-bundle/49b7b2a30329c511.woff2 +0 -0
  76. package/dist/remotion-bundle/4c8b25a1a9337045.woff2 +0 -0
  77. package/dist/remotion-bundle/4cba14788ca9259b.woff2 +0 -0
  78. package/dist/remotion-bundle/4cd6c589c004a6a7.woff2 +0 -0
  79. package/dist/remotion-bundle/4cd8d79c1021608d.woff2 +0 -0
  80. package/dist/remotion-bundle/4d8fa99b3f00f9f0.woff2 +0 -0
  81. package/dist/remotion-bundle/4e7805a643f86d53.woff2 +0 -0
  82. package/dist/remotion-bundle/4ff91be454542e3f.woff2 +0 -0
  83. package/dist/remotion-bundle/504cbcba1f63591b.woff2 +0 -0
  84. package/dist/remotion-bundle/5202d792e5791d6c.woff2 +0 -0
  85. package/dist/remotion-bundle/534db5ad4770cc1d.woff2 +0 -0
  86. package/dist/remotion-bundle/53b9568eb85f866b.woff2 +0 -0
  87. package/dist/remotion-bundle/543ad386ca171de9.woff2 +0 -0
  88. package/dist/remotion-bundle/54798e55bbf7976e.woff2 +0 -0
  89. package/dist/remotion-bundle/580.bundle.js +11 -0
  90. package/dist/remotion-bundle/580.bundle.js.map +1 -0
  91. package/dist/remotion-bundle/58d174d1193af6d1.woff2 +0 -0
  92. package/dist/remotion-bundle/591d29ff3ff53c80.woff2 +0 -0
  93. package/dist/remotion-bundle/5c28c4f4824383c6.woff2 +0 -0
  94. package/dist/remotion-bundle/5da9740d2ce894c8.woff2 +0 -0
  95. package/dist/remotion-bundle/6197735364642360.woff2 +0 -0
  96. package/dist/remotion-bundle/6265a4335724080f.woff2 +0 -0
  97. package/dist/remotion-bundle/633f5e4f6394daa7.woff2 +0 -0
  98. package/dist/remotion-bundle/637d95ace6a69c49.woff2 +0 -0
  99. package/dist/remotion-bundle/648e04a04dacff8f.woff2 +0 -0
  100. package/dist/remotion-bundle/64a6e83045a008b2.woff2 +0 -0
  101. package/dist/remotion-bundle/651.bundle.js +11 -0
  102. package/dist/remotion-bundle/651.bundle.js.map +1 -0
  103. package/dist/remotion-bundle/65e2a988c070facc.woff2 +0 -0
  104. package/dist/remotion-bundle/66a2f6ce5cc69105.woff2 +0 -0
  105. package/dist/remotion-bundle/690.bundle.js +3479 -0
  106. package/dist/remotion-bundle/690.bundle.js.map +1 -0
  107. package/dist/remotion-bundle/690ff55252ca715d.woff2 +0 -0
  108. package/dist/remotion-bundle/6a01a1cff49314fc.woff2 +0 -0
  109. package/dist/remotion-bundle/6cbc32670982986c.woff2 +0 -0
  110. package/dist/remotion-bundle/6d3cc42ae547f454.woff2 +0 -0
  111. package/dist/remotion-bundle/6d8f4cfa1ddc0830.woff2 +0 -0
  112. package/dist/remotion-bundle/6e4d7c6ae65e2dc3.woff2 +0 -0
  113. package/dist/remotion-bundle/6e86418bbcefb2e8.woff2 +0 -0
  114. package/dist/remotion-bundle/6ee02884b29cf7fb.woff2 +0 -0
  115. package/dist/remotion-bundle/6f436a74c9e3252c.woff2 +0 -0
  116. package/dist/remotion-bundle/78c8022f1657618b.woff2 +0 -0
  117. package/dist/remotion-bundle/7c5444169792bca4.woff2 +0 -0
  118. package/dist/remotion-bundle/7c86bddd9d997212.woff2 +0 -0
  119. package/dist/remotion-bundle/7e1284684767f584.woff2 +0 -0
  120. package/dist/remotion-bundle/7e81c17522d182b2.woff2 +0 -0
  121. package/dist/remotion-bundle/7eb87be198f7858c.woff2 +0 -0
  122. package/dist/remotion-bundle/8060c928f948aab5.woff2 +0 -0
  123. package/dist/remotion-bundle/80bc9dfbea2b35ae.woff2 +0 -0
  124. package/dist/remotion-bundle/811b83f69963bb48.woff2 +0 -0
  125. package/dist/remotion-bundle/813.bundle.js +117511 -0
  126. package/dist/remotion-bundle/813.bundle.js.map +1 -0
  127. package/dist/remotion-bundle/84df492e349f82e9.woff2 +0 -0
  128. package/dist/remotion-bundle/8501bfd73eb36f2b.woff2 +0 -0
  129. package/dist/remotion-bundle/854236a8376093fe.woff2 +0 -0
  130. package/dist/remotion-bundle/8571d74529082753.woff2 +0 -0
  131. package/dist/remotion-bundle/860bf44f8e6f4b5d.woff2 +0 -0
  132. package/dist/remotion-bundle/879.bundle.js +64 -0
  133. package/dist/remotion-bundle/879.bundle.js.map +1 -0
  134. package/dist/remotion-bundle/887dd482f848d56f.woff2 +0 -0
  135. package/dist/remotion-bundle/89b2132e85fbbb5a.woff2 +0 -0
  136. package/dist/remotion-bundle/8ba60d6c306010c2.woff2 +0 -0
  137. package/dist/remotion-bundle/8c7c4dadea897806.woff2 +0 -0
  138. package/dist/remotion-bundle/8c943f9999706f61.woff2 +0 -0
  139. package/dist/remotion-bundle/8f2a718c90575cc9.woff2 +0 -0
  140. package/dist/remotion-bundle/906b6edb3e1772c9.woff2 +0 -0
  141. package/dist/remotion-bundle/930ff9daccdf14eb.woff2 +0 -0
  142. package/dist/remotion-bundle/934db2f1c403c4d0.woff2 +0 -0
  143. package/dist/remotion-bundle/938.bundle.js +451 -0
  144. package/dist/remotion-bundle/938.bundle.js.map +1 -0
  145. package/dist/remotion-bundle/967.bundle.js +4462 -0
  146. package/dist/remotion-bundle/967.bundle.js.map +1 -0
  147. package/dist/remotion-bundle/9684a1093d3c02ce.woff2 +0 -0
  148. package/dist/remotion-bundle/973dcd0faa6116cc.woff2 +0 -0
  149. package/dist/remotion-bundle/9745400694e76cd8.woff2 +0 -0
  150. package/dist/remotion-bundle/999ef957bed3bdca.woff2 +0 -0
  151. package/dist/remotion-bundle/99a3d67c8b0f43e3.woff2 +0 -0
  152. package/dist/remotion-bundle/a0586c3e03127283.woff2 +0 -0
  153. package/dist/remotion-bundle/a0eb654fdae46269.woff2 +0 -0
  154. package/dist/remotion-bundle/a20e35d3b08f7994.woff2 +0 -0
  155. package/dist/remotion-bundle/a2dcaced7c8c25ab.woff2 +0 -0
  156. package/dist/remotion-bundle/a79255a972a2681a.woff2 +0 -0
  157. package/dist/remotion-bundle/a804b352cb9fec1a.woff2 +0 -0
  158. package/dist/remotion-bundle/aae7117164e1eabc.woff2 +0 -0
  159. package/dist/remotion-bundle/affd121385d0442d.woff2 +0 -0
  160. package/dist/remotion-bundle/b19a6083987ee0d7.woff2 +0 -0
  161. package/dist/remotion-bundle/b1b2bd04d8637981.woff2 +0 -0
  162. package/dist/remotion-bundle/b2c07f341486be87.woff2 +0 -0
  163. package/dist/remotion-bundle/b33d8f82e575c4ce.woff2 +0 -0
  164. package/dist/remotion-bundle/b366c0bed35ef491.woff2 +0 -0
  165. package/dist/remotion-bundle/b41e857ec1b85642.woff2 +0 -0
  166. package/dist/remotion-bundle/b420bb34ccf23e7f.woff2 +0 -0
  167. package/dist/remotion-bundle/b4f7bf4efb0c0ccf.woff2 +0 -0
  168. package/dist/remotion-bundle/b60fe5eca03cff93.woff2 +0 -0
  169. package/dist/remotion-bundle/b6bd31a336e64bce.woff2 +0 -0
  170. package/dist/remotion-bundle/b6d2befba3dfefeb.woff2 +0 -0
  171. package/dist/remotion-bundle/b75f39ab06c43bf4.woff2 +0 -0
  172. package/dist/remotion-bundle/b77880e8c413d4fd.woff2 +0 -0
  173. package/dist/remotion-bundle/b7e38ec441e4a77a.woff2 +0 -0
  174. package/dist/remotion-bundle/b83baa383ff0bf2b.woff2 +0 -0
  175. package/dist/remotion-bundle/b9ad7b6c0a11450a.woff2 +0 -0
  176. package/dist/remotion-bundle/baf84486e8ae3aaf.woff2 +0 -0
  177. package/dist/remotion-bundle/bc047b1f6869cffa.woff2 +0 -0
  178. package/dist/remotion-bundle/bf4f3ac6e93f33aa.woff2 +0 -0
  179. package/dist/remotion-bundle/bf6835ffec5897a2.woff2 +0 -0
  180. package/dist/remotion-bundle/bf8885f581eb1724.woff2 +0 -0
  181. package/dist/remotion-bundle/bundle.js +83376 -0
  182. package/dist/remotion-bundle/bundle.js.map +1 -0
  183. package/dist/remotion-bundle/c03f046bccd789d0.woff2 +0 -0
  184. package/dist/remotion-bundle/c0bb1f8962b73bc3.woff2 +0 -0
  185. package/dist/remotion-bundle/c1003f9a7db6e1cf.woff2 +0 -0
  186. package/dist/remotion-bundle/c15d83fb1e199515.woff2 +0 -0
  187. package/dist/remotion-bundle/c28e7e5d310f73ef.woff2 +0 -0
  188. package/dist/remotion-bundle/c2b840274db78aea.woff2 +0 -0
  189. package/dist/remotion-bundle/c3000e3299d4e45f.woff2 +0 -0
  190. package/dist/remotion-bundle/c83ce886e5288510.woff2 +0 -0
  191. package/dist/remotion-bundle/c87a5a64d4ac0918.woff2 +0 -0
  192. package/dist/remotion-bundle/c8a7e0d049e965fa.woff2 +0 -0
  193. package/dist/remotion-bundle/c949a35d3a3b1faf.woff2 +0 -0
  194. package/dist/remotion-bundle/c9618c9b9ac2bc78.woff2 +0 -0
  195. package/dist/remotion-bundle/ca3add3b84152d5b.woff2 +0 -0
  196. package/dist/remotion-bundle/cad9dd036408d707.woff2 +0 -0
  197. package/dist/remotion-bundle/cbb24916619df439.woff2 +0 -0
  198. package/dist/remotion-bundle/cc054f0b5514e177.woff2 +0 -0
  199. package/dist/remotion-bundle/ccc248ed9312bc71.woff2 +0 -0
  200. package/dist/remotion-bundle/cd9d623aa07af925.woff2 +0 -0
  201. package/dist/remotion-bundle/ce2ba7a321bd1247.woff2 +0 -0
  202. package/dist/remotion-bundle/cf72455f79a29b14.woff2 +0 -0
  203. package/dist/remotion-bundle/d267cbfefab452ac.woff2 +0 -0
  204. package/dist/remotion-bundle/d435cff46a64955f.woff +0 -0
  205. package/dist/remotion-bundle/d494d07f67e363f6.woff2 +0 -0
  206. package/dist/remotion-bundle/d7aa0cc1fa47bf38.woff2 +0 -0
  207. package/dist/remotion-bundle/d7c5ca93d885160a.woff2 +0 -0
  208. package/dist/remotion-bundle/d855d3e252db74e2.woff2 +0 -0
  209. package/dist/remotion-bundle/d8f13d47f02f82c2.woff2 +0 -0
  210. package/dist/remotion-bundle/d9567cce2ee11019.woff2 +0 -0
  211. package/dist/remotion-bundle/db8d4456fc75dd86.woff +0 -0
  212. package/dist/remotion-bundle/dc274628378c47ee.woff2 +0 -0
  213. package/dist/remotion-bundle/dc3e06947bb69903.woff2 +0 -0
  214. package/dist/remotion-bundle/dd67040ac3b6d523.woff2 +0 -0
  215. package/dist/remotion-bundle/e0b04bd488f953f4.woff2 +0 -0
  216. package/dist/remotion-bundle/e2a572ff95089370.woff2 +0 -0
  217. package/dist/remotion-bundle/e2e18a86b1c2b0cc.woff2 +0 -0
  218. package/dist/remotion-bundle/e3a78ee2fc9c6931.woff2 +0 -0
  219. package/dist/remotion-bundle/e654c9d547605a9f.woff2 +0 -0
  220. package/dist/remotion-bundle/e67a3a64c129927c.woff2 +0 -0
  221. package/dist/remotion-bundle/e6be28b4203cd6ce.woff2 +0 -0
  222. package/dist/remotion-bundle/e841907ad9b0a191.woff +0 -0
  223. package/dist/remotion-bundle/e889d1541c69fffa.woff2 +0 -0
  224. package/dist/remotion-bundle/e88ef8c76373a9e2.woff2 +0 -0
  225. package/dist/remotion-bundle/e9c72f4bc37defef.woff2 +0 -0
  226. package/dist/remotion-bundle/e9e35f863403a255.woff2 +0 -0
  227. package/dist/remotion-bundle/eb23b37b009375da.woff2 +0 -0
  228. package/dist/remotion-bundle/ee1342b741625721.woff2 +0 -0
  229. package/dist/remotion-bundle/f07da88543a57ec9.woff2 +0 -0
  230. package/dist/remotion-bundle/f522982115306f8a.woff2 +0 -0
  231. package/dist/remotion-bundle/f8449bd864e6d8bc.woff2 +0 -0
  232. package/dist/remotion-bundle/f906dd5bd95ff9ab.woff2 +0 -0
  233. package/dist/remotion-bundle/f9e9e9413e3c38bb.woff2 +0 -0
  234. package/dist/remotion-bundle/fa5a5b16280994a8.woff2 +0 -0
  235. package/dist/remotion-bundle/favicon.ico +0 -0
  236. package/dist/remotion-bundle/fb19c0517725599b.woff2 +0 -0
  237. package/dist/remotion-bundle/fcaf24232f684b9b.woff2 +0 -0
  238. package/dist/remotion-bundle/fe09e084a3eea8cf.woff2 +0 -0
  239. package/dist/remotion-bundle/ff38d5317df7345a.woff2 +0 -0
  240. package/dist/remotion-bundle/ffe7ea1ea08f455a.woff2 +0 -0
  241. package/dist/remotion-bundle/index.html +49 -0
  242. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/0.mp3 +0 -0
  243. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/1.mp3 +0 -0
  244. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/2.mp3 +0 -0
  245. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/3.mp3 +0 -0
  246. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/0.mp3 +0 -0
  247. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/1.mp3 +0 -0
  248. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/2.mp3 +0 -0
  249. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/3.mp3 +0 -0
  250. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/0.mp3 +0 -0
  251. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/1.mp3 +0 -0
  252. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/2.mp3 +0 -0
  253. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/3.mp3 +0 -0
  254. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/0.mp3 +0 -0
  255. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/1.mp3 +0 -0
  256. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/2.mp3 +0 -0
  257. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/3.mp3 +0 -0
  258. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/0.mp3 +0 -0
  259. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/1.mp3 +0 -0
  260. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/2.mp3 +0 -0
  261. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/3.mp3 +0 -0
  262. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/0.mp3 +0 -0
  263. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/1.mp3 +0 -0
  264. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/2.mp3 +0 -0
  265. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/3.mp3 +0 -0
  266. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/0.mp3 +0 -0
  267. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/1.mp3 +0 -0
  268. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/2.mp3 +0 -0
  269. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/3.mp3 +0 -0
  270. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/0.mp3 +0 -0
  271. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/1.mp3 +0 -0
  272. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/2.mp3 +0 -0
  273. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/3.mp3 +0 -0
  274. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/0.mp3 +0 -0
  275. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/1.mp3 +0 -0
  276. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/2.mp3 +0 -0
  277. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/3.mp3 +0 -0
  278. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/0.mp3 +0 -0
  279. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/1.mp3 +0 -0
  280. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/2.mp3 +0 -0
  281. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/3.mp3 +0 -0
  282. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/0.mp3 +0 -0
  283. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/1.mp3 +0 -0
  284. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/2.mp3 +0 -0
  285. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/3.mp3 +0 -0
  286. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/4.mp3 +0 -0
  287. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/0.mp3 +0 -0
  288. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/1.mp3 +0 -0
  289. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/2.mp3 +0 -0
  290. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/3.mp3 +0 -0
  291. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/0.mp3 +0 -0
  292. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/1.mp3 +0 -0
  293. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/2.mp3 +0 -0
  294. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/3.mp3 +0 -0
  295. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/4.mp3 +0 -0
  296. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/0.mp3 +0 -0
  297. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/1.mp3 +0 -0
  298. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/2.mp3 +0 -0
  299. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/3.mp3 +0 -0
  300. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/4.mp3 +0 -0
  301. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/0.mp3 +0 -0
  302. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/1.mp3 +0 -0
  303. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/2.mp3 +0 -0
  304. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/3.mp3 +0 -0
  305. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/4.mp3 +0 -0
  306. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/0.mp3 +0 -0
  307. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/1.mp3 +0 -0
  308. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/2.mp3 +0 -0
  309. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/3.mp3 +0 -0
  310. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/4.mp3 +0 -0
  311. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/0.mp3 +0 -0
  312. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/1.mp3 +0 -0
  313. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/2.mp3 +0 -0
  314. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/3.mp3 +0 -0
  315. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/0.mp3 +0 -0
  316. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/1.mp3 +0 -0
  317. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/2.mp3 +0 -0
  318. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/3.mp3 +0 -0
  319. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/0.mp3 +0 -0
  320. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/1.mp3 +0 -0
  321. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/2.mp3 +0 -0
  322. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/3.mp3 +0 -0
  323. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/0.mp3 +0 -0
  324. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/1.mp3 +0 -0
  325. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/2.mp3 +0 -0
  326. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/3.mp3 +0 -0
  327. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/4.mp3 +0 -0
  328. package/dist/remotion-bundle/public/voiceover/ai-life/card-0.mp3 +0 -0
  329. package/dist/remotion-bundle/public/voiceover/ai-life/card-1.mp3 +0 -0
  330. package/dist/remotion-bundle/public/voiceover/ai-life/card-2.mp3 +0 -0
  331. package/dist/remotion-bundle/public/voiceover/ai-life/card-3.mp3 +0 -0
  332. package/dist/remotion-bundle/public/voiceover/ai-life/card-4.mp3 +0 -0
  333. package/dist/remotion-bundle/public/voiceover/ai-life/card-5.mp3 +0 -0
  334. package/dist/remotion-bundle/public/voiceover/coffee-science/card-0.mp3 +0 -0
  335. package/dist/remotion-bundle/public/voiceover/coffee-science/card-1.mp3 +0 -0
  336. package/dist/remotion-bundle/public/voiceover/coffee-science/card-2.mp3 +0 -0
  337. package/dist/remotion-bundle/public/voiceover/coffee-science/card-3.mp3 +0 -0
  338. package/dist/remotion-bundle/public/voiceover/coffee-science/card-4.mp3 +0 -0
  339. package/dist/remotion-bundle/public/voiceover/coffee-science/card-5.mp3 +0 -0
  340. package/dist/remotion-bundle/public/voiceover/coffee-science/card-6.mp3 +0 -0
  341. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-0.mp3 +0 -0
  342. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-1.mp3 +0 -0
  343. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-2.mp3 +0 -0
  344. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-3.mp3 +0 -0
  345. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-4.mp3 +0 -0
  346. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-5.mp3 +0 -0
  347. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-6.mp3 +0 -0
  348. package/dist/remotion-bundle/public/voiceover/remote-work/card-0.mp3 +0 -0
  349. package/dist/remotion-bundle/public/voiceover/remote-work/card-1.mp3 +0 -0
  350. package/dist/remotion-bundle/public/voiceover/remote-work/card-2.mp3 +0 -0
  351. package/dist/remotion-bundle/public/voiceover/remote-work/card-3.mp3 +0 -0
  352. package/dist/remotion-bundle/public/voiceover/remote-work/card-4.mp3 +0 -0
  353. package/dist/remotion-bundle/public/voiceover/remote-work/card-5.mp3 +0 -0
  354. package/dist/remotion-bundle/source-map-helper.wasm +0 -0
  355. package/lib/cli.js +270 -0
  356. package/lib/commands/_registry.js +48 -0
  357. package/lib/commands/add.js +242 -0
  358. package/lib/commands/asr/azure-transcribe.js +336 -0
  359. package/lib/commands/asr/cloud-transcribe.js +384 -0
  360. package/lib/commands/asr/helpers.js +76 -0
  361. package/lib/commands/asr/index.js +236 -0
  362. package/lib/commands/asr/local-transcribe.js +125 -0
  363. package/lib/commands/asr-jobs.js +257 -0
  364. package/lib/commands/asr.js +11 -0
  365. package/lib/commands/auth-cmds.js +358 -0
  366. package/lib/commands/dub.js +542 -0
  367. package/lib/commands/explain.js +512 -0
  368. package/lib/commands/feedback.js +152 -0
  369. package/lib/commands/image.js +207 -0
  370. package/lib/commands/mcp-key.js +166 -0
  371. package/lib/commands/narrate.js +639 -0
  372. package/lib/commands/picstory-templates.js +276 -0
  373. package/lib/commands/picstory.js +547 -0
  374. package/lib/commands/podcast/dialogue.js +109 -0
  375. package/lib/commands/podcast/generate.js +127 -0
  376. package/lib/commands/podcast/index.js +561 -0
  377. package/lib/commands/podcast/synthesize.js +188 -0
  378. package/lib/commands/podcast.js +11 -0
  379. package/lib/commands/present.js +519 -0
  380. package/lib/commands/publish.js +415 -0
  381. package/lib/commands/skills.js +473 -0
  382. package/lib/commands/slice-render.js +282 -0
  383. package/lib/commands/slice-stage.js +264 -0
  384. package/lib/commands/slice.js +346 -0
  385. package/lib/commands/slides/constants.js +108 -0
  386. package/lib/commands/slides/html-renderer.js +338 -0
  387. package/lib/commands/slides/index.js +345 -0
  388. package/lib/commands/slides.js +11 -0
  389. package/lib/commands/story.js +302 -0
  390. package/lib/commands/summarize.js +532 -0
  391. package/lib/commands/synthesize.js +261 -0
  392. package/lib/commands/translate.js +593 -0
  393. package/lib/commands/upgrade.js +249 -0
  394. package/lib/commands/video-translate.js +577 -0
  395. package/lib/commands/voices.js +292 -0
  396. package/lib/core/agent-env.js +104 -0
  397. package/lib/core/args.js +107 -0
  398. package/lib/core/asr-client.js +448 -0
  399. package/lib/core/asr-jobs-client.js +126 -0
  400. package/lib/core/asr-jobs-store.js +105 -0
  401. package/lib/core/asr-r2-upload.js +181 -0
  402. package/lib/core/asr-upload.js +132 -0
  403. package/lib/core/audio-extract.js +150 -0
  404. package/lib/core/audio.js +219 -0
  405. package/lib/core/auth.js +880 -0
  406. package/lib/core/config.js +197 -0
  407. package/lib/core/feedback.js +64 -0
  408. package/lib/core/ffmpeg.js +476 -0
  409. package/lib/core/http.js +188 -0
  410. package/lib/core/image-client.js +55 -0
  411. package/lib/core/intent-params.js +11 -0
  412. package/lib/core/llm-client.js +76 -0
  413. package/lib/core/logger.js +208 -0
  414. package/lib/core/mic-recorder.js +182 -0
  415. package/lib/core/pause-markers.js +94 -0
  416. package/lib/core/podcast-pacing.js +118 -0
  417. package/lib/core/spinner.js +33 -0
  418. package/lib/core/srt.js +394 -0
  419. package/lib/core/telemetry.js +100 -0
  420. package/lib/core/timeline.js +92 -0
  421. package/lib/core/tts-synthesizer.js +70 -0
  422. package/lib/core/update-check.js +185 -0
  423. package/lib/core/url-download.js +148 -0
  424. package/lib/core/whisper-local.js +279 -0
  425. package/lib/internal/deck-validator.js +488 -0
  426. package/lib/internal/slice-themes.json +370 -0
  427. package/lib/stage-core/cloud-render.js +170 -0
  428. package/lib/stage-core/deck-format.js +133 -0
  429. package/lib/stage-core/edit-prompt.js +104 -0
  430. package/lib/stage-core/event-bus.js +31 -0
  431. package/lib/stage-core/port.js +46 -0
  432. package/lib/stage-core/server.js +352 -0
  433. package/lib/stage-core/snapshot-store.js +198 -0
  434. package/lib/stage-core/watcher.js +106 -0
  435. package/lib/stage-ui/slice/template.js +1672 -0
  436. package/package.json +9 -4
  437. package/skills/.claude-plugin/marketplace.json +22 -0
  438. package/skills/.claude-plugin/plugin.json +25 -0
  439. package/skills/LICENSE +21 -0
  440. package/skills/README.md +120 -0
  441. package/skills/hub/SKILL.md +317 -0
  442. package/skills/podcast/SKILL.md +146 -0
  443. package/skills/slice/SKILL.md +205 -0
  444. package/skills/slice/agents/openai.yaml +4 -0
  445. package/skills/slice/references/deck-schema.md +183 -0
  446. package/skills/slice/references/example-decks.md +108 -0
  447. package/skills/slice/references/themes.md +172 -0
  448. package/skills/transcribe/SKILL.md +473 -0
  449. package/skills/video/SKILL.md +261 -0
  450. package/skills/voxflow-slice/SKILL.md +271 -0
  451. package/skills/voxflow-slice/examples/article.md +13 -0
  452. package/skills/voxflow-slice/examples/expected-deck.json +39 -0
  453. package/skills/voxflow-slice/examples/validate.mjs +46 -0
@@ -0,0 +1,880 @@
1
+ /**
2
+ * VoxFlow CLI — Authentication module
3
+ *
4
+ * Device-flow login (RFC 8628 inspired):
5
+ * 1. CLI: POST /api/device-auth/code → { deviceCode, userCode, verifyUrl }
6
+ * 2. CLI: print userCode + verifyUrl, optionally open browser
7
+ * 3. User: open verifyUrl in browser, log in via Supabase, confirm pairing
8
+ * 4. CLI: poll POST /api/device-auth/token until 200 or timeout
9
+ * 5. CLI: cache token
10
+ *
11
+ * No localhost callback. Works under PNA, SSH, containers, AI agents.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const crypto = require('crypto');
16
+ const readline = require('readline');
17
+ const { TOKEN_PATH, getConfigDir, LOGIN_PAGE, AUTH_TIMEOUT_MS, API_BASE, SUPABASE_URL, SUPABASE_ANON_KEY } = require('./config');
18
+ const { detectAIAgent, buildUserActionPanel } = require('./agent-env');
19
+
20
+ // Inside an AI agent, user round-trips are slow (the agent has to relay the
21
+ // URL to a human, the human has to context-switch into a browser, etc.).
22
+ // The default 3-minute timeout is too short for that flow — bump to 10 min.
23
+ const AI_AGENT_AUTH_TIMEOUT_MS = 600_000;
24
+
25
+ // ─── Token cache ────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Token 剩余有效期低于此阈值(秒)时视为即将过期,需要刷新或重新登录。
29
+ *
30
+ * 10 分钟 (600s) — 比 5 分钟更激进。voxflow 不少命令是长任务
31
+ * (`video-translate` / `asr-jobs` 轮询 / `render`),启动时多留点裕度
32
+ * 可以避免请求中途 401。AWS botocore 用的就是 600s 的 mandatory_refresh_timeout。
33
+ */
34
+ const MIN_TOKEN_REMAINING_SEC = 600;
35
+ const SUPABASE_AUTH_TIMEOUT_MS = 15_000;
36
+
37
+ // ─── Refresh-token plausibility ─────────────────────────────────────────────
38
+
39
+ /**
40
+ * Detect obviously-bad refresh tokens **before** writing them to the cache,
41
+ * so a malformed paste doesn't silently break the next refresh cycle.
42
+ *
43
+ * Empty / undefined is allowed — "I deliberately don't have one" (CI with
44
+ * only VOXFLOW_TOKEN env var, or first call before login).
45
+ *
46
+ * **History — 2026-05-07 lesson learned**: an earlier version of this
47
+ * validator required ≥20 chars based on a wrong assumption that all
48
+ * Supabase refresh tokens are long. The actual VoxFlow project signs
49
+ * 12-char `[a-z0-9]{12}` refresh tokens, so the strict floor was
50
+ * silently rejecting *every* legitimate token returned by `/auth/v1/token`
51
+ * and stripping it from the cache. That broke the silent-refresh loop
52
+ * for every user who upgraded to 1.10.19 (~24h window).
53
+ *
54
+ * The check now leans on **shape**, not length:
55
+ * - non-empty string
56
+ * - only opaque-token-safe characters (matches what Supabase, OAuth 2,
57
+ * and JWT-style refresh tokens use); rejects whitespace, newlines,
58
+ * quotes, and other paste-corruption signs
59
+ * - very small floor (≥6) to catch literal placeholders ("test", "todo")
60
+ */
61
+ function isPlausibleRefreshToken(token) {
62
+ if (token == null || token === '') return true;
63
+ if (typeof token !== 'string') return false;
64
+ if (token.length < 6) return false;
65
+ if (!/^[A-Za-z0-9._\-+/=]+$/.test(token)) return false;
66
+ return true;
67
+ }
68
+
69
+ function classifyRefreshToken(token) {
70
+ if (token == null || token === '') return 'missing';
71
+ if (!isPlausibleRefreshToken(token)) return 'malformed';
72
+ return 'ok';
73
+ }
74
+
75
+ // ─── Single-flight refresh lock ─────────────────────────────────────────────
76
+ //
77
+ // Multiple parallel CLI invocations (e.g. running 5 `voxflow synthesize` in a
78
+ // shell loop, or a long task spawning sub-tasks) can all hit "token expiring"
79
+ // at the same time. Without a lock, each one independently calls
80
+ // /auth/v1/token?grant_type=refresh_token. Supabase rotates the refresh_token
81
+ // on every call, so whichever response comes back last wins — earlier callers
82
+ // have a stale refresh_token and the next refresh will hard-fail, kicking the
83
+ // user to a browser login. gcloud solves this with a sqlite row lock; we use
84
+ // a simple mkdir-based file lock (atomic on POSIX & NTFS, no native deps).
85
+
86
+ const LOCK_DIR_NAME = 'token.json.lock';
87
+ const LOCK_STALE_MS = 30_000;
88
+ const LOCK_RETRY_MS = 50;
89
+ const LOCK_MAX_WAIT_MS = 5_000;
90
+
91
+ function lockPath() {
92
+ return TOKEN_PATH + '.lock';
93
+ }
94
+
95
+ async function withRefreshLock(fn) {
96
+ const start = Date.now();
97
+ const lockDir = lockPath();
98
+ // Ensure parent dir exists so mkdir(lockDir) doesn't fail with ENOENT
99
+ // before the user has ever logged in.
100
+ try { fs.mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 }); } catch { /* race-safe */ }
101
+
102
+ while (true) {
103
+ try {
104
+ fs.mkdirSync(lockDir);
105
+ try {
106
+ return await fn();
107
+ } finally {
108
+ try { fs.rmdirSync(lockDir); } catch { /* lock already gone */ }
109
+ }
110
+ } catch (err) {
111
+ if (err.code !== 'EEXIST') throw err;
112
+
113
+ // Steal a stale lock from a crashed previous process. statSync may race
114
+ // with the holder's rmdir — that's fine, we'll just retry.
115
+ try {
116
+ const stat = fs.statSync(lockDir);
117
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
118
+ try { fs.rmdirSync(lockDir); } catch { /* someone else freed it */ }
119
+ continue;
120
+ }
121
+ } catch { /* lock disappeared between EEXIST and stat — retry */ }
122
+
123
+ if (Date.now() - start > LOCK_MAX_WAIT_MS) {
124
+ // Degrade gracefully — don't deadlock the user. The worst case is a
125
+ // duplicate refresh; better than blocking forever on a stuck lock.
126
+ return await fn();
127
+ }
128
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
129
+ }
130
+ }
131
+ }
132
+
133
+ // Suppress unused-export warning — exposed via _test for unit tests.
134
+ void LOCK_DIR_NAME;
135
+
136
+ /**
137
+ * Read cached token from disk.
138
+ * Returns null if missing, corrupt, or expired.
139
+ */
140
+ function readCachedToken() {
141
+ try {
142
+ const raw = fs.readFileSync(TOKEN_PATH, 'utf8');
143
+ const cached = JSON.parse(raw);
144
+ if (!cached.access_token) return null;
145
+
146
+ // Decode JWT payload to check expiry
147
+ const payload = decodeJwtPayload(cached.access_token);
148
+ if (!payload || !payload.exp) return null;
149
+
150
+ // Expired if less than 5 minutes remain
151
+ const now = Math.floor(Date.now() / 1000);
152
+ if (payload.exp - now < MIN_TOKEN_REMAINING_SEC) return null;
153
+
154
+ return cached;
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Read cached token without expiry check (for refresh flow).
162
+ * Returns null only if file is missing or corrupt.
163
+ */
164
+ function readRawCachedToken() {
165
+ try {
166
+ const raw = fs.readFileSync(TOKEN_PATH, 'utf8');
167
+ const cached = JSON.parse(raw);
168
+ if (!cached.access_token) return null;
169
+ return cached;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Write token to cache file with restrictive permissions.
177
+ *
178
+ * Defensively strips obviously-malformed refresh_tokens — see
179
+ * isPlausibleRefreshToken() for the rationale (the 12-char paste incident).
180
+ * If a bad refresh_token was provided, we warn on stderr and drop it, so
181
+ * the cache never carries a value that we *know* will fail at refresh time.
182
+ */
183
+ function writeCachedToken(tokenData) {
184
+ const dir = getConfigDir();
185
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
186
+
187
+ let safeData = tokenData;
188
+ if (tokenData && tokenData.refresh_token != null && tokenData.refresh_token !== '') {
189
+ if (!isPlausibleRefreshToken(tokenData.refresh_token)) {
190
+ try {
191
+ process.stderr.write(
192
+ '\x1b[33m⚠ Dropped malformed refresh_token (length ' +
193
+ String(tokenData.refresh_token).length +
194
+ '). Run `voxflow login` to refresh credentials cleanly.\x1b[0m\n'
195
+ );
196
+ } catch { /* stderr might be closed in some sandbox harnesses */ }
197
+ safeData = { ...tokenData, refresh_token: '' };
198
+ }
199
+ }
200
+
201
+ const tmpPath = TOKEN_PATH + '.tmp';
202
+ // Use openSync(O_CREAT|O_WRONLY|O_TRUNC) with mode 0o600 so a leftover
203
+ // .tmp from a crashed previous run is truncated but its mode is the new
204
+ // one, not whatever was on disk. writeFileSync's `mode` only applies on
205
+ // CREATE and is umask-masked, which is why we also chmod afterwards.
206
+ const fd = fs.openSync(tmpPath, 'w', 0o600);
207
+ try {
208
+ fs.writeSync(fd, JSON.stringify(safeData, null, 2));
209
+ } finally {
210
+ fs.closeSync(fd);
211
+ }
212
+ fs.chmodSync(tmpPath, 0o600);
213
+ fs.renameSync(tmpPath, TOKEN_PATH);
214
+ }
215
+
216
+ /**
217
+ * Delete cached token.
218
+ */
219
+ function clearToken() {
220
+ try {
221
+ fs.unlinkSync(TOKEN_PATH);
222
+ } catch {
223
+ // Already gone — fine
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Decode JWT payload (base64url → JSON). No signature verification.
229
+ */
230
+ function decodeJwtPayload(jwt) {
231
+ try {
232
+ const parts = jwt.split('.');
233
+ if (parts.length !== 3) return null;
234
+ const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
235
+ return JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ function parseManualAuthInput(input) {
242
+ const value = String(input || '').trim();
243
+ if (!value) return null;
244
+
245
+ let payload = null;
246
+ if (value.startsWith('{')) {
247
+ try {
248
+ payload = JSON.parse(value);
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+
254
+ const accessToken = payload
255
+ ? (payload.access_token || payload.accessToken || payload.token || '')
256
+ : value;
257
+ const rawRefresh = payload
258
+ ? (payload.refresh_token || payload.refreshToken || '')
259
+ : '';
260
+
261
+ if (!decodeJwtPayload(accessToken)) return null;
262
+
263
+ // Drop a refresh_token that's clearly garbage — but DON'T fail the parse,
264
+ // a valid access_token alone is still usable for ~1h. The browser-paste
265
+ // copy panel sometimes includes the access_token without the refresh_token
266
+ // (older clipboard targets), so a missing refresh is not invalid input.
267
+ const refreshToken = isPlausibleRefreshToken(rawRefresh) ? rawRefresh : '';
268
+
269
+ return { accessToken, refreshToken };
270
+ }
271
+
272
+ /**
273
+ * Resolve token from environment for non-interactive automation.
274
+ * Supported vars: VOXFLOW_TOKEN, VOXFLOW_JWT
275
+ */
276
+ function readEnvToken() {
277
+ const token = (process.env.VOXFLOW_TOKEN || process.env.VOXFLOW_JWT || '').trim();
278
+ if (!token) return null;
279
+
280
+ const payload = decodeJwtPayload(token);
281
+ if (!payload) return null;
282
+
283
+ if (!payload.exp) return null;
284
+
285
+ const now = Math.floor(Date.now() / 1000);
286
+ if (payload.exp - now < MIN_TOKEN_REMAINING_SEC) return null;
287
+
288
+ return token;
289
+ }
290
+
291
+ // ─── Public API ─────────────────────────────────────────────────────────────
292
+
293
+ /**
294
+ * Get a valid JWT token. Uses cache if available, otherwise opens browser login.
295
+ *
296
+ * @param {object} opts
297
+ * @param {string} [opts.api] - API base URL override
298
+ * @param {boolean} [opts.force] - Force re-login even if cached token is valid
299
+ * @param {boolean} [opts.refresh] - Force refresh_token exchange before browser login
300
+ * @returns {Promise<string>} JWT access token
301
+ */
302
+ async function getToken({ api, force, refresh } = {}) {
303
+ // Prefer env token for automation scenarios.
304
+ // This bypasses browser login and works in CI/agent environments.
305
+ if (!force && !refresh) {
306
+ const envToken = readEnvToken();
307
+ if (envToken) return envToken;
308
+ }
309
+
310
+ // Check cache first (unless force)
311
+ if (!force && !refresh) {
312
+ const cached = readCachedToken();
313
+ if (cached) {
314
+ const apiMatch = !api || api === cached.api;
315
+ if (apiMatch) return cached.access_token;
316
+ }
317
+ }
318
+
319
+ if (!force) {
320
+ // Token expired or explicitly rejected — try silent refresh before opening browser.
321
+ const refreshed = await refreshCachedToken(api);
322
+ if (refreshed) return refreshed;
323
+ }
324
+
325
+ // Need to login via browser (works in both interactive and non-interactive environments)
326
+ return browserLogin(api || API_BASE);
327
+ }
328
+
329
+ async function refreshCachedToken(apiBase) {
330
+ return withRefreshLock(async () => {
331
+ // Re-read under lock — another process / parallel `voxflow` invocation
332
+ // may have just refreshed for us, in which case readCachedToken returns
333
+ // a still-valid token (>10 min remaining). Skipping the network call
334
+ // avoids burning a Supabase rotation slot we don't need.
335
+ const stillValid = readCachedToken();
336
+ if (stillValid) return stillValid.access_token;
337
+
338
+ const raw = readRawCachedToken();
339
+ if (!raw?.refresh_token) return null;
340
+ if (!isPlausibleRefreshToken(raw.refresh_token)) return null;
341
+ return tryRefreshToken(raw.refresh_token, apiBase || raw.api || API_BASE);
342
+ });
343
+ }
344
+
345
+ /**
346
+ * Silently refresh an expired access_token using the stored refresh_token.
347
+ * Calls Supabase /auth/v1/token?grant_type=refresh_token.
348
+ *
349
+ * @param {string} refreshToken
350
+ * @param {string} apiBase - API base URL for caching
351
+ * @returns {Promise<string|null>} new access_token, or null on failure
352
+ */
353
+ async function tryRefreshToken(refreshToken, apiBase = API_BASE) {
354
+ try {
355
+ const result = await supabasePost(
356
+ `${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`,
357
+ { refresh_token: refreshToken }
358
+ );
359
+
360
+ if (result.status >= 400 || !result.data?.access_token) return null;
361
+
362
+ const token = result.data.access_token;
363
+ const payload = decodeJwtPayload(token);
364
+
365
+ writeCachedToken({
366
+ access_token: token,
367
+ refresh_token: result.data.refresh_token || refreshToken,
368
+ expires_at: payload?.exp || 0,
369
+ email: payload?.email || '',
370
+ api: apiBase,
371
+ cached_at: new Date().toISOString(),
372
+ });
373
+
374
+ return token;
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Get info about the currently cached token.
382
+ * Returns null if no valid cached token.
383
+ */
384
+ function getTokenInfo() {
385
+ const cached = readCachedToken();
386
+ if (!cached) return null;
387
+
388
+ const payload = decodeJwtPayload(cached.access_token);
389
+ if (!payload) return null;
390
+
391
+ const now = Math.floor(Date.now() / 1000);
392
+ return {
393
+ email: payload.email || cached.email || '(unknown)',
394
+ expiresAt: new Date(payload.exp * 1000).toISOString(),
395
+ remaining: payload.exp - now,
396
+ valid: payload.exp - now > MIN_TOKEN_REMAINING_SEC,
397
+ api: cached.api || API_BASE,
398
+ refreshHealth: classifyRefreshToken(cached.refresh_token),
399
+ };
400
+ }
401
+
402
+ async function getFreshTokenInfo({ api } = {}) {
403
+ const current = getTokenInfo();
404
+ if (current) return current;
405
+
406
+ const refreshed = await refreshCachedToken(api);
407
+ if (!refreshed) return null;
408
+ return getTokenInfo();
409
+ }
410
+
411
+ // ─── Background refresh for long-running commands ───────────────────────────
412
+ //
413
+ // Long-running commands (`video-translate`, `asr-jobs` polling, large
414
+ // `dub` runs, future `render`) routinely outlive the 1-hour Supabase JWT.
415
+ // runWithRetry catches a 401 *after* it happens — that's a single
416
+ // recoverable miss, but the user sees a noisy "Token expired, refreshing..."
417
+ // line in the middle of progress output. This helper schedules a refresh
418
+ // **before** expiry so the long task feels seamless.
419
+ //
420
+ // Usage:
421
+ // const stop = startBackgroundRefresh({ api });
422
+ // try {
423
+ // // ... long task ...
424
+ // } finally {
425
+ // stop();
426
+ // }
427
+ //
428
+ // Aligned with AWS botocore's RefreshableCredentials pattern but stripped
429
+ // to a single Node.js setTimeout — no thread lock needed since we already
430
+ // hold withRefreshLock() inside refreshCachedToken().
431
+
432
+ const BG_REFRESH_LEAD_SEC = 300; // refresh 5 min before expiry
433
+
434
+ function startBackgroundRefresh({ api } = {}) {
435
+ let stopped = false;
436
+ let timer = null;
437
+
438
+ function schedule() {
439
+ if (stopped) return;
440
+ const cached = readRawCachedToken();
441
+ if (!cached?.access_token) return;
442
+ const payload = decodeJwtPayload(cached.access_token);
443
+ if (!payload?.exp) return;
444
+
445
+ const now = Math.floor(Date.now() / 1000);
446
+ const secsUntilRefresh = Math.max(30, payload.exp - now - BG_REFRESH_LEAD_SEC);
447
+
448
+ timer = setTimeout(async () => {
449
+ if (stopped) return;
450
+ try { await refreshCachedToken(api); } catch { /* swallow — retry on 401 will catch it */ }
451
+ schedule();
452
+ }, secsUntilRefresh * 1000);
453
+
454
+ // Don't keep the event loop alive solely for the refresh timer — when
455
+ // the long-running command finishes, Node should exit even if stop()
456
+ // wasn't explicitly called (defensive against forgotten cleanup).
457
+ if (typeof timer.unref === 'function') timer.unref();
458
+ }
459
+
460
+ schedule();
461
+
462
+ return function stop() {
463
+ stopped = true;
464
+ if (timer) { clearTimeout(timer); timer = null; }
465
+ };
466
+ }
467
+
468
+ // ─── Device-flow login (RFC 8628 inspired) ──────────────────────────────────
469
+ //
470
+ // The previous browserLogin used a localhost callback server, which broke
471
+ // under Chrome's Private Network Access policy (HTTPS → http://127.0.0.1
472
+ // requires a CORS+PNA preflight that we never answered). It also failed in
473
+ // SSH sessions, dev containers, and Claude-Code-piped environments.
474
+ //
475
+ // New flow:
476
+ // 1. POST /api/device-auth/code → { deviceCode, userCode, verifyUrl, interval, expiresIn }
477
+ // 2. Display userCode + verifyUrl. Optionally open browser.
478
+ // 3. Poll POST /api/device-auth/token { deviceCode } every `interval` seconds:
479
+ // 202 authorization_pending → keep polling
480
+ // 200 success → cache tokens, return
481
+ // 410 expired / 404 invalid / 409 consumed / 429 too_many → fail
482
+ //
483
+ // No localhost listener, no stdin paste. Works in every environment that
484
+ // can reach the API.
485
+
486
+ /**
487
+ * POST a JSON body to a URL, parse JSON response. Tiny self-contained helper
488
+ * to keep this module dep-free of `core/http.js` (which depends on logger
489
+ * which depends on a bunch of stuff we don't want pulled in here).
490
+ */
491
+ function postDeviceAuth(apiBase, path, body) {
492
+ return new Promise((resolve, reject) => {
493
+ const url = new URL(path, apiBase);
494
+ const mod = url.protocol === 'https:' ? require('https') : require('http');
495
+ const postData = JSON.stringify(body || {});
496
+ const req = mod.request({
497
+ hostname: url.hostname,
498
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
499
+ path: url.pathname + url.search,
500
+ method: 'POST',
501
+ headers: {
502
+ 'Content-Type': 'application/json',
503
+ 'Content-Length': Buffer.byteLength(postData),
504
+ 'X-Client-Source': 'cli',
505
+ },
506
+ }, (resp) => {
507
+ const chunks = [];
508
+ resp.on('data', (c) => chunks.push(c));
509
+ resp.on('end', () => {
510
+ const raw = Buffer.concat(chunks).toString('utf8');
511
+ try {
512
+ resolve({ status: resp.statusCode, data: JSON.parse(raw) });
513
+ } catch {
514
+ resolve({ status: resp.statusCode, data: { raw } });
515
+ }
516
+ });
517
+ });
518
+ req.on('error', reject);
519
+ req.setTimeout(30_000, () => {
520
+ req.destroy(new Error('Device auth request timeout (30s)'));
521
+ });
522
+ req.write(postData);
523
+ req.end();
524
+ });
525
+ }
526
+
527
+ const POLL_INTERVAL_FALLBACK_SEC = 5;
528
+
529
+ function browserLogin(apiBase) {
530
+ return new Promise((resolve, reject) => {
531
+ (async () => {
532
+ // ─── Step 1: request a device code ─────────────────────────────────
533
+ let deviceCode, userCode, verifyUrl, intervalSec, expiresInSec;
534
+ try {
535
+ const { status, data } = await postDeviceAuth(apiBase, '/api/device-auth/code', {});
536
+ if (status !== 200 || data?.code !== 'success' || !data?.data?.deviceCode) {
537
+ return reject(new Error(
538
+ `Failed to start device login (HTTP ${status}): ${data?.message || 'unexpected response'}`
539
+ ));
540
+ }
541
+ ({ deviceCode, userCode, verifyUrl } = data.data);
542
+ intervalSec = data.data.interval || POLL_INTERVAL_FALLBACK_SEC;
543
+ expiresInSec = data.data.expiresIn || 300;
544
+ } catch (err) {
545
+ return reject(new Error(`Cannot reach ${apiBase}: ${err.message}`));
546
+ }
547
+
548
+ // ─── Step 2: show code + URL, open browser if GUI ──────────────────
549
+ const isInteractive = process.stdin.isTTY;
550
+ const agentName = detectAIAgent();
551
+ const inAIAgent = agentName !== null;
552
+ const hasGui = process.platform === 'darwin'
553
+ || process.platform === 'win32'
554
+ || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
555
+ const shouldOpenBrowser = !process.env.CI && (isInteractive || hasGui);
556
+
557
+ if (inAIAgent) {
558
+ const timeoutMs = Math.min(AI_AGENT_AUTH_TIMEOUT_MS, expiresInSec * 1000);
559
+ process.stderr.write(buildUserActionPanel({
560
+ loginUrl: verifyUrl,
561
+ agentName,
562
+ timeoutSec: Math.round(timeoutMs / 1000),
563
+ userCode,
564
+ }));
565
+ } else {
566
+ const out = isInteractive ? console.log : (msg) => process.stderr.write(msg + '\n');
567
+ out('');
568
+ out('\x1b[33m🔐 Confirm device authorization\x1b[0m');
569
+ out('');
570
+ out(` Pairing code: \x1b[1;36m${userCode}\x1b[0m`);
571
+ out(` Verify at: \x1b[36m${verifyUrl}\x1b[0m`);
572
+ out('');
573
+ out(` Code expires in ${Math.round(expiresInSec / 60)} min. Polling every ${intervalSec}s...`);
574
+ out('');
575
+ }
576
+
577
+ if (shouldOpenBrowser) {
578
+ try {
579
+ const open = (await import('open')).default;
580
+ const cp = await open(verifyUrl);
581
+ if (cp && typeof cp.on === 'function') {
582
+ cp.on('error', () => {
583
+ process.stderr.write('\x1b[31m Browser auto-open failed. Open the URL above manually.\x1b[0m\n');
584
+ });
585
+ }
586
+ } catch {
587
+ process.stderr.write('\x1b[31m Browser auto-open failed. Open the URL above manually.\x1b[0m\n');
588
+ }
589
+ }
590
+
591
+ // ─── Step 3: poll until approved or expired ────────────────────────
592
+ const overallTimeoutMs = Math.min(
593
+ inAIAgent ? AI_AGENT_AUTH_TIMEOUT_MS : AUTH_TIMEOUT_MS,
594
+ expiresInSec * 1000
595
+ );
596
+ const deadline = Date.now() + overallTimeoutMs;
597
+ let pollIntervalMs = intervalSec * 1000;
598
+
599
+ while (Date.now() < deadline) {
600
+ await new Promise(r => setTimeout(r, pollIntervalMs));
601
+
602
+ let pollResp;
603
+ try {
604
+ pollResp = await postDeviceAuth(apiBase, '/api/device-auth/token', { deviceCode });
605
+ } catch (err) {
606
+ // Transient network error — keep polling, don't bail.
607
+ process.stderr.write(`\x1b[33m Poll error (${err.message}), retrying...\x1b[0m\n`);
608
+ continue;
609
+ }
610
+
611
+ const { status, data } = pollResp;
612
+ const code = data?.code;
613
+
614
+ if (status === 200 && code === 'success' && data?.data?.token) {
615
+ const token = data.data.token;
616
+ const refreshToken = data.data.refreshToken || '';
617
+ const payload = decodeJwtPayload(token);
618
+ writeCachedToken({
619
+ access_token: token,
620
+ refresh_token: refreshToken,
621
+ expires_at: payload?.exp || 0,
622
+ email: payload?.email || '',
623
+ api: apiBase,
624
+ cached_at: new Date().toISOString(),
625
+ });
626
+ if (isInteractive) {
627
+ console.log(`\x1b[32m✓ Authorized (${payload?.email || 'user'})\x1b[0m\n`);
628
+ }
629
+ return resolve(token);
630
+ }
631
+
632
+ if (status === 202 || code === 'authorization_pending') {
633
+ continue;
634
+ }
635
+ if (code === 'too_many_attempts') {
636
+ // Server says back off — double interval (with cap).
637
+ pollIntervalMs = Math.min(pollIntervalMs * 2, 30_000);
638
+ continue;
639
+ }
640
+ if (code === 'expired' || status === 410) {
641
+ return reject(new Error(
642
+ `Pairing code expired before login. Run \`voxflow login\` again.`
643
+ ));
644
+ }
645
+ if (code === 'invalid_device_code' || status === 404) {
646
+ return reject(new Error(
647
+ `Pairing code invalidated by server. Run \`voxflow login\` again.`
648
+ ));
649
+ }
650
+ if (code === 'already_consumed' || status === 409) {
651
+ return reject(new Error(
652
+ `Token already issued for this code. Run \`voxflow login\` again.`
653
+ ));
654
+ }
655
+ // Unknown response — keep polling but log it.
656
+ process.stderr.write(`\x1b[33m Unexpected poll response (HTTP ${status}, code=${code}), retrying...\x1b[0m\n`);
657
+ }
658
+
659
+ return reject(new Error(
660
+ `Login timed out (${Math.round(overallTimeoutMs / 1000)}s). ` +
661
+ `Please retry: voxflow login ` +
662
+ `(or set VOXFLOW_TOKEN in env to skip browser login)`
663
+ ));
664
+ })();
665
+ });
666
+ }
667
+
668
+ // ─── Terminal OTP login (no browser) ────────────────────────────────────────
669
+
670
+ /**
671
+ * Prompt for email + OTP code in terminal, authenticate via Supabase REST API.
672
+ * No browser needed — works in SSH, containers, and headless environments.
673
+ *
674
+ * @param {string} apiBase - API base URL (for caching)
675
+ * @returns {Promise<string>} JWT access token
676
+ */
677
+ function terminalOtpLogin(apiBase = API_BASE) {
678
+ return new Promise((resolve, reject) => {
679
+ const rl = readline.createInterface({
680
+ input: process.stdin,
681
+ output: process.stdout,
682
+ });
683
+
684
+ function ask(question) {
685
+ return new Promise(r => rl.question(question, r));
686
+ }
687
+
688
+ (async () => {
689
+ try {
690
+ console.log('\n\x1b[33m🔐 Terminal login (no browser needed)\x1b[0m\n');
691
+
692
+ // Step 1: Get email
693
+ const email = (await ask(' Email: ')).trim();
694
+ if (!email || !email.includes('@')) {
695
+ throw new Error('Invalid email address.');
696
+ }
697
+
698
+ // Step 2: Send OTP via Supabase REST API
699
+ process.stdout.write(' Sending verification code... ');
700
+
701
+ const otpResult = await supabasePost(`${SUPABASE_URL}/auth/v1/otp`, {
702
+ email,
703
+ create_user: true,
704
+ });
705
+
706
+ if (otpResult.status >= 400) {
707
+ const msg = otpResult.data?.error_description || otpResult.data?.msg || otpResult.data?.message || JSON.stringify(otpResult.data);
708
+ throw new Error(`OTP send failed (${otpResult.status}): ${msg}`);
709
+ }
710
+
711
+ console.log('✓ Code sent!');
712
+ console.log(' \x1b[36mCheck your email for the 6-digit verification code.\x1b[0m\n');
713
+
714
+ // Step 3: Get OTP code
715
+ const code = (await ask(' Verification code: ')).trim();
716
+ if (!code) {
717
+ throw new Error('No verification code entered.');
718
+ }
719
+
720
+ // Step 4: Verify OTP via Supabase REST API
721
+ process.stdout.write(' Verifying... ');
722
+
723
+ const verifyResult = await supabasePost(`${SUPABASE_URL}/auth/v1/verify`, {
724
+ email,
725
+ token: code,
726
+ type: 'email',
727
+ });
728
+
729
+ if (verifyResult.status >= 400 || !verifyResult.data?.access_token) {
730
+ const msg = verifyResult.data?.error_description || verifyResult.data?.msg || verifyResult.data?.message || 'Invalid or expired code';
731
+ throw new Error(`Verification failed: ${msg}`);
732
+ }
733
+
734
+ const token = verifyResult.data.access_token;
735
+ const payload = decodeJwtPayload(token);
736
+
737
+ // Cache token
738
+ writeCachedToken({
739
+ access_token: token,
740
+ refresh_token: verifyResult.data.refresh_token || '',
741
+ expires_at: payload?.exp || 0,
742
+ email: payload?.email || email,
743
+ api: apiBase,
744
+ cached_at: new Date().toISOString(),
745
+ });
746
+
747
+ console.log('✓ Success!');
748
+ console.log(`\n\x1b[32m✓ Logged in as ${payload?.email || email}\x1b[0m\n`);
749
+
750
+ rl.close();
751
+ resolve(token);
752
+ } catch (err) {
753
+ rl.close();
754
+ reject(err);
755
+ }
756
+ })();
757
+ });
758
+ }
759
+
760
+ /**
761
+ * POST to Supabase auth endpoint with apikey header.
762
+ */
763
+ function supabasePost(url, body) {
764
+ const https = require('https');
765
+ const postData = JSON.stringify(body);
766
+ const parsed = new URL(url);
767
+
768
+ return new Promise((resolve, reject) => {
769
+ const req = https.request({
770
+ hostname: parsed.hostname,
771
+ port: 443,
772
+ // Keep query string (e.g. ?grant_type=refresh_token), otherwise
773
+ // refresh-token flow will silently fail and always fall back to login.
774
+ path: parsed.pathname + parsed.search,
775
+ method: 'POST',
776
+ headers: {
777
+ 'Content-Type': 'application/json',
778
+ 'apikey': SUPABASE_ANON_KEY,
779
+ 'Content-Length': Buffer.byteLength(postData),
780
+ },
781
+ }, (resp) => {
782
+ let data = '';
783
+ resp.on('data', d => data += d);
784
+ resp.on('end', () => {
785
+ try {
786
+ resolve({ status: resp.statusCode, data: JSON.parse(data) });
787
+ } catch {
788
+ resolve({ status: resp.statusCode, data });
789
+ }
790
+ });
791
+ });
792
+ req.on('error', reject);
793
+ req.setTimeout(SUPABASE_AUTH_TIMEOUT_MS, () => {
794
+ req.destroy(new Error(`Supabase auth request timeout (${SUPABASE_AUTH_TIMEOUT_MS / 1000}s)`));
795
+ });
796
+ req.write(postData);
797
+ req.end();
798
+ });
799
+ }
800
+
801
+ /**
802
+ * Build the same login URL that browserLogin generates, but DO NOT listen
803
+ * for the callback. Used by `voxflow login --print-url` so AI agents that
804
+ * can't run interactive flows can hand the URL to a human, then receive
805
+ * the auth payload back through `voxflow login --paste`.
806
+ *
807
+ * The URL still carries a `callback_port` for compatibility with the page,
808
+ * but it points to nothing — the page's JS detects the callback failure
809
+ * and shows the manual-paste UI with the JSON payload visible.
810
+ */
811
+ function buildPrintOnlyLoginUrl() {
812
+ // callback_port=0 is the sentinel — clearly not a real listener, so the
813
+ // page falls back to the manual-paste UI showing the JSON payload.
814
+ // No state param: the paste-back flow returns the JWT directly, so a
815
+ // request/response state-binding token would not bind anything that
816
+ // matters (an attacker controlling the URL only forces the user to
817
+ // paste back their own session). Removed to avoid signaling a CSRF
818
+ // protection that does not exist.
819
+ return `${LOGIN_PAGE}?callback_port=0`;
820
+ }
821
+
822
+ /**
823
+ * Accept a manually-provided auth payload (JSON or raw JWT) and write it
824
+ * to the local token cache. Validates that the access_token decodes as a
825
+ * JWT and is not already expired. Returns { ok, info, reason } where info
826
+ * is the same shape as getTokenInfo() on success.
827
+ *
828
+ * Used by `voxflow login --paste '<payload>'`.
829
+ */
830
+ function acceptManualAuthPayload(input, apiBase = API_BASE) {
831
+ const parsed = parseManualAuthInput(input);
832
+ if (!parsed) {
833
+ return { ok: false, reason: 'invalid-payload', message: 'Could not parse the auth payload — expected JSON {"access_token":"..."} or a raw JWT.' };
834
+ }
835
+ const payload = decodeJwtPayload(parsed.accessToken);
836
+ if (!payload) {
837
+ return { ok: false, reason: 'invalid-jwt', message: 'access_token is not a valid JWT.' };
838
+ }
839
+ const now = Math.floor(Date.now() / 1000);
840
+ if (payload.exp && payload.exp < now) {
841
+ return { ok: false, reason: 'expired', message: 'Token has already expired. Re-run the login URL and paste a fresh payload.' };
842
+ }
843
+ writeCachedToken({
844
+ access_token: parsed.accessToken,
845
+ refresh_token: parsed.refreshToken || '',
846
+ expires_at: payload.exp || 0,
847
+ email: payload.email || '',
848
+ api: apiBase,
849
+ cached_at: new Date().toISOString(),
850
+ });
851
+ return {
852
+ ok: true,
853
+ info: {
854
+ email: payload.email || '(unknown)',
855
+ expiresAt: new Date((payload.exp || 0) * 1000).toISOString(),
856
+ api: apiBase,
857
+ },
858
+ };
859
+ }
860
+
861
+ module.exports = {
862
+ getToken,
863
+ clearToken,
864
+ getTokenInfo,
865
+ getFreshTokenInfo,
866
+ terminalOtpLogin,
867
+ readCachedToken,
868
+ buildPrintOnlyLoginUrl,
869
+ acceptManualAuthPayload,
870
+ startBackgroundRefresh,
871
+ isPlausibleRefreshToken,
872
+ _test: {
873
+ parseManualAuthInput,
874
+ tryRefreshToken,
875
+ withRefreshLock,
876
+ refreshCachedToken,
877
+ classifyRefreshToken,
878
+ MIN_TOKEN_REMAINING_SEC,
879
+ },
880
+ };