@rubytech/create-maxy-code 0.1.307 → 0.1.309

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 (206) hide show
  1. package/dist/__tests__/claude-ptys-slice.test.js +1 -1
  2. package/dist/__tests__/cloudflared-slice.test.js +1 -1
  3. package/dist/__tests__/macos-darwin-branch.test.js +4 -6
  4. package/package.json +1 -1
  5. package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +142 -0
  6. package/payload/platform/lib/admin-access-password/dist/index.d.ts +49 -0
  7. package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -0
  8. package/payload/platform/lib/admin-access-password/dist/index.js +198 -0
  9. package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -0
  10. package/payload/platform/lib/admin-access-password/src/index.ts +176 -0
  11. package/payload/platform/lib/admin-access-password/tsconfig.json +8 -0
  12. package/payload/platform/lib/models/dist/index.d.ts +13 -0
  13. package/payload/platform/lib/models/dist/index.d.ts.map +1 -1
  14. package/payload/platform/lib/models/dist/index.js +20 -1
  15. package/payload/platform/lib/models/dist/index.js.map +1 -1
  16. package/payload/platform/lib/models/src/index.ts +19 -0
  17. package/payload/platform/package.json +2 -2
  18. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  19. package/payload/platform/plugins/admin/mcp/dist/index.js +63 -19
  20. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  21. package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +8 -2
  22. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +23 -3
  23. package/payload/platform/plugins/docs/references/admin-ui.md +21 -1
  24. package/payload/platform/plugins/docs/references/internals.md +1 -1
  25. package/payload/platform/scripts/check-no-esm-require.mjs +4 -0
  26. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  27. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  28. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  29. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +4 -0
  30. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  31. package/payload/platform/services/claude-session-manager/dist/http-server.js +193 -1
  32. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  33. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +12 -0
  34. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  35. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +52 -1
  36. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +9 -3
  39. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts +8 -0
  41. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js +7 -0
  43. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js.map +1 -1
  44. package/payload/platform/services/webchat-channel/dist/instructions.d.ts +8 -0
  45. package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -0
  46. package/payload/platform/services/webchat-channel/dist/instructions.js +44 -0
  47. package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -0
  48. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
  49. package/payload/platform/services/webchat-channel/dist/server.js +11 -18
  50. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  51. package/payload/server/{chunk-4QFPGAUZ.js → chunk-J6WDAWFJ.js} +190 -82
  52. package/payload/server/maxy-edge.js +1 -1
  53. package/payload/server/public/assets/AccessGate-CU4Qw6M6.js +1 -0
  54. package/payload/server/public/assets/AdminLoginScreens-DUpRIM1h.js +1 -0
  55. package/payload/server/public/assets/AdminShell-DkoQSsqo.js +1 -0
  56. package/payload/server/public/assets/{Checkbox-DoPOlZTV.js → Checkbox-DMA1hVhn.js} +1 -1
  57. package/payload/server/public/assets/OperatorConversations-BIHiyyxU.js +1 -0
  58. package/payload/server/public/assets/admin-BHvOMcCh.js +1 -0
  59. package/payload/server/public/assets/{arc-DxSm8tli.js → arc-Dl9QFDgW.js} +1 -1
  60. package/payload/server/public/assets/architecture-YZFGNWBL-Crq5RSDG.js +1 -0
  61. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-7JSAh0vL.js → architectureDiagram-Q4EWVU46-CNLQUVi9.js} +1 -1
  62. package/payload/server/public/assets/{audio-attachment-mime-BA10IzqQ.js → audio-attachment-mime-DOBET-W1.js} +3 -3
  63. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DLoVmHKD.js → blockDiagram-DXYQGD6D-B-QxfVjk.js} +1 -1
  64. package/payload/server/public/assets/brand-4F9ZXBF8.css +1 -0
  65. package/payload/server/public/assets/{brand-BaRMO3mZ.js → brand-C7kj0USi.js} +1 -1
  66. package/payload/server/public/assets/browser-eLSiC7_b.js +1 -0
  67. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Nv1G0o9P.js → c4Diagram-AHTNJAMY-NuYFvPDU.js} +1 -1
  68. package/payload/server/public/assets/channel-KT66MFN2.js +1 -0
  69. package/payload/server/public/assets/chat-DedpVKUl.js +1 -0
  70. package/payload/server/public/assets/{chunk-2KRD3SAO-BpqPMhoA.js → chunk-2KRD3SAO-l5VL1KQm.js} +1 -1
  71. package/payload/server/public/assets/{chunk-336JU56O-DgXHlVU_.js → chunk-336JU56O-C5gVznC2.js} +2 -2
  72. package/payload/server/public/assets/chunk-426QAEUC-CWMiOm8Y.js +1 -0
  73. package/payload/server/public/assets/{chunk-4BX2VUAB-nwyZSZH8.js → chunk-4BX2VUAB-BtMVz_Sq.js} +1 -1
  74. package/payload/server/public/assets/{chunk-4TB4RGXK-Tfxz9LHX.js → chunk-4TB4RGXK-C8-dmCuT.js} +1 -1
  75. package/payload/server/public/assets/{chunk-55IACEB6-6FMJQGXG.js → chunk-55IACEB6-C5gWvEJW.js} +1 -1
  76. package/payload/server/public/assets/{chunk-5FUZZQ4R-g2FgZF3D.js → chunk-5FUZZQ4R-BZ7VZoAC.js} +1 -1
  77. package/payload/server/public/assets/{chunk-5PVQY5BW-DtLwXsPH.js → chunk-5PVQY5BW-C_ajQOGC.js} +1 -1
  78. package/payload/server/public/assets/{chunk-67CJDMHE-K91CP5ZU.js → chunk-67CJDMHE-BJOR2dwK.js} +1 -1
  79. package/payload/server/public/assets/{chunk-7N4EOEYR-BGpCQhqK.js → chunk-7N4EOEYR-DvZLpquX.js} +1 -1
  80. package/payload/server/public/assets/{chunk-AA7GKIK3-jkDbInck.js → chunk-AA7GKIK3-h5tVsPlW.js} +1 -1
  81. package/payload/server/public/assets/{chunk-BSJP7CBP-mDosdzGs.js → chunk-BSJP7CBP-DLOF2-m3.js} +1 -1
  82. package/payload/server/public/assets/{chunk-CIAEETIT-DhBxewpQ.js → chunk-CIAEETIT-CQRQMPCf.js} +1 -1
  83. package/payload/server/public/assets/{chunk-EDXVE4YY-B7c-9Emg.js → chunk-EDXVE4YY-K9-EfuM2.js} +1 -1
  84. package/payload/server/public/assets/{chunk-ENJZ2VHE-Cqhhncox.js → chunk-ENJZ2VHE-DVoy6NvU.js} +1 -1
  85. package/payload/server/public/assets/{chunk-FMBD7UC4-BpDq6wj1.js → chunk-FMBD7UC4-CQn_2o2b.js} +1 -1
  86. package/payload/server/public/assets/{chunk-FOC6F5B3-Ds02-ktu.js → chunk-FOC6F5B3-CuN73goS.js} +1 -1
  87. package/payload/server/public/assets/{chunk-ICPOFSXX-BTX5POFr.js → chunk-ICPOFSXX-CKhl9J-H.js} +2 -2
  88. package/payload/server/public/assets/{chunk-K5T4RW27-Du1PXXBU.js → chunk-K5T4RW27-CBskuQmq.js} +1 -1
  89. package/payload/server/public/assets/{chunk-KGLVRYIC-siasOAf8.js → chunk-KGLVRYIC-Chtjt8xK.js} +1 -1
  90. package/payload/server/public/assets/{chunk-LIHQZDEY-xf1rbZtf.js → chunk-LIHQZDEY-EOBiBNSH.js} +1 -1
  91. package/payload/server/public/assets/{chunk-ORNJ4GCN-DeTLQ_LD.js → chunk-ORNJ4GCN-uPPq-5MN.js} +1 -1
  92. package/payload/server/public/assets/{chunk-OYMX7WX6-DQ7_oVJn.js → chunk-OYMX7WX6-BRzbxJOT.js} +1 -1
  93. package/payload/server/public/assets/chunk-QZHKN3VN-DCzpF46A.js +1 -0
  94. package/payload/server/public/assets/{chunk-U2HBQHQK-CXmFUKgn.js → chunk-U2HBQHQK-Bndyn5nF.js} +1 -1
  95. package/payload/server/public/assets/{chunk-X2U36JSP-Bj8-8Wkz.js → chunk-X2U36JSP-BspGBHXW.js} +1 -1
  96. package/payload/server/public/assets/{chunk-XPW4576I-Con3TXqt.js → chunk-XPW4576I-HgXQ2k9h.js} +1 -1
  97. package/payload/server/public/assets/{chunk-YZCP3GAM-Dx8BHGWf.js → chunk-YZCP3GAM-C7oTyCnw.js} +1 -1
  98. package/payload/server/public/assets/{chunk-ZZ45TVLE-Dp4Q5Y-f.js → chunk-ZZ45TVLE-CufWa7QL.js} +1 -1
  99. package/payload/server/public/assets/classDiagram-6PBFFD2Q-QOU4HzJJ.js +1 -0
  100. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DcOUai3Z.js +1 -0
  101. package/payload/server/public/assets/clone-CHh05KN-.js +1 -0
  102. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-pZiCv69E.js → cose-bilkent-S5V4N54A-BM3yEbF-.js} +1 -1
  103. package/payload/server/public/assets/{dagre-CLqhEgbL.js → dagre-C7WHNZO8.js} +1 -1
  104. package/payload/server/public/assets/{dagre-KV5264BT-BCxE8iyk.js → dagre-KV5264BT-bcZx3l06.js} +1 -1
  105. package/payload/server/public/assets/data-BcoBY39U.js +1 -0
  106. package/payload/server/public/assets/{diagram-5BDNPKRD-slUEdZzz.js → diagram-5BDNPKRD-B7VotaMF.js} +1 -1
  107. package/payload/server/public/assets/{diagram-G4DWMVQ6-DPcraMfu.js → diagram-G4DWMVQ6-CWWvEsFx.js} +1 -1
  108. package/payload/server/public/assets/{diagram-MMDJMWI5-DR6luhGK.js → diagram-MMDJMWI5-DYHVkTYJ.js} +1 -1
  109. package/payload/server/public/assets/{diagram-TYMM5635-Jap1B4wA.js → diagram-TYMM5635-CHeB6Ama.js} +1 -1
  110. package/payload/server/public/assets/{dist-Dbh7HrZX.js → dist-CjjtmoWL.js} +1 -1
  111. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DJKUs2hO.js → erDiagram-SMLLAGMA-C9gog8QP.js} +1 -1
  112. package/payload/server/public/assets/{flatten-Cl1Bc3dR.js → flatten-QcJDm7yK.js} +1 -1
  113. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-C8W-ZZ9W.js → flowDiagram-DWJPFMVM-B2O9lKTh.js} +1 -1
  114. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DYCX4Xu2.js → ganttDiagram-T4ZO3ILL-DEa4AwbR.js} +1 -1
  115. package/payload/server/public/assets/gitGraph-7Q5UKJZL-BC-u4jgy.js +1 -0
  116. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CZu_fpgS.js → gitGraphDiagram-UUTBAWPF-BjXPQpBL.js} +1 -1
  117. package/payload/server/public/assets/{graph-D29p1FJH.js → graph-C7Hye_vp.js} +2 -2
  118. package/payload/server/public/assets/{graph-labels-DF3QVHkH.js → graph-labels-YYgGsEVd.js} +1 -1
  119. package/payload/server/public/assets/{graphlib-Bdp7TA4y.js → graphlib-CBevn2DP.js} +1 -1
  120. package/payload/server/public/assets/info-OMHHGYJF-DPUetTz-.js +1 -0
  121. package/payload/server/public/assets/infoDiagram-42DDH7IO-q2dl09-M.js +2 -0
  122. package/payload/server/public/assets/{isEmpty-D1pGOD1J.js → isEmpty-BO6BAEn7.js} +1 -1
  123. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-B7KfIX6z.js → ishikawaDiagram-UXIWVN3A-CPLfpvqv.js} +1 -1
  124. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CSDwBfhF.js → journeyDiagram-VCZTEJTY-CTHe8MiD.js} +1 -1
  125. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CWzcEKTr.js → kanban-definition-6JOO6SKY-Bm9h1O3Y.js} +1 -1
  126. package/payload/server/public/assets/{line-BNQSlbYn.js → line-C31b0rtY.js} +1 -1
  127. package/payload/server/public/assets/{linear-BN6kGN93.js → linear-Cfm_8_pU.js} +1 -1
  128. package/payload/server/public/assets/{mermaid-parser.core-CcFhkElq.js → mermaid-parser.core-B3tGwOcE.js} +2 -2
  129. package/payload/server/public/assets/{mermaid.core-BsIeJ7Cc.js → mermaid.core-BM3s85ZW.js} +3 -3
  130. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CQVzj6D_.js → mindmap-definition-QFDTVHPH-CaGv_mIB.js} +1 -1
  131. package/payload/server/public/assets/operator-WsXhcbBF.js +1 -0
  132. package/payload/server/public/assets/{ordinal-CBZeH4Yp.js → ordinal-jLCwuvYg.js} +1 -1
  133. package/payload/server/public/assets/packet-4T2RLAQJ-BcfR-ZwA.js +1 -0
  134. package/payload/server/public/assets/page-BhHTnqv4.js +1 -0
  135. package/payload/server/public/assets/pie-ZZUOXDRM-BiKgZdNB.js +1 -0
  136. package/payload/server/public/assets/{pieDiagram-DEJITSTG-DnrQ9fyn.js → pieDiagram-DEJITSTG-BhAXps4u.js} +1 -1
  137. package/payload/server/public/assets/public-ViXjC_dA.js +6 -0
  138. package/payload/server/public/assets/public-next-BKECyMz2.js +1 -0
  139. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BQWlCyWi.js → quadrantDiagram-34T5L4WZ-BWtTdNs8.js} +1 -1
  140. package/payload/server/public/assets/radar-PYXPWWZC-nvCjiW_J.js +1 -0
  141. package/payload/server/public/assets/{reduce-PuPlFPRX.js → reduce-Dmi_NdVH.js} +1 -1
  142. package/payload/server/public/assets/{requirementDiagram-MS252O5E-Du_vZhU1.js → requirementDiagram-MS252O5E-CWU0qCcP.js} +1 -1
  143. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-Cf6Z8GPQ.js → sankeyDiagram-XADWPNL6-DYvutQld.js} +1 -1
  144. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-CS8GVol8.js → sequenceDiagram-FGHM5R23-Cir0hBWS.js} +1 -1
  145. package/payload/server/public/assets/{src-BoaldmnP.js → src-CwB968DO.js} +1 -1
  146. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-Bv1NW2F1.js → stateDiagram-FHFEXIEX-Be-h8YSS.js} +1 -1
  147. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CAnmjSj7.js +1 -0
  148. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-ClGYAsr0.js → timeline-definition-GMOUNBTQ-C3C4Wuyk.js} +1 -1
  149. package/payload/server/public/assets/treeView-SZITEDCU-BinXCVyM.js +1 -0
  150. package/payload/server/public/assets/treemap-W4RFUUIX-CSmh20Ze.js +1 -0
  151. package/payload/server/public/assets/{useSelectionMode-ghXuZicl.js → useSelectionMode-CG_Iel5F.js} +1 -1
  152. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-C2wTcxQT.js → vennDiagram-DHZGUBPP-mDclw0Sa.js} +1 -1
  153. package/payload/server/public/assets/wardley-RL74JXVD-BiCmRKWx.js +1 -0
  154. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-D86-ivEx.js → wardleyDiagram-NUSXRM2D-mvk-YA9g.js} +1 -1
  155. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-Ba5WAN8P.js → xychartDiagram-5P7HB3ND-9M7k1hmD.js} +1 -1
  156. package/payload/server/public/browser.html +8 -8
  157. package/payload/server/public/chat.html +11 -11
  158. package/payload/server/public/data.html +7 -7
  159. package/payload/server/public/graph.html +10 -10
  160. package/payload/server/public/index.html +10 -10
  161. package/payload/server/public/operator.html +13 -13
  162. package/payload/server/public/public-next.html +24 -0
  163. package/payload/server/public/public.html +9 -8
  164. package/payload/server/server.js +4450 -3781
  165. package/dist/__tests__/onboarding-state-readback.test.js +0 -61
  166. package/dist/__tests__/rss-sampler-installer.test.js +0 -27
  167. package/dist/onboarding-readback.js +0 -27
  168. package/payload/server/public/assets/AdminLoginScreens-BsC-EwAn.js +0 -1
  169. package/payload/server/public/assets/AdminShell-CiKKN22G.js +0 -1
  170. package/payload/server/public/assets/admin-DSwmdCKp.js +0 -1
  171. package/payload/server/public/assets/architecture-YZFGNWBL-6g9jRVgc.js +0 -1
  172. package/payload/server/public/assets/brand-C001cin3.css +0 -1
  173. package/payload/server/public/assets/browser-BKR1eJJb.js +0 -1
  174. package/payload/server/public/assets/channel-BgQLJanG.js +0 -1
  175. package/payload/server/public/assets/chat-DZXNRiy-.js +0 -1
  176. package/payload/server/public/assets/chunk-426QAEUC-Dr_XVuUq.js +0 -1
  177. package/payload/server/public/assets/chunk-QZHKN3VN-DF4o9HRJ.js +0 -1
  178. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DAbPKU6u.js +0 -1
  179. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BKuFQLcv.js +0 -1
  180. package/payload/server/public/assets/clone-DnNHGhNe.js +0 -1
  181. package/payload/server/public/assets/data-CuVmW1WN.js +0 -1
  182. package/payload/server/public/assets/file-download-4_hQmMwU.js +0 -1
  183. package/payload/server/public/assets/gitGraph-7Q5UKJZL-g7mvu6IY.js +0 -1
  184. package/payload/server/public/assets/info-OMHHGYJF-DZqJuIkB.js +0 -1
  185. package/payload/server/public/assets/infoDiagram-42DDH7IO-cGfGccHz.js +0 -2
  186. package/payload/server/public/assets/operator-CQRaBNN7.js +0 -1
  187. package/payload/server/public/assets/packet-4T2RLAQJ-Hn8rlHpy.js +0 -1
  188. package/payload/server/public/assets/page-Dz2F3QtB.js +0 -1
  189. package/payload/server/public/assets/pie-ZZUOXDRM-BPGWVqBm.js +0 -1
  190. package/payload/server/public/assets/public-B7BQwWpP.js +0 -6
  191. package/payload/server/public/assets/radar-PYXPWWZC-DMdLrj3E.js +0 -1
  192. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BGUCxPmp.js +0 -1
  193. package/payload/server/public/assets/treeView-SZITEDCU-CWxofUbI.js +0 -1
  194. package/payload/server/public/assets/treemap-W4RFUUIX-Cfgb_ZG0.js +0 -1
  195. package/payload/server/public/assets/wardley-RL74JXVD-B45olYjj.js +0 -1
  196. /package/payload/server/public/assets/{file-download-qc_xy7Az.css → OperatorConversations-qc_xy7Az.css} +0 -0
  197. /package/payload/server/public/assets/{_baseFor-brRjpUOX.js → _baseFor-D8P3gUw3.js} +0 -0
  198. /package/payload/server/public/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
  199. /package/payload/server/public/assets/{chunk-Pqm5yXtL.js → chunk-CAM3fms7.js} +0 -0
  200. /package/payload/server/public/assets/{cytoscape.esm-TvRJ2tGX.js → cytoscape.esm-mZ4wTuB7.js} +0 -0
  201. /package/payload/server/public/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
  202. /package/payload/server/public/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
  203. /package/payload/server/public/assets/{katex-RxgSu_dy.js → katex-DaHGEgm7.js} +0 -0
  204. /package/payload/server/public/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
  205. /package/payload/server/public/assets/{preload-helper-CQ36nxok.js → preload-helper-TKOBYGYD.js} +0 -0
  206. /package/payload/server/public/assets/{rough.esm-6CnTHTkH.js → rough.esm-JX0wREDd.js} +0 -0
@@ -10,7 +10,7 @@ import { buildClaudePtysSliceUnitFile } from "../port-resolution.js";
10
10
  test("buildClaudePtysSliceUnitFile emits a valid systemd slice unit", () => {
11
11
  const unit = buildClaudePtysSliceUnitFile();
12
12
  assert.match(unit, /^\[Unit\]$/m);
13
- assert.match(unit, /^Description=Claude Code PTY sessions \(Task 250\)$/m);
13
+ assert.match(unit, /^Description=.+$/m);
14
14
  assert.match(unit, /^\[Slice\]$/m);
15
15
  assert.match(unit, /^\[Install\]$/m);
16
16
  assert.match(unit, /^WantedBy=default\.target$/m);
@@ -8,7 +8,7 @@ import { buildCloudflaredSliceUnitFile } from "../port-resolution.js";
8
8
  test("buildCloudflaredSliceUnitFile emits a valid systemd slice unit", () => {
9
9
  const unit = buildCloudflaredSliceUnitFile();
10
10
  assert.match(unit, /^\[Unit\]$/m);
11
- assert.match(unit, /^Description=Cloudflare tunnel connectors \(Task 602\)$/m);
11
+ assert.match(unit, /^Description=.+$/m);
12
12
  assert.match(unit, /^\[Slice\]$/m);
13
13
  assert.match(unit, /^\[Install\]$/m);
14
14
  assert.match(unit, /^WantedBy=default\.target$/m);
@@ -31,10 +31,10 @@ test("installSystemDeps has darwin branch that calls setMacosHostnameViaScutil o
31
31
  assert.match(SRC, /darwin-hostname-mode=brand-fallback/, "brand-fallback path must emit observability log");
32
32
  });
33
33
  test("installSystemDeps darwin branch installs DARWIN_BASE_DEPS via brew", () => {
34
- // The shared dep set the darwin path feeds to installAllBrewPackages. If
35
- // this list shrinks or a name changes, brew-resolve.test.ts (DARWIN_BASE_DEPS_FIXTURE)
36
- // must move in lockstep — the comment in brew-resolve.test.ts says so.
37
- assert.match(SRC, /DARWIN_BASE_DEPS\s*=\s*\[\s*"curl"\s*,\s*"git"\s*,\s*"unzip"\s*,\s*"jq"\s*,\s*"poppler"\s*,\s*"ffmpeg"\s*,\s*"tesseract"\s*,\s*"ocrmypdf"\s*\]/);
34
+ // The darwin path feeds the shared dep set to installAllBrewPackages. The
35
+ // exact membership of DARWIN_BASE_DEPS is asserted behaviourally in
36
+ // brew-resolve.test.ts (DARWIN_BASE_DEPS_FIXTURE); here we only pin that the
37
+ // branch still routes that set through brew.
38
38
  assert.match(SRC, /installAllBrewPackages\(DARWIN_BASE_DEPS,\s*logFile\)/);
39
39
  });
40
40
  test("installCloudflared darwin branch logs the documented skip and brew-installs cloudflared", () => {
@@ -58,8 +58,6 @@ test("installService darwin branch delegates to installServiceDarwin (launchd pa
58
58
  // and installServiceDarwin writes the plist + bootstraps it via launchctl.
59
59
  assert.match(SRC, /platform\s*===\s*"darwin"\s*\)\s*\{\s*installServiceDarwin\(\)/);
60
60
  assert.match(SRC, /launchctl["']?,\s*\[["']bootstrap["']/);
61
- assert.match(SRC, /launchd-plist=\$\{path\}\s+loaded=true/, "successful bootstrap must emit launchd-plist=… loaded=true");
62
- assert.match(SRC, /launchd-plist=\$\{path\}\s+loaded=false/, "failed bootstrap must emit launchd-plist=… loaded=false");
63
61
  });
64
62
  test("installNodejs darwin branch brew-installs the `nodejs` alias name (resolver hops to node@22)", () => {
65
63
  // The shared call site uses the apt-style `nodejs` name. brew-resolve.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.307",
3
+ "version": "0.1.309",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mkdtempSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import {
6
+ hashAccessPassword,
7
+ setAccessPassword,
8
+ clearAccessPassword,
9
+ verifyAccessPassword,
10
+ anyAccessPasswordConfigured,
11
+ accessStoreUnreadable,
12
+ accessPasswordCollides,
13
+ migrateLegacyRemotePassword,
14
+ } from '../src/index'
15
+
16
+ function tmpUsers(content: unknown): string {
17
+ const dir = mkdtempSync(join(tmpdir(), 'aap-'))
18
+ const f = join(dir, 'users.json')
19
+ writeFileSync(f, JSON.stringify(content, null, 2))
20
+ return f
21
+ }
22
+
23
+ describe('admin-access-password', () => {
24
+ it('sets accessHash on the target record only, preserving siblings', async () => {
25
+ const f = tmpUsers([
26
+ { userId: 'a', pin: 'pinA', phone: '+1' },
27
+ { userId: 'b', pin: 'pinB' },
28
+ ])
29
+ await setAccessPassword('a', 'Sw0rdfish!', f)
30
+ const users = JSON.parse(readFileSync(f, 'utf-8'))
31
+ expect(users[0].accessHash).toMatch(/^[0-9a-f]+:[0-9a-f]+$/)
32
+ expect(users[0].pin).toBe('pinA')
33
+ expect(users[0].phone).toBe('+1')
34
+ expect(users[1].accessHash).toBeUndefined()
35
+ })
36
+
37
+ it('verifyAccessPassword returns the matching userId, null on miss', async () => {
38
+ const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
39
+ await setAccessPassword('b', 'Sw0rdfish!', f)
40
+ expect(await verifyAccessPassword('Sw0rdfish!', f)).toBe('b')
41
+ expect(await verifyAccessPassword('wrong', f)).toBeNull()
42
+ })
43
+
44
+ it("clearAccessPassword removes only that record's hash", async () => {
45
+ const f = tmpUsers([{ userId: 'a', pin: 'x' }])
46
+ await setAccessPassword('a', 'Sw0rdfish!', f)
47
+ clearAccessPassword('a', f)
48
+ const users = JSON.parse(readFileSync(f, 'utf-8'))
49
+ expect(users[0].accessHash).toBeUndefined()
50
+ expect(users[0].pin).toBe('x')
51
+ })
52
+
53
+ it('anyAccessPasswordConfigured reflects presence', async () => {
54
+ const f = tmpUsers([{ userId: 'a', pin: 'x' }])
55
+ expect(anyAccessPasswordConfigured(f)).toBe(false)
56
+ await setAccessPassword('a', 'Sw0rdfish!', f)
57
+ expect(anyAccessPasswordConfigured(f)).toBe(true)
58
+ })
59
+
60
+ it('verifyAccessPassword returns null when no record has accessHash', async () => {
61
+ const f = tmpUsers([{ userId: 'a', pin: 'x' }])
62
+ expect(await verifyAccessPassword('anything', f)).toBeNull()
63
+ })
64
+
65
+ it('accessPasswordCollides: true when a DIFFERENT admin already has that password', async () => {
66
+ const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
67
+ await setAccessPassword('a', 'Sw0rdfish!', f)
68
+ // b is choosing the same password a already uses
69
+ expect(await accessPasswordCollides('Sw0rdfish!', f, 'b')).toBe(true)
70
+ })
71
+
72
+ it('accessPasswordCollides: false when only the excluded admin holds that password', async () => {
73
+ const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
74
+ await setAccessPassword('a', 'Sw0rdfish!', f)
75
+ // a rotating to the same value it already has is not a cross-admin collision
76
+ expect(await accessPasswordCollides('Sw0rdfish!', f, 'a')).toBe(false)
77
+ })
78
+
79
+ it('accessPasswordCollides: false for a password no other admin uses', async () => {
80
+ const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
81
+ await setAccessPassword('a', 'Sw0rdfish!', f)
82
+ expect(await accessPasswordCollides('Different1!', f, 'b')).toBe(false)
83
+ })
84
+
85
+ it('accessPasswordCollides: false when the store is unreadable', async () => {
86
+ const dir = mkdtempSync(join(tmpdir(), 'aapc-'))
87
+ const corrupt = join(dir, 'users.json')
88
+ writeFileSync(corrupt, '{ not json')
89
+ expect(await accessPasswordCollides('anything', corrupt, 'b')).toBe(false)
90
+ })
91
+
92
+ it('accessStoreUnreadable: false for absent/clean, true for corrupt (fail-closed signal)', () => {
93
+ const dir = mkdtempSync(join(tmpdir(), 'aapu-'))
94
+ const absent = join(dir, 'users.json')
95
+ expect(accessStoreUnreadable(absent)).toBe(false) // absent → not unreadable
96
+ writeFileSync(absent, JSON.stringify([{ userId: 'a', pin: 'x' }]))
97
+ expect(accessStoreUnreadable(absent)).toBe(false) // clean → readable
98
+ writeFileSync(absent, '{ this is not json')
99
+ expect(accessStoreUnreadable(absent)).toBe(true) // corrupt → unreadable → caller fails closed
100
+ })
101
+ })
102
+
103
+ describe('migrateLegacyRemotePassword', () => {
104
+ function setup(usersContent: unknown, legacy?: string): { usersFile: string; legacyFile: string } {
105
+ const dir = mkdtempSync(join(tmpdir(), 'aapm-'))
106
+ const usersFile = join(dir, 'users.json')
107
+ const legacyFile = join(dir, '.remote-password')
108
+ writeFileSync(usersFile, JSON.stringify(usersContent, null, 2))
109
+ if (legacy !== undefined) writeFileSync(legacyFile, legacy)
110
+ return { usersFile, legacyFile }
111
+ }
112
+
113
+ it('moves the legacy hash into the owner record and deletes the file', async () => {
114
+ const legacyHash = await hashAccessPassword('OwnerPass1!')
115
+ const { usersFile, legacyFile } = setup([{ userId: 'owner', pin: 'x' }], legacyHash)
116
+ const r = migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')
117
+ expect(r).toBe('migrated')
118
+ expect(existsSync(legacyFile)).toBe(false)
119
+ const users = JSON.parse(readFileSync(usersFile, 'utf-8'))
120
+ expect(users[0].accessHash).toBe(legacyHash)
121
+ })
122
+
123
+ it('is idempotent — second run is noop', async () => {
124
+ const legacyHash = await hashAccessPassword('OwnerPass1!')
125
+ const { usersFile, legacyFile } = setup([{ userId: 'owner', pin: 'x' }], legacyHash)
126
+ migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')
127
+ expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')).toBe('noop-no-legacy')
128
+ })
129
+
130
+ it('no owner record → leaves the legacy file in place', async () => {
131
+ const legacyHash = await hashAccessPassword('OwnerPass1!')
132
+ const { usersFile, legacyFile } = setup([{ userId: 'someoneelse', pin: 'x' }], legacyHash)
133
+ expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')).toBe('noop-no-owner')
134
+ expect(existsSync(legacyFile)).toBe(true)
135
+ })
136
+
137
+ it('owner already has accessHash → deletes stale legacy file, noop', async () => {
138
+ const { usersFile, legacyFile } = setup([{ userId: 'owner', pin: 'x', accessHash: 'aa:bb' }], 'cc:dd')
139
+ expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')).toBe('noop-owner-has-hash')
140
+ expect(existsSync(legacyFile)).toBe(false)
141
+ })
142
+ })
@@ -0,0 +1,49 @@
1
+ export interface AccessUserEntry {
2
+ userId: string;
3
+ pin: string;
4
+ phone?: string;
5
+ accessHash?: string;
6
+ [k: string]: unknown;
7
+ }
8
+ export declare function hashAccessPassword(password: string): Promise<string>;
9
+ declare function matchesHash(password: string, stored: string): Promise<boolean>;
10
+ /** Write `accessHash` onto the userId's record. Throws if the record is absent. */
11
+ export declare function setAccessPassword(userId: string, password: string, usersFile: string): Promise<void>;
12
+ /** Remove `accessHash` from the userId's record. No-op if absent. */
13
+ export declare function clearAccessPassword(userId: string, usersFile: string): void;
14
+ /** Return the userId whose accessHash matches, or null. */
15
+ export declare function verifyAccessPassword(password: string, usersFile: string): Promise<string | null>;
16
+ /**
17
+ * True when some admin OTHER than excludeUserId already has an accessHash that
18
+ * matches `password`. Used by the set/change surfaces to refuse a value that
19
+ * would collide with another admin's perimeter credential — the scan-based
20
+ * verify attributes a login to the first matching record, so two admins sharing
21
+ * a password lets one silently overwrite the other. An unreadable store returns
22
+ * false: the subsequent write is what owns the read-fault error path.
23
+ */
24
+ export declare function accessPasswordCollides(password: string, usersFile: string, excludeUserId: string): Promise<boolean>;
25
+ /** True when at least one record carries an accessHash. */
26
+ export declare function anyAccessPasswordConfigured(usersFile: string): boolean;
27
+ /**
28
+ * True when users.json is present on disk but cannot be read or parsed. The
29
+ * gate's `isRemoteAuthConfigured` checks this so a transient read fault on a
30
+ * configured install fails CLOSED (stays "configured" → login screen, not the
31
+ * setup flow) instead of downgrading to "unconfigured". A genuinely absent or
32
+ * cleanly-empty store is NOT unreadable.
33
+ */
34
+ export declare function accessStoreUnreadable(usersFile: string): boolean;
35
+ export type MigrateResult = 'migrated' | 'noop-no-legacy' | 'noop-no-owner' | 'noop-owner-has-hash';
36
+ /**
37
+ * One-time, idempotent. If the legacy single-password file exists and the
38
+ * owner record has no accessHash, copy the legacy hash (already scrypt
39
+ * "salt:hash") onto the owner and delete the legacy file. If the owner already
40
+ * has an accessHash, the legacy file is stale → delete it. If no owner record
41
+ * exists yet, leave the legacy file untouched (verify still reads it as a
42
+ * candidate during this window — see readLegacyRemoteHash).
43
+ */
44
+ export declare function migrateLegacyRemotePassword(usersFile: string, legacyFile: string, ownerUserId: string): MigrateResult;
45
+ /** Read a still-present legacy single-password hash, or null. Used by verify
46
+ * as one additional candidate before the boot migration runs. */
47
+ export declare function readLegacyRemoteHash(legacyFile: string): string | null;
48
+ export { matchesHash as verifyHashCandidate };
49
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AAWD,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E;AAED,iBAAe,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS7E;AAiBD,mFAAmF;AACnF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM1G;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAQ3E;AAED,2DAA2D;AAC3D,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAStG;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,2DAA2D;AAC3D,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAItE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAGhE;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,gBAAgB,GAAG,eAAe,GAAG,qBAAqB,CAAA;AAEnG;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,aAAa,CAgBf;AAED;kEACkE;AAClE,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAItE;AAED,OAAO,EAAE,WAAW,IAAI,mBAAmB,EAAE,CAAA"}
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hashAccessPassword = hashAccessPassword;
4
+ exports.setAccessPassword = setAccessPassword;
5
+ exports.clearAccessPassword = clearAccessPassword;
6
+ exports.verifyAccessPassword = verifyAccessPassword;
7
+ exports.accessPasswordCollides = accessPasswordCollides;
8
+ exports.anyAccessPasswordConfigured = anyAccessPasswordConfigured;
9
+ exports.accessStoreUnreadable = accessStoreUnreadable;
10
+ exports.migrateLegacyRemotePassword = migrateLegacyRemotePassword;
11
+ exports.readLegacyRemoteHash = readLegacyRemoteHash;
12
+ exports.verifyHashCandidate = matchesHash;
13
+ // Per-admin device access passwords. Single owner of the `accessHash` field
14
+ // on users.json admin records. Imported by both platform/ui (src) and the
15
+ // admin MCP plugin (dist) — mirrors platform/lib/admins-write.
16
+ const node_crypto_1 = require("node:crypto");
17
+ const node_fs_1 = require("node:fs");
18
+ const SCRYPT_N = 16384;
19
+ const SCRYPT_R = 8;
20
+ const SCRYPT_P = 1;
21
+ const SCRYPT_KEYLEN = 64;
22
+ function scryptAsync(password, salt) {
23
+ return new Promise((resolve, reject) => {
24
+ (0, node_crypto_1.scrypt)(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (err, key) => {
25
+ if (err)
26
+ reject(err);
27
+ else
28
+ resolve(key);
29
+ });
30
+ });
31
+ }
32
+ async function hashAccessPassword(password) {
33
+ const salt = (0, node_crypto_1.randomBytes)(32);
34
+ const hash = await scryptAsync(password, salt);
35
+ return `${salt.toString('hex')}:${hash.toString('hex')}`;
36
+ }
37
+ async function matchesHash(password, stored) {
38
+ const colon = stored.indexOf(':');
39
+ if (colon === -1)
40
+ return false;
41
+ const salt = Buffer.from(stored.slice(0, colon), 'hex');
42
+ const storedHash = Buffer.from(stored.slice(colon + 1), 'hex');
43
+ if (storedHash.length !== SCRYPT_KEYLEN)
44
+ return false;
45
+ let computed;
46
+ try {
47
+ computed = await scryptAsync(password, salt);
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ return (0, node_crypto_1.timingSafeEqual)(computed, storedHash);
53
+ }
54
+ function readUsers(usersFile) {
55
+ if (!(0, node_fs_1.existsSync)(usersFile))
56
+ return [];
57
+ const raw = (0, node_fs_1.readFileSync)(usersFile, 'utf-8').trim();
58
+ if (!raw)
59
+ return [];
60
+ const parsed = JSON.parse(raw);
61
+ if (!Array.isArray(parsed))
62
+ throw new Error('users.json is not an array');
63
+ return parsed;
64
+ }
65
+ function writeUsersAtomic(usersFile, users) {
66
+ const tmp = `${usersFile}.tmp-${process.pid}`;
67
+ (0, node_fs_1.writeFileSync)(tmp, JSON.stringify(users, null, 2) + '\n', { mode: 0o600 });
68
+ (0, node_fs_1.renameSync)(tmp, usersFile);
69
+ }
70
+ /** Write `accessHash` onto the userId's record. Throws if the record is absent. */
71
+ async function setAccessPassword(userId, password, usersFile) {
72
+ const users = readUsers(usersFile);
73
+ const idx = users.findIndex(u => u.userId === userId);
74
+ if (idx === -1)
75
+ throw new Error(`no users.json record for userId=${userId}`);
76
+ users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) };
77
+ writeUsersAtomic(usersFile, users);
78
+ }
79
+ /** Remove `accessHash` from the userId's record. No-op if absent. */
80
+ function clearAccessPassword(userId, usersFile) {
81
+ const users = readUsers(usersFile);
82
+ const idx = users.findIndex(u => u.userId === userId);
83
+ if (idx === -1)
84
+ return;
85
+ const { accessHash, ...rest } = users[idx];
86
+ void accessHash;
87
+ users[idx] = rest;
88
+ writeUsersAtomic(usersFile, users);
89
+ }
90
+ /** Return the userId whose accessHash matches, or null. */
91
+ async function verifyAccessPassword(password, usersFile) {
92
+ let users;
93
+ try {
94
+ users = readUsers(usersFile);
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ for (const u of users) {
100
+ if (typeof u.accessHash === 'string' && (await matchesHash(password, u.accessHash))) {
101
+ return u.userId;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ /**
107
+ * True when some admin OTHER than excludeUserId already has an accessHash that
108
+ * matches `password`. Used by the set/change surfaces to refuse a value that
109
+ * would collide with another admin's perimeter credential — the scan-based
110
+ * verify attributes a login to the first matching record, so two admins sharing
111
+ * a password lets one silently overwrite the other. An unreadable store returns
112
+ * false: the subsequent write is what owns the read-fault error path.
113
+ */
114
+ async function accessPasswordCollides(password, usersFile, excludeUserId) {
115
+ let users;
116
+ try {
117
+ users = readUsers(usersFile);
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ for (const u of users) {
123
+ if (u.userId === excludeUserId)
124
+ continue;
125
+ if (typeof u.accessHash === 'string' && (await matchesHash(password, u.accessHash))) {
126
+ return true;
127
+ }
128
+ }
129
+ return false;
130
+ }
131
+ /** True when at least one record carries an accessHash. */
132
+ function anyAccessPasswordConfigured(usersFile) {
133
+ let users;
134
+ try {
135
+ users = readUsers(usersFile);
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ return users.some(u => typeof u.accessHash === 'string' && u.accessHash.length > 0);
141
+ }
142
+ /**
143
+ * True when users.json is present on disk but cannot be read or parsed. The
144
+ * gate's `isRemoteAuthConfigured` checks this so a transient read fault on a
145
+ * configured install fails CLOSED (stays "configured" → login screen, not the
146
+ * setup flow) instead of downgrading to "unconfigured". A genuinely absent or
147
+ * cleanly-empty store is NOT unreadable.
148
+ */
149
+ function accessStoreUnreadable(usersFile) {
150
+ if (!(0, node_fs_1.existsSync)(usersFile))
151
+ return false;
152
+ try {
153
+ readUsers(usersFile);
154
+ return false;
155
+ }
156
+ catch {
157
+ return true;
158
+ }
159
+ }
160
+ /**
161
+ * One-time, idempotent. If the legacy single-password file exists and the
162
+ * owner record has no accessHash, copy the legacy hash (already scrypt
163
+ * "salt:hash") onto the owner and delete the legacy file. If the owner already
164
+ * has an accessHash, the legacy file is stale → delete it. If no owner record
165
+ * exists yet, leave the legacy file untouched (verify still reads it as a
166
+ * candidate during this window — see readLegacyRemoteHash).
167
+ */
168
+ function migrateLegacyRemotePassword(usersFile, legacyFile, ownerUserId) {
169
+ if (!(0, node_fs_1.existsSync)(legacyFile))
170
+ return 'noop-no-legacy';
171
+ const legacy = (0, node_fs_1.readFileSync)(legacyFile, 'utf-8').trim();
172
+ const users = readUsers(usersFile);
173
+ const idx = users.findIndex(u => u.userId === ownerUserId);
174
+ if (idx === -1)
175
+ return 'noop-no-owner';
176
+ const existing = users[idx].accessHash;
177
+ if (typeof existing === 'string' && existing.length > 0) {
178
+ (0, node_fs_1.unlinkSync)(legacyFile);
179
+ return 'noop-owner-has-hash';
180
+ }
181
+ if (!legacy.includes(':')) {
182
+ (0, node_fs_1.unlinkSync)(legacyFile);
183
+ return 'noop-no-legacy';
184
+ }
185
+ users[idx] = { ...users[idx], accessHash: legacy };
186
+ writeUsersAtomic(usersFile, users);
187
+ (0, node_fs_1.unlinkSync)(legacyFile);
188
+ return 'migrated';
189
+ }
190
+ /** Read a still-present legacy single-password hash, or null. Used by verify
191
+ * as one additional candidate before the boot migration runs. */
192
+ function readLegacyRemoteHash(legacyFile) {
193
+ if (!(0, node_fs_1.existsSync)(legacyFile))
194
+ return null;
195
+ const raw = (0, node_fs_1.readFileSync)(legacyFile, 'utf-8').trim();
196
+ return raw.includes(':') ? raw : null;
197
+ }
198
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,176 @@
1
+ // Per-admin device access passwords. Single owner of the `accessHash` field
2
+ // on users.json admin records. Imported by both platform/ui (src) and the
3
+ // admin MCP plugin (dist) — mirrors platform/lib/admins-write.
4
+ import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto'
5
+ import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs'
6
+
7
+ const SCRYPT_N = 16384
8
+ const SCRYPT_R = 8
9
+ const SCRYPT_P = 1
10
+ const SCRYPT_KEYLEN = 64
11
+
12
+ export interface AccessUserEntry {
13
+ userId: string
14
+ pin: string
15
+ phone?: string
16
+ accessHash?: string
17
+ [k: string]: unknown
18
+ }
19
+
20
+ function scryptAsync(password: string, salt: Buffer): Promise<Buffer> {
21
+ return new Promise((resolve, reject) => {
22
+ scrypt(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (err, key) => {
23
+ if (err) reject(err)
24
+ else resolve(key)
25
+ })
26
+ })
27
+ }
28
+
29
+ export async function hashAccessPassword(password: string): Promise<string> {
30
+ const salt = randomBytes(32)
31
+ const hash = await scryptAsync(password, salt)
32
+ return `${salt.toString('hex')}:${hash.toString('hex')}`
33
+ }
34
+
35
+ async function matchesHash(password: string, stored: string): Promise<boolean> {
36
+ const colon = stored.indexOf(':')
37
+ if (colon === -1) return false
38
+ const salt = Buffer.from(stored.slice(0, colon), 'hex')
39
+ const storedHash = Buffer.from(stored.slice(colon + 1), 'hex')
40
+ if (storedHash.length !== SCRYPT_KEYLEN) return false
41
+ let computed: Buffer
42
+ try { computed = await scryptAsync(password, salt) } catch { return false }
43
+ return timingSafeEqual(computed, storedHash)
44
+ }
45
+
46
+ function readUsers(usersFile: string): AccessUserEntry[] {
47
+ if (!existsSync(usersFile)) return []
48
+ const raw = readFileSync(usersFile, 'utf-8').trim()
49
+ if (!raw) return []
50
+ const parsed = JSON.parse(raw)
51
+ if (!Array.isArray(parsed)) throw new Error('users.json is not an array')
52
+ return parsed as AccessUserEntry[]
53
+ }
54
+
55
+ function writeUsersAtomic(usersFile: string, users: AccessUserEntry[]): void {
56
+ const tmp = `${usersFile}.tmp-${process.pid}`
57
+ writeFileSync(tmp, JSON.stringify(users, null, 2) + '\n', { mode: 0o600 })
58
+ renameSync(tmp, usersFile)
59
+ }
60
+
61
+ /** Write `accessHash` onto the userId's record. Throws if the record is absent. */
62
+ export async function setAccessPassword(userId: string, password: string, usersFile: string): Promise<void> {
63
+ const users = readUsers(usersFile)
64
+ const idx = users.findIndex(u => u.userId === userId)
65
+ if (idx === -1) throw new Error(`no users.json record for userId=${userId}`)
66
+ users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) }
67
+ writeUsersAtomic(usersFile, users)
68
+ }
69
+
70
+ /** Remove `accessHash` from the userId's record. No-op if absent. */
71
+ export function clearAccessPassword(userId: string, usersFile: string): void {
72
+ const users = readUsers(usersFile)
73
+ const idx = users.findIndex(u => u.userId === userId)
74
+ if (idx === -1) return
75
+ const { accessHash, ...rest } = users[idx]
76
+ void accessHash
77
+ users[idx] = rest as AccessUserEntry
78
+ writeUsersAtomic(usersFile, users)
79
+ }
80
+
81
+ /** Return the userId whose accessHash matches, or null. */
82
+ export async function verifyAccessPassword(password: string, usersFile: string): Promise<string | null> {
83
+ let users: AccessUserEntry[]
84
+ try { users = readUsers(usersFile) } catch { return null }
85
+ for (const u of users) {
86
+ if (typeof u.accessHash === 'string' && (await matchesHash(password, u.accessHash))) {
87
+ return u.userId
88
+ }
89
+ }
90
+ return null
91
+ }
92
+
93
+ /**
94
+ * True when some admin OTHER than excludeUserId already has an accessHash that
95
+ * matches `password`. Used by the set/change surfaces to refuse a value that
96
+ * would collide with another admin's perimeter credential — the scan-based
97
+ * verify attributes a login to the first matching record, so two admins sharing
98
+ * a password lets one silently overwrite the other. An unreadable store returns
99
+ * false: the subsequent write is what owns the read-fault error path.
100
+ */
101
+ export async function accessPasswordCollides(
102
+ password: string,
103
+ usersFile: string,
104
+ excludeUserId: string,
105
+ ): Promise<boolean> {
106
+ let users: AccessUserEntry[]
107
+ try { users = readUsers(usersFile) } catch { return false }
108
+ for (const u of users) {
109
+ if (u.userId === excludeUserId) continue
110
+ if (typeof u.accessHash === 'string' && (await matchesHash(password, u.accessHash))) {
111
+ return true
112
+ }
113
+ }
114
+ return false
115
+ }
116
+
117
+ /** True when at least one record carries an accessHash. */
118
+ export function anyAccessPasswordConfigured(usersFile: string): boolean {
119
+ let users: AccessUserEntry[]
120
+ try { users = readUsers(usersFile) } catch { return false }
121
+ return users.some(u => typeof u.accessHash === 'string' && u.accessHash.length > 0)
122
+ }
123
+
124
+ /**
125
+ * True when users.json is present on disk but cannot be read or parsed. The
126
+ * gate's `isRemoteAuthConfigured` checks this so a transient read fault on a
127
+ * configured install fails CLOSED (stays "configured" → login screen, not the
128
+ * setup flow) instead of downgrading to "unconfigured". A genuinely absent or
129
+ * cleanly-empty store is NOT unreadable.
130
+ */
131
+ export function accessStoreUnreadable(usersFile: string): boolean {
132
+ if (!existsSync(usersFile)) return false
133
+ try { readUsers(usersFile); return false } catch { return true }
134
+ }
135
+
136
+ export type MigrateResult = 'migrated' | 'noop-no-legacy' | 'noop-no-owner' | 'noop-owner-has-hash'
137
+
138
+ /**
139
+ * One-time, idempotent. If the legacy single-password file exists and the
140
+ * owner record has no accessHash, copy the legacy hash (already scrypt
141
+ * "salt:hash") onto the owner and delete the legacy file. If the owner already
142
+ * has an accessHash, the legacy file is stale → delete it. If no owner record
143
+ * exists yet, leave the legacy file untouched (verify still reads it as a
144
+ * candidate during this window — see readLegacyRemoteHash).
145
+ */
146
+ export function migrateLegacyRemotePassword(
147
+ usersFile: string,
148
+ legacyFile: string,
149
+ ownerUserId: string,
150
+ ): MigrateResult {
151
+ if (!existsSync(legacyFile)) return 'noop-no-legacy'
152
+ const legacy = readFileSync(legacyFile, 'utf-8').trim()
153
+ const users = readUsers(usersFile)
154
+ const idx = users.findIndex(u => u.userId === ownerUserId)
155
+ if (idx === -1) return 'noop-no-owner'
156
+ const existing = users[idx].accessHash
157
+ if (typeof existing === 'string' && existing.length > 0) {
158
+ unlinkSync(legacyFile)
159
+ return 'noop-owner-has-hash'
160
+ }
161
+ if (!legacy.includes(':')) { unlinkSync(legacyFile); return 'noop-no-legacy' }
162
+ users[idx] = { ...users[idx], accessHash: legacy }
163
+ writeUsersAtomic(usersFile, users)
164
+ unlinkSync(legacyFile)
165
+ return 'migrated'
166
+ }
167
+
168
+ /** Read a still-present legacy single-password hash, or null. Used by verify
169
+ * as one additional candidate before the boot migration runs. */
170
+ export function readLegacyRemoteHash(legacyFile: string): string | null {
171
+ if (!existsSync(legacyFile)) return null
172
+ const raw = readFileSync(legacyFile, 'utf-8').trim()
173
+ return raw.includes(':') ? raw : null
174
+ }
175
+
176
+ export { matchesHash as verifyHashCandidate }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }