chainlesschain 0.166.65 → 0.166.69
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 +20 -12
- package/package.json +4 -4
- package/src/assets/web-panel/assets/{AIOps-Byz17CTN.js → AIOps-FhevRfL6.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C5LiW2-J.js → ActionButton-rOeOykx5.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D1INcvZF.js → Analytics-BK_wYlfz.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DZzHO1VV.js → AppLayout-_2cCtZKV.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-WfYQL6Ia.js → Artifacts-D5MSvVzk.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DohBoGWO.js → Audit-yd-Ah-A3.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-Chtk-Hde.js → BackgroundAgents-yCaw_Gu_.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-B813PuMz.js → Backup-G3ADD4N-.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DaDdWWgK.js → BaseInput-ByDhkLbF.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-D4pJ6oZ7.js → Chat-BMrc7qjz.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-nA7n0I6S.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-Clo9TvO6.js → Checkbox-BHYgyZWA.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-uejLg8Ih.js → Codegen-DJfcy0jC.js} +1 -1
- package/src/assets/web-panel/assets/{Col-B7nbx-iJ.js → Col-C27zROxc.js} +1 -1
- package/src/assets/web-panel/assets/{Community-FEyjPK1j.js → Community-CuS5tq_1.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-B6Fk0Kn6.js → Compact-BrYJNe7Q.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BQG8goUl.js → Compliance-8hfahYHT.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CbBeEfHp.js → Cowork-DdE0n2lp.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-DPzlFcEZ.js → Cron-CUq3aSiX.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B8pWJrbg.js → Crosschain-BM4XSPWG.js} +1 -1
- package/src/assets/web-panel/assets/{DID-gGQHUlFN.js → DID-CdaBY57N.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-C2TMFHrn.js → Dashboard-Cwtd52So.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BVPxGbll.js → Dropdown-BvlObh6U.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CNPxs0hd.js → EmailListRenderer-e3fLhjnX.js} +1 -1
- package/src/assets/web-panel/assets/{EvolutionSettings-_XTiB7q1.js → EvolutionSettings-CTtk1Zo8.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-UyE3VWtI.js → FamilyGuardDashboard-BpPJUnCU.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CLt6FHK-.js → Federation-vzjZ6_nQ.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CdNQPRPw.js → FormItemContext-DEDNLhkE.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BJ0AMhrh.js → GenericCardRenderer-DEl2Te5f.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CQnvMfOM.js → Git-BzsYQnCq.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-deYsHC-o.js → Governance-zxJs6Y8s.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-qaSck43z.js → Inference-5JUsrmF1.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-De0aJ3Ls.js → KnowledgeGraph-Br8b5Htz.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-ZtS98jCm.js → Logs-D-tubDMJ.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-In4itZX2.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Byz3xCBN.js → Marketplace-s8MmWkS9.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DOZUUW6z.js → McpTools-Wu-WRg4M.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-C33wC2hA.js → Memory-B_i-eX9w.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-zZe-T_6M.js → MobileBridge-Ca64kjVx.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CRnUIume.js → MobileProjects-Cq9AFiO4.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-C1xlRVAN.js → Mtc-DKvGmG3y.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-D0U07bIi.js → MtcAudit-BVvWx8pF.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DF8uvV3h.js → Multisig-wqeJ2O9x.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-bBPGAFlt.js → NLProgramming-DX87ysYu.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BHAaOsgg.js → Notes-ChenGdu0.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Cl0fG_rA.js → NotificationSettings-BZRa68-x.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BEPAs8Zv.js → OrderTableRenderer-DZ6xG1M6.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BAxSwgk5.js → Organization-BWJp4VPX.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-D68KubAu.js → Overflow-BoM4bYt5.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-bhdT17zk.js → P2P-Cvi8YV09.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DqaO6wgH.js → PdhVaultBrowser-CNVC1NIj.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-Bq8fvkak.js → Permissions-DNd1MRjW.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BYd2JEic.js → PersonalDataHub-DGSBYZ5-.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Csg_ja69.js → Pipeline-DkcoGMBo.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-lwxwLj4M.js → Privacy-DltwvMAW.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BdBZkH8z.js → ProjectInit-ChYbpHg8.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-ClWgFy-K.js → ProjectSettings-B8FWiHTJ.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Bu-qj8N1.js → Projects-DuyhtSm4.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DEjxrxw3.js → Providers-Bo18Tx_6.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-eccy-8Vi.js → QrScannerModal-6W85KlsG.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Vx3uDvFg.js → QuickAsk-DznRUedP.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CfUivNps.js → Recommend-CQDN3_p-.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BU4gk7GI.js → RemoteSession-CiJgokEj.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-CbPzdZ0C.js → Reputation-DexzGH7E.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CYLrgfKA.js → Row-BgZOIvSi.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DbY1oyuv.js → RssFeed-BWIZJ_Jr.js} +3 -3
- package/src/assets/web-panel/assets/{Search-QeW41K5r.js → Search-B376EwWJ.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CJxf1kLZ.js → Security-92mUR5OD.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CFO9ySZc.js → Services-ClSEAOHk.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CQFjeQN0.js → Skeleton-DVhLbv99.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CLLB4INW.js → Skills-JTcXs_xI.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DxeLIJk0.js → Sla-DnKY9zy9.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-D4FA9KYI.js → SpeechSettings-GO1JefAH.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-C6NOkcQK.js → SyncSettings-jX5tB2St.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-LmVPWDfQ.js → Tasks-BxRBzhUc.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BdEfUoVB.js → Templates-CAVkbTG1.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-IUlESacN.js → Tenant-5Y7sHBkz.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B9pDe7sn.js → Terminal-CbbLShYb.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-ZCwB-Pki.js → TimelineRenderer-2HptWihL.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-4qk16LX1.js → Tokens-B-jGxzHl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-jXNyargn.js → Trigger-9DGa90DT.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-D13PydtT.js → Trust-BSJEkEOO.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-yPgws6mS.js → UkeySign-C807So5R.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B329WjpG.js → VideoEditing-ByiieAv-.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-T8VM-Ttm.js → Wallet-BixbIOUv.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XuRibrpP.js → WebAuthn-z7akhor4.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-D1Gwz3YR.js → WorkflowEditor-KvijuUuR.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DjydtnT-.js → chat-BYKoh0YG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-gFtBvPrr.js → colors-CLT8IYBt.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CndxhGfx.js → compact-item-DlBIyYHF.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DnfGpd0L.js → createContext-5XmAnNLr.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BmcCIHwY.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CmT5hRVW.js → hasIn-rPdRhm7K.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoBKSzqu.js → index-8un-eUDI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGbKeg8G.js → index-B-0nAxf5.js} +1 -1
- package/src/assets/web-panel/assets/{index-DreYqebI.js → index-B1VfBUPB.js} +1 -1
- package/src/assets/web-panel/assets/{index-zQf599Uq.js → index-BBt2CSAx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlTcxT_L.js → index-BDtlwjSr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqpR0Des.js → index-BIRl2Ii5.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3HvYeA3.js → index-BgevTkUv.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0WFjxWs.js → index-Bve7Wsgf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNHdO6Jr.js → index-Bzd0sFkp.js} +1 -1
- package/src/assets/web-panel/assets/{index-DV2PR3Fa.js → index-C2sFDVb_.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2d7358Y.js → index-CErvHCDe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaEPE2DC.js → index-CM_a9spK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKyuE_Y2.js → index-CY6TMc2m.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVM_w30N.js → index-ChlzaTV3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZLDbPTa.js → index-CsnbOKdI.js} +1 -1
- package/src/assets/web-panel/assets/{index-HHP1zzo7.js → index-D3-NL-x-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFif4Cev.js → index-D3f9Jzx2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C14jryXi.js → index-DCDSycWo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB27echm.js → index-DIVrVGvf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGlmPbl_.js → index-DI_tYFBe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsRKmb6k.js → index-DPS4mWnI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXNUTiW8.js → index-DTslLzPJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dbpk75MO.js → index-DW8vozBS.js} +1 -1
- package/src/assets/web-panel/assets/index-DY8rmd6x.js +1 -0
- package/src/assets/web-panel/assets/{index-DjY-3sMA.js → index-DZiSMYoD.js} +3 -3
- package/src/assets/web-panel/assets/{index-DV6NrfJx.js → index-DZjGcIGU.js} +1 -1
- package/src/assets/web-panel/assets/{index-ktN-K1Tf.js → index-Dea32nOK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjcR-CwS.js → index-Djd7uqwM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuOx4Pbe.js → index-Dntid6oj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BW3nWiu0.js → index-DqZU4_7J.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4Mqhmas.js → index-DuvweiCz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRqz8TtI.js → index-DzExCCLS.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5na0B7G.js → index-LKT-Ol0A.js} +1 -1
- package/src/assets/web-panel/assets/{index-_-wGLtAg.js → index-akwXS-FF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgMKElUh.js → index-b-rQCMkx.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUlubR4l.js → index-eHbr3f5I.js} +1 -1
- package/src/assets/web-panel/assets/{index-D88-8bsB.js → index-kx6zDWew.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuAvYTmY.js → index-rRuCGdKW.js} +1 -1
- package/src/assets/web-panel/assets/index-sbwcUeRS.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-KV3V-PNH.js → initDefaultProps-a8411LiK.js} +1 -1
- package/src/assets/web-panel/assets/{motion-PFGQg7_X.js → motion-oVcQdpCm.js} +1 -1
- package/src/assets/web-panel/assets/{move-Dnl79wq4.js → move-B-eyzzzL.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-Bwl5JH9V.js → mtc-parser-BCYIxuG7.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DCcTtM1l.js → omit-DuFtWiYh.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Cl6BjjDf.js → pickAttrs-CVAoJhGu.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-fdqhyo42.js → placementArrow-Y2iub2bO.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DYQpmcDW.js → responsiveObserve-BHenWIVD.js} +1 -1
- package/src/assets/web-panel/assets/{slide-C4l-VfO4.js → slide-CKrMaL29.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DYDw_dyZ.js → statusUtils-w8eb_1da.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BEsubSMu.js → styleChecker-CCVi_Ww3.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Cj82ggQ_.js → useFlexGapSupport-BsmR_q0r.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-B7L1eJ0V.js → useFs-a8GMBjnc.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DVlJVD4d.js → usePersonalDataHub-BMUYSPn-.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BBAbRN_Y.js → vnode-DL2pI31V.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BYn48q-I.js → zoom-Cbi8fISi.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/data/changelog.json +33 -7
- package/src/lib/dependency-install-policy.js +1 -1
- package/src/lib/evolution/browser-download-action-authority.js +747 -0
- package/src/lib/evolution/browser-download-artifact-disposal-authority.js +436 -0
- package/src/lib/evolution/browser-filesystem-quarantine-custody.js +2453 -0
- package/src/lib/evolution/browser-filesystem-quarantine-disposal.js +36 -0
- package/src/lib/evolution/browser-keyboard-action-authority.js +560 -0
- package/src/lib/evolution/browser-navigation-action-authority.js +546 -0
- package/src/lib/evolution/browser-quarantine-lock-maintenance-authority.js +573 -0
- package/src/lib/evolution/browser-quarantine-operator-revocation-authority.js +442 -0
- package/src/lib/evolution/browser-quarantine-retention-authority.js +503 -0
- package/src/lib/evolution/browser-quarantine-retention-scheduler.js +207 -0
- package/src/lib/evolution/browser-quarantined-download-executor.js +525 -0
- package/src/lib/evolution/browser-tab-open-action-authority.js +491 -0
- package/src/lib/evolution/browser-vision-action-authority.js +444 -0
- package/src/lib/evolution/browser-vision-observation-authority.js +304 -0
- package/src/lib/evolution/evolution-deployment-loader.js +241 -1
- package/src/lib/evolution/evolution-eval-child-evidence-ledger-adapter.js +5 -0
- package/src/lib/evolution/evolution-eval-gate.js +2 -1
- package/src/lib/evolution/evolution-eval-process-supervisor-worker.mjs +71 -9
- package/src/lib/evolution/evolution-eval-process-supervisor.js +276 -9
- package/src/lib/evolution/evolution-workbench-metrics.js +81 -21
- package/src/lib/evolution/pm-exploration-benchmark.js +747 -0
- package/src/lib/evolution/pm-exploration-budget-executor.js +106 -9
- package/src/lib/evolution/pm-exploration-egress-authority.js +382 -0
- package/src/lib/evolution/pm-exploration-evidence-bundle.js +149 -44
- package/src/lib/evolution/pm-exploration-execution-host.js +1254 -8
- package/src/lib/evolution/pm-exploration-process-grader.js +458 -0
- package/src/lib/evolution/pm-exploration-process-reviewer.js +456 -0
- package/src/lib/evolution/pm-exploration-process-runner.js +500 -0
- package/src/lib/evolution/pm-exploration-receipts.js +211 -11
- package/src/lib/evolution/pm-exploration-recovery-snapshot-store.js +465 -46
- package/src/lib/evolution/pm-exploration-transition-committer.js +185 -34
- package/src/lib/evolution/skill-outcome-index-authority.js +7 -3
- package/src/lib/evolution/skill-writer-inventory-manifest.js +0 -39
- package/src/lib/evolution/volcengine-function-execution-authority.js +2949 -0
- package/src/lib/evolution/volcengine-function-process-executor.js +615 -0
- package/src/lib/evolution/volcengine-function-replay-store.js +836 -0
- package/src/lib/evolution/volcengine-function-revocation-status-floor-store.js +480 -0
- package/src/lib/governance-coverage.js +1 -1
- package/src/lib/headless-manifest.js +1 -1
- package/src/lib/protocol-replay.js +1 -1
- package/src/lib/skill-outcome-transcript-authority.js +45 -5
- package/src/runtime/agent-core.js +17 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Cf3LLgDc.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-CTsYo9NN.js +0 -1
- package/src/assets/web-panel/assets/devWarning-D60SbiD-.js +0 -1
- package/src/assets/web-panel/assets/index-C0yR4_7h.js +0 -1
- package/src/assets/web-panel/assets/index-D4xuvo03.js +0 -1
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BROWSER_FILESYSTEM_QUARANTINE_EXPIRY_PLAN_SCHEMA,
|
|
6
|
+
captureBrowserFilesystemQuarantineCustody,
|
|
7
|
+
} from "./browser-filesystem-quarantine-custody.js";
|
|
8
|
+
|
|
9
|
+
export const BROWSER_QUARANTINE_RETENTION_SWEEP_REQUEST_SCHEMA =
|
|
10
|
+
"chainlesschain.browser-quarantine-retention-sweep-request/v1";
|
|
11
|
+
export const BROWSER_QUARANTINE_RETENTION_SWEEP_RECEIPT_SCHEMA =
|
|
12
|
+
"chainlesschain.browser-quarantine-retention-sweep-receipt/v1";
|
|
13
|
+
export const BROWSER_QUARANTINE_RETENTION_SWEEP_RESULT_SCHEMA =
|
|
14
|
+
"chainlesschain.browser-quarantine-retention-sweep-result/v1";
|
|
15
|
+
export const BROWSER_QUARANTINE_RETENTION_OUTCOME_REQUEST_SCHEMA =
|
|
16
|
+
"chainlesschain.browser-quarantine-retention-outcome-request/v1";
|
|
17
|
+
export const BROWSER_QUARANTINE_RETENTION_OUTCOME_ACK_SCHEMA =
|
|
18
|
+
"chainlesschain.browser-quarantine-retention-outcome-ack/v1";
|
|
19
|
+
|
|
20
|
+
const DELETION_ACK_SCHEMA =
|
|
21
|
+
"chainlesschain.browser-download-artifact-deletion-ack/v1";
|
|
22
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
23
|
+
const ID = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
24
|
+
const ARTIFACT_REF = /^quarantine:[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/u;
|
|
25
|
+
const authorities = new WeakMap();
|
|
26
|
+
|
|
27
|
+
function canonical(value) {
|
|
28
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
29
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
30
|
+
return `{${Object.keys(value)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
33
|
+
.join(",")}}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function digest(domain, value) {
|
|
37
|
+
return `sha256:${createHash("sha256")
|
|
38
|
+
.update(`${domain}\0`)
|
|
39
|
+
.update(canonical(value))
|
|
40
|
+
.digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exact(value, keys, label) {
|
|
44
|
+
if (
|
|
45
|
+
!value ||
|
|
46
|
+
typeof value !== "object" ||
|
|
47
|
+
Array.isArray(value) ||
|
|
48
|
+
types.isProxy(value) ||
|
|
49
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
|
|
50
|
+
Reflect.ownKeys(value).length !== keys.length ||
|
|
51
|
+
keys.some((key) => {
|
|
52
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
53
|
+
return !descriptor?.enumerable || !("value" in descriptor);
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
throw new TypeError(`${label} has unexpected or accessor fields`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeDecision(value, nowMs, maxGrantTtlMs) {
|
|
60
|
+
if (
|
|
61
|
+
!value ||
|
|
62
|
+
typeof value !== "object" ||
|
|
63
|
+
Array.isArray(value) ||
|
|
64
|
+
types.isProxy(value) ||
|
|
65
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
66
|
+
)
|
|
67
|
+
throw new TypeError("retention sweep decision is invalid");
|
|
68
|
+
const decision = Object.getOwnPropertyDescriptor(value, "decision");
|
|
69
|
+
if (!decision?.enumerable || !("value" in decision))
|
|
70
|
+
throw new TypeError("retention sweep decision is invalid");
|
|
71
|
+
if (decision.value === "deny") {
|
|
72
|
+
exact(value, ["decision", "reason"], "retention sweep denial");
|
|
73
|
+
if (
|
|
74
|
+
typeof value.reason !== "string" ||
|
|
75
|
+
value.reason.length < 1 ||
|
|
76
|
+
value.reason.length > 512
|
|
77
|
+
)
|
|
78
|
+
throw new TypeError("retention sweep denial is invalid");
|
|
79
|
+
return Object.freeze({ ...value });
|
|
80
|
+
}
|
|
81
|
+
exact(
|
|
82
|
+
value,
|
|
83
|
+
["decision", "policyEvidenceRef", "validUntil"],
|
|
84
|
+
"retention sweep approval",
|
|
85
|
+
);
|
|
86
|
+
const validUntilMs = Date.parse(value.validUntil);
|
|
87
|
+
if (
|
|
88
|
+
value.decision !== "allow" ||
|
|
89
|
+
typeof value.policyEvidenceRef !== "string" ||
|
|
90
|
+
value.policyEvidenceRef.length < 1 ||
|
|
91
|
+
value.policyEvidenceRef.length > 512 ||
|
|
92
|
+
!Number.isFinite(validUntilMs) ||
|
|
93
|
+
validUntilMs <= nowMs ||
|
|
94
|
+
validUntilMs > nowMs + maxGrantTtlMs
|
|
95
|
+
)
|
|
96
|
+
throw new TypeError("retention sweep approval is invalid");
|
|
97
|
+
return Object.freeze({ ...value, validUntilMs });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizePlanArtifact(value, cutoffAt, nowMs) {
|
|
101
|
+
exact(
|
|
102
|
+
value,
|
|
103
|
+
[
|
|
104
|
+
"artifactRef",
|
|
105
|
+
"artifactRefDigest",
|
|
106
|
+
"artifactDigest",
|
|
107
|
+
"sourceActionReceiptDigest",
|
|
108
|
+
"expiresAt",
|
|
109
|
+
],
|
|
110
|
+
"retention expiry plan artifact",
|
|
111
|
+
);
|
|
112
|
+
const expiresAtMs = Date.parse(value.expiresAt);
|
|
113
|
+
if (
|
|
114
|
+
!ARTIFACT_REF.test(value.artifactRef) ||
|
|
115
|
+
value.artifactRefDigest !==
|
|
116
|
+
digest(
|
|
117
|
+
"chainlesschain.browser-download-artifact-ref/v1",
|
|
118
|
+
value.artifactRef,
|
|
119
|
+
) ||
|
|
120
|
+
!DIGEST.test(value.artifactDigest) ||
|
|
121
|
+
!DIGEST.test(value.sourceActionReceiptDigest) ||
|
|
122
|
+
!Number.isFinite(expiresAtMs) ||
|
|
123
|
+
new Date(expiresAtMs).toISOString() !== value.expiresAt ||
|
|
124
|
+
expiresAtMs > Date.parse(cutoffAt) ||
|
|
125
|
+
expiresAtMs > nowMs
|
|
126
|
+
)
|
|
127
|
+
throw new Error("retention expiry plan artifact is invalid");
|
|
128
|
+
return Object.freeze({ ...value });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validatePlan(plan, descriptor, sweepId, cutoffAt, nowMs) {
|
|
132
|
+
exact(
|
|
133
|
+
plan,
|
|
134
|
+
[
|
|
135
|
+
"schema",
|
|
136
|
+
"custodyId",
|
|
137
|
+
"authorityId",
|
|
138
|
+
"tenantId",
|
|
139
|
+
"handlerArtifactDigest",
|
|
140
|
+
"policyRevision",
|
|
141
|
+
"sweepId",
|
|
142
|
+
"cutoffAt",
|
|
143
|
+
"plannedAt",
|
|
144
|
+
"artifacts",
|
|
145
|
+
"planDigest",
|
|
146
|
+
],
|
|
147
|
+
"retention expiry plan",
|
|
148
|
+
);
|
|
149
|
+
if (!Array.isArray(plan.artifacts))
|
|
150
|
+
throw new Error("retention expiry plan is invalid");
|
|
151
|
+
const artifacts = Object.freeze(
|
|
152
|
+
plan.artifacts.map((artifact) =>
|
|
153
|
+
normalizePlanArtifact(artifact, cutoffAt, nowMs),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
if (
|
|
157
|
+
new Set(artifacts.map((artifact) => artifact.artifactRef)).size !==
|
|
158
|
+
artifacts.length ||
|
|
159
|
+
artifacts.some(
|
|
160
|
+
(artifact, index) =>
|
|
161
|
+
index > 0 &&
|
|
162
|
+
artifacts[index - 1].artifactRef.localeCompare(artifact.artifactRef) >=
|
|
163
|
+
0,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
throw new Error("retention expiry plan artifacts are not canonical");
|
|
167
|
+
const core = Object.freeze({
|
|
168
|
+
schema: plan.schema,
|
|
169
|
+
custodyId: plan.custodyId,
|
|
170
|
+
authorityId: plan.authorityId,
|
|
171
|
+
tenantId: plan.tenantId,
|
|
172
|
+
handlerArtifactDigest: plan.handlerArtifactDigest,
|
|
173
|
+
policyRevision: plan.policyRevision,
|
|
174
|
+
sweepId: plan.sweepId,
|
|
175
|
+
cutoffAt: plan.cutoffAt,
|
|
176
|
+
plannedAt: plan.plannedAt,
|
|
177
|
+
artifacts,
|
|
178
|
+
});
|
|
179
|
+
if (
|
|
180
|
+
plan.schema !== BROWSER_FILESYSTEM_QUARANTINE_EXPIRY_PLAN_SCHEMA ||
|
|
181
|
+
!ID.test(plan.custodyId) ||
|
|
182
|
+
plan.authorityId !== descriptor.authorityId ||
|
|
183
|
+
plan.tenantId !== descriptor.tenantId ||
|
|
184
|
+
plan.handlerArtifactDigest !== descriptor.handlerArtifactDigest ||
|
|
185
|
+
plan.policyRevision !== descriptor.policyRevision ||
|
|
186
|
+
plan.sweepId !== sweepId ||
|
|
187
|
+
plan.cutoffAt !== cutoffAt ||
|
|
188
|
+
Date.parse(plan.plannedAt) !== nowMs ||
|
|
189
|
+
artifacts.length > descriptor.maxBatchSize ||
|
|
190
|
+
plan.planDigest !==
|
|
191
|
+
digest(BROWSER_FILESYSTEM_QUARANTINE_EXPIRY_PLAN_SCHEMA, core)
|
|
192
|
+
)
|
|
193
|
+
throw new Error("retention expiry plan is invalid");
|
|
194
|
+
return Object.freeze({ ...plan, artifacts });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function validateDeletionAck(value, descriptor, artifact, receiptDigest) {
|
|
198
|
+
exact(
|
|
199
|
+
value,
|
|
200
|
+
[
|
|
201
|
+
"schema",
|
|
202
|
+
"authorityId",
|
|
203
|
+
"tenantId",
|
|
204
|
+
"handlerArtifactDigest",
|
|
205
|
+
"actionReceiptDigest",
|
|
206
|
+
"requestDigest",
|
|
207
|
+
"artifactRef",
|
|
208
|
+
"artifactDigest",
|
|
209
|
+
"sourceActionReceiptDigest",
|
|
210
|
+
"discardedAt",
|
|
211
|
+
"deletionReceiptDigest",
|
|
212
|
+
"authenticated",
|
|
213
|
+
"durable",
|
|
214
|
+
"readbackVerified",
|
|
215
|
+
"bytesUnavailable",
|
|
216
|
+
"qualifiesForPromotion",
|
|
217
|
+
],
|
|
218
|
+
"retention deletion acknowledgement",
|
|
219
|
+
);
|
|
220
|
+
if (
|
|
221
|
+
value.schema !== DELETION_ACK_SCHEMA ||
|
|
222
|
+
value.authorityId !== descriptor.authorityId ||
|
|
223
|
+
value.tenantId !== descriptor.tenantId ||
|
|
224
|
+
value.handlerArtifactDigest !== descriptor.handlerArtifactDigest ||
|
|
225
|
+
value.actionReceiptDigest !== receiptDigest ||
|
|
226
|
+
!DIGEST.test(value.requestDigest) ||
|
|
227
|
+
value.artifactRef !== artifact.artifactRef ||
|
|
228
|
+
value.artifactDigest !== artifact.artifactDigest ||
|
|
229
|
+
value.sourceActionReceiptDigest !== artifact.sourceActionReceiptDigest ||
|
|
230
|
+
!Number.isFinite(Date.parse(value.discardedAt)) ||
|
|
231
|
+
!DIGEST.test(value.deletionReceiptDigest) ||
|
|
232
|
+
value.authenticated !== true ||
|
|
233
|
+
value.durable !== true ||
|
|
234
|
+
value.readbackVerified !== true ||
|
|
235
|
+
value.bytesUnavailable !== true ||
|
|
236
|
+
value.qualifiesForPromotion !== false
|
|
237
|
+
)
|
|
238
|
+
throw new Error("retention deletion acknowledgement is invalid");
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeOutcomeAck(value, descriptor, request) {
|
|
243
|
+
exact(
|
|
244
|
+
value,
|
|
245
|
+
[
|
|
246
|
+
"schema",
|
|
247
|
+
"authorityId",
|
|
248
|
+
"tenantId",
|
|
249
|
+
"handlerArtifactDigest",
|
|
250
|
+
"sweepReceiptDigest",
|
|
251
|
+
"outcomeRequestDigest",
|
|
252
|
+
"auditEventDigest",
|
|
253
|
+
"durabilityReceiptDigest",
|
|
254
|
+
"authenticated",
|
|
255
|
+
"durable",
|
|
256
|
+
"readbackVerified",
|
|
257
|
+
"qualifiesForPromotion",
|
|
258
|
+
],
|
|
259
|
+
"retention sweep outcome acknowledgement",
|
|
260
|
+
);
|
|
261
|
+
if (
|
|
262
|
+
value.schema !== BROWSER_QUARANTINE_RETENTION_OUTCOME_ACK_SCHEMA ||
|
|
263
|
+
value.authorityId !== descriptor.authorityId ||
|
|
264
|
+
value.tenantId !== descriptor.tenantId ||
|
|
265
|
+
value.handlerArtifactDigest !== descriptor.handlerArtifactDigest ||
|
|
266
|
+
value.sweepReceiptDigest !== request.sweepReceiptDigest ||
|
|
267
|
+
value.outcomeRequestDigest !== request.outcomeRequestDigest ||
|
|
268
|
+
!DIGEST.test(value.auditEventDigest) ||
|
|
269
|
+
!DIGEST.test(value.durabilityReceiptDigest) ||
|
|
270
|
+
value.authenticated !== true ||
|
|
271
|
+
value.durable !== true ||
|
|
272
|
+
value.readbackVerified !== true ||
|
|
273
|
+
value.qualifiesForPromotion !== false
|
|
274
|
+
)
|
|
275
|
+
throw new Error("retention sweep outcome acknowledgement is invalid");
|
|
276
|
+
return Object.freeze({ ...value });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function createBrowserQuarantineRetentionAuthority(options) {
|
|
280
|
+
exact(
|
|
281
|
+
options,
|
|
282
|
+
["descriptor", "custody", "authorizeSweep", "recordOutcome", "now"],
|
|
283
|
+
"browser quarantine retention authority",
|
|
284
|
+
);
|
|
285
|
+
const custody = captureBrowserFilesystemQuarantineCustody(options.custody);
|
|
286
|
+
const retention = custody.bindRetentionAuthority(options.descriptor);
|
|
287
|
+
if (
|
|
288
|
+
typeof options.authorizeSweep !== "function" ||
|
|
289
|
+
types.isProxy(options.authorizeSweep) ||
|
|
290
|
+
typeof options.recordOutcome !== "function" ||
|
|
291
|
+
types.isProxy(options.recordOutcome) ||
|
|
292
|
+
typeof options.now !== "function" ||
|
|
293
|
+
types.isProxy(options.now)
|
|
294
|
+
)
|
|
295
|
+
throw new TypeError("browser quarantine retention ports are invalid");
|
|
296
|
+
const authority = Object.freeze({});
|
|
297
|
+
authorities.set(authority, {
|
|
298
|
+
descriptor: Object.freeze({ ...options.descriptor }),
|
|
299
|
+
authorizeSweep: options.authorizeSweep,
|
|
300
|
+
recordOutcome: options.recordOutcome,
|
|
301
|
+
now: options.now,
|
|
302
|
+
retention,
|
|
303
|
+
running: false,
|
|
304
|
+
});
|
|
305
|
+
return authority;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function captureBrowserQuarantineRetentionAuthority(value) {
|
|
309
|
+
const state = authorities.get(value);
|
|
310
|
+
if (!state)
|
|
311
|
+
throw new TypeError(
|
|
312
|
+
"A branded browser quarantine retention authority is required",
|
|
313
|
+
);
|
|
314
|
+
return Object.freeze({
|
|
315
|
+
descriptor: state.descriptor,
|
|
316
|
+
runExpirySweep: async () => {
|
|
317
|
+
if (state.running)
|
|
318
|
+
throw new Error("browser quarantine retention sweep is already active");
|
|
319
|
+
state.running = true;
|
|
320
|
+
try {
|
|
321
|
+
const nowMs = state.now();
|
|
322
|
+
if (!Number.isFinite(nowMs))
|
|
323
|
+
throw new Error("browser quarantine retention clock is invalid");
|
|
324
|
+
const cutoffAt = new Date(nowMs).toISOString();
|
|
325
|
+
const sweepId = randomUUID();
|
|
326
|
+
const plan = validatePlan(
|
|
327
|
+
await state.retention.planExpiredArtifacts({
|
|
328
|
+
sweepId,
|
|
329
|
+
cutoffAt,
|
|
330
|
+
maxArtifacts: state.descriptor.maxBatchSize,
|
|
331
|
+
}),
|
|
332
|
+
state.descriptor,
|
|
333
|
+
sweepId,
|
|
334
|
+
cutoffAt,
|
|
335
|
+
nowMs,
|
|
336
|
+
);
|
|
337
|
+
const request = Object.freeze({
|
|
338
|
+
schema: BROWSER_QUARANTINE_RETENTION_SWEEP_REQUEST_SCHEMA,
|
|
339
|
+
authorityId: state.descriptor.authorityId,
|
|
340
|
+
tenantId: state.descriptor.tenantId,
|
|
341
|
+
handlerArtifactDigest: state.descriptor.handlerArtifactDigest,
|
|
342
|
+
policyRevision: state.descriptor.policyRevision,
|
|
343
|
+
sweepId,
|
|
344
|
+
cutoffAt,
|
|
345
|
+
planDigest: plan.planDigest,
|
|
346
|
+
artifactCount: plan.artifacts.length,
|
|
347
|
+
artifacts: Object.freeze(
|
|
348
|
+
plan.artifacts.map((artifact) =>
|
|
349
|
+
Object.freeze({
|
|
350
|
+
artifactRefDigest: artifact.artifactRefDigest,
|
|
351
|
+
artifactDigest: artifact.artifactDigest,
|
|
352
|
+
expiresAt: artifact.expiresAt,
|
|
353
|
+
}),
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
requestedAt: cutoffAt,
|
|
357
|
+
});
|
|
358
|
+
const decision = normalizeDecision(
|
|
359
|
+
await state.authorizeSweep(request),
|
|
360
|
+
nowMs,
|
|
361
|
+
state.descriptor.maxGrantTtlMs,
|
|
362
|
+
);
|
|
363
|
+
if (decision.decision === "deny") {
|
|
364
|
+
const error = new Error(decision.reason);
|
|
365
|
+
error.code = "BROWSER_QUARANTINE_RETENTION_DENIED";
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
const receiptCore = Object.freeze({
|
|
369
|
+
schema: BROWSER_QUARANTINE_RETENTION_SWEEP_RECEIPT_SCHEMA,
|
|
370
|
+
authorityId: state.descriptor.authorityId,
|
|
371
|
+
tenantId: state.descriptor.tenantId,
|
|
372
|
+
handlerArtifactDigest: state.descriptor.handlerArtifactDigest,
|
|
373
|
+
policyRevision: state.descriptor.policyRevision,
|
|
374
|
+
sweepId,
|
|
375
|
+
cutoffAt,
|
|
376
|
+
planDigest: plan.planDigest,
|
|
377
|
+
artifactCount: plan.artifacts.length,
|
|
378
|
+
policyEvidenceRef: decision.policyEvidenceRef,
|
|
379
|
+
authorizedAt: cutoffAt,
|
|
380
|
+
validUntil: decision.validUntil,
|
|
381
|
+
});
|
|
382
|
+
const receiptDigest = digest(
|
|
383
|
+
BROWSER_QUARANTINE_RETENTION_SWEEP_RECEIPT_SCHEMA,
|
|
384
|
+
receiptCore,
|
|
385
|
+
);
|
|
386
|
+
const deletions = [];
|
|
387
|
+
let executionError = null;
|
|
388
|
+
for (const artifact of plan.artifacts) {
|
|
389
|
+
try {
|
|
390
|
+
const executionMs = state.now();
|
|
391
|
+
if (
|
|
392
|
+
!Number.isFinite(executionMs) ||
|
|
393
|
+
executionMs >= decision.validUntilMs
|
|
394
|
+
)
|
|
395
|
+
throw new Error("retention sweep grant expired during execution");
|
|
396
|
+
const acknowledgement = validateDeletionAck(
|
|
397
|
+
await state.retention.disposeExpiredArtifact({
|
|
398
|
+
sweepReceiptDigest: receiptDigest,
|
|
399
|
+
planDigest: plan.planDigest,
|
|
400
|
+
sweepId,
|
|
401
|
+
cutoffAt,
|
|
402
|
+
...artifact,
|
|
403
|
+
}),
|
|
404
|
+
state.descriptor,
|
|
405
|
+
artifact,
|
|
406
|
+
receiptDigest,
|
|
407
|
+
);
|
|
408
|
+
deletions.push(
|
|
409
|
+
Object.freeze({
|
|
410
|
+
artifactRefDigest: artifact.artifactRefDigest,
|
|
411
|
+
artifactDigest: artifact.artifactDigest,
|
|
412
|
+
discardedAt: acknowledgement.discardedAt,
|
|
413
|
+
deletionReceiptDigest: acknowledgement.deletionReceiptDigest,
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
executionError = error;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const completedMs = state.now();
|
|
422
|
+
if (!Number.isFinite(completedMs))
|
|
423
|
+
throw new Error("browser quarantine retention clock is invalid");
|
|
424
|
+
const status = executionError === null ? "succeeded" : "failed";
|
|
425
|
+
const failureDigest =
|
|
426
|
+
executionError === null
|
|
427
|
+
? null
|
|
428
|
+
: digest("chainlesschain.browser-quarantine-retention-failure/v1", {
|
|
429
|
+
name: executionError?.name ?? "Error",
|
|
430
|
+
code: executionError?.code ?? null,
|
|
431
|
+
});
|
|
432
|
+
const outcomeCore = Object.freeze({
|
|
433
|
+
schema: BROWSER_QUARANTINE_RETENTION_OUTCOME_REQUEST_SCHEMA,
|
|
434
|
+
authorityId: state.descriptor.authorityId,
|
|
435
|
+
tenantId: state.descriptor.tenantId,
|
|
436
|
+
handlerArtifactDigest: state.descriptor.handlerArtifactDigest,
|
|
437
|
+
policyRevision: state.descriptor.policyRevision,
|
|
438
|
+
sweepId,
|
|
439
|
+
sweepReceiptDigest: receiptDigest,
|
|
440
|
+
planDigest: plan.planDigest,
|
|
441
|
+
status,
|
|
442
|
+
plannedCount: plan.artifacts.length,
|
|
443
|
+
deletedCount: deletions.length,
|
|
444
|
+
deletionReceiptDigests: Object.freeze(
|
|
445
|
+
deletions.map((entry) => entry.deletionReceiptDigest),
|
|
446
|
+
),
|
|
447
|
+
failureDigest,
|
|
448
|
+
recordedAt: new Date(completedMs).toISOString(),
|
|
449
|
+
});
|
|
450
|
+
const outcomeRequest = Object.freeze({
|
|
451
|
+
...outcomeCore,
|
|
452
|
+
outcomeRequestDigest: digest(
|
|
453
|
+
BROWSER_QUARANTINE_RETENTION_OUTCOME_REQUEST_SCHEMA,
|
|
454
|
+
outcomeCore,
|
|
455
|
+
),
|
|
456
|
+
});
|
|
457
|
+
const outcomeAck = normalizeOutcomeAck(
|
|
458
|
+
await state.recordOutcome(outcomeRequest),
|
|
459
|
+
state.descriptor,
|
|
460
|
+
outcomeRequest,
|
|
461
|
+
);
|
|
462
|
+
if (executionError !== null) {
|
|
463
|
+
const uncertain = new Error("retention sweep execution failed", {
|
|
464
|
+
cause: executionError,
|
|
465
|
+
});
|
|
466
|
+
uncertain.code = "BROWSER_QUARANTINE_RETENTION_SWEEP_FAILED";
|
|
467
|
+
throw uncertain;
|
|
468
|
+
}
|
|
469
|
+
const resultCore = Object.freeze({
|
|
470
|
+
schema: BROWSER_QUARANTINE_RETENTION_SWEEP_RESULT_SCHEMA,
|
|
471
|
+
authorityId: state.descriptor.authorityId,
|
|
472
|
+
tenantId: state.descriptor.tenantId,
|
|
473
|
+
handlerArtifactDigest: state.descriptor.handlerArtifactDigest,
|
|
474
|
+
policyRevision: state.descriptor.policyRevision,
|
|
475
|
+
sweepId,
|
|
476
|
+
cutoffAt,
|
|
477
|
+
planDigest: plan.planDigest,
|
|
478
|
+
sweepReceiptDigest: receiptDigest,
|
|
479
|
+
plannedCount: plan.artifacts.length,
|
|
480
|
+
deletedCount: deletions.length,
|
|
481
|
+
deletions: Object.freeze(deletions),
|
|
482
|
+
outcomeRequestDigest: outcomeRequest.outcomeRequestDigest,
|
|
483
|
+
auditEventDigest: outcomeAck.auditEventDigest,
|
|
484
|
+
durabilityReceiptDigest: outcomeAck.durabilityReceiptDigest,
|
|
485
|
+
completedAt: new Date(completedMs).toISOString(),
|
|
486
|
+
});
|
|
487
|
+
return Object.freeze({
|
|
488
|
+
...resultCore,
|
|
489
|
+
resultDigest: digest(
|
|
490
|
+
BROWSER_QUARANTINE_RETENTION_SWEEP_RESULT_SCHEMA,
|
|
491
|
+
resultCore,
|
|
492
|
+
),
|
|
493
|
+
authenticated: true,
|
|
494
|
+
durable: true,
|
|
495
|
+
readbackVerified: true,
|
|
496
|
+
qualifiesForPromotion: false,
|
|
497
|
+
});
|
|
498
|
+
} finally {
|
|
499
|
+
state.running = false;
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
|
|
4
|
+
import { captureBrowserQuarantineRetentionAuthority } from "./browser-quarantine-retention-authority.js";
|
|
5
|
+
|
|
6
|
+
export const BROWSER_QUARANTINE_RETENTION_SCHEDULER_DESCRIPTOR_SCHEMA =
|
|
7
|
+
"chainlesschain.browser-quarantine-retention-scheduler-descriptor/v1";
|
|
8
|
+
|
|
9
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
10
|
+
const ID = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
11
|
+
const schedulers = new WeakMap();
|
|
12
|
+
|
|
13
|
+
function canonical(value) {
|
|
14
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
15
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
16
|
+
return `{${Object.keys(value)
|
|
17
|
+
.sort()
|
|
18
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
19
|
+
.join(",")}}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function digest(domain, value) {
|
|
23
|
+
return `sha256:${createHash("sha256")
|
|
24
|
+
.update(`${domain}\0`)
|
|
25
|
+
.update(canonical(value))
|
|
26
|
+
.digest("hex")}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function exact(value, keys, label) {
|
|
30
|
+
if (
|
|
31
|
+
!value ||
|
|
32
|
+
typeof value !== "object" ||
|
|
33
|
+
Array.isArray(value) ||
|
|
34
|
+
types.isProxy(value) ||
|
|
35
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
|
|
36
|
+
Reflect.ownKeys(value).length !== keys.length ||
|
|
37
|
+
keys.some((key) => {
|
|
38
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
39
|
+
return !descriptor?.enumerable || !("value" in descriptor);
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
throw new TypeError(`${label} has unexpected or accessor fields`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeDescriptor(value, authorityDescriptor) {
|
|
46
|
+
exact(
|
|
47
|
+
value,
|
|
48
|
+
[
|
|
49
|
+
"schema",
|
|
50
|
+
"schedulerId",
|
|
51
|
+
"tenantId",
|
|
52
|
+
"handlerArtifactDigest",
|
|
53
|
+
"intervalMs",
|
|
54
|
+
"runOnStart",
|
|
55
|
+
"overlapMode",
|
|
56
|
+
"shutdownMode",
|
|
57
|
+
],
|
|
58
|
+
"browser quarantine retention scheduler descriptor",
|
|
59
|
+
);
|
|
60
|
+
if (
|
|
61
|
+
value.schema !== BROWSER_QUARANTINE_RETENTION_SCHEDULER_DESCRIPTOR_SCHEMA ||
|
|
62
|
+
!ID.test(value.schedulerId) ||
|
|
63
|
+
value.tenantId !== authorityDescriptor.tenantId ||
|
|
64
|
+
value.handlerArtifactDigest !== authorityDescriptor.handlerArtifactDigest ||
|
|
65
|
+
!Number.isSafeInteger(value.intervalMs) ||
|
|
66
|
+
value.intervalMs < 1000 ||
|
|
67
|
+
value.intervalMs > 24 * 60 * 60 * 1000 ||
|
|
68
|
+
typeof value.runOnStart !== "boolean" ||
|
|
69
|
+
value.overlapMode !== "skip" ||
|
|
70
|
+
value.shutdownMode !== "drain"
|
|
71
|
+
)
|
|
72
|
+
throw new TypeError(
|
|
73
|
+
"browser quarantine retention scheduler descriptor is invalid",
|
|
74
|
+
);
|
|
75
|
+
return Object.freeze({ ...value });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createBrowserQuarantineRetentionScheduler(options) {
|
|
79
|
+
exact(
|
|
80
|
+
options,
|
|
81
|
+
["descriptor", "authority", "setIntervalFn", "clearIntervalFn", "now"],
|
|
82
|
+
"browser quarantine retention scheduler",
|
|
83
|
+
);
|
|
84
|
+
const authority = captureBrowserQuarantineRetentionAuthority(
|
|
85
|
+
options.authority,
|
|
86
|
+
);
|
|
87
|
+
const descriptor = normalizeDescriptor(
|
|
88
|
+
options.descriptor,
|
|
89
|
+
authority.descriptor,
|
|
90
|
+
);
|
|
91
|
+
for (const [name, port] of Object.entries({
|
|
92
|
+
setIntervalFn: options.setIntervalFn,
|
|
93
|
+
clearIntervalFn: options.clearIntervalFn,
|
|
94
|
+
now: options.now,
|
|
95
|
+
})) {
|
|
96
|
+
if (typeof port !== "function" || types.isProxy(port))
|
|
97
|
+
throw new TypeError(
|
|
98
|
+
`browser quarantine retention scheduler ${name} is invalid`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const scheduler = Object.freeze({});
|
|
102
|
+
schedulers.set(scheduler, {
|
|
103
|
+
descriptor,
|
|
104
|
+
runExpirySweep: authority.runExpirySweep,
|
|
105
|
+
setIntervalFn: options.setIntervalFn,
|
|
106
|
+
clearIntervalFn: options.clearIntervalFn,
|
|
107
|
+
now: options.now,
|
|
108
|
+
started: false,
|
|
109
|
+
timer: null,
|
|
110
|
+
active: null,
|
|
111
|
+
lastRun: null,
|
|
112
|
+
});
|
|
113
|
+
return scheduler;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runOnce(state, trigger) {
|
|
117
|
+
if (state.active !== null)
|
|
118
|
+
return Object.freeze({ status: "skipped", reason: "already-active" });
|
|
119
|
+
const startedAtMs = state.now();
|
|
120
|
+
if (!Number.isFinite(startedAtMs))
|
|
121
|
+
throw new Error("browser quarantine retention scheduler clock is invalid");
|
|
122
|
+
state.active = (async () => {
|
|
123
|
+
try {
|
|
124
|
+
const result = await state.runExpirySweep();
|
|
125
|
+
if (
|
|
126
|
+
!result ||
|
|
127
|
+
typeof result !== "object" ||
|
|
128
|
+
types.isProxy(result) ||
|
|
129
|
+
!DIGEST.test(result.resultDigest) ||
|
|
130
|
+
result.authenticated !== true ||
|
|
131
|
+
result.durable !== true ||
|
|
132
|
+
result.readbackVerified !== true
|
|
133
|
+
)
|
|
134
|
+
throw new Error("browser quarantine retention sweep result is invalid");
|
|
135
|
+
state.lastRun = Object.freeze({
|
|
136
|
+
status: "succeeded",
|
|
137
|
+
trigger,
|
|
138
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
139
|
+
resultDigest: result.resultDigest,
|
|
140
|
+
failureDigest: null,
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
state.lastRun = Object.freeze({
|
|
144
|
+
status: "failed",
|
|
145
|
+
trigger,
|
|
146
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
147
|
+
resultDigest: null,
|
|
148
|
+
failureDigest: digest(
|
|
149
|
+
"chainlesschain.browser-quarantine-retention-scheduler-failure/v1",
|
|
150
|
+
{ name: error?.name ?? "Error", code: error?.code ?? null },
|
|
151
|
+
),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return state.lastRun;
|
|
155
|
+
})();
|
|
156
|
+
try {
|
|
157
|
+
return await state.active;
|
|
158
|
+
} finally {
|
|
159
|
+
state.active = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function captureBrowserQuarantineRetentionScheduler(value) {
|
|
164
|
+
const state = schedulers.get(value);
|
|
165
|
+
if (!state)
|
|
166
|
+
throw new TypeError(
|
|
167
|
+
"A branded browser quarantine retention scheduler is required",
|
|
168
|
+
);
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
descriptor: state.descriptor,
|
|
171
|
+
start: async () => {
|
|
172
|
+
if (state.started)
|
|
173
|
+
throw new Error(
|
|
174
|
+
"browser quarantine retention scheduler is already started",
|
|
175
|
+
);
|
|
176
|
+
const callback = () => {
|
|
177
|
+
if (!state.started) return;
|
|
178
|
+
void runOnce(state, "interval").catch(() => {});
|
|
179
|
+
};
|
|
180
|
+
const timer = state.setIntervalFn(callback, state.descriptor.intervalMs);
|
|
181
|
+
if (timer === null || timer === undefined)
|
|
182
|
+
throw new Error(
|
|
183
|
+
"browser quarantine retention scheduler timer is invalid",
|
|
184
|
+
);
|
|
185
|
+
state.timer = timer;
|
|
186
|
+
state.started = true;
|
|
187
|
+
if (state.descriptor.runOnStart) await runOnce(state, "startup");
|
|
188
|
+
return Object.freeze({ status: "started" });
|
|
189
|
+
},
|
|
190
|
+
runNow: async () => runOnce(state, "manual"),
|
|
191
|
+
stop: async () => {
|
|
192
|
+
if (state.started) {
|
|
193
|
+
state.clearIntervalFn(state.timer);
|
|
194
|
+
state.timer = null;
|
|
195
|
+
state.started = false;
|
|
196
|
+
}
|
|
197
|
+
if (state.active !== null) await state.active;
|
|
198
|
+
return Object.freeze({ status: "stopped", lastRun: state.lastRun });
|
|
199
|
+
},
|
|
200
|
+
inspect: () =>
|
|
201
|
+
Object.freeze({
|
|
202
|
+
started: state.started,
|
|
203
|
+
running: state.active !== null,
|
|
204
|
+
lastRun: state.lastRun,
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
}
|