chainlesschain 0.165.2 → 0.165.4
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 +151 -29
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-lS1R0Cza.js → AIOps-C__nnraW.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BmlgBPsj.js → ActionButton-CsKWHEZ-.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CNKczv1S.js → Analytics-BhIjfjWO.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BuZGJARQ.js → AppLayout-BjuXFzWZ.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-Dm73Sl-O.js → Artifacts-k6whDkZJ.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BGMW7A5e.js → Audit-C4FBMYRh.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BQ6M54JR.js → BackgroundAgents-C_vRrSXk.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DBzGP0bR.js → Backup-CngFZeVp.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-D1mLCoOV.js → BaseInput-B_H9EBRm.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BUfcN7p8.js → Chat-DLEC5kt7.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-xijcWkJe.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-Du0Yg5Pi.js → Checkbox-m3ClR4xh.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B2RXziHW.js → Codegen-CmLSFrgp.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DWhekOrR.js → Col-DFGQteHN.js} +1 -1
- package/src/assets/web-panel/assets/{Community-HQhrkon4.js → Community-EUUVORHE.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-izHNb4uz.js → Compact-2PSg0Yr7.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BDwLy41-.js → Compliance-CcKur7gB.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DFJhLa7K.js → Cowork-DK_Fezfa.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DYY9RG7-.js → Cron-Dbw89xl7.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-rr7tJTtv.js → Crosschain-CH66h_Ec.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D2sfHRLi.js → DID-D1igRb17.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-q_eGdhha.js → Dashboard-DVAsrobk.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Cufrh2Bz.js → Dropdown-Dl7W8AQs.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-B-kdv10f.js → EmailListRenderer-D5tpaEQC.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D21XjgfG.js → FamilyGuardDashboard-O_VQf8gq.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CAwRTLZH.js → Federation-BTVcgS9n.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-vx3daNDF.js → FormItemContext-BRnkJAIz.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CRWY2wU4.js → GenericCardRenderer-8OWSCkDj.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Q6SwauyD.js → Git-WXLYTQj2.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BJ1q4IQZ.js → Governance-BezaUhtc.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CSWjROgD.js → Inference-CuRay3Gu.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-fzuQsWuY.js → KnowledgeGraph-DfWLDW1m.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DVF8Mnns.js → Logs-C55y2pPY.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-hDpWdiBc.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BQP7LOA4.js → Marketplace-Dk08Uycg.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BNlJCmNc.js → McpTools-C11fSB5V.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-DLLg5WQ4.js → Memory-mr0eZc97.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CR2skNAF.js → MobileBridge-C0iCSRbb.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BxVavObl.js → MobileProjects-CY-xMG8Z.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-B17pgqMI.js → Mtc-Bplqc_Wc.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-4UmV5cIL.js → MtcAudit-BlzwsoSM.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DoMEXEcU.js → Multisig-BBLmTty6.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-h9DMXFX4.js → NLProgramming-DtT1uf1W.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Cihn-xX8.js → Notes-DcqQAxTK.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BE6ZGa3R.js → NotificationSettings-cb0kWoZe.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CnwojoSI.js → OrderTableRenderer-kw9HZQNN.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B2vpwmjb.js → Organization-DlTlbkUN.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C4z28R0-.js → Overflow-pp1qs-oY.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BVlFkwhW.js → P2P-BESLnyYz.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BXbEdL5B.js → PdhVaultBrowser-B-J1mGuf.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-C28xs4lp.js → Permissions-B36PeT1_.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Dgl_rBjM.js → PersonalDataHub-CTK2ad-3.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-3QUIhrKI.js → Pipeline-zJNGXsac.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-8Gh33OZ5.js → Privacy-b9fz0Qf0.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BNmg4tZi.js → ProjectInit-DjZbZgiB.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-vgMfeCf9.js → ProjectSettings-C-hHJXlD.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BC9KSDM6.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CXoOa8eX.js → Providers-Dz-CAYYU.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BhfTF0oS.js → QrScannerModal-A0yoQ3-3.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-C746HEAj.js → QuickAsk-DrFgFJNC.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BSej8Eo0.js → Recommend-KC8hF1g4.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-f3gmnlG3.js → RemoteSession-DGTPIk6u.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-CyvybpkN.js → Reputation-QmbwM7Yk.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DwV2PeYn.js → Row-BxjJylia.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-11GrzjZy.js → RssFeed-B7Wda1J6.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BjKXkrAY.js → Search-_plQ_YSA.js} +1 -1
- package/src/assets/web-panel/assets/{Security-eWiFb9Yk.js → Security-CD2YJZfx.js} +2 -2
- package/src/assets/web-panel/assets/{Services-CdCsMAtt.js → Services-qQE7NmwE.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-4Q2UTKQ5.js → Skeleton-DgQqkFf7.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-v4rtYrRj.js → Skills-C4C0oFFt.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CRhsFvd_.js → Sla-Px2N2B5c.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DN6C3lQd.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-D16zonl7.js → SyncSettings-jm5aH_m7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Xxozz0i_.js → Tasks-DlkJcv67.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CXJpPtCT.js → Templates-DyrTHUBs.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-XRV0L1ZF.js → Tenant-mC434PtL.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CYuO1i_w.js → Terminal-o0_s3NgK.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BXqitWsu.js → TimelineRenderer-DAGlAXOw.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BvPrDHIy.js → Tokens-BnulS_bs.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DhCrSTf5.js → Trigger-suQbVXYM.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DmsuZD6K.js → Trust-DbIJAPo5.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-D_yrts5C.js → UkeySign-CQNpbIHi.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B01ihPrm.js → VideoEditing-D8aWCERk.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DpUjKa-N.js → Wallet-CuLpYfZc.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-CL07yON6.js → WebAuthn-CsfG8ZMc.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-CLMflnRr.js → WorkflowEditor-BYsEAZbd.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DtUqe8h_.js → chat-FT4Um3-I.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DPKrgVfy.js → colors-CKbS_Wlt.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BTt2GtOm.js → compact-item-CFJtzuJM.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D_VeUc1J.js → createContext-B9kSRW6K.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DJZ0gAMu.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-1kndiylj.js → hasIn-DR-x0Xpi.js} +1 -1
- package/src/assets/web-panel/assets/{index-dVc-CXY4.js → index-7jHr_A_k.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFIZ_1Sf.js → index-B3ZX81mK.js} +1 -1
- package/src/assets/web-panel/assets/{index-fk2Rjacw.js → index-B7osrXqy.js} +3 -3
- package/src/assets/web-panel/assets/{index-CHG-KcS4.js → index-BGbPnC5x.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dbbymxpj.js → index-BI5hP_qy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaZAbw9A.js → index-BK3TiKVq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzjkKUmR.js → index-BVPU43b1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAXfRKfG.js → index-BW-t9n-q.js} +1 -1
- package/src/assets/web-panel/assets/{index-8nz_fmlj.js → index-BfFCQHL3.js} +1 -1
- package/src/assets/web-panel/assets/index-Bizb4hkw.js +1 -0
- package/src/assets/web-panel/assets/{index-DdkplL9R.js → index-Bt4fS2eL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DG2MZQ3f.js → index-BwR7yc2h.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfKBRdSS.js → index-Bzz0C1BZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-GsoUQ2M7.js → index-C2JvmYRc.js} +1 -1
- package/src/assets/web-panel/assets/{index-l1BETrDb.js → index-CGaXUsIs.js} +1 -1
- package/src/assets/web-panel/assets/{index-4bna2_SR.js → index-CJmIQ9XR.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfGl8CqG.js → index-C_enk9I_.js} +1 -1
- package/src/assets/web-panel/assets/{index-D95OFht7.js → index-Cr2FLUsD.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsJCDUJG.js → index-CvrCCqJt.js} +1 -1
- package/src/assets/web-panel/assets/{index-sDoMHTNA.js → index-D4LfPvWA.js} +1 -1
- package/src/assets/web-panel/assets/{index-By5n8up9.js → index-DMGmg497.js} +1 -1
- package/src/assets/web-panel/assets/{index-ocf06EVQ.js → index-DShSkww-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bhudc_Ez.js → index-DTxNy0ZU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CX81qSM4.js → index-DW4BWjTk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlDNVgBt.js → index-DWdqx7zI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcO04eDn.js → index-Dap9c3eD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dwqmtk65.js → index-DcfxhKXR.js} +1 -1
- package/src/assets/web-panel/assets/{index-2eyAXE3d.js → index-DdkIhfi-.js} +1 -1
- package/src/assets/web-panel/assets/{index-brCInrzq.js → index-DeSgw-WV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJ8vXGOa.js → index-DiNUEpSQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHpBMWCh.js → index-DjEP7WkH.js} +1 -1
- package/src/assets/web-panel/assets/{index-m31Ka8hZ.js → index-DtkFqPVA.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtF7EN49.js → index-KqDaCApw.js} +1 -1
- package/src/assets/web-panel/assets/index-OHSl-xCh.js +1 -0
- package/src/assets/web-panel/assets/{index-B9KvcS7R.js → index-OSzjTzGM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CauoBQkH.js → index-QBSeNVO3.js} +1 -1
- package/src/assets/web-panel/assets/{index-FzdwcSy2.js → index-WE08gMJP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxdTdBix.js → index-r2VI1Ikk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BepLZwgB.js → index-tqSEHPRT.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CvyYJd7z.js → initDefaultProps-Dd7n7VgI.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DYZkhUdz.js → motion-BwxouRw1.js} +1 -1
- package/src/assets/web-panel/assets/{move-slnJRVT1.js → move-DObN0PG9.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DVc0CKmy.js → mtc-parser-B3nOUQc7.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BCXAtHCq.js → omit-o9WU2Nbl.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DBsemz_K.js → pickAttrs-CL_vvfQ_.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BRtkJ-nv.js → placementArrow-CYrM2tyK.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Cebq7EZn.js → responsiveObserve-k_06cQ96.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DT7qvId2.js → slide-CPKG0pxS.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-pQiITLkG.js → statusUtils-BDDEeLTw.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-c-19BVKQ.js → styleChecker-Cmb7RTh7.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DsS5-0bE.js → useFlexGapSupport-e2IedC4N.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CxGiMe5n.js → useFs-CE_gxVnm.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-D9btVID5.js → usePersonalDataHub-BkMIIKMz.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CUYFsFEN.js → vnode-DW886XV1.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CXmn_35j.js → zoom-Dwo_adaf.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/agent.js +105 -35
- package/src/commands/artifacts.js +363 -33
- package/src/commands/cowork.js +202 -36
- package/src/commands/session-budget.js +155 -0
- package/src/commands/session-location.js +948 -0
- package/src/commands/session.js +72 -46
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/artifact-protocol.js +77 -13
- package/src/gateways/ws/session-protocol.js +391 -24
- package/src/gateways/ws/ws-agent-handler.js +191 -14
- package/src/gateways/ws/ws-server.js +14 -3
- package/src/gateways/ws/ws-session-gateway.js +101 -5
- package/src/harness/jsonl-session-store.js +1210 -0
- package/src/lib/agent-team/team-runner.js +43 -13
- package/src/lib/artifact-access-ledger.js +461 -0
- package/src/lib/artifact-cleanup-ledger.js +861 -0
- package/src/lib/artifact-deletion-ledger.js +677 -0
- package/src/lib/artifact-store.js +256 -51
- package/src/lib/background-agent-keeper-protocol.js +75 -4
- package/src/lib/background-agent-launch-barrier.js +74 -0
- package/src/lib/background-agent-supervisor.js +149 -5
- package/src/lib/background-turn-bootstrap-protocol.js +4 -0
- package/src/lib/command-lifecycle-report.js +8 -4
- package/src/lib/cowork-adapter.js +9 -5
- package/src/lib/cowork-task-runner.js +20 -0
- package/src/lib/cowork-workflow.js +178 -20
- package/src/lib/direct-model-usage.js +62 -22
- package/src/lib/dynamic-workflow-runtime.js +3184 -111
- package/src/lib/execution-location-result-apply.js +339 -0
- package/src/lib/execution-location-result-artifact.js +271 -0
- package/src/lib/execution-location-result-review.js +253 -0
- package/src/lib/execution-location-result-store.js +331 -0
- package/src/lib/execution-location-result.js +617 -0
- package/src/lib/execution-location-target.js +236 -1
- package/src/lib/managed-tool-checkpoint.js +47 -4
- package/src/lib/process-execution-broker/workspace-transaction.js +3 -0
- package/src/lib/session-budget-production-root.js +376 -0
- package/src/lib/session-budget-runtime.js +109 -18
- package/src/lib/session-budget-usage.js +63 -0
- package/src/lib/session-host-snapshot.js +19 -2
- package/src/lib/session-resource-budget.js +766 -105
- package/src/lib/sub-agent-context.js +69 -22
- package/src/lib/web-ui-server.js +12 -1
- package/src/lib/with-file-lock.js +175 -26
- package/src/repl/agent-repl.js +237 -16
- package/src/runtime/agent-core.js +123 -0
- package/src/runtime/headless-runner.js +135 -46
- package/src/runtime/headless-stream.js +133 -25
- package/src/runtime/policies/agent-policy.js +10 -0
- package/src/workers/background-agent-keeper.js +112 -19
- package/src/workers/background-agent-worker.js +383 -134
- package/src/workers/background-turn-bootstrap.js +13 -8
- package/src/assets/web-panel/assets/ChatBubbleRenderer-CY5m8ZHg.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +0 -1
- package/src/assets/web-panel/assets/Projects-BhtAVilc.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +0 -1
- package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +0 -1
- package/src/assets/web-panel/assets/index-CPcM-eqo.js +0 -1
- package/src/assets/web-panel/assets/index-xeSyVoRe.js +0 -1
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { TextDecoder } from "node:util";
|
|
5
|
+
import { canonicalJson } from "./scheduler-kernel/contract.js";
|
|
6
|
+
import {
|
|
7
|
+
sameFileStatIdentity,
|
|
8
|
+
samePathHandleFileIdentity,
|
|
9
|
+
withTrustedFileParentSync,
|
|
10
|
+
} from "./secure-file-identity.js";
|
|
11
|
+
import {
|
|
12
|
+
ARTIFACT_DELETION_CLIENTS,
|
|
13
|
+
settleArtifactDeletion,
|
|
14
|
+
} from "./artifact-deletion-ledger.js";
|
|
15
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
16
|
+
|
|
17
|
+
export const ARTIFACT_CLEANUP_EVENT_SCHEMA =
|
|
18
|
+
"cc-artifact-cleanup-settlement/v1";
|
|
19
|
+
export const ARTIFACT_CLEANUP_LEDGER_SCHEMA =
|
|
20
|
+
"cc-artifact-cleanup-settlement-ledger/v1";
|
|
21
|
+
export const ARTIFACT_CLEANUP_RECEIPT_SCHEMA = "cc-artifact-cleanup-receipt/v1";
|
|
22
|
+
|
|
23
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,255}$/u;
|
|
24
|
+
const SHA256_RE = /^sha256:[a-f0-9]{64}$/u;
|
|
25
|
+
const RAW_SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
26
|
+
const MAX_LEDGER_BYTES = 32 * 1024 * 1024;
|
|
27
|
+
const MAX_LEDGER_EVENTS = 100_000;
|
|
28
|
+
const MAX_BATCH_ITEMS = 10_000;
|
|
29
|
+
|
|
30
|
+
function digest(domain, value) {
|
|
31
|
+
return `sha256:${createHash("sha256")
|
|
32
|
+
.update(domain, "utf8")
|
|
33
|
+
.update(canonicalJson(value, "artifactCleanupLedger"), "utf8")
|
|
34
|
+
.digest("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function exactObject(value, fields, label) {
|
|
38
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
39
|
+
throw new TypeError(`${label} must be an object`);
|
|
40
|
+
}
|
|
41
|
+
const actual = Object.keys(value).sort();
|
|
42
|
+
const expected = [...fields].sort();
|
|
43
|
+
if (
|
|
44
|
+
actual.length !== expected.length ||
|
|
45
|
+
actual.some((field, index) => field !== expected[index])
|
|
46
|
+
) {
|
|
47
|
+
throw new TypeError(`${label} has an invalid schema`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function optionalDigest(value, label) {
|
|
53
|
+
if (value === null) return null;
|
|
54
|
+
const normalized = String(value || "");
|
|
55
|
+
if (!SHA256_RE.test(normalized)) throw new TypeError(`${label} is invalid`);
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function optionalId(value, label) {
|
|
60
|
+
if (value === null) return null;
|
|
61
|
+
const normalized = String(value || "");
|
|
62
|
+
if (!ID_RE.test(normalized)) throw new TypeError(`${label} is invalid`);
|
|
63
|
+
return normalized;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeStoredFile(value) {
|
|
67
|
+
const normalized = String(value || "");
|
|
68
|
+
if (
|
|
69
|
+
!normalized ||
|
|
70
|
+
normalized !== path.basename(normalized) ||
|
|
71
|
+
normalized.includes("..") ||
|
|
72
|
+
normalized.includes("/") ||
|
|
73
|
+
normalized.includes("\\")
|
|
74
|
+
) {
|
|
75
|
+
throw new TypeError("artifact cleanup stored file is invalid");
|
|
76
|
+
}
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizePreparedItem(input) {
|
|
81
|
+
const value = exactObject(
|
|
82
|
+
input,
|
|
83
|
+
[
|
|
84
|
+
"artifactId",
|
|
85
|
+
"artifactSha256",
|
|
86
|
+
"artifactSize",
|
|
87
|
+
"recordDigest",
|
|
88
|
+
"artifactSessionId",
|
|
89
|
+
"storedFile",
|
|
90
|
+
"immutable",
|
|
91
|
+
"expiresAt",
|
|
92
|
+
"deletionId",
|
|
93
|
+
],
|
|
94
|
+
"artifact cleanup prepared item",
|
|
95
|
+
);
|
|
96
|
+
const artifactId = String(value.artifactId || "");
|
|
97
|
+
const artifactSha256 = String(value.artifactSha256 || "");
|
|
98
|
+
const artifactSize = Number(value.artifactSize);
|
|
99
|
+
const recordDigest = optionalDigest(
|
|
100
|
+
value.recordDigest,
|
|
101
|
+
"artifact cleanup record digest",
|
|
102
|
+
);
|
|
103
|
+
const artifactSessionId = optionalId(
|
|
104
|
+
value.artifactSessionId,
|
|
105
|
+
"artifact cleanup session id",
|
|
106
|
+
);
|
|
107
|
+
const storedFile = safeStoredFile(value.storedFile);
|
|
108
|
+
const expiresAt = String(value.expiresAt || "");
|
|
109
|
+
const deletionId = String(value.deletionId || "");
|
|
110
|
+
if (
|
|
111
|
+
!ID_RE.test(artifactId) ||
|
|
112
|
+
!SHA256_RE.test(artifactSha256) ||
|
|
113
|
+
!Number.isSafeInteger(artifactSize) ||
|
|
114
|
+
artifactSize < 0 ||
|
|
115
|
+
typeof value.immutable !== "boolean" ||
|
|
116
|
+
!Number.isFinite(Date.parse(expiresAt)) ||
|
|
117
|
+
!ID_RE.test(deletionId)
|
|
118
|
+
) {
|
|
119
|
+
throw new TypeError("artifact cleanup prepared item is invalid");
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
artifactId,
|
|
123
|
+
artifactSha256,
|
|
124
|
+
artifactSize,
|
|
125
|
+
recordDigest,
|
|
126
|
+
artifactSessionId,
|
|
127
|
+
storedFile,
|
|
128
|
+
immutable: value.immutable,
|
|
129
|
+
expiresAt,
|
|
130
|
+
deletionId,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeTerminalItem(input) {
|
|
135
|
+
const value = exactObject(
|
|
136
|
+
input,
|
|
137
|
+
[
|
|
138
|
+
"artifactId",
|
|
139
|
+
"deletionId",
|
|
140
|
+
"deletionEventDigest",
|
|
141
|
+
"managedCopyDisposition",
|
|
142
|
+
],
|
|
143
|
+
"artifact cleanup terminal item",
|
|
144
|
+
);
|
|
145
|
+
const artifactId = String(value.artifactId || "");
|
|
146
|
+
const deletionId = String(value.deletionId || "");
|
|
147
|
+
const deletionEventDigest = String(value.deletionEventDigest || "");
|
|
148
|
+
const managedCopyDisposition = String(value.managedCopyDisposition || "");
|
|
149
|
+
if (
|
|
150
|
+
!ID_RE.test(artifactId) ||
|
|
151
|
+
!ID_RE.test(deletionId) ||
|
|
152
|
+
!SHA256_RE.test(deletionEventDigest) ||
|
|
153
|
+
!["removed", "already-absent"].includes(managedCopyDisposition)
|
|
154
|
+
) {
|
|
155
|
+
throw new TypeError("artifact cleanup terminal item is invalid");
|
|
156
|
+
}
|
|
157
|
+
return Object.freeze({
|
|
158
|
+
artifactId,
|
|
159
|
+
deletionId,
|
|
160
|
+
deletionEventDigest,
|
|
161
|
+
managedCopyDisposition,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function normalizeArtifactCleanupEvent(input, previous = null) {
|
|
166
|
+
const value = exactObject(
|
|
167
|
+
input,
|
|
168
|
+
[
|
|
169
|
+
"schema",
|
|
170
|
+
"sequence",
|
|
171
|
+
"previousEventDigest",
|
|
172
|
+
"cleanupId",
|
|
173
|
+
"phase",
|
|
174
|
+
"preparedEventDigest",
|
|
175
|
+
"client",
|
|
176
|
+
"cutoffAt",
|
|
177
|
+
"scopeDigest",
|
|
178
|
+
"itemCount",
|
|
179
|
+
"items",
|
|
180
|
+
"indexGenerationBefore",
|
|
181
|
+
"indexGenerationAfter",
|
|
182
|
+
"occurredAt",
|
|
183
|
+
"eventDigest",
|
|
184
|
+
],
|
|
185
|
+
"artifact cleanup event",
|
|
186
|
+
);
|
|
187
|
+
const sequence = Number(value.sequence);
|
|
188
|
+
const previousEventDigest = optionalDigest(
|
|
189
|
+
value.previousEventDigest,
|
|
190
|
+
"artifact cleanup predecessor digest",
|
|
191
|
+
);
|
|
192
|
+
const cleanupId = String(value.cleanupId || "");
|
|
193
|
+
const phase = String(value.phase || "");
|
|
194
|
+
const preparedEventDigest = optionalDigest(
|
|
195
|
+
value.preparedEventDigest,
|
|
196
|
+
"artifact cleanup prepared event digest",
|
|
197
|
+
);
|
|
198
|
+
const client = String(value.client || "");
|
|
199
|
+
const cutoffAt = String(value.cutoffAt || "");
|
|
200
|
+
const scopeDigest = String(value.scopeDigest || "");
|
|
201
|
+
const itemCount = Number(value.itemCount);
|
|
202
|
+
const indexGenerationBefore = String(value.indexGenerationBefore || "");
|
|
203
|
+
const indexGenerationAfter = optionalDigest(
|
|
204
|
+
value.indexGenerationAfter,
|
|
205
|
+
"artifact cleanup terminal index generation",
|
|
206
|
+
);
|
|
207
|
+
const occurredAt = String(value.occurredAt || "");
|
|
208
|
+
if (
|
|
209
|
+
value.schema !== ARTIFACT_CLEANUP_EVENT_SCHEMA ||
|
|
210
|
+
!Number.isSafeInteger(sequence) ||
|
|
211
|
+
sequence < 1 ||
|
|
212
|
+
!ID_RE.test(cleanupId) ||
|
|
213
|
+
!["prepared", "terminal"].includes(phase) ||
|
|
214
|
+
!ARTIFACT_DELETION_CLIENTS.includes(client) ||
|
|
215
|
+
!Number.isFinite(Date.parse(cutoffAt)) ||
|
|
216
|
+
!SHA256_RE.test(scopeDigest) ||
|
|
217
|
+
!Number.isSafeInteger(itemCount) ||
|
|
218
|
+
itemCount < 0 ||
|
|
219
|
+
itemCount > MAX_BATCH_ITEMS ||
|
|
220
|
+
!Array.isArray(value.items) ||
|
|
221
|
+
value.items.length !== itemCount ||
|
|
222
|
+
!SHA256_RE.test(indexGenerationBefore) ||
|
|
223
|
+
!Number.isFinite(Date.parse(occurredAt)) ||
|
|
224
|
+
!SHA256_RE.test(String(value.eventDigest || ""))
|
|
225
|
+
) {
|
|
226
|
+
throw new TypeError("artifact cleanup event is invalid");
|
|
227
|
+
}
|
|
228
|
+
const items = Object.freeze(
|
|
229
|
+
value.items.map((item) =>
|
|
230
|
+
phase === "prepared"
|
|
231
|
+
? normalizePreparedItem(item)
|
|
232
|
+
: normalizeTerminalItem(item),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
if (
|
|
236
|
+
(phase === "prepared" &&
|
|
237
|
+
(preparedEventDigest !== null || indexGenerationAfter !== null)) ||
|
|
238
|
+
(phase === "terminal" && (!preparedEventDigest || !indexGenerationAfter))
|
|
239
|
+
) {
|
|
240
|
+
throw new TypeError("artifact cleanup phase fields are invalid");
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
sequence !== (previous?.sequence || 0) + 1 ||
|
|
244
|
+
previousEventDigest !== (previous?.eventDigest || null)
|
|
245
|
+
) {
|
|
246
|
+
throw new Error("artifact cleanup event chain is invalid");
|
|
247
|
+
}
|
|
248
|
+
const material = {
|
|
249
|
+
schema: value.schema,
|
|
250
|
+
sequence,
|
|
251
|
+
previousEventDigest,
|
|
252
|
+
cleanupId,
|
|
253
|
+
phase,
|
|
254
|
+
preparedEventDigest,
|
|
255
|
+
client,
|
|
256
|
+
cutoffAt,
|
|
257
|
+
scopeDigest,
|
|
258
|
+
itemCount,
|
|
259
|
+
items,
|
|
260
|
+
indexGenerationBefore,
|
|
261
|
+
indexGenerationAfter,
|
|
262
|
+
occurredAt,
|
|
263
|
+
};
|
|
264
|
+
if (
|
|
265
|
+
value.eventDigest !==
|
|
266
|
+
digest("chainlesschain.artifact.cleanup-settlement-event.v1\0", material)
|
|
267
|
+
) {
|
|
268
|
+
throw new Error("artifact cleanup event digest is invalid");
|
|
269
|
+
}
|
|
270
|
+
return Object.freeze({ ...material, eventDigest: value.eventDigest });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function validateBatchEvents(events) {
|
|
274
|
+
const byId = new Map();
|
|
275
|
+
for (const event of events) {
|
|
276
|
+
const current = byId.get(event.cleanupId) || {
|
|
277
|
+
prepared: null,
|
|
278
|
+
terminal: null,
|
|
279
|
+
};
|
|
280
|
+
if (event.phase === "prepared") {
|
|
281
|
+
if (current.prepared) {
|
|
282
|
+
throw new Error("artifact cleanup id has duplicate preparations");
|
|
283
|
+
}
|
|
284
|
+
current.prepared = event;
|
|
285
|
+
} else {
|
|
286
|
+
if (current.terminal) {
|
|
287
|
+
throw new Error("artifact cleanup id has duplicate terminals");
|
|
288
|
+
}
|
|
289
|
+
current.terminal = event;
|
|
290
|
+
}
|
|
291
|
+
byId.set(event.cleanupId, current);
|
|
292
|
+
}
|
|
293
|
+
for (const state of byId.values()) {
|
|
294
|
+
if (!state.terminal) continue;
|
|
295
|
+
const { prepared, terminal } = state;
|
|
296
|
+
if (
|
|
297
|
+
!prepared ||
|
|
298
|
+
terminal.preparedEventDigest !== prepared.eventDigest ||
|
|
299
|
+
terminal.client !== prepared.client ||
|
|
300
|
+
terminal.cutoffAt !== prepared.cutoffAt ||
|
|
301
|
+
terminal.scopeDigest !== prepared.scopeDigest ||
|
|
302
|
+
terminal.itemCount !== prepared.itemCount ||
|
|
303
|
+
terminal.indexGenerationBefore !== prepared.indexGenerationBefore
|
|
304
|
+
) {
|
|
305
|
+
throw new Error("artifact cleanup terminal has no unique preparation");
|
|
306
|
+
}
|
|
307
|
+
for (let index = 0; index < prepared.items.length; index += 1) {
|
|
308
|
+
const preparedItem = prepared.items[index];
|
|
309
|
+
const terminalItem = terminal.items[index];
|
|
310
|
+
if (
|
|
311
|
+
terminalItem.artifactId !== preparedItem.artifactId ||
|
|
312
|
+
terminalItem.deletionId !== preparedItem.deletionId
|
|
313
|
+
) {
|
|
314
|
+
throw new Error("artifact cleanup terminal item order is invalid");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return byId;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function ledgerPath(store) {
|
|
322
|
+
return path.join(path.resolve(store.dir), "cleanup-settlements.jsonl");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function readLedgerBytes(filePath, runtimeFs, runtime = undefined) {
|
|
326
|
+
return withTrustedFileParentSync(
|
|
327
|
+
runtimeFs,
|
|
328
|
+
filePath,
|
|
329
|
+
({ canonicalPath, parentDevice }) => {
|
|
330
|
+
let before;
|
|
331
|
+
try {
|
|
332
|
+
before = runtimeFs.lstatSync(canonicalPath, { bigint: true });
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (error?.code === "ENOENT") return Buffer.alloc(0);
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
if (
|
|
338
|
+
before.isSymbolicLink() ||
|
|
339
|
+
!before.isFile() ||
|
|
340
|
+
Number(before.nlink) !== 1 ||
|
|
341
|
+
Number(before.size) > MAX_LEDGER_BYTES
|
|
342
|
+
) {
|
|
343
|
+
throw new Error("artifact cleanup ledger identity is invalid");
|
|
344
|
+
}
|
|
345
|
+
let descriptor = null;
|
|
346
|
+
try {
|
|
347
|
+
descriptor = runtimeFs.openSync(
|
|
348
|
+
canonicalPath,
|
|
349
|
+
runtimeFs.constants.O_RDONLY |
|
|
350
|
+
Number(runtimeFs.constants.O_NOFOLLOW || 0),
|
|
351
|
+
);
|
|
352
|
+
const opened = runtimeFs.fstatSync(descriptor, { bigint: true });
|
|
353
|
+
if (
|
|
354
|
+
!opened.isFile() ||
|
|
355
|
+
Number(opened.nlink) !== 1 ||
|
|
356
|
+
!samePathHandleFileIdentity(before, opened, parentDevice, runtime)
|
|
357
|
+
) {
|
|
358
|
+
throw new Error("artifact cleanup ledger handle is invalid");
|
|
359
|
+
}
|
|
360
|
+
const bytes = runtimeFs.readFileSync(descriptor);
|
|
361
|
+
const after = runtimeFs.fstatSync(descriptor, { bigint: true });
|
|
362
|
+
if (
|
|
363
|
+
bytes.length > MAX_LEDGER_BYTES ||
|
|
364
|
+
Number(after.size) !== bytes.length ||
|
|
365
|
+
!sameFileStatIdentity(opened, after)
|
|
366
|
+
) {
|
|
367
|
+
throw new Error("artifact cleanup ledger changed while reading");
|
|
368
|
+
}
|
|
369
|
+
return bytes;
|
|
370
|
+
} finally {
|
|
371
|
+
if (descriptor !== null) runtimeFs.closeSync(descriptor);
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{ runtime },
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function parseLedger(bytes) {
|
|
379
|
+
if (bytes.length === 0) return [];
|
|
380
|
+
let text;
|
|
381
|
+
try {
|
|
382
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
383
|
+
} catch {
|
|
384
|
+
throw new Error("artifact cleanup ledger is not strict UTF-8");
|
|
385
|
+
}
|
|
386
|
+
if (!text.endsWith("\n")) {
|
|
387
|
+
throw new Error("artifact cleanup ledger has a truncated tail");
|
|
388
|
+
}
|
|
389
|
+
const lines = text.slice(0, -1).split("\n");
|
|
390
|
+
if (lines.length > MAX_LEDGER_EVENTS) {
|
|
391
|
+
throw new Error("artifact cleanup ledger exceeds its event limit");
|
|
392
|
+
}
|
|
393
|
+
const events = [];
|
|
394
|
+
for (const line of lines) {
|
|
395
|
+
let parsed;
|
|
396
|
+
try {
|
|
397
|
+
parsed = JSON.parse(line);
|
|
398
|
+
} catch {
|
|
399
|
+
throw new Error("artifact cleanup ledger contains invalid JSON");
|
|
400
|
+
}
|
|
401
|
+
events.push(normalizeArtifactCleanupEvent(parsed, events.at(-1) || null));
|
|
402
|
+
}
|
|
403
|
+
validateBatchEvents(events);
|
|
404
|
+
return events;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function readArtifactCleanupLedger(store, options = {}) {
|
|
408
|
+
if (!store?.dir) {
|
|
409
|
+
throw new TypeError("artifact cleanup ledger requires an ArtifactStore");
|
|
410
|
+
}
|
|
411
|
+
const events = parseLedger(
|
|
412
|
+
readLedgerBytes(ledgerPath(store), options.fs || fs, options.runtime),
|
|
413
|
+
);
|
|
414
|
+
return Object.freeze({
|
|
415
|
+
schema: ARTIFACT_CLEANUP_LEDGER_SCHEMA,
|
|
416
|
+
eventCount: events.length,
|
|
417
|
+
preparedCount: events.filter((event) => event.phase === "prepared").length,
|
|
418
|
+
terminalCount: events.filter((event) => event.phase === "terminal").length,
|
|
419
|
+
pendingCount: [...validateBatchEvents(events).values()].filter(
|
|
420
|
+
(state) => state.prepared && !state.terminal,
|
|
421
|
+
).length,
|
|
422
|
+
headDigest: events.at(-1)?.eventDigest || null,
|
|
423
|
+
events: Object.freeze(events),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function normalizeRequest(input = {}) {
|
|
428
|
+
const cleanupId = String(
|
|
429
|
+
input.cleanupId || `cleanup_${randomUUID().replaceAll("-", "")}`,
|
|
430
|
+
);
|
|
431
|
+
const client = String(input.client || "cli");
|
|
432
|
+
if (!ID_RE.test(cleanupId) || !ARTIFACT_DELETION_CLIENTS.includes(client)) {
|
|
433
|
+
throw new TypeError("artifact cleanup request is invalid");
|
|
434
|
+
}
|
|
435
|
+
return Object.freeze({ cleanupId, client });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function indexGenerationDigest(entries) {
|
|
439
|
+
return digest("chainlesschain.artifact.index-generation.v1\0", entries);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function cleanupDeletionId(cleanupId, entry) {
|
|
443
|
+
return `cleanup_item_${createHash("sha256")
|
|
444
|
+
.update("chainlesschain.artifact.cleanup-item.v1\0", "utf8")
|
|
445
|
+
.update(cleanupId, "utf8")
|
|
446
|
+
.update("\0", "utf8")
|
|
447
|
+
.update(String(entry.id || ""), "utf8")
|
|
448
|
+
.update("\0", "utf8")
|
|
449
|
+
.update(String(entry.sha256 || ""), "utf8")
|
|
450
|
+
.digest("hex")}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function preparedItem(cleanupId, entry) {
|
|
454
|
+
const artifactId = String(entry?.id || "");
|
|
455
|
+
const rawSha256 = String(entry?.sha256 || "");
|
|
456
|
+
const artifactSize = Number(entry?.size);
|
|
457
|
+
const expiresAt = String(entry?.expiresAt || "");
|
|
458
|
+
if (
|
|
459
|
+
!ID_RE.test(artifactId) ||
|
|
460
|
+
!RAW_SHA256_RE.test(rawSha256) ||
|
|
461
|
+
!Number.isSafeInteger(artifactSize) ||
|
|
462
|
+
artifactSize < 0 ||
|
|
463
|
+
!Number.isFinite(Date.parse(expiresAt))
|
|
464
|
+
) {
|
|
465
|
+
throw new Error("artifact index row cannot authorize cleanup");
|
|
466
|
+
}
|
|
467
|
+
return normalizePreparedItem({
|
|
468
|
+
artifactId,
|
|
469
|
+
artifactSha256: `sha256:${rawSha256}`,
|
|
470
|
+
artifactSize,
|
|
471
|
+
recordDigest:
|
|
472
|
+
entry.recordDigest == null
|
|
473
|
+
? null
|
|
474
|
+
: optionalDigest(entry.recordDigest, "artifact cleanup record digest"),
|
|
475
|
+
artifactSessionId:
|
|
476
|
+
entry.sessionId == null
|
|
477
|
+
? null
|
|
478
|
+
: optionalId(entry.sessionId, "artifact cleanup session id"),
|
|
479
|
+
storedFile: safeStoredFile(entry.file),
|
|
480
|
+
immutable: entry.immutable === true,
|
|
481
|
+
expiresAt,
|
|
482
|
+
deletionId: cleanupDeletionId(cleanupId, entry),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function appendEvent(filePath, event, expectedSize, runtimeFs) {
|
|
487
|
+
let descriptor = null;
|
|
488
|
+
try {
|
|
489
|
+
descriptor = runtimeFs.openSync(
|
|
490
|
+
filePath,
|
|
491
|
+
runtimeFs.constants.O_WRONLY |
|
|
492
|
+
runtimeFs.constants.O_APPEND |
|
|
493
|
+
runtimeFs.constants.O_CREAT |
|
|
494
|
+
Number(runtimeFs.constants.O_NOFOLLOW || 0),
|
|
495
|
+
0o600,
|
|
496
|
+
);
|
|
497
|
+
const opened = runtimeFs.fstatSync(descriptor, { bigint: true });
|
|
498
|
+
if (
|
|
499
|
+
!opened.isFile() ||
|
|
500
|
+
Number(opened.nlink) !== 1 ||
|
|
501
|
+
Number(opened.size) !== expectedSize
|
|
502
|
+
) {
|
|
503
|
+
throw new Error("artifact cleanup ledger changed before append");
|
|
504
|
+
}
|
|
505
|
+
runtimeFs.writeFileSync(descriptor, `${JSON.stringify(event)}\n`, "utf8");
|
|
506
|
+
runtimeFs.fsyncSync(descriptor);
|
|
507
|
+
} finally {
|
|
508
|
+
if (descriptor !== null) runtimeFs.closeSync(descriptor);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function eventFor(material, previous) {
|
|
513
|
+
return normalizeArtifactCleanupEvent(
|
|
514
|
+
{
|
|
515
|
+
...material,
|
|
516
|
+
eventDigest: digest(
|
|
517
|
+
"chainlesschain.artifact.cleanup-settlement-event.v1\0",
|
|
518
|
+
material,
|
|
519
|
+
),
|
|
520
|
+
},
|
|
521
|
+
previous,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function sameTerminalItems(left, right) {
|
|
526
|
+
return (
|
|
527
|
+
canonicalJson(left, "artifactCleanupTerminalItems") ===
|
|
528
|
+
canonicalJson(right, "artifactCleanupTerminalItems")
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function managedPath(store, storedFile) {
|
|
533
|
+
const filesRoot = path.resolve(store.dir, "files");
|
|
534
|
+
const target = path.resolve(filesRoot, safeStoredFile(storedFile));
|
|
535
|
+
if (
|
|
536
|
+
target !== path.join(filesRoot, storedFile) ||
|
|
537
|
+
!target.startsWith(`${filesRoot}${path.sep}`)
|
|
538
|
+
) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
"artifact cleanup path escapes the managed files directory",
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
return target;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function assertPreparedScopeAbsent(store, prepared, runtimeFs) {
|
|
547
|
+
const currentEntries = store._readEntries();
|
|
548
|
+
const selectedIds = new Set(prepared.items.map((item) => item.artifactId));
|
|
549
|
+
if (currentEntries.some((entry) => selectedIds.has(String(entry.id)))) {
|
|
550
|
+
throw new Error("artifact cleanup scope reappeared after settlement");
|
|
551
|
+
}
|
|
552
|
+
for (const item of prepared.items) {
|
|
553
|
+
try {
|
|
554
|
+
runtimeFs.lstatSync(managedPath(store, item.storedFile));
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (error?.code === "ENOENT") continue;
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
throw new Error(
|
|
560
|
+
"artifact cleanup managed path reappeared after settlement",
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function prepareBatch(store, request, options, runtimeFs, filePath) {
|
|
566
|
+
return store._withIndexLock(() =>
|
|
567
|
+
(options.withFileLock || withFileLock)(
|
|
568
|
+
filePath,
|
|
569
|
+
() => {
|
|
570
|
+
const ledgerBytes = readLedgerBytes(
|
|
571
|
+
filePath,
|
|
572
|
+
runtimeFs,
|
|
573
|
+
options.runtime,
|
|
574
|
+
);
|
|
575
|
+
const events = parseLedger(ledgerBytes);
|
|
576
|
+
const batches = validateBatchEvents(events);
|
|
577
|
+
const state = batches.get(request.cleanupId) || {
|
|
578
|
+
prepared: null,
|
|
579
|
+
terminal: null,
|
|
580
|
+
};
|
|
581
|
+
if (state.prepared) {
|
|
582
|
+
if (state.prepared.client !== request.client) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
"artifact cleanup id is already bound to other inputs",
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
if (state.terminal) {
|
|
588
|
+
assertPreparedScopeAbsent(store, state.prepared, runtimeFs);
|
|
589
|
+
}
|
|
590
|
+
return Object.freeze({
|
|
591
|
+
prepared: state.prepared,
|
|
592
|
+
terminal: state.terminal,
|
|
593
|
+
preparedRecorded: false,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const entries = store._readEntries();
|
|
598
|
+
const cutoffAt = new Date(
|
|
599
|
+
typeof options.now === "function" ? options.now() : Date.now(),
|
|
600
|
+
).toISOString();
|
|
601
|
+
const cutoffMs = Date.parse(cutoffAt);
|
|
602
|
+
const expired = entries.filter((entry) => {
|
|
603
|
+
const expiresAt = Date.parse(entry.expiresAt || "");
|
|
604
|
+
return Number.isFinite(expiresAt) && expiresAt <= cutoffMs;
|
|
605
|
+
});
|
|
606
|
+
if (expired.length > MAX_BATCH_ITEMS) {
|
|
607
|
+
throw new Error("artifact cleanup batch exceeds its item limit");
|
|
608
|
+
}
|
|
609
|
+
const duplicateIds = new Set();
|
|
610
|
+
const seenIds = new Set();
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
const id = String(entry?.id || "");
|
|
613
|
+
if (seenIds.has(id)) duplicateIds.add(id);
|
|
614
|
+
seenIds.add(id);
|
|
615
|
+
}
|
|
616
|
+
if (expired.some((entry) => duplicateIds.has(String(entry.id)))) {
|
|
617
|
+
throw new Error("artifact cleanup scope contains an ambiguous id");
|
|
618
|
+
}
|
|
619
|
+
const items = Object.freeze(
|
|
620
|
+
expired.map((entry) => preparedItem(request.cleanupId, entry)),
|
|
621
|
+
);
|
|
622
|
+
const itemIds = new Set(items.map((item) => item.artifactId));
|
|
623
|
+
for (const batch of batches.values()) {
|
|
624
|
+
if (!batch.prepared || batch.terminal) continue;
|
|
625
|
+
if (
|
|
626
|
+
batch.prepared.items.some((item) => itemIds.has(item.artifactId))
|
|
627
|
+
) {
|
|
628
|
+
throw new Error("artifact cleanup scope overlaps a pending batch");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const scopeDigest = digest(
|
|
632
|
+
"chainlesschain.artifact.cleanup-scope.v1\0",
|
|
633
|
+
{ cutoffAt, items },
|
|
634
|
+
);
|
|
635
|
+
const previous = events.at(-1) || null;
|
|
636
|
+
const material = {
|
|
637
|
+
schema: ARTIFACT_CLEANUP_EVENT_SCHEMA,
|
|
638
|
+
sequence: (previous?.sequence || 0) + 1,
|
|
639
|
+
previousEventDigest: previous?.eventDigest || null,
|
|
640
|
+
cleanupId: request.cleanupId,
|
|
641
|
+
phase: "prepared",
|
|
642
|
+
preparedEventDigest: null,
|
|
643
|
+
client: request.client,
|
|
644
|
+
cutoffAt,
|
|
645
|
+
scopeDigest,
|
|
646
|
+
itemCount: items.length,
|
|
647
|
+
items,
|
|
648
|
+
indexGenerationBefore: indexGenerationDigest(entries),
|
|
649
|
+
indexGenerationAfter: null,
|
|
650
|
+
occurredAt: cutoffAt,
|
|
651
|
+
};
|
|
652
|
+
const prepared = eventFor(material, previous);
|
|
653
|
+
appendEvent(filePath, prepared, ledgerBytes.length, runtimeFs);
|
|
654
|
+
return Object.freeze({
|
|
655
|
+
prepared,
|
|
656
|
+
terminal: null,
|
|
657
|
+
preparedRecorded: true,
|
|
658
|
+
});
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
failIfUnavailable: true,
|
|
662
|
+
timeoutMs: 30_000,
|
|
663
|
+
retryMs: 1,
|
|
664
|
+
maxRetryMs: 8,
|
|
665
|
+
retryJitterMs: 4,
|
|
666
|
+
},
|
|
667
|
+
),
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function appendTerminal(
|
|
672
|
+
store,
|
|
673
|
+
request,
|
|
674
|
+
prepared,
|
|
675
|
+
terminalItems,
|
|
676
|
+
options,
|
|
677
|
+
runtimeFs,
|
|
678
|
+
filePath,
|
|
679
|
+
) {
|
|
680
|
+
return store._withIndexLock(() =>
|
|
681
|
+
(options.withFileLock || withFileLock)(
|
|
682
|
+
filePath,
|
|
683
|
+
() => {
|
|
684
|
+
const ledgerBytes = readLedgerBytes(
|
|
685
|
+
filePath,
|
|
686
|
+
runtimeFs,
|
|
687
|
+
options.runtime,
|
|
688
|
+
);
|
|
689
|
+
const events = parseLedger(ledgerBytes);
|
|
690
|
+
const state = validateBatchEvents(events).get(request.cleanupId);
|
|
691
|
+
if (
|
|
692
|
+
!state?.prepared ||
|
|
693
|
+
state.prepared.eventDigest !== prepared.eventDigest
|
|
694
|
+
) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
"artifact cleanup preparation changed before terminal",
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
assertPreparedScopeAbsent(store, prepared, runtimeFs);
|
|
700
|
+
if (state.terminal) {
|
|
701
|
+
if (!sameTerminalItems(state.terminal.items, terminalItems)) {
|
|
702
|
+
throw new Error("artifact cleanup terminal summary is ambiguous");
|
|
703
|
+
}
|
|
704
|
+
return Object.freeze({ terminal: state.terminal, recorded: false });
|
|
705
|
+
}
|
|
706
|
+
const currentEntries = store._readEntries();
|
|
707
|
+
const previous = events.at(-1) || null;
|
|
708
|
+
const terminal = eventFor(
|
|
709
|
+
{
|
|
710
|
+
schema: ARTIFACT_CLEANUP_EVENT_SCHEMA,
|
|
711
|
+
sequence: (previous?.sequence || 0) + 1,
|
|
712
|
+
previousEventDigest: previous?.eventDigest || null,
|
|
713
|
+
cleanupId: request.cleanupId,
|
|
714
|
+
phase: "terminal",
|
|
715
|
+
preparedEventDigest: prepared.eventDigest,
|
|
716
|
+
client: prepared.client,
|
|
717
|
+
cutoffAt: prepared.cutoffAt,
|
|
718
|
+
scopeDigest: prepared.scopeDigest,
|
|
719
|
+
itemCount: prepared.itemCount,
|
|
720
|
+
items: terminalItems,
|
|
721
|
+
indexGenerationBefore: prepared.indexGenerationBefore,
|
|
722
|
+
indexGenerationAfter: indexGenerationDigest(currentEntries),
|
|
723
|
+
occurredAt: new Date(
|
|
724
|
+
typeof options.now === "function" ? options.now() : Date.now(),
|
|
725
|
+
).toISOString(),
|
|
726
|
+
},
|
|
727
|
+
previous,
|
|
728
|
+
);
|
|
729
|
+
appendEvent(filePath, terminal, ledgerBytes.length, runtimeFs);
|
|
730
|
+
return Object.freeze({ terminal, recorded: true });
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
failIfUnavailable: true,
|
|
734
|
+
timeoutMs: 30_000,
|
|
735
|
+
retryMs: 1,
|
|
736
|
+
maxRetryMs: 8,
|
|
737
|
+
retryJitterMs: 4,
|
|
738
|
+
},
|
|
739
|
+
),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function receipt(request, prepared, terminal, recorded, preparedRecorded) {
|
|
744
|
+
const removedNow = terminal.items.filter(
|
|
745
|
+
(item) => item.managedCopyDisposition === "removed",
|
|
746
|
+
).length;
|
|
747
|
+
const alreadyAbsent = terminal.items.length - removedNow;
|
|
748
|
+
return Object.freeze({
|
|
749
|
+
schema: ARTIFACT_CLEANUP_RECEIPT_SCHEMA,
|
|
750
|
+
cleanupId: request.cleanupId,
|
|
751
|
+
client: request.client,
|
|
752
|
+
cutoffAt: prepared.cutoffAt,
|
|
753
|
+
scopeDigest: prepared.scopeDigest,
|
|
754
|
+
selected: prepared.itemCount,
|
|
755
|
+
removed: prepared.itemCount,
|
|
756
|
+
removedNow,
|
|
757
|
+
alreadyAbsent,
|
|
758
|
+
settled: true,
|
|
759
|
+
preparedRecorded,
|
|
760
|
+
recorded,
|
|
761
|
+
cleanup: terminal,
|
|
762
|
+
items: terminal.items,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function settleArtifactCleanup(store, input = {}, options = {}) {
|
|
767
|
+
if (
|
|
768
|
+
!store?.dir ||
|
|
769
|
+
typeof store._withIndexLock !== "function" ||
|
|
770
|
+
typeof store._readEntries !== "function"
|
|
771
|
+
) {
|
|
772
|
+
throw new TypeError("artifact cleanup requires an ArtifactStore");
|
|
773
|
+
}
|
|
774
|
+
const request = normalizeRequest(input);
|
|
775
|
+
const runtimeFs = options.fs || fs;
|
|
776
|
+
const filePath = ledgerPath(store);
|
|
777
|
+
runtimeFs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
778
|
+
try {
|
|
779
|
+
const batch = prepareBatch(store, request, options, runtimeFs, filePath);
|
|
780
|
+
const settleDeletion =
|
|
781
|
+
options.settleArtifactDeletion || settleArtifactDeletion;
|
|
782
|
+
const terminalItems = [];
|
|
783
|
+
for (let index = 0; index < batch.prepared.items.length; index += 1) {
|
|
784
|
+
const item = batch.prepared.items[index];
|
|
785
|
+
try {
|
|
786
|
+
const deletion = settleDeletion(
|
|
787
|
+
store,
|
|
788
|
+
{
|
|
789
|
+
deletionId: item.deletionId,
|
|
790
|
+
artifactId: item.artifactId,
|
|
791
|
+
reason: "expired",
|
|
792
|
+
client: request.client,
|
|
793
|
+
},
|
|
794
|
+
options.deletionOptions || {},
|
|
795
|
+
);
|
|
796
|
+
if (!deletion?.found || !deletion?.settled || !deletion?.deletion) {
|
|
797
|
+
throw new Error("artifact cleanup item has no deletion settlement");
|
|
798
|
+
}
|
|
799
|
+
const terminalItem = normalizeTerminalItem({
|
|
800
|
+
artifactId: item.artifactId,
|
|
801
|
+
deletionId: item.deletionId,
|
|
802
|
+
deletionEventDigest: deletion.deletion.eventDigest,
|
|
803
|
+
managedCopyDisposition: deletion.deletion.managedCopyDisposition,
|
|
804
|
+
});
|
|
805
|
+
terminalItems.push(terminalItem);
|
|
806
|
+
if (typeof options.afterItem === "function") {
|
|
807
|
+
options.afterItem(
|
|
808
|
+
Object.freeze({
|
|
809
|
+
cleanupId: request.cleanupId,
|
|
810
|
+
index,
|
|
811
|
+
selected: batch.prepared.itemCount,
|
|
812
|
+
item: terminalItem,
|
|
813
|
+
}),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
} catch (error) {
|
|
817
|
+
error.cleanupId = request.cleanupId;
|
|
818
|
+
error.cleanup = Object.freeze({
|
|
819
|
+
selected: batch.prepared.itemCount,
|
|
820
|
+
settled: terminalItems.length,
|
|
821
|
+
pending: batch.prepared.itemCount - terminalItems.length,
|
|
822
|
+
artifactId: item.artifactId,
|
|
823
|
+
deletionId: item.deletionId,
|
|
824
|
+
});
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
let terminalResult;
|
|
829
|
+
try {
|
|
830
|
+
terminalResult = appendTerminal(
|
|
831
|
+
store,
|
|
832
|
+
request,
|
|
833
|
+
batch.prepared,
|
|
834
|
+
Object.freeze(terminalItems),
|
|
835
|
+
options,
|
|
836
|
+
runtimeFs,
|
|
837
|
+
filePath,
|
|
838
|
+
);
|
|
839
|
+
} catch (error) {
|
|
840
|
+
error.cleanupId = request.cleanupId;
|
|
841
|
+
error.cleanup = Object.freeze({
|
|
842
|
+
selected: batch.prepared.itemCount,
|
|
843
|
+
settled: terminalItems.length,
|
|
844
|
+
pending: batch.prepared.itemCount - terminalItems.length,
|
|
845
|
+
artifactId: null,
|
|
846
|
+
deletionId: null,
|
|
847
|
+
});
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
return receipt(
|
|
851
|
+
request,
|
|
852
|
+
batch.prepared,
|
|
853
|
+
terminalResult.terminal,
|
|
854
|
+
terminalResult.recorded,
|
|
855
|
+
batch.preparedRecorded,
|
|
856
|
+
);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
error.cleanupId = request.cleanupId;
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
}
|