chainlesschain 0.166.91 → 0.166.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/package.json +3 -3
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DgV7xb8F.js → AIOps-C74YzqwJ.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CgANpkKu.js → ActionButton-TFef1hOE.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DnvpX7RB.js → Analytics-CYsWpkkU.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-C2bHccBM.js → AppLayout-CFf9kjwu.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BIhijC9g.js → Artifacts-TQqa94YO.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CKRjWGdp.js → Audit-B79ayqfS.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CblYtgBE.js → BackgroundAgents-pjkHfLP9.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cq35b78a.js → Backup-CE6A-t_r.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DcB7NoRz.js → BaseInput-CwcgWMBY.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-tc3b-s7i.js → Chat-BIkDNesY.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DgaokYts.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-BLD5bH5u.js → Checkbox-MbypIy5V.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BLOsoazN.js → Codegen-DiFHWDnt.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Bdr7JwIx.js → Col-BNMlYMso.js} +1 -1
- package/src/assets/web-panel/assets/{Community-C84BsWE0.js → Community-B3HVyHOX.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Dzdwd04z.js → Compact-C7hlAubb.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CCG-R0EV.js → Compliance-PaXZu6sE.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYj72SK0.js → Cowork-BGiojbm-.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-B6iKAwOf.js → Cron-Bmr-1QAx.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BJ-oDC6u.js → Crosschain-BwUvWCVd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BnMckGLr.js → DID-k_4QZocW.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Cyje-gCO.js → Dashboard-_ieRo2C5.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BpNlenjK.js → Dropdown-DYHh6T27.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BhonO7Nv.js → EmailListRenderer-BNlQcWQX.js} +1 -1
- package/src/assets/web-panel/assets/{EvolutionSettings-DVoNU-Id.js → EvolutionSettings-DIkZ6U2D.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-tdn-KC67.js → FamilyGuardDashboard-Dq7qSr0P.js} +1 -1
- package/src/assets/web-panel/assets/{Federation--EEVrHhz.js → Federation-C64e3Cbx.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cy8zr0-4.js → FormItemContext-D-GE2vUa.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BuUhpgFi.js → GenericCardRenderer-DO3B2gis.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Ctd3E5Pe.js → Git-0ETirIQH.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-Ce7dV6aU.js → Governance-C9fnRygX.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CD1vExIH.js → Inference-CLJ9RTsK.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Dn64OdQ7.js → KnowledgeGraph-D3C_GxrN.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-PmElVRrT.js → Logs-C5ctGbkN.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-Ce-eWK3j.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-OW4jTXwx.js → Marketplace-GF_t6hJ_.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BEq2Ib4x.js → McpTools-BGsGzgQs.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-Can_P2oK.js → Memory-CcrY2A0j.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DJ8R-dn3.js → MobileBridge-Bi4T0RlS.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-B7kV9GOt.js → MobileProjects-DvjEGyvf.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BfxamolS.js → Mtc-DL77Vf-k.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DHtuv7av.js → MtcAudit-CDy_Uswb.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DLkLpvxc.js → Multisig-DL0BAO_c.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-u4XtJTIt.js → NLProgramming-B-woh5IR.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BwuuPJLS.js → Notes-CEFVCxk7.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-HEeu-E9v.js → NotificationSettings-BqPPMuAP.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BtvN-Qwh.js → OrderTableRenderer-GUKQ1xwp.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-Yh-czs3b.js → Organization-B1gX8jlr.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DmE8mYVM.js → Overflow-CeMxuUZy.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CWVck8WN.js → P2P-B5aIRWwJ.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DgFqekod.js → PdhVaultBrowser-6rYgejd4.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BtJBVOHV.js → Permissions-CqCp2SIl.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-6VBObXzd.js → PersonalDataHub-BsA7DJWW.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-D222FmqY.js → Pipeline-CiW6HeZh.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bi9FMikp.js → Privacy-DckUOLYq.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Bo52H47H.js → ProjectInit-CZppOOYR.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BPkesfOc.js → ProjectSettings-B9QPzhDU.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-GynohSFX.js → Projects-BErjjGha.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DVdMg04n.js → Providers-DehU6Qiz.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DqJCb595.js → QrScannerModal-E1SUvv1m.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-J7loQbXi.js → QuickAsk-COWFxyH3.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BI5aURVc.js → Recommend-D26pWKxX.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BN2sqGvz.js → RemoteSession-DCOIaId3.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BGOND-Fi.js → Reputation-DT_AHGAR.js} +1 -1
- package/src/assets/web-panel/assets/{Row-77dfwaUk.js → Row-B50S-1nl.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-FtBAxS3g.js → RssFeed-BaBnHg3Q.js} +2 -2
- package/src/assets/web-panel/assets/{Search-BaYVLFb7.js → Search-BVdnLuuS.js} +1 -1
- package/src/assets/web-panel/assets/{Security-LE3Vv6ab.js → Security-BnacH74J.js} +3 -3
- package/src/assets/web-panel/assets/{Services-C5pP-Whz.js → Services-CxM08Zch.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CLqKUVsF.js → Skeleton-DE7CIc1N.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BPjN0SX5.js → Skills-CGI0SJmf.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-14jD70kg.js → Sla-DpEzPcqc.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Dr9ij42W.js → SpeechSettings-CTmnmMHn.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BzLHP7B8.js → SyncSettings-Dxgrd9hd.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-YqV9b0rV.js → Tasks-CW4slS0A.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CxxAE4PK.js → Templates-DxvGWDgB.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CRb3PcKJ.js → Tenant-_Q0o2tNK.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CTzPpBaP.js → Terminal-BTZn8pow.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DjFUj_hy.js → TimelineRenderer-CauL0KFe.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Bbb8tPvx.js → Tokens-CDoM2qeT.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CSeWGM0W.js → Trigger-B-QYGEU7.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-JgESMBo3.js → Trust-BD6XJHT-.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CiObLk0T.js → UkeySign-x4TFwCy0.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CFEZaQ2x.js → VideoEditing-t3imBK_0.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DR8ZGDNn.js → Wallet-BCfkWwNC.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DxZukvu7.js → WebAuthn-WH314Mdm.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-CKgSZQwn.js → WorkflowEditor-jUlcjrUg.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CTxEQE66.js → chat-Dg3VASTL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CAjKMUu3.js → colors-BNvmBRKE.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BXuz49at.js → compact-item-BGOfoqf4.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CFN_Hwr9.js → createContext-BNQeqKAs.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-B7_ypN1G.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BwoSBSfP.js → hasIn-Bg3AjCrq.js} +1 -1
- package/src/assets/web-panel/assets/{index-u9_DM3ZC.js → index--I3UF5t1.js} +1 -1
- package/src/assets/web-panel/assets/{index-IahILygB.js → index-2D04Zltx.js} +3 -3
- package/src/assets/web-panel/assets/{index-Bqv_Cray.js → index-60AnjUTX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSmFby3Q.js → index-7QJUWemf.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTX9CJPx.js → index-B--0QM0S.js} +1 -1
- package/src/assets/web-panel/assets/index-B-jSKJK6.js +1 -0
- package/src/assets/web-panel/assets/{index-C8er4W9u.js → index-B0mgngoB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DViK2bkv.js → index-BBCMulQ-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmkbAADP.js → index-BSNhW95G.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2tEnBPx.js → index-B_JmInC0.js} +1 -1
- package/src/assets/web-panel/assets/{index-_ZtCyUdz.js → index-BaPvEvB4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CN53dE_u.js → index-Bkz1tgFR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bj4hIiLW.js → index-BmTacaQV.js} +1 -1
- package/src/assets/web-panel/assets/{index-Tz5dheZa.js → index-CCFZN66A.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dv7V2kg6.js → index-COol3xfA.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bu2P6u8m.js → index-CY8cRKHR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bw9s2d3c.js → index-Cbp-wp-U.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9yR7zqr.js → index-Co_LnCWD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BK4ftQ4d.js → index-Cx2EXRfr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxunxaTw.js → index-D-mSvCQK.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7kp-zRE.js → index-D072oLjp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB3kVd9c.js → index-D6UqK83L.js} +1 -1
- package/src/assets/web-panel/assets/{index-CF9lWi1o.js → index-DF_FKSVH.js} +1 -1
- package/src/assets/web-panel/assets/{index-IY9bd5az.js → index-DLwEXRrb.js} +1 -1
- package/src/assets/web-panel/assets/{index-F2E5RCwz.js → index-DO0VlNtc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2o_BBjg.js → index-DS04wRll.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY-gBSm8.js → index-DYn5DnDk.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0QbLlWn.js → index-D_O2ZjPk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjF5BOZx.js → index-DcCnO0RU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CehdwJ_Q.js → index-Dkp-okWa.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmXpdcHI.js → index-OUFoxUgH.js} +1 -1
- package/src/assets/web-panel/assets/index-P4FXZJqI.js +1 -0
- package/src/assets/web-panel/assets/{index-t2Ch_JKn.js → index-Wo0f2iqb.js} +1 -1
- package/src/assets/web-panel/assets/{index-T3MTE6jB.js → index-ZD5Kd8tx.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cp5y7lX_.js → index-fOwii3LP.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cgk0YEiq.js → index-hG7jXGEW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpdDnZX_.js → index-pVdQieac.js} +1 -1
- package/src/assets/web-panel/assets/{index-o9DpkZKf.js → index-uH2LnAs_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmQITjMU.js → index-xzaUx19r.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BoiGJDoO.js → initDefaultProps-B8PN9AxJ.js} +1 -1
- package/src/assets/web-panel/assets/{motion-5eL4E0pA.js → motion-BHivfw5Q.js} +1 -1
- package/src/assets/web-panel/assets/{move-DjggDi_1.js → move-B0QU5nDd.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-Ce8Vo2T3.js → mtc-parser-DRwRM3PE.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D1yOBaO2.js → omit-SHt2rV2p.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-8NLceQe8.js → pickAttrs-CcmDlEb2.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CsIdBRmQ.js → placementArrow-DYatmXDS.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-GM5NE8En.js → responsiveObserve-B7qrsXq5.js} +1 -1
- package/src/assets/web-panel/assets/{slide-44yj3u49.js → slide-BRGhlOWd.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-B8tqtWcX.js → statusUtils-CT-7NXsQ.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-NhDPBbyB.js → styleChecker-rZdw_Yyp.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-C8Qz1GtN.js → useFlexGapSupport-D8YNIP4P.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-ClUv4kih.js → useFs-B8pt0Gu-.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BwkOVEh6.js → usePersonalDataHub-VMmYM1tt.js} +1 -1
- package/src/assets/web-panel/assets/{vnode--gMF33R0.js → vnode-UNDuoPoq.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DXg3TqOP.js → zoom-dN-u667S.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/data/changelog.json +39 -0
- package/src/lib/evolution/evolution-artifact-ports.js +26 -0
- package/src/lib/evolution/evolution-eval-cohort-enrollment.js +713 -47
- package/src/lib/evolution/evolution-eval-gate.js +146 -0
- package/src/lib/evolution/evolution-eval-launch-admission.js +168 -90
- package/src/lib/evolution/evolution-eval-ledger-capture.js +50 -0
- package/src/lib/evolution/evolution-eval-readonly-audit.js +106 -0
- package/src/lib/evolution/evolution-ledger-file-backend.js +20 -0
- package/src/lib/evolution/evolution-ledger-ports.js +59 -27
- package/src/lib/evolution/evolution-ledger-v2-journal.js +55 -18
- package/src/lib/evolution/evolution-workbench-file-resources.js +19 -1
- package/src/lib/evolution/rrsi-cohort-registration.js +275 -0
- package/src/lib/evolution/rrsi-contracts.js +742 -0
- package/src/lib/evolution/rrsi-data.js +196 -0
- package/src/lib/evolution/rrsi-evaluation-adapter.js +275 -0
- package/src/lib/evolution/rrsi-evaluation-variants.js +267 -0
- package/src/lib/evolution/rrsi-group-statistics.js +553 -0
- package/src/lib/evolution/rrsi-history-ledger-adapter.js +3261 -0
- package/src/lib/evolution/rrsi-native-batch-evidence.js +520 -0
- package/src/lib/evolution/rrsi-native-eval-row-collector.js +538 -0
- package/src/lib/evolution/rrsi-native-evaluation-batch.js +547 -0
- package/src/lib/evolution/rrsi-native-evaluation-plan.js +431 -0
- package/src/lib/evolution/rrsi-native-group-statistics-plan.js +121 -0
- package/src/lib/evolution/rrsi-native-group-statistics.js +482 -0
- package/src/lib/evolution/rrsi-native-quality-receipt-contracts.js +321 -0
- package/src/lib/evolution/rrsi-native-quality-receipt.js +256 -0
- package/src/lib/evolution/rrsi-native-statistics-protocol.js +330 -0
- package/src/lib/evolution/rrsi-parent-binding.js +290 -0
- package/src/lib/evolution/rrsi-pm-execution-bridge.js +500 -0
- package/src/lib/evolution/rrsi-pm-training-mapping.js +134 -0
- package/src/lib/evolution/rrsi-preparation-contracts.js +213 -0
- package/src/lib/evolution/rrsi-registry-history-binding.js +193 -0
- package/src/lib/evolution/rrsi-registry-store-policy.js +1191 -0
- package/src/lib/evolution/rrsi-runtime-composition.js +79 -0
- package/src/lib/evolution/rrsi-selector.js +434 -0
- package/src/lib/evolution/rrsi-statistics-simulation.js +354 -0
- package/src/lib/evolution/skill-candidate-registry.js +216 -29
- package/src/lib/evolution/skill-registry-maintenance.js +80 -0
- package/src/lib/evolution/skill-registry-writer-control.js +138 -0
- package/src/lib/evolution/skill-release-registry.js +399 -169
- package/src/lib/formal-quality-eval-runtime.js +2 -14
- package/src/lib/goal-store.js +225 -16
- package/src/lib/record-replay/skill-recorder.js +38 -3
- package/src/lib/scheduler-kernel/authority-resolver.js +12 -163
- package/src/lib/scheduler-kernel/contract.js +27 -414
- package/src/lib/scheduler-kernel/runtime.js +21 -823
- package/src/lib/scheduler-kernel/service.js +11 -328
- package/src/lib/scheduler-kernel/source-locator-path.js +7 -142
- package/src/lib/scheduler-kernel/store.js +67 -5931
- package/src/lib/secure-fs.js +16 -1314
- package/src/lib/with-file-lock.js +122 -20
- package/src/assets/web-panel/assets/ChatBubbleRenderer-fcc_Tvqu.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-BFiVKf5t.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CfB8DxT2.js +0 -1
- package/src/assets/web-panel/assets/index-CCsRfd0k.js +0 -1
- package/src/assets/web-panel/assets/index-CsmiZIQ4.js +0 -1
package/src/lib/secure-fs.js
CHANGED
|
@@ -1,1314 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
function samePath(left, right, caseInsensitive = false) {
|
|
18
|
-
const trim = (value) => String(value).replace(/[\\/]+$/, "");
|
|
19
|
-
const a = trim(left);
|
|
20
|
-
const b = trim(right);
|
|
21
|
-
return caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Guard every ACL/chmod repair entry point, not just paths.js callers. */
|
|
25
|
-
export function assertSafeOwnerOnlyPath(target, platform = process.platform) {
|
|
26
|
-
const value = String(target || "").trim();
|
|
27
|
-
if (!value) throw new Error("Owner-only storage path must not be empty");
|
|
28
|
-
const pathApi =
|
|
29
|
-
platform === "win32"
|
|
30
|
-
? win32
|
|
31
|
-
: platform === "darwin" || platform === "linux"
|
|
32
|
-
? posix
|
|
33
|
-
: { isAbsolute: isNativeAbsolute, parse, resolve };
|
|
34
|
-
const native = pathApi.resolve(value);
|
|
35
|
-
const nativeRoot = pathApi.parse(native).root;
|
|
36
|
-
const posixPath = posix.isAbsolute(value) ? posix.normalize(value) : null;
|
|
37
|
-
const windowsPath = win32.isAbsolute(value) ? win32.normalize(value) : null;
|
|
38
|
-
const isAbsolute = pathApi.isAbsolute(value);
|
|
39
|
-
const windowsLiteral = value.replaceAll("/", "\\");
|
|
40
|
-
const normalizedWindowsLiteral = String(windowsPath || windowsLiteral);
|
|
41
|
-
const extendedWindowsRoot =
|
|
42
|
-
/^\\\\\?\\[A-Za-z]:\\?$/.test(normalizedWindowsLiteral) ||
|
|
43
|
-
/^\\\\\?\\UNC\\[^\\]+\\[^\\]+\\?$/.test(normalizedWindowsLiteral);
|
|
44
|
-
const normalizedWindowsLower = normalizedWindowsLiteral.toLowerCase();
|
|
45
|
-
const forbiddenWindowsDevice =
|
|
46
|
-
normalizedWindowsLower.startsWith("\\\\?\\") ||
|
|
47
|
-
normalizedWindowsLower.startsWith("\\\\.\\");
|
|
48
|
-
const isRoot =
|
|
49
|
-
samePath(native, nativeRoot, platform === "win32") ||
|
|
50
|
-
(posixPath !== null && samePath(posixPath, posix.parse(posixPath).root)) ||
|
|
51
|
-
(windowsPath !== null &&
|
|
52
|
-
samePath(windowsPath, win32.parse(windowsPath).root, true)) ||
|
|
53
|
-
/^[A-Za-z]:$/.test(value) ||
|
|
54
|
-
extendedWindowsRoot ||
|
|
55
|
-
forbiddenWindowsDevice;
|
|
56
|
-
if (isRoot) {
|
|
57
|
-
const error = new Error(
|
|
58
|
-
`Refusing owner-only permission changes on a filesystem root: ${value}`,
|
|
59
|
-
);
|
|
60
|
-
error.code = "CONFIG_HOME_UNSAFE";
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
const caseInsensitive = ["win32", "darwin"].includes(platform);
|
|
64
|
-
const protectedLocation = samePath(
|
|
65
|
-
native,
|
|
66
|
-
pathApi.resolve(homedir()),
|
|
67
|
-
caseInsensitive,
|
|
68
|
-
);
|
|
69
|
-
if (protectedLocation || !isAbsolute) {
|
|
70
|
-
const error = new Error(
|
|
71
|
-
`Refusing owner-only permission changes on a broad protected directory: ${value}`,
|
|
72
|
-
);
|
|
73
|
-
error.code = "CONFIG_HOME_UNSAFE";
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
return target;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const TRUSTED_DARWIN_SYSTEM_ALIASES = new Map([["/var", "/private/var"]]);
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* macOS exposes its real temporary-directory tree through the root-owned
|
|
83
|
-
* `/var -> private/var` compatibility alias. Rejecting that immutable system
|
|
84
|
-
* alias makes every `os.tmpdir()`-backed session fail, while accepting generic
|
|
85
|
-
* links would re-open the chmod/DACL traversal attack this guard prevents.
|
|
86
|
-
*
|
|
87
|
-
* Keep the exception deliberately narrow: exact alias, exact canonical target,
|
|
88
|
-
* root ownership on both entries, and a non-writable canonical directory.
|
|
89
|
-
*/
|
|
90
|
-
function isTrustedDarwinSystemAlias(current, entry, deps, platform) {
|
|
91
|
-
if (platform !== "darwin") return false;
|
|
92
|
-
const expected = TRUSTED_DARWIN_SYSTEM_ALIASES.get(current);
|
|
93
|
-
if (!expected || Number(entry?.uid) !== 0) return false;
|
|
94
|
-
try {
|
|
95
|
-
const canonical = deps.realpathSync(current);
|
|
96
|
-
if (!samePath(canonical, expected)) return false;
|
|
97
|
-
const target = deps.lstatSync(canonical);
|
|
98
|
-
const targetMode = Number(target.mode);
|
|
99
|
-
return (
|
|
100
|
-
!target.isSymbolicLink() &&
|
|
101
|
-
target.isDirectory() &&
|
|
102
|
-
Number(target.uid) === 0 &&
|
|
103
|
-
Number.isInteger(targetMode) &&
|
|
104
|
-
(targetMode & 0o022) === 0
|
|
105
|
-
);
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function readWindowsDirectoryEntry(target, deps) {
|
|
112
|
-
if (typeof deps.readdirSync !== "function") return null;
|
|
113
|
-
const parent = win32.dirname(target);
|
|
114
|
-
if (!parent || samePath(parent, target, true)) return null;
|
|
115
|
-
let entries;
|
|
116
|
-
try {
|
|
117
|
-
entries = deps.readdirSync(parent, { withFileTypes: true });
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
if (!Array.isArray(entries)) return null;
|
|
122
|
-
const name = win32.basename(target);
|
|
123
|
-
const matches = entries.filter(
|
|
124
|
-
(candidate) => String(candidate?.name || "") === name,
|
|
125
|
-
);
|
|
126
|
-
// A successful complete directory enumeration is positive evidence that the
|
|
127
|
-
// child is absent. A case-folded match is not absence on Windows; preserve it
|
|
128
|
-
// as ambiguous so native verification remains fail-closed.
|
|
129
|
-
if (matches.length === 0) {
|
|
130
|
-
const caseFoldedMatches = entries.filter(
|
|
131
|
-
(candidate) =>
|
|
132
|
-
String(candidate?.name || "").toLowerCase() === name.toLowerCase(),
|
|
133
|
-
);
|
|
134
|
-
if (caseFoldedMatches.length === 0) return { exists: false };
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
if (matches.length !== 1) return null;
|
|
138
|
-
const [entry] = matches;
|
|
139
|
-
if (
|
|
140
|
-
typeof entry.isSymbolicLink !== "function" ||
|
|
141
|
-
typeof entry.isDirectory !== "function" ||
|
|
142
|
-
typeof entry.isFile !== "function"
|
|
143
|
-
) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
const isSymbolicLink = Boolean(entry.isSymbolicLink());
|
|
148
|
-
const isDirectory = Boolean(entry.isDirectory());
|
|
149
|
-
const isFile = Boolean(entry.isFile());
|
|
150
|
-
if (!isSymbolicLink && isDirectory === isFile) return null;
|
|
151
|
-
return {
|
|
152
|
-
exists: true,
|
|
153
|
-
isDirectory,
|
|
154
|
-
isFile,
|
|
155
|
-
isSymbolicLink,
|
|
156
|
-
};
|
|
157
|
-
} catch {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function windowsUnreliablePathStatError(target, cause) {
|
|
163
|
-
const error = new Error(
|
|
164
|
-
`Windows path metadata could not reliably classify an existing path: ${target}`,
|
|
165
|
-
cause ? { cause } : undefined,
|
|
166
|
-
);
|
|
167
|
-
error.code = "WINDOWS_PATH_STAT_UNRELIABLE";
|
|
168
|
-
return error;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function assertNoLinkTraversal(
|
|
172
|
-
target,
|
|
173
|
-
deps = fs,
|
|
174
|
-
checkedPaths = null,
|
|
175
|
-
platform = process.platform,
|
|
176
|
-
) {
|
|
177
|
-
// Dependency-injected cross-platform tests must walk the target platform's
|
|
178
|
-
// path grammar, not the host running Vitest (for example `/var` on Windows).
|
|
179
|
-
const pathApi =
|
|
180
|
-
platform === "win32"
|
|
181
|
-
? win32
|
|
182
|
-
: platform === "darwin" || platform === "linux"
|
|
183
|
-
? posix
|
|
184
|
-
: { resolve, parse };
|
|
185
|
-
const absolute = pathApi.resolve(String(target));
|
|
186
|
-
const root = pathApi.parse(absolute).root;
|
|
187
|
-
const segments = absolute
|
|
188
|
-
.slice(root.length)
|
|
189
|
-
.split(/[\\/]+/)
|
|
190
|
-
.filter(Boolean);
|
|
191
|
-
let current = root;
|
|
192
|
-
for (const segment of segments) {
|
|
193
|
-
current = pathApi.resolve(current, segment);
|
|
194
|
-
const cacheKey =
|
|
195
|
-
process.platform === "win32" ? current.toLowerCase() : current;
|
|
196
|
-
if (checkedPaths?.has(cacheKey)) continue;
|
|
197
|
-
let entry;
|
|
198
|
-
try {
|
|
199
|
-
entry = deps.lstatSync(current);
|
|
200
|
-
} catch (error) {
|
|
201
|
-
if (
|
|
202
|
-
platform === "win32" &&
|
|
203
|
-
["ENOENT", "EPERM", "EACCES"].includes(error?.code)
|
|
204
|
-
) {
|
|
205
|
-
const directoryEntry = readWindowsDirectoryEntry(current, deps);
|
|
206
|
-
if (directoryEntry?.exists === false) break;
|
|
207
|
-
if (directoryEntry) {
|
|
208
|
-
if (directoryEntry.isSymbolicLink) {
|
|
209
|
-
const unsafe = new Error(
|
|
210
|
-
`Refusing owner-only permission changes through a symbolic link or junction: ${current}`,
|
|
211
|
-
);
|
|
212
|
-
unsafe.code = "CONFIG_HOME_UNSAFE";
|
|
213
|
-
throw unsafe;
|
|
214
|
-
}
|
|
215
|
-
checkedPaths?.add(cacheKey);
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
throw windowsUnreliablePathStatError(current, error);
|
|
219
|
-
}
|
|
220
|
-
if (error?.code === "ENOENT") break;
|
|
221
|
-
throw error;
|
|
222
|
-
}
|
|
223
|
-
if (
|
|
224
|
-
entry.isSymbolicLink() &&
|
|
225
|
-
!isTrustedDarwinSystemAlias(current, entry, deps, platform)
|
|
226
|
-
) {
|
|
227
|
-
const error = new Error(
|
|
228
|
-
`Refusing owner-only permission changes through a symbolic link or junction: ${current}`,
|
|
229
|
-
);
|
|
230
|
-
error.code = "CONFIG_HOME_UNSAFE";
|
|
231
|
-
throw error;
|
|
232
|
-
}
|
|
233
|
-
checkedPaths?.add(cacheKey);
|
|
234
|
-
}
|
|
235
|
-
return target;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Reapplying an unchanged directory DACL propagates to existing children on
|
|
239
|
-
// Windows and changes their ctime, invalidating otherwise intact session
|
|
240
|
-
// witnesses. Serialize check-and-repair across processes and skip a verified
|
|
241
|
-
// owner-only ACL. Keep the post-repair inspection and all traversal guards.
|
|
242
|
-
const WINDOWS_ACL_IDEMPOTENT_REPAIR = String.raw`
|
|
243
|
-
function Test-CcOwnerOnlySecurity($item, $security) {
|
|
244
|
-
$owner = $security.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
|
245
|
-
$rules = @($security.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))
|
|
246
|
-
if ($owner -ne $sid.Value -or $rules.Count -ne 1 -or ($item.PSIsContainer -and -not $security.AreAccessRulesProtected)) { return $false }
|
|
247
|
-
$rule = $rules[0]
|
|
248
|
-
$inheritance = [System.Security.AccessControl.InheritanceFlags]::None
|
|
249
|
-
if ($item.PSIsContainer) {
|
|
250
|
-
$inheritance = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
251
|
-
}
|
|
252
|
-
return ($rule.IdentityReference.Value -eq $sid.Value -and
|
|
253
|
-
$rule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and
|
|
254
|
-
(-not $item.PSIsContainer -or -not $rule.IsInherited) -and
|
|
255
|
-
$rule.InheritanceFlags -eq $inheritance -and
|
|
256
|
-
$rule.PropagationFlags -eq [System.Security.AccessControl.PropagationFlags]::None -and
|
|
257
|
-
($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl)
|
|
258
|
-
}
|
|
259
|
-
function Write-CcOwnerOnlyAcl($item, [string]$path) {
|
|
260
|
-
$script:ccAclStage = 'repair-lock'
|
|
261
|
-
[Console]::Error.WriteLine('CC_WINDOWS_ACL_STAGE=repair-lock')
|
|
262
|
-
$digest = [System.Security.Cryptography.SHA256]::Create()
|
|
263
|
-
try {
|
|
264
|
-
$key = [System.BitConverter]::ToString($digest.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($sid.Value + ':' + [System.IO.Path]::GetFullPath($path).ToUpperInvariant()))).Replace('-', '')
|
|
265
|
-
} finally { $digest.Dispose() }
|
|
266
|
-
$mutex = [System.Threading.Mutex]::new($false, ('Global\ChainlessChain.PrivateAcl.' + $key))
|
|
267
|
-
$held = $false
|
|
268
|
-
try {
|
|
269
|
-
try { $held = $mutex.WaitOne(10000) }
|
|
270
|
-
catch [System.Threading.AbandonedMutexException] { $held = $true }
|
|
271
|
-
if (-not $held) { throw 'Timed out waiting for owner-only ACL repair' }
|
|
272
|
-
Assert-CcNoReparseTraversal $path
|
|
273
|
-
$script:ccAclStage = 'repair-inspect'
|
|
274
|
-
[Console]::Error.WriteLine('CC_WINDOWS_ACL_STAGE=repair-inspect')
|
|
275
|
-
$current = Get-Item -LiteralPath $path -Force
|
|
276
|
-
$security = Read-CcAcl $path
|
|
277
|
-
if (Test-CcOwnerOnlySecurity $current $security) { return }
|
|
278
|
-
$script:ccAclStage = 'repair-write'
|
|
279
|
-
[Console]::Error.WriteLine('CC_WINDOWS_ACL_STAGE=repair-write')
|
|
280
|
-
Write-CcOwnerOnlyAclImpl $current $path
|
|
281
|
-
} finally {
|
|
282
|
-
if ($held) { $mutex.ReleaseMutex() }
|
|
283
|
-
$mutex.Dispose()
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
`;
|
|
287
|
-
|
|
288
|
-
const WINDOWS_ACL_SCRIPT = String.raw`
|
|
289
|
-
param([string]$target, [string]$operation)
|
|
290
|
-
$ErrorActionPreference = 'Stop'
|
|
291
|
-
$script:ccAclStage = 'initialize'
|
|
292
|
-
try {
|
|
293
|
-
[Console]::Error.WriteLine('CC_WINDOWS_ACL_STAGE=initialize')
|
|
294
|
-
$utf8 = [System.Text.UTF8Encoding]::new($false)
|
|
295
|
-
[Console]::OutputEncoding = $utf8
|
|
296
|
-
$sections =
|
|
297
|
-
[System.Security.AccessControl.AccessControlSections]::Access -bor
|
|
298
|
-
[System.Security.AccessControl.AccessControlSections]::Owner
|
|
299
|
-
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
300
|
-
$sid = $identity.User
|
|
301
|
-
$tokenOwner = $identity.Owner
|
|
302
|
-
|
|
303
|
-
function Assert-CcNoReparseTraversal([string]$path) {
|
|
304
|
-
$cursor = [System.IO.Path]::GetFullPath($path)
|
|
305
|
-
while (-not (Test-Path -LiteralPath $cursor)) {
|
|
306
|
-
$parent = [System.IO.Directory]::GetParent($cursor)
|
|
307
|
-
if ($null -eq $parent) { return }
|
|
308
|
-
$cursor = $parent.FullName
|
|
309
|
-
}
|
|
310
|
-
$entry = Get-Item -LiteralPath $cursor -Force
|
|
311
|
-
while ($null -ne $entry) {
|
|
312
|
-
if (($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
313
|
-
throw "symbolic links and reparse points are not allowed: $($entry.FullName)"
|
|
314
|
-
}
|
|
315
|
-
$entry = $entry.Parent
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
$script:ccAclStage = 'traversal'
|
|
320
|
-
[Console]::Error.WriteLine('CC_WINDOWS_ACL_STAGE=traversal')
|
|
321
|
-
Assert-CcNoReparseTraversal $target
|
|
322
|
-
$script:ccAclStage = 'lookup'
|
|
323
|
-
[Console]::Error.WriteLine('CC_WINDOWS_ACL_STAGE=lookup')
|
|
324
|
-
$item = Get-Item -LiteralPath $target -Force
|
|
325
|
-
|
|
326
|
-
function Read-CcAcl([string]$path) {
|
|
327
|
-
if ([System.IO.Directory]::Exists($path)) {
|
|
328
|
-
return [System.Security.AccessControl.DirectorySecurity]::new($path, $sections)
|
|
329
|
-
}
|
|
330
|
-
return [System.Security.AccessControl.FileSecurity]::new($path, $sections)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
${WINDOWS_ACL_IDEMPOTENT_REPAIR}
|
|
334
|
-
function Write-CcOwnerOnlyAclImpl($item, [string]$path) {
|
|
335
|
-
$security = Read-CcAcl $path
|
|
336
|
-
$owner = $security.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
337
|
-
if ($owner.Value -ne $sid.Value) {
|
|
338
|
-
# Elevated/service tokens can create files whose owner is the token's
|
|
339
|
-
# default owner group (for example BUILTIN\Administrators on hosted CI),
|
|
340
|
-
# even though the token user created the object. Only that exact token-owned
|
|
341
|
-
# case may converge to the narrower user SID; every unrelated owner remains
|
|
342
|
-
# fail-closed.
|
|
343
|
-
if ($null -eq $tokenOwner -or $owner.Value -ne $tokenOwner.Value) {
|
|
344
|
-
throw "owner-only ACL repair refuses a path owned by another identity: $path"
|
|
345
|
-
}
|
|
346
|
-
$security.SetOwner($sid)
|
|
347
|
-
}
|
|
348
|
-
$security.SetAccessRuleProtection($true, $false)
|
|
349
|
-
$existingRules = @($security.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]))
|
|
350
|
-
foreach ($existingRule in $existingRules) {
|
|
351
|
-
$security.RemoveAccessRuleAll($existingRule)
|
|
352
|
-
}
|
|
353
|
-
if ($item.PSIsContainer) {
|
|
354
|
-
$inheritance =
|
|
355
|
-
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
356
|
-
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
357
|
-
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
358
|
-
$sid,
|
|
359
|
-
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
360
|
-
$inheritance,
|
|
361
|
-
[System.Security.AccessControl.PropagationFlags]::None,
|
|
362
|
-
[System.Security.AccessControl.AccessControlType]::Allow
|
|
363
|
-
)
|
|
364
|
-
$security.AddAccessRule($rule) | Out-Null
|
|
365
|
-
[System.IO.DirectoryInfo]::new($path).SetAccessControl($security)
|
|
366
|
-
} else {
|
|
367
|
-
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
368
|
-
$sid,
|
|
369
|
-
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
370
|
-
[System.Security.AccessControl.InheritanceFlags]::None,
|
|
371
|
-
[System.Security.AccessControl.PropagationFlags]::None,
|
|
372
|
-
[System.Security.AccessControl.AccessControlType]::Allow
|
|
373
|
-
)
|
|
374
|
-
$security.AddAccessRule($rule) | Out-Null
|
|
375
|
-
[System.IO.FileInfo]::new($path).SetAccessControl($security)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if ($operation -eq 'repair') {
|
|
380
|
-
Write-CcOwnerOnlyAcl $item $target
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
$script:ccAclStage = 'verify'
|
|
384
|
-
[Console]::Error.WriteLine('CC_WINDOWS_ACL_STAGE=verify')
|
|
385
|
-
$acl = Read-CcAcl $target
|
|
386
|
-
$owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
|
387
|
-
$rules = @($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))
|
|
388
|
-
$protectionOk = -not $item.PSIsContainer -or $acl.AreAccessRulesProtected
|
|
389
|
-
$ownerOnly =
|
|
390
|
-
$protectionOk -and
|
|
391
|
-
$owner -eq $sid.Value -and
|
|
392
|
-
$rules.Count -eq 1
|
|
393
|
-
if ($ownerOnly) {
|
|
394
|
-
foreach ($rule in $rules) {
|
|
395
|
-
$full = ($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl
|
|
396
|
-
if ($item.PSIsContainer) {
|
|
397
|
-
$requiredInheritance =
|
|
398
|
-
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
399
|
-
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
400
|
-
$inheritanceOk =
|
|
401
|
-
($rule.InheritanceFlags -band $requiredInheritance) -eq $requiredInheritance
|
|
402
|
-
} else {
|
|
403
|
-
$inheritanceOk =
|
|
404
|
-
$rule.InheritanceFlags -eq [System.Security.AccessControl.InheritanceFlags]::None
|
|
405
|
-
}
|
|
406
|
-
$propagationOk =
|
|
407
|
-
$rule.PropagationFlags -eq [System.Security.AccessControl.PropagationFlags]::None
|
|
408
|
-
if ($rule.IdentityReference.Value -ne $sid.Value -or
|
|
409
|
-
$rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
|
|
410
|
-
($item.PSIsContainer -and $rule.IsInherited) -or
|
|
411
|
-
-not $inheritanceOk -or
|
|
412
|
-
-not $propagationOk -or
|
|
413
|
-
-not $full) {
|
|
414
|
-
$ownerOnly = $false
|
|
415
|
-
break
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
[pscustomobject]@{
|
|
420
|
-
ownerOnly = [bool]$ownerOnly
|
|
421
|
-
isDirectory = [bool]$item.PSIsContainer
|
|
422
|
-
ownerSid = $owner
|
|
423
|
-
currentSid = $sid.Value
|
|
424
|
-
tokenOwnerSid = if ($null -eq $tokenOwner) { $null } else { $tokenOwner.Value }
|
|
425
|
-
protected = [bool]$acl.AreAccessRulesProtected
|
|
426
|
-
aceCount = $rules.Count
|
|
427
|
-
} | ConvertTo-Json -Compress
|
|
428
|
-
if (-not $ownerOnly) { exit 4 }
|
|
429
|
-
} catch {
|
|
430
|
-
# Emit only a fixed stage and the deepest native HRESULT. Never emit the
|
|
431
|
-
# exception message, target path or arbitrary PowerShell error record.
|
|
432
|
-
$failure = $_.Exception
|
|
433
|
-
while ($null -ne $failure.InnerException) { $failure = $failure.InnerException }
|
|
434
|
-
[pscustomobject]@{
|
|
435
|
-
ownerOnly = $false
|
|
436
|
-
failureStage = $script:ccAclStage
|
|
437
|
-
hresult = ('0x{0:X8}' -f $failure.HResult)
|
|
438
|
-
} | ConvertTo-Json -Compress
|
|
439
|
-
exit 5
|
|
440
|
-
}
|
|
441
|
-
`;
|
|
442
|
-
|
|
443
|
-
const WINDOWS_ACL_BATCH_SCRIPT = String.raw`
|
|
444
|
-
$ErrorActionPreference = 'Stop'
|
|
445
|
-
$utf8 = [System.Text.UTF8Encoding]::new($false)
|
|
446
|
-
[Console]::InputEncoding = $utf8
|
|
447
|
-
[Console]::OutputEncoding = $utf8
|
|
448
|
-
$sections =
|
|
449
|
-
[System.Security.AccessControl.AccessControlSections]::Access -bor
|
|
450
|
-
[System.Security.AccessControl.AccessControlSections]::Owner
|
|
451
|
-
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
452
|
-
$sid = $identity.User
|
|
453
|
-
$tokenOwner = $identity.Owner
|
|
454
|
-
$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
455
|
-
$operation = [string]$request.operation
|
|
456
|
-
$targets = @($request.targets)
|
|
457
|
-
$expectedKind = [string]$request.expectedKind
|
|
458
|
-
if ($operation -ne 'inspect' -and $operation -ne 'repair' -and $operation -ne 'preflight') {
|
|
459
|
-
throw "unsupported ACL batch operation: $operation"
|
|
460
|
-
}
|
|
461
|
-
if ($expectedKind -ne '' -and $expectedKind -ne 'file' -and $expectedKind -ne 'directory') {
|
|
462
|
-
throw "unsupported expected path kind: $expectedKind"
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function Assert-CcNoReparseTraversal([string]$path) {
|
|
466
|
-
$cursor = [System.IO.Path]::GetFullPath($path)
|
|
467
|
-
while (-not (Test-Path -LiteralPath $cursor)) {
|
|
468
|
-
$parent = [System.IO.Directory]::GetParent($cursor)
|
|
469
|
-
if ($null -eq $parent) { return }
|
|
470
|
-
$cursor = $parent.FullName
|
|
471
|
-
}
|
|
472
|
-
$entry = Get-Item -LiteralPath $cursor -Force
|
|
473
|
-
while ($null -ne $entry) {
|
|
474
|
-
if (($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
475
|
-
throw "symbolic links and reparse points are not allowed: $($entry.FullName)"
|
|
476
|
-
}
|
|
477
|
-
$entry = $entry.Parent
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
# A repair is all-or-nothing with respect to path safety. Preflight every
|
|
482
|
-
# existing target before the first SetAccessControl call so a junction root
|
|
483
|
-
# cannot fail after a regular-looking descendant has already been modified.
|
|
484
|
-
if ($operation -eq 'repair' -or $operation -eq 'preflight') {
|
|
485
|
-
$preflightFailures = @()
|
|
486
|
-
foreach ($target in $targets) {
|
|
487
|
-
try {
|
|
488
|
-
Assert-CcNoReparseTraversal $target
|
|
489
|
-
$candidate = Get-Item -LiteralPath $target -Force
|
|
490
|
-
$kindMismatch =
|
|
491
|
-
($expectedKind -eq 'file' -and $candidate.PSIsContainer) -or
|
|
492
|
-
($expectedKind -eq 'directory' -and -not $candidate.PSIsContainer)
|
|
493
|
-
if ($kindMismatch) {
|
|
494
|
-
$preflightFailures += [pscustomobject]@{
|
|
495
|
-
target = [string]$target
|
|
496
|
-
error = "expected $expectedKind but found $(if ($candidate.PSIsContainer) { 'directory' } else { 'file' })"
|
|
497
|
-
errorCode = 'EXPECTED_KIND_MISMATCH'
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
} catch {
|
|
501
|
-
if ($_.CategoryInfo.Category -ne [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
|
|
502
|
-
$preflightFailures += [pscustomobject]@{
|
|
503
|
-
target = [string]$target
|
|
504
|
-
error = $_.Exception.Message
|
|
505
|
-
errorCode = 'PATH_PREFLIGHT_FAILED'
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
if ($preflightFailures.Count -gt 0) {
|
|
511
|
-
$firstFailure = $preflightFailures[0]
|
|
512
|
-
$output = @($targets | ForEach-Object {
|
|
513
|
-
[pscustomobject]@{
|
|
514
|
-
target = [string]$_
|
|
515
|
-
exists = [bool](Test-Path -LiteralPath $_)
|
|
516
|
-
isDirectory = $null
|
|
517
|
-
ok = $false
|
|
518
|
-
error = "ACL batch preflight failed at $($firstFailure.target): $($firstFailure.error)"
|
|
519
|
-
errorCode = $firstFailure.errorCode
|
|
520
|
-
}
|
|
521
|
-
})
|
|
522
|
-
[Console]::Write((ConvertTo-Json -InputObject $output -Compress -Depth 4))
|
|
523
|
-
exit 4
|
|
524
|
-
}
|
|
525
|
-
if ($operation -eq 'preflight') {
|
|
526
|
-
$output = @($targets | ForEach-Object {
|
|
527
|
-
$preflightTarget = [string]$_
|
|
528
|
-
$nativeItem = $null
|
|
529
|
-
try {
|
|
530
|
-
$nativeItem = Get-Item -LiteralPath $preflightTarget -Force
|
|
531
|
-
} catch {
|
|
532
|
-
if ($_.CategoryInfo.Category -ne [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
|
|
533
|
-
throw
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
[pscustomobject]@{
|
|
537
|
-
target = $preflightTarget
|
|
538
|
-
exists = [bool]($null -ne $nativeItem)
|
|
539
|
-
isDirectory = if ($null -eq $nativeItem) { $null } else { [bool]$nativeItem.PSIsContainer }
|
|
540
|
-
ok = $true
|
|
541
|
-
error = $null
|
|
542
|
-
}
|
|
543
|
-
})
|
|
544
|
-
[Console]::Write((ConvertTo-Json -InputObject $output -Compress -Depth 4))
|
|
545
|
-
exit 0
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function Read-CcAcl([string]$path) {
|
|
550
|
-
if ([System.IO.Directory]::Exists($path)) {
|
|
551
|
-
return [System.Security.AccessControl.DirectorySecurity]::new($path, $sections)
|
|
552
|
-
}
|
|
553
|
-
return [System.Security.AccessControl.FileSecurity]::new($path, $sections)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
${WINDOWS_ACL_IDEMPOTENT_REPAIR}
|
|
557
|
-
function Write-CcOwnerOnlyAclImpl($item, [string]$path) {
|
|
558
|
-
$security = Read-CcAcl $path
|
|
559
|
-
$owner = $security.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
560
|
-
if ($owner.Value -ne $sid.Value) {
|
|
561
|
-
if ($null -eq $tokenOwner -or $owner.Value -ne $tokenOwner.Value) {
|
|
562
|
-
throw "owner-only ACL repair refuses a path owned by another identity: $path"
|
|
563
|
-
}
|
|
564
|
-
$security.SetOwner($sid)
|
|
565
|
-
}
|
|
566
|
-
$security.SetAccessRuleProtection($true, $false)
|
|
567
|
-
$existingRules = @($security.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]))
|
|
568
|
-
foreach ($existingRule in $existingRules) {
|
|
569
|
-
$security.RemoveAccessRuleAll($existingRule)
|
|
570
|
-
}
|
|
571
|
-
if ($item.PSIsContainer) {
|
|
572
|
-
$inheritance =
|
|
573
|
-
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
574
|
-
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
575
|
-
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
576
|
-
$sid,
|
|
577
|
-
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
578
|
-
$inheritance,
|
|
579
|
-
[System.Security.AccessControl.PropagationFlags]::None,
|
|
580
|
-
[System.Security.AccessControl.AccessControlType]::Allow
|
|
581
|
-
)
|
|
582
|
-
$security.AddAccessRule($rule) | Out-Null
|
|
583
|
-
[System.IO.DirectoryInfo]::new($path).SetAccessControl($security)
|
|
584
|
-
} else {
|
|
585
|
-
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
586
|
-
$sid,
|
|
587
|
-
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
588
|
-
[System.Security.AccessControl.InheritanceFlags]::None,
|
|
589
|
-
[System.Security.AccessControl.PropagationFlags]::None,
|
|
590
|
-
[System.Security.AccessControl.AccessControlType]::Allow
|
|
591
|
-
)
|
|
592
|
-
$security.AddAccessRule($rule) | Out-Null
|
|
593
|
-
[System.IO.FileInfo]::new($path).SetAccessControl($security)
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
$results = foreach ($target in $targets) {
|
|
598
|
-
$item = $null
|
|
599
|
-
try {
|
|
600
|
-
Assert-CcNoReparseTraversal $target
|
|
601
|
-
$item = Get-Item -LiteralPath $target -Force
|
|
602
|
-
$kindMismatch =
|
|
603
|
-
($expectedKind -eq 'file' -and $item.PSIsContainer) -or
|
|
604
|
-
($expectedKind -eq 'directory' -and -not $item.PSIsContainer)
|
|
605
|
-
if ($kindMismatch) {
|
|
606
|
-
[pscustomobject]@{
|
|
607
|
-
target = [string]$target
|
|
608
|
-
exists = $true
|
|
609
|
-
isDirectory = [bool]$item.PSIsContainer
|
|
610
|
-
ok = $false
|
|
611
|
-
error = "expected $expectedKind but found $(if ($item.PSIsContainer) { 'directory' } else { 'file' })"
|
|
612
|
-
errorCode = 'EXPECTED_KIND_MISMATCH'
|
|
613
|
-
}
|
|
614
|
-
continue
|
|
615
|
-
}
|
|
616
|
-
if ($operation -eq 'repair') {
|
|
617
|
-
Write-CcOwnerOnlyAcl $item $target
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
$acl = Read-CcAcl $target
|
|
621
|
-
$owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
|
622
|
-
$rules = @($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))
|
|
623
|
-
$protectionOk = -not $item.PSIsContainer -or $acl.AreAccessRulesProtected
|
|
624
|
-
$ownerOnly =
|
|
625
|
-
$protectionOk -and
|
|
626
|
-
$owner -eq $sid.Value -and
|
|
627
|
-
$rules.Count -eq 1
|
|
628
|
-
if ($ownerOnly) {
|
|
629
|
-
foreach ($rule in $rules) {
|
|
630
|
-
$full = ($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl
|
|
631
|
-
if ($item.PSIsContainer) {
|
|
632
|
-
$requiredInheritance =
|
|
633
|
-
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
634
|
-
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
635
|
-
$inheritanceOk =
|
|
636
|
-
($rule.InheritanceFlags -band $requiredInheritance) -eq $requiredInheritance
|
|
637
|
-
} else {
|
|
638
|
-
$inheritanceOk =
|
|
639
|
-
$rule.InheritanceFlags -eq [System.Security.AccessControl.InheritanceFlags]::None
|
|
640
|
-
}
|
|
641
|
-
$propagationOk =
|
|
642
|
-
$rule.PropagationFlags -eq [System.Security.AccessControl.PropagationFlags]::None
|
|
643
|
-
if ($rule.IdentityReference.Value -ne $sid.Value -or
|
|
644
|
-
$rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
|
|
645
|
-
($item.PSIsContainer -and $rule.IsInherited) -or
|
|
646
|
-
-not $inheritanceOk -or
|
|
647
|
-
-not $propagationOk -or
|
|
648
|
-
-not $full) {
|
|
649
|
-
$ownerOnly = $false
|
|
650
|
-
break
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
[pscustomobject]@{
|
|
655
|
-
target = [string]$target
|
|
656
|
-
exists = $true
|
|
657
|
-
isDirectory = [bool]$item.PSIsContainer
|
|
658
|
-
ok = [bool]$ownerOnly
|
|
659
|
-
ownerSid = $owner
|
|
660
|
-
currentSid = $sid.Value
|
|
661
|
-
tokenOwnerSid = if ($null -eq $tokenOwner) { $null } else { $tokenOwner.Value }
|
|
662
|
-
protected = [bool]$acl.AreAccessRulesProtected
|
|
663
|
-
aceCount = $rules.Count
|
|
664
|
-
error = if ($ownerOnly) { $null } else { 'path is not owner-only' }
|
|
665
|
-
errorCode = $null
|
|
666
|
-
}
|
|
667
|
-
} catch {
|
|
668
|
-
[pscustomobject]@{
|
|
669
|
-
target = [string]$target
|
|
670
|
-
exists = [bool](($null -ne $item) -or (Test-Path -LiteralPath $target))
|
|
671
|
-
isDirectory = $null
|
|
672
|
-
ok = $false
|
|
673
|
-
error = $_.Exception.Message
|
|
674
|
-
errorCode = 'PATH_OPERATION_FAILED'
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
$output = [array]@($results)
|
|
679
|
-
[Console]::Write((ConvertTo-Json -InputObject $output -Compress -Depth 4))
|
|
680
|
-
if (@($output | Where-Object { -not $_.ok }).Count -gt 0) { exit 4 }
|
|
681
|
-
`;
|
|
682
|
-
|
|
683
|
-
export const _deps = {
|
|
684
|
-
fs,
|
|
685
|
-
spawnSync,
|
|
686
|
-
platform: () => process.platform,
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
// ACL repair starts a short PowerShell process on Windows. Cache successful
|
|
690
|
-
// production repairs by path and filesystem identity for this process; injected-dependency
|
|
691
|
-
// tests and repairPrivatePath() always execute and verify the operation.
|
|
692
|
-
const securedWindowsPaths = new Map();
|
|
693
|
-
|
|
694
|
-
function windowsPathIdentity(target, deps) {
|
|
695
|
-
try {
|
|
696
|
-
const entry = deps.fs.lstatSync(target, { bigint: true });
|
|
697
|
-
if (entry.isSymbolicLink() || !entry.ino) return null;
|
|
698
|
-
return `${entry.dev}:${entry.ino}:${entry.birthtimeNs}`;
|
|
699
|
-
} catch {
|
|
700
|
-
// An unreliable Node stat must take the native verification path again.
|
|
701
|
-
return null;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function rememberSecuredWindowsPath(target, deps) {
|
|
706
|
-
const identity = windowsPathIdentity(target, deps);
|
|
707
|
-
if (identity) securedWindowsPaths.set(target, identity);
|
|
708
|
-
else securedWindowsPaths.delete(target);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function isSecuredWindowsPath(target, deps) {
|
|
712
|
-
const cached = securedWindowsPaths.get(target);
|
|
713
|
-
return Boolean(cached && cached === windowsPathIdentity(target, deps));
|
|
714
|
-
}
|
|
715
|
-
const WINDOWS_ACL_BATCH_SIZE = 500;
|
|
716
|
-
const WINDOWS_ACL_TIMEOUT_ENV = "CC_SECURE_FS_WINDOWS_ACL_TIMEOUT_MS";
|
|
717
|
-
const MAX_WINDOWS_ACL_TIMEOUT_MS = 5 * 60_000;
|
|
718
|
-
|
|
719
|
-
export function _windowsAclWorkingDirectory(
|
|
720
|
-
environment = process.env,
|
|
721
|
-
filesystem = fs,
|
|
722
|
-
) {
|
|
723
|
-
// Isolated profiles can make Windows PowerShell first-use caches relative
|
|
724
|
-
// to cwd. Contain those writes in application state, never the source tree.
|
|
725
|
-
const isolatedLocalTarget =
|
|
726
|
-
typeof environment.CC_EXECUTION_LOCATION_RUNNER_ID === "string" &&
|
|
727
|
-
environment.CC_EXECUTION_LOCATION_RUNNER_ID.length > 0 &&
|
|
728
|
-
typeof environment.CHAINLESSCHAIN_HOME === "string" &&
|
|
729
|
-
isNativeAbsolute(environment.CHAINLESSCHAIN_HOME);
|
|
730
|
-
if (!isFormalQualityHermeticRuntime(environment) && !isolatedLocalTarget)
|
|
731
|
-
return null;
|
|
732
|
-
const configuredHome = resolve(String(environment.CHAINLESSCHAIN_HOME));
|
|
733
|
-
try {
|
|
734
|
-
const entry = filesystem.lstatSync(configuredHome);
|
|
735
|
-
if (!entry.isDirectory() || entry.isSymbolicLink()) return null;
|
|
736
|
-
} catch {
|
|
737
|
-
return null;
|
|
738
|
-
}
|
|
739
|
-
return configuredHome;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Windows hosted runners can need more than the normal 15/30 second allowance
|
|
743
|
-
// to cold-start PowerShell and load the access-control types. A trusted harness
|
|
744
|
-
// may raise (but never lower) that allowance; the cap prevents an inherited
|
|
745
|
-
// environment value from wedging owner-only storage initialization forever.
|
|
746
|
-
export function _resolveWindowsAclTimeout(
|
|
747
|
-
defaultTimeoutMs,
|
|
748
|
-
environment = process.env,
|
|
749
|
-
) {
|
|
750
|
-
const fallback = Math.max(1, Math.floor(Number(defaultTimeoutMs) || 1));
|
|
751
|
-
const configured = Number(environment?.[WINDOWS_ACL_TIMEOUT_ENV]);
|
|
752
|
-
if (!Number.isFinite(configured) || configured <= 0) return fallback;
|
|
753
|
-
return Math.min(
|
|
754
|
-
Math.max(fallback, Math.floor(configured)),
|
|
755
|
-
MAX_WINDOWS_ACL_TIMEOUT_MS,
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Give one cold PowerShell process the full bounded operation budget. Splitting
|
|
760
|
-
// it into two shorter spawns repeatedly kills a slow startup before it can
|
|
761
|
-
// finish (and can leave less time than the native mutex's 10-second wait).
|
|
762
|
-
// A process that exhausts the budget remains a failure; never start another
|
|
763
|
-
// process with a fresh budget or accept an unverified ACL after a timeout.
|
|
764
|
-
function runWindowsAclCommand(deps, args, options) {
|
|
765
|
-
return deps.spawnSync("powershell.exe", args, {
|
|
766
|
-
...options,
|
|
767
|
-
timeout: Math.max(1, Math.floor(Number(options?.timeout) || 1)),
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function repairWindowsAclOnce(target, deps, options) {
|
|
772
|
-
const cacheable = !options.deps;
|
|
773
|
-
if (cacheable && isSecuredWindowsPath(target, deps)) {
|
|
774
|
-
return { ok: true, platform: "win32", cached: true };
|
|
775
|
-
}
|
|
776
|
-
const result = windowsAcl(target, "repair", deps);
|
|
777
|
-
if (result.ok && cacheable) rememberSecuredWindowsPath(target, deps);
|
|
778
|
-
return result;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function windowsAcl(target, operation, deps) {
|
|
782
|
-
const workingDirectory = _windowsAclWorkingDirectory(process.env, deps.fs);
|
|
783
|
-
const result = runWindowsAclCommand(
|
|
784
|
-
deps,
|
|
785
|
-
[
|
|
786
|
-
"-NoLogo",
|
|
787
|
-
"-NoProfile",
|
|
788
|
-
"-NonInteractive",
|
|
789
|
-
"-ExecutionPolicy",
|
|
790
|
-
"Bypass",
|
|
791
|
-
"-Command",
|
|
792
|
-
`& { ${WINDOWS_ACL_SCRIPT} }`,
|
|
793
|
-
target,
|
|
794
|
-
operation,
|
|
795
|
-
],
|
|
796
|
-
{
|
|
797
|
-
encoding: "utf8",
|
|
798
|
-
windowsHide: true,
|
|
799
|
-
timeout: _resolveWindowsAclTimeout(15_000),
|
|
800
|
-
...(workingDirectory ? { cwd: workingDirectory } : {}),
|
|
801
|
-
},
|
|
802
|
-
);
|
|
803
|
-
let details = null;
|
|
804
|
-
try {
|
|
805
|
-
details = JSON.parse(String(result?.stdout || "").trim());
|
|
806
|
-
} catch {
|
|
807
|
-
details = null;
|
|
808
|
-
}
|
|
809
|
-
if (result?.error) {
|
|
810
|
-
const progress =
|
|
811
|
-
[
|
|
812
|
-
...String(result.stderr || "")
|
|
813
|
-
.slice(0, 8192)
|
|
814
|
-
.matchAll(
|
|
815
|
-
/^CC_WINDOWS_ACL_STAGE=(initialize|traversal|lookup|repair-lock|repair-inspect|repair-write|verify)\r?$/gmu,
|
|
816
|
-
),
|
|
817
|
-
].at(-1)?.[1] || "startup";
|
|
818
|
-
const stage =
|
|
819
|
-
result.error.code === "ETIMEDOUT" ? `timeout-${progress}` : "spawn";
|
|
820
|
-
return {
|
|
821
|
-
ok: false,
|
|
822
|
-
platform: "win32",
|
|
823
|
-
error: `owner-only ACL process failed [windows-acl:${stage}]`,
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
if (result?.status !== 0 || details?.ownerOnly !== true) {
|
|
827
|
-
return {
|
|
828
|
-
ok: false,
|
|
829
|
-
platform: "win32",
|
|
830
|
-
error: `owner-only ACL verification failed ${windowsAclFailureMarker(details)}`,
|
|
831
|
-
details,
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
return { ok: true, platform: "win32", details };
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function windowsAclFailureMarker(details) {
|
|
838
|
-
const allowedStages = new Set([
|
|
839
|
-
"initialize",
|
|
840
|
-
"traversal",
|
|
841
|
-
"lookup",
|
|
842
|
-
"repair-lock",
|
|
843
|
-
"repair-inspect",
|
|
844
|
-
"repair-write",
|
|
845
|
-
"verify",
|
|
846
|
-
]);
|
|
847
|
-
if (
|
|
848
|
-
allowedStages.has(details?.failureStage) &&
|
|
849
|
-
/^0x[0-9a-f]{8}$/iu.test(details?.hresult || "")
|
|
850
|
-
) {
|
|
851
|
-
return `[windows-acl:${details.failureStage}:${details.hresult.toLowerCase()}]`;
|
|
852
|
-
}
|
|
853
|
-
return details?.ownerOnly === false
|
|
854
|
-
? "[windows-acl:verify]"
|
|
855
|
-
: "[windows-acl:output]";
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function windowsAclBatch(targets, operation, deps, expectedKind = null) {
|
|
859
|
-
if (targets.length === 0) return [];
|
|
860
|
-
const workingDirectory = _windowsAclWorkingDirectory(process.env, deps.fs);
|
|
861
|
-
const result = runWindowsAclCommand(
|
|
862
|
-
deps,
|
|
863
|
-
[
|
|
864
|
-
"-NoLogo",
|
|
865
|
-
"-NoProfile",
|
|
866
|
-
"-NonInteractive",
|
|
867
|
-
"-ExecutionPolicy",
|
|
868
|
-
"Bypass",
|
|
869
|
-
"-Command",
|
|
870
|
-
`& { ${WINDOWS_ACL_BATCH_SCRIPT} }`,
|
|
871
|
-
],
|
|
872
|
-
{
|
|
873
|
-
encoding: "utf8",
|
|
874
|
-
input: JSON.stringify({
|
|
875
|
-
operation,
|
|
876
|
-
targets,
|
|
877
|
-
...(expectedKind ? { expectedKind } : {}),
|
|
878
|
-
}),
|
|
879
|
-
windowsHide: true,
|
|
880
|
-
timeout: _resolveWindowsAclTimeout(30_000),
|
|
881
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
882
|
-
...(workingDirectory ? { cwd: workingDirectory } : {}),
|
|
883
|
-
},
|
|
884
|
-
);
|
|
885
|
-
let details = [];
|
|
886
|
-
try {
|
|
887
|
-
const parsed = JSON.parse(String(result?.stdout || "").trim());
|
|
888
|
-
details = Array.isArray(parsed) ? parsed : [parsed];
|
|
889
|
-
} catch {
|
|
890
|
-
details = [];
|
|
891
|
-
}
|
|
892
|
-
const anyFailed = details.some((entry) => entry?.ok !== true);
|
|
893
|
-
const statusOk = result?.status === 0 || (result?.status === 4 && anyFailed);
|
|
894
|
-
const uniqueDetailTargets = new Set(details.map((entry) => entry?.target));
|
|
895
|
-
if (
|
|
896
|
-
result?.error ||
|
|
897
|
-
!statusOk ||
|
|
898
|
-
details.length !== targets.length ||
|
|
899
|
-
uniqueDetailTargets.size !== targets.length
|
|
900
|
-
) {
|
|
901
|
-
const error =
|
|
902
|
-
result?.error?.message ||
|
|
903
|
-
String(result?.stderr || "").trim() ||
|
|
904
|
-
"owner-only ACL batch verification failed";
|
|
905
|
-
return targets.map((target) => ({
|
|
906
|
-
target,
|
|
907
|
-
ok: false,
|
|
908
|
-
platform: "win32",
|
|
909
|
-
error,
|
|
910
|
-
}));
|
|
911
|
-
}
|
|
912
|
-
const byTarget = new Map(details.map((entry) => [entry.target, entry]));
|
|
913
|
-
return targets.map((target) => {
|
|
914
|
-
const entry = byTarget.get(target);
|
|
915
|
-
return {
|
|
916
|
-
target,
|
|
917
|
-
exists: entry?.exists !== false,
|
|
918
|
-
ok: entry?.ok === true,
|
|
919
|
-
platform: "win32",
|
|
920
|
-
details: entry || null,
|
|
921
|
-
...(entry?.errorCode ? { errorCode: entry.errorCode } : {}),
|
|
922
|
-
...(entry?.ok === true
|
|
923
|
-
? {}
|
|
924
|
-
: {
|
|
925
|
-
error:
|
|
926
|
-
entry?.error ||
|
|
927
|
-
String(result?.stderr || "").trim() ||
|
|
928
|
-
`owner-only ACL batch verification exited ${result?.status}`,
|
|
929
|
-
}),
|
|
930
|
-
};
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function isWindowsPathStatUnreliable(error, platform) {
|
|
935
|
-
return (
|
|
936
|
-
platform === "win32" &&
|
|
937
|
-
(error?.code === "EPERM" ||
|
|
938
|
-
error?.code === "EACCES" ||
|
|
939
|
-
error?.code === "ENOENT" ||
|
|
940
|
-
error?.code === "WINDOWS_PATH_STAT_UNRELIABLE")
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function preflightWindowsPaths(targets, deps) {
|
|
945
|
-
const results = [];
|
|
946
|
-
for (
|
|
947
|
-
let offset = 0;
|
|
948
|
-
offset < targets.length;
|
|
949
|
-
offset += WINDOWS_ACL_BATCH_SIZE
|
|
950
|
-
) {
|
|
951
|
-
results.push(
|
|
952
|
-
...windowsAclBatch(
|
|
953
|
-
targets.slice(offset, offset + WINDOWS_ACL_BATCH_SIZE),
|
|
954
|
-
"preflight",
|
|
955
|
-
deps,
|
|
956
|
-
),
|
|
957
|
-
);
|
|
958
|
-
}
|
|
959
|
-
const failed = results.find((entry) => entry.ok !== true);
|
|
960
|
-
if (failed) {
|
|
961
|
-
const error = new Error(
|
|
962
|
-
`Could not verify Windows path ancestors for ${failed.target}: ${failed.error || "native preflight failed"}`,
|
|
963
|
-
);
|
|
964
|
-
error.code = "CONFIG_HOME_UNSAFE";
|
|
965
|
-
throw error;
|
|
966
|
-
}
|
|
967
|
-
return results;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function privatePathEntryFromNativeEvidence(target, nativeResult) {
|
|
971
|
-
if (!nativeResult || !samePath(nativeResult.target, target, true)) {
|
|
972
|
-
const unsafe = new Error(
|
|
973
|
-
`Native Windows path evidence did not match requested target: ${target}`,
|
|
974
|
-
);
|
|
975
|
-
unsafe.code = "CONFIG_HOME_UNSAFE";
|
|
976
|
-
throw unsafe;
|
|
977
|
-
}
|
|
978
|
-
if (nativeResult.exists === false) {
|
|
979
|
-
return { exists: false, entry: null };
|
|
980
|
-
}
|
|
981
|
-
const isDirectory = nativeResult.details?.isDirectory;
|
|
982
|
-
if (typeof isDirectory !== "boolean") {
|
|
983
|
-
const unsafe = new Error(
|
|
984
|
-
`Could not classify Windows path type after native preflight: ${target}`,
|
|
985
|
-
);
|
|
986
|
-
unsafe.code = "CONFIG_HOME_UNSAFE";
|
|
987
|
-
throw unsafe;
|
|
988
|
-
}
|
|
989
|
-
return {
|
|
990
|
-
exists: true,
|
|
991
|
-
entry: {
|
|
992
|
-
isDirectory: () => isDirectory,
|
|
993
|
-
isSymbolicLink: () => false,
|
|
994
|
-
},
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function readPrivatePathEntry(target, deps, platform, nativeEvidence = null) {
|
|
999
|
-
if (platform === "win32" && nativeEvidence) {
|
|
1000
|
-
return privatePathEntryFromNativeEvidence(target, nativeEvidence);
|
|
1001
|
-
}
|
|
1002
|
-
try {
|
|
1003
|
-
return { exists: true, entry: deps.fs.lstatSync(target) };
|
|
1004
|
-
} catch (error) {
|
|
1005
|
-
if (platform !== "win32") {
|
|
1006
|
-
if (error?.code === "ENOENT") return { exists: false, entry: null };
|
|
1007
|
-
throw error;
|
|
1008
|
-
}
|
|
1009
|
-
if (!isWindowsPathStatUnreliable(error, platform)) throw error;
|
|
1010
|
-
const directoryEntry = readWindowsDirectoryEntry(target, deps.fs);
|
|
1011
|
-
if (directoryEntry?.exists === false) {
|
|
1012
|
-
return { exists: false, entry: null };
|
|
1013
|
-
}
|
|
1014
|
-
if (directoryEntry) {
|
|
1015
|
-
return {
|
|
1016
|
-
exists: true,
|
|
1017
|
-
entry: {
|
|
1018
|
-
isDirectory: () => directoryEntry.isDirectory,
|
|
1019
|
-
isSymbolicLink: () => directoryEntry.isSymbolicLink,
|
|
1020
|
-
},
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
const [nativeResult] = preflightWindowsPaths([String(target)], deps);
|
|
1024
|
-
return privatePathEntryFromNativeEvidence(target, nativeResult);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function assertNoLinkTraversalWithWindowsFallback(
|
|
1029
|
-
target,
|
|
1030
|
-
deps,
|
|
1031
|
-
checkedPaths,
|
|
1032
|
-
platform,
|
|
1033
|
-
) {
|
|
1034
|
-
try {
|
|
1035
|
-
assertNoLinkTraversal(target, deps.fs, checkedPaths, platform);
|
|
1036
|
-
return null;
|
|
1037
|
-
} catch (error) {
|
|
1038
|
-
if (!isWindowsPathStatUnreliable(error, platform)) throw error;
|
|
1039
|
-
return preflightWindowsPaths([String(target)], deps)[0];
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
export function inspectPrivatePaths(targets, options = {}) {
|
|
1044
|
-
const uniqueTargets = [...new Set((targets || []).map(String))];
|
|
1045
|
-
const deps = { ..._deps, ...(options.deps || {}) };
|
|
1046
|
-
if ((options.platform || deps.platform()) === "win32") {
|
|
1047
|
-
const results = [];
|
|
1048
|
-
for (
|
|
1049
|
-
let offset = 0;
|
|
1050
|
-
offset < uniqueTargets.length;
|
|
1051
|
-
offset += WINDOWS_ACL_BATCH_SIZE
|
|
1052
|
-
) {
|
|
1053
|
-
results.push(
|
|
1054
|
-
...windowsAclBatch(
|
|
1055
|
-
uniqueTargets.slice(offset, offset + WINDOWS_ACL_BATCH_SIZE),
|
|
1056
|
-
"inspect",
|
|
1057
|
-
deps,
|
|
1058
|
-
),
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
return results;
|
|
1062
|
-
}
|
|
1063
|
-
return uniqueTargets.map((target) => ({
|
|
1064
|
-
target,
|
|
1065
|
-
...inspectPrivatePath(target, { ...options, deps }),
|
|
1066
|
-
}));
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
export function repairPrivatePaths(targets, options = {}) {
|
|
1070
|
-
const uniqueTargets = [...new Set((targets || []).map(String))];
|
|
1071
|
-
const deps = { ..._deps, ...(options.deps || {}) };
|
|
1072
|
-
const platform = options.platform || deps.platform();
|
|
1073
|
-
const checkedPaths = new Set();
|
|
1074
|
-
let nativeWindowsPreflightRequired = false;
|
|
1075
|
-
// Complete every path-safety check before starting a batched mutation. This
|
|
1076
|
-
// prevents a linked trust root from failing only after a descendant changed.
|
|
1077
|
-
for (const target of uniqueTargets) {
|
|
1078
|
-
assertSafeOwnerOnlyPath(target, platform);
|
|
1079
|
-
}
|
|
1080
|
-
for (const target of uniqueTargets) {
|
|
1081
|
-
try {
|
|
1082
|
-
assertNoLinkTraversal(target, deps.fs, checkedPaths, platform);
|
|
1083
|
-
} catch (error) {
|
|
1084
|
-
if (!isWindowsPathStatUnreliable(error, platform)) throw error;
|
|
1085
|
-
nativeWindowsPreflightRequired = true;
|
|
1086
|
-
break;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
if (nativeWindowsPreflightRequired) {
|
|
1090
|
-
preflightWindowsPaths(uniqueTargets, deps);
|
|
1091
|
-
}
|
|
1092
|
-
if (platform === "win32") {
|
|
1093
|
-
const results = [];
|
|
1094
|
-
for (
|
|
1095
|
-
let offset = 0;
|
|
1096
|
-
offset < uniqueTargets.length;
|
|
1097
|
-
offset += WINDOWS_ACL_BATCH_SIZE
|
|
1098
|
-
) {
|
|
1099
|
-
results.push(
|
|
1100
|
-
...windowsAclBatch(
|
|
1101
|
-
uniqueTargets.slice(offset, offset + WINDOWS_ACL_BATCH_SIZE),
|
|
1102
|
-
"repair",
|
|
1103
|
-
deps,
|
|
1104
|
-
),
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
const failed = results.filter(
|
|
1108
|
-
(entry) => !entry.ok && entry.exists !== false,
|
|
1109
|
-
);
|
|
1110
|
-
if (failed.length > 0) {
|
|
1111
|
-
throw new Error(
|
|
1112
|
-
`Could not repair owner-only ACL for ${failed[0].target}: ${failed[0].error}`,
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
if (!options.deps) {
|
|
1116
|
-
for (const result of results) {
|
|
1117
|
-
if (result.ok && result.exists !== false) {
|
|
1118
|
-
rememberSecuredWindowsPath(result.target, deps);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
return results;
|
|
1123
|
-
}
|
|
1124
|
-
return uniqueTargets.map((target) => {
|
|
1125
|
-
try {
|
|
1126
|
-
return {
|
|
1127
|
-
target,
|
|
1128
|
-
...repairPrivatePathAfterPreflight(target, options, deps),
|
|
1129
|
-
};
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
if (error?.code === "ENOENT") {
|
|
1132
|
-
return { target, ok: true, exists: false, skipped: true };
|
|
1133
|
-
}
|
|
1134
|
-
throw error;
|
|
1135
|
-
}
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
export function inspectPrivatePath(target, options = {}) {
|
|
1140
|
-
const deps = { ..._deps, ...(options.deps || {}) };
|
|
1141
|
-
const platform = options.platform || deps.platform();
|
|
1142
|
-
if (platform === "win32") {
|
|
1143
|
-
return windowsAclBatch([String(target)], "inspect", deps)[0];
|
|
1144
|
-
}
|
|
1145
|
-
let stat;
|
|
1146
|
-
try {
|
|
1147
|
-
stat = deps.fs.lstatSync(target);
|
|
1148
|
-
} catch (error) {
|
|
1149
|
-
return { ok: false, exists: false, error: error.message };
|
|
1150
|
-
}
|
|
1151
|
-
if (stat.isSymbolicLink()) {
|
|
1152
|
-
return { ok: false, exists: true, error: "symbolic links are not allowed" };
|
|
1153
|
-
}
|
|
1154
|
-
const expected = stat.isDirectory()
|
|
1155
|
-
? PRIVATE_DIRECTORY_MODE
|
|
1156
|
-
: PRIVATE_FILE_MODE;
|
|
1157
|
-
const actual = stat.mode & 0o777;
|
|
1158
|
-
const ownerOk =
|
|
1159
|
-
typeof process.getuid !== "function" || stat.uid === process.getuid();
|
|
1160
|
-
return {
|
|
1161
|
-
ok: actual === expected && ownerOk,
|
|
1162
|
-
exists: true,
|
|
1163
|
-
platform: options.platform || deps.platform(),
|
|
1164
|
-
expectedMode: expected,
|
|
1165
|
-
actualMode: actual,
|
|
1166
|
-
ownerOk,
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
export function repairPrivatePath(target, options = {}) {
|
|
1171
|
-
const deps = { ..._deps, ...(options.deps || {}) };
|
|
1172
|
-
const platform = options.platform || deps.platform();
|
|
1173
|
-
assertSafeOwnerOnlyPath(target, platform);
|
|
1174
|
-
assertNoLinkTraversalWithWindowsFallback(target, deps, null, platform);
|
|
1175
|
-
return repairPrivatePathAfterPreflight(target, options, deps);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function repairPrivatePathAfterPreflight(target, options, deps) {
|
|
1179
|
-
if ((options.platform || deps.platform()) === "win32") {
|
|
1180
|
-
const result = windowsAcl(target, "repair", deps);
|
|
1181
|
-
if (!result.ok)
|
|
1182
|
-
throw new Error(result.error || "owner-only ACL repair failed");
|
|
1183
|
-
if (!options.deps) rememberSecuredWindowsPath(target, deps);
|
|
1184
|
-
return result;
|
|
1185
|
-
}
|
|
1186
|
-
const stat = deps.fs.lstatSync(target);
|
|
1187
|
-
if (stat.isSymbolicLink()) {
|
|
1188
|
-
throw new Error(
|
|
1189
|
-
`Refusing to change permissions through a symbolic link: ${target}`,
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
deps.fs.chmodSync(
|
|
1193
|
-
target,
|
|
1194
|
-
stat.isDirectory() ? PRIVATE_DIRECTORY_MODE : PRIVATE_FILE_MODE,
|
|
1195
|
-
);
|
|
1196
|
-
const result = inspectPrivatePath(target, { ...options, deps });
|
|
1197
|
-
if (!result.ok)
|
|
1198
|
-
throw new Error(`Could not verify owner-only permissions: ${target}`);
|
|
1199
|
-
return result;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
export function ensurePrivateDirectory(target, options = {}) {
|
|
1203
|
-
const deps = { ..._deps, ...(options.deps || {}) };
|
|
1204
|
-
const platform = options.platform || deps.platform();
|
|
1205
|
-
assertSafeOwnerOnlyPath(target, platform);
|
|
1206
|
-
const nativeEvidence = assertNoLinkTraversalWithWindowsFallback(
|
|
1207
|
-
target,
|
|
1208
|
-
deps,
|
|
1209
|
-
null,
|
|
1210
|
-
platform,
|
|
1211
|
-
);
|
|
1212
|
-
let existed = deps.fs.existsSync(target);
|
|
1213
|
-
let existingMode = null;
|
|
1214
|
-
if (existed) {
|
|
1215
|
-
const pathEntry = readPrivatePathEntry(
|
|
1216
|
-
target,
|
|
1217
|
-
deps,
|
|
1218
|
-
platform,
|
|
1219
|
-
nativeEvidence,
|
|
1220
|
-
);
|
|
1221
|
-
existed = pathEntry.exists;
|
|
1222
|
-
const existing = pathEntry.entry;
|
|
1223
|
-
existingMode = existing ? Number(existing.mode) : null;
|
|
1224
|
-
if (existed && existing.isSymbolicLink()) {
|
|
1225
|
-
throw new Error(
|
|
1226
|
-
`Refusing owner-only directory through a symbolic link: ${target}`,
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
if (
|
|
1230
|
-
existed &&
|
|
1231
|
-
typeof existing.isDirectory === "function" &&
|
|
1232
|
-
!existing.isDirectory()
|
|
1233
|
-
) {
|
|
1234
|
-
throw new Error(`Expected a directory for owner-only storage: ${target}`);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
deps.fs.mkdirSync(target, {
|
|
1238
|
-
recursive: true,
|
|
1239
|
-
mode: PRIVATE_DIRECTORY_MODE,
|
|
1240
|
-
});
|
|
1241
|
-
if ((options.platform || deps.platform()) !== "win32") {
|
|
1242
|
-
// chmod updates ctime even when the mode is already correct. Session and
|
|
1243
|
-
// security stores use ctime as part of their physical witness, so avoid
|
|
1244
|
-
// invalidating an otherwise unchanged owner-only directory.
|
|
1245
|
-
if (
|
|
1246
|
-
!existed ||
|
|
1247
|
-
!Number.isFinite(existingMode) ||
|
|
1248
|
-
(existingMode & 0o7777) !== PRIVATE_DIRECTORY_MODE
|
|
1249
|
-
) {
|
|
1250
|
-
deps.fs.chmodSync(target, PRIVATE_DIRECTORY_MODE);
|
|
1251
|
-
}
|
|
1252
|
-
} else if (
|
|
1253
|
-
options.applyWindowsAcl === true ||
|
|
1254
|
-
(!existed && options.applyWindowsAcl !== false)
|
|
1255
|
-
) {
|
|
1256
|
-
const result = repairWindowsAclOnce(target, deps, options);
|
|
1257
|
-
if (!result.ok && options.failIfUnavailable) {
|
|
1258
|
-
throw new Error(result.error || `Could not secure ${target}`);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
return target;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
export function ensurePrivateFile(target, options = {}) {
|
|
1265
|
-
const deps = { ..._deps, ...(options.deps || {}) };
|
|
1266
|
-
const platform = options.platform || deps.platform();
|
|
1267
|
-
assertSafeOwnerOnlyPath(target, platform);
|
|
1268
|
-
const nativeEvidence = assertNoLinkTraversalWithWindowsFallback(
|
|
1269
|
-
target,
|
|
1270
|
-
deps,
|
|
1271
|
-
null,
|
|
1272
|
-
platform,
|
|
1273
|
-
);
|
|
1274
|
-
const pathEntry = readPrivatePathEntry(
|
|
1275
|
-
target,
|
|
1276
|
-
deps,
|
|
1277
|
-
platform,
|
|
1278
|
-
nativeEvidence,
|
|
1279
|
-
);
|
|
1280
|
-
if (!pathEntry.exists) return target;
|
|
1281
|
-
const existing = pathEntry.entry;
|
|
1282
|
-
if (existing.isSymbolicLink()) {
|
|
1283
|
-
throw new Error(
|
|
1284
|
-
`Refusing owner-only file through a symbolic link: ${target}`,
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1287
|
-
if (typeof existing.isDirectory === "function" && existing.isDirectory()) {
|
|
1288
|
-
throw new Error(`Expected a file for owner-only storage: ${target}`);
|
|
1289
|
-
}
|
|
1290
|
-
if (platform !== "win32") {
|
|
1291
|
-
// Preserve ctime for an already-secure file. Rejected CAS writes and
|
|
1292
|
-
// read-only preflights must not make a persisted physical witness stale.
|
|
1293
|
-
const existingMode = Number(existing.mode);
|
|
1294
|
-
if (
|
|
1295
|
-
!Number.isFinite(existingMode) ||
|
|
1296
|
-
(existingMode & 0o7777) !== PRIVATE_FILE_MODE
|
|
1297
|
-
) {
|
|
1298
|
-
deps.fs.chmodSync(target, PRIVATE_FILE_MODE);
|
|
1299
|
-
}
|
|
1300
|
-
} else if (
|
|
1301
|
-
options.applyWindowsAcl === true &&
|
|
1302
|
-
(options.deps || !isSecuredWindowsPath(target, deps))
|
|
1303
|
-
) {
|
|
1304
|
-
const [result] = windowsAclBatch([String(target)], "repair", deps, "file");
|
|
1305
|
-
if (result.errorCode === "EXPECTED_KIND_MISMATCH") {
|
|
1306
|
-
throw new Error(`Expected a file for owner-only storage: ${target}`);
|
|
1307
|
-
}
|
|
1308
|
-
if (!result.ok && result.exists !== false && options.failIfUnavailable) {
|
|
1309
|
-
throw new Error(result.error || `Could not secure ${target}`);
|
|
1310
|
-
}
|
|
1311
|
-
if (result.ok && !options.deps) rememberSecuredWindowsPath(target, deps);
|
|
1312
|
-
}
|
|
1313
|
-
return target;
|
|
1314
|
-
}
|
|
1
|
+
// One owner-only path/ACL implementation shared by CLI and native desktop hosts.
|
|
2
|
+
import storage from "@chainlesschain/session-core/private-storage";
|
|
3
|
+
export const {
|
|
4
|
+
PRIVATE_DIRECTORY_MODE,
|
|
5
|
+
PRIVATE_FILE_MODE,
|
|
6
|
+
assertSafeOwnerOnlyPath,
|
|
7
|
+
_deps,
|
|
8
|
+
_windowsAclWorkingDirectory,
|
|
9
|
+
_resolveWindowsAclTimeout,
|
|
10
|
+
inspectPrivatePaths,
|
|
11
|
+
repairPrivatePaths,
|
|
12
|
+
inspectPrivatePath,
|
|
13
|
+
repairPrivatePath,
|
|
14
|
+
ensurePrivateDirectory,
|
|
15
|
+
ensurePrivateFile,
|
|
16
|
+
} = storage;
|