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.
- package/README.md +34 -0
- package/bin/voxflow.js +27 -0
- package/dist/index.js +1 -1
- package/dist/remotion-bundle/02a2fb2eb80bc7bf.woff2 +0 -0
- package/dist/remotion-bundle/052ca5351e5e06ba.woff2 +0 -0
- package/dist/remotion-bundle/05853dd28f4019cb.woff2 +0 -0
- package/dist/remotion-bundle/072ead3737f7c0d0.woff2 +0 -0
- package/dist/remotion-bundle/07d4248613c86a2e.woff2 +0 -0
- package/dist/remotion-bundle/0884a5c2d1d2d99b.woff2 +0 -0
- package/dist/remotion-bundle/0b0e185b2752095e.woff2 +0 -0
- package/dist/remotion-bundle/0e66c11bde067d91.woff2 +0 -0
- package/dist/remotion-bundle/0f7794cfba2c5d21.woff2 +0 -0
- package/dist/remotion-bundle/0fdbae5a4365783a.woff2 +0 -0
- package/dist/remotion-bundle/112.bundle.js +11 -0
- package/dist/remotion-bundle/112.bundle.js.map +1 -0
- package/dist/remotion-bundle/113.bundle.js +11 -0
- package/dist/remotion-bundle/113.bundle.js.map +1 -0
- package/dist/remotion-bundle/119cae0c4c16f7ed.woff2 +0 -0
- package/dist/remotion-bundle/14725f649fd1e78c.woff2 +0 -0
- package/dist/remotion-bundle/14abe9e3f95f7888.woff2 +0 -0
- package/dist/remotion-bundle/163.bundle.js +14678 -0
- package/dist/remotion-bundle/163.bundle.js.map +1 -0
- package/dist/remotion-bundle/1808c54072bf6d14.woff2 +0 -0
- package/dist/remotion-bundle/18948bec3e3012fe.woff2 +0 -0
- package/dist/remotion-bundle/1a661c60d0fc84fc.woff2 +0 -0
- package/dist/remotion-bundle/1af94941e1bc7e1e.woff2 +0 -0
- package/dist/remotion-bundle/1bee0219595f606c.woff2 +0 -0
- package/dist/remotion-bundle/1bfd5da7ce9d4ec4.woff2 +0 -0
- package/dist/remotion-bundle/1c158d56f1884f3f.woff2 +0 -0
- package/dist/remotion-bundle/1cf5e88e667610eb.woff2 +0 -0
- package/dist/remotion-bundle/1d431bd10f53c481.woff2 +0 -0
- package/dist/remotion-bundle/1d701a81a7670db2.woff2 +0 -0
- package/dist/remotion-bundle/1da0fecad4240f16.woff2 +0 -0
- package/dist/remotion-bundle/1ed14d3d0c5c63fe.woff2 +0 -0
- package/dist/remotion-bundle/1edfecf40e586f53.woff2 +0 -0
- package/dist/remotion-bundle/1f479711bc34b054.woff +0 -0
- package/dist/remotion-bundle/1f86e54a0ff5fcd1.woff2 +0 -0
- package/dist/remotion-bundle/2043ea87d9aabd11.woff2 +0 -0
- package/dist/remotion-bundle/20563c39ee8a0e40.woff2 +0 -0
- package/dist/remotion-bundle/20c231590fd12c44.woff2 +0 -0
- package/dist/remotion-bundle/20ce61713f754c07.woff2 +0 -0
- package/dist/remotion-bundle/21eb9306fce24bb1.woff2 +0 -0
- package/dist/remotion-bundle/244bf71c0cc851af.woff2 +0 -0
- package/dist/remotion-bundle/274d4cfc02bffbcb.woff2 +0 -0
- package/dist/remotion-bundle/275.bundle.js +21 -0
- package/dist/remotion-bundle/275.bundle.js.map +1 -0
- package/dist/remotion-bundle/2958f540b39513dc.woff2 +0 -0
- package/dist/remotion-bundle/2a168b98fd97722e.woff2 +0 -0
- package/dist/remotion-bundle/2d1f6373937ab55f.woff2 +0 -0
- package/dist/remotion-bundle/2d213ae47ff6daa9.woff2 +0 -0
- package/dist/remotion-bundle/2e4b1f04fcd05047.woff2 +0 -0
- package/dist/remotion-bundle/304170d98f4c4563.woff2 +0 -0
- package/dist/remotion-bundle/30d02e136e7a5642.woff2 +0 -0
- package/dist/remotion-bundle/3135562b52a714cd.woff2 +0 -0
- package/dist/remotion-bundle/313713af2c8144e9.woff2 +0 -0
- package/dist/remotion-bundle/325fa4108d2285b9.woff2 +0 -0
- package/dist/remotion-bundle/338e927ed3345e0c.woff2 +0 -0
- package/dist/remotion-bundle/35fc6b190365bc17.woff2 +0 -0
- package/dist/remotion-bundle/37a51f1122d4efc5.woff2 +0 -0
- package/dist/remotion-bundle/39a4d63e02736f5e.woff2 +0 -0
- package/dist/remotion-bundle/3a00e0d62dfc4171.woff2 +0 -0
- package/dist/remotion-bundle/3a6955e6561affe1.woff2 +0 -0
- package/dist/remotion-bundle/3c573945aef49b89.woff2 +0 -0
- package/dist/remotion-bundle/3cdbfbfa23b516a5.woff2 +0 -0
- package/dist/remotion-bundle/3e42f85a9e64ca8a.woff2 +0 -0
- package/dist/remotion-bundle/3e83eaf1ec859415.woff2 +0 -0
- package/dist/remotion-bundle/3f3c8c90de1250ee.woff2 +0 -0
- package/dist/remotion-bundle/434.bundle.js +205 -0
- package/dist/remotion-bundle/434.bundle.js.map +1 -0
- package/dist/remotion-bundle/44ffc6ca4d781692.woff2 +0 -0
- package/dist/remotion-bundle/4670d9c4580b09eb.woff2 +0 -0
- package/dist/remotion-bundle/479756881b302824.woff2 +0 -0
- package/dist/remotion-bundle/481b82134bfa9c82.woff2 +0 -0
- package/dist/remotion-bundle/48d27029626f4328.woff2 +0 -0
- package/dist/remotion-bundle/49b7b2a30329c511.woff2 +0 -0
- package/dist/remotion-bundle/4c8b25a1a9337045.woff2 +0 -0
- package/dist/remotion-bundle/4cba14788ca9259b.woff2 +0 -0
- package/dist/remotion-bundle/4cd6c589c004a6a7.woff2 +0 -0
- package/dist/remotion-bundle/4cd8d79c1021608d.woff2 +0 -0
- package/dist/remotion-bundle/4d8fa99b3f00f9f0.woff2 +0 -0
- package/dist/remotion-bundle/4e7805a643f86d53.woff2 +0 -0
- package/dist/remotion-bundle/4ff91be454542e3f.woff2 +0 -0
- package/dist/remotion-bundle/504cbcba1f63591b.woff2 +0 -0
- package/dist/remotion-bundle/5202d792e5791d6c.woff2 +0 -0
- package/dist/remotion-bundle/534db5ad4770cc1d.woff2 +0 -0
- package/dist/remotion-bundle/53b9568eb85f866b.woff2 +0 -0
- package/dist/remotion-bundle/543ad386ca171de9.woff2 +0 -0
- package/dist/remotion-bundle/54798e55bbf7976e.woff2 +0 -0
- package/dist/remotion-bundle/580.bundle.js +11 -0
- package/dist/remotion-bundle/580.bundle.js.map +1 -0
- package/dist/remotion-bundle/58d174d1193af6d1.woff2 +0 -0
- package/dist/remotion-bundle/591d29ff3ff53c80.woff2 +0 -0
- package/dist/remotion-bundle/5c28c4f4824383c6.woff2 +0 -0
- package/dist/remotion-bundle/5da9740d2ce894c8.woff2 +0 -0
- package/dist/remotion-bundle/6197735364642360.woff2 +0 -0
- package/dist/remotion-bundle/6265a4335724080f.woff2 +0 -0
- package/dist/remotion-bundle/633f5e4f6394daa7.woff2 +0 -0
- package/dist/remotion-bundle/637d95ace6a69c49.woff2 +0 -0
- package/dist/remotion-bundle/648e04a04dacff8f.woff2 +0 -0
- package/dist/remotion-bundle/64a6e83045a008b2.woff2 +0 -0
- package/dist/remotion-bundle/651.bundle.js +11 -0
- package/dist/remotion-bundle/651.bundle.js.map +1 -0
- package/dist/remotion-bundle/65e2a988c070facc.woff2 +0 -0
- package/dist/remotion-bundle/66a2f6ce5cc69105.woff2 +0 -0
- package/dist/remotion-bundle/690.bundle.js +3479 -0
- package/dist/remotion-bundle/690.bundle.js.map +1 -0
- package/dist/remotion-bundle/690ff55252ca715d.woff2 +0 -0
- package/dist/remotion-bundle/6a01a1cff49314fc.woff2 +0 -0
- package/dist/remotion-bundle/6cbc32670982986c.woff2 +0 -0
- package/dist/remotion-bundle/6d3cc42ae547f454.woff2 +0 -0
- package/dist/remotion-bundle/6d8f4cfa1ddc0830.woff2 +0 -0
- package/dist/remotion-bundle/6e4d7c6ae65e2dc3.woff2 +0 -0
- package/dist/remotion-bundle/6e86418bbcefb2e8.woff2 +0 -0
- package/dist/remotion-bundle/6ee02884b29cf7fb.woff2 +0 -0
- package/dist/remotion-bundle/6f436a74c9e3252c.woff2 +0 -0
- package/dist/remotion-bundle/78c8022f1657618b.woff2 +0 -0
- package/dist/remotion-bundle/7c5444169792bca4.woff2 +0 -0
- package/dist/remotion-bundle/7c86bddd9d997212.woff2 +0 -0
- package/dist/remotion-bundle/7e1284684767f584.woff2 +0 -0
- package/dist/remotion-bundle/7e81c17522d182b2.woff2 +0 -0
- package/dist/remotion-bundle/7eb87be198f7858c.woff2 +0 -0
- package/dist/remotion-bundle/8060c928f948aab5.woff2 +0 -0
- package/dist/remotion-bundle/80bc9dfbea2b35ae.woff2 +0 -0
- package/dist/remotion-bundle/811b83f69963bb48.woff2 +0 -0
- package/dist/remotion-bundle/813.bundle.js +117511 -0
- package/dist/remotion-bundle/813.bundle.js.map +1 -0
- package/dist/remotion-bundle/84df492e349f82e9.woff2 +0 -0
- package/dist/remotion-bundle/8501bfd73eb36f2b.woff2 +0 -0
- package/dist/remotion-bundle/854236a8376093fe.woff2 +0 -0
- package/dist/remotion-bundle/8571d74529082753.woff2 +0 -0
- package/dist/remotion-bundle/860bf44f8e6f4b5d.woff2 +0 -0
- package/dist/remotion-bundle/879.bundle.js +64 -0
- package/dist/remotion-bundle/879.bundle.js.map +1 -0
- package/dist/remotion-bundle/887dd482f848d56f.woff2 +0 -0
- package/dist/remotion-bundle/89b2132e85fbbb5a.woff2 +0 -0
- package/dist/remotion-bundle/8ba60d6c306010c2.woff2 +0 -0
- package/dist/remotion-bundle/8c7c4dadea897806.woff2 +0 -0
- package/dist/remotion-bundle/8c943f9999706f61.woff2 +0 -0
- package/dist/remotion-bundle/8f2a718c90575cc9.woff2 +0 -0
- package/dist/remotion-bundle/906b6edb3e1772c9.woff2 +0 -0
- package/dist/remotion-bundle/930ff9daccdf14eb.woff2 +0 -0
- package/dist/remotion-bundle/934db2f1c403c4d0.woff2 +0 -0
- package/dist/remotion-bundle/938.bundle.js +451 -0
- package/dist/remotion-bundle/938.bundle.js.map +1 -0
- package/dist/remotion-bundle/967.bundle.js +4462 -0
- package/dist/remotion-bundle/967.bundle.js.map +1 -0
- package/dist/remotion-bundle/9684a1093d3c02ce.woff2 +0 -0
- package/dist/remotion-bundle/973dcd0faa6116cc.woff2 +0 -0
- package/dist/remotion-bundle/9745400694e76cd8.woff2 +0 -0
- package/dist/remotion-bundle/999ef957bed3bdca.woff2 +0 -0
- package/dist/remotion-bundle/99a3d67c8b0f43e3.woff2 +0 -0
- package/dist/remotion-bundle/a0586c3e03127283.woff2 +0 -0
- package/dist/remotion-bundle/a0eb654fdae46269.woff2 +0 -0
- package/dist/remotion-bundle/a20e35d3b08f7994.woff2 +0 -0
- package/dist/remotion-bundle/a2dcaced7c8c25ab.woff2 +0 -0
- package/dist/remotion-bundle/a79255a972a2681a.woff2 +0 -0
- package/dist/remotion-bundle/a804b352cb9fec1a.woff2 +0 -0
- package/dist/remotion-bundle/aae7117164e1eabc.woff2 +0 -0
- package/dist/remotion-bundle/affd121385d0442d.woff2 +0 -0
- package/dist/remotion-bundle/b19a6083987ee0d7.woff2 +0 -0
- package/dist/remotion-bundle/b1b2bd04d8637981.woff2 +0 -0
- package/dist/remotion-bundle/b2c07f341486be87.woff2 +0 -0
- package/dist/remotion-bundle/b33d8f82e575c4ce.woff2 +0 -0
- package/dist/remotion-bundle/b366c0bed35ef491.woff2 +0 -0
- package/dist/remotion-bundle/b41e857ec1b85642.woff2 +0 -0
- package/dist/remotion-bundle/b420bb34ccf23e7f.woff2 +0 -0
- package/dist/remotion-bundle/b4f7bf4efb0c0ccf.woff2 +0 -0
- package/dist/remotion-bundle/b60fe5eca03cff93.woff2 +0 -0
- package/dist/remotion-bundle/b6bd31a336e64bce.woff2 +0 -0
- package/dist/remotion-bundle/b6d2befba3dfefeb.woff2 +0 -0
- package/dist/remotion-bundle/b75f39ab06c43bf4.woff2 +0 -0
- package/dist/remotion-bundle/b77880e8c413d4fd.woff2 +0 -0
- package/dist/remotion-bundle/b7e38ec441e4a77a.woff2 +0 -0
- package/dist/remotion-bundle/b83baa383ff0bf2b.woff2 +0 -0
- package/dist/remotion-bundle/b9ad7b6c0a11450a.woff2 +0 -0
- package/dist/remotion-bundle/baf84486e8ae3aaf.woff2 +0 -0
- package/dist/remotion-bundle/bc047b1f6869cffa.woff2 +0 -0
- package/dist/remotion-bundle/bf4f3ac6e93f33aa.woff2 +0 -0
- package/dist/remotion-bundle/bf6835ffec5897a2.woff2 +0 -0
- package/dist/remotion-bundle/bf8885f581eb1724.woff2 +0 -0
- package/dist/remotion-bundle/bundle.js +83376 -0
- package/dist/remotion-bundle/bundle.js.map +1 -0
- package/dist/remotion-bundle/c03f046bccd789d0.woff2 +0 -0
- package/dist/remotion-bundle/c0bb1f8962b73bc3.woff2 +0 -0
- package/dist/remotion-bundle/c1003f9a7db6e1cf.woff2 +0 -0
- package/dist/remotion-bundle/c15d83fb1e199515.woff2 +0 -0
- package/dist/remotion-bundle/c28e7e5d310f73ef.woff2 +0 -0
- package/dist/remotion-bundle/c2b840274db78aea.woff2 +0 -0
- package/dist/remotion-bundle/c3000e3299d4e45f.woff2 +0 -0
- package/dist/remotion-bundle/c83ce886e5288510.woff2 +0 -0
- package/dist/remotion-bundle/c87a5a64d4ac0918.woff2 +0 -0
- package/dist/remotion-bundle/c8a7e0d049e965fa.woff2 +0 -0
- package/dist/remotion-bundle/c949a35d3a3b1faf.woff2 +0 -0
- package/dist/remotion-bundle/c9618c9b9ac2bc78.woff2 +0 -0
- package/dist/remotion-bundle/ca3add3b84152d5b.woff2 +0 -0
- package/dist/remotion-bundle/cad9dd036408d707.woff2 +0 -0
- package/dist/remotion-bundle/cbb24916619df439.woff2 +0 -0
- package/dist/remotion-bundle/cc054f0b5514e177.woff2 +0 -0
- package/dist/remotion-bundle/ccc248ed9312bc71.woff2 +0 -0
- package/dist/remotion-bundle/cd9d623aa07af925.woff2 +0 -0
- package/dist/remotion-bundle/ce2ba7a321bd1247.woff2 +0 -0
- package/dist/remotion-bundle/cf72455f79a29b14.woff2 +0 -0
- package/dist/remotion-bundle/d267cbfefab452ac.woff2 +0 -0
- package/dist/remotion-bundle/d435cff46a64955f.woff +0 -0
- package/dist/remotion-bundle/d494d07f67e363f6.woff2 +0 -0
- package/dist/remotion-bundle/d7aa0cc1fa47bf38.woff2 +0 -0
- package/dist/remotion-bundle/d7c5ca93d885160a.woff2 +0 -0
- package/dist/remotion-bundle/d855d3e252db74e2.woff2 +0 -0
- package/dist/remotion-bundle/d8f13d47f02f82c2.woff2 +0 -0
- package/dist/remotion-bundle/d9567cce2ee11019.woff2 +0 -0
- package/dist/remotion-bundle/db8d4456fc75dd86.woff +0 -0
- package/dist/remotion-bundle/dc274628378c47ee.woff2 +0 -0
- package/dist/remotion-bundle/dc3e06947bb69903.woff2 +0 -0
- package/dist/remotion-bundle/dd67040ac3b6d523.woff2 +0 -0
- package/dist/remotion-bundle/e0b04bd488f953f4.woff2 +0 -0
- package/dist/remotion-bundle/e2a572ff95089370.woff2 +0 -0
- package/dist/remotion-bundle/e2e18a86b1c2b0cc.woff2 +0 -0
- package/dist/remotion-bundle/e3a78ee2fc9c6931.woff2 +0 -0
- package/dist/remotion-bundle/e654c9d547605a9f.woff2 +0 -0
- package/dist/remotion-bundle/e67a3a64c129927c.woff2 +0 -0
- package/dist/remotion-bundle/e6be28b4203cd6ce.woff2 +0 -0
- package/dist/remotion-bundle/e841907ad9b0a191.woff +0 -0
- package/dist/remotion-bundle/e889d1541c69fffa.woff2 +0 -0
- package/dist/remotion-bundle/e88ef8c76373a9e2.woff2 +0 -0
- package/dist/remotion-bundle/e9c72f4bc37defef.woff2 +0 -0
- package/dist/remotion-bundle/e9e35f863403a255.woff2 +0 -0
- package/dist/remotion-bundle/eb23b37b009375da.woff2 +0 -0
- package/dist/remotion-bundle/ee1342b741625721.woff2 +0 -0
- package/dist/remotion-bundle/f07da88543a57ec9.woff2 +0 -0
- package/dist/remotion-bundle/f522982115306f8a.woff2 +0 -0
- package/dist/remotion-bundle/f8449bd864e6d8bc.woff2 +0 -0
- package/dist/remotion-bundle/f906dd5bd95ff9ab.woff2 +0 -0
- package/dist/remotion-bundle/f9e9e9413e3c38bb.woff2 +0 -0
- package/dist/remotion-bundle/fa5a5b16280994a8.woff2 +0 -0
- package/dist/remotion-bundle/favicon.ico +0 -0
- package/dist/remotion-bundle/fb19c0517725599b.woff2 +0 -0
- package/dist/remotion-bundle/fcaf24232f684b9b.woff2 +0 -0
- package/dist/remotion-bundle/fe09e084a3eea8cf.woff2 +0 -0
- package/dist/remotion-bundle/ff38d5317df7345a.woff2 +0 -0
- package/dist/remotion-bundle/ffe7ea1ea08f455a.woff2 +0 -0
- package/dist/remotion-bundle/index.html +49 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-6.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-6.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-5.mp3 +0 -0
- package/dist/remotion-bundle/source-map-helper.wasm +0 -0
- package/lib/cli.js +270 -0
- package/lib/commands/_registry.js +48 -0
- package/lib/commands/add.js +242 -0
- package/lib/commands/asr/azure-transcribe.js +336 -0
- package/lib/commands/asr/cloud-transcribe.js +384 -0
- package/lib/commands/asr/helpers.js +76 -0
- package/lib/commands/asr/index.js +236 -0
- package/lib/commands/asr/local-transcribe.js +125 -0
- package/lib/commands/asr-jobs.js +257 -0
- package/lib/commands/asr.js +11 -0
- package/lib/commands/auth-cmds.js +358 -0
- package/lib/commands/dub.js +542 -0
- package/lib/commands/explain.js +512 -0
- package/lib/commands/feedback.js +152 -0
- package/lib/commands/image.js +207 -0
- package/lib/commands/mcp-key.js +166 -0
- package/lib/commands/narrate.js +639 -0
- package/lib/commands/picstory-templates.js +276 -0
- package/lib/commands/picstory.js +547 -0
- package/lib/commands/podcast/dialogue.js +109 -0
- package/lib/commands/podcast/generate.js +127 -0
- package/lib/commands/podcast/index.js +561 -0
- package/lib/commands/podcast/synthesize.js +188 -0
- package/lib/commands/podcast.js +11 -0
- package/lib/commands/present.js +519 -0
- package/lib/commands/publish.js +415 -0
- package/lib/commands/skills.js +473 -0
- package/lib/commands/slice-render.js +282 -0
- package/lib/commands/slice-stage.js +264 -0
- package/lib/commands/slice.js +346 -0
- package/lib/commands/slides/constants.js +108 -0
- package/lib/commands/slides/html-renderer.js +338 -0
- package/lib/commands/slides/index.js +345 -0
- package/lib/commands/slides.js +11 -0
- package/lib/commands/story.js +302 -0
- package/lib/commands/summarize.js +532 -0
- package/lib/commands/synthesize.js +261 -0
- package/lib/commands/translate.js +593 -0
- package/lib/commands/upgrade.js +249 -0
- package/lib/commands/video-translate.js +577 -0
- package/lib/commands/voices.js +292 -0
- package/lib/core/agent-env.js +104 -0
- package/lib/core/args.js +107 -0
- package/lib/core/asr-client.js +448 -0
- package/lib/core/asr-jobs-client.js +126 -0
- package/lib/core/asr-jobs-store.js +105 -0
- package/lib/core/asr-r2-upload.js +181 -0
- package/lib/core/asr-upload.js +132 -0
- package/lib/core/audio-extract.js +150 -0
- package/lib/core/audio.js +219 -0
- package/lib/core/auth.js +880 -0
- package/lib/core/config.js +197 -0
- package/lib/core/feedback.js +64 -0
- package/lib/core/ffmpeg.js +476 -0
- package/lib/core/http.js +188 -0
- package/lib/core/image-client.js +55 -0
- package/lib/core/intent-params.js +11 -0
- package/lib/core/llm-client.js +76 -0
- package/lib/core/logger.js +208 -0
- package/lib/core/mic-recorder.js +182 -0
- package/lib/core/pause-markers.js +94 -0
- package/lib/core/podcast-pacing.js +118 -0
- package/lib/core/spinner.js +33 -0
- package/lib/core/srt.js +394 -0
- package/lib/core/telemetry.js +100 -0
- package/lib/core/timeline.js +92 -0
- package/lib/core/tts-synthesizer.js +70 -0
- package/lib/core/update-check.js +185 -0
- package/lib/core/url-download.js +148 -0
- package/lib/core/whisper-local.js +279 -0
- package/lib/internal/deck-validator.js +488 -0
- package/lib/internal/slice-themes.json +370 -0
- package/lib/stage-core/cloud-render.js +170 -0
- package/lib/stage-core/deck-format.js +133 -0
- package/lib/stage-core/edit-prompt.js +104 -0
- package/lib/stage-core/event-bus.js +31 -0
- package/lib/stage-core/port.js +46 -0
- package/lib/stage-core/server.js +352 -0
- package/lib/stage-core/snapshot-store.js +198 -0
- package/lib/stage-core/watcher.js +106 -0
- package/lib/stage-ui/slice/template.js +1672 -0
- package/package.json +9 -4
- package/skills/.claude-plugin/marketplace.json +22 -0
- package/skills/.claude-plugin/plugin.json +25 -0
- package/skills/LICENSE +21 -0
- package/skills/README.md +120 -0
- package/skills/hub/SKILL.md +317 -0
- package/skills/podcast/SKILL.md +146 -0
- package/skills/slice/SKILL.md +205 -0
- package/skills/slice/agents/openai.yaml +4 -0
- package/skills/slice/references/deck-schema.md +183 -0
- package/skills/slice/references/example-decks.md +108 -0
- package/skills/slice/references/themes.md +172 -0
- package/skills/transcribe/SKILL.md +473 -0
- package/skills/video/SKILL.md +261 -0
- package/skills/voxflow-slice/SKILL.md +271 -0
- package/skills/voxflow-slice/examples/article.md +13 -0
- package/skills/voxflow-slice/examples/expected-deck.json +39 -0
- package/skills/voxflow-slice/examples/validate.mjs +46 -0
package/lib/core/auth.js
ADDED
|
@@ -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
|
+
};
|