token-goat 2.9.29 → 2.9.31

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 (197) hide show
  1. package/README.md +53 -42
  2. package/SECURITY.md +37 -11
  3. package/THIRD_PARTY_NOTICES.md +41 -4
  4. package/dist/native/linux-arm64/tg-hook +0 -0
  5. package/dist/native/linux-x64/tg-hook +0 -0
  6. package/dist/native/win32-arm64/tg-hook.exe +0 -0
  7. package/dist/native/win32-x64/tg-hook.exe +0 -0
  8. package/dist/ort-wasm-simd-threaded.mjs +59 -0
  9. package/dist/{token-goat-chunk-SE6E4BKJ.mjs → token-goat-chunk-25TRAWET.mjs} +23 -6
  10. package/dist/token-goat-chunk-2KXO6V2B.mjs +203 -0
  11. package/dist/{token-goat-chunk-UXSXQQIY.mjs → token-goat-chunk-2M7E4PF7.mjs} +43 -14
  12. package/dist/token-goat-chunk-2UPZGKKY.mjs +188 -0
  13. package/dist/token-goat-chunk-2WLPWKHV.mjs +135 -0
  14. package/dist/token-goat-chunk-2XBMMDBD.mjs +126 -0
  15. package/dist/token-goat-chunk-32ZOY4N5.mjs +23 -0
  16. package/dist/token-goat-chunk-3EZRNKTK.mjs +188 -0
  17. package/dist/token-goat-chunk-4ADZHI7J.mjs +63 -0
  18. package/dist/token-goat-chunk-4GBVPH3D.mjs +242 -0
  19. package/dist/{token-goat-chunk-2WOUYWLM.mjs → token-goat-chunk-4GILLLWY.mjs} +34 -7
  20. package/dist/token-goat-chunk-4NAPX7BA.mjs +90 -0
  21. package/dist/{token-goat-chunk-NMBWLRHW.mjs → token-goat-chunk-4SC7UJYV.mjs} +9981 -11563
  22. package/dist/token-goat-chunk-4SMRO4TO.mjs +366 -0
  23. package/dist/{token-goat-chunk-O6I2XU4P.mjs → token-goat-chunk-4ZBFWLXF.mjs} +310 -441
  24. package/dist/token-goat-chunk-57BIAXJL.mjs +286 -0
  25. package/dist/token-goat-chunk-5AC66HNP.mjs +115 -0
  26. package/dist/token-goat-chunk-5IMLKSOP.mjs +145 -0
  27. package/dist/token-goat-chunk-5RG5SBL6.mjs +221 -0
  28. package/dist/{token-goat-chunk-RRNZMM3A.mjs → token-goat-chunk-5Y6ZYL6U.mjs} +21 -2
  29. package/dist/token-goat-chunk-646E6U44.mjs +398 -0
  30. package/dist/token-goat-chunk-65N4RHP4.mjs +33 -0
  31. package/dist/{token-goat-chunk-6AK46ZT7.mjs → token-goat-chunk-6DG6HWP3.mjs} +13 -3
  32. package/dist/token-goat-chunk-6ENQSNRZ.mjs +573 -0
  33. package/dist/token-goat-chunk-6ZY4GNF2.mjs +91 -0
  34. package/dist/{token-goat-chunk-E5TK5DNO.mjs → token-goat-chunk-76FGDLRO.mjs} +574 -340
  35. package/dist/{token-goat-chunk-TGOXNAHW.mjs → token-goat-chunk-7EZTZOMC.mjs} +780 -767
  36. package/dist/token-goat-chunk-7MAOA6X4.mjs +3647 -0
  37. package/dist/token-goat-chunk-7R2ZVVAX.mjs +218 -0
  38. package/dist/{token-goat-chunk-TCQANXYC.mjs → token-goat-chunk-AM5V6GSX.mjs} +18 -2
  39. package/dist/token-goat-chunk-AUNGYYQX.mjs +184 -0
  40. package/dist/token-goat-chunk-AUQYRMMH.mjs +2985 -0
  41. package/dist/token-goat-chunk-B3PEKBYG.mjs +128 -0
  42. package/dist/{token-goat-chunk-KOPIR3KZ.mjs → token-goat-chunk-BA4HI6BW.mjs} +1910 -165
  43. package/dist/{token-goat-chunk-52NSDWYY.mjs → token-goat-chunk-BFCWY73P.mjs} +58 -20
  44. package/dist/token-goat-chunk-BPGCUMR7.mjs +30 -0
  45. package/dist/token-goat-chunk-BWGFUZZE.mjs +25 -0
  46. package/dist/token-goat-chunk-C75NA7PL.mjs +322 -0
  47. package/dist/token-goat-chunk-CBIQ7WTD.mjs +87 -0
  48. package/dist/token-goat-chunk-CETM74KC.mjs +307 -0
  49. package/dist/token-goat-chunk-CIMQ2BB3.mjs +233 -0
  50. package/dist/{token-goat-chunk-DNSPXQXQ.mjs → token-goat-chunk-CJLS3CDL.mjs} +70 -18
  51. package/dist/{token-goat-chunk-MELOKFDJ.mjs → token-goat-chunk-CO3UYX6G.mjs} +7 -1
  52. package/dist/token-goat-chunk-COPNXRJM.mjs +154 -0
  53. package/dist/token-goat-chunk-CSJ5L3NL.mjs +479 -0
  54. package/dist/token-goat-chunk-CVCEVUGK.mjs +315 -0
  55. package/dist/token-goat-chunk-CXHOFN7R.mjs +371 -0
  56. package/dist/token-goat-chunk-D735C4QM.mjs +603 -0
  57. package/dist/{token-goat-chunk-E4ZOJIZK.mjs → token-goat-chunk-DGS2LVNA.mjs} +70 -160
  58. package/dist/{token-goat-chunk-L7H3U27L.mjs → token-goat-chunk-DOWRSMWO.mjs} +1 -1
  59. package/dist/token-goat-chunk-E45INMY3.mjs +71 -0
  60. package/dist/token-goat-chunk-EC5F37F4.mjs +244 -0
  61. package/dist/{token-goat-chunk-XTQAOTSO.mjs → token-goat-chunk-EDBDRVH5.mjs} +4 -1
  62. package/dist/token-goat-chunk-EELTBKDM.mjs +613 -0
  63. package/dist/token-goat-chunk-EVMZJ27Z.mjs +147 -0
  64. package/dist/token-goat-chunk-EXKGNHEG.mjs +2096 -0
  65. package/dist/{token-goat-chunk-YOOVFPTP.mjs → token-goat-chunk-F4HYL64R.mjs} +12 -2
  66. package/dist/token-goat-chunk-F6PAUIEK.mjs +624 -0
  67. package/dist/token-goat-chunk-FDSREWLX.mjs +692 -0
  68. package/dist/{token-goat-chunk-NMTKNYGF.mjs → token-goat-chunk-FNFBKLRD.mjs} +74 -9
  69. package/dist/token-goat-chunk-G5PJ7AGM.mjs +36 -0
  70. package/dist/token-goat-chunk-GGGG7RWB.mjs +236 -0
  71. package/dist/{token-goat-chunk-TDUT2CL6.mjs → token-goat-chunk-GL3ETKSC.mjs} +276 -19
  72. package/dist/{token-goat-chunk-7CPMDFGB.mjs → token-goat-chunk-GVB2MUST.mjs} +265 -1567
  73. package/dist/token-goat-chunk-GWFLAUMQ.mjs +307 -0
  74. package/dist/token-goat-chunk-HAVAOB3S.mjs +87 -0
  75. package/dist/token-goat-chunk-HLPW63A7.mjs +48 -0
  76. package/dist/token-goat-chunk-HU4TORC5.mjs +1377 -0
  77. package/dist/token-goat-chunk-I7VKZZAW.mjs +35 -0
  78. package/dist/token-goat-chunk-IA6XSPE3.mjs +218 -0
  79. package/dist/token-goat-chunk-IAGXMHIK.mjs +863 -0
  80. package/dist/{token-goat-chunk-435UQTKD.mjs → token-goat-chunk-IK2DRMFX.mjs} +129 -74
  81. package/dist/{token-goat-chunk-ZER6EAXZ.mjs → token-goat-chunk-INNPTRUX.mjs} +2954 -2783
  82. package/dist/token-goat-chunk-JJ5BX4F4.mjs +129 -0
  83. package/dist/token-goat-chunk-JVFSHVFJ.mjs +129 -0
  84. package/dist/token-goat-chunk-KGEFFSHD.mjs +50 -0
  85. package/dist/token-goat-chunk-KKUFXAPD.mjs +25 -0
  86. package/dist/token-goat-chunk-KQBLRPBN.mjs +433 -0
  87. package/dist/token-goat-chunk-LCNQ2CNN.mjs +497 -0
  88. package/dist/token-goat-chunk-M3LFJDPU.mjs +58 -0
  89. package/dist/token-goat-chunk-M52KPT3C.mjs +87 -0
  90. package/dist/token-goat-chunk-MBEG42H7.mjs +150 -0
  91. package/dist/{token-goat-chunk-EVC4TOLE.mjs → token-goat-chunk-METECDOH.mjs} +3 -12
  92. package/dist/{token-goat-chunk-2AWQ2R5X.mjs → token-goat-chunk-MHX7XCC3.mjs} +3 -2
  93. package/dist/token-goat-chunk-MNWKPRAL.mjs +13 -0
  94. package/dist/token-goat-chunk-N5LSOVSU.mjs +137 -0
  95. package/dist/{token-goat-chunk-DPNLUJWB.mjs → token-goat-chunk-N5MMR2IW.mjs} +30 -8
  96. package/dist/token-goat-chunk-N7B3JQ2C.mjs +160 -0
  97. package/dist/{token-goat-chunk-VMFWNHGR.mjs → token-goat-chunk-NISNMD7T.mjs} +2545 -2953
  98. package/dist/token-goat-chunk-NMD42SFQ.mjs +434 -0
  99. package/dist/token-goat-chunk-NOSS5VWY.mjs +174 -0
  100. package/dist/token-goat-chunk-NSNSPPBG.mjs +69 -0
  101. package/dist/token-goat-chunk-OBOFIHKN.mjs +38 -0
  102. package/dist/token-goat-chunk-OBRFJ5O5.mjs +76 -0
  103. package/dist/token-goat-chunk-ODBT2USM.mjs +800 -0
  104. package/dist/token-goat-chunk-OS5EM5PP.mjs +107 -0
  105. package/dist/token-goat-chunk-OUKDTVNU.mjs +1178 -0
  106. package/dist/token-goat-chunk-OXFMP75N.mjs +566 -0
  107. package/dist/{token-goat-chunk-ASLWOQ3I.mjs → token-goat-chunk-OYIYKDDT.mjs} +2 -1
  108. package/dist/{token-goat-chunk-67BNIKKO.mjs → token-goat-chunk-P6HQSWQ3.mjs} +55 -47
  109. package/dist/token-goat-chunk-P6VGNTQT.mjs +71 -0
  110. package/dist/token-goat-chunk-PFWIMH2E.mjs +261 -0
  111. package/dist/token-goat-chunk-PJZXIS3G.mjs +39 -0
  112. package/dist/token-goat-chunk-PUO5INXP.mjs +606 -0
  113. package/dist/token-goat-chunk-PX5PQWTY.mjs +462 -0
  114. package/dist/token-goat-chunk-Q5QDBC6O.mjs +111 -0
  115. package/dist/token-goat-chunk-QFNQXSDT.mjs +40 -0
  116. package/dist/token-goat-chunk-QH7YVYOY.mjs +3957 -0
  117. package/dist/token-goat-chunk-QLMUHEPB.mjs +43 -0
  118. package/dist/{token-goat-chunk-PYSUADX4.mjs → token-goat-chunk-QQOJIHL3.mjs} +50 -14
  119. package/dist/token-goat-chunk-QQTWJY2O.mjs +85 -0
  120. package/dist/token-goat-chunk-QYHO5CPH.mjs +93 -0
  121. package/dist/token-goat-chunk-RELAVWAD.mjs +27 -0
  122. package/dist/token-goat-chunk-RSLPOBKQ.mjs +339 -0
  123. package/dist/token-goat-chunk-SAMFDIS2.mjs +712 -0
  124. package/dist/token-goat-chunk-SDELXLDM.mjs +48 -0
  125. package/dist/{token-goat-chunk-PGTH2U4C.mjs → token-goat-chunk-SOAFZNNZ.mjs} +164 -40
  126. package/dist/{token-goat-chunk-G23DWHV4.mjs → token-goat-chunk-SRTHT7KV.mjs} +33 -15
  127. package/dist/token-goat-chunk-T5P5ACNQ.mjs +212 -0
  128. package/dist/token-goat-chunk-T5YGG4V3.mjs +45 -0
  129. package/dist/token-goat-chunk-TAUZ4OFS.mjs +110 -0
  130. package/dist/token-goat-chunk-TM6MC754.mjs +40 -0
  131. package/dist/token-goat-chunk-TWUKBOMP.mjs +72 -0
  132. package/dist/token-goat-chunk-U2KSB4SG.mjs +56 -0
  133. package/dist/token-goat-chunk-U4WHMG6Y.mjs +898 -0
  134. package/dist/token-goat-chunk-UA5V6P3P.mjs +122 -0
  135. package/dist/token-goat-chunk-UGJHGR5A.mjs +52 -0
  136. package/dist/{token-goat-chunk-RQGGMPCR.mjs → token-goat-chunk-UOUDVA5U.mjs} +76 -16
  137. package/dist/{token-goat-chunk-HNFZZHFW.mjs → token-goat-chunk-URRJ33GR.mjs} +22 -4
  138. package/dist/token-goat-chunk-UZOROKIS.mjs +165 -0
  139. package/dist/token-goat-chunk-VIHGGGEI.mjs +4234 -0
  140. package/dist/token-goat-chunk-VTJLAA7P.mjs +21 -0
  141. package/dist/token-goat-chunk-VWDXLT4F.mjs +678 -0
  142. package/dist/token-goat-chunk-VWZA2H4P.mjs +368 -0
  143. package/dist/{token-goat-chunk-QO3LXVVR.mjs → token-goat-chunk-VXEXMJDL.mjs} +2964 -3153
  144. package/dist/token-goat-chunk-WBA5SKT4.mjs +547 -0
  145. package/dist/{token-goat-chunk-ONXH6NZ5.mjs → token-goat-chunk-WDH44VAU.mjs} +19 -22
  146. package/dist/token-goat-chunk-X2SUP3EI.mjs +107 -0
  147. package/dist/token-goat-chunk-X3BELZSJ.mjs +125 -0
  148. package/dist/{token-goat-chunk-RLP6J7WR.mjs → token-goat-chunk-XLRBNZCC.mjs} +40 -21
  149. package/dist/{token-goat-chunk-S4VSXURL.mjs → token-goat-chunk-XRKUX6CM.mjs} +4 -0
  150. package/dist/token-goat-chunk-XX6O25RN.mjs +82 -0
  151. package/dist/token-goat-chunk-XZSUHBVS.mjs +65 -0
  152. package/dist/token-goat-chunk-Y2ZTQOCL.mjs +35 -0
  153. package/dist/{token-goat-chunk-UA5BKWWQ.mjs → token-goat-chunk-YB4VMGK2.mjs} +15 -4
  154. package/dist/token-goat-chunk-YBIIY3HR.mjs +51 -0
  155. package/dist/{token-goat-chunk-PKRWMHVP.mjs → token-goat-chunk-YHVDKQBR.mjs} +389 -221
  156. package/dist/token-goat-chunk-YJB5XLWI.mjs +26 -0
  157. package/dist/{token-goat-chunk-PQ53BSCF.mjs → token-goat-chunk-YKB6AQG3.mjs} +53 -31
  158. package/dist/token-goat-chunk-YMTG2YA5.mjs +35 -0
  159. package/dist/token-goat-chunk-YPBZYQRV.mjs +40 -0
  160. package/dist/{token-goat-chunk-HUYUHODZ.mjs → token-goat-chunk-YY6AVUFL.mjs} +13246 -12247
  161. package/dist/token-goat-chunk-Z7N5OMMP.mjs +98 -0
  162. package/dist/{token-goat-chunk-BGHSSJUA.mjs → token-goat-chunk-ZFOBKUWH.mjs} +1 -1
  163. package/dist/token-goat-chunk-ZMBL6UVV.mjs +110 -0
  164. package/dist/token-goat-chunk-ZNMNUPAZ.mjs +938 -0
  165. package/dist/token-goat-hook-client.cjs +87 -23
  166. package/dist/token-goat-hook-client.mjs +3 -3
  167. package/dist/token-goat-hook.mjs +76 -31
  168. package/dist/token-goat.core.mjs +6 -6
  169. package/docs/C4_RUNTIME_ARCHITECTURE.md +2 -2
  170. package/docs/cli.md +113 -60
  171. package/docs/install.md +63 -20
  172. package/docs/security.md +10 -8
  173. package/package.json +18 -7
  174. package/scripts/verify-native-dist.mjs +386 -0
  175. package/dist/token-goat-chunk-2NNUA2LR.mjs +0 -143
  176. package/dist/token-goat-chunk-44BM7DDQ.mjs +0 -294
  177. package/dist/token-goat-chunk-BGYOZ6DO.mjs +0 -24
  178. package/dist/token-goat-chunk-C5YPNZKQ.mjs +0 -318
  179. package/dist/token-goat-chunk-CIQ2FTY3.mjs +0 -1990
  180. package/dist/token-goat-chunk-D4ETHJD6.mjs +0 -246
  181. package/dist/token-goat-chunk-DNRPXHYH.mjs +0 -1062
  182. package/dist/token-goat-chunk-EUSDXRXU.mjs +0 -1429
  183. package/dist/token-goat-chunk-FTP4U4QA.mjs +0 -64
  184. package/dist/token-goat-chunk-G7IWXCFF.mjs +0 -456
  185. package/dist/token-goat-chunk-GLXRTTLE.mjs +0 -36
  186. package/dist/token-goat-chunk-H5J3C56Q.mjs +0 -1511
  187. package/dist/token-goat-chunk-IVWI2OCK.mjs +0 -368
  188. package/dist/token-goat-chunk-JTKCQMQA.mjs +0 -418
  189. package/dist/token-goat-chunk-LZNBQTDK.mjs +0 -26
  190. package/dist/token-goat-chunk-ORNFMIER.mjs +0 -295
  191. package/dist/token-goat-chunk-RTMVLHWX.mjs +0 -7113
  192. package/dist/token-goat-chunk-VQ6F6T7X.mjs +0 -36
  193. package/dist/token-goat-chunk-VZQEWRVH.mjs +0 -312
  194. package/dist/token-goat-chunk-XYWPOSCN.mjs +0 -807
  195. package/dist/token-goat-chunk-YAGLESS3.mjs +0 -23
  196. package/dist/token-goat-chunk-YKSRAWUU.mjs +0 -33
  197. package/dist/token-goat-chunk-Z6KBMOBI.mjs +0 -53
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ /** The publish gate for the native hook client: refuses a dist/native that is not exactly the release binaries (all four, or the two Linux ones under `--without-windows`), each a statically linked executable for its target and byte-identical to the hash its producing job recorded, with both Windows files carrying an Authenticode signature. Run by the publish job of .github/workflows/publish.yml, which holds the registry token, so it imports nothing but Node builtins: no dependency code runs beside the credential. The signature check binds the signature to these bytes without trusting a certificate chain: the PE certificate table must end the file and hold a WIN_CERTIFICATE wrapping a PKCS#7 SignedData with exactly one signer, every constructed DER element in it holding whole elements and nothing else, and the Authenticode image digest that SignedData signs must equal the digest of the file itself, so a signature lifted from another binary is refused; the sign job has already verified the signature cryptographically, and the hash match is what ties the file here to the file it verified. Usage: `node scripts/verify-native-dist.mjs [--without-windows] <dist/native> <SHA256SUMS>...`, where every manifest is in `sha256sum` format with paths relative to the directory; `--without-windows` is the release with no Windows signing configuration, which ships the two Linux binaries and no Windows one, so Windows installs keep the Node hook. `node scripts/verify-native-dist.mjs --pack <dist/native>` is the prepack check, which npm runs on `npm pack` and on `npm publish` from a directory, so a tarball packed by hand cannot carry what a direct publish would refuse: no manifests, so it checks shape only, and a missing directory passes. Exits 0 when everything holds, otherwise 1 with one line per problem. */
3
+ import { createHash } from 'node:crypto'
4
+ import { readdirSync, readFileSync, realpathSync } from 'node:fs'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ /** Every target a release ships: the rustc triple CI builds, the directory the installer (src/native_hook.ts packagedNativeBinary) looks in, the file name there, and the machine the executable header must declare. Linux is built against musl so one static binary serves glibc and musl distributions alike. */
9
+ export const NATIVE_TARGETS = Object.freeze([
10
+ Object.freeze({ triple: 'x86_64-pc-windows-msvc', platformArch: 'win32-x64', exe: 'tg-hook.exe', format: 'pe', machine: 0x8664 }),
11
+ Object.freeze({ triple: 'aarch64-pc-windows-msvc', platformArch: 'win32-arm64', exe: 'tg-hook.exe', format: 'pe', machine: 0xaa64 }),
12
+ Object.freeze({ triple: 'x86_64-unknown-linux-musl', platformArch: 'linux-x64', exe: 'tg-hook', format: 'elf', machine: 62 }),
13
+ Object.freeze({ triple: 'aarch64-unknown-linux-musl', platformArch: 'linux-arm64', exe: 'tg-hook', format: 'elf', machine: 183 }),
14
+ ])
15
+
16
+ // Microsoft PE/COFF specification (https://learn.microsoft.com/en-us/windows/win32/debug/pe-format): the Certificate Table is data directory 4, and unlike every other directory its address is a file offset, not an RVA. Each entry is a WIN_CERTIFICATE; Authenticode uses revision 2.0 and type PKCS_SIGNED_DATA.
17
+ const CERTIFICATE_TABLE = 4
18
+ const WIN_CERT_REVISION_2_0 = 0x0200
19
+ const WIN_CERT_TYPE_PKCS_SIGNED_DATA = 0x0002
20
+
21
+ /** Why `buf` is not a signed PE for `machine`, or undefined when its certificate table ends the file and holds a PKCS#7 WIN_CERTIFICATE whose signed Authenticode image digest is the digest of `buf` itself. */
22
+ export function peSignatureProblem(buf, machine) {
23
+ if (buf.length < 0x40 || buf.readUInt16LE(0) !== 0x5a4d) return 'is not a PE file (no MZ header)'
24
+ const pe = buf.readUInt32LE(0x3c)
25
+ if (pe + 24 > buf.length || buf.readUInt32LE(pe) !== 0x00004550) return 'is not a PE file (no PE signature where the MZ header points)'
26
+ const declared = buf.readUInt16LE(pe + 4)
27
+ if (declared !== machine) return `declares machine 0x${declared.toString(16)}, expected 0x${machine.toString(16)}`
28
+ const optionalSize = buf.readUInt16LE(pe + 20)
29
+ const optional = pe + 24
30
+ if (optionalSize < 2 || optional + optionalSize > buf.length) return 'has a truncated optional header'
31
+ const magic = buf.readUInt16LE(optional)
32
+ // PE32+ puts NumberOfRvaAndSizes at 108 and the directories at 112; PE32 at 92 and 96.
33
+ const [countAt, dirsAt] = magic === 0x20b ? [108, 112] : magic === 0x10b ? [92, 96] : [undefined, undefined]
34
+ if (countAt === undefined) return `has an unknown optional header magic 0x${magic.toString(16)}`
35
+ const entry = dirsAt + CERTIFICATE_TABLE * 8
36
+ if (optionalSize < entry + 8 || buf.readUInt32LE(optional + countAt) <= CERTIFICATE_TABLE) return 'has no certificate table directory, so it carries no Authenticode signature'
37
+ const offset = buf.readUInt32LE(optional + entry)
38
+ const size = buf.readUInt32LE(optional + entry + 4)
39
+ if (offset === 0 && size === 0) return 'carries no Authenticode signature (its certificate table is empty)'
40
+ if (size < 8 || offset + size > buf.length) return `has a certificate table (offset ${offset}, size ${size}) that does not fit in the file`
41
+ const length = buf.readUInt32LE(offset)
42
+ if (length < 9 || length > size) return `has a WIN_CERTIFICATE whose length ${length} does not fit its table of ${size} bytes`
43
+ // The table's size sits in the directory entry, which the image digest skips, so a table grown past its one WIN_CERTIFICATE and its quadword padding would carry appended bytes the signature does not cover.
44
+ if (size !== length && size !== ((length + 7) & ~7)) return `has ${size - length} bytes in its certificate table beyond its WIN_CERTIFICATE, which the signature does not cover`
45
+ if (buf.readUInt16LE(offset + 4) !== WIN_CERT_REVISION_2_0) return 'has a WIN_CERTIFICATE that is not revision 2.0'
46
+ if (buf.readUInt16LE(offset + 6) !== WIN_CERT_TYPE_PKCS_SIGNED_DATA) return 'has a WIN_CERTIFICATE that is not PKCS#7 signed data'
47
+ // RFC 5652 ContentInfo: SEQUENCE { contentType OID signedData, [0] EXPLICIT SignedData SEQUENCE }; each element must fit inside the one that holds it.
48
+ const contentInfo = derElement(buf, offset + 8, offset + length)
49
+ if (contentInfo?.tag !== 0x30) return 'has a WIN_CERTIFICATE whose content is not a DER SEQUENCE that fits it'
50
+ const contentType = derElement(buf, contentInfo.start, contentInfo.end)
51
+ if (contentType?.tag !== 0x06 || !buf.subarray(contentType.start, contentType.end).equals(OID_PKCS7_SIGNED_DATA)) return 'has a WIN_CERTIFICATE whose ContentInfo is not PKCS#7 signedData'
52
+ const explicit = derElement(buf, contentType.end, contentInfo.end)
53
+ const signedData = explicit?.tag === 0xa0 ? derElement(buf, explicit.start, explicit.end) : undefined
54
+ if (signedData?.tag !== 0x30) return 'has a WIN_CERTIFICATE whose signedData content is missing or truncated'
55
+ // The signature covers the image, not the WIN_CERTIFICATE around it, so bytes after the DER (the CVE-2013-3900 shape) are refused unless they are the at most 7 zero bytes signers pad to a quadword with.
56
+ if (explicit.end !== contentInfo.end || signedData.end !== explicit.end) return 'has a WIN_CERTIFICATE whose ContentInfo holds bytes after its signedData, which the signature does not cover'
57
+ if (!derTiles(buf, contentInfo)) return 'has a WIN_CERTIFICATE whose DER is malformed: a constructed element holds bytes that are not whole elements, or nests past 64 levels'
58
+ const tail = offset + size - contentInfo.end
59
+ if (tail > 7) return `has ${tail} bytes in its certificate table after its signature, which the signature does not cover`
60
+ if (buf.subarray(contentInfo.end, offset + size).some((b) => b !== 0)) return 'pads its signature with non-zero bytes, which the signature does not cover'
61
+ if (offset + size !== buf.length) return 'has data after its certificate table, which the signature does not cover'
62
+ const signed = signedImageDigest(buf, signedData)
63
+ if (typeof signed === 'string') return `has a WIN_CERTIFICATE whose signedData ${signed}`
64
+ const actual = authenticodeDigest(buf, signed.algorithm, optional + 64, optional + entry, offset)
65
+ if (!actual.equals(signed.digest)) return 'has an Authenticode signature whose image digest does not match the file, so the signature belongs to other bytes'
66
+ return undefined
67
+ }
68
+
69
+ /** The image digest a SignedData SEQUENCE signs, per RFC 5652 and the Authenticode PE specification: SignedData { version, digestAlgorithms SET, encapContentInfo { SPC_INDIRECT_DATA OID, [0] SpcIndirectDataContent { data, DigestInfo { AlgorithmIdentifier { OID, NULL? }, OCTET STRING } } }, [0] certificates?, [1] crls?, signerInfos SET { SignerInfo } }, each holding exactly those elements in that order, so no element carries one the shape leaves out. A string says what does not match. Authenticode allows one signer, a SignerInfo { version, sid, digestAlgorithm, [0] signedAttrs, signatureAlgorithm, signature, [1] unsignedAttrs? }; a second signature nests inside the first's unsigned attributes. */
70
+ function signedImageDigest(buf, signedData) {
71
+ const parts = derChildren(buf, signedData) ?? []
72
+ const tags = derTags(parts)
73
+ const head = /^02 31 30( a0)?( a1)? 31/.exec(tags)
74
+ if (head === null) return 'is not version, digest algorithms, content, optional certificates and CRLs, then signers'
75
+ if (head[0].length !== tags.length) return 'holds bytes after its signers, which the signature does not cover'
76
+ if (!/^0[1345]$/.test(buf.subarray(parts[0].start, parts[0].end).toString('hex'))) return 'has a version that is not one of the four RFC 5652 gives SignedData'
77
+ const encap = derShaped(buf, parts[2], /^06 a0$/)
78
+ if (encap === undefined) return 'has encapsulated content that is not exactly a content type and one [0] content'
79
+ if (!buf.subarray(encap[0].start, encap[0].end).equals(OID_SPC_INDIRECT_DATA)) return 'does not sign Authenticode indirect data'
80
+ const [indirect] = derShaped(buf, encap[1], /^30$/) ?? []
81
+ const [, digestInfo] = (indirect && derShaped(buf, indirect, /^30 30$/)) ?? []
82
+ const [algorithmId, digest] = (digestInfo && derShaped(buf, digestInfo, /^30 04$/)) ?? []
83
+ const [algorithmOid] = (algorithmId && derShaped(buf, algorithmId, /^06( 05)?$/)) ?? []
84
+ if (algorithmOid === undefined) return 'carries no image digest as one SpcIndirectDataContent of data and a DigestInfo'
85
+ const algorithm = DIGEST_ALGORITHMS.get(buf.subarray(algorithmOid.start, algorithmOid.end).toString('hex'))
86
+ if (algorithm === undefined) return 'uses an image digest algorithm this check does not know'
87
+ const signers = derChildren(buf, parts[parts.length - 1]) ?? []
88
+ if (signers.length === 0) return 'has no signer'
89
+ if (signers.length > 1) return `has ${signers.length} signers where Authenticode allows one`
90
+ const signer = signers[0].tag === 0x30 ? derShaped(buf, signers[0], /^02 (30|80) 30 a0 30 04( a1)?$/) : undefined
91
+ if (signer === undefined) return 'has a signer that is not version, signer identifier, digest algorithm, signed attributes, signature algorithm, signature and optional unsigned attributes'
92
+ if (buf.subarray(signer[0].start, signer[0].end).toString('hex') !== (signer[1].tag === 0x30 ? '01' : '03')) return 'has a signer whose version is not 1 for an issuer and serial number or 3 for a key identifier'
93
+ return { algorithm, digest: buf.subarray(digest.start, digest.end) }
94
+ }
95
+
96
+ /** The Authenticode image hash of `buf`: every byte before the certificate table except the CheckSum field and the Certificate Table directory entry. This is the specification's section-by-section hash for an image whose headers and sections are contiguous, which is how linkers lay them out and how signing tools hash them. */
97
+ function authenticodeDigest(buf, algorithm, checksumAt, entryAt, tableAt) {
98
+ return createHash(algorithm)
99
+ .update(buf.subarray(0, checksumAt))
100
+ .update(buf.subarray(checksumAt + 4, entryAt))
101
+ .update(buf.subarray(entryAt + 8, tableAt))
102
+ .digest()
103
+ }
104
+
105
+ // 1.2.840.113549.1.7.2, the PKCS#7 signedData content type, as DER OID content bytes.
106
+ const OID_PKCS7_SIGNED_DATA = Buffer.from([0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02])
107
+ // 1.3.6.1.4.1.311.2.1.4, SPC_INDIRECT_DATA_OBJID, the content type an Authenticode signature signs.
108
+ const OID_SPC_INDIRECT_DATA = Buffer.from([0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x04])
109
+ // Digest algorithm OIDs (RFC 3279 for SHA-1, RFC 5754 for the SHA-2 family) as DER content hex, to Node hash names.
110
+ const DIGEST_ALGORITHMS = new Map([
111
+ ['2b0e03021a', 'sha1'],
112
+ ['608648016503040201', 'sha256'],
113
+ ['608648016503040202', 'sha384'],
114
+ ['608648016503040203', 'sha512'],
115
+ ])
116
+
117
+ /** The DER element (X.690) whose identifier octets start at `at`: its first identifier octet as `tag`, and where its content starts and ends. Undefined when the header or the content runs past `limit`, the length is the indefinite form DER forbids, or a high tag number runs past four octets. */
118
+ function derElement(buf, at, limit) {
119
+ let header = at + 1
120
+ // Tag number bits all set mean the number follows in base-128 octets, the last with bit 8 clear.
121
+ if (at < limit && (buf[at] & 0x1f) === 0x1f) {
122
+ while (header < limit && header - at <= 4 && buf[header] & 0x80) header++
123
+ if (++header - at > 5) return undefined
124
+ }
125
+ if (header + 1 > limit) return undefined
126
+ const first = buf[header]
127
+ let start = header + 1
128
+ let len = first
129
+ if (first >= 0x80) {
130
+ const octets = first & 0x7f
131
+ if (octets === 0 || octets > 4 || start + octets > limit) return undefined
132
+ len = buf.readUIntBE(start, octets)
133
+ start += octets
134
+ }
135
+ return start + len > limit ? undefined : { tag: buf[at], start, end: start + len }
136
+ }
137
+
138
+ /** The elements `parent`'s content holds, or undefined when they do not tile it exactly: X.690 makes a constructed element's content whole elements, so a gap or an overrun is bytes no element accounts for. */
139
+ function derChildren(buf, parent) {
140
+ const children = []
141
+ for (let at = parent.start; at < parent.end; at = children[children.length - 1].end) {
142
+ const child = derElement(buf, at, parent.end)
143
+ if (child === undefined) return undefined
144
+ children.push(child)
145
+ }
146
+ return children
147
+ }
148
+
149
+ /** Whether every constructed element within `element`, itself included, holds whole elements and nothing else, nesting no deeper than 64 levels. Primitive content is opaque bytes, so only constructed content is walked. */
150
+ function derTiles(buf, element, depth = 0) {
151
+ if ((element.tag & 0x20) === 0) return true
152
+ if (depth >= 64) return false
153
+ const children = derChildren(buf, element)
154
+ return children !== undefined && children.every((child) => derTiles(buf, child, depth + 1))
155
+ }
156
+
157
+ /** `parent`'s children when their tags, as derTags lays them out, match `shape`; undefined otherwise. */
158
+ function derShaped(buf, parent, shape) {
159
+ const children = derChildren(buf, parent)
160
+ return children !== undefined && shape.test(derTags(children)) ? children : undefined
161
+ }
162
+
163
+ /** The first identifier octet of each of `elements`, as two-digit hex joined by spaces, the form the shape patterns match. */
164
+ function derTags(elements) {
165
+ return elements.map((element) => element.tag.toString(16).padStart(2, '0')).join(' ')
166
+ }
167
+
168
+ /** Why `buf` is not a static 64-bit little-endian ELF executable for `machine`, or undefined when it is. Header layout per the System V ABI (elf(5)): e_machine at 18, e_phoff at 32, e_phentsize and e_phnum at 54 and 56. A program header of type PT_INTERP (3) names a dynamic loader, which a musl-static build does not have and a glibc build does; a DT_NEEDED entry in the dynamic segment names a shared library, which a static-pie build does not have either. A PT_LOAD (1) segment's file bytes (p_offset at 8, p_filesz at 32) must lie inside the file, and at least one must carry PF_X (p_flags at 4), so a header with nothing to run behind it is refused. */
169
+ export function elfProblem(buf, machine) {
170
+ if (buf.length < 64 || buf.readUInt32BE(0) !== 0x7f454c46) return 'is not an ELF file'
171
+ if (buf[4] !== 2 || buf[5] !== 1) return 'is not a 64-bit little-endian ELF file'
172
+ const declared = buf.readUInt16LE(18)
173
+ if (declared !== machine) return `declares machine ${declared}, expected ${machine}`
174
+ const type = buf.readUInt16LE(16)
175
+ if (type !== ET_EXEC && type !== ET_DYN) return `is not an executable (e_type ${type})`
176
+ const phoff = Number(buf.readBigUInt64LE(32))
177
+ const phentsize = buf.readUInt16LE(54)
178
+ const phnum = buf.readUInt16LE(56)
179
+ if (phentsize < ELF64_PHDR_SIZE || phoff + phentsize * phnum > buf.length) return 'has program headers that do not fit in the file'
180
+ const dynamicSegments = []
181
+ let executable = false
182
+ for (let i = 0; i < phnum; i++) {
183
+ const at = phoff + i * phentsize
184
+ const segment = buf.readUInt32LE(at)
185
+ if (segment === PT_INTERP) return 'is dynamically linked (it names an ELF interpreter); the release ships musl-static binaries only'
186
+ if (segment === PT_DYNAMIC) dynamicSegments.push(at)
187
+ if (segment !== PT_LOAD) continue
188
+ if (Number(buf.readBigUInt64LE(at + 8)) + Number(buf.readBigUInt64LE(at + 32)) > buf.length) return 'has a loadable segment that does not fit in the file'
189
+ if ((buf.readUInt32LE(at + 4) & PF_X) !== 0) executable = true
190
+ }
191
+ if (!executable) return 'has no executable loadable segment, so it holds no code to run'
192
+ // A static-pie has a PT_DYNAMIC segment for its own relocations but no DT_NEEDED entry; a DT_NEEDED names a shared library some loader would have to supply. Entries are 16 bytes (d_tag, d_val), ending at DT_NULL.
193
+ for (const at of dynamicSegments) {
194
+ const start = Number(buf.readBigUInt64LE(at + 8))
195
+ const end = start + Number(buf.readBigUInt64LE(at + 32))
196
+ if (end > buf.length) return 'has a dynamic segment that does not fit in the file'
197
+ for (let entry = start; entry + 16 <= end; entry += 16) {
198
+ const tag = buf.readBigInt64LE(entry)
199
+ if (tag === DT_NULL) break
200
+ if (tag === DT_NEEDED) return 'is dynamically linked (it needs a shared library); the release ships musl-static binaries only'
201
+ }
202
+ }
203
+ return undefined
204
+ }
205
+
206
+ // elf(5): e_type values, program header types, the execute permission flag, the dynamic tags read above, and the size of an Elf64_Phdr.
207
+ const ET_EXEC = 2
208
+ const ET_DYN = 3
209
+ const PT_LOAD = 1
210
+ const PT_DYNAMIC = 2
211
+ const PT_INTERP = 3
212
+ const PF_X = 1
213
+ const DT_NULL = 0n
214
+ const DT_NEEDED = 1n
215
+ const ELF64_PHDR_SIZE = 56
216
+
217
+ /** `sha256sum` output (text or binary mode) as a map from relative path to lowercase hex digest. A line that is not a digest and a path is a problem, not something to skip. */
218
+ export function parseManifest(text, label = 'manifest') {
219
+ const entries = new Map()
220
+ const problems = []
221
+ for (const line of text.split(/\r?\n/)) {
222
+ if (line.trim() === '') continue
223
+ const m = /^([0-9a-fA-F]{64}) [ *](.+)$/.exec(line)
224
+ if (m === null) {
225
+ problems.push(`${label}: not a sha256sum line: ${JSON.stringify(line)}`)
226
+ continue
227
+ }
228
+ const rel = m[2].replace(/\\/g, '/').replace(/^\.\//, '')
229
+ if (entries.has(rel)) problems.push(`${label}: lists ${rel} twice`)
230
+ entries.set(rel, m[1].toLowerCase())
231
+ }
232
+ return { entries, problems }
233
+ }
234
+
235
+ function filesUnder(dir) {
236
+ const out = []
237
+ const walk = (at) => {
238
+ for (const entry of readdirSync(path.join(dir, at), { withFileTypes: true })) {
239
+ const rel = at === '' ? entry.name : `${at}/${entry.name}`
240
+ if (entry.isDirectory()) walk(rel)
241
+ else out.push(rel)
242
+ }
243
+ }
244
+ walk('')
245
+ return out.sort()
246
+ }
247
+
248
+ const targetPath = (t) => `${t.platformArch}/${t.exe}`
249
+
250
+ /** The targets a release ships: all of them, or only the Linux ones when there is no Windows signing configuration, since a Windows binary ships signed or not at all. */
251
+ export const releaseTargets = (withoutWindows = false) => (withoutWindows ? NATIVE_TARGETS.filter((t) => t.format !== 'pe') : NATIVE_TARGETS)
252
+
253
+ const UNSIGNED_RELEASE = 'Windows binaries ship only signed, and this release has no Windows signing configuration'
254
+
255
+ /** Every reason `dir` must not be published, given the manifests' text keyed by a label for messages. Empty when the directory holds exactly the release binaries, each matching exactly one manifest entry and, for Windows, signed. */
256
+ export function verifyNativeDist(dir, manifests, { withoutWindows = false } = {}) {
257
+ const problems = []
258
+ const expected = new Map(releaseTargets(withoutWindows).map((t) => [targetPath(t), t]))
259
+ const windows = new Set(NATIVE_TARGETS.filter((t) => t.format === 'pe').map(targetPath))
260
+ const hashes = new Map()
261
+ for (const [label, text] of Object.entries(manifests)) {
262
+ const parsed = parseManifest(text, label)
263
+ problems.push(...parsed.problems)
264
+ for (const [rel, digest] of parsed.entries) {
265
+ if (!expected.has(rel)) problems.push(`${label}: lists ${rel}, which is not a release binary`)
266
+ else if (hashes.has(rel)) problems.push(`${rel}: listed by more than one manifest`)
267
+ else hashes.set(rel, digest)
268
+ }
269
+ }
270
+ let present
271
+ try {
272
+ present = filesUnder(dir)
273
+ } catch (e) {
274
+ return [...problems, `${dir}: cannot be read (${e instanceof Error ? e.message : String(e)})`]
275
+ }
276
+ for (const rel of present) {
277
+ if (expected.has(rel)) continue
278
+ problems.push(withoutWindows && windows.has(rel) ? `${rel}: ${UNSIGNED_RELEASE}` : `${rel}: is not a release binary; dist/native must hold the release artifacts and nothing else`)
279
+ }
280
+ for (const [rel, target] of expected) {
281
+ if (!present.includes(rel)) {
282
+ problems.push(`${rel}: missing`)
283
+ continue
284
+ }
285
+ const read = readBinary(dir, rel)
286
+ if (typeof read === 'string') {
287
+ problems.push(`${rel}: ${read}`)
288
+ continue
289
+ }
290
+ const want = hashes.get(rel)
291
+ const got = createHash('sha256').update(read).digest('hex')
292
+ if (want === undefined) problems.push(`${rel}: no manifest records its sha256`)
293
+ else if (want !== got) problems.push(`${rel}: sha256 ${got} does not match the recorded ${want}`)
294
+ const shape = shapeProblem(read, target)
295
+ if (shape !== undefined) problems.push(`${rel}: ${shape}`)
296
+ }
297
+ return problems
298
+ }
299
+
300
+ /** The bytes of `dir/rel`, or why they cannot be read: a dangling link or a link to a directory is listed as a file but has none. */
301
+ function readBinary(dir, rel) {
302
+ try {
303
+ return readFileSync(path.join(dir, rel))
304
+ } catch (e) {
305
+ return `cannot be read (${e instanceof Error ? e.message : String(e)})`
306
+ }
307
+ }
308
+
309
+ const shapeProblem = (buf, target) => (target.format === 'pe' ? peSignatureProblem(buf, target.machine) : elfProblem(buf, target.machine))
310
+
311
+ const PACK_REMEDY = 'delete dist/native, or publish through the release workflow, which signs it'
312
+
313
+ /** Every reason a publish from a working tree must not pack `dir`. A missing directory is the supported state, since the Node hook serves every platform without it; anything present must be a release target in its release shape, so a local `npm run build:native` output, an unsigned Windows build above all, never reaches the registry. No manifest exists here, so this checks shape, not provenance. */
314
+ export function verifyPackDir(dir) {
315
+ let present
316
+ try {
317
+ present = filesUnder(dir)
318
+ } catch (e) {
319
+ if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') return []
320
+ return [`${dir}: cannot be read (${e instanceof Error ? e.message : String(e)})`]
321
+ }
322
+ const targets = new Map(NATIVE_TARGETS.map((t) => [targetPath(t), t]))
323
+ const problems = []
324
+ for (const rel of present) {
325
+ const target = targets.get(rel)
326
+ if (target === undefined) {
327
+ problems.push(`${rel}: is not a release binary; ${PACK_REMEDY}`)
328
+ continue
329
+ }
330
+ const read = readBinary(dir, rel)
331
+ const shape = typeof read === 'string' ? read : shapeProblem(read, target)
332
+ if (shape !== undefined) problems.push(`${rel}: ${shape}; ${PACK_REMEDY}`)
333
+ }
334
+ return problems
335
+ }
336
+
337
+ const USAGE = 'usage: node scripts/verify-native-dist.mjs [--without-windows] <dist/native> <SHA256SUMS>...\n node scripts/verify-native-dist.mjs --pack <dist/native>\n'
338
+
339
+ function refuse(problems) {
340
+ for (const p of problems) process.stderr.write(`verify-native-dist: ${p}\n`)
341
+ process.stderr.write('verify-native-dist: refusing to publish\n')
342
+ return 1
343
+ }
344
+
345
+ function main(argv) {
346
+ if (argv[0] === '--pack') {
347
+ if (argv.length !== 2) {
348
+ process.stderr.write(USAGE)
349
+ return 2
350
+ }
351
+ const problems = verifyPackDir(argv[1])
352
+ if (problems.length > 0) return refuse(problems)
353
+ process.stdout.write(`verify-native-dist: ${argv[1]} holds no unsigned or unexpected native binary\n`)
354
+ return 0
355
+ }
356
+ const withoutWindows = argv[0] === '--without-windows'
357
+ const [dir, ...manifestPaths] = withoutWindows ? argv.slice(1) : argv
358
+ if (dir === undefined || dir.startsWith('--') || manifestPaths.length === 0) {
359
+ process.stderr.write(USAGE)
360
+ return 2
361
+ }
362
+ const manifests = {}
363
+ for (const file of manifestPaths) {
364
+ try {
365
+ manifests[file] = readFileSync(file, 'utf8')
366
+ } catch (e) {
367
+ process.stderr.write(`verify-native-dist: cannot read ${file}: ${e instanceof Error ? e.message : String(e)}\n`)
368
+ return 1
369
+ }
370
+ }
371
+ const problems = verifyNativeDist(dir, manifests, { withoutWindows })
372
+ if (problems.length > 0) return refuse(problems)
373
+ const note = withoutWindows ? ' (no Windows binaries: Windows installs keep the Node hook)' : ''
374
+ process.stdout.write(`verify-native-dist: ${releaseTargets(withoutWindows).length} native binaries verified${note}\n`)
375
+ return 0
376
+ }
377
+
378
+ // Node resolves symlinks in the module URL but not in argv[1], so both sides are resolved: comparing the raw path let a run through a linked directory (an npm link or a global install of a checkout) skip the check and exit 0.
379
+ const realOrResolved = (p) => {
380
+ try {
381
+ return realpathSync.native(p)
382
+ } catch {
383
+ return path.resolve(p)
384
+ }
385
+ }
386
+ if (process.argv[1] && realOrResolved(process.argv[1]) === realOrResolved(fileURLToPath(import.meta.url))) process.exit(main(process.argv.slice(2)))
@@ -1,143 +0,0 @@
1
- import { createRequire as __cjsRequire } from 'node:module';
2
- const require = __cjsRequire(import.meta.url);
3
- import {
4
- loadConfig
5
- } from "./token-goat-chunk-TGOXNAHW.mjs";
6
- import {
7
- displaySafeJson
8
- } from "./token-goat-chunk-NMTKNYGF.mjs";
9
- import {
10
- VERSION
11
- } from "./token-goat-chunk-MELOKFDJ.mjs";
12
- import {
13
- init_define_import_meta_env
14
- } from "./token-goat-chunk-A37V4PBF.mjs";
15
-
16
- // src/cli_upgrade.ts
17
- init_define_import_meta_env();
18
- import { spawnSync } from "node:child_process";
19
- import * as fs from "node:fs";
20
- import * as path from "node:path";
21
- import * as https from "node:https";
22
- async function fetchLatestVersion(timeoutMs = 3500) {
23
- if (loadConfig().network.offline) {
24
- return null;
25
- }
26
- return new Promise((resolve) => {
27
- const req = https.get(
28
- "https://registry.npmjs.org/token-goat/latest",
29
- {
30
- headers: { "User-Agent": `token-goat/${VERSION} (node/${process.version})` },
31
- timeout: timeoutMs
32
- },
33
- (res) => {
34
- if (res.statusCode !== 200) {
35
- res.resume();
36
- resolve(null);
37
- return;
38
- }
39
- let data = "";
40
- res.setEncoding("utf8");
41
- res.on("data", (chunk) => {
42
- data += chunk;
43
- if (data.length > 32768) {
44
- req.destroy();
45
- resolve(null);
46
- }
47
- });
48
- res.on("end", () => {
49
- try {
50
- const parsed = JSON.parse(data);
51
- resolve(typeof parsed.version === "string" ? parsed.version : null);
52
- } catch {
53
- resolve(null);
54
- }
55
- });
56
- }
57
- );
58
- req.on("timeout", () => {
59
- req.destroy();
60
- resolve(null);
61
- });
62
- req.on("error", () => {
63
- resolve(null);
64
- });
65
- });
66
- }
67
- function compareSemver(a, b) {
68
- const pa = a.split("-")[0].split(".").map((x) => parseInt(x, 10) || 0);
69
- const pb = b.split("-")[0].split(".").map((x) => parseInt(x, 10) || 0);
70
- for (let i = 0; i < 3; i++) {
71
- const na = pa[i] ?? 0;
72
- const nb = pb[i] ?? 0;
73
- if (na > nb) return 1;
74
- if (na < nb) return -1;
75
- }
76
- return 0;
77
- }
78
- async function checkUpdateStatus(timeoutMs = 3500) {
79
- const latest = await fetchLatestVersion(timeoutMs);
80
- if (!latest) {
81
- return {
82
- current: VERSION,
83
- latest: null,
84
- updateAvailable: false,
85
- error: "Could not reach npm registry (offline or request timed out)"
86
- };
87
- }
88
- const updateAvailable = compareSemver(latest, VERSION) > 0;
89
- return {
90
- current: VERSION,
91
- latest,
92
- updateAvailable
93
- };
94
- }
95
- async function cmdUpgrade(opts = {}, onSyncHooks) {
96
- const status = await checkUpdateStatus();
97
- if (opts.check) {
98
- if (opts.json) {
99
- console.log(displaySafeJson(status, 2));
100
- return;
101
- }
102
- if (status.error) {
103
- console.log(`Current version: v${status.current}`);
104
- console.log(`[!] ${status.error}`);
105
- return;
106
- }
107
- if (status.updateAvailable && status.latest) {
108
- console.log(`Update available: v${status.current} -> v${status.latest}`);
109
- console.log(`Run 'token-goat upgrade' to update.`);
110
- } else {
111
- console.log(`token-goat is up to date (v${status.current})`);
112
- }
113
- return;
114
- }
115
- const isLocalGitRepo = fs.existsSync(path.join(process.cwd(), ".git")) && fs.existsSync(path.join(process.cwd(), "esbuild.config.mjs"));
116
- if (isLocalGitRepo) {
117
- console.log(`Detected local token-goat development repository.`);
118
- console.log(`To update your local build, run:`);
119
- console.log(` git pull && npm run build && token-goat install`);
120
- return;
121
- }
122
- console.log(`Upgrading token-goat to latest...`);
123
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
124
- const npmRes = spawnSync(npmCmd, ["install", "-g", "token-goat@latest"], {
125
- stdio: "inherit"
126
- });
127
- if (npmRes.status !== 0) {
128
- console.error(`npm install failed with exit code ${npmRes.status ?? 1}.`);
129
- process.exit(npmRes.status ?? 1);
130
- }
131
- console.log(`Syncing token-goat hooks and integration manifests...`);
132
- if (onSyncHooks) {
133
- await onSyncHooks();
134
- }
135
- console.log(`[\u2713] token-goat successfully updated.`);
136
- }
137
-
138
- export {
139
- fetchLatestVersion,
140
- compareSemver,
141
- checkUpdateStatus,
142
- cmdUpgrade
143
- };