chainlesschain 0.166.15 → 0.166.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/completions/_cc +1 -1
- package/completions/cc.bash +1 -1
- package/completions/cc.fish +1 -1
- package/completions/cc.ps1 +1 -1
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-To-6rimH.js → AIOps-CVW2n2rh.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CpdCnTv3.js → ActionButton-DtuM163O.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DK7VNES_.js → Analytics-BXvl_qHu.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-NZ5FvhCx.js → AppLayout-VDBTFKOp.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-NXceQPmN.js → Artifacts-BNz2xCiG.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-B2d64DH2.js → Audit-DTu9Hn9P.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-ByYgvEVY.js → BackgroundAgents-D2OCPzLe.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BXvQLDvN.js → Backup-C9M-vG7G.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C4GZk-As.js → BaseInput-BUkCNiUQ.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CvPuohL8.js → Chat-OJ2ajF7l.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DTftcclX.js → ChatBubbleRenderer-BdZt76Ab.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-C8dQGIqD.js → Checkbox-C1gXKDsc.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DRM6S1W4.js → Codegen-BXbznLg7.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BDM8l-r5.js → Col-CbCq5N0i.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Bvmfi1fX.js → Community-V8Ks6Yun.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DGQTCima.js → Compact-DkeHQTpQ.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-D_0C-2P6.js → Compliance-JeLKwn4Q.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-3C_q99ok.js → Cowork-azfetcBz.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-CIiMQ1Hl.js → Cron-Q691i_Gp.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-peebGnES.js → Crosschain-BLjDRbr_.js} +1 -1
- package/src/assets/web-panel/assets/{DID-OkX4KB9T.js → DID-D8ptXplK.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-D9lm0Wxa.js → Dashboard-Ctj2VVZL.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CitActXx.js → Dropdown-D8VgkMJ8.js} +1 -1
- package/src/assets/web-panel/assets/EmailListRenderer-CQPe5S6q.js +1 -0
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CBWAi8XL.js → FamilyGuardDashboard-ySA-7SiN.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-afj8jDcp.js → Federation-1PMD-fBf.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Dabt59KY.js → FormItemContext-C_jr6U4Q.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BQpXA9kj.js → GenericCardRenderer-Bu3bKf2B.js} +1 -1
- package/src/assets/web-panel/assets/{Git-D2p2Moo1.js → Git-Dv4Fa_T5.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DkFfPykv.js → Governance-DOEy1n1k.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-C1DKpeN2.js → Inference-DkvqzWcJ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CjfVg4oQ.js → KnowledgeGraph-DdUJK949.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-D-zMvdSf.js → Logs-CRelcdUZ.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-jzEpGkbc.js → MarkdownRenderer-0AHbbTA4.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DfAjJLyS.js → Marketplace-BuuBKsev.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BGDHQES1.js → McpTools-DlgpEmA1.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-BYqrGAPS.js → Memory-DTfdqUGy.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CQ6FEtty.js → MobileBridge-CXLE5ZFO.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BpMvh5JW.js → MobileProjects-Dz25OB0n.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CWH25SrU.js → Mtc-kEL0fhgD.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-D6DiLmei.js → MtcAudit-BE4UvFJU.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-BSxRN4jD.js → Multisig-CSApSn8c.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-C0FDPKT4.js → NLProgramming-D11FpKWx.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-VKR78bH-.js → Notes-CDL-D2CS.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DLSklSZB.js → NotificationSettings-C0Q6lO-c.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-ZRE0OKbj.js +1 -0
- package/src/assets/web-panel/assets/{Organization-BB3pB27e.js → Organization-Dc-maVAq.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Cr7bzwcs.js → Overflow-B5mUpSvi.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D1fkMMzI.js → P2P-TjXV53wq.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-DvV2btIr.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-c3Ijv0v4.js → Permissions-DKRzQ2O6.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BJwbh3P2.js → PersonalDataHub-Dx2eqft8.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-C4fhXafB.js → Pipeline-CnYZMB5p.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DzSx3CD-.js → Privacy-BonKpPnp.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-D9fL5fED.js → ProjectInit-DWO0Du3J.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CB5FVrOR.js → ProjectSettings-C9jqyYRv.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DWhwdgfY.js → Projects-CYpKwXbz.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B9ZH4faS.js → Providers-CW-fkwJq.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BLKDHT8_.js → QrScannerModal-Cw0J3Zud.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D1oYUcSY.js → QuickAsk-BH4A4IpR.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CC2nmrOD.js → Recommend-CDwMwJus.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-qW9QF4-r.js → RemoteSession-DKIPwzTf.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BDWqfzcr.js → Reputation-CklDj7qS.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BBV2iCjT.js → Row-DJWZKsH0.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CcwrNwZB.js → RssFeed-BfSLCewo.js} +3 -3
- package/src/assets/web-panel/assets/{Search-Dr6fZ0D_.js → Search-B0IST--x.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BPzaoCU6.js → Security-h1sYNWtt.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BoKUU1I_.js → Services-Gh0-oxI_.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C8lT8GUg.js → Skeleton-CNDV_jOs.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DPpWeicz.js → Skills-ByCLTh1n.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-UiijzpMU.js → Sla-BfyrJTJz.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DoofrLwS.js → SpeechSettings-BQzo9kNX.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DXnyfTDw.js → SyncSettings-B6fS6z_1.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-g_WHjKeg.js → Tasks-C-uKCP-l.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DRuCh9g2.js → Templates-D6oDy7wL.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-QHIq1pH1.js → Tenant-D3LT2wNQ.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-G0fuI216.js → Terminal-BteLcv_O.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-4MT7SpGk.js → TimelineRenderer-DHHuxfqH.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-B5xT5umA.js → Tokens-BRQeykXw.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D7HPbNgc.js → Trigger-p2XaFWEu.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CxH69dU5.js → Trust-BxadBMft.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CPPosveH.js → UkeySign-DRBP7LDk.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Du5lGwAt.js → VideoEditing-CZ3NZkh7.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BBIGNESK.js → Wallet-C3NTBN7G.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Cc3HSGmv.js → WebAuthn-C7LVgMlb.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DBoVPNCh.js → WorkflowEditor-0_Iveog7.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DHcjgiem.js → chat-DzzluYo3.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DrRrvJIV.js → colors-DSMYXpaj.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BuY_sAoN.js → compact-item-W76u1VSV.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CepH1Fha.js → createContext-otsESjb5.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BztaXF84.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Dh80Ot6o.js → hasIn-3sNLrvug.js} +1 -1
- package/src/assets/web-panel/assets/index-4jP_hG_4.js +1 -0
- package/src/assets/web-panel/assets/{index-LBkSOwky.js → index-B1I1AYJs.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0rvJjId.js → index-B5LOYdYm.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6tOVlq-.js → index-B9X_jqXe.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7lstbTh.js → index-BA8mZxd-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPDEOOlA.js → index-BDlDO2px.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuSUCH7h.js → index-BJsQio5O.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEfVkKIY.js → index-BRsD1PT2.js} +1 -1
- package/src/assets/web-panel/assets/{index-CYydOga6.js → index-BXLx9HfA.js} +1 -1
- package/src/assets/web-panel/assets/{index-DkZP_QaB.js → index-BcGJtmHM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnVIvc15.js → index-BdtmEKat.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuRYBWrj.js → index-BpcQYzP7.js} +1 -1
- package/src/assets/web-panel/assets/{index-By0qqC3S.js → index-C2aht9-M.js} +1 -1
- package/src/assets/web-panel/assets/{index-C10033eO.js → index-CE7tSow-.js} +1 -1
- package/src/assets/web-panel/assets/{index-9crgJ3Id.js → index-CENWwjef.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNQK9DIr.js → index-CKoFyHYQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUNyTmkS.js → index-Cg86EzNp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnRK8_Dd.js → index-ClOFeZ-t.js} +1 -1
- package/src/assets/web-panel/assets/{index-BW9Pycxh.js → index-CtFOt5x1.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5_K3GKu.js → index-D7A5U2IH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRy3_beb.js → index-DH4cl52p.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWANFMlq.js → index-DLDh1OW3.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5vhNRqS.js → index-DMmdIpLA.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ca5d64IH.js → index-DS9WYDMf.js} +1 -1
- package/src/assets/web-panel/assets/{index-zGH9zH3w.js → index-DTA8Iavv.js} +3 -3
- package/src/assets/web-panel/assets/{index-BoRy2yup.js → index-DZKAzEge.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx21_p3m.js → index-DdU7IkKX.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9Gh-SOn.js → index-Ddd4mird.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiUx-tkV.js → index-DmpaC2yk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYN49m7k.js → index-GlIOE4ja.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUnd5hyp.js → index-MhQQ3h8q.js} +1 -1
- package/src/assets/web-panel/assets/{index-CW_23-gU.js → index-NaO9AE-F.js} +1 -1
- package/src/assets/web-panel/assets/{index-0TYLnd11.js → index-UUHCwfNq.js} +1 -1
- package/src/assets/web-panel/assets/{index-NS9dW1F1.js → index-pVm_iUre.js} +1 -1
- package/src/assets/web-panel/assets/{index-hYaigdGm.js → index-q7ftLqNt.js} +1 -1
- package/src/assets/web-panel/assets/{index-A5lVwYVq.js → index-uSCkbpZH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtqigRzf.js → index-vBKPcmAP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiZQooWK.js → index-vUmYkqmu.js} +1 -1
- package/src/assets/web-panel/assets/index-zxwuAXL5.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-nI_X3wKl.js → initDefaultProps-DF6llUXV.js} +1 -1
- package/src/assets/web-panel/assets/{motion-LUHbQu_R.js → motion-vtDV8_5K.js} +1 -1
- package/src/assets/web-panel/assets/{move-DHI_KvJt.js → move-B6c0bdy2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-AYdL_7Mo.js → mtc-parser-DlLYWuBK.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CtV3HRKa.js → omit-BH0zOxLN.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CcJLTOu4.js → pickAttrs-D9di8_In.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DHbiLUps.js → placementArrow-BDSngufM.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BC2DGkWP.js → responsiveObserve-C_EawMvP.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B4ga4pPY.js → slide-B_4wShxA.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-hzphgKfH.js → statusUtils-CbQu6qJ-.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-IfN9Bxoi.js → styleChecker-CvXJWYXb.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-pcGKJ4pT.js → useFlexGapSupport-jjY1XknO.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-COXhQpTG.js → useFs-CMXBdsK_.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-xp9F9szv.js → usePersonalDataHub-YbAhhPXx.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BdSQy1Ve.js → vnode-BFCTKc0H.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CHCt5P55.js → zoom-CY9aBl5O.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +2 -2
- package/src/command-manifest.json +2 -2
- package/src/commands/evolution.js +30 -17
- package/src/commands/learning.js +95 -9
- package/src/commands/team.js +5 -0
- package/src/data/changelog.json +28 -2
- package/src/lib/agent-team/task-lease.js +12 -1
- package/src/lib/capability-manifest.js +212 -1
- package/src/lib/evolution/bounded-skill-improvement-pilot.js +498 -0
- 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/evolution-run-runtime-adapter.js +5 -0
- package/src/lib/evolution/skill-candidate-registry.js +2258 -0
- package/src/lib/evolution/skill-evaluated-promotion-durability.js +899 -0
- package/src/lib/evolution/skill-evaluated-promotion.js +481 -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 +618 -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/evolution/wiki-informed-skill-proposer.js +343 -0
- package/src/lib/formal-quality-eval-runtime.js +100 -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/evolution-run-runtime-adapter.js +5 -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/hooks-v2-runtime.js +20 -0
- package/src/lib/learning/skill-improver.js +311 -43
- package/src/lib/learning/skill-synthesizer.js +236 -16
- package/src/lib/secure-fs.js +20 -0
- package/src/runtime/agent-core.js +68 -0
- package/src/assets/web-panel/assets/EmailListRenderer-DSmFkNKV.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Dpj7D6tC.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-BiDEbLNA.js +0 -7
- package/src/assets/web-panel/assets/devWarning-DIb8j04q.js +0 -1
- package/src/assets/web-panel/assets/index-BJc3t4PC.js +0 -1
- package/src/assets/web-panel/assets/index-CkksSUnI.js +0 -1
|
@@ -0,0 +1,1677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-closed, in-process authority for Skill candidate and active mutations.
|
|
3
|
+
*
|
|
4
|
+
* The authority deliberately does not accept a caller-selected role and does
|
|
5
|
+
* not return a serializable bearer token. Identity/role resolution and
|
|
6
|
+
* receipt authenticity are delegated to injected trusted ports. A successful
|
|
7
|
+
* authorization returns an empty opaque object whose authority exists only in
|
|
8
|
+
* this instance's WeakMap and which can be consumed exactly once. The returned
|
|
9
|
+
* consumption receipt is content-addressed audit evidence, not a standalone
|
|
10
|
+
* bearer credential; cross-process consumers must verify its audit head through
|
|
11
|
+
* the trusted ledger rather than trusting its public digest alone.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
import { digestSkillMutationDependencyLock as digestExecutionManifestDependencyLock } from "./skill-execution-manifest.js";
|
|
17
|
+
|
|
18
|
+
export const SKILL_MUTATION_REQUEST_SCHEMA =
|
|
19
|
+
"chainlesschain.skill-mutation-request/v3";
|
|
20
|
+
export const SKILL_MUTATION_CONSUME_SCHEMA =
|
|
21
|
+
"chainlesschain.skill-mutation-consume/v2";
|
|
22
|
+
export const SKILL_MUTATION_PRINCIPAL_SCHEMA =
|
|
23
|
+
"chainlesschain.skill-mutation-principal/v2";
|
|
24
|
+
export const SKILL_MUTATION_RECEIPT_VERIFICATION_SCHEMA =
|
|
25
|
+
"chainlesschain.skill-mutation-receipt-verification/v2";
|
|
26
|
+
export const SKILL_MUTATION_RECEIPT_BINDING_SCHEMA =
|
|
27
|
+
"chainlesschain.skill-mutation-receipt-binding/v2";
|
|
28
|
+
export const SKILL_MUTATION_AUDIT_SCHEMA =
|
|
29
|
+
"chainlesschain.skill-mutation-audit/v3";
|
|
30
|
+
export const SKILL_MUTATION_NONCE_CLAIM_SCHEMA =
|
|
31
|
+
"chainlesschain.skill-mutation-nonce-claim/v1";
|
|
32
|
+
export const SKILL_MUTATION_NONCE_ACK_SCHEMA =
|
|
33
|
+
"chainlesschain.skill-mutation-nonce-ack/v1";
|
|
34
|
+
export const SKILL_MUTATION_CONSUMPTION_RECEIPT_SCHEMA =
|
|
35
|
+
"chainlesschain.skill-mutation-consumption-receipt/v2";
|
|
36
|
+
export const SKILL_MUTATION_TRANSITION_SUBJECT_SCHEMA =
|
|
37
|
+
"chainlesschain.skill-mutation-transition-subject/v1";
|
|
38
|
+
|
|
39
|
+
export const SKILL_MUTATION_REQUEST_INVALID_CODE =
|
|
40
|
+
"CC_SKILL_MUTATION_REQUEST_INVALID";
|
|
41
|
+
export const SKILL_MUTATION_PRINCIPAL_INVALID_CODE =
|
|
42
|
+
"CC_SKILL_MUTATION_PRINCIPAL_INVALID";
|
|
43
|
+
export const SKILL_MUTATION_RECEIPT_INVALID_CODE =
|
|
44
|
+
"CC_SKILL_MUTATION_RECEIPT_INVALID";
|
|
45
|
+
export const SKILL_MUTATION_SCOPE_DENIED_CODE =
|
|
46
|
+
"CC_SKILL_MUTATION_SCOPE_DENIED";
|
|
47
|
+
export const SKILL_MUTATION_REQUEST_EXPIRED_CODE =
|
|
48
|
+
"CC_SKILL_MUTATION_REQUEST_EXPIRED";
|
|
49
|
+
export const SKILL_MUTATION_NONCE_REUSED_CODE =
|
|
50
|
+
"CC_SKILL_MUTATION_NONCE_REUSED";
|
|
51
|
+
export const SKILL_MUTATION_NONCE_STORE_FAILED_CODE =
|
|
52
|
+
"CC_SKILL_MUTATION_NONCE_STORE_FAILED";
|
|
53
|
+
export const SKILL_MUTATION_CAPABILITY_INVALID_CODE =
|
|
54
|
+
"CC_SKILL_MUTATION_CAPABILITY_INVALID";
|
|
55
|
+
export const SKILL_MUTATION_CAPABILITY_REPLAYED_CODE =
|
|
56
|
+
"CC_SKILL_MUTATION_CAPABILITY_REPLAYED";
|
|
57
|
+
export const SKILL_MUTATION_CAPABILITY_CONTEXT_MISMATCH_CODE =
|
|
58
|
+
"CC_SKILL_MUTATION_CAPABILITY_CONTEXT_MISMATCH";
|
|
59
|
+
export const SKILL_MUTATION_CAPABILITY_EXPIRED_CODE =
|
|
60
|
+
"CC_SKILL_MUTATION_CAPABILITY_EXPIRED";
|
|
61
|
+
export const SKILL_MUTATION_AUDIT_FAILED_CODE =
|
|
62
|
+
"CC_SKILL_MUTATION_AUDIT_FAILED";
|
|
63
|
+
|
|
64
|
+
export const SKILL_MUTATION_ROLES = Object.freeze({
|
|
65
|
+
PROMOTION_CONTROLLER: "promotion-controller",
|
|
66
|
+
CANDIDATE_WRITER: "candidate-writer",
|
|
67
|
+
PROPOSER: "proposer",
|
|
68
|
+
LEARNING: "learning",
|
|
69
|
+
SYNC: "sync",
|
|
70
|
+
MANUAL_IMPORT: "manual-import",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const SKILL_MUTATION_TARGET_SCOPES = Object.freeze({
|
|
74
|
+
CANDIDATE: "candidate",
|
|
75
|
+
ACTIVE: "active",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const SKILL_MUTATION_OPERATIONS = Object.freeze({
|
|
79
|
+
CREATE_CANDIDATE: "create-candidate",
|
|
80
|
+
PROMOTE: "promote",
|
|
81
|
+
ROLLBACK: "rollback",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const SKILL_MUTATION_RECEIPT_KINDS = Object.freeze([
|
|
85
|
+
"candidate",
|
|
86
|
+
"eval",
|
|
87
|
+
"policy",
|
|
88
|
+
"actor",
|
|
89
|
+
"parent",
|
|
90
|
+
"target",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const MAX_AUTHORIZATION_TTL_MS = 5 * 60 * 1000;
|
|
94
|
+
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
95
|
+
const SKILL_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
|
|
96
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
|
|
97
|
+
const NONCE_PATTERN = /^[A-Za-z0-9_-]{16,128}$/u;
|
|
98
|
+
const ROLE_VALUES = new Set(Object.values(SKILL_MUTATION_ROLES));
|
|
99
|
+
const TARGET_VALUES = new Set(Object.values(SKILL_MUTATION_TARGET_SCOPES));
|
|
100
|
+
const OPERATION_VALUES = new Set(Object.values(SKILL_MUTATION_OPERATIONS));
|
|
101
|
+
const CANDIDATE_ROLES = new Set([
|
|
102
|
+
SKILL_MUTATION_ROLES.CANDIDATE_WRITER,
|
|
103
|
+
SKILL_MUTATION_ROLES.PROPOSER,
|
|
104
|
+
SKILL_MUTATION_ROLES.LEARNING,
|
|
105
|
+
SKILL_MUTATION_ROLES.SYNC,
|
|
106
|
+
SKILL_MUTATION_ROLES.MANUAL_IMPORT,
|
|
107
|
+
]);
|
|
108
|
+
const RECEIPT_KEYS = new Set(
|
|
109
|
+
SKILL_MUTATION_RECEIPT_KINDS.map((kind) => `${kind}Receipt`),
|
|
110
|
+
);
|
|
111
|
+
const BINDING_KEYS = new Set(SKILL_MUTATION_RECEIPT_KINDS);
|
|
112
|
+
const REQUEST_INPUT_KEYS = new Set([
|
|
113
|
+
"tenantId",
|
|
114
|
+
"audience",
|
|
115
|
+
"operationId",
|
|
116
|
+
"operation",
|
|
117
|
+
"transitionSubjectDigest",
|
|
118
|
+
"skillName",
|
|
119
|
+
"targetScope",
|
|
120
|
+
"expectedTargetDigest",
|
|
121
|
+
"expectedTargetRevision",
|
|
122
|
+
"expiresAt",
|
|
123
|
+
"nonce",
|
|
124
|
+
"receipts",
|
|
125
|
+
]);
|
|
126
|
+
const REQUEST_KEYS = new Set([
|
|
127
|
+
"schema",
|
|
128
|
+
...REQUEST_INPUT_KEYS,
|
|
129
|
+
"requestDigest",
|
|
130
|
+
]);
|
|
131
|
+
const REQUEST_CONTEXT_KEYS = new Set([
|
|
132
|
+
"tenantId",
|
|
133
|
+
"audience",
|
|
134
|
+
"operationId",
|
|
135
|
+
"operation",
|
|
136
|
+
"transitionSubjectDigest",
|
|
137
|
+
"skillName",
|
|
138
|
+
"targetScope",
|
|
139
|
+
"expectedTargetDigest",
|
|
140
|
+
"expectedTargetRevision",
|
|
141
|
+
"expiresAt",
|
|
142
|
+
"nonce",
|
|
143
|
+
"requestDigest",
|
|
144
|
+
]);
|
|
145
|
+
const CONSUME_INPUT_KEYS = new Set([
|
|
146
|
+
"tenantId",
|
|
147
|
+
"audience",
|
|
148
|
+
"operationId",
|
|
149
|
+
"operation",
|
|
150
|
+
"transitionSubjectDigest",
|
|
151
|
+
"skillName",
|
|
152
|
+
"targetScope",
|
|
153
|
+
"expectedTargetDigest",
|
|
154
|
+
"expectedTargetRevision",
|
|
155
|
+
"expiresAt",
|
|
156
|
+
"nonce",
|
|
157
|
+
]);
|
|
158
|
+
const CONSUME_KEYS = new Set(["schema", ...CONSUME_INPUT_KEYS]);
|
|
159
|
+
const PRINCIPAL_KEYS = new Set([
|
|
160
|
+
"schema",
|
|
161
|
+
"authenticated",
|
|
162
|
+
"principalId",
|
|
163
|
+
"role",
|
|
164
|
+
"tenantId",
|
|
165
|
+
"audience",
|
|
166
|
+
"operationId",
|
|
167
|
+
"operation",
|
|
168
|
+
"transitionSubjectDigest",
|
|
169
|
+
"requestDigest",
|
|
170
|
+
"expiresAt",
|
|
171
|
+
]);
|
|
172
|
+
const VERIFICATION_KEYS = new Set(["schema", "verified", "bindings"]);
|
|
173
|
+
const RECEIPT_BINDING_KEYS = new Set([
|
|
174
|
+
"schema",
|
|
175
|
+
"kind",
|
|
176
|
+
"receiptDigest",
|
|
177
|
+
"principalId",
|
|
178
|
+
"role",
|
|
179
|
+
...REQUEST_CONTEXT_KEYS,
|
|
180
|
+
]);
|
|
181
|
+
const AUDIT_KEYS = new Set([
|
|
182
|
+
"schema",
|
|
183
|
+
"phase",
|
|
184
|
+
"decision",
|
|
185
|
+
"code",
|
|
186
|
+
"tenantId",
|
|
187
|
+
"audience",
|
|
188
|
+
"operationId",
|
|
189
|
+
"operation",
|
|
190
|
+
"transitionSubjectDigest",
|
|
191
|
+
"skillName",
|
|
192
|
+
"targetScope",
|
|
193
|
+
"expectedTargetDigest",
|
|
194
|
+
"expectedTargetRevision",
|
|
195
|
+
"expiresAt",
|
|
196
|
+
"nonce",
|
|
197
|
+
"principalId",
|
|
198
|
+
"role",
|
|
199
|
+
"requestDigest",
|
|
200
|
+
"occurredAt",
|
|
201
|
+
"auditDigest",
|
|
202
|
+
]);
|
|
203
|
+
const AUDIT_ACK_KEYS = new Set([
|
|
204
|
+
"persisted",
|
|
205
|
+
"auditDigest",
|
|
206
|
+
"headDigest",
|
|
207
|
+
"sequence",
|
|
208
|
+
]);
|
|
209
|
+
const NONCE_CLAIM_KEYS = new Set([
|
|
210
|
+
"schema",
|
|
211
|
+
"tenantId",
|
|
212
|
+
"audience",
|
|
213
|
+
"operationId",
|
|
214
|
+
"nonce",
|
|
215
|
+
"requestDigest",
|
|
216
|
+
"expiresAt",
|
|
217
|
+
"claimedAt",
|
|
218
|
+
"claimDigest",
|
|
219
|
+
]);
|
|
220
|
+
const NONCE_ACK_KEYS = new Set([
|
|
221
|
+
"schema",
|
|
222
|
+
"persisted",
|
|
223
|
+
"claimed",
|
|
224
|
+
"claimDigest",
|
|
225
|
+
"expiresAt",
|
|
226
|
+
"headDigest",
|
|
227
|
+
"sequence",
|
|
228
|
+
]);
|
|
229
|
+
const CONSUMPTION_RECEIPT_KEYS = new Set([
|
|
230
|
+
"schema",
|
|
231
|
+
"consumed",
|
|
232
|
+
"tenantId",
|
|
233
|
+
"audience",
|
|
234
|
+
"operationId",
|
|
235
|
+
"operation",
|
|
236
|
+
"transitionSubjectDigest",
|
|
237
|
+
"skillName",
|
|
238
|
+
"targetScope",
|
|
239
|
+
"expectedTargetDigest",
|
|
240
|
+
"expectedTargetRevision",
|
|
241
|
+
"expiresAt",
|
|
242
|
+
"nonce",
|
|
243
|
+
"principalId",
|
|
244
|
+
"role",
|
|
245
|
+
"requestDigest",
|
|
246
|
+
"contextDigest",
|
|
247
|
+
"occurredAt",
|
|
248
|
+
"auditDigest",
|
|
249
|
+
"headDigest",
|
|
250
|
+
"sequence",
|
|
251
|
+
"receiptDigest",
|
|
252
|
+
]);
|
|
253
|
+
const AUDIT_PHASES = new Set(["authorize", "consume"]);
|
|
254
|
+
const AUDIT_DECISIONS = new Set(["allow", "deny"]);
|
|
255
|
+
const AUDIT_CODES = new Set([
|
|
256
|
+
"CC_SKILL_MUTATION_AUTHORIZED",
|
|
257
|
+
"CC_SKILL_MUTATION_CONSUMED",
|
|
258
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
259
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
260
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
261
|
+
SKILL_MUTATION_SCOPE_DENIED_CODE,
|
|
262
|
+
SKILL_MUTATION_REQUEST_EXPIRED_CODE,
|
|
263
|
+
SKILL_MUTATION_NONCE_REUSED_CODE,
|
|
264
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
265
|
+
SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
266
|
+
SKILL_MUTATION_CAPABILITY_REPLAYED_CODE,
|
|
267
|
+
SKILL_MUTATION_CAPABILITY_CONTEXT_MISMATCH_CODE,
|
|
268
|
+
SKILL_MUTATION_CAPABILITY_EXPIRED_CODE,
|
|
269
|
+
]);
|
|
270
|
+
const AUTHORIZE_DENIAL_CODES = new Set([
|
|
271
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
272
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
273
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
274
|
+
SKILL_MUTATION_SCOPE_DENIED_CODE,
|
|
275
|
+
SKILL_MUTATION_REQUEST_EXPIRED_CODE,
|
|
276
|
+
SKILL_MUTATION_NONCE_REUSED_CODE,
|
|
277
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
278
|
+
]);
|
|
279
|
+
const CONSUME_DENIAL_CODES = new Set([
|
|
280
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
281
|
+
SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
282
|
+
SKILL_MUTATION_CAPABILITY_REPLAYED_CODE,
|
|
283
|
+
SKILL_MUTATION_CAPABILITY_CONTEXT_MISMATCH_CODE,
|
|
284
|
+
SKILL_MUTATION_CAPABILITY_EXPIRED_CODE,
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
export class SkillMutationAuthorityError extends Error {
|
|
288
|
+
constructor(code, message, details = {}) {
|
|
289
|
+
super(message, details.cause ? { cause: details.cause } : undefined);
|
|
290
|
+
this.name = "SkillMutationAuthorityError";
|
|
291
|
+
this.code = code;
|
|
292
|
+
for (const [key, value] of Object.entries(details)) {
|
|
293
|
+
if (key !== "cause") this[key] = value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function authorityError(code, message, details = {}) {
|
|
299
|
+
return new SkillMutationAuthorityError(code, message, details);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isPlainObject(value) {
|
|
303
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const prototype = Object.getPrototypeOf(value);
|
|
307
|
+
return prototype === Object.prototype || prototype === null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function assertExactDataRecord(
|
|
311
|
+
value,
|
|
312
|
+
expectedKeys,
|
|
313
|
+
label,
|
|
314
|
+
code = SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
315
|
+
) {
|
|
316
|
+
if (!isPlainObject(value)) {
|
|
317
|
+
throw authorityError(code, `${label} must be a plain object`);
|
|
318
|
+
}
|
|
319
|
+
const keys = Reflect.ownKeys(value);
|
|
320
|
+
if (
|
|
321
|
+
keys.length !== expectedKeys.size ||
|
|
322
|
+
keys.some((key) => typeof key !== "string" || !expectedKeys.has(key))
|
|
323
|
+
) {
|
|
324
|
+
throw authorityError(
|
|
325
|
+
code,
|
|
326
|
+
`${label} must contain exactly the supported fields`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
for (const key of keys) {
|
|
330
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
331
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
332
|
+
throw authorityError(
|
|
333
|
+
code,
|
|
334
|
+
`${label}.${String(key)} must be an enumerable own data property`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function canonicalJson(value) {
|
|
341
|
+
if (value === null || typeof value !== "object") {
|
|
342
|
+
return JSON.stringify(value);
|
|
343
|
+
}
|
|
344
|
+
if (Array.isArray(value)) {
|
|
345
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
346
|
+
}
|
|
347
|
+
return `{${Object.keys(value)
|
|
348
|
+
.sort()
|
|
349
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
350
|
+
.join(",")}}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function canonicalDigest(value, domain) {
|
|
354
|
+
return `sha256:${createHash("sha256")
|
|
355
|
+
.update(`${domain}\0${canonicalJson(value)}`, "utf8")
|
|
356
|
+
.digest("hex")}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function deepFreeze(value, seen = new WeakSet()) {
|
|
360
|
+
if (!value || typeof value !== "object" || seen.has(value)) return value;
|
|
361
|
+
seen.add(value);
|
|
362
|
+
for (const key of Reflect.ownKeys(value)) deepFreeze(value[key], seen);
|
|
363
|
+
return Object.freeze(value);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function normalizeBoundedString(value, label, maximum = 256) {
|
|
367
|
+
if (
|
|
368
|
+
typeof value !== "string" ||
|
|
369
|
+
value.length < 1 ||
|
|
370
|
+
value.length > maximum ||
|
|
371
|
+
value.trim() !== value ||
|
|
372
|
+
[...value].some((character) => {
|
|
373
|
+
const point = character.codePointAt(0);
|
|
374
|
+
return point < 0x20 || point === 0x7f;
|
|
375
|
+
})
|
|
376
|
+
) {
|
|
377
|
+
throw authorityError(
|
|
378
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
379
|
+
`${label} must be a non-empty bounded string without control characters`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function normalizeIdentifier(value, label) {
|
|
386
|
+
const normalized = normalizeBoundedString(value, label);
|
|
387
|
+
if (!IDENTIFIER_PATTERN.test(normalized)) {
|
|
388
|
+
throw authorityError(
|
|
389
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
390
|
+
`${label} is not a valid identifier`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return normalized;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function normalizeSkillName(value) {
|
|
397
|
+
const normalized = normalizeBoundedString(value, "skillName", 128);
|
|
398
|
+
if (!SKILL_NAME_PATTERN.test(normalized)) {
|
|
399
|
+
throw authorityError(
|
|
400
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
401
|
+
"skillName must use kebab-case",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return normalized;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function normalizeDigest(
|
|
408
|
+
value,
|
|
409
|
+
label,
|
|
410
|
+
code = SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
411
|
+
) {
|
|
412
|
+
if (typeof value !== "string" || !DIGEST_PATTERN.test(value)) {
|
|
413
|
+
throw authorityError(code, `${label} must be a lowercase sha256 digest`);
|
|
414
|
+
}
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function normalizeRevision(
|
|
419
|
+
value,
|
|
420
|
+
label,
|
|
421
|
+
code = SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
422
|
+
) {
|
|
423
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
424
|
+
throw authorityError(code, `${label} must be a non-negative safe integer`);
|
|
425
|
+
}
|
|
426
|
+
return value;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function normalizeIsoTimestamp(
|
|
430
|
+
value,
|
|
431
|
+
label,
|
|
432
|
+
code = SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
433
|
+
) {
|
|
434
|
+
if (typeof value !== "string") {
|
|
435
|
+
throw authorityError(code, `${label} must be an ISO timestamp`);
|
|
436
|
+
}
|
|
437
|
+
const parsed = new Date(value);
|
|
438
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {
|
|
439
|
+
throw authorityError(code, `${label} must be a canonical ISO timestamp`);
|
|
440
|
+
}
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function normalizeNonce(value) {
|
|
445
|
+
if (typeof value !== "string" || !NONCE_PATTERN.test(value)) {
|
|
446
|
+
throw authorityError(
|
|
447
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
448
|
+
"nonce must contain 16-128 URL-safe characters",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizeTargetScope(value) {
|
|
455
|
+
if (!TARGET_VALUES.has(value)) {
|
|
456
|
+
throw authorityError(
|
|
457
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
458
|
+
"targetScope must be candidate or active",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function normalizeOperation(value) {
|
|
465
|
+
if (!OPERATION_VALUES.has(value)) {
|
|
466
|
+
throw authorityError(
|
|
467
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
468
|
+
"operation must be create-candidate, promote, or rollback",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return value;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function assertOperationScope(operation, targetScope) {
|
|
475
|
+
const compatible =
|
|
476
|
+
(operation === SKILL_MUTATION_OPERATIONS.CREATE_CANDIDATE &&
|
|
477
|
+
targetScope === SKILL_MUTATION_TARGET_SCOPES.CANDIDATE) ||
|
|
478
|
+
([
|
|
479
|
+
SKILL_MUTATION_OPERATIONS.PROMOTE,
|
|
480
|
+
SKILL_MUTATION_OPERATIONS.ROLLBACK,
|
|
481
|
+
].includes(operation) &&
|
|
482
|
+
targetScope === SKILL_MUTATION_TARGET_SCOPES.ACTIVE);
|
|
483
|
+
if (!compatible) {
|
|
484
|
+
throw authorityError(
|
|
485
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
486
|
+
`operation ${operation} is incompatible with ${targetScope} scope`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Compatibility surface for mutation v3 callers. Canonicalization and hashing
|
|
493
|
+
* live in the execution-manifest module, while legacy callers retain this
|
|
494
|
+
* module's public error type and code.
|
|
495
|
+
*/
|
|
496
|
+
export function digestSkillMutationDependencyLock(value) {
|
|
497
|
+
try {
|
|
498
|
+
return digestExecutionManifestDependencyLock(value);
|
|
499
|
+
} catch (cause) {
|
|
500
|
+
if (cause?.code === SKILL_MUTATION_REQUEST_INVALID_CODE) {
|
|
501
|
+
throw authorityError(SKILL_MUTATION_REQUEST_INVALID_CODE, cause.message);
|
|
502
|
+
}
|
|
503
|
+
throw cause;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const TRANSITION_SUBJECT_INPUT_KEYS = new Set([
|
|
508
|
+
"tenantId",
|
|
509
|
+
"skillName",
|
|
510
|
+
"operation",
|
|
511
|
+
"candidateId",
|
|
512
|
+
"rollbackTargetReleaseDigest",
|
|
513
|
+
"dependencyLockDigest",
|
|
514
|
+
"expectedActiveContentDigest",
|
|
515
|
+
"expectedActiveRevision",
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
export function buildSkillMutationTransitionSubject(input) {
|
|
519
|
+
assertExactDataRecord(
|
|
520
|
+
input,
|
|
521
|
+
TRANSITION_SUBJECT_INPUT_KEYS,
|
|
522
|
+
"transition subject input",
|
|
523
|
+
);
|
|
524
|
+
const operation = normalizeOperation(input.operation);
|
|
525
|
+
const candidateId =
|
|
526
|
+
input.candidateId === null
|
|
527
|
+
? null
|
|
528
|
+
: normalizeDigest(input.candidateId, "candidateId");
|
|
529
|
+
const rollbackTargetReleaseDigest =
|
|
530
|
+
input.rollbackTargetReleaseDigest === null
|
|
531
|
+
? null
|
|
532
|
+
: normalizeDigest(
|
|
533
|
+
input.rollbackTargetReleaseDigest,
|
|
534
|
+
"rollbackTargetReleaseDigest",
|
|
535
|
+
);
|
|
536
|
+
const dependencyLockDigest =
|
|
537
|
+
input.dependencyLockDigest === null
|
|
538
|
+
? null
|
|
539
|
+
: normalizeDigest(input.dependencyLockDigest, "dependencyLockDigest");
|
|
540
|
+
if (
|
|
541
|
+
(operation === SKILL_MUTATION_OPERATIONS.CREATE_CANDIDATE &&
|
|
542
|
+
(candidateId === null ||
|
|
543
|
+
rollbackTargetReleaseDigest !== null ||
|
|
544
|
+
dependencyLockDigest !== null)) ||
|
|
545
|
+
(operation === SKILL_MUTATION_OPERATIONS.PROMOTE &&
|
|
546
|
+
(candidateId === null ||
|
|
547
|
+
rollbackTargetReleaseDigest !== null ||
|
|
548
|
+
dependencyLockDigest === null)) ||
|
|
549
|
+
(operation === SKILL_MUTATION_OPERATIONS.ROLLBACK &&
|
|
550
|
+
(candidateId !== null ||
|
|
551
|
+
rollbackTargetReleaseDigest === null ||
|
|
552
|
+
dependencyLockDigest === null))
|
|
553
|
+
) {
|
|
554
|
+
throw authorityError(
|
|
555
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
556
|
+
"transition subject target or dependency lock is incompatible with its operation",
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const core = {
|
|
560
|
+
schema: SKILL_MUTATION_TRANSITION_SUBJECT_SCHEMA,
|
|
561
|
+
tenantId: normalizeIdentifier(input.tenantId, "subject tenantId"),
|
|
562
|
+
skillName: normalizeSkillName(input.skillName),
|
|
563
|
+
operation,
|
|
564
|
+
candidateId,
|
|
565
|
+
rollbackTargetReleaseDigest,
|
|
566
|
+
dependencyLockDigest,
|
|
567
|
+
expectedActiveContentDigest: normalizeDigest(
|
|
568
|
+
input.expectedActiveContentDigest,
|
|
569
|
+
"expectedActiveContentDigest",
|
|
570
|
+
),
|
|
571
|
+
expectedActiveRevision: normalizeRevision(
|
|
572
|
+
input.expectedActiveRevision,
|
|
573
|
+
"expectedActiveRevision",
|
|
574
|
+
),
|
|
575
|
+
};
|
|
576
|
+
return deepFreeze({
|
|
577
|
+
...core,
|
|
578
|
+
transitionSubjectDigest: canonicalDigest(
|
|
579
|
+
core,
|
|
580
|
+
SKILL_MUTATION_TRANSITION_SUBJECT_SCHEMA,
|
|
581
|
+
),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function digestSkillMutationTransitionSubject(input) {
|
|
586
|
+
return buildSkillMutationTransitionSubject(input).transitionSubjectDigest;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function requiredReceiptKinds(targetScope) {
|
|
590
|
+
return targetScope === SKILL_MUTATION_TARGET_SCOPES.ACTIVE
|
|
591
|
+
? new Set(SKILL_MUTATION_RECEIPT_KINDS)
|
|
592
|
+
: new Set(["actor", "target"]);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function normalizeReceiptEnvelopes(value, targetScope) {
|
|
596
|
+
assertExactDataRecord(value, RECEIPT_KEYS, "mutation receipt envelopes");
|
|
597
|
+
const required = requiredReceiptKinds(targetScope);
|
|
598
|
+
const normalized = {};
|
|
599
|
+
for (const kind of SKILL_MUTATION_RECEIPT_KINDS) {
|
|
600
|
+
const key = `${kind}Receipt`;
|
|
601
|
+
const envelope = value[key];
|
|
602
|
+
if (required.has(kind)) {
|
|
603
|
+
normalized[key] = normalizeBoundedString(
|
|
604
|
+
envelope,
|
|
605
|
+
`${kind} receipt envelope`,
|
|
606
|
+
4096,
|
|
607
|
+
);
|
|
608
|
+
} else if (envelope === null) {
|
|
609
|
+
normalized[key] = null;
|
|
610
|
+
} else {
|
|
611
|
+
throw authorityError(
|
|
612
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
613
|
+
`candidate mutation must not carry a ${kind} receipt envelope`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return deepFreeze(normalized);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function requestCore(input) {
|
|
621
|
+
assertExactDataRecord(input, REQUEST_INPUT_KEYS, "mutation request input");
|
|
622
|
+
const targetScope = normalizeTargetScope(input.targetScope);
|
|
623
|
+
const operation = normalizeOperation(input.operation);
|
|
624
|
+
assertOperationScope(operation, targetScope);
|
|
625
|
+
return {
|
|
626
|
+
schema: SKILL_MUTATION_REQUEST_SCHEMA,
|
|
627
|
+
tenantId: normalizeIdentifier(input.tenantId, "tenantId"),
|
|
628
|
+
audience: normalizeIdentifier(input.audience, "audience"),
|
|
629
|
+
operationId: normalizeIdentifier(input.operationId, "operationId"),
|
|
630
|
+
operation,
|
|
631
|
+
transitionSubjectDigest: normalizeDigest(
|
|
632
|
+
input.transitionSubjectDigest,
|
|
633
|
+
"transitionSubjectDigest",
|
|
634
|
+
),
|
|
635
|
+
skillName: normalizeSkillName(input.skillName),
|
|
636
|
+
targetScope,
|
|
637
|
+
expectedTargetDigest: normalizeDigest(
|
|
638
|
+
input.expectedTargetDigest,
|
|
639
|
+
"expectedTargetDigest",
|
|
640
|
+
),
|
|
641
|
+
expectedTargetRevision: normalizeRevision(
|
|
642
|
+
input.expectedTargetRevision,
|
|
643
|
+
"expectedTargetRevision",
|
|
644
|
+
),
|
|
645
|
+
expiresAt: normalizeIsoTimestamp(input.expiresAt, "expiresAt"),
|
|
646
|
+
nonce: normalizeNonce(input.nonce),
|
|
647
|
+
receipts: normalizeReceiptEnvelopes(input.receipts, targetScope),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function buildSkillMutationRequest(input) {
|
|
652
|
+
const core = requestCore(input);
|
|
653
|
+
return deepFreeze({
|
|
654
|
+
...core,
|
|
655
|
+
requestDigest: canonicalDigest(
|
|
656
|
+
core,
|
|
657
|
+
"chainlesschain.skill-mutation-request/v3",
|
|
658
|
+
),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function verifySkillMutationRequest(value) {
|
|
663
|
+
assertExactDataRecord(value, REQUEST_KEYS, "mutation request");
|
|
664
|
+
if (value.schema !== SKILL_MUTATION_REQUEST_SCHEMA) {
|
|
665
|
+
throw authorityError(
|
|
666
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
667
|
+
"mutation request schema is invalid",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
const normalized = buildSkillMutationRequest({
|
|
671
|
+
tenantId: value.tenantId,
|
|
672
|
+
audience: value.audience,
|
|
673
|
+
operationId: value.operationId,
|
|
674
|
+
operation: value.operation,
|
|
675
|
+
transitionSubjectDigest: value.transitionSubjectDigest,
|
|
676
|
+
skillName: value.skillName,
|
|
677
|
+
targetScope: value.targetScope,
|
|
678
|
+
expectedTargetDigest: value.expectedTargetDigest,
|
|
679
|
+
expectedTargetRevision: value.expectedTargetRevision,
|
|
680
|
+
expiresAt: value.expiresAt,
|
|
681
|
+
nonce: value.nonce,
|
|
682
|
+
receipts: value.receipts,
|
|
683
|
+
});
|
|
684
|
+
if (normalized.requestDigest !== value.requestDigest) {
|
|
685
|
+
throw authorityError(
|
|
686
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
687
|
+
"mutation request digest verification failed",
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
return normalized;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function consumeCore(input) {
|
|
694
|
+
assertExactDataRecord(input, CONSUME_INPUT_KEYS, "mutation consume input");
|
|
695
|
+
const targetScope = normalizeTargetScope(input.targetScope);
|
|
696
|
+
const operation = normalizeOperation(input.operation);
|
|
697
|
+
assertOperationScope(operation, targetScope);
|
|
698
|
+
return {
|
|
699
|
+
schema: SKILL_MUTATION_CONSUME_SCHEMA,
|
|
700
|
+
tenantId: normalizeIdentifier(input.tenantId, "tenantId"),
|
|
701
|
+
audience: normalizeIdentifier(input.audience, "audience"),
|
|
702
|
+
operationId: normalizeIdentifier(input.operationId, "operationId"),
|
|
703
|
+
operation,
|
|
704
|
+
transitionSubjectDigest: normalizeDigest(
|
|
705
|
+
input.transitionSubjectDigest,
|
|
706
|
+
"transitionSubjectDigest",
|
|
707
|
+
),
|
|
708
|
+
skillName: normalizeSkillName(input.skillName),
|
|
709
|
+
targetScope,
|
|
710
|
+
expectedTargetDigest: normalizeDigest(
|
|
711
|
+
input.expectedTargetDigest,
|
|
712
|
+
"expectedTargetDigest",
|
|
713
|
+
),
|
|
714
|
+
expectedTargetRevision: normalizeRevision(
|
|
715
|
+
input.expectedTargetRevision,
|
|
716
|
+
"expectedTargetRevision",
|
|
717
|
+
),
|
|
718
|
+
expiresAt: normalizeIsoTimestamp(input.expiresAt, "expiresAt"),
|
|
719
|
+
nonce: normalizeNonce(input.nonce),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function buildSkillMutationConsumeContext(input) {
|
|
724
|
+
return deepFreeze(consumeCore(input));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function verifyConsumeContext(value) {
|
|
728
|
+
assertExactDataRecord(value, CONSUME_KEYS, "mutation consume context");
|
|
729
|
+
if (value.schema !== SKILL_MUTATION_CONSUME_SCHEMA) {
|
|
730
|
+
throw authorityError(
|
|
731
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
732
|
+
"mutation consume context schema is invalid",
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
return buildSkillMutationConsumeContext({
|
|
736
|
+
tenantId: value.tenantId,
|
|
737
|
+
audience: value.audience,
|
|
738
|
+
operationId: value.operationId,
|
|
739
|
+
operation: value.operation,
|
|
740
|
+
transitionSubjectDigest: value.transitionSubjectDigest,
|
|
741
|
+
skillName: value.skillName,
|
|
742
|
+
targetScope: value.targetScope,
|
|
743
|
+
expectedTargetDigest: value.expectedTargetDigest,
|
|
744
|
+
expectedTargetRevision: value.expectedTargetRevision,
|
|
745
|
+
expiresAt: value.expiresAt,
|
|
746
|
+
nonce: value.nonce,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function requestContext(request) {
|
|
751
|
+
return deepFreeze({
|
|
752
|
+
tenantId: request.tenantId,
|
|
753
|
+
audience: request.audience,
|
|
754
|
+
operationId: request.operationId,
|
|
755
|
+
operation: request.operation,
|
|
756
|
+
transitionSubjectDigest: request.transitionSubjectDigest,
|
|
757
|
+
skillName: request.skillName,
|
|
758
|
+
targetScope: request.targetScope,
|
|
759
|
+
expectedTargetDigest: request.expectedTargetDigest,
|
|
760
|
+
expectedTargetRevision: request.expectedTargetRevision,
|
|
761
|
+
expiresAt: request.expiresAt,
|
|
762
|
+
nonce: request.nonce,
|
|
763
|
+
requestDigest: request.requestDigest,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function buildNonceClaim(request, claimedAt) {
|
|
768
|
+
const core = {
|
|
769
|
+
schema: SKILL_MUTATION_NONCE_CLAIM_SCHEMA,
|
|
770
|
+
tenantId: request.tenantId,
|
|
771
|
+
audience: request.audience,
|
|
772
|
+
operationId: request.operationId,
|
|
773
|
+
nonce: request.nonce,
|
|
774
|
+
requestDigest: request.requestDigest,
|
|
775
|
+
expiresAt: request.expiresAt,
|
|
776
|
+
claimedAt,
|
|
777
|
+
};
|
|
778
|
+
return deepFreeze({
|
|
779
|
+
...core,
|
|
780
|
+
claimDigest: canonicalDigest(
|
|
781
|
+
core,
|
|
782
|
+
"chainlesschain.skill-mutation-nonce-claim/v1",
|
|
783
|
+
),
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function verifySkillMutationNonceClaim(value) {
|
|
788
|
+
assertExactDataRecord(
|
|
789
|
+
value,
|
|
790
|
+
NONCE_CLAIM_KEYS,
|
|
791
|
+
"mutation nonce claim",
|
|
792
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
793
|
+
);
|
|
794
|
+
const core = { ...value };
|
|
795
|
+
delete core.claimDigest;
|
|
796
|
+
if (
|
|
797
|
+
core.schema !== SKILL_MUTATION_NONCE_CLAIM_SCHEMA ||
|
|
798
|
+
normalizeIdentifier(core.tenantId, "nonce claim tenantId") !==
|
|
799
|
+
core.tenantId ||
|
|
800
|
+
normalizeIdentifier(core.audience, "nonce claim audience") !==
|
|
801
|
+
core.audience ||
|
|
802
|
+
normalizeIdentifier(core.operationId, "nonce claim operationId") !==
|
|
803
|
+
core.operationId ||
|
|
804
|
+
normalizeNonce(core.nonce) !== core.nonce ||
|
|
805
|
+
normalizeDigest(
|
|
806
|
+
core.requestDigest,
|
|
807
|
+
"nonce claim requestDigest",
|
|
808
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
809
|
+
) !== core.requestDigest ||
|
|
810
|
+
normalizeIsoTimestamp(
|
|
811
|
+
core.expiresAt,
|
|
812
|
+
"nonce claim expiresAt",
|
|
813
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
814
|
+
) !== core.expiresAt ||
|
|
815
|
+
normalizeIsoTimestamp(
|
|
816
|
+
core.claimedAt,
|
|
817
|
+
"nonce claim claimedAt",
|
|
818
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
819
|
+
) !== core.claimedAt ||
|
|
820
|
+
normalizeDigest(
|
|
821
|
+
value.claimDigest,
|
|
822
|
+
"nonce claim digest",
|
|
823
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
824
|
+
) !== canonicalDigest(core, "chainlesschain.skill-mutation-nonce-claim/v1")
|
|
825
|
+
) {
|
|
826
|
+
throw authorityError(
|
|
827
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
828
|
+
"mutation nonce claim is invalid",
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
return deepFreeze({ ...core, claimDigest: value.claimDigest });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function normalizePrincipal(value, context) {
|
|
835
|
+
assertExactDataRecord(
|
|
836
|
+
value,
|
|
837
|
+
PRINCIPAL_KEYS,
|
|
838
|
+
"resolved mutation principal",
|
|
839
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
840
|
+
);
|
|
841
|
+
if (
|
|
842
|
+
value.schema !== SKILL_MUTATION_PRINCIPAL_SCHEMA ||
|
|
843
|
+
value.authenticated !== true ||
|
|
844
|
+
!ROLE_VALUES.has(value.role)
|
|
845
|
+
) {
|
|
846
|
+
throw authorityError(
|
|
847
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
848
|
+
"resolved mutation principal is not authenticated or has an invalid role",
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
let normalized;
|
|
852
|
+
try {
|
|
853
|
+
normalized = {
|
|
854
|
+
schema: SKILL_MUTATION_PRINCIPAL_SCHEMA,
|
|
855
|
+
authenticated: true,
|
|
856
|
+
principalId: normalizeIdentifier(value.principalId, "principalId"),
|
|
857
|
+
role: value.role,
|
|
858
|
+
tenantId: normalizeIdentifier(value.tenantId, "principal tenantId"),
|
|
859
|
+
audience: normalizeIdentifier(value.audience, "principal audience"),
|
|
860
|
+
operationId: normalizeIdentifier(
|
|
861
|
+
value.operationId,
|
|
862
|
+
"principal operationId",
|
|
863
|
+
),
|
|
864
|
+
operation: normalizeOperation(value.operation),
|
|
865
|
+
transitionSubjectDigest: normalizeDigest(
|
|
866
|
+
value.transitionSubjectDigest,
|
|
867
|
+
"principal transitionSubjectDigest",
|
|
868
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
869
|
+
),
|
|
870
|
+
requestDigest: normalizeDigest(
|
|
871
|
+
value.requestDigest,
|
|
872
|
+
"principal requestDigest",
|
|
873
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
874
|
+
),
|
|
875
|
+
expiresAt: normalizeIsoTimestamp(
|
|
876
|
+
value.expiresAt,
|
|
877
|
+
"principal expiresAt",
|
|
878
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
879
|
+
),
|
|
880
|
+
};
|
|
881
|
+
} catch (cause) {
|
|
882
|
+
throw authorityError(
|
|
883
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
884
|
+
"resolved mutation principal contains invalid claims",
|
|
885
|
+
{ cause },
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
for (const field of [
|
|
889
|
+
"tenantId",
|
|
890
|
+
"audience",
|
|
891
|
+
"operationId",
|
|
892
|
+
"operation",
|
|
893
|
+
"transitionSubjectDigest",
|
|
894
|
+
"requestDigest",
|
|
895
|
+
"expiresAt",
|
|
896
|
+
]) {
|
|
897
|
+
if (normalized[field] !== context[field]) {
|
|
898
|
+
throw authorityError(
|
|
899
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
900
|
+
`resolved principal is not bound to request ${field}`,
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return deepFreeze(normalized);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function roleCanMutateTarget(role, targetScope) {
|
|
908
|
+
return (
|
|
909
|
+
(targetScope === SKILL_MUTATION_TARGET_SCOPES.CANDIDATE &&
|
|
910
|
+
CANDIDATE_ROLES.has(role)) ||
|
|
911
|
+
(targetScope === SKILL_MUTATION_TARGET_SCOPES.ACTIVE &&
|
|
912
|
+
role === SKILL_MUTATION_ROLES.PROMOTION_CONTROLLER)
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
export function digestSkillMutationReceiptEnvelope(envelope) {
|
|
917
|
+
const normalized = normalizeBoundedString(envelope, "receipt envelope", 4096);
|
|
918
|
+
return canonicalDigest(
|
|
919
|
+
normalized,
|
|
920
|
+
"chainlesschain.skill-mutation-receipt-envelope/v1",
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function validateReceiptBinding(value, kind, request, principal, envelope) {
|
|
925
|
+
assertExactDataRecord(
|
|
926
|
+
value,
|
|
927
|
+
RECEIPT_BINDING_KEYS,
|
|
928
|
+
`${kind} receipt binding`,
|
|
929
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
930
|
+
);
|
|
931
|
+
if (
|
|
932
|
+
value.schema !== SKILL_MUTATION_RECEIPT_BINDING_SCHEMA ||
|
|
933
|
+
value.kind !== kind ||
|
|
934
|
+
value.receiptDigest !== digestSkillMutationReceiptEnvelope(envelope) ||
|
|
935
|
+
value.principalId !== principal.principalId ||
|
|
936
|
+
value.role !== principal.role
|
|
937
|
+
) {
|
|
938
|
+
throw authorityError(
|
|
939
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
940
|
+
`${kind} receipt is not authenticated for the resolved principal`,
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const context = requestContext(request);
|
|
944
|
+
for (const key of REQUEST_CONTEXT_KEYS) {
|
|
945
|
+
if (value[key] !== context[key]) {
|
|
946
|
+
throw authorityError(
|
|
947
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
948
|
+
`${kind} receipt is not bound to request ${key}`,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
normalizeDigest(
|
|
953
|
+
value.receiptDigest,
|
|
954
|
+
`${kind} receipt digest`,
|
|
955
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function normalizeReceiptVerification(value, request, principal) {
|
|
960
|
+
assertExactDataRecord(
|
|
961
|
+
value,
|
|
962
|
+
VERIFICATION_KEYS,
|
|
963
|
+
"receipt verification",
|
|
964
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
965
|
+
);
|
|
966
|
+
if (
|
|
967
|
+
value.schema !== SKILL_MUTATION_RECEIPT_VERIFICATION_SCHEMA ||
|
|
968
|
+
value.verified !== true
|
|
969
|
+
) {
|
|
970
|
+
throw authorityError(
|
|
971
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
972
|
+
"receipt verifier did not return a verified result",
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
assertExactDataRecord(
|
|
976
|
+
value.bindings,
|
|
977
|
+
BINDING_KEYS,
|
|
978
|
+
"receipt verification bindings",
|
|
979
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
980
|
+
);
|
|
981
|
+
const required = requiredReceiptKinds(request.targetScope);
|
|
982
|
+
for (const kind of SKILL_MUTATION_RECEIPT_KINDS) {
|
|
983
|
+
const envelope = request.receipts[`${kind}Receipt`];
|
|
984
|
+
const binding = value.bindings[kind];
|
|
985
|
+
if (!required.has(kind)) {
|
|
986
|
+
if (binding !== null) {
|
|
987
|
+
throw authorityError(
|
|
988
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
989
|
+
`unused ${kind} receipt binding must be null`,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
validateReceiptBinding(binding, kind, request, principal, envelope);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function safeOwnValue(value, key) {
|
|
999
|
+
try {
|
|
1000
|
+
if (!value || typeof value !== "object") return null;
|
|
1001
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1002
|
+
return descriptor && "value" in descriptor ? descriptor.value : null;
|
|
1003
|
+
} catch {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function safeAuditContext(value) {
|
|
1009
|
+
const revision = safeOwnValue(value, "expectedTargetRevision");
|
|
1010
|
+
const ownString = (key, maximum) => {
|
|
1011
|
+
const candidate = safeOwnValue(value, key);
|
|
1012
|
+
return typeof candidate === "string" ? candidate.slice(0, maximum) : null;
|
|
1013
|
+
};
|
|
1014
|
+
const digest = ownString("expectedTargetDigest", 71);
|
|
1015
|
+
const transitionSubjectDigest = ownString("transitionSubjectDigest", 71);
|
|
1016
|
+
const requestDigest = ownString("requestDigest", 71);
|
|
1017
|
+
return {
|
|
1018
|
+
tenantId: ownString("tenantId", 256),
|
|
1019
|
+
audience: ownString("audience", 256),
|
|
1020
|
+
operationId: ownString("operationId", 256),
|
|
1021
|
+
operation: OPERATION_VALUES.has(safeOwnValue(value, "operation"))
|
|
1022
|
+
? safeOwnValue(value, "operation")
|
|
1023
|
+
: null,
|
|
1024
|
+
transitionSubjectDigest:
|
|
1025
|
+
transitionSubjectDigest && DIGEST_PATTERN.test(transitionSubjectDigest)
|
|
1026
|
+
? transitionSubjectDigest
|
|
1027
|
+
: null,
|
|
1028
|
+
skillName: ownString("skillName", 128),
|
|
1029
|
+
targetScope: TARGET_VALUES.has(safeOwnValue(value, "targetScope"))
|
|
1030
|
+
? safeOwnValue(value, "targetScope")
|
|
1031
|
+
: null,
|
|
1032
|
+
expectedTargetDigest: digest && DIGEST_PATTERN.test(digest) ? digest : null,
|
|
1033
|
+
expectedTargetRevision:
|
|
1034
|
+
Number.isSafeInteger(revision) && revision >= 0 ? revision : null,
|
|
1035
|
+
expiresAt: ownString("expiresAt", 64),
|
|
1036
|
+
nonce: ownString("nonce", 128),
|
|
1037
|
+
requestDigest:
|
|
1038
|
+
requestDigest && DIGEST_PATTERN.test(requestDigest)
|
|
1039
|
+
? requestDigest
|
|
1040
|
+
: null,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function buildAuditEvent({
|
|
1045
|
+
phase,
|
|
1046
|
+
decision,
|
|
1047
|
+
code,
|
|
1048
|
+
context,
|
|
1049
|
+
principal,
|
|
1050
|
+
occurredAt,
|
|
1051
|
+
}) {
|
|
1052
|
+
const core = {
|
|
1053
|
+
schema: SKILL_MUTATION_AUDIT_SCHEMA,
|
|
1054
|
+
phase,
|
|
1055
|
+
decision,
|
|
1056
|
+
code,
|
|
1057
|
+
tenantId: context.tenantId,
|
|
1058
|
+
audience: context.audience,
|
|
1059
|
+
operationId: context.operationId,
|
|
1060
|
+
operation: context.operation,
|
|
1061
|
+
transitionSubjectDigest: context.transitionSubjectDigest,
|
|
1062
|
+
skillName: context.skillName,
|
|
1063
|
+
targetScope: context.targetScope,
|
|
1064
|
+
expectedTargetDigest: context.expectedTargetDigest,
|
|
1065
|
+
expectedTargetRevision: context.expectedTargetRevision,
|
|
1066
|
+
expiresAt: context.expiresAt,
|
|
1067
|
+
nonce: context.nonce,
|
|
1068
|
+
principalId: principal?.principalId || null,
|
|
1069
|
+
role: principal?.role || null,
|
|
1070
|
+
requestDigest: context.requestDigest,
|
|
1071
|
+
occurredAt,
|
|
1072
|
+
};
|
|
1073
|
+
return deepFreeze({
|
|
1074
|
+
...core,
|
|
1075
|
+
auditDigest: canonicalDigest(
|
|
1076
|
+
core,
|
|
1077
|
+
"chainlesschain.skill-mutation-audit/v3",
|
|
1078
|
+
),
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
export function verifySkillMutationAuditEvent(value) {
|
|
1083
|
+
assertExactDataRecord(value, AUDIT_KEYS, "mutation audit event");
|
|
1084
|
+
const core = { ...value };
|
|
1085
|
+
delete core.auditDigest;
|
|
1086
|
+
const expectedAllowCode =
|
|
1087
|
+
core.phase === "authorize"
|
|
1088
|
+
? "CC_SKILL_MUTATION_AUTHORIZED"
|
|
1089
|
+
: "CC_SKILL_MUTATION_CONSUMED";
|
|
1090
|
+
const denialCodes =
|
|
1091
|
+
core.phase === "authorize" ? AUTHORIZE_DENIAL_CODES : CONSUME_DENIAL_CODES;
|
|
1092
|
+
if (
|
|
1093
|
+
core.schema !== SKILL_MUTATION_AUDIT_SCHEMA ||
|
|
1094
|
+
!AUDIT_PHASES.has(core.phase) ||
|
|
1095
|
+
!AUDIT_DECISIONS.has(core.decision) ||
|
|
1096
|
+
!AUDIT_CODES.has(core.code) ||
|
|
1097
|
+
(core.decision === "allow" && core.code !== expectedAllowCode) ||
|
|
1098
|
+
(core.decision === "deny" && !denialCodes.has(core.code)) ||
|
|
1099
|
+
(core.role !== null && !ROLE_VALUES.has(core.role)) ||
|
|
1100
|
+
(core.principalId !== null && typeof core.principalId !== "string") ||
|
|
1101
|
+
(core.requestDigest !== null && !DIGEST_PATTERN.test(core.requestDigest)) ||
|
|
1102
|
+
normalizeIsoTimestamp(
|
|
1103
|
+
core.occurredAt,
|
|
1104
|
+
"audit occurredAt",
|
|
1105
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
1106
|
+
) !== core.occurredAt ||
|
|
1107
|
+
canonicalDigest(core, "chainlesschain.skill-mutation-audit/v3") !==
|
|
1108
|
+
value.auditDigest
|
|
1109
|
+
) {
|
|
1110
|
+
throw authorityError(
|
|
1111
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
1112
|
+
"mutation audit event is invalid",
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
if (core.decision === "allow") {
|
|
1116
|
+
try {
|
|
1117
|
+
normalizeIdentifier(core.tenantId, "audit tenantId");
|
|
1118
|
+
normalizeIdentifier(core.audience, "audit audience");
|
|
1119
|
+
normalizeIdentifier(core.operationId, "audit operationId");
|
|
1120
|
+
const operation = normalizeOperation(core.operation);
|
|
1121
|
+
normalizeDigest(
|
|
1122
|
+
core.transitionSubjectDigest,
|
|
1123
|
+
"audit transitionSubjectDigest",
|
|
1124
|
+
);
|
|
1125
|
+
normalizeSkillName(core.skillName);
|
|
1126
|
+
const targetScope = normalizeTargetScope(core.targetScope);
|
|
1127
|
+
assertOperationScope(operation, targetScope);
|
|
1128
|
+
normalizeDigest(core.expectedTargetDigest, "audit target digest");
|
|
1129
|
+
normalizeRevision(core.expectedTargetRevision, "audit target revision");
|
|
1130
|
+
normalizeIsoTimestamp(core.expiresAt, "audit expiresAt");
|
|
1131
|
+
normalizeNonce(core.nonce);
|
|
1132
|
+
normalizeIdentifier(core.principalId, "audit principalId");
|
|
1133
|
+
normalizeDigest(core.requestDigest, "audit requestDigest");
|
|
1134
|
+
if (!roleCanMutateTarget(core.role, core.targetScope)) {
|
|
1135
|
+
throw authorityError(
|
|
1136
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
1137
|
+
"audit role cannot mutate target scope",
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
} catch (cause) {
|
|
1141
|
+
throw authorityError(
|
|
1142
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
1143
|
+
"allow audit event must contain a complete authorized context",
|
|
1144
|
+
{ cause },
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return deepFreeze({ ...core, auditDigest: value.auditDigest });
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function buildConsumptionReceipt(context, principal, clock, audit) {
|
|
1152
|
+
const contextDigest = canonicalDigest(
|
|
1153
|
+
context,
|
|
1154
|
+
"chainlesschain.skill-mutation-consume-context/v2",
|
|
1155
|
+
);
|
|
1156
|
+
const core = {
|
|
1157
|
+
schema: SKILL_MUTATION_CONSUMPTION_RECEIPT_SCHEMA,
|
|
1158
|
+
consumed: true,
|
|
1159
|
+
tenantId: context.tenantId,
|
|
1160
|
+
audience: context.audience,
|
|
1161
|
+
operationId: context.operationId,
|
|
1162
|
+
operation: context.operation,
|
|
1163
|
+
transitionSubjectDigest: context.transitionSubjectDigest,
|
|
1164
|
+
skillName: context.skillName,
|
|
1165
|
+
targetScope: context.targetScope,
|
|
1166
|
+
expectedTargetDigest: context.expectedTargetDigest,
|
|
1167
|
+
expectedTargetRevision: context.expectedTargetRevision,
|
|
1168
|
+
expiresAt: context.expiresAt,
|
|
1169
|
+
nonce: context.nonce,
|
|
1170
|
+
principalId: principal.principalId,
|
|
1171
|
+
role: principal.role,
|
|
1172
|
+
requestDigest: context.requestDigest,
|
|
1173
|
+
contextDigest,
|
|
1174
|
+
occurredAt: clock.iso,
|
|
1175
|
+
auditDigest: audit.event.auditDigest,
|
|
1176
|
+
headDigest: audit.acknowledgement.headDigest,
|
|
1177
|
+
sequence: audit.acknowledgement.sequence,
|
|
1178
|
+
};
|
|
1179
|
+
return deepFreeze({
|
|
1180
|
+
...core,
|
|
1181
|
+
receiptDigest: canonicalDigest(
|
|
1182
|
+
core,
|
|
1183
|
+
"chainlesschain.skill-mutation-consumption-receipt/v2",
|
|
1184
|
+
),
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
export function verifySkillMutationConsumptionReceipt(value) {
|
|
1189
|
+
assertExactDataRecord(
|
|
1190
|
+
value,
|
|
1191
|
+
CONSUMPTION_RECEIPT_KEYS,
|
|
1192
|
+
"mutation consumption receipt",
|
|
1193
|
+
SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
1194
|
+
);
|
|
1195
|
+
const core = { ...value };
|
|
1196
|
+
delete core.receiptDigest;
|
|
1197
|
+
try {
|
|
1198
|
+
if (
|
|
1199
|
+
core.schema !== SKILL_MUTATION_CONSUMPTION_RECEIPT_SCHEMA ||
|
|
1200
|
+
core.consumed !== true ||
|
|
1201
|
+
normalizeIdentifier(core.tenantId, "receipt tenantId") !==
|
|
1202
|
+
core.tenantId ||
|
|
1203
|
+
normalizeIdentifier(core.audience, "receipt audience") !==
|
|
1204
|
+
core.audience ||
|
|
1205
|
+
normalizeIdentifier(core.operationId, "receipt operationId") !==
|
|
1206
|
+
core.operationId ||
|
|
1207
|
+
normalizeOperation(core.operation) !== core.operation ||
|
|
1208
|
+
normalizeDigest(
|
|
1209
|
+
core.transitionSubjectDigest,
|
|
1210
|
+
"receipt transitionSubjectDigest",
|
|
1211
|
+
) !== core.transitionSubjectDigest ||
|
|
1212
|
+
normalizeSkillName(core.skillName) !== core.skillName ||
|
|
1213
|
+
normalizeTargetScope(core.targetScope) !== core.targetScope ||
|
|
1214
|
+
normalizeDigest(core.expectedTargetDigest, "receipt target digest") !==
|
|
1215
|
+
core.expectedTargetDigest ||
|
|
1216
|
+
normalizeRevision(core.expectedTargetRevision, "receipt revision") !==
|
|
1217
|
+
core.expectedTargetRevision ||
|
|
1218
|
+
normalizeIsoTimestamp(core.expiresAt, "receipt expiresAt") !==
|
|
1219
|
+
core.expiresAt ||
|
|
1220
|
+
normalizeNonce(core.nonce) !== core.nonce ||
|
|
1221
|
+
normalizeIdentifier(core.principalId, "receipt principalId") !==
|
|
1222
|
+
core.principalId ||
|
|
1223
|
+
!ROLE_VALUES.has(core.role) ||
|
|
1224
|
+
!roleCanMutateTarget(core.role, core.targetScope) ||
|
|
1225
|
+
normalizeDigest(core.requestDigest, "receipt requestDigest") !==
|
|
1226
|
+
core.requestDigest ||
|
|
1227
|
+
normalizeDigest(core.contextDigest, "receipt contextDigest") !==
|
|
1228
|
+
core.contextDigest ||
|
|
1229
|
+
normalizeIsoTimestamp(core.occurredAt, "receipt occurredAt") !==
|
|
1230
|
+
core.occurredAt ||
|
|
1231
|
+
normalizeDigest(core.auditDigest, "receipt auditDigest") !==
|
|
1232
|
+
core.auditDigest ||
|
|
1233
|
+
normalizeDigest(core.headDigest, "receipt headDigest") !==
|
|
1234
|
+
core.headDigest ||
|
|
1235
|
+
normalizeRevision(core.sequence, "receipt sequence") !== core.sequence ||
|
|
1236
|
+
core.sequence < 1 ||
|
|
1237
|
+
normalizeDigest(value.receiptDigest, "receiptDigest") !==
|
|
1238
|
+
canonicalDigest(
|
|
1239
|
+
core,
|
|
1240
|
+
"chainlesschain.skill-mutation-consumption-receipt/v2",
|
|
1241
|
+
)
|
|
1242
|
+
) {
|
|
1243
|
+
throw authorityError(
|
|
1244
|
+
SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
1245
|
+
"mutation consumption receipt is invalid",
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
assertOperationScope(core.operation, core.targetScope);
|
|
1249
|
+
const context = {
|
|
1250
|
+
tenantId: core.tenantId,
|
|
1251
|
+
audience: core.audience,
|
|
1252
|
+
operationId: core.operationId,
|
|
1253
|
+
operation: core.operation,
|
|
1254
|
+
transitionSubjectDigest: core.transitionSubjectDigest,
|
|
1255
|
+
skillName: core.skillName,
|
|
1256
|
+
targetScope: core.targetScope,
|
|
1257
|
+
expectedTargetDigest: core.expectedTargetDigest,
|
|
1258
|
+
expectedTargetRevision: core.expectedTargetRevision,
|
|
1259
|
+
expiresAt: core.expiresAt,
|
|
1260
|
+
nonce: core.nonce,
|
|
1261
|
+
requestDigest: core.requestDigest,
|
|
1262
|
+
};
|
|
1263
|
+
if (
|
|
1264
|
+
canonicalDigest(
|
|
1265
|
+
context,
|
|
1266
|
+
"chainlesschain.skill-mutation-consume-context/v2",
|
|
1267
|
+
) !== core.contextDigest
|
|
1268
|
+
) {
|
|
1269
|
+
throw authorityError(
|
|
1270
|
+
SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
1271
|
+
"mutation consumption receipt context digest is invalid",
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
} catch (cause) {
|
|
1275
|
+
if (
|
|
1276
|
+
cause instanceof SkillMutationAuthorityError &&
|
|
1277
|
+
cause.code === SKILL_MUTATION_CAPABILITY_INVALID_CODE
|
|
1278
|
+
) {
|
|
1279
|
+
throw cause;
|
|
1280
|
+
}
|
|
1281
|
+
throw authorityError(
|
|
1282
|
+
SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
1283
|
+
"mutation consumption receipt is invalid",
|
|
1284
|
+
{ cause },
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
return deepFreeze({ ...core, receiptDigest: value.receiptDigest });
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function normalizeDenial(error, fallbackCode, fallbackMessage) {
|
|
1291
|
+
return error instanceof SkillMutationAuthorityError
|
|
1292
|
+
? error
|
|
1293
|
+
: authorityError(fallbackCode, fallbackMessage, { cause: error });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function contextsMatch(request, consume) {
|
|
1297
|
+
for (const key of CONSUME_INPUT_KEYS) {
|
|
1298
|
+
if (request[key] !== consume[key]) return false;
|
|
1299
|
+
}
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
export class SkillMutationAuthority {
|
|
1304
|
+
#capabilities = new WeakMap();
|
|
1305
|
+
|
|
1306
|
+
#principalResolver;
|
|
1307
|
+
|
|
1308
|
+
#receiptVerifier;
|
|
1309
|
+
|
|
1310
|
+
#auditSink;
|
|
1311
|
+
|
|
1312
|
+
#nonceStore;
|
|
1313
|
+
|
|
1314
|
+
#now;
|
|
1315
|
+
|
|
1316
|
+
constructor({
|
|
1317
|
+
principalResolver,
|
|
1318
|
+
receiptVerifier,
|
|
1319
|
+
auditSink,
|
|
1320
|
+
nonceStore,
|
|
1321
|
+
now,
|
|
1322
|
+
} = {}) {
|
|
1323
|
+
if (!principalResolver || typeof principalResolver.resolve !== "function") {
|
|
1324
|
+
throw new TypeError(
|
|
1325
|
+
"SkillMutationAuthority requires a trusted principalResolver.resolve port",
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
if (!receiptVerifier || typeof receiptVerifier.verify !== "function") {
|
|
1329
|
+
throw new TypeError(
|
|
1330
|
+
"SkillMutationAuthority requires a trusted receiptVerifier.verify port",
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
if (!auditSink || typeof auditSink.append !== "function") {
|
|
1334
|
+
throw new TypeError(
|
|
1335
|
+
"SkillMutationAuthority requires a durable auditSink.append port",
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
if (!nonceStore || typeof nonceStore.claim !== "function") {
|
|
1339
|
+
throw new TypeError(
|
|
1340
|
+
"SkillMutationAuthority requires a durable nonceStore.claim port",
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
if (now !== undefined && typeof now !== "function") {
|
|
1344
|
+
throw new TypeError("SkillMutationAuthority now must be a function");
|
|
1345
|
+
}
|
|
1346
|
+
this.#principalResolver = principalResolver.resolve.bind(principalResolver);
|
|
1347
|
+
this.#receiptVerifier = receiptVerifier.verify.bind(receiptVerifier);
|
|
1348
|
+
this.#auditSink = auditSink.append.bind(auditSink);
|
|
1349
|
+
this.#nonceStore = nonceStore.claim.bind(nonceStore);
|
|
1350
|
+
this.#now = now || (() => new Date());
|
|
1351
|
+
Object.freeze(this);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
#clock() {
|
|
1355
|
+
const value = this.#now();
|
|
1356
|
+
const date =
|
|
1357
|
+
value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
1358
|
+
if (!Number.isFinite(date.getTime())) {
|
|
1359
|
+
throw authorityError(
|
|
1360
|
+
SKILL_MUTATION_AUDIT_FAILED_CODE,
|
|
1361
|
+
"Skill mutation authority clock returned an invalid timestamp",
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
return { milliseconds: date.getTime(), iso: date.toISOString() };
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
async #appendAudit(event) {
|
|
1368
|
+
const verifiedEvent = verifySkillMutationAuditEvent(event);
|
|
1369
|
+
let acknowledgement;
|
|
1370
|
+
try {
|
|
1371
|
+
acknowledgement = await this.#auditSink(verifiedEvent);
|
|
1372
|
+
assertExactDataRecord(
|
|
1373
|
+
acknowledgement,
|
|
1374
|
+
AUDIT_ACK_KEYS,
|
|
1375
|
+
"mutation audit acknowledgement",
|
|
1376
|
+
SKILL_MUTATION_AUDIT_FAILED_CODE,
|
|
1377
|
+
);
|
|
1378
|
+
if (
|
|
1379
|
+
acknowledgement.persisted !== true ||
|
|
1380
|
+
acknowledgement.auditDigest !== verifiedEvent.auditDigest ||
|
|
1381
|
+
!DIGEST_PATTERN.test(acknowledgement.headDigest || "") ||
|
|
1382
|
+
!Number.isSafeInteger(acknowledgement.sequence) ||
|
|
1383
|
+
acknowledgement.sequence < 1
|
|
1384
|
+
) {
|
|
1385
|
+
throw authorityError(
|
|
1386
|
+
SKILL_MUTATION_AUDIT_FAILED_CODE,
|
|
1387
|
+
"mutation audit acknowledgement is not durable or bound to the event",
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
} catch (cause) {
|
|
1391
|
+
if (
|
|
1392
|
+
cause instanceof SkillMutationAuthorityError &&
|
|
1393
|
+
cause.code === SKILL_MUTATION_AUDIT_FAILED_CODE
|
|
1394
|
+
) {
|
|
1395
|
+
throw cause;
|
|
1396
|
+
}
|
|
1397
|
+
throw authorityError(
|
|
1398
|
+
SKILL_MUTATION_AUDIT_FAILED_CODE,
|
|
1399
|
+
"Skill mutation audit append failed; authority is denied",
|
|
1400
|
+
{ cause, auditDigest: verifiedEvent.auditDigest },
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
return deepFreeze({ ...acknowledgement });
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
async #audit({ phase, decision, code, context, principal, clock }) {
|
|
1407
|
+
const event = buildAuditEvent({
|
|
1408
|
+
phase,
|
|
1409
|
+
decision,
|
|
1410
|
+
code,
|
|
1411
|
+
context,
|
|
1412
|
+
principal,
|
|
1413
|
+
occurredAt: clock.iso,
|
|
1414
|
+
});
|
|
1415
|
+
const acknowledgement = await this.#appendAudit(event);
|
|
1416
|
+
return { event, acknowledgement };
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async #claimNonce(request, clock) {
|
|
1420
|
+
const claim = buildNonceClaim(request, clock.iso);
|
|
1421
|
+
let acknowledgement;
|
|
1422
|
+
try {
|
|
1423
|
+
acknowledgement = await this.#nonceStore(claim);
|
|
1424
|
+
assertExactDataRecord(
|
|
1425
|
+
acknowledgement,
|
|
1426
|
+
NONCE_ACK_KEYS,
|
|
1427
|
+
"mutation nonce acknowledgement",
|
|
1428
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
1429
|
+
);
|
|
1430
|
+
if (
|
|
1431
|
+
acknowledgement.schema !== SKILL_MUTATION_NONCE_ACK_SCHEMA ||
|
|
1432
|
+
acknowledgement.persisted !== true ||
|
|
1433
|
+
typeof acknowledgement.claimed !== "boolean" ||
|
|
1434
|
+
acknowledgement.claimDigest !== claim.claimDigest ||
|
|
1435
|
+
acknowledgement.expiresAt !== claim.expiresAt ||
|
|
1436
|
+
!DIGEST_PATTERN.test(acknowledgement.headDigest || "") ||
|
|
1437
|
+
!Number.isSafeInteger(acknowledgement.sequence) ||
|
|
1438
|
+
acknowledgement.sequence < 1
|
|
1439
|
+
) {
|
|
1440
|
+
throw authorityError(
|
|
1441
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
1442
|
+
"mutation nonce acknowledgement is not durable or bound to the claim",
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
} catch (cause) {
|
|
1446
|
+
if (
|
|
1447
|
+
cause instanceof SkillMutationAuthorityError &&
|
|
1448
|
+
cause.code === SKILL_MUTATION_NONCE_STORE_FAILED_CODE
|
|
1449
|
+
) {
|
|
1450
|
+
throw cause;
|
|
1451
|
+
}
|
|
1452
|
+
throw authorityError(
|
|
1453
|
+
SKILL_MUTATION_NONCE_STORE_FAILED_CODE,
|
|
1454
|
+
"Skill mutation nonce claim failed; authority is denied",
|
|
1455
|
+
{ cause, claimDigest: claim.claimDigest },
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
if (!acknowledgement.claimed) {
|
|
1459
|
+
throw authorityError(
|
|
1460
|
+
SKILL_MUTATION_NONCE_REUSED_CODE,
|
|
1461
|
+
"Skill mutation request nonce has already been durably claimed",
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
return deepFreeze({ ...acknowledgement });
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
async authorize(value) {
|
|
1468
|
+
let request = null;
|
|
1469
|
+
let context = safeAuditContext(value);
|
|
1470
|
+
let principal = null;
|
|
1471
|
+
let clock;
|
|
1472
|
+
try {
|
|
1473
|
+
clock = this.#clock();
|
|
1474
|
+
request = verifySkillMutationRequest(value);
|
|
1475
|
+
context = requestContext(request);
|
|
1476
|
+
const expiry = new Date(request.expiresAt).getTime();
|
|
1477
|
+
if (expiry <= clock.milliseconds) {
|
|
1478
|
+
throw authorityError(
|
|
1479
|
+
SKILL_MUTATION_REQUEST_EXPIRED_CODE,
|
|
1480
|
+
"Skill mutation request has expired",
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
if (expiry - clock.milliseconds > MAX_AUTHORIZATION_TTL_MS) {
|
|
1484
|
+
throw authorityError(
|
|
1485
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
1486
|
+
"Skill mutation request expiry exceeds the maximum authority TTL",
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
let resolved;
|
|
1491
|
+
try {
|
|
1492
|
+
resolved = await this.#principalResolver(
|
|
1493
|
+
deepFreeze({ request: context }),
|
|
1494
|
+
);
|
|
1495
|
+
} catch (cause) {
|
|
1496
|
+
throw authorityError(
|
|
1497
|
+
SKILL_MUTATION_PRINCIPAL_INVALID_CODE,
|
|
1498
|
+
"Skill mutation principal resolution failed",
|
|
1499
|
+
{ cause },
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
principal = normalizePrincipal(resolved, context);
|
|
1503
|
+
if (!roleCanMutateTarget(principal.role, request.targetScope)) {
|
|
1504
|
+
throw authorityError(
|
|
1505
|
+
SKILL_MUTATION_SCOPE_DENIED_CODE,
|
|
1506
|
+
`${principal.role} cannot mutate the ${request.targetScope} Skill scope`,
|
|
1507
|
+
{ role: principal.role, targetScope: request.targetScope },
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
let verifiedReceipts;
|
|
1512
|
+
try {
|
|
1513
|
+
verifiedReceipts = await this.#receiptVerifier(
|
|
1514
|
+
deepFreeze({
|
|
1515
|
+
receipts: request.receipts,
|
|
1516
|
+
request: context,
|
|
1517
|
+
principal,
|
|
1518
|
+
}),
|
|
1519
|
+
);
|
|
1520
|
+
} catch (cause) {
|
|
1521
|
+
throw authorityError(
|
|
1522
|
+
SKILL_MUTATION_RECEIPT_INVALID_CODE,
|
|
1523
|
+
"Skill mutation receipt verification failed",
|
|
1524
|
+
{ cause },
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
normalizeReceiptVerification(verifiedReceipts, request, principal);
|
|
1528
|
+
|
|
1529
|
+
clock = this.#clock();
|
|
1530
|
+
if (expiry <= clock.milliseconds) {
|
|
1531
|
+
throw authorityError(
|
|
1532
|
+
SKILL_MUTATION_REQUEST_EXPIRED_CODE,
|
|
1533
|
+
"Skill mutation request expired during verification",
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
await this.#claimNonce(request, clock);
|
|
1537
|
+
|
|
1538
|
+
await this.#audit({
|
|
1539
|
+
phase: "authorize",
|
|
1540
|
+
decision: "allow",
|
|
1541
|
+
code: "CC_SKILL_MUTATION_AUTHORIZED",
|
|
1542
|
+
context,
|
|
1543
|
+
principal,
|
|
1544
|
+
clock,
|
|
1545
|
+
});
|
|
1546
|
+
clock = this.#clock();
|
|
1547
|
+
if (expiry <= clock.milliseconds) {
|
|
1548
|
+
throw authorityError(
|
|
1549
|
+
SKILL_MUTATION_REQUEST_EXPIRED_CODE,
|
|
1550
|
+
"Skill mutation request expired before capability issuance",
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const capability = Object.freeze(Object.create(null));
|
|
1555
|
+
this.#capabilities.set(capability, {
|
|
1556
|
+
status: "issued",
|
|
1557
|
+
request,
|
|
1558
|
+
principal,
|
|
1559
|
+
});
|
|
1560
|
+
return capability;
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
if (
|
|
1563
|
+
error instanceof SkillMutationAuthorityError &&
|
|
1564
|
+
error.code === SKILL_MUTATION_AUDIT_FAILED_CODE
|
|
1565
|
+
) {
|
|
1566
|
+
throw error;
|
|
1567
|
+
}
|
|
1568
|
+
const denial = normalizeDenial(
|
|
1569
|
+
error,
|
|
1570
|
+
SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
1571
|
+
"Skill mutation authorization failed",
|
|
1572
|
+
);
|
|
1573
|
+
clock ||= this.#clock();
|
|
1574
|
+
await this.#audit({
|
|
1575
|
+
phase: "authorize",
|
|
1576
|
+
decision: "deny",
|
|
1577
|
+
code: AUDIT_CODES.has(denial.code)
|
|
1578
|
+
? denial.code
|
|
1579
|
+
: SKILL_MUTATION_REQUEST_INVALID_CODE,
|
|
1580
|
+
context,
|
|
1581
|
+
principal,
|
|
1582
|
+
clock,
|
|
1583
|
+
});
|
|
1584
|
+
throw denial;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
async consume(capability, value) {
|
|
1589
|
+
let consume = null;
|
|
1590
|
+
let context = safeAuditContext(value);
|
|
1591
|
+
let state = null;
|
|
1592
|
+
let principal = null;
|
|
1593
|
+
let clock = this.#clock();
|
|
1594
|
+
try {
|
|
1595
|
+
consume = verifyConsumeContext(value);
|
|
1596
|
+
context = { ...consume, requestDigest: null };
|
|
1597
|
+
delete context.schema;
|
|
1598
|
+
state =
|
|
1599
|
+
capability && typeof capability === "object"
|
|
1600
|
+
? this.#capabilities.get(capability)
|
|
1601
|
+
: null;
|
|
1602
|
+
if (!state) {
|
|
1603
|
+
throw authorityError(
|
|
1604
|
+
SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
1605
|
+
"Skill mutation capability is forged or belongs to another authority instance",
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
principal = state.principal;
|
|
1609
|
+
context.requestDigest = state.request.requestDigest;
|
|
1610
|
+
if (state.status !== "issued") {
|
|
1611
|
+
throw authorityError(
|
|
1612
|
+
SKILL_MUTATION_CAPABILITY_REPLAYED_CODE,
|
|
1613
|
+
"Skill mutation capability has already been consumed",
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
if (!contextsMatch(state.request, consume)) {
|
|
1617
|
+
throw authorityError(
|
|
1618
|
+
SKILL_MUTATION_CAPABILITY_CONTEXT_MISMATCH_CODE,
|
|
1619
|
+
"Skill mutation capability does not match the requested tenant, audience, operation, Skill, or CAS target",
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
if (new Date(state.request.expiresAt).getTime() <= clock.milliseconds) {
|
|
1623
|
+
state.status = "expired";
|
|
1624
|
+
throw authorityError(
|
|
1625
|
+
SKILL_MUTATION_CAPABILITY_EXPIRED_CODE,
|
|
1626
|
+
"Skill mutation capability has expired",
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
state.status = "consuming";
|
|
1631
|
+
const audit = await this.#audit({
|
|
1632
|
+
phase: "consume",
|
|
1633
|
+
decision: "allow",
|
|
1634
|
+
code: "CC_SKILL_MUTATION_CONSUMED",
|
|
1635
|
+
context,
|
|
1636
|
+
principal,
|
|
1637
|
+
clock,
|
|
1638
|
+
});
|
|
1639
|
+
clock = this.#clock();
|
|
1640
|
+
if (new Date(state.request.expiresAt).getTime() <= clock.milliseconds) {
|
|
1641
|
+
state.status = "expired";
|
|
1642
|
+
throw authorityError(
|
|
1643
|
+
SKILL_MUTATION_CAPABILITY_EXPIRED_CODE,
|
|
1644
|
+
"Skill mutation capability expired before durable consumption completed",
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const receipt = buildConsumptionReceipt(context, principal, clock, audit);
|
|
1648
|
+
state.status = "consumed";
|
|
1649
|
+
return receipt;
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
if (
|
|
1652
|
+
error instanceof SkillMutationAuthorityError &&
|
|
1653
|
+
error.code === SKILL_MUTATION_AUDIT_FAILED_CODE
|
|
1654
|
+
) {
|
|
1655
|
+
throw error;
|
|
1656
|
+
}
|
|
1657
|
+
const denial = normalizeDenial(
|
|
1658
|
+
error,
|
|
1659
|
+
SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
1660
|
+
"Skill mutation capability consumption failed",
|
|
1661
|
+
);
|
|
1662
|
+
await this.#audit({
|
|
1663
|
+
phase: "consume",
|
|
1664
|
+
decision: "deny",
|
|
1665
|
+
code: AUDIT_CODES.has(denial.code)
|
|
1666
|
+
? denial.code
|
|
1667
|
+
: SKILL_MUTATION_CAPABILITY_INVALID_CODE,
|
|
1668
|
+
context,
|
|
1669
|
+
principal,
|
|
1670
|
+
clock,
|
|
1671
|
+
});
|
|
1672
|
+
throw denial;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
Object.freeze(SkillMutationAuthority.prototype);
|