chainlesschain 0.166.14 → 0.166.16
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-C8SyFhOo.js → AIOps-CTxa2Wbq.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Dup5gFK0.js → ActionButton-DHrQyYhy.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-8FyOWA7V.js → Analytics-DQiUqcNl.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DBzdU9dg.js → AppLayout-_Iz4r6wt.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-C_0Z8gNy.js → Artifacts-BNs63YHo.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-Cf2a-58m.js → Audit-BgHIqRLn.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-M7w-2zgB.js → BackgroundAgents-DUQ5axBB.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CCTHQXoL.js → Backup-CJ51E9Vw.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DjXpeFwJ.js → BaseInput-o-5toA_R.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-cAvchsJU.js → Chat-Cu7a-x1L.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DvEvxlZv.js → ChatBubbleRenderer-BUyYkAXr.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DKs6qvOn.js → Checkbox-DwdmZ71L.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DlkA7b9X.js → Codegen-BrH2eYiP.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CkfdKhyJ.js → Col-BVJQEP2j.js} +1 -1
- package/src/assets/web-panel/assets/{Community-iNXMHzj6.js → Community-FGO3vZPq.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DQxPEd09.js → Compact-DYGAFVnT.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CSKvMCjs.js → Compliance-CRvc8Bdt.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CkWqazpD.js → Cowork-D6Ah2m7A.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-k6W8IzhB.js → Cron-AXbU2ifR.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-C8MS1OuY.js → Crosschain-C5DXgEFd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DJuCcgiQ.js → DID-Bxh9QV9v.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-LeThKzp3.js → Dashboard-B-ZHEta-.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BvxwgpE4.js → Dropdown-66Ik1jqa.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-n3e_a1M3.js → EmailListRenderer-CCWk1BIM.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-509W6W6v.js → FamilyGuardDashboard--9faIULK.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-xgZReQm0.js → Federation-CejW9CgX.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-jZOhKcQx.js → FormItemContext-acHgcXki.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-Jo0ULzHv.js → GenericCardRenderer-D2ghd_K4.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BsRcyWQP.js → Git-BOe1YJrV.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-JlpMYTAk.js → Governance-BjgzJsof.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CUEz872-.js → Inference-CXCD5rnP.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DgrvvEDU.js → KnowledgeGraph-CHA8QgJT.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Df-KY4U3.js → Logs-DORPMcL6.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-CPPHz01z.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-kPZDTOeI.js → Marketplace-BCFlo1Yx.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CRRzPqNX.js → McpTools-Dl_uZAKh.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-D6pNSkg9.js → Memory-DupHamwz.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-g-pvtwRC.js → MobileBridge-BRSMPJxW.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Bzayfrfe.js → MobileProjects-BHpuzsDl.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-B7S3W-XL.js → Mtc-Ch0jOQfZ.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-B8OMEWVb.js → MtcAudit-qzwQrDi7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CowRp7GH.js → Multisig-B5wHzUcm.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BkQNyi4c.js → NLProgramming-Hy9uAiA0.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Cm7gtkKz.js → Notes-DkuchMC7.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-CbPTVh9m.js → NotificationSettings-CYZhD7Pi.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dswi26mJ.js → OrderTableRenderer-DKOoqajt.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BkjQ9yy7.js → Organization-D9AXn6j8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DmRFXP7Y.js → Overflow-Bg15J6hX.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DQE1toAu.js → P2P--B54HoQn.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-Cz9Kk2Nb.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-xSjfOaiU.js → Permissions-DHx38eS1.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-B9ICLqVP.js → PersonalDataHub-Dw8i2hAs.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-CUNwyN9E.js → Pipeline-B2o6Ur2G.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-SOoCricX.js → Privacy-8g-h2fnu.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CSaFul0F.js → ProjectInit-DuVTcINx.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CueiB_oe.js → ProjectSettings-C8njvdZX.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Ca9BAfu7.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CTqf_31h.js → Providers-wFqgod_W.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C8ZP31E9.js → QrScannerModal-9NCXrpmA.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CPgn4713.js → QuickAsk-WUTNvFkY.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-8_e0zw0i.js → Recommend-xT4nYmju.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-iT7lbGqw.js → RemoteSession-fak1PBGC.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-B4q9SUOz.js → Reputation-D1AkQvD-.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CA_nASyL.js → Row-DVfm6xGt.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Cwb0E3yv.js → RssFeed-BDunoLSE.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D9iVsOxJ.js → Search-OPycRoU3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DPLbYchG.js → Security-C8-Fm3Dt.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DGMVL_LF.js → Services-qM_sMpMl.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B_6kym0D.js → Skeleton-D3HBJjVF.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-Ds6r0dXB.js → Skills-jr67X-tP.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BLHAbBhb.js → Sla-CS3ggyCg.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CZuddd14.js → SpeechSettings-CTOCYxOi.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CdBq7oSl.js → SyncSettings-D3s3cLT5.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DfT60Nyl.js → Tasks-CdfoJ25m.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-RLfsKy2V.js → Templates-Drb4AI8X.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Cw8xCNLE.js → Tenant-DzwR3Qkz.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BOUkNu4f.js → Terminal-D-ESq7xp.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CgGZWaRm.js → TimelineRenderer-BK-AUuws.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CpwjVyir.js → Tokens-xGTouuNp.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DL21H-eb.js → Trigger-BvZj-v2H.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B3ffFclW.js → Trust-BLC9CGmc.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CkBeb5aI.js → UkeySign-BSuCXpmE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-U2iVaE3r.js → VideoEditing-DT3A-_Ql.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CQk2wzYd.js → Wallet-c1OXao85.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DYIuIYzT.js → WebAuthn-CuA-F7KJ.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-tg1c2mZQ.js → WorkflowEditor-MANZrf82.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CtL61spq.js → chat-D4Y4Ey0O.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DKpbeo8s.js → colors-DDH06hKO.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-gPZsrswt.js → compact-item-Cym756Xj.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CfHlwzVr.js → createContext-By4Nb1k-.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-KO3pAGeO.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-MV47g_6X.js → hasIn-Ch7OY258.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMMvlLp9.js → index-6-2eYBXr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsH-i_3d.js → index-6CpOV3Pn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB1HnbNK.js → index-ArMrnx0v.js} +1 -1
- package/src/assets/web-panel/assets/{index-DU4rZpiK.js → index-B51otVoZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRv_uMLV.js → index-BBBUn5v7.js} +1 -1
- package/src/assets/web-panel/assets/{index-srBc9U_s.js → index-BFydNU6a.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnZ_zS1M.js → index-BIrRX-ra.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGtEBodr.js → index-BZB9q-Lp.js} +1 -1
- package/src/assets/web-panel/assets/index-Bh4Rgkg2.js +1 -0
- package/src/assets/web-panel/assets/{index-BjlPD7GO.js → index-Bo20BfdS.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_B9LiKs.js → index-CF3Lhyns.js} +1 -1
- package/src/assets/web-panel/assets/{index-Rb7CRMw9.js → index-CGsvEbbq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIeJmlAA.js → index-CJPks33Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-xUlgG0Xe.js → index-CT-3FYwd.js} +1 -1
- package/src/assets/web-panel/assets/{index-6G0ETFTW.js → index-CUSUzaB8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzfxTSPW.js → index-CV1UC4zo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmPJpLoD.js → index-CV8qXTvb.js} +1 -1
- package/src/assets/web-panel/assets/{index-fYcS7NS0.js → index-CZn6dmZe.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cjv0kKCU.js → index-CjNUTBwe.js} +1 -1
- package/src/assets/web-panel/assets/{index-Df9sWKpl.js → index-CpudmDhf.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9RF4ieu.js → index-CykROEJA.js} +1 -1
- package/src/assets/web-panel/assets/{index-Zvkt9xqi.js → index-D5sADPGY.js} +1 -1
- package/src/assets/web-panel/assets/{index-yxxXlqBA.js → index-D9le0-dc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBw52sEl.js → index-DAkk1M7x.js} +1 -1
- package/src/assets/web-panel/assets/{index-ExAGPAnX.js → index-DNEJzc0r.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQp5M0s1.js → index-DNlxihs8.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ddcpj1El.js → index-DiewptTL.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOyGUuBE.js → index-DjwgJ5Jd.js} +1 -1
- package/src/assets/web-panel/assets/index-Dox_coTY.js +1 -0
- package/src/assets/web-panel/assets/{index-CmM9jqwe.js → index-DtGValNi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfLyHCWi.js → index-DvnywwUj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN-wxSQ8.js → index-OU8DZ9BL.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1XB5-t5.js → index-XGeQMvWK.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4lF-f_z.js → index-ZY2Qs_Sn.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9gtY4nG.js → index-jx7kr6mB.js} +3 -3
- package/src/assets/web-panel/assets/{index-C6nC_DyY.js → index-mL3nOcH6.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dd19vRwy.js → index-oBsfUbyH.js} +1 -1
- package/src/assets/web-panel/assets/{index-bSFrAszw.js → index-q-s04OKn.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_Pl1GnW.js → index-tZ5BUdad.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BeOZoSZ4.js → initDefaultProps-DGcX9iPP.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BXE8VMKx.js → motion-2SeL1D8A.js} +1 -1
- package/src/assets/web-panel/assets/{move-I9sdvPGX.js → move-3cYbQCpS.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-d25zrJd0.js → mtc-parser-IDdxfw0z.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D71V_T4e.js → omit-BNswdKO6.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BNZKpIAR.js → pickAttrs-BwCnem2I.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-jOcgxTTz.js → placementArrow-B9PtZ5rq.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-pta_O8AU.js → responsiveObserve-CPB6iSzV.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Dlq6om7z.js → slide-DZgyblZa.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BnkO_yRv.js → statusUtils-D82IBdsx.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-p8NfWmcG.js → styleChecker-qSHtosM-.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-KcjZZ5U7.js → useFlexGapSupport-CmWO7h0I.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-gqonDMWB.js → useFs-yLrs_ub4.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-jIIfE4go.js → usePersonalDataHub-CJ1VW6rh.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Dyo7fBrM.js → vnode-CzY_moQ4.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-6l8BMg9W.js → zoom-DAa7rEY5.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +62 -11
- package/src/commands/learning.js +71 -1
- package/src/commands/team.js +100 -3
- package/src/data/changelog.json +29 -3
- package/src/harness/worktree-isolator.js +74 -9
- package/src/lib/agent-team/task-lease.js +12 -1
- package/src/lib/agent-team/team-graph-runtime-adapter.js +6 -0
- package/src/lib/agent-team/team-runner.js +4 -0
- package/src/lib/agent-team/team-worktree.js +4 -0
- package/src/lib/capability-manifest.js +212 -1
- package/src/lib/evolution/evolution-artifact-ports.js +3277 -0
- package/src/lib/evolution/evolution-eval-gate.js +6442 -0
- package/src/lib/evolution/evolution-evidence-projector.js +3786 -0
- package/src/lib/evolution/evolution-ledger-ports.js +2689 -0
- package/src/lib/evolution/evolution-ledger.js +4280 -0
- package/src/lib/evolution/skill-candidate-registry.js +2258 -0
- package/src/lib/evolution/skill-execution-manifest.js +942 -0
- package/src/lib/evolution/skill-mutation-authority.js +1677 -0
- package/src/lib/evolution/skill-promotion-controller.js +506 -0
- package/src/lib/evolution/skill-release-registry.js +3277 -0
- package/src/lib/evolution/skill-target-matrix-eval.js +3233 -0
- package/src/lib/evolution/skill-writer-inventory-manifest.js +998 -0
- package/src/lib/evolution/skill-writer-inventory-validator.js +816 -0
- package/src/lib/formal-quality-eval-runtime.js +192 -0
- package/src/lib/graph-kernel/cutover-contract.js +7 -0
- package/src/lib/graph-kernel/cutover-ledger.js +8 -7
- package/src/lib/graph-kernel/evidence-digest.js +47 -0
- package/src/lib/graph-kernel/graph-runtime-surfaces.json +8 -1
- package/src/lib/graph-kernel/production-cutover-evidence.js +342 -15
- package/src/lib/graph-kernel/production-source-evidence.js +1599 -0
- package/src/lib/graph-kernel/retirement-evidence.js +15 -5
- package/src/lib/graph-kernel/runtime-surface-manifest.js +67 -2
- package/src/lib/headless-manifest.js +3 -1
- package/src/lib/learning/skill-improver.js +311 -43
- package/src/lib/learning/skill-synthesizer.js +236 -16
- package/src/lib/process-execution-broker/index.js +32 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +5 -4
- package/src/lib/secure-fs.js +45 -2
- package/src/runtime/agent-core.js +17 -5
- package/src/runtime/headless-runner.js +16 -9
- package/src/assets/web-panel/assets/MarkdownRenderer-BEPDcSwz.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-BujyoDRG.js +0 -7
- package/src/assets/web-panel/assets/Projects-BOeXvU7y.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DdqnMhzi.js +0 -1
- package/src/assets/web-panel/assets/index-BcK8o4ke.js +0 -1
- package/src/assets/web-panel/assets/index-DvbAjyYI.js +0 -1
|
@@ -0,0 +1,3277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant-bound durable artifact/envelope adapter for the evolution plane.
|
|
3
|
+
*
|
|
4
|
+
* ArtifactStore ids are deliberately treated only as short, local locators.
|
|
5
|
+
* Authority comes from the signed envelope, its recordDigest, exact lineage,
|
|
6
|
+
* and a fresh readback of the canonical bytes. ArtifactStore does not fsync its
|
|
7
|
+
* parent directory, so receipts below claim persisted/read-back integrity, not
|
|
8
|
+
* power-loss durability, WORM storage, or durable CAS.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import crypto from "node:crypto";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { types as utilTypes } from "node:util";
|
|
15
|
+
import { ArtifactStore } from "../artifact-store.js";
|
|
16
|
+
import {
|
|
17
|
+
EVOLUTION_ARTIFACT_REF_SCHEMA,
|
|
18
|
+
EVOLUTION_ARTIFACT_RESOLUTION_SCHEMA,
|
|
19
|
+
} from "./evolution-ledger.js";
|
|
20
|
+
|
|
21
|
+
export const EVOLUTION_DURABLE_ARTIFACT_RECORD_SCHEMA =
|
|
22
|
+
"chainlesschain.evolution-durable-artifact-record/v1";
|
|
23
|
+
export const EVOLUTION_ARTIFACT_ENVELOPE_CORE_SCHEMA =
|
|
24
|
+
"chainlesschain.evolution-artifact-envelope-core/v1";
|
|
25
|
+
export const EVOLUTION_ARTIFACT_ENVELOPE_SCHEMA =
|
|
26
|
+
"chainlesschain.evolution-artifact-envelope/v1";
|
|
27
|
+
export const EVOLUTION_ARTIFACT_LINEAGE_SCHEMA =
|
|
28
|
+
"chainlesschain.evolution-artifact-lineage/v1";
|
|
29
|
+
export const EVOLUTION_ARTIFACT_AUTHORITY_REQUEST_SCHEMA =
|
|
30
|
+
"chainlesschain.evolution-artifact-authority-request/v1";
|
|
31
|
+
export const EVOLUTION_ARTIFACT_AUTHORITY_DECISION_SCHEMA =
|
|
32
|
+
"chainlesschain.evolution-artifact-authority-decision/v1";
|
|
33
|
+
export const EVOLUTION_ARTIFACT_PERSISTENCE_RECEIPT_SCHEMA =
|
|
34
|
+
"chainlesschain.evolution-artifact-persistence-receipt/v1";
|
|
35
|
+
export const EVOLUTION_ARTIFACT_RESOLVED_SCHEMA =
|
|
36
|
+
"chainlesschain.evolution-artifact-resolved/v1";
|
|
37
|
+
export const EVOLUTION_ARTIFACT_RESOLUTION_RECEIPT_SCHEMA =
|
|
38
|
+
"chainlesschain.evolution-artifact-resolution-receipt/v1";
|
|
39
|
+
|
|
40
|
+
export const EVOLUTION_ARTIFACT_INVALID_CODE = "CC_EVOLUTION_ARTIFACT_INVALID";
|
|
41
|
+
export const EVOLUTION_ARTIFACT_TYPE_DENIED_CODE =
|
|
42
|
+
"CC_EVOLUTION_ARTIFACT_TYPE_DENIED";
|
|
43
|
+
export const EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE =
|
|
44
|
+
"CC_EVOLUTION_ARTIFACT_AUTHORITY_DENIED";
|
|
45
|
+
export const EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE =
|
|
46
|
+
"CC_EVOLUTION_ARTIFACT_SIGNATURE_INVALID";
|
|
47
|
+
export const EVOLUTION_ARTIFACT_EXPIRED_CODE = "CC_EVOLUTION_ARTIFACT_EXPIRED";
|
|
48
|
+
export const EVOLUTION_ARTIFACT_NOT_FOUND_CODE =
|
|
49
|
+
"CC_EVOLUTION_ARTIFACT_NOT_FOUND";
|
|
50
|
+
export const EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE =
|
|
51
|
+
"CC_EVOLUTION_ARTIFACT_INTEGRITY_FAILED";
|
|
52
|
+
export const EVOLUTION_ARTIFACT_STORE_FAILED_CODE =
|
|
53
|
+
"CC_EVOLUTION_ARTIFACT_STORE_FAILED";
|
|
54
|
+
|
|
55
|
+
export const EVOLUTION_ARTIFACT_MAX_CANONICAL_BYTES = 1024 * 1024;
|
|
56
|
+
export const EVOLUTION_ARTIFACT_MAX_ENVELOPE_BYTES = 4096;
|
|
57
|
+
export const EVOLUTION_ARTIFACT_MAX_INDEX_ENTRIES = 10_000;
|
|
58
|
+
export const EVOLUTION_ARTIFACT_MAX_INDEX_BYTES = 64 * 1024 * 1024;
|
|
59
|
+
export const EVOLUTION_ARTIFACT_DEFAULT_TTL_MS = 15 * 60 * 1000;
|
|
60
|
+
export const EVOLUTION_ARTIFACT_MAX_TTL_MS = 24 * 60 * 60 * 1000;
|
|
61
|
+
export const EVOLUTION_ARTIFACT_RETENTIONS = Object.freeze(["ttl", "ledger"]);
|
|
62
|
+
export const EVOLUTION_ARTIFACT_LEDGER_RETENTION_TYPES = Object.freeze([
|
|
63
|
+
"skill-release-transition-intent",
|
|
64
|
+
"skill-release-finalization",
|
|
65
|
+
"skill-mutation-audit",
|
|
66
|
+
"skill-mutation-nonce-claim",
|
|
67
|
+
]);
|
|
68
|
+
export const EVOLUTION_ARTIFACT_LEDGER_RETENTION_PURPOSES = Object.freeze([
|
|
69
|
+
"evolution-ledger",
|
|
70
|
+
"skill-mutation",
|
|
71
|
+
"skill-release-transition",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A finite product allow-list. Deployments may pass a narrower constructor
|
|
76
|
+
* allow-list, but may not widen it with arbitrary caller-provided strings.
|
|
77
|
+
*/
|
|
78
|
+
export const EVOLUTION_ARTIFACT_TYPES = Object.freeze([
|
|
79
|
+
"actor",
|
|
80
|
+
"candidate",
|
|
81
|
+
"candidate-diff",
|
|
82
|
+
"diff",
|
|
83
|
+
"eval",
|
|
84
|
+
"eval-policy",
|
|
85
|
+
"eval-receipt",
|
|
86
|
+
"eval-suite",
|
|
87
|
+
"evaluation",
|
|
88
|
+
"evaluation-result",
|
|
89
|
+
"evidence",
|
|
90
|
+
"model-projection",
|
|
91
|
+
"policy",
|
|
92
|
+
"projection",
|
|
93
|
+
"promotion-receipt",
|
|
94
|
+
"receipt",
|
|
95
|
+
"recording",
|
|
96
|
+
"release-receipt",
|
|
97
|
+
"runtime",
|
|
98
|
+
"skill-candidate",
|
|
99
|
+
"skill-mutation-audit",
|
|
100
|
+
"skill-mutation-nonce-claim",
|
|
101
|
+
"skill-release-finalization",
|
|
102
|
+
"skill-release-transition-intent",
|
|
103
|
+
"source",
|
|
104
|
+
"source-evidence",
|
|
105
|
+
"target",
|
|
106
|
+
"trusted-projection",
|
|
107
|
+
"witness",
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const ENVELOPE_SIGNATURE_DOMAIN =
|
|
111
|
+
"chainlesschain.evolution-artifact-envelope/v1\0";
|
|
112
|
+
const AUTHORITY_RECEIPT_DOMAIN =
|
|
113
|
+
"chainlesschain.evolution-artifact-authority-decision/v1\0";
|
|
114
|
+
const PERSISTENCE_RECEIPT_DOMAIN =
|
|
115
|
+
"chainlesschain.evolution-artifact-persistence-receipt/v1\0";
|
|
116
|
+
const RESOLUTION_RECEIPT_DOMAIN =
|
|
117
|
+
"chainlesschain.evolution-artifact-resolution-receipt/v1\0";
|
|
118
|
+
const ARTIFACT_REF_PREFIX = "cc-evolution-artifact:";
|
|
119
|
+
const MAX_CANONICAL_DEPTH = 32;
|
|
120
|
+
const MAX_CANONICAL_NODES = 100_000;
|
|
121
|
+
const MAX_ARRAY_ENTRIES = 65_536;
|
|
122
|
+
const MAX_OBJECT_FIELDS = 4096;
|
|
123
|
+
const MAX_KEY_CHARS = 512;
|
|
124
|
+
const MAX_STRING_CHARS = EVOLUTION_ARTIFACT_MAX_CANONICAL_BYTES;
|
|
125
|
+
const MAX_AUTHORITY_DECISION_TTL_MS = 60_000;
|
|
126
|
+
const MAX_INDEX_ENTRY_BYTES = 16 * 1024;
|
|
127
|
+
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
128
|
+
// ArtifactStore requires housekeeping TTL metadata. This deliberately distant
|
|
129
|
+
// operational horizon is neither embedded in the signed ledger envelope nor
|
|
130
|
+
// returned as an authority promise. Removal at any time still fails closed.
|
|
131
|
+
const LEDGER_HOUSEKEEPING_TTL_DAYS = 1_000_000;
|
|
132
|
+
const MAX_DATE_MILLISECONDS = 8_640_000_000_000_000;
|
|
133
|
+
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
134
|
+
const SAFE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
|
|
135
|
+
const TYPE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
|
|
136
|
+
const ALGORITHM_PATTERN = /^[a-z0-9][a-z0-9._+-]{0,63}$/u;
|
|
137
|
+
const KEY_ID_PATTERN = /^[a-z][a-z0-9+.-]{1,31}:[^\s\\]{1,254}$/u;
|
|
138
|
+
const SIGNATURE_VALUE_PATTERN = /^[A-Za-z0-9_-]{32,2048}$/u;
|
|
139
|
+
const ARTIFACT_ID_PATTERN = /^art_[a-z0-9-]{1,32}_[a-f0-9]{8}$/u;
|
|
140
|
+
const INDEX_ENTRY_KEYS = new Set([
|
|
141
|
+
"createdAt",
|
|
142
|
+
"expiresAt",
|
|
143
|
+
"file",
|
|
144
|
+
"id",
|
|
145
|
+
"immutable",
|
|
146
|
+
"kind",
|
|
147
|
+
"lineage",
|
|
148
|
+
"mime",
|
|
149
|
+
"recordDigest",
|
|
150
|
+
"sessionId",
|
|
151
|
+
"sha256",
|
|
152
|
+
"size",
|
|
153
|
+
"sourcePath",
|
|
154
|
+
"title",
|
|
155
|
+
]);
|
|
156
|
+
const LINEAGE_KEYS = new Set([
|
|
157
|
+
"audience",
|
|
158
|
+
"envelope",
|
|
159
|
+
"envelopeDigest",
|
|
160
|
+
"purpose",
|
|
161
|
+
"recordDigest",
|
|
162
|
+
"retention",
|
|
163
|
+
"schema",
|
|
164
|
+
"tenantId",
|
|
165
|
+
"type",
|
|
166
|
+
]);
|
|
167
|
+
const RECORD_KEYS = new Set([
|
|
168
|
+
"audience",
|
|
169
|
+
"purpose",
|
|
170
|
+
"retention",
|
|
171
|
+
"schema",
|
|
172
|
+
"tenantId",
|
|
173
|
+
"type",
|
|
174
|
+
"value",
|
|
175
|
+
]);
|
|
176
|
+
const ENVELOPE_KEYS = new Set(["core", "schema", "signature"]);
|
|
177
|
+
const ENVELOPE_CORE_KEYS = new Set([
|
|
178
|
+
"algorithm",
|
|
179
|
+
"audience",
|
|
180
|
+
"authorityReceiptDigest",
|
|
181
|
+
"digest",
|
|
182
|
+
"expiresAt",
|
|
183
|
+
"issuedAt",
|
|
184
|
+
"keyId",
|
|
185
|
+
"policyDigest",
|
|
186
|
+
"policyRevision",
|
|
187
|
+
"purpose",
|
|
188
|
+
"recordDigest",
|
|
189
|
+
"retention",
|
|
190
|
+
"revocationRevision",
|
|
191
|
+
"schema",
|
|
192
|
+
"tenantId",
|
|
193
|
+
"type",
|
|
194
|
+
]);
|
|
195
|
+
const SIGNATURE_KEYS = new Set(["algorithm", "keyId", "value"]);
|
|
196
|
+
const PUT_CONTEXT_KEYS = new Set(["audience", "purpose", "retention", "ttlMs"]);
|
|
197
|
+
const RESOLVE_OPTION_KEYS = new Set([
|
|
198
|
+
"expectedDigest",
|
|
199
|
+
"expectedType",
|
|
200
|
+
"purpose",
|
|
201
|
+
"tenantId",
|
|
202
|
+
]);
|
|
203
|
+
const AUTHORITY_DECISION_KEYS = new Set([
|
|
204
|
+
"action",
|
|
205
|
+
"algorithm",
|
|
206
|
+
"allowed",
|
|
207
|
+
"audience",
|
|
208
|
+
"checkedAt",
|
|
209
|
+
"decisionExpiresAt",
|
|
210
|
+
"digest",
|
|
211
|
+
"keyId",
|
|
212
|
+
"policyDigest",
|
|
213
|
+
"policyRevision",
|
|
214
|
+
"issuedAt",
|
|
215
|
+
"issuedPolicyDigest",
|
|
216
|
+
"issuedPolicyRevision",
|
|
217
|
+
"issuedPolicyTrusted",
|
|
218
|
+
"receiptDigest",
|
|
219
|
+
"requestedAt",
|
|
220
|
+
"retention",
|
|
221
|
+
"revocationRevision",
|
|
222
|
+
"revoked",
|
|
223
|
+
"schema",
|
|
224
|
+
"tenantId",
|
|
225
|
+
"type",
|
|
226
|
+
"purpose",
|
|
227
|
+
]);
|
|
228
|
+
const PUBLISH_RESULT_KEYS = new Set(["entry", "published"]);
|
|
229
|
+
const INTEGRITY_RESULT_KEYS = new Set([
|
|
230
|
+
"actualSha256",
|
|
231
|
+
"expectedSha256",
|
|
232
|
+
"ok",
|
|
233
|
+
"reason",
|
|
234
|
+
]);
|
|
235
|
+
const ARTIFACT_REF_KEYS = new Set(["digest", "ref", "schema"]);
|
|
236
|
+
const LEDGER_REQUEST_KEYS = new Set(["epoch", "ledgerId", "ref", "tenantId"]);
|
|
237
|
+
const CONSTRUCTOR_OPTION_KEYS = new Set([
|
|
238
|
+
"allowedTypes",
|
|
239
|
+
"artifactStore",
|
|
240
|
+
"audience",
|
|
241
|
+
"currentAuthorityResolver",
|
|
242
|
+
"envelopeSigner",
|
|
243
|
+
"envelopeVerifier",
|
|
244
|
+
"now",
|
|
245
|
+
"tenantId",
|
|
246
|
+
]);
|
|
247
|
+
const CONSTRUCTOR_REQUIRED_KEYS = new Set([
|
|
248
|
+
"artifactStore",
|
|
249
|
+
"audience",
|
|
250
|
+
"currentAuthorityResolver",
|
|
251
|
+
"envelopeSigner",
|
|
252
|
+
"envelopeVerifier",
|
|
253
|
+
"tenantId",
|
|
254
|
+
]);
|
|
255
|
+
const LEDGER_RESOLVER_OPTION_KEYS = new Set(["purpose"]);
|
|
256
|
+
const isProxy = Object.freeze(utilTypes.isProxy.bind(utilTypes));
|
|
257
|
+
const isDate = Object.freeze(utilTypes.isDate.bind(utilTypes));
|
|
258
|
+
const dateGetTime = Object.freeze(
|
|
259
|
+
Function.prototype.call.bind(Date.prototype.getTime),
|
|
260
|
+
);
|
|
261
|
+
const RETENTION_SET = new Set(EVOLUTION_ARTIFACT_RETENTIONS);
|
|
262
|
+
const LEDGER_RETENTION_PURPOSES_BY_TYPE = new Map([
|
|
263
|
+
[
|
|
264
|
+
"skill-release-transition-intent",
|
|
265
|
+
new Set(["evolution-ledger", "skill-release-transition"]),
|
|
266
|
+
],
|
|
267
|
+
[
|
|
268
|
+
"skill-release-finalization",
|
|
269
|
+
new Set(["evolution-ledger", "skill-release-transition"]),
|
|
270
|
+
],
|
|
271
|
+
["skill-mutation-audit", new Set(["evolution-ledger", "skill-mutation"])],
|
|
272
|
+
[
|
|
273
|
+
"skill-mutation-nonce-claim",
|
|
274
|
+
new Set(["evolution-ledger", "skill-mutation"]),
|
|
275
|
+
],
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
export class EvolutionArtifactPortError extends Error {
|
|
279
|
+
constructor(code, message, details = {}) {
|
|
280
|
+
super(message, details.cause ? { cause: details.cause } : undefined);
|
|
281
|
+
this.name = "EvolutionArtifactPortError";
|
|
282
|
+
this.code = code;
|
|
283
|
+
for (const [key, value] of Object.entries(details)) {
|
|
284
|
+
if (key !== "cause") this[key] = value;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isEvolutionArtifactPortError(value) {
|
|
290
|
+
if (!value || (typeof value !== "object" && typeof value !== "function")) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
let cursor = value;
|
|
294
|
+
try {
|
|
295
|
+
while (cursor) {
|
|
296
|
+
if (isProxy(cursor)) return false;
|
|
297
|
+
if (cursor === EvolutionArtifactPortError.prototype) return true;
|
|
298
|
+
cursor = Object.getPrototypeOf(cursor);
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function hasStablePrototype(value, expectedPrototype, label) {
|
|
307
|
+
if (!value || (typeof value !== "object" && typeof value !== "function")) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
let cursor = value;
|
|
311
|
+
while (cursor) {
|
|
312
|
+
rejectProxy(cursor, `${label} prototype chain`);
|
|
313
|
+
if (cursor === expectedPrototype) return true;
|
|
314
|
+
cursor = Object.getPrototypeOf(cursor);
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function artifactError(code, message, details = {}) {
|
|
320
|
+
return new EvolutionArtifactPortError(code, message, details);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function rejectProxy(value, label, code = EVOLUTION_ARTIFACT_INVALID_CODE) {
|
|
324
|
+
if (
|
|
325
|
+
value !== null &&
|
|
326
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
327
|
+
isProxy(value)
|
|
328
|
+
) {
|
|
329
|
+
throw artifactError(
|
|
330
|
+
code,
|
|
331
|
+
`${label} must not be a Proxy; provide stable own-data JSON values`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isPlainRecord(
|
|
337
|
+
value,
|
|
338
|
+
label = "value",
|
|
339
|
+
code = EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
340
|
+
) {
|
|
341
|
+
if (!value || typeof value !== "object") return false;
|
|
342
|
+
rejectProxy(value, label, code);
|
|
343
|
+
if (Array.isArray(value)) return false;
|
|
344
|
+
const prototype = Object.getPrototypeOf(value);
|
|
345
|
+
return prototype === Object.prototype || prototype === null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function ownData(
|
|
349
|
+
value,
|
|
350
|
+
key,
|
|
351
|
+
label,
|
|
352
|
+
{ enumerable = true, code = EVOLUTION_ARTIFACT_INVALID_CODE } = {},
|
|
353
|
+
) {
|
|
354
|
+
rejectProxy(value, label, code);
|
|
355
|
+
let descriptor;
|
|
356
|
+
try {
|
|
357
|
+
descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
358
|
+
} catch (cause) {
|
|
359
|
+
throw artifactError(
|
|
360
|
+
code,
|
|
361
|
+
`${label}.${String(key)} descriptor could not be inspected`,
|
|
362
|
+
{ cause },
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (
|
|
366
|
+
!descriptor ||
|
|
367
|
+
!("value" in descriptor) ||
|
|
368
|
+
(enumerable && descriptor.enumerable !== true)
|
|
369
|
+
) {
|
|
370
|
+
throw artifactError(
|
|
371
|
+
code,
|
|
372
|
+
`${label}.${String(key)} must be an enumerable own data property`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return descriptor.value;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function assertExactRecord(
|
|
379
|
+
value,
|
|
380
|
+
keys,
|
|
381
|
+
label,
|
|
382
|
+
code = EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
383
|
+
) {
|
|
384
|
+
if (!isPlainRecord(value, label, code)) {
|
|
385
|
+
throw artifactError(code, `${label} must be a plain object`);
|
|
386
|
+
}
|
|
387
|
+
let ownKeys;
|
|
388
|
+
try {
|
|
389
|
+
ownKeys = Reflect.ownKeys(value);
|
|
390
|
+
} catch (cause) {
|
|
391
|
+
throw artifactError(code, `${label} keys could not be inspected`, {
|
|
392
|
+
cause,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (
|
|
396
|
+
ownKeys.length !== keys.size ||
|
|
397
|
+
ownKeys.some((key) => typeof key !== "string" || !keys.has(key))
|
|
398
|
+
) {
|
|
399
|
+
throw artifactError(
|
|
400
|
+
code,
|
|
401
|
+
`${label} must contain exactly the supported fields`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
for (const key of ownKeys) ownData(value, key, label, { code });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function assertOptionalExactRecord(
|
|
408
|
+
value,
|
|
409
|
+
keys,
|
|
410
|
+
required,
|
|
411
|
+
label,
|
|
412
|
+
code = EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
413
|
+
) {
|
|
414
|
+
if (!isPlainRecord(value, label, code)) {
|
|
415
|
+
throw artifactError(code, `${label} must be a plain object`);
|
|
416
|
+
}
|
|
417
|
+
let ownKeys;
|
|
418
|
+
try {
|
|
419
|
+
ownKeys = Reflect.ownKeys(value);
|
|
420
|
+
} catch (cause) {
|
|
421
|
+
throw artifactError(code, `${label} keys could not be inspected`, {
|
|
422
|
+
cause,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (
|
|
426
|
+
ownKeys.some((key) => typeof key !== "string" || !keys.has(key)) ||
|
|
427
|
+
[...required].some((key) => !ownKeys.includes(key))
|
|
428
|
+
) {
|
|
429
|
+
throw artifactError(
|
|
430
|
+
code,
|
|
431
|
+
`${label} contains missing or unsupported fields`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
for (const key of ownKeys) ownData(value, key, label, { code });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function assertDenseDataArray(value, label, maximum) {
|
|
438
|
+
rejectProxy(value, label);
|
|
439
|
+
if (!Array.isArray(value) || value.length > maximum) {
|
|
440
|
+
throw artifactError(
|
|
441
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
442
|
+
`${label} must be a bounded array`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const allowed = new Set([
|
|
446
|
+
"length",
|
|
447
|
+
...Array.from({ length: value.length }, (_, index) => String(index)),
|
|
448
|
+
]);
|
|
449
|
+
let ownKeys;
|
|
450
|
+
try {
|
|
451
|
+
ownKeys = Reflect.ownKeys(value);
|
|
452
|
+
} catch (cause) {
|
|
453
|
+
throw artifactError(
|
|
454
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
455
|
+
`${label} keys could not be inspected`,
|
|
456
|
+
{ cause },
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (
|
|
460
|
+
ownKeys.length !== value.length + 1 ||
|
|
461
|
+
ownKeys.some((key) => typeof key !== "string" || !allowed.has(key))
|
|
462
|
+
) {
|
|
463
|
+
throw artifactError(
|
|
464
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
465
|
+
`${label} must be dense and contain only indexed entries`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
469
|
+
ownData(value, String(index), label);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function appendCanonical(state, fragment) {
|
|
474
|
+
state.byteLength += Buffer.byteLength(fragment, "utf8");
|
|
475
|
+
if (state.byteLength > state.maximumBytes) {
|
|
476
|
+
throw artifactError(
|
|
477
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
478
|
+
`canonical JSON exceeds ${state.maximumBytes} bytes`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
state.fragments.push(fragment);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function canonicalWalk(value, state, depth) {
|
|
485
|
+
state.nodes += 1;
|
|
486
|
+
if (state.nodes > MAX_CANONICAL_NODES || depth > MAX_CANONICAL_DEPTH) {
|
|
487
|
+
throw artifactError(
|
|
488
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
489
|
+
"canonical JSON exceeds its node or depth limit",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
if (value === null || typeof value === "boolean") {
|
|
493
|
+
appendCanonical(state, JSON.stringify(value));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (typeof value === "number") {
|
|
497
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) {
|
|
498
|
+
throw artifactError(
|
|
499
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
500
|
+
"canonical JSON numbers must be finite and must not be negative zero",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
appendCanonical(state, JSON.stringify(value));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (typeof value === "string") {
|
|
507
|
+
if (value.length > MAX_STRING_CHARS) {
|
|
508
|
+
throw artifactError(
|
|
509
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
510
|
+
"canonical JSON string exceeds its character limit",
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
appendCanonical(state, JSON.stringify(value));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (!value || typeof value !== "object") {
|
|
517
|
+
throw artifactError(
|
|
518
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
519
|
+
"canonical JSON contains an unsupported value",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
rejectProxy(value, "canonical JSON value");
|
|
523
|
+
if (state.seen.has(value)) {
|
|
524
|
+
throw artifactError(
|
|
525
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
526
|
+
"canonical JSON must not contain cycles",
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
state.seen.add(value);
|
|
530
|
+
try {
|
|
531
|
+
if (Array.isArray(value)) {
|
|
532
|
+
assertDenseDataArray(value, "canonical JSON array", MAX_ARRAY_ENTRIES);
|
|
533
|
+
appendCanonical(state, "[");
|
|
534
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
535
|
+
if (index > 0) appendCanonical(state, ",");
|
|
536
|
+
canonicalWalk(
|
|
537
|
+
ownData(value, String(index), "canonical JSON array"),
|
|
538
|
+
state,
|
|
539
|
+
depth + 1,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
appendCanonical(state, "]");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!isPlainRecord(value, "canonical JSON object")) {
|
|
546
|
+
throw artifactError(
|
|
547
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
548
|
+
"canonical JSON objects must use Object or null prototypes",
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
let keys;
|
|
552
|
+
try {
|
|
553
|
+
keys = Reflect.ownKeys(value);
|
|
554
|
+
} catch (cause) {
|
|
555
|
+
throw artifactError(
|
|
556
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
557
|
+
"canonical JSON object keys could not be inspected",
|
|
558
|
+
{ cause },
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
if (
|
|
562
|
+
keys.length > MAX_OBJECT_FIELDS ||
|
|
563
|
+
keys.some((key) => typeof key !== "string" || key.length > MAX_KEY_CHARS)
|
|
564
|
+
) {
|
|
565
|
+
throw artifactError(
|
|
566
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
567
|
+
"canonical JSON object keys exceed their limits",
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
keys.sort();
|
|
571
|
+
appendCanonical(state, "{");
|
|
572
|
+
for (const [index, key] of keys.entries()) {
|
|
573
|
+
if (index > 0) appendCanonical(state, ",");
|
|
574
|
+
appendCanonical(state, JSON.stringify(key));
|
|
575
|
+
appendCanonical(state, ":");
|
|
576
|
+
canonicalWalk(
|
|
577
|
+
ownData(value, key, "canonical JSON object"),
|
|
578
|
+
state,
|
|
579
|
+
depth + 1,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
appendCanonical(state, "}");
|
|
583
|
+
} finally {
|
|
584
|
+
state.seen.delete(value);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function canonicalJson(
|
|
589
|
+
value,
|
|
590
|
+
maximumBytes = EVOLUTION_ARTIFACT_MAX_CANONICAL_BYTES,
|
|
591
|
+
) {
|
|
592
|
+
const state = {
|
|
593
|
+
byteLength: 0,
|
|
594
|
+
fragments: [],
|
|
595
|
+
maximumBytes,
|
|
596
|
+
nodes: 0,
|
|
597
|
+
seen: new Set(),
|
|
598
|
+
};
|
|
599
|
+
canonicalWalk(value, state, 0);
|
|
600
|
+
return state.fragments.join("");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function deepFreeze(value, seen = new WeakSet()) {
|
|
604
|
+
if (!value || typeof value !== "object" || seen.has(value)) return value;
|
|
605
|
+
rejectProxy(value, "value being frozen");
|
|
606
|
+
seen.add(value);
|
|
607
|
+
for (const child of Object.values(value)) deepFreeze(child, seen);
|
|
608
|
+
return Object.freeze(value);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function frozenCanonicalClone(value) {
|
|
612
|
+
return deepFreeze(JSON.parse(canonicalJson(value)));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function sha256(bytes) {
|
|
616
|
+
return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function domainDigest(domain, value) {
|
|
620
|
+
return sha256(Buffer.from(`${domain}${canonicalJson(value)}`, "utf8"));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function normalizeDigest(value, label, code = EVOLUTION_ARTIFACT_INVALID_CODE) {
|
|
624
|
+
if (typeof value !== "string" || !DIGEST_PATTERN.test(value)) {
|
|
625
|
+
throw artifactError(code, `${label} must be a lowercase SHA-256 digest`);
|
|
626
|
+
}
|
|
627
|
+
return value;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function normalizeSafeId(value, label) {
|
|
631
|
+
if (typeof value !== "string" || !SAFE_ID_PATTERN.test(value)) {
|
|
632
|
+
throw artifactError(
|
|
633
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
634
|
+
`${label} must be a bounded identifier`,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function normalizeType(value, allowedTypes) {
|
|
641
|
+
if (
|
|
642
|
+
typeof value !== "string" ||
|
|
643
|
+
!TYPE_PATTERN.test(value) ||
|
|
644
|
+
!allowedTypes.has(value)
|
|
645
|
+
) {
|
|
646
|
+
throw artifactError(
|
|
647
|
+
EVOLUTION_ARTIFACT_TYPE_DENIED_CODE,
|
|
648
|
+
"evolution artifact type is not in the captured allow-list",
|
|
649
|
+
{ type: typeof value === "string" ? value.slice(0, 128) : null },
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
return value;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function normalizeRetention(
|
|
656
|
+
value,
|
|
657
|
+
label,
|
|
658
|
+
code = EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
659
|
+
) {
|
|
660
|
+
if (typeof value !== "string" || !RETENTION_SET.has(value)) {
|
|
661
|
+
throw artifactError(
|
|
662
|
+
code,
|
|
663
|
+
`${label} must be one of the supported retention classes`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return value;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function assertLedgerRetentionAdmission(type, purpose) {
|
|
670
|
+
const purposes = LEDGER_RETENTION_PURPOSES_BY_TYPE.get(type);
|
|
671
|
+
if (!purposes || !purposes.has(purpose)) {
|
|
672
|
+
throw artifactError(
|
|
673
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
674
|
+
"ledger retention is denied for this artifact type and purpose",
|
|
675
|
+
{ purpose, type },
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function ledgerHousekeepingTtlDays(issuedAtMs) {
|
|
681
|
+
const duration = LEDGER_HOUSEKEEPING_TTL_DAYS * MILLISECONDS_PER_DAY;
|
|
682
|
+
if (
|
|
683
|
+
!Number.isFinite(issuedAtMs) ||
|
|
684
|
+
issuedAtMs > MAX_DATE_MILLISECONDS - duration
|
|
685
|
+
) {
|
|
686
|
+
throw artifactError(
|
|
687
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
688
|
+
"ledger artifact issuance instant cannot be represented by ArtifactStore housekeeping metadata",
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
return LEDGER_HOUSEKEEPING_TTL_DAYS;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function addCanonicalTimestamp(baseMs, durationMs, label) {
|
|
695
|
+
if (
|
|
696
|
+
!Number.isSafeInteger(durationMs) ||
|
|
697
|
+
durationMs < 0 ||
|
|
698
|
+
baseMs > MAX_DATE_MILLISECONDS - durationMs
|
|
699
|
+
) {
|
|
700
|
+
throw artifactError(
|
|
701
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
702
|
+
`${label} exceeds the portable JavaScript Date range`,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
return new Date(baseMs + durationMs).toISOString();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function normalizeTimestamp(
|
|
709
|
+
value,
|
|
710
|
+
label,
|
|
711
|
+
code = EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
712
|
+
) {
|
|
713
|
+
if (typeof value !== "string" || value.length > 64) {
|
|
714
|
+
throw artifactError(code, `${label} must be a canonical timestamp`);
|
|
715
|
+
}
|
|
716
|
+
let canonical;
|
|
717
|
+
try {
|
|
718
|
+
canonical = new Date(value).toISOString();
|
|
719
|
+
} catch {
|
|
720
|
+
canonical = null;
|
|
721
|
+
}
|
|
722
|
+
if (canonical !== value) {
|
|
723
|
+
throw artifactError(code, `${label} must be a canonical timestamp`);
|
|
724
|
+
}
|
|
725
|
+
return value;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function normalizeNullableTimestamp(value, label) {
|
|
729
|
+
return value === null
|
|
730
|
+
? null
|
|
731
|
+
: normalizeTimestamp(
|
|
732
|
+
value,
|
|
733
|
+
label,
|
|
734
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function normalizeClockValue(value) {
|
|
739
|
+
rejectProxy(value, "evolution artifact clock value");
|
|
740
|
+
if (
|
|
741
|
+
!isDate(value) &&
|
|
742
|
+
typeof value !== "number" &&
|
|
743
|
+
typeof value !== "string"
|
|
744
|
+
) {
|
|
745
|
+
throw artifactError(
|
|
746
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
747
|
+
"evolution artifact clock must return Date, number, or string",
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
const date = isDate(value) ? new Date(dateGetTime(value)) : new Date(value);
|
|
751
|
+
if (!Number.isFinite(date.getTime())) {
|
|
752
|
+
throw artifactError(
|
|
753
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
754
|
+
"evolution artifact clock returned an invalid instant",
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return { iso: date.toISOString(), milliseconds: date.getTime() };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function normalizeRevision(value, label) {
|
|
761
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
762
|
+
throw artifactError(
|
|
763
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
764
|
+
`${label} must be a non-negative safe integer`,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return value;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function normalizeNullableRevision(value, label) {
|
|
771
|
+
return value === null ? null : normalizeRevision(value, label);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function normalizeNullableDigest(value, label) {
|
|
775
|
+
return value === null
|
|
776
|
+
? null
|
|
777
|
+
: normalizeDigest(value, label, EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function normalizeAlgorithm(value, label) {
|
|
781
|
+
if (typeof value !== "string" || !ALGORITHM_PATTERN.test(value)) {
|
|
782
|
+
throw artifactError(
|
|
783
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
784
|
+
`${label} is invalid`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
return value;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function normalizeKeyId(value, label) {
|
|
791
|
+
if (typeof value !== "string" || !KEY_ID_PATTERN.test(value)) {
|
|
792
|
+
throw artifactError(
|
|
793
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
794
|
+
`${label} must be an opaque absolute key URI`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
return value;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function captureMethod(port, method, label) {
|
|
801
|
+
if (!port || (typeof port !== "object" && typeof port !== "function")) {
|
|
802
|
+
throw new TypeError(`EvolutionArtifactPorts requires ${label}.${method}`);
|
|
803
|
+
}
|
|
804
|
+
rejectProxy(port, label);
|
|
805
|
+
let cursor = port;
|
|
806
|
+
while (cursor) {
|
|
807
|
+
rejectProxy(cursor, `${label} prototype`);
|
|
808
|
+
const descriptor = Object.getOwnPropertyDescriptor(cursor, method);
|
|
809
|
+
if (descriptor) {
|
|
810
|
+
if (!("value" in descriptor) || typeof descriptor.value !== "function") {
|
|
811
|
+
throw new TypeError(
|
|
812
|
+
`EvolutionArtifactPorts requires ${label}.${method} as a data method`,
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return Object.freeze(descriptor.value.bind(port));
|
|
816
|
+
}
|
|
817
|
+
cursor = Object.getPrototypeOf(cursor);
|
|
818
|
+
}
|
|
819
|
+
throw new TypeError(`EvolutionArtifactPorts requires ${label}.${method}`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function captureArtifactStore(store) {
|
|
823
|
+
rejectProxy(store, "artifactStore");
|
|
824
|
+
if (!hasStablePrototype(store, ArtifactStore.prototype, "artifactStore")) {
|
|
825
|
+
throw new TypeError(
|
|
826
|
+
"EvolutionArtifactPorts requires the repository ArtifactStore implementation",
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
const dir = ownData(store, "dir", "artifactStore", { enumerable: false });
|
|
830
|
+
if (typeof dir !== "string" || dir.length < 1) {
|
|
831
|
+
throw new TypeError("EvolutionArtifactPorts artifactStore.dir is invalid");
|
|
832
|
+
}
|
|
833
|
+
const resolvedDir = path.resolve(dir);
|
|
834
|
+
const layout = establishStoreLayout(resolvedDir);
|
|
835
|
+
const captured = Object.freeze({
|
|
836
|
+
dir: resolvedDir,
|
|
837
|
+
get: captureMethod(store, "get", "artifactStore"),
|
|
838
|
+
layout,
|
|
839
|
+
list: captureMethod(store, "list", "artifactStore"),
|
|
840
|
+
publishDataOnce: captureMethod(store, "publishDataOnce", "artifactStore"),
|
|
841
|
+
storedPath: captureMethod(store, "storedPath", "artifactStore"),
|
|
842
|
+
verifyIntegrity: captureMethod(store, "verifyIntegrity", "artifactStore"),
|
|
843
|
+
});
|
|
844
|
+
// ArtifactStore methods consult public instance fields such as `dir` on
|
|
845
|
+
// every call. Freeze the trusted instance after capturing its methods so a
|
|
846
|
+
// later collaborator cannot redirect a bound get/list/integrity call to a
|
|
847
|
+
// different store root.
|
|
848
|
+
Object.freeze(store);
|
|
849
|
+
return captured;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function rejectPromise(value, label, code) {
|
|
853
|
+
rejectProxy(value, label, code);
|
|
854
|
+
if (value && (typeof value === "object" || typeof value === "function")) {
|
|
855
|
+
let cursor = value;
|
|
856
|
+
while (cursor) {
|
|
857
|
+
rejectProxy(cursor, `${label} prototype`, code);
|
|
858
|
+
const descriptor = Object.getOwnPropertyDescriptor(cursor, "then");
|
|
859
|
+
if (descriptor) {
|
|
860
|
+
if (
|
|
861
|
+
!("value" in descriptor) ||
|
|
862
|
+
typeof descriptor.value === "function"
|
|
863
|
+
) {
|
|
864
|
+
throw artifactError(code, `${label} must be synchronous`);
|
|
865
|
+
}
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
cursor = Object.getPrototypeOf(cursor);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return value;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function samePath(left, right) {
|
|
875
|
+
const normalizedLeft = path.resolve(left);
|
|
876
|
+
const normalizedRight = path.resolve(right);
|
|
877
|
+
return process.platform === "win32"
|
|
878
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
879
|
+
: normalizedLeft === normalizedRight;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function isContained(root, candidate) {
|
|
883
|
+
const relative = path.relative(root, candidate);
|
|
884
|
+
return (
|
|
885
|
+
relative === "" ||
|
|
886
|
+
(relative !== ".." &&
|
|
887
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
888
|
+
!path.isAbsolute(relative))
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function physicalIdentity(stat) {
|
|
893
|
+
return Object.freeze({
|
|
894
|
+
birthtimeMs: Number(stat.birthtimeMs),
|
|
895
|
+
dev: String(stat.dev),
|
|
896
|
+
ino: String(stat.ino),
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function samePhysicalIdentity(stat, expected) {
|
|
901
|
+
return (
|
|
902
|
+
String(stat.dev) === expected.dev &&
|
|
903
|
+
String(stat.ino) === expected.ino &&
|
|
904
|
+
Number(stat.birthtimeMs) === expected.birthtimeMs
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function assertDirectory(stat, label) {
|
|
909
|
+
if (
|
|
910
|
+
!stat ||
|
|
911
|
+
typeof stat.isDirectory !== "function" ||
|
|
912
|
+
!stat.isDirectory() ||
|
|
913
|
+
(typeof stat.isSymbolicLink === "function" && stat.isSymbolicLink())
|
|
914
|
+
) {
|
|
915
|
+
throw artifactError(
|
|
916
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
917
|
+
`${label} must be a physical, non-symlink directory`,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function assertExistingPathComponentsArePhysical(target) {
|
|
923
|
+
const resolved = path.resolve(target);
|
|
924
|
+
const parsed = path.parse(resolved);
|
|
925
|
+
const relative = resolved.slice(parsed.root.length);
|
|
926
|
+
let cursor = parsed.root;
|
|
927
|
+
for (const component of relative.split(path.sep).filter(Boolean)) {
|
|
928
|
+
cursor = path.join(cursor, component);
|
|
929
|
+
let stat;
|
|
930
|
+
try {
|
|
931
|
+
stat = fs.lstatSync(cursor);
|
|
932
|
+
} catch (cause) {
|
|
933
|
+
if (cause?.code === "ENOENT") break;
|
|
934
|
+
throw artifactError(
|
|
935
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
936
|
+
`artifact store path component could not be inspected: ${cursor}`,
|
|
937
|
+
{ cause },
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
assertDirectory(stat, `artifact store path component ${cursor}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function inspectPhysicalDirectory(directory, label) {
|
|
945
|
+
let stat;
|
|
946
|
+
let realPath;
|
|
947
|
+
try {
|
|
948
|
+
stat = fs.lstatSync(directory);
|
|
949
|
+
assertDirectory(stat, label);
|
|
950
|
+
realPath = fs.realpathSync(directory);
|
|
951
|
+
} catch (cause) {
|
|
952
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
953
|
+
throw artifactError(
|
|
954
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
955
|
+
`${label} could not be physically resolved`,
|
|
956
|
+
{ cause },
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
if (!samePath(realPath, directory)) {
|
|
960
|
+
throw artifactError(
|
|
961
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
962
|
+
`${label} or one of its ancestors is a symlink or replaced path`,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
return Object.freeze({
|
|
966
|
+
identity: physicalIdentity(stat),
|
|
967
|
+
realPath: path.resolve(realPath),
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function inspectPhysicalIndex(indexPath, rootRealPath) {
|
|
972
|
+
let pathStat;
|
|
973
|
+
let realPath;
|
|
974
|
+
let descriptor = null;
|
|
975
|
+
try {
|
|
976
|
+
pathStat = fs.lstatSync(indexPath);
|
|
977
|
+
assertRegularSingleLink(pathStat, "ArtifactStore index");
|
|
978
|
+
realPath = fs.realpathSync(indexPath);
|
|
979
|
+
if (
|
|
980
|
+
!samePath(realPath, indexPath) ||
|
|
981
|
+
!isContained(rootRealPath, realPath)
|
|
982
|
+
) {
|
|
983
|
+
throw artifactError(
|
|
984
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
985
|
+
"ArtifactStore index path is not physically contained in its root",
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
descriptor = fs.openSync(
|
|
989
|
+
indexPath,
|
|
990
|
+
fs.constants.O_RDONLY | Number(fs.constants.O_NOFOLLOW || 0),
|
|
991
|
+
);
|
|
992
|
+
const descriptorStat = fs.fstatSync(descriptor);
|
|
993
|
+
assertRegularSingleLink(descriptorStat, "ArtifactStore index descriptor");
|
|
994
|
+
if (!sameFileIdentity(pathStat, descriptorStat)) {
|
|
995
|
+
throw artifactError(
|
|
996
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
997
|
+
"ArtifactStore index pathname and descriptor identities differ",
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
} catch (cause) {
|
|
1001
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
1002
|
+
throw artifactError(
|
|
1003
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1004
|
+
"ArtifactStore index could not be physically captured",
|
|
1005
|
+
{ cause },
|
|
1006
|
+
);
|
|
1007
|
+
} finally {
|
|
1008
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
1009
|
+
}
|
|
1010
|
+
return Object.freeze({
|
|
1011
|
+
identity: physicalIdentity(pathStat),
|
|
1012
|
+
realPath: path.resolve(realPath),
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function establishStoreLayout(directory) {
|
|
1017
|
+
const rootDir = path.resolve(directory);
|
|
1018
|
+
const filesDir = path.join(rootDir, "files");
|
|
1019
|
+
const indexPath = path.join(rootDir, "index.jsonl");
|
|
1020
|
+
assertExistingPathComponentsArePhysical(rootDir);
|
|
1021
|
+
try {
|
|
1022
|
+
fs.mkdirSync(rootDir, { recursive: true, mode: 0o700 });
|
|
1023
|
+
assertExistingPathComponentsArePhysical(rootDir);
|
|
1024
|
+
fs.mkdirSync(filesDir, { mode: 0o700 });
|
|
1025
|
+
} catch (cause) {
|
|
1026
|
+
if (cause?.code !== "EEXIST") {
|
|
1027
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
1028
|
+
throw artifactError(
|
|
1029
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1030
|
+
"ArtifactStore root could not be initialized safely",
|
|
1031
|
+
{ cause },
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const root = inspectPhysicalDirectory(rootDir, "ArtifactStore root");
|
|
1036
|
+
const files = inspectPhysicalDirectory(filesDir, "ArtifactStore files root");
|
|
1037
|
+
if (!isContained(root.realPath, files.realPath)) {
|
|
1038
|
+
throw artifactError(
|
|
1039
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1040
|
+
"ArtifactStore files root escapes the captured store root",
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
let descriptor = null;
|
|
1044
|
+
try {
|
|
1045
|
+
descriptor = fs.openSync(
|
|
1046
|
+
indexPath,
|
|
1047
|
+
fs.constants.O_WRONLY |
|
|
1048
|
+
fs.constants.O_CREAT |
|
|
1049
|
+
fs.constants.O_EXCL |
|
|
1050
|
+
Number(fs.constants.O_NOFOLLOW || 0),
|
|
1051
|
+
0o600,
|
|
1052
|
+
);
|
|
1053
|
+
fs.fsyncSync(descriptor);
|
|
1054
|
+
} catch (cause) {
|
|
1055
|
+
if (cause?.code !== "EEXIST") {
|
|
1056
|
+
throw artifactError(
|
|
1057
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1058
|
+
"ArtifactStore index could not be initialized safely",
|
|
1059
|
+
{ cause },
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
} finally {
|
|
1063
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
1064
|
+
}
|
|
1065
|
+
const index = inspectPhysicalIndex(indexPath, root.realPath);
|
|
1066
|
+
return Object.freeze({
|
|
1067
|
+
filesDir,
|
|
1068
|
+
filesIdentity: files.identity,
|
|
1069
|
+
filesRealPath: files.realPath,
|
|
1070
|
+
indexIdentity: index.identity,
|
|
1071
|
+
indexPath,
|
|
1072
|
+
indexRealPath: index.realPath,
|
|
1073
|
+
rootDir,
|
|
1074
|
+
rootIdentity: root.identity,
|
|
1075
|
+
rootRealPath: root.realPath,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function attestCapturedDirectory(directory, realPath, identity, label) {
|
|
1080
|
+
const inspected = inspectPhysicalDirectory(directory, label);
|
|
1081
|
+
if (
|
|
1082
|
+
!samePath(inspected.realPath, realPath) ||
|
|
1083
|
+
inspected.identity.dev !== identity.dev ||
|
|
1084
|
+
inspected.identity.ino !== identity.ino ||
|
|
1085
|
+
inspected.identity.birthtimeMs !== identity.birthtimeMs
|
|
1086
|
+
) {
|
|
1087
|
+
throw artifactError(
|
|
1088
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1089
|
+
`${label} physical identity changed`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function sameFileIdentity(left, right) {
|
|
1095
|
+
return (
|
|
1096
|
+
left.dev === right.dev &&
|
|
1097
|
+
left.ino === right.ino &&
|
|
1098
|
+
left.size === right.size &&
|
|
1099
|
+
left.mtimeMs === right.mtimeMs &&
|
|
1100
|
+
left.ctimeMs === right.ctimeMs
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function assertRegularSingleLink(stat, label) {
|
|
1105
|
+
if (
|
|
1106
|
+
!stat ||
|
|
1107
|
+
typeof stat.isFile !== "function" ||
|
|
1108
|
+
!stat.isFile() ||
|
|
1109
|
+
(typeof stat.isSymbolicLink === "function" && stat.isSymbolicLink()) ||
|
|
1110
|
+
(Number.isFinite(Number(stat.nlink)) && Number(stat.nlink) !== 1)
|
|
1111
|
+
) {
|
|
1112
|
+
throw artifactError(
|
|
1113
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1114
|
+
`${label} must be a regular, non-symlink, single-link file`,
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function attestStoreDirectories(layout) {
|
|
1120
|
+
attestCapturedDirectory(
|
|
1121
|
+
layout.rootDir,
|
|
1122
|
+
layout.rootRealPath,
|
|
1123
|
+
layout.rootIdentity,
|
|
1124
|
+
"ArtifactStore root",
|
|
1125
|
+
);
|
|
1126
|
+
attestCapturedDirectory(
|
|
1127
|
+
layout.filesDir,
|
|
1128
|
+
layout.filesRealPath,
|
|
1129
|
+
layout.filesIdentity,
|
|
1130
|
+
"ArtifactStore files root",
|
|
1131
|
+
);
|
|
1132
|
+
if (!isContained(layout.rootRealPath, layout.filesRealPath)) {
|
|
1133
|
+
throw artifactError(
|
|
1134
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1135
|
+
"captured ArtifactStore files root escapes its store root",
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function indexEntriesFingerprint(entries, label) {
|
|
1141
|
+
assertDenseDataArray(entries, label, EVOLUTION_ARTIFACT_MAX_INDEX_ENTRIES);
|
|
1142
|
+
const hash = crypto.createHash("sha256");
|
|
1143
|
+
hash.update(`${entries.length}\0`, "utf8");
|
|
1144
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1145
|
+
const entry = ownData(entries, String(index), label);
|
|
1146
|
+
rejectProxy(entry, `${label}[${index}]`);
|
|
1147
|
+
const canonical = canonicalJson(entry, MAX_INDEX_ENTRY_BYTES);
|
|
1148
|
+
hash.update(`${Buffer.byteLength(canonical, "utf8")}:`, "utf8");
|
|
1149
|
+
hash.update(canonical, "utf8");
|
|
1150
|
+
}
|
|
1151
|
+
return `sha256:${hash.digest("hex")}`;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function parseTrustedIndexBytes(bytes) {
|
|
1155
|
+
if (!Buffer.isBuffer(bytes)) {
|
|
1156
|
+
throw artifactError(
|
|
1157
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1158
|
+
"ArtifactStore index snapshot is not a Buffer",
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
const text = bytes.toString("utf8");
|
|
1162
|
+
if (!Buffer.from(text, "utf8").equals(bytes)) {
|
|
1163
|
+
throw artifactError(
|
|
1164
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1165
|
+
"ArtifactStore index is not exact UTF-8",
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
if (text === "") {
|
|
1169
|
+
return Object.freeze({
|
|
1170
|
+
entries: Object.freeze([]),
|
|
1171
|
+
entriesFingerprint: indexEntriesFingerprint(
|
|
1172
|
+
[],
|
|
1173
|
+
"trusted ArtifactStore index entries",
|
|
1174
|
+
),
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
if (!text.endsWith("\n")) {
|
|
1178
|
+
throw artifactError(
|
|
1179
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1180
|
+
"ArtifactStore index must end at a complete JSONL record",
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const lines = text.slice(0, -1).split("\n");
|
|
1184
|
+
if (
|
|
1185
|
+
lines.length > EVOLUTION_ARTIFACT_MAX_INDEX_ENTRIES ||
|
|
1186
|
+
lines.some(
|
|
1187
|
+
(line) =>
|
|
1188
|
+
line.length < 2 ||
|
|
1189
|
+
Buffer.byteLength(line, "utf8") > MAX_INDEX_ENTRY_BYTES,
|
|
1190
|
+
)
|
|
1191
|
+
) {
|
|
1192
|
+
throw artifactError(
|
|
1193
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1194
|
+
"ArtifactStore index exceeds its trusted snapshot limits",
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const entries = [];
|
|
1198
|
+
for (const [index, line] of lines.entries()) {
|
|
1199
|
+
let entry;
|
|
1200
|
+
try {
|
|
1201
|
+
entry = JSON.parse(line);
|
|
1202
|
+
} catch (cause) {
|
|
1203
|
+
throw artifactError(
|
|
1204
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1205
|
+
`ArtifactStore index row ${index} is not JSON`,
|
|
1206
|
+
{ cause },
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
if (
|
|
1210
|
+
!isPlainRecord(
|
|
1211
|
+
entry,
|
|
1212
|
+
`ArtifactStore index row ${index}`,
|
|
1213
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1214
|
+
) ||
|
|
1215
|
+
JSON.stringify(entry) !== line
|
|
1216
|
+
) {
|
|
1217
|
+
throw artifactError(
|
|
1218
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1219
|
+
`ArtifactStore index row ${index} is not exact single-record JSON`,
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
const id = ownData(entry, "id", `ArtifactStore index row ${index}`);
|
|
1223
|
+
if (typeof id !== "string" || id.length < 1 || id.length > 256) {
|
|
1224
|
+
throw artifactError(
|
|
1225
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1226
|
+
`ArtifactStore index row ${index} has an invalid id`,
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
entries.push(entry);
|
|
1230
|
+
}
|
|
1231
|
+
return Object.freeze({
|
|
1232
|
+
entries: Object.freeze(entries),
|
|
1233
|
+
entriesFingerprint: indexEntriesFingerprint(
|
|
1234
|
+
entries,
|
|
1235
|
+
"trusted ArtifactStore index entries",
|
|
1236
|
+
),
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function readTrustedIndexSnapshot(layout) {
|
|
1241
|
+
attestStoreDirectories(layout);
|
|
1242
|
+
let beforePathStat;
|
|
1243
|
+
let beforeDescriptorStat;
|
|
1244
|
+
let afterDescriptorStat;
|
|
1245
|
+
let descriptor = null;
|
|
1246
|
+
let bytes;
|
|
1247
|
+
let indexRealPath;
|
|
1248
|
+
try {
|
|
1249
|
+
beforePathStat = fs.lstatSync(layout.indexPath);
|
|
1250
|
+
assertRegularSingleLink(beforePathStat, "ArtifactStore index");
|
|
1251
|
+
if (!samePhysicalIdentity(beforePathStat, layout.indexIdentity)) {
|
|
1252
|
+
throw artifactError(
|
|
1253
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1254
|
+
"ArtifactStore index physical identity changed",
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
indexRealPath = fs.realpathSync(layout.indexPath);
|
|
1258
|
+
if (
|
|
1259
|
+
!samePath(indexRealPath, layout.indexRealPath) ||
|
|
1260
|
+
!samePath(indexRealPath, layout.indexPath) ||
|
|
1261
|
+
!isContained(layout.rootRealPath, indexRealPath)
|
|
1262
|
+
) {
|
|
1263
|
+
throw artifactError(
|
|
1264
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1265
|
+
"ArtifactStore index realpath changed or escaped its root",
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
descriptor = fs.openSync(
|
|
1269
|
+
layout.indexPath,
|
|
1270
|
+
fs.constants.O_RDONLY | Number(fs.constants.O_NOFOLLOW || 0),
|
|
1271
|
+
);
|
|
1272
|
+
beforeDescriptorStat = fs.fstatSync(descriptor);
|
|
1273
|
+
assertRegularSingleLink(
|
|
1274
|
+
beforeDescriptorStat,
|
|
1275
|
+
"ArtifactStore index descriptor",
|
|
1276
|
+
);
|
|
1277
|
+
if (
|
|
1278
|
+
!sameFileIdentity(beforePathStat, beforeDescriptorStat) ||
|
|
1279
|
+
beforeDescriptorStat.size > EVOLUTION_ARTIFACT_MAX_INDEX_BYTES
|
|
1280
|
+
) {
|
|
1281
|
+
throw artifactError(
|
|
1282
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1283
|
+
"ArtifactStore index descriptor is replaced or oversized",
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
bytes = fs.readFileSync(descriptor);
|
|
1287
|
+
afterDescriptorStat = fs.fstatSync(descriptor);
|
|
1288
|
+
if (
|
|
1289
|
+
!sameFileIdentity(beforeDescriptorStat, afterDescriptorStat) ||
|
|
1290
|
+
bytes.length !== beforeDescriptorStat.size
|
|
1291
|
+
) {
|
|
1292
|
+
throw artifactError(
|
|
1293
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1294
|
+
"ArtifactStore index changed while its descriptor was read",
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
} catch (cause) {
|
|
1298
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
1299
|
+
throw artifactError(
|
|
1300
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1301
|
+
"ArtifactStore index could not be read from a trusted descriptor",
|
|
1302
|
+
{ cause },
|
|
1303
|
+
);
|
|
1304
|
+
} finally {
|
|
1305
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
1306
|
+
}
|
|
1307
|
+
let afterPathStat;
|
|
1308
|
+
try {
|
|
1309
|
+
afterPathStat = fs.lstatSync(layout.indexPath);
|
|
1310
|
+
assertRegularSingleLink(afterPathStat, "ArtifactStore index");
|
|
1311
|
+
if (
|
|
1312
|
+
!sameFileIdentity(beforePathStat, afterPathStat) ||
|
|
1313
|
+
!samePhysicalIdentity(afterPathStat, layout.indexIdentity) ||
|
|
1314
|
+
!samePath(fs.realpathSync(layout.indexPath), indexRealPath)
|
|
1315
|
+
) {
|
|
1316
|
+
throw artifactError(
|
|
1317
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1318
|
+
"ArtifactStore index pathname changed during descriptor read",
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
} catch (cause) {
|
|
1322
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
1323
|
+
throw artifactError(
|
|
1324
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1325
|
+
"ArtifactStore index could not be re-attested after descriptor read",
|
|
1326
|
+
{ cause },
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
attestStoreDirectories(layout);
|
|
1330
|
+
const parsed = parseTrustedIndexBytes(bytes);
|
|
1331
|
+
return Object.freeze({
|
|
1332
|
+
bytesDigest: sha256(bytes),
|
|
1333
|
+
entries: parsed.entries,
|
|
1334
|
+
entriesFingerprint: parsed.entriesFingerprint,
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function validateIntegrityResult(value, expectedHex) {
|
|
1339
|
+
assertExactRecord(
|
|
1340
|
+
value,
|
|
1341
|
+
INTEGRITY_RESULT_KEYS,
|
|
1342
|
+
"artifact integrity result",
|
|
1343
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1344
|
+
);
|
|
1345
|
+
if (
|
|
1346
|
+
ownData(value, "ok", "artifact integrity result") !== true ||
|
|
1347
|
+
ownData(value, "reason", "artifact integrity result") !== "ok" ||
|
|
1348
|
+
ownData(value, "expectedSha256", "artifact integrity result") !==
|
|
1349
|
+
expectedHex ||
|
|
1350
|
+
ownData(value, "actualSha256", "artifact integrity result") !== expectedHex
|
|
1351
|
+
) {
|
|
1352
|
+
throw artifactError(
|
|
1353
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1354
|
+
"ArtifactStore integrity verification failed",
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function parseCanonicalEnvelope(envelope) {
|
|
1360
|
+
if (
|
|
1361
|
+
typeof envelope !== "string" ||
|
|
1362
|
+
Buffer.byteLength(envelope, "utf8") >
|
|
1363
|
+
EVOLUTION_ARTIFACT_MAX_ENVELOPE_BYTES ||
|
|
1364
|
+
envelope.length < 2
|
|
1365
|
+
) {
|
|
1366
|
+
throw artifactError(
|
|
1367
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1368
|
+
"evolution artifact envelope is missing or oversized",
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
let parsed;
|
|
1372
|
+
try {
|
|
1373
|
+
parsed = JSON.parse(envelope);
|
|
1374
|
+
} catch (cause) {
|
|
1375
|
+
throw artifactError(
|
|
1376
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1377
|
+
"evolution artifact envelope is not JSON",
|
|
1378
|
+
{ cause },
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
let canonical;
|
|
1382
|
+
try {
|
|
1383
|
+
canonical = canonicalJson(parsed, EVOLUTION_ARTIFACT_MAX_ENVELOPE_BYTES);
|
|
1384
|
+
} catch (cause) {
|
|
1385
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
1386
|
+
throw artifactError(
|
|
1387
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1388
|
+
"evolution artifact envelope is not canonical JSON",
|
|
1389
|
+
{ cause },
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
if (canonical !== envelope) {
|
|
1393
|
+
throw artifactError(
|
|
1394
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1395
|
+
"evolution artifact envelope must use exact canonical JSON bytes",
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
assertExactRecord(parsed, ENVELOPE_KEYS, "artifact envelope");
|
|
1399
|
+
if (
|
|
1400
|
+
ownData(parsed, "schema", "artifact envelope") !==
|
|
1401
|
+
EVOLUTION_ARTIFACT_ENVELOPE_SCHEMA
|
|
1402
|
+
) {
|
|
1403
|
+
throw artifactError(
|
|
1404
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1405
|
+
"evolution artifact envelope schema is unsupported",
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
const core = ownData(parsed, "core", "artifact envelope");
|
|
1409
|
+
const signature = ownData(parsed, "signature", "artifact envelope");
|
|
1410
|
+
assertExactRecord(core, ENVELOPE_CORE_KEYS, "artifact envelope core");
|
|
1411
|
+
assertExactRecord(signature, SIGNATURE_KEYS, "artifact envelope signature");
|
|
1412
|
+
return {
|
|
1413
|
+
parsed,
|
|
1414
|
+
core,
|
|
1415
|
+
signature,
|
|
1416
|
+
envelopeDigest: sha256(Buffer.from(envelope, "utf8")),
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function validateEnvelopeCore(core, signature, allowedTypes) {
|
|
1421
|
+
if (
|
|
1422
|
+
ownData(core, "schema", "artifact envelope core") !==
|
|
1423
|
+
EVOLUTION_ARTIFACT_ENVELOPE_CORE_SCHEMA
|
|
1424
|
+
) {
|
|
1425
|
+
throw artifactError(
|
|
1426
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1427
|
+
"evolution artifact envelope core schema is unsupported",
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
const retention = normalizeRetention(
|
|
1431
|
+
ownData(core, "retention", "artifact envelope core"),
|
|
1432
|
+
"artifact envelope retention",
|
|
1433
|
+
);
|
|
1434
|
+
const rawExpiresAt = ownData(core, "expiresAt", "artifact envelope core");
|
|
1435
|
+
if (retention === "ledger" && rawExpiresAt !== null) {
|
|
1436
|
+
throw artifactError(
|
|
1437
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1438
|
+
"ledger-retained artifact envelopes must not carry an expiresAt authority",
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
const normalized = {
|
|
1442
|
+
algorithm: normalizeAlgorithm(
|
|
1443
|
+
ownData(core, "algorithm", "artifact envelope core"),
|
|
1444
|
+
"artifact envelope algorithm",
|
|
1445
|
+
),
|
|
1446
|
+
audience: normalizeSafeId(
|
|
1447
|
+
ownData(core, "audience", "artifact envelope core"),
|
|
1448
|
+
"artifact envelope audience",
|
|
1449
|
+
),
|
|
1450
|
+
authorityReceiptDigest: normalizeDigest(
|
|
1451
|
+
ownData(core, "authorityReceiptDigest", "artifact envelope core"),
|
|
1452
|
+
"artifact envelope authorityReceiptDigest",
|
|
1453
|
+
),
|
|
1454
|
+
digest: normalizeDigest(
|
|
1455
|
+
ownData(core, "digest", "artifact envelope core"),
|
|
1456
|
+
"artifact envelope digest",
|
|
1457
|
+
),
|
|
1458
|
+
expiresAt:
|
|
1459
|
+
retention === "ledger"
|
|
1460
|
+
? null
|
|
1461
|
+
: normalizeTimestamp(rawExpiresAt, "artifact envelope expiresAt"),
|
|
1462
|
+
issuedAt: normalizeTimestamp(
|
|
1463
|
+
ownData(core, "issuedAt", "artifact envelope core"),
|
|
1464
|
+
"artifact envelope issuedAt",
|
|
1465
|
+
),
|
|
1466
|
+
keyId: normalizeKeyId(
|
|
1467
|
+
ownData(core, "keyId", "artifact envelope core"),
|
|
1468
|
+
"artifact envelope keyId",
|
|
1469
|
+
),
|
|
1470
|
+
policyDigest: normalizeDigest(
|
|
1471
|
+
ownData(core, "policyDigest", "artifact envelope core"),
|
|
1472
|
+
"artifact envelope policyDigest",
|
|
1473
|
+
),
|
|
1474
|
+
policyRevision: normalizeRevision(
|
|
1475
|
+
ownData(core, "policyRevision", "artifact envelope core"),
|
|
1476
|
+
"artifact envelope policyRevision",
|
|
1477
|
+
),
|
|
1478
|
+
purpose: normalizeSafeId(
|
|
1479
|
+
ownData(core, "purpose", "artifact envelope core"),
|
|
1480
|
+
"artifact envelope purpose",
|
|
1481
|
+
),
|
|
1482
|
+
recordDigest: normalizeDigest(
|
|
1483
|
+
ownData(core, "recordDigest", "artifact envelope core"),
|
|
1484
|
+
"artifact envelope recordDigest",
|
|
1485
|
+
),
|
|
1486
|
+
retention,
|
|
1487
|
+
revocationRevision: normalizeRevision(
|
|
1488
|
+
ownData(core, "revocationRevision", "artifact envelope core"),
|
|
1489
|
+
"artifact envelope revocationRevision",
|
|
1490
|
+
),
|
|
1491
|
+
schema: EVOLUTION_ARTIFACT_ENVELOPE_CORE_SCHEMA,
|
|
1492
|
+
tenantId: normalizeSafeId(
|
|
1493
|
+
ownData(core, "tenantId", "artifact envelope core"),
|
|
1494
|
+
"artifact envelope tenantId",
|
|
1495
|
+
),
|
|
1496
|
+
type: normalizeType(
|
|
1497
|
+
ownData(core, "type", "artifact envelope core"),
|
|
1498
|
+
allowedTypes,
|
|
1499
|
+
),
|
|
1500
|
+
};
|
|
1501
|
+
if (normalized.digest !== normalized.recordDigest) {
|
|
1502
|
+
throw artifactError(
|
|
1503
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1504
|
+
"artifact envelope digest and recordDigest must match exactly",
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
if (retention === "ledger") {
|
|
1508
|
+
assertLedgerRetentionAdmission(normalized.type, normalized.purpose);
|
|
1509
|
+
}
|
|
1510
|
+
const issuedAtMs = new Date(normalized.issuedAt).getTime();
|
|
1511
|
+
const expiresAtMs =
|
|
1512
|
+
normalized.expiresAt === null
|
|
1513
|
+
? null
|
|
1514
|
+
: new Date(normalized.expiresAt).getTime();
|
|
1515
|
+
if (
|
|
1516
|
+
retention === "ttl" &&
|
|
1517
|
+
(expiresAtMs <= issuedAtMs ||
|
|
1518
|
+
expiresAtMs - issuedAtMs > EVOLUTION_ARTIFACT_MAX_TTL_MS)
|
|
1519
|
+
) {
|
|
1520
|
+
throw artifactError(
|
|
1521
|
+
EVOLUTION_ARTIFACT_EXPIRED_CODE,
|
|
1522
|
+
"artifact envelope TTL is invalid or exceeds the maximum",
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
const signatureAlgorithm = normalizeAlgorithm(
|
|
1526
|
+
ownData(signature, "algorithm", "artifact envelope signature"),
|
|
1527
|
+
"artifact envelope signature algorithm",
|
|
1528
|
+
);
|
|
1529
|
+
const signatureKeyId = normalizeKeyId(
|
|
1530
|
+
ownData(signature, "keyId", "artifact envelope signature"),
|
|
1531
|
+
"artifact envelope signature keyId",
|
|
1532
|
+
);
|
|
1533
|
+
const signatureValue = ownData(
|
|
1534
|
+
signature,
|
|
1535
|
+
"value",
|
|
1536
|
+
"artifact envelope signature",
|
|
1537
|
+
);
|
|
1538
|
+
if (
|
|
1539
|
+
signatureAlgorithm !== normalized.algorithm ||
|
|
1540
|
+
signatureKeyId !== normalized.keyId ||
|
|
1541
|
+
typeof signatureValue !== "string" ||
|
|
1542
|
+
!SIGNATURE_VALUE_PATTERN.test(signatureValue)
|
|
1543
|
+
) {
|
|
1544
|
+
throw artifactError(
|
|
1545
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
1546
|
+
"artifact envelope signature header is not bound to its core",
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
return deepFreeze({
|
|
1550
|
+
...normalized,
|
|
1551
|
+
issuedAtMs,
|
|
1552
|
+
expiresAtMs,
|
|
1553
|
+
signature: {
|
|
1554
|
+
algorithm: signatureAlgorithm,
|
|
1555
|
+
keyId: signatureKeyId,
|
|
1556
|
+
value: signatureValue,
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function normalizePutContext(value, audience, type) {
|
|
1562
|
+
const context = value === undefined ? {} : value;
|
|
1563
|
+
assertOptionalExactRecord(
|
|
1564
|
+
context,
|
|
1565
|
+
PUT_CONTEXT_KEYS,
|
|
1566
|
+
new Set(["purpose"]),
|
|
1567
|
+
"artifact put context",
|
|
1568
|
+
);
|
|
1569
|
+
const suppliedAudience = Object.hasOwn(context, "audience")
|
|
1570
|
+
? ownData(context, "audience", "artifact put context")
|
|
1571
|
+
: audience;
|
|
1572
|
+
if (normalizeSafeId(suppliedAudience, "artifact audience") !== audience) {
|
|
1573
|
+
throw artifactError(
|
|
1574
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
1575
|
+
"artifact audience does not match the captured audience",
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
const purpose = normalizeSafeId(
|
|
1579
|
+
ownData(context, "purpose", "artifact put context"),
|
|
1580
|
+
"artifact purpose",
|
|
1581
|
+
);
|
|
1582
|
+
const retention = Object.hasOwn(context, "retention")
|
|
1583
|
+
? normalizeRetention(
|
|
1584
|
+
ownData(context, "retention", "artifact put context"),
|
|
1585
|
+
"artifact retention",
|
|
1586
|
+
)
|
|
1587
|
+
: "ttl";
|
|
1588
|
+
const hasTtl = Object.hasOwn(context, "ttlMs");
|
|
1589
|
+
if (retention === "ledger") {
|
|
1590
|
+
if (hasTtl) {
|
|
1591
|
+
throw artifactError(
|
|
1592
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1593
|
+
"ledger retention must not be combined with caller-controlled ttlMs",
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
assertLedgerRetentionAdmission(type, purpose);
|
|
1597
|
+
return Object.freeze({ audience, purpose, retention, ttlMs: null });
|
|
1598
|
+
}
|
|
1599
|
+
const ttlMs = hasTtl
|
|
1600
|
+
? ownData(context, "ttlMs", "artifact put context")
|
|
1601
|
+
: EVOLUTION_ARTIFACT_DEFAULT_TTL_MS;
|
|
1602
|
+
if (
|
|
1603
|
+
!Number.isSafeInteger(ttlMs) ||
|
|
1604
|
+
ttlMs < 1 ||
|
|
1605
|
+
ttlMs > EVOLUTION_ARTIFACT_MAX_TTL_MS
|
|
1606
|
+
) {
|
|
1607
|
+
throw artifactError(
|
|
1608
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1609
|
+
`artifact ttlMs must be between 1 and ${EVOLUTION_ARTIFACT_MAX_TTL_MS}`,
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
return Object.freeze({ audience, purpose, retention, ttlMs });
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function normalizeResolveOptions(value, tenantId, audience, allowedTypes) {
|
|
1616
|
+
assertExactRecord(value, RESOLVE_OPTION_KEYS, "artifact resolve options");
|
|
1617
|
+
const requestedTenant = normalizeSafeId(
|
|
1618
|
+
ownData(value, "tenantId", "artifact resolve options"),
|
|
1619
|
+
"artifact resolve tenantId",
|
|
1620
|
+
);
|
|
1621
|
+
if (requestedTenant !== tenantId) {
|
|
1622
|
+
throw artifactError(
|
|
1623
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
1624
|
+
"cross-tenant artifact resolution is denied",
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
return Object.freeze({
|
|
1628
|
+
audience,
|
|
1629
|
+
expectedDigest: normalizeDigest(
|
|
1630
|
+
ownData(value, "expectedDigest", "artifact resolve options"),
|
|
1631
|
+
"artifact expectedDigest",
|
|
1632
|
+
),
|
|
1633
|
+
expectedType: normalizeType(
|
|
1634
|
+
ownData(value, "expectedType", "artifact resolve options"),
|
|
1635
|
+
allowedTypes,
|
|
1636
|
+
),
|
|
1637
|
+
purpose: normalizeSafeId(
|
|
1638
|
+
ownData(value, "purpose", "artifact resolve options"),
|
|
1639
|
+
"artifact resolve purpose",
|
|
1640
|
+
),
|
|
1641
|
+
tenantId,
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function normalizeSignature(value, authority) {
|
|
1646
|
+
assertExactRecord(
|
|
1647
|
+
value,
|
|
1648
|
+
SIGNATURE_KEYS,
|
|
1649
|
+
"artifact signer response",
|
|
1650
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
1651
|
+
);
|
|
1652
|
+
const signature = {
|
|
1653
|
+
algorithm: normalizeAlgorithm(
|
|
1654
|
+
ownData(value, "algorithm", "artifact signer response"),
|
|
1655
|
+
"artifact signer algorithm",
|
|
1656
|
+
),
|
|
1657
|
+
keyId: normalizeKeyId(
|
|
1658
|
+
ownData(value, "keyId", "artifact signer response"),
|
|
1659
|
+
"artifact signer keyId",
|
|
1660
|
+
),
|
|
1661
|
+
value: ownData(value, "value", "artifact signer response"),
|
|
1662
|
+
};
|
|
1663
|
+
if (
|
|
1664
|
+
signature.algorithm !== authority.algorithm ||
|
|
1665
|
+
signature.keyId !== authority.keyId ||
|
|
1666
|
+
typeof signature.value !== "string" ||
|
|
1667
|
+
!SIGNATURE_VALUE_PATTERN.test(signature.value)
|
|
1668
|
+
) {
|
|
1669
|
+
throw artifactError(
|
|
1670
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
1671
|
+
"artifact signer response is not bound to current authority",
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
return deepFreeze(signature);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function normalizeArtifactRef(value) {
|
|
1678
|
+
assertExactRecord(value, ARTIFACT_REF_KEYS, "evolution artifact ref");
|
|
1679
|
+
if (
|
|
1680
|
+
ownData(value, "schema", "evolution artifact ref") !==
|
|
1681
|
+
EVOLUTION_ARTIFACT_REF_SCHEMA
|
|
1682
|
+
) {
|
|
1683
|
+
throw artifactError(
|
|
1684
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1685
|
+
"evolution artifact ref schema is unsupported",
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
const digest = normalizeDigest(
|
|
1689
|
+
ownData(value, "digest", "evolution artifact ref"),
|
|
1690
|
+
"evolution artifact ref digest",
|
|
1691
|
+
);
|
|
1692
|
+
const ref = ownData(value, "ref", "evolution artifact ref");
|
|
1693
|
+
if (
|
|
1694
|
+
typeof ref !== "string" ||
|
|
1695
|
+
ref.length > 2048 ||
|
|
1696
|
+
!ref.startsWith(ARTIFACT_REF_PREFIX)
|
|
1697
|
+
) {
|
|
1698
|
+
throw artifactError(
|
|
1699
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1700
|
+
"evolution artifact ref is not a supported short locator",
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const artifactId = ref.slice(ARTIFACT_REF_PREFIX.length);
|
|
1704
|
+
if (!ARTIFACT_ID_PATTERN.test(artifactId)) {
|
|
1705
|
+
throw artifactError(
|
|
1706
|
+
EVOLUTION_ARTIFACT_INVALID_CODE,
|
|
1707
|
+
"evolution artifact ref contains an invalid ArtifactStore id",
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
return Object.freeze({ digest, ref, artifactId });
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function buildArtifactRef(artifactId, digest) {
|
|
1714
|
+
return deepFreeze({
|
|
1715
|
+
digest,
|
|
1716
|
+
ref: `${ARTIFACT_REF_PREFIX}${artifactId}`,
|
|
1717
|
+
schema: EVOLUTION_ARTIFACT_REF_SCHEMA,
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function validateAllowedTypes(value) {
|
|
1722
|
+
assertDenseDataArray(value, "allowedTypes", EVOLUTION_ARTIFACT_TYPES.length);
|
|
1723
|
+
if (value.length < 1) {
|
|
1724
|
+
throw new TypeError(
|
|
1725
|
+
"EvolutionArtifactPorts allowedTypes must not be empty",
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
const productTypes = new Set(EVOLUTION_ARTIFACT_TYPES);
|
|
1729
|
+
const normalized = [];
|
|
1730
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1731
|
+
const type = ownData(value, String(index), "allowedTypes");
|
|
1732
|
+
if (
|
|
1733
|
+
typeof type !== "string" ||
|
|
1734
|
+
!TYPE_PATTERN.test(type) ||
|
|
1735
|
+
!productTypes.has(type)
|
|
1736
|
+
) {
|
|
1737
|
+
throw new TypeError(
|
|
1738
|
+
"EvolutionArtifactPorts allowedTypes may only narrow the product allow-list",
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
normalized.push(type);
|
|
1742
|
+
}
|
|
1743
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
1744
|
+
throw new TypeError(
|
|
1745
|
+
"EvolutionArtifactPorts allowedTypes contains duplicates",
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
return new Set(normalized);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
export class EvolutionArtifactPorts {
|
|
1752
|
+
#tenantId;
|
|
1753
|
+
#audience;
|
|
1754
|
+
#allowedTypes;
|
|
1755
|
+
#store;
|
|
1756
|
+
#signEnvelope;
|
|
1757
|
+
#verifyEnvelope;
|
|
1758
|
+
#resolveCurrentAuthority;
|
|
1759
|
+
#now;
|
|
1760
|
+
|
|
1761
|
+
constructor(options = {}) {
|
|
1762
|
+
assertOptionalExactRecord(
|
|
1763
|
+
options,
|
|
1764
|
+
CONSTRUCTOR_OPTION_KEYS,
|
|
1765
|
+
CONSTRUCTOR_REQUIRED_KEYS,
|
|
1766
|
+
"EvolutionArtifactPorts constructor options",
|
|
1767
|
+
);
|
|
1768
|
+
const tenantId = ownData(
|
|
1769
|
+
options,
|
|
1770
|
+
"tenantId",
|
|
1771
|
+
"EvolutionArtifactPorts constructor options",
|
|
1772
|
+
);
|
|
1773
|
+
const audience = ownData(
|
|
1774
|
+
options,
|
|
1775
|
+
"audience",
|
|
1776
|
+
"EvolutionArtifactPorts constructor options",
|
|
1777
|
+
);
|
|
1778
|
+
const artifactStore = ownData(
|
|
1779
|
+
options,
|
|
1780
|
+
"artifactStore",
|
|
1781
|
+
"EvolutionArtifactPorts constructor options",
|
|
1782
|
+
);
|
|
1783
|
+
const envelopeSigner = ownData(
|
|
1784
|
+
options,
|
|
1785
|
+
"envelopeSigner",
|
|
1786
|
+
"EvolutionArtifactPorts constructor options",
|
|
1787
|
+
);
|
|
1788
|
+
const envelopeVerifier = ownData(
|
|
1789
|
+
options,
|
|
1790
|
+
"envelopeVerifier",
|
|
1791
|
+
"EvolutionArtifactPorts constructor options",
|
|
1792
|
+
);
|
|
1793
|
+
const currentAuthorityResolver = ownData(
|
|
1794
|
+
options,
|
|
1795
|
+
"currentAuthorityResolver",
|
|
1796
|
+
"EvolutionArtifactPorts constructor options",
|
|
1797
|
+
);
|
|
1798
|
+
const allowedTypes = Object.hasOwn(options, "allowedTypes")
|
|
1799
|
+
? ownData(
|
|
1800
|
+
options,
|
|
1801
|
+
"allowedTypes",
|
|
1802
|
+
"EvolutionArtifactPorts constructor options",
|
|
1803
|
+
)
|
|
1804
|
+
: EVOLUTION_ARTIFACT_TYPES;
|
|
1805
|
+
const now = Object.hasOwn(options, "now")
|
|
1806
|
+
? ownData(options, "now", "EvolutionArtifactPorts constructor options")
|
|
1807
|
+
: Date.now;
|
|
1808
|
+
this.#tenantId = normalizeSafeId(tenantId, "tenantId");
|
|
1809
|
+
this.#audience = normalizeSafeId(audience, "audience");
|
|
1810
|
+
this.#allowedTypes = validateAllowedTypes(allowedTypes);
|
|
1811
|
+
this.#store = captureArtifactStore(artifactStore);
|
|
1812
|
+
this.#signEnvelope = captureMethod(
|
|
1813
|
+
envelopeSigner,
|
|
1814
|
+
"sign",
|
|
1815
|
+
"envelopeSigner",
|
|
1816
|
+
);
|
|
1817
|
+
this.#verifyEnvelope = captureMethod(
|
|
1818
|
+
envelopeVerifier,
|
|
1819
|
+
"verify",
|
|
1820
|
+
"envelopeVerifier",
|
|
1821
|
+
);
|
|
1822
|
+
this.#resolveCurrentAuthority = captureMethod(
|
|
1823
|
+
currentAuthorityResolver,
|
|
1824
|
+
"resolve",
|
|
1825
|
+
"currentAuthorityResolver",
|
|
1826
|
+
);
|
|
1827
|
+
rejectProxy(now, "EvolutionArtifactPorts clock");
|
|
1828
|
+
if (typeof now !== "function") {
|
|
1829
|
+
throw new TypeError("EvolutionArtifactPorts now must be a function");
|
|
1830
|
+
}
|
|
1831
|
+
this.#now = Object.freeze((...args) => now(...args));
|
|
1832
|
+
Object.freeze(this);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
#clock() {
|
|
1836
|
+
return normalizeClockValue(this.#now());
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
#boundedIndexEntries() {
|
|
1840
|
+
const trustedBefore = readTrustedIndexSnapshot(this.#store.layout);
|
|
1841
|
+
let entries;
|
|
1842
|
+
try {
|
|
1843
|
+
entries = rejectPromise(
|
|
1844
|
+
this.#store.list(),
|
|
1845
|
+
"ArtifactStore.list",
|
|
1846
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1847
|
+
);
|
|
1848
|
+
} catch (cause) {
|
|
1849
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
1850
|
+
throw artifactError(
|
|
1851
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1852
|
+
"ArtifactStore index could not be read",
|
|
1853
|
+
{ cause },
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
rejectProxy(
|
|
1857
|
+
entries,
|
|
1858
|
+
"ArtifactStore.list result",
|
|
1859
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1860
|
+
);
|
|
1861
|
+
if (!Array.isArray(entries)) {
|
|
1862
|
+
throw artifactError(
|
|
1863
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1864
|
+
"ArtifactStore.list did not return an array",
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
// ArtifactStore currently materializes its whole JSONL index before this
|
|
1868
|
+
// adapter can enforce a limit. Reject immediately after return; this is an
|
|
1869
|
+
// explicit scalability boundary, not a claim of bounded store-side query.
|
|
1870
|
+
if (entries.length > EVOLUTION_ARTIFACT_MAX_INDEX_ENTRIES) {
|
|
1871
|
+
throw artifactError(
|
|
1872
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1873
|
+
`ArtifactStore index exceeds the adapter limit of ${EVOLUTION_ARTIFACT_MAX_INDEX_ENTRIES}`,
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
assertDenseDataArray(
|
|
1877
|
+
entries,
|
|
1878
|
+
"ArtifactStore index entries",
|
|
1879
|
+
EVOLUTION_ARTIFACT_MAX_INDEX_ENTRIES,
|
|
1880
|
+
);
|
|
1881
|
+
let storeFingerprint;
|
|
1882
|
+
try {
|
|
1883
|
+
storeFingerprint = indexEntriesFingerprint(
|
|
1884
|
+
entries,
|
|
1885
|
+
"ArtifactStore.list entries",
|
|
1886
|
+
);
|
|
1887
|
+
} catch (cause) {
|
|
1888
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
1889
|
+
throw artifactError(
|
|
1890
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1891
|
+
"ArtifactStore.list entries could not be canonicalized",
|
|
1892
|
+
{ cause },
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
const trustedAfter = readTrustedIndexSnapshot(this.#store.layout);
|
|
1896
|
+
if (
|
|
1897
|
+
trustedBefore.bytesDigest !== trustedAfter.bytesDigest ||
|
|
1898
|
+
trustedBefore.entriesFingerprint !== trustedAfter.entriesFingerprint ||
|
|
1899
|
+
storeFingerprint !== trustedAfter.entriesFingerprint
|
|
1900
|
+
) {
|
|
1901
|
+
throw artifactError(
|
|
1902
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1903
|
+
"ArtifactStore.list is not exactly bound to one trusted index snapshot",
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
return entries;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
#entriesByDataField(entries, field, expected) {
|
|
1910
|
+
const matches = [];
|
|
1911
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1912
|
+
const entry = ownData(
|
|
1913
|
+
entries,
|
|
1914
|
+
String(index),
|
|
1915
|
+
"ArtifactStore index entries",
|
|
1916
|
+
);
|
|
1917
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1918
|
+
rejectProxy(
|
|
1919
|
+
entry,
|
|
1920
|
+
`ArtifactStore index entry ${index}`,
|
|
1921
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1922
|
+
);
|
|
1923
|
+
if (Array.isArray(entry)) continue;
|
|
1924
|
+
const descriptor = Object.getOwnPropertyDescriptor(entry, field);
|
|
1925
|
+
if (!descriptor) continue;
|
|
1926
|
+
if (!("value" in descriptor) || descriptor.enumerable !== true) {
|
|
1927
|
+
throw artifactError(
|
|
1928
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1929
|
+
`ArtifactStore index ${field} must be an enumerable data property`,
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
if (descriptor.value === expected) matches.push(entry);
|
|
1933
|
+
}
|
|
1934
|
+
return matches;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
#loadCurrentEntryById(artifactId) {
|
|
1938
|
+
const listed = this.#entriesByDataField(
|
|
1939
|
+
this.#boundedIndexEntries(),
|
|
1940
|
+
"id",
|
|
1941
|
+
artifactId,
|
|
1942
|
+
);
|
|
1943
|
+
if (listed.length === 0) {
|
|
1944
|
+
throw artifactError(
|
|
1945
|
+
EVOLUTION_ARTIFACT_NOT_FOUND_CODE,
|
|
1946
|
+
"evolution artifact index entry was removed or expired",
|
|
1947
|
+
{ artifactId },
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
if (listed.length !== 1) {
|
|
1951
|
+
throw artifactError(
|
|
1952
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1953
|
+
"evolution artifact id has duplicate index rows",
|
|
1954
|
+
{ artifactId },
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
let current;
|
|
1958
|
+
try {
|
|
1959
|
+
current = rejectPromise(
|
|
1960
|
+
this.#store.get(artifactId),
|
|
1961
|
+
"ArtifactStore.get",
|
|
1962
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1963
|
+
);
|
|
1964
|
+
} catch (cause) {
|
|
1965
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
1966
|
+
throw artifactError(
|
|
1967
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
1968
|
+
"ArtifactStore entry could not be re-read",
|
|
1969
|
+
{ cause, artifactId },
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
if (!current) {
|
|
1973
|
+
throw artifactError(
|
|
1974
|
+
EVOLUTION_ARTIFACT_NOT_FOUND_CODE,
|
|
1975
|
+
"evolution artifact index entry disappeared during readback",
|
|
1976
|
+
{ artifactId },
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
const listedCanonical = canonicalJson(listed[0]);
|
|
1980
|
+
const currentCanonical = canonicalJson(current);
|
|
1981
|
+
if (listedCanonical !== currentCanonical) {
|
|
1982
|
+
throw artifactError(
|
|
1983
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1984
|
+
"ArtifactStore index generation changed during readback",
|
|
1985
|
+
{ artifactId },
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
const afterGet = this.#entriesByDataField(
|
|
1989
|
+
this.#boundedIndexEntries(),
|
|
1990
|
+
"id",
|
|
1991
|
+
artifactId,
|
|
1992
|
+
);
|
|
1993
|
+
if (
|
|
1994
|
+
afterGet.length !== 1 ||
|
|
1995
|
+
canonicalJson(afterGet[0]) !== currentCanonical
|
|
1996
|
+
) {
|
|
1997
|
+
throw artifactError(
|
|
1998
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
1999
|
+
"ArtifactStore.get is not bound to the trusted index snapshot",
|
|
2000
|
+
{ artifactId },
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
return current;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
#assertEntryStable(artifactId, original) {
|
|
2007
|
+
const current = this.#loadCurrentEntryById(artifactId);
|
|
2008
|
+
if (canonicalJson(current) !== canonicalJson(original)) {
|
|
2009
|
+
throw artifactError(
|
|
2010
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2011
|
+
"ArtifactStore index entry was replaced during readback",
|
|
2012
|
+
{ artifactId },
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
#assertUniqueRecordDigest(recordDigest, artifactId) {
|
|
2018
|
+
const matches = this.#entriesByDataField(
|
|
2019
|
+
this.#boundedIndexEntries(),
|
|
2020
|
+
"recordDigest",
|
|
2021
|
+
recordDigest,
|
|
2022
|
+
);
|
|
2023
|
+
if (matches.length !== 1) {
|
|
2024
|
+
throw artifactError(
|
|
2025
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2026
|
+
"recordDigest must have exactly one ArtifactStore index row",
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
rejectProxy(
|
|
2030
|
+
matches[0],
|
|
2031
|
+
"ArtifactStore recordDigest match",
|
|
2032
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2033
|
+
);
|
|
2034
|
+
const matchedId = Object.getOwnPropertyDescriptor(matches[0], "id");
|
|
2035
|
+
if (
|
|
2036
|
+
!matchedId ||
|
|
2037
|
+
!("value" in matchedId) ||
|
|
2038
|
+
matchedId.enumerable !== true ||
|
|
2039
|
+
matchedId.value !== artifactId
|
|
2040
|
+
) {
|
|
2041
|
+
throw artifactError(
|
|
2042
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2043
|
+
"recordDigest index authority does not match the requested locator",
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
#validateLineage(value, envelope, core, envelopeDigest) {
|
|
2049
|
+
assertExactRecord(
|
|
2050
|
+
value,
|
|
2051
|
+
LINEAGE_KEYS,
|
|
2052
|
+
"artifact lineage",
|
|
2053
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2054
|
+
);
|
|
2055
|
+
const expected = {
|
|
2056
|
+
audience: core.audience,
|
|
2057
|
+
envelope,
|
|
2058
|
+
envelopeDigest,
|
|
2059
|
+
purpose: core.purpose,
|
|
2060
|
+
recordDigest: core.recordDigest,
|
|
2061
|
+
retention: core.retention,
|
|
2062
|
+
schema: EVOLUTION_ARTIFACT_LINEAGE_SCHEMA,
|
|
2063
|
+
tenantId: core.tenantId,
|
|
2064
|
+
type: core.type,
|
|
2065
|
+
};
|
|
2066
|
+
if (canonicalJson(value) !== canonicalJson(expected)) {
|
|
2067
|
+
throw artifactError(
|
|
2068
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2069
|
+
"artifact lineage is not exactly bound to the signed envelope",
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
return deepFreeze(expected);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
#validateIndexEntry(entry, envelope, core, envelopeDigest) {
|
|
2076
|
+
assertExactRecord(
|
|
2077
|
+
entry,
|
|
2078
|
+
INDEX_ENTRY_KEYS,
|
|
2079
|
+
"ArtifactStore index entry",
|
|
2080
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2081
|
+
);
|
|
2082
|
+
const id = ownData(entry, "id", "ArtifactStore index entry");
|
|
2083
|
+
if (!ARTIFACT_ID_PATTERN.test(id || "")) {
|
|
2084
|
+
throw artifactError(
|
|
2085
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2086
|
+
"ArtifactStore id is invalid",
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
const file = ownData(entry, "file", "ArtifactStore index entry");
|
|
2090
|
+
const size = ownData(entry, "size", "ArtifactStore index entry");
|
|
2091
|
+
const createdAt = normalizeTimestamp(
|
|
2092
|
+
ownData(entry, "createdAt", "ArtifactStore index entry"),
|
|
2093
|
+
"ArtifactStore createdAt",
|
|
2094
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2095
|
+
);
|
|
2096
|
+
const expiresAt = normalizeTimestamp(
|
|
2097
|
+
ownData(entry, "expiresAt", "ArtifactStore index entry"),
|
|
2098
|
+
"ArtifactStore expiresAt",
|
|
2099
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2100
|
+
);
|
|
2101
|
+
const createdAtMs = new Date(createdAt).getTime();
|
|
2102
|
+
const storeExpiresAtMs = new Date(expiresAt).getTime();
|
|
2103
|
+
const storeRetentionIsAdmissible =
|
|
2104
|
+
core.retention === "ledger"
|
|
2105
|
+
? storeExpiresAtMs - createdAtMs ===
|
|
2106
|
+
LEDGER_HOUSEKEEPING_TTL_DAYS * MILLISECONDS_PER_DAY
|
|
2107
|
+
: storeExpiresAtMs >= core.expiresAtMs;
|
|
2108
|
+
if (
|
|
2109
|
+
file !== `${id}.json` ||
|
|
2110
|
+
path.basename(file) !== file ||
|
|
2111
|
+
ownData(entry, "title", "ArtifactStore index entry") !==
|
|
2112
|
+
`Evolution ${core.type} artifact` ||
|
|
2113
|
+
ownData(entry, "kind", "ArtifactStore index entry") !== "data" ||
|
|
2114
|
+
ownData(entry, "mime", "ArtifactStore index entry") !==
|
|
2115
|
+
"application/json" ||
|
|
2116
|
+
!Number.isSafeInteger(size) ||
|
|
2117
|
+
size < 1 ||
|
|
2118
|
+
size > EVOLUTION_ARTIFACT_MAX_CANONICAL_BYTES ||
|
|
2119
|
+
ownData(entry, "sha256", "ArtifactStore index entry") !==
|
|
2120
|
+
core.recordDigest.slice("sha256:".length) ||
|
|
2121
|
+
ownData(entry, "sourcePath", "ArtifactStore index entry") !== null ||
|
|
2122
|
+
ownData(entry, "sessionId", "ArtifactStore index entry") !== null ||
|
|
2123
|
+
ownData(entry, "immutable", "ArtifactStore index entry") !== true ||
|
|
2124
|
+
ownData(entry, "recordDigest", "ArtifactStore index entry") !==
|
|
2125
|
+
core.recordDigest ||
|
|
2126
|
+
storeExpiresAtMs <= createdAtMs ||
|
|
2127
|
+
createdAtMs < core.issuedAtMs ||
|
|
2128
|
+
!storeRetentionIsAdmissible
|
|
2129
|
+
) {
|
|
2130
|
+
throw artifactError(
|
|
2131
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2132
|
+
"ArtifactStore index entry is not exactly admissible for this envelope",
|
|
2133
|
+
{ artifactId: id },
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
const lineage = this.#validateLineage(
|
|
2137
|
+
ownData(entry, "lineage", "ArtifactStore index entry"),
|
|
2138
|
+
envelope,
|
|
2139
|
+
core,
|
|
2140
|
+
envelopeDigest,
|
|
2141
|
+
);
|
|
2142
|
+
return Object.freeze({ createdAt, expiresAt, file, id, lineage, size });
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
#readStoredBytes(entry, normalizedEntry, expectedDigest) {
|
|
2146
|
+
attestStoreDirectories(this.#store.layout);
|
|
2147
|
+
const expectedHex = expectedDigest.slice("sha256:".length);
|
|
2148
|
+
let storedPath;
|
|
2149
|
+
try {
|
|
2150
|
+
storedPath = this.#store.storedPath(entry);
|
|
2151
|
+
} catch (cause) {
|
|
2152
|
+
throw artifactError(
|
|
2153
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2154
|
+
"ArtifactStore stored path could not be resolved",
|
|
2155
|
+
{ cause },
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
if (typeof storedPath !== "string") {
|
|
2159
|
+
throw artifactError(
|
|
2160
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2161
|
+
"ArtifactStore stored path is unavailable",
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
const filesRoot = this.#store.layout.filesDir;
|
|
2165
|
+
const expectedPath = path.resolve(filesRoot, normalizedEntry.file);
|
|
2166
|
+
const resolvedStoredPath = path.resolve(storedPath);
|
|
2167
|
+
if (
|
|
2168
|
+
!samePath(expectedPath, resolvedStoredPath) ||
|
|
2169
|
+
!isContained(filesRoot, resolvedStoredPath)
|
|
2170
|
+
) {
|
|
2171
|
+
throw artifactError(
|
|
2172
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2173
|
+
"ArtifactStore stored path escapes the captured files root",
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
let integrityBefore;
|
|
2178
|
+
try {
|
|
2179
|
+
integrityBefore = this.#store.verifyIntegrity(entry);
|
|
2180
|
+
} catch (cause) {
|
|
2181
|
+
throw artifactError(
|
|
2182
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2183
|
+
"ArtifactStore integrity verification threw",
|
|
2184
|
+
{ cause },
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
validateIntegrityResult(integrityBefore, expectedHex);
|
|
2188
|
+
|
|
2189
|
+
let rootRealPath;
|
|
2190
|
+
let beforePathStat;
|
|
2191
|
+
let targetRealPath;
|
|
2192
|
+
let descriptor = null;
|
|
2193
|
+
let beforeDescriptorStat;
|
|
2194
|
+
let afterDescriptorStat;
|
|
2195
|
+
let bytes;
|
|
2196
|
+
try {
|
|
2197
|
+
rootRealPath = fs.realpathSync(filesRoot);
|
|
2198
|
+
if (!samePath(rootRealPath, this.#store.layout.filesRealPath)) {
|
|
2199
|
+
throw artifactError(
|
|
2200
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2201
|
+
"ArtifactStore files root physical identity changed before byte read",
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
beforePathStat = fs.lstatSync(resolvedStoredPath);
|
|
2205
|
+
assertRegularSingleLink(beforePathStat, "stored artifact path");
|
|
2206
|
+
targetRealPath = fs.realpathSync(resolvedStoredPath);
|
|
2207
|
+
if (!isContained(rootRealPath, targetRealPath)) {
|
|
2208
|
+
throw artifactError(
|
|
2209
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2210
|
+
"stored artifact realpath escapes the captured files root",
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
descriptor = fs.openSync(
|
|
2214
|
+
resolvedStoredPath,
|
|
2215
|
+
fs.constants.O_RDONLY | Number(fs.constants.O_NOFOLLOW || 0),
|
|
2216
|
+
);
|
|
2217
|
+
beforeDescriptorStat = fs.fstatSync(descriptor);
|
|
2218
|
+
assertRegularSingleLink(
|
|
2219
|
+
beforeDescriptorStat,
|
|
2220
|
+
"stored artifact descriptor",
|
|
2221
|
+
);
|
|
2222
|
+
if (!sameFileIdentity(beforePathStat, beforeDescriptorStat)) {
|
|
2223
|
+
throw artifactError(
|
|
2224
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2225
|
+
"stored artifact pathname and descriptor identities differ",
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
bytes = fs.readFileSync(descriptor);
|
|
2229
|
+
afterDescriptorStat = fs.fstatSync(descriptor);
|
|
2230
|
+
if (!sameFileIdentity(beforeDescriptorStat, afterDescriptorStat)) {
|
|
2231
|
+
throw artifactError(
|
|
2232
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2233
|
+
"stored artifact changed while it was read",
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
} catch (cause) {
|
|
2237
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
2238
|
+
throw artifactError(
|
|
2239
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2240
|
+
"stored artifact could not be opened safely",
|
|
2241
|
+
{ cause },
|
|
2242
|
+
);
|
|
2243
|
+
} finally {
|
|
2244
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
2245
|
+
}
|
|
2246
|
+
let afterPathStat;
|
|
2247
|
+
try {
|
|
2248
|
+
afterPathStat = fs.lstatSync(resolvedStoredPath);
|
|
2249
|
+
assertRegularSingleLink(afterPathStat, "stored artifact path");
|
|
2250
|
+
if (!sameFileIdentity(beforePathStat, afterPathStat)) {
|
|
2251
|
+
throw artifactError(
|
|
2252
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2253
|
+
"stored artifact pathname changed during readback",
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
const afterRealPath = fs.realpathSync(resolvedStoredPath);
|
|
2257
|
+
if (!samePath(targetRealPath, afterRealPath)) {
|
|
2258
|
+
throw artifactError(
|
|
2259
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2260
|
+
"stored artifact realpath changed during readback",
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
} catch (cause) {
|
|
2264
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
2265
|
+
throw artifactError(
|
|
2266
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2267
|
+
"stored artifact could not be re-attested",
|
|
2268
|
+
{ cause },
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
if (
|
|
2272
|
+
bytes.length !== normalizedEntry.size ||
|
|
2273
|
+
sha256(bytes) !== expectedDigest
|
|
2274
|
+
) {
|
|
2275
|
+
throw artifactError(
|
|
2276
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2277
|
+
"stored artifact bytes do not match the signed recordDigest",
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
let integrityAfter;
|
|
2281
|
+
try {
|
|
2282
|
+
integrityAfter = this.#store.verifyIntegrity(entry);
|
|
2283
|
+
} catch (cause) {
|
|
2284
|
+
throw artifactError(
|
|
2285
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2286
|
+
"ArtifactStore post-read integrity verification threw",
|
|
2287
|
+
{ cause },
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
validateIntegrityResult(integrityAfter, expectedHex);
|
|
2291
|
+
attestStoreDirectories(this.#store.layout);
|
|
2292
|
+
return bytes;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
#parseRecordBytes(bytes, core) {
|
|
2296
|
+
const text = bytes.toString("utf8");
|
|
2297
|
+
if (!Buffer.from(text, "utf8").equals(bytes)) {
|
|
2298
|
+
throw artifactError(
|
|
2299
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2300
|
+
"artifact bytes are not exact UTF-8",
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
let record;
|
|
2304
|
+
try {
|
|
2305
|
+
record = JSON.parse(text);
|
|
2306
|
+
} catch (cause) {
|
|
2307
|
+
throw artifactError(
|
|
2308
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2309
|
+
"artifact bytes are not JSON",
|
|
2310
|
+
{ cause },
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
if (canonicalJson(record) !== text) {
|
|
2314
|
+
throw artifactError(
|
|
2315
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2316
|
+
"artifact bytes are not exact canonical JSON",
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
assertExactRecord(
|
|
2320
|
+
record,
|
|
2321
|
+
RECORD_KEYS,
|
|
2322
|
+
"durable evolution artifact record",
|
|
2323
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2324
|
+
);
|
|
2325
|
+
if (
|
|
2326
|
+
ownData(record, "schema", "durable evolution artifact record") !==
|
|
2327
|
+
EVOLUTION_DURABLE_ARTIFACT_RECORD_SCHEMA ||
|
|
2328
|
+
ownData(record, "tenantId", "durable evolution artifact record") !==
|
|
2329
|
+
core.tenantId ||
|
|
2330
|
+
ownData(record, "audience", "durable evolution artifact record") !==
|
|
2331
|
+
core.audience ||
|
|
2332
|
+
ownData(record, "purpose", "durable evolution artifact record") !==
|
|
2333
|
+
core.purpose ||
|
|
2334
|
+
ownData(record, "retention", "durable evolution artifact record") !==
|
|
2335
|
+
core.retention ||
|
|
2336
|
+
ownData(record, "type", "durable evolution artifact record") !== core.type
|
|
2337
|
+
) {
|
|
2338
|
+
throw artifactError(
|
|
2339
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2340
|
+
"artifact record context does not match the signed envelope",
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
return deepFreeze(record);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
#authorityRequest({
|
|
2347
|
+
action,
|
|
2348
|
+
digest,
|
|
2349
|
+
type,
|
|
2350
|
+
purpose,
|
|
2351
|
+
retention,
|
|
2352
|
+
keyId,
|
|
2353
|
+
issuedAt,
|
|
2354
|
+
issuedPolicyDigest,
|
|
2355
|
+
issuedPolicyRevision,
|
|
2356
|
+
clock,
|
|
2357
|
+
}) {
|
|
2358
|
+
return deepFreeze({
|
|
2359
|
+
action,
|
|
2360
|
+
audience: this.#audience,
|
|
2361
|
+
digest,
|
|
2362
|
+
issuedAt,
|
|
2363
|
+
issuedPolicyDigest,
|
|
2364
|
+
issuedPolicyRevision,
|
|
2365
|
+
keyId,
|
|
2366
|
+
purpose,
|
|
2367
|
+
requestedAt: clock.iso,
|
|
2368
|
+
retention,
|
|
2369
|
+
schema: EVOLUTION_ARTIFACT_AUTHORITY_REQUEST_SCHEMA,
|
|
2370
|
+
tenantId: this.#tenantId,
|
|
2371
|
+
type,
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
#currentAuthority({
|
|
2376
|
+
action,
|
|
2377
|
+
digest,
|
|
2378
|
+
type,
|
|
2379
|
+
purpose,
|
|
2380
|
+
retention,
|
|
2381
|
+
keyId = null,
|
|
2382
|
+
issuedAt = null,
|
|
2383
|
+
issuedPolicyDigest = null,
|
|
2384
|
+
issuedPolicyRevision = null,
|
|
2385
|
+
}) {
|
|
2386
|
+
const requestClock = this.#clock();
|
|
2387
|
+
const request = this.#authorityRequest({
|
|
2388
|
+
action,
|
|
2389
|
+
digest,
|
|
2390
|
+
type,
|
|
2391
|
+
purpose,
|
|
2392
|
+
retention,
|
|
2393
|
+
keyId,
|
|
2394
|
+
issuedAt,
|
|
2395
|
+
issuedPolicyDigest,
|
|
2396
|
+
issuedPolicyRevision,
|
|
2397
|
+
clock: requestClock,
|
|
2398
|
+
});
|
|
2399
|
+
let value;
|
|
2400
|
+
try {
|
|
2401
|
+
value = rejectPromise(
|
|
2402
|
+
this.#resolveCurrentAuthority(request),
|
|
2403
|
+
"currentAuthorityResolver.resolve",
|
|
2404
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2405
|
+
);
|
|
2406
|
+
} catch (cause) {
|
|
2407
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
2408
|
+
throw artifactError(
|
|
2409
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2410
|
+
"current artifact authority could not be resolved",
|
|
2411
|
+
{ cause },
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
const responseClock = this.#clock();
|
|
2415
|
+
if (responseClock.milliseconds < requestClock.milliseconds) {
|
|
2416
|
+
throw artifactError(
|
|
2417
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2418
|
+
"artifact authority clock moved backwards",
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
assertExactRecord(
|
|
2422
|
+
value,
|
|
2423
|
+
AUTHORITY_DECISION_KEYS,
|
|
2424
|
+
"current artifact authority decision",
|
|
2425
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2426
|
+
);
|
|
2427
|
+
const decisionCore = { ...value };
|
|
2428
|
+
delete decisionCore.receiptDigest;
|
|
2429
|
+
const checkedAt = normalizeTimestamp(
|
|
2430
|
+
ownData(value, "checkedAt", "current artifact authority decision"),
|
|
2431
|
+
"authority checkedAt",
|
|
2432
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2433
|
+
);
|
|
2434
|
+
const decisionExpiresAt = normalizeTimestamp(
|
|
2435
|
+
ownData(
|
|
2436
|
+
value,
|
|
2437
|
+
"decisionExpiresAt",
|
|
2438
|
+
"current artifact authority decision",
|
|
2439
|
+
),
|
|
2440
|
+
"authority decisionExpiresAt",
|
|
2441
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2442
|
+
);
|
|
2443
|
+
const requestedAt = normalizeTimestamp(
|
|
2444
|
+
ownData(value, "requestedAt", "current artifact authority decision"),
|
|
2445
|
+
"authority requestedAt",
|
|
2446
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2447
|
+
);
|
|
2448
|
+
const checkedAtMs = new Date(checkedAt).getTime();
|
|
2449
|
+
const decisionExpiresAtMs = new Date(decisionExpiresAt).getTime();
|
|
2450
|
+
const receiptDigest = normalizeDigest(
|
|
2451
|
+
ownData(value, "receiptDigest", "current artifact authority decision"),
|
|
2452
|
+
"authority receiptDigest",
|
|
2453
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2454
|
+
);
|
|
2455
|
+
const normalized = {
|
|
2456
|
+
action: ownData(value, "action", "current artifact authority decision"),
|
|
2457
|
+
algorithm: normalizeAlgorithm(
|
|
2458
|
+
ownData(value, "algorithm", "current artifact authority decision"),
|
|
2459
|
+
"authority algorithm",
|
|
2460
|
+
),
|
|
2461
|
+
allowed: ownData(value, "allowed", "current artifact authority decision"),
|
|
2462
|
+
audience: ownData(
|
|
2463
|
+
value,
|
|
2464
|
+
"audience",
|
|
2465
|
+
"current artifact authority decision",
|
|
2466
|
+
),
|
|
2467
|
+
checkedAt,
|
|
2468
|
+
decisionExpiresAt,
|
|
2469
|
+
decisionExpiresAtMs,
|
|
2470
|
+
digest: ownData(value, "digest", "current artifact authority decision"),
|
|
2471
|
+
issuedAt: normalizeNullableTimestamp(
|
|
2472
|
+
ownData(value, "issuedAt", "current artifact authority decision"),
|
|
2473
|
+
"authority issuedAt",
|
|
2474
|
+
),
|
|
2475
|
+
issuedPolicyDigest: normalizeNullableDigest(
|
|
2476
|
+
ownData(
|
|
2477
|
+
value,
|
|
2478
|
+
"issuedPolicyDigest",
|
|
2479
|
+
"current artifact authority decision",
|
|
2480
|
+
),
|
|
2481
|
+
"authority issuedPolicyDigest",
|
|
2482
|
+
),
|
|
2483
|
+
issuedPolicyRevision: normalizeNullableRevision(
|
|
2484
|
+
ownData(
|
|
2485
|
+
value,
|
|
2486
|
+
"issuedPolicyRevision",
|
|
2487
|
+
"current artifact authority decision",
|
|
2488
|
+
),
|
|
2489
|
+
"authority issuedPolicyRevision",
|
|
2490
|
+
),
|
|
2491
|
+
issuedPolicyTrusted: ownData(
|
|
2492
|
+
value,
|
|
2493
|
+
"issuedPolicyTrusted",
|
|
2494
|
+
"current artifact authority decision",
|
|
2495
|
+
),
|
|
2496
|
+
keyId: normalizeKeyId(
|
|
2497
|
+
ownData(value, "keyId", "current artifact authority decision"),
|
|
2498
|
+
"authority keyId",
|
|
2499
|
+
),
|
|
2500
|
+
policyDigest: normalizeDigest(
|
|
2501
|
+
ownData(value, "policyDigest", "current artifact authority decision"),
|
|
2502
|
+
"authority policyDigest",
|
|
2503
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2504
|
+
),
|
|
2505
|
+
policyRevision: normalizeRevision(
|
|
2506
|
+
ownData(value, "policyRevision", "current artifact authority decision"),
|
|
2507
|
+
"authority policyRevision",
|
|
2508
|
+
),
|
|
2509
|
+
purpose: ownData(value, "purpose", "current artifact authority decision"),
|
|
2510
|
+
receiptDigest,
|
|
2511
|
+
requestedAt,
|
|
2512
|
+
retention: normalizeRetention(
|
|
2513
|
+
ownData(value, "retention", "current artifact authority decision"),
|
|
2514
|
+
"authority retention",
|
|
2515
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2516
|
+
),
|
|
2517
|
+
revocationRevision: normalizeRevision(
|
|
2518
|
+
ownData(
|
|
2519
|
+
value,
|
|
2520
|
+
"revocationRevision",
|
|
2521
|
+
"current artifact authority decision",
|
|
2522
|
+
),
|
|
2523
|
+
"authority revocationRevision",
|
|
2524
|
+
),
|
|
2525
|
+
revoked: ownData(value, "revoked", "current artifact authority decision"),
|
|
2526
|
+
tenantId: ownData(
|
|
2527
|
+
value,
|
|
2528
|
+
"tenantId",
|
|
2529
|
+
"current artifact authority decision",
|
|
2530
|
+
),
|
|
2531
|
+
type: ownData(value, "type", "current artifact authority decision"),
|
|
2532
|
+
};
|
|
2533
|
+
if (
|
|
2534
|
+
ownData(value, "schema", "current artifact authority decision") !==
|
|
2535
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DECISION_SCHEMA ||
|
|
2536
|
+
normalized.action !== action ||
|
|
2537
|
+
normalized.allowed !== true ||
|
|
2538
|
+
normalized.revoked !== false ||
|
|
2539
|
+
normalized.tenantId !== this.#tenantId ||
|
|
2540
|
+
normalized.audience !== this.#audience ||
|
|
2541
|
+
normalized.purpose !== purpose ||
|
|
2542
|
+
normalized.type !== type ||
|
|
2543
|
+
normalized.digest !== digest ||
|
|
2544
|
+
normalized.retention !== retention ||
|
|
2545
|
+
normalized.issuedAt !== issuedAt ||
|
|
2546
|
+
normalized.issuedPolicyDigest !== issuedPolicyDigest ||
|
|
2547
|
+
normalized.issuedPolicyRevision !== issuedPolicyRevision ||
|
|
2548
|
+
normalized.issuedPolicyTrusted !== true ||
|
|
2549
|
+
normalized.requestedAt !== request.requestedAt ||
|
|
2550
|
+
(keyId !== null && normalized.keyId !== keyId) ||
|
|
2551
|
+
checkedAtMs < requestClock.milliseconds ||
|
|
2552
|
+
checkedAtMs > responseClock.milliseconds ||
|
|
2553
|
+
decisionExpiresAtMs <= responseClock.milliseconds ||
|
|
2554
|
+
decisionExpiresAtMs - requestClock.milliseconds >
|
|
2555
|
+
MAX_AUTHORITY_DECISION_TTL_MS ||
|
|
2556
|
+
receiptDigest !== domainDigest(AUTHORITY_RECEIPT_DOMAIN, decisionCore)
|
|
2557
|
+
) {
|
|
2558
|
+
throw artifactError(
|
|
2559
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2560
|
+
"current artifact authority denied or returned an unbound decision",
|
|
2561
|
+
);
|
|
2562
|
+
}
|
|
2563
|
+
return deepFreeze(normalized);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
#sign(core, authority) {
|
|
2567
|
+
const coreJson = canonicalJson(core, EVOLUTION_ARTIFACT_MAX_ENVELOPE_BYTES);
|
|
2568
|
+
const message = `${ENVELOPE_SIGNATURE_DOMAIN}${coreJson}`;
|
|
2569
|
+
const request = deepFreeze({
|
|
2570
|
+
algorithm: authority.algorithm,
|
|
2571
|
+
audience: core.audience,
|
|
2572
|
+
digest: core.digest,
|
|
2573
|
+
keyId: authority.keyId,
|
|
2574
|
+
message,
|
|
2575
|
+
messageDigest: sha256(Buffer.from(message, "utf8")),
|
|
2576
|
+
purpose: core.purpose,
|
|
2577
|
+
retention: core.retention,
|
|
2578
|
+
tenantId: core.tenantId,
|
|
2579
|
+
type: core.type,
|
|
2580
|
+
});
|
|
2581
|
+
let value;
|
|
2582
|
+
try {
|
|
2583
|
+
value = rejectPromise(
|
|
2584
|
+
this.#signEnvelope(request),
|
|
2585
|
+
"envelopeSigner.sign",
|
|
2586
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
2587
|
+
);
|
|
2588
|
+
} catch (cause) {
|
|
2589
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
2590
|
+
throw artifactError(
|
|
2591
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
2592
|
+
"evolution artifact envelope signing failed",
|
|
2593
|
+
{ cause },
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
return normalizeSignature(value, authority);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
#verifySignature(envelope, parsed, normalizedCore) {
|
|
2600
|
+
const coreJson = canonicalJson(
|
|
2601
|
+
parsed.core,
|
|
2602
|
+
EVOLUTION_ARTIFACT_MAX_ENVELOPE_BYTES,
|
|
2603
|
+
);
|
|
2604
|
+
const message = `${ENVELOPE_SIGNATURE_DOMAIN}${coreJson}`;
|
|
2605
|
+
const request = deepFreeze({
|
|
2606
|
+
algorithm: normalizedCore.algorithm,
|
|
2607
|
+
audience: normalizedCore.audience,
|
|
2608
|
+
digest: normalizedCore.digest,
|
|
2609
|
+
envelope,
|
|
2610
|
+
envelopeDigest: sha256(Buffer.from(envelope, "utf8")),
|
|
2611
|
+
keyId: normalizedCore.keyId,
|
|
2612
|
+
message,
|
|
2613
|
+
messageDigest: sha256(Buffer.from(message, "utf8")),
|
|
2614
|
+
purpose: normalizedCore.purpose,
|
|
2615
|
+
retention: normalizedCore.retention,
|
|
2616
|
+
signature: normalizedCore.signature,
|
|
2617
|
+
tenantId: normalizedCore.tenantId,
|
|
2618
|
+
type: normalizedCore.type,
|
|
2619
|
+
});
|
|
2620
|
+
let verified;
|
|
2621
|
+
try {
|
|
2622
|
+
verified = rejectPromise(
|
|
2623
|
+
this.#verifyEnvelope(request),
|
|
2624
|
+
"envelopeVerifier.verify",
|
|
2625
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
2626
|
+
);
|
|
2627
|
+
} catch (cause) {
|
|
2628
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
2629
|
+
throw artifactError(
|
|
2630
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
2631
|
+
"evolution artifact envelope verification failed",
|
|
2632
|
+
{ cause },
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
if (verified !== true) {
|
|
2636
|
+
throw artifactError(
|
|
2637
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
2638
|
+
"evolution artifact envelope signature is invalid",
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
#authenticateEnvelope(envelope, options) {
|
|
2644
|
+
const parsed = parseCanonicalEnvelope(envelope);
|
|
2645
|
+
const core = validateEnvelopeCore(
|
|
2646
|
+
parsed.core,
|
|
2647
|
+
parsed.signature,
|
|
2648
|
+
this.#allowedTypes,
|
|
2649
|
+
);
|
|
2650
|
+
if (
|
|
2651
|
+
core.tenantId !== options.tenantId ||
|
|
2652
|
+
core.audience !== options.audience ||
|
|
2653
|
+
core.purpose !== options.purpose ||
|
|
2654
|
+
core.type !== options.expectedType ||
|
|
2655
|
+
core.digest !== options.expectedDigest
|
|
2656
|
+
) {
|
|
2657
|
+
throw artifactError(
|
|
2658
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2659
|
+
"artifact envelope is not bound to the requested tenant, audience, purpose, type, and digest",
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
this.#verifySignature(envelope, parsed.parsed, core);
|
|
2663
|
+
const verificationClock = this.#clock();
|
|
2664
|
+
if (
|
|
2665
|
+
verificationClock.milliseconds < core.issuedAtMs ||
|
|
2666
|
+
(core.expiresAtMs !== null &&
|
|
2667
|
+
verificationClock.milliseconds >= core.expiresAtMs)
|
|
2668
|
+
) {
|
|
2669
|
+
throw artifactError(
|
|
2670
|
+
EVOLUTION_ARTIFACT_EXPIRED_CODE,
|
|
2671
|
+
"evolution artifact envelope is not currently valid",
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
const authority = this.#currentAuthority({
|
|
2675
|
+
action: "resolve",
|
|
2676
|
+
digest: core.digest,
|
|
2677
|
+
type: core.type,
|
|
2678
|
+
purpose: core.purpose,
|
|
2679
|
+
retention: core.retention,
|
|
2680
|
+
keyId: core.keyId,
|
|
2681
|
+
issuedAt: core.issuedAt,
|
|
2682
|
+
issuedPolicyDigest: core.policyDigest,
|
|
2683
|
+
issuedPolicyRevision: core.policyRevision,
|
|
2684
|
+
});
|
|
2685
|
+
const currentPolicyIsAdmissible =
|
|
2686
|
+
core.retention === "ledger"
|
|
2687
|
+
? authority.issuedAt === core.issuedAt &&
|
|
2688
|
+
authority.issuedPolicyDigest === core.policyDigest &&
|
|
2689
|
+
authority.issuedPolicyRevision === core.policyRevision &&
|
|
2690
|
+
authority.issuedPolicyTrusted === true &&
|
|
2691
|
+
authority.policyRevision >= core.policyRevision
|
|
2692
|
+
: authority.policyDigest === core.policyDigest &&
|
|
2693
|
+
authority.policyRevision === core.policyRevision;
|
|
2694
|
+
if (
|
|
2695
|
+
authority.algorithm !== core.algorithm ||
|
|
2696
|
+
authority.keyId !== core.keyId ||
|
|
2697
|
+
!currentPolicyIsAdmissible ||
|
|
2698
|
+
authority.revocationRevision < core.revocationRevision
|
|
2699
|
+
) {
|
|
2700
|
+
throw artifactError(
|
|
2701
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
2702
|
+
"artifact envelope is stale under current policy or revocation authority",
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
const completionClock = this.#clock();
|
|
2706
|
+
if (
|
|
2707
|
+
completionClock.milliseconds < verificationClock.milliseconds ||
|
|
2708
|
+
(core.expiresAtMs !== null &&
|
|
2709
|
+
completionClock.milliseconds >= core.expiresAtMs) ||
|
|
2710
|
+
completionClock.milliseconds >= authority.decisionExpiresAtMs
|
|
2711
|
+
) {
|
|
2712
|
+
throw artifactError(
|
|
2713
|
+
EVOLUTION_ARTIFACT_EXPIRED_CODE,
|
|
2714
|
+
"artifact authority expired before envelope authentication completed",
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
return Object.freeze({
|
|
2718
|
+
authority,
|
|
2719
|
+
core,
|
|
2720
|
+
envelopeDigest: parsed.envelopeDigest,
|
|
2721
|
+
parsed: parsed.parsed,
|
|
2722
|
+
resolvedAt: completionClock.iso,
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
#resolveAuthenticatedEntry(
|
|
2727
|
+
envelope,
|
|
2728
|
+
options,
|
|
2729
|
+
artifactId,
|
|
2730
|
+
preauthenticated = null,
|
|
2731
|
+
) {
|
|
2732
|
+
const authentication =
|
|
2733
|
+
preauthenticated || this.#authenticateEnvelope(envelope, options);
|
|
2734
|
+
this.#assertUniqueRecordDigest(
|
|
2735
|
+
authentication.core.recordDigest,
|
|
2736
|
+
artifactId,
|
|
2737
|
+
);
|
|
2738
|
+
const entry = this.#loadCurrentEntryById(artifactId);
|
|
2739
|
+
const normalizedEntry = this.#validateIndexEntry(
|
|
2740
|
+
entry,
|
|
2741
|
+
envelope,
|
|
2742
|
+
authentication.core,
|
|
2743
|
+
authentication.envelopeDigest,
|
|
2744
|
+
);
|
|
2745
|
+
if (normalizedEntry.id !== artifactId) {
|
|
2746
|
+
throw artifactError(
|
|
2747
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2748
|
+
"ArtifactStore entry id does not match the requested locator",
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
const bytes = this.#readStoredBytes(
|
|
2752
|
+
entry,
|
|
2753
|
+
normalizedEntry,
|
|
2754
|
+
authentication.core.recordDigest,
|
|
2755
|
+
);
|
|
2756
|
+
const record = this.#parseRecordBytes(bytes, authentication.core);
|
|
2757
|
+
this.#assertEntryStable(artifactId, entry);
|
|
2758
|
+
this.#assertUniqueRecordDigest(
|
|
2759
|
+
authentication.core.recordDigest,
|
|
2760
|
+
artifactId,
|
|
2761
|
+
);
|
|
2762
|
+
const releaseClock = this.#clock();
|
|
2763
|
+
if (
|
|
2764
|
+
releaseClock.milliseconds <
|
|
2765
|
+
new Date(authentication.resolvedAt).getTime() ||
|
|
2766
|
+
(authentication.core.expiresAtMs !== null &&
|
|
2767
|
+
releaseClock.milliseconds >= authentication.core.expiresAtMs) ||
|
|
2768
|
+
releaseClock.milliseconds >= authentication.authority.decisionExpiresAtMs
|
|
2769
|
+
) {
|
|
2770
|
+
throw artifactError(
|
|
2771
|
+
EVOLUTION_ARTIFACT_EXPIRED_CODE,
|
|
2772
|
+
"artifact authority expired before canonical bytes could be released",
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
const artifactRef = buildArtifactRef(
|
|
2776
|
+
artifactId,
|
|
2777
|
+
authentication.core.digest,
|
|
2778
|
+
);
|
|
2779
|
+
const receiptCore = {
|
|
2780
|
+
authenticated: true,
|
|
2781
|
+
audience: authentication.core.audience,
|
|
2782
|
+
currentAuthorityReceiptDigest: authentication.authority.receiptDigest,
|
|
2783
|
+
digest: authentication.core.digest,
|
|
2784
|
+
envelopeDigest: authentication.envelopeDigest,
|
|
2785
|
+
policyDigest: authentication.core.policyDigest,
|
|
2786
|
+
policyRevision: authentication.core.policyRevision,
|
|
2787
|
+
purpose: authentication.core.purpose,
|
|
2788
|
+
recordDigest: authentication.core.recordDigest,
|
|
2789
|
+
retention: authentication.core.retention,
|
|
2790
|
+
ref: artifactRef.ref,
|
|
2791
|
+
resolvedAt: releaseClock.iso,
|
|
2792
|
+
revocationRevision: authentication.authority.revocationRevision,
|
|
2793
|
+
schema: EVOLUTION_ARTIFACT_RESOLUTION_RECEIPT_SCHEMA,
|
|
2794
|
+
tenantId: authentication.core.tenantId,
|
|
2795
|
+
type: authentication.core.type,
|
|
2796
|
+
};
|
|
2797
|
+
const receipt = deepFreeze({
|
|
2798
|
+
...receiptCore,
|
|
2799
|
+
receiptDigest: domainDigest(RESOLUTION_RECEIPT_DOMAIN, receiptCore),
|
|
2800
|
+
});
|
|
2801
|
+
return {
|
|
2802
|
+
artifactRef,
|
|
2803
|
+
authentication,
|
|
2804
|
+
bytes,
|
|
2805
|
+
receipt,
|
|
2806
|
+
record,
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
#findUniqueEntryIdByRecordDigest(recordDigest) {
|
|
2811
|
+
const matches = this.#entriesByDataField(
|
|
2812
|
+
this.#boundedIndexEntries(),
|
|
2813
|
+
"recordDigest",
|
|
2814
|
+
recordDigest,
|
|
2815
|
+
);
|
|
2816
|
+
if (matches.length === 0) {
|
|
2817
|
+
throw artifactError(
|
|
2818
|
+
EVOLUTION_ARTIFACT_NOT_FOUND_CODE,
|
|
2819
|
+
"evolution artifact was removed, expired, or never persisted",
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
if (matches.length !== 1) {
|
|
2823
|
+
throw artifactError(
|
|
2824
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2825
|
+
"recordDigest has duplicate ArtifactStore index rows",
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
assertExactRecord(
|
|
2829
|
+
matches[0],
|
|
2830
|
+
INDEX_ENTRY_KEYS,
|
|
2831
|
+
"ArtifactStore index entry",
|
|
2832
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2833
|
+
);
|
|
2834
|
+
const artifactId = ownData(matches[0], "id", "ArtifactStore index entry");
|
|
2835
|
+
if (!ARTIFACT_ID_PATTERN.test(artifactId || "")) {
|
|
2836
|
+
throw artifactError(
|
|
2837
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
2838
|
+
"recordDigest index row has an invalid ArtifactStore id",
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
return artifactId;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
putCanonical(type, value, context) {
|
|
2845
|
+
const normalizedType = normalizeType(type, this.#allowedTypes);
|
|
2846
|
+
const normalizedContext = normalizePutContext(
|
|
2847
|
+
context,
|
|
2848
|
+
this.#audience,
|
|
2849
|
+
normalizedType,
|
|
2850
|
+
);
|
|
2851
|
+
const valueClone = frozenCanonicalClone(value);
|
|
2852
|
+
const record = deepFreeze({
|
|
2853
|
+
audience: this.#audience,
|
|
2854
|
+
purpose: normalizedContext.purpose,
|
|
2855
|
+
retention: normalizedContext.retention,
|
|
2856
|
+
schema: EVOLUTION_DURABLE_ARTIFACT_RECORD_SCHEMA,
|
|
2857
|
+
tenantId: this.#tenantId,
|
|
2858
|
+
type: normalizedType,
|
|
2859
|
+
value: valueClone,
|
|
2860
|
+
});
|
|
2861
|
+
const recordJson = canonicalJson(record);
|
|
2862
|
+
const recordBytes = Buffer.from(recordJson, "utf8");
|
|
2863
|
+
const recordDigest = sha256(recordBytes);
|
|
2864
|
+
readTrustedIndexSnapshot(this.#store.layout);
|
|
2865
|
+
const issuanceClock = this.#clock();
|
|
2866
|
+
const expiresAt =
|
|
2867
|
+
normalizedContext.retention === "ledger"
|
|
2868
|
+
? null
|
|
2869
|
+
: addCanonicalTimestamp(
|
|
2870
|
+
issuanceClock.milliseconds,
|
|
2871
|
+
normalizedContext.ttlMs,
|
|
2872
|
+
"artifact expiresAt",
|
|
2873
|
+
);
|
|
2874
|
+
const storeTtlDays =
|
|
2875
|
+
normalizedContext.retention === "ledger"
|
|
2876
|
+
? ledgerHousekeepingTtlDays(issuanceClock.milliseconds)
|
|
2877
|
+
: Math.max(
|
|
2878
|
+
1,
|
|
2879
|
+
Math.ceil(normalizedContext.ttlMs / MILLISECONDS_PER_DAY),
|
|
2880
|
+
);
|
|
2881
|
+
const authority = this.#currentAuthority({
|
|
2882
|
+
action: "publish",
|
|
2883
|
+
digest: recordDigest,
|
|
2884
|
+
type: normalizedType,
|
|
2885
|
+
purpose: normalizedContext.purpose,
|
|
2886
|
+
retention: normalizedContext.retention,
|
|
2887
|
+
});
|
|
2888
|
+
const core = deepFreeze({
|
|
2889
|
+
algorithm: authority.algorithm,
|
|
2890
|
+
audience: this.#audience,
|
|
2891
|
+
authorityReceiptDigest: authority.receiptDigest,
|
|
2892
|
+
digest: recordDigest,
|
|
2893
|
+
expiresAt,
|
|
2894
|
+
issuedAt: issuanceClock.iso,
|
|
2895
|
+
keyId: authority.keyId,
|
|
2896
|
+
policyDigest: authority.policyDigest,
|
|
2897
|
+
policyRevision: authority.policyRevision,
|
|
2898
|
+
purpose: normalizedContext.purpose,
|
|
2899
|
+
recordDigest,
|
|
2900
|
+
retention: normalizedContext.retention,
|
|
2901
|
+
revocationRevision: authority.revocationRevision,
|
|
2902
|
+
schema: EVOLUTION_ARTIFACT_ENVELOPE_CORE_SCHEMA,
|
|
2903
|
+
tenantId: this.#tenantId,
|
|
2904
|
+
type: normalizedType,
|
|
2905
|
+
});
|
|
2906
|
+
const signature = this.#sign(core, authority);
|
|
2907
|
+
const signingClock = this.#clock();
|
|
2908
|
+
if (
|
|
2909
|
+
signingClock.milliseconds < issuanceClock.milliseconds ||
|
|
2910
|
+
signingClock.milliseconds >= authority.decisionExpiresAtMs ||
|
|
2911
|
+
(expiresAt !== null &&
|
|
2912
|
+
signingClock.milliseconds >= new Date(expiresAt).getTime())
|
|
2913
|
+
) {
|
|
2914
|
+
throw artifactError(
|
|
2915
|
+
EVOLUTION_ARTIFACT_EXPIRED_CODE,
|
|
2916
|
+
"artifact publish authority expired before envelope signing completed",
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
const envelope = canonicalJson(
|
|
2920
|
+
{
|
|
2921
|
+
core,
|
|
2922
|
+
schema: EVOLUTION_ARTIFACT_ENVELOPE_SCHEMA,
|
|
2923
|
+
signature,
|
|
2924
|
+
},
|
|
2925
|
+
EVOLUTION_ARTIFACT_MAX_ENVELOPE_BYTES,
|
|
2926
|
+
);
|
|
2927
|
+
if (
|
|
2928
|
+
Buffer.byteLength(envelope, "utf8") >
|
|
2929
|
+
EVOLUTION_ARTIFACT_MAX_ENVELOPE_BYTES
|
|
2930
|
+
) {
|
|
2931
|
+
throw artifactError(
|
|
2932
|
+
EVOLUTION_ARTIFACT_SIGNATURE_INVALID_CODE,
|
|
2933
|
+
"signed evolution artifact envelope exceeds 4096 bytes",
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
const envelopeDigest = sha256(Buffer.from(envelope, "utf8"));
|
|
2937
|
+
const lineage = deepFreeze({
|
|
2938
|
+
audience: this.#audience,
|
|
2939
|
+
envelope,
|
|
2940
|
+
envelopeDigest,
|
|
2941
|
+
purpose: normalizedContext.purpose,
|
|
2942
|
+
recordDigest,
|
|
2943
|
+
retention: normalizedContext.retention,
|
|
2944
|
+
schema: EVOLUTION_ARTIFACT_LINEAGE_SCHEMA,
|
|
2945
|
+
tenantId: this.#tenantId,
|
|
2946
|
+
type: normalizedType,
|
|
2947
|
+
});
|
|
2948
|
+
// Validate the captured physical store/index generation immediately before
|
|
2949
|
+
// granting ArtifactStore the append. Post-publication readback re-attests
|
|
2950
|
+
// it and cross-checks the repository API against a descriptor snapshot.
|
|
2951
|
+
readTrustedIndexSnapshot(this.#store.layout);
|
|
2952
|
+
let publication;
|
|
2953
|
+
try {
|
|
2954
|
+
publication = rejectPromise(
|
|
2955
|
+
this.#store.publishDataOnce({
|
|
2956
|
+
data: recordBytes,
|
|
2957
|
+
fileName: `${recordDigest.slice("sha256:".length)}.json`,
|
|
2958
|
+
title: `Evolution ${normalizedType} artifact`,
|
|
2959
|
+
kind: "data",
|
|
2960
|
+
mime: "application/json",
|
|
2961
|
+
sessionId: null,
|
|
2962
|
+
ttlDays: storeTtlDays,
|
|
2963
|
+
immutable: true,
|
|
2964
|
+
recordDigest,
|
|
2965
|
+
lineage,
|
|
2966
|
+
}),
|
|
2967
|
+
"ArtifactStore.publishDataOnce",
|
|
2968
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
2969
|
+
);
|
|
2970
|
+
} catch (cause) {
|
|
2971
|
+
if (isEvolutionArtifactPortError(cause)) throw cause;
|
|
2972
|
+
throw artifactError(
|
|
2973
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
2974
|
+
"canonical evolution artifact could not be persisted",
|
|
2975
|
+
{ cause, recordDigest },
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
assertExactRecord(
|
|
2979
|
+
publication,
|
|
2980
|
+
PUBLISH_RESULT_KEYS,
|
|
2981
|
+
"ArtifactStore publication result",
|
|
2982
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
2983
|
+
);
|
|
2984
|
+
const published = ownData(
|
|
2985
|
+
publication,
|
|
2986
|
+
"published",
|
|
2987
|
+
"ArtifactStore publication result",
|
|
2988
|
+
);
|
|
2989
|
+
if (typeof published !== "boolean") {
|
|
2990
|
+
throw artifactError(
|
|
2991
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
2992
|
+
"ArtifactStore publication result is invalid",
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
const publishedEntry = ownData(
|
|
2996
|
+
publication,
|
|
2997
|
+
"entry",
|
|
2998
|
+
"ArtifactStore publication result",
|
|
2999
|
+
);
|
|
3000
|
+
if (!publishedEntry || typeof publishedEntry !== "object") {
|
|
3001
|
+
throw artifactError(
|
|
3002
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
3003
|
+
"ArtifactStore publication did not return an entry",
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
const artifactId = ownData(
|
|
3007
|
+
publishedEntry,
|
|
3008
|
+
"id",
|
|
3009
|
+
"ArtifactStore publication entry",
|
|
3010
|
+
);
|
|
3011
|
+
if (!ARTIFACT_ID_PATTERN.test(artifactId || "")) {
|
|
3012
|
+
throw artifactError(
|
|
3013
|
+
EVOLUTION_ARTIFACT_STORE_FAILED_CODE,
|
|
3014
|
+
"ArtifactStore publication returned an invalid id",
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// A response-loss retry may find the prior immutable row. In that case its
|
|
3019
|
+
// exact signed envelope is authoritative and must be returned, rather than
|
|
3020
|
+
// replacing lineage with a newly issued envelope.
|
|
3021
|
+
const currentEntry = this.#loadCurrentEntryById(artifactId);
|
|
3022
|
+
assertExactRecord(
|
|
3023
|
+
currentEntry,
|
|
3024
|
+
INDEX_ENTRY_KEYS,
|
|
3025
|
+
"ArtifactStore index entry",
|
|
3026
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
3027
|
+
);
|
|
3028
|
+
const currentLineage = ownData(
|
|
3029
|
+
currentEntry,
|
|
3030
|
+
"lineage",
|
|
3031
|
+
"ArtifactStore index entry",
|
|
3032
|
+
);
|
|
3033
|
+
assertExactRecord(
|
|
3034
|
+
currentLineage,
|
|
3035
|
+
LINEAGE_KEYS,
|
|
3036
|
+
"artifact lineage",
|
|
3037
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
3038
|
+
);
|
|
3039
|
+
const returnedEnvelope = ownData(
|
|
3040
|
+
currentLineage,
|
|
3041
|
+
"envelope",
|
|
3042
|
+
"artifact lineage",
|
|
3043
|
+
);
|
|
3044
|
+
if (published && returnedEnvelope !== envelope) {
|
|
3045
|
+
throw artifactError(
|
|
3046
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
3047
|
+
"published artifact lineage does not contain the exact returned envelope",
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
const resolved = this.#resolveAuthenticatedEntry(
|
|
3051
|
+
returnedEnvelope,
|
|
3052
|
+
Object.freeze({
|
|
3053
|
+
audience: this.#audience,
|
|
3054
|
+
expectedDigest: recordDigest,
|
|
3055
|
+
expectedType: normalizedType,
|
|
3056
|
+
purpose: normalizedContext.purpose,
|
|
3057
|
+
tenantId: this.#tenantId,
|
|
3058
|
+
}),
|
|
3059
|
+
artifactId,
|
|
3060
|
+
);
|
|
3061
|
+
if (
|
|
3062
|
+
canonicalJson(resolved.record) !== recordJson ||
|
|
3063
|
+
resolved.authentication.envelopeDigest !==
|
|
3064
|
+
ownData(currentLineage, "envelopeDigest", "artifact lineage")
|
|
3065
|
+
) {
|
|
3066
|
+
throw artifactError(
|
|
3067
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
3068
|
+
"canonical artifact readback does not match the requested value and lineage",
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
const receiptCore = {
|
|
3072
|
+
audience: this.#audience,
|
|
3073
|
+
createdAt: ownData(
|
|
3074
|
+
currentEntry,
|
|
3075
|
+
"createdAt",
|
|
3076
|
+
"ArtifactStore index entry",
|
|
3077
|
+
),
|
|
3078
|
+
digest: recordDigest,
|
|
3079
|
+
envelopeDigest: resolved.authentication.envelopeDigest,
|
|
3080
|
+
expiresAt: resolved.authentication.core.expiresAt,
|
|
3081
|
+
immutable: true,
|
|
3082
|
+
integrityVerified: true,
|
|
3083
|
+
persisted: true,
|
|
3084
|
+
published,
|
|
3085
|
+
purpose: normalizedContext.purpose,
|
|
3086
|
+
readbackVerified: true,
|
|
3087
|
+
recordDigest,
|
|
3088
|
+
retention: resolved.authentication.core.retention,
|
|
3089
|
+
ref: resolved.artifactRef.ref,
|
|
3090
|
+
schema: EVOLUTION_ARTIFACT_PERSISTENCE_RECEIPT_SCHEMA,
|
|
3091
|
+
...(resolved.authentication.core.retention === "ttl"
|
|
3092
|
+
? {
|
|
3093
|
+
storeExpiresAt: ownData(
|
|
3094
|
+
currentEntry,
|
|
3095
|
+
"expiresAt",
|
|
3096
|
+
"ArtifactStore index entry",
|
|
3097
|
+
),
|
|
3098
|
+
}
|
|
3099
|
+
: {}),
|
|
3100
|
+
tenantId: this.#tenantId,
|
|
3101
|
+
type: normalizedType,
|
|
3102
|
+
};
|
|
3103
|
+
const receipt = deepFreeze({
|
|
3104
|
+
...receiptCore,
|
|
3105
|
+
receiptDigest: domainDigest(PERSISTENCE_RECEIPT_DOMAIN, receiptCore),
|
|
3106
|
+
});
|
|
3107
|
+
return deepFreeze({
|
|
3108
|
+
digest: recordDigest,
|
|
3109
|
+
envelope: returnedEnvelope,
|
|
3110
|
+
receipt,
|
|
3111
|
+
ref: resolved.artifactRef,
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
/**
|
|
3116
|
+
* Resolve a standalone signed envelope. This management interface must scan
|
|
3117
|
+
* the current ArtifactStore index and rejects above a hard admission limit;
|
|
3118
|
+
* production ledger reads should use the short-ref resolver below.
|
|
3119
|
+
*/
|
|
3120
|
+
resolve(envelope, options) {
|
|
3121
|
+
const normalizedOptions = normalizeResolveOptions(
|
|
3122
|
+
options,
|
|
3123
|
+
this.#tenantId,
|
|
3124
|
+
this.#audience,
|
|
3125
|
+
this.#allowedTypes,
|
|
3126
|
+
);
|
|
3127
|
+
readTrustedIndexSnapshot(this.#store.layout);
|
|
3128
|
+
const authentication = this.#authenticateEnvelope(
|
|
3129
|
+
envelope,
|
|
3130
|
+
normalizedOptions,
|
|
3131
|
+
);
|
|
3132
|
+
const recordDigest = authentication.core.recordDigest;
|
|
3133
|
+
const artifactId = this.#findUniqueEntryIdByRecordDigest(recordDigest);
|
|
3134
|
+
const resolved = this.#resolveAuthenticatedEntry(
|
|
3135
|
+
envelope,
|
|
3136
|
+
normalizedOptions,
|
|
3137
|
+
artifactId,
|
|
3138
|
+
authentication,
|
|
3139
|
+
);
|
|
3140
|
+
return deepFreeze({
|
|
3141
|
+
authenticated: true,
|
|
3142
|
+
digest: resolved.authentication.core.digest,
|
|
3143
|
+
envelopeDigest: resolved.authentication.envelopeDigest,
|
|
3144
|
+
receipt: resolved.receipt,
|
|
3145
|
+
ref: resolved.artifactRef,
|
|
3146
|
+
schema: EVOLUTION_ARTIFACT_RESOLVED_SCHEMA,
|
|
3147
|
+
tenantId: this.#tenantId,
|
|
3148
|
+
type: resolved.authentication.core.type,
|
|
3149
|
+
retention: resolved.authentication.core.retention,
|
|
3150
|
+
value: resolved.record.value,
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
#resolveLedgerRequest(request, purpose) {
|
|
3155
|
+
assertExactRecord(
|
|
3156
|
+
request,
|
|
3157
|
+
LEDGER_REQUEST_KEYS,
|
|
3158
|
+
"evolution ledger artifact request",
|
|
3159
|
+
);
|
|
3160
|
+
const tenantId = normalizeSafeId(
|
|
3161
|
+
ownData(request, "tenantId", "evolution ledger artifact request"),
|
|
3162
|
+
"evolution ledger tenantId",
|
|
3163
|
+
);
|
|
3164
|
+
if (tenantId !== this.#tenantId) {
|
|
3165
|
+
throw artifactError(
|
|
3166
|
+
EVOLUTION_ARTIFACT_AUTHORITY_DENIED_CODE,
|
|
3167
|
+
"cross-tenant evolution ledger artifact resolution is denied",
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const ledgerId = normalizeSafeId(
|
|
3171
|
+
ownData(request, "ledgerId", "evolution ledger artifact request"),
|
|
3172
|
+
"evolution ledger ledgerId",
|
|
3173
|
+
);
|
|
3174
|
+
const epoch = normalizeSafeId(
|
|
3175
|
+
ownData(request, "epoch", "evolution ledger artifact request"),
|
|
3176
|
+
"evolution ledger epoch",
|
|
3177
|
+
);
|
|
3178
|
+
const artifactRef = normalizeArtifactRef(
|
|
3179
|
+
ownData(request, "ref", "evolution ledger artifact request"),
|
|
3180
|
+
);
|
|
3181
|
+
const entry = this.#loadCurrentEntryById(artifactRef.artifactId);
|
|
3182
|
+
assertExactRecord(
|
|
3183
|
+
entry,
|
|
3184
|
+
INDEX_ENTRY_KEYS,
|
|
3185
|
+
"ArtifactStore index entry",
|
|
3186
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
3187
|
+
);
|
|
3188
|
+
const lineage = ownData(entry, "lineage", "ArtifactStore index entry");
|
|
3189
|
+
assertExactRecord(
|
|
3190
|
+
lineage,
|
|
3191
|
+
LINEAGE_KEYS,
|
|
3192
|
+
"artifact lineage",
|
|
3193
|
+
EVOLUTION_ARTIFACT_INTEGRITY_FAILED_CODE,
|
|
3194
|
+
);
|
|
3195
|
+
const envelope = ownData(lineage, "envelope", "artifact lineage");
|
|
3196
|
+
const parsed = parseCanonicalEnvelope(envelope);
|
|
3197
|
+
const expectedType = normalizeType(
|
|
3198
|
+
ownData(parsed.core, "type", "artifact envelope core"),
|
|
3199
|
+
this.#allowedTypes,
|
|
3200
|
+
);
|
|
3201
|
+
const options = Object.freeze({
|
|
3202
|
+
audience: this.#audience,
|
|
3203
|
+
expectedDigest: artifactRef.digest,
|
|
3204
|
+
expectedType,
|
|
3205
|
+
purpose,
|
|
3206
|
+
tenantId: this.#tenantId,
|
|
3207
|
+
});
|
|
3208
|
+
const authentication = this.#authenticateEnvelope(envelope, options);
|
|
3209
|
+
const resolved = this.#resolveAuthenticatedEntry(
|
|
3210
|
+
envelope,
|
|
3211
|
+
options,
|
|
3212
|
+
artifactRef.artifactId,
|
|
3213
|
+
authentication,
|
|
3214
|
+
);
|
|
3215
|
+
const ledgerReceiptCore = {
|
|
3216
|
+
artifactReceiptDigest: resolved.receipt.receiptDigest,
|
|
3217
|
+
digest: artifactRef.digest,
|
|
3218
|
+
epoch,
|
|
3219
|
+
ledgerId,
|
|
3220
|
+
ref: artifactRef.ref,
|
|
3221
|
+
tenantId,
|
|
3222
|
+
};
|
|
3223
|
+
const receiptDigest = domainDigest(
|
|
3224
|
+
"chainlesschain.evolution-ledger-artifact-resolution/v1\0",
|
|
3225
|
+
ledgerReceiptCore,
|
|
3226
|
+
);
|
|
3227
|
+
return Object.freeze({
|
|
3228
|
+
authenticated: true,
|
|
3229
|
+
bytes: Buffer.from(resolved.bytes),
|
|
3230
|
+
digest: artifactRef.digest,
|
|
3231
|
+
found: true,
|
|
3232
|
+
receiptDigest,
|
|
3233
|
+
ref: artifactRef.ref,
|
|
3234
|
+
schema: EVOLUTION_ARTIFACT_RESOLUTION_SCHEMA,
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
/** Return a frozen read-only function; it exposes no put/signing authority. */
|
|
3239
|
+
createEvolutionLedgerArtifactResolver(options = {}) {
|
|
3240
|
+
assertOptionalExactRecord(
|
|
3241
|
+
options,
|
|
3242
|
+
LEDGER_RESOLVER_OPTION_KEYS,
|
|
3243
|
+
LEDGER_RESOLVER_OPTION_KEYS,
|
|
3244
|
+
"evolution ledger resolver options",
|
|
3245
|
+
);
|
|
3246
|
+
const purpose = ownData(
|
|
3247
|
+
options,
|
|
3248
|
+
"purpose",
|
|
3249
|
+
"evolution ledger resolver options",
|
|
3250
|
+
);
|
|
3251
|
+
const normalizedPurpose = normalizeSafeId(
|
|
3252
|
+
purpose,
|
|
3253
|
+
"evolution ledger artifact purpose",
|
|
3254
|
+
);
|
|
3255
|
+
const resolveReadOnly = (request) =>
|
|
3256
|
+
this.#resolveLedgerRequest(request, normalizedPurpose);
|
|
3257
|
+
return Object.freeze(resolveReadOnly);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
export function createEvolutionLedgerArtifactResolver(ports, options) {
|
|
3262
|
+
rejectProxy(ports, "EvolutionArtifactPorts instance");
|
|
3263
|
+
if (
|
|
3264
|
+
!hasStablePrototype(
|
|
3265
|
+
ports,
|
|
3266
|
+
EvolutionArtifactPorts.prototype,
|
|
3267
|
+
"EvolutionArtifactPorts instance",
|
|
3268
|
+
)
|
|
3269
|
+
) {
|
|
3270
|
+
throw new TypeError(
|
|
3271
|
+
"createEvolutionLedgerArtifactResolver requires EvolutionArtifactPorts",
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
return ports.createEvolutionLedgerArtifactResolver(options);
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
Object.freeze(EvolutionArtifactPorts.prototype);
|