chainlesschain 0.166.83 → 0.166.85

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.
Files changed (179) hide show
  1. package/README.md +9 -4
  2. package/package.json +1 -1
  3. package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
  4. package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
  5. package/src/assets/web-panel/assets/{AIOps-D5a4i3N7.js → AIOps-Cgo7OFpi.js} +1 -1
  6. package/src/assets/web-panel/assets/{ActionButton-DxsRk5n1.js → ActionButton-lUMnE2Ln.js} +1 -1
  7. package/src/assets/web-panel/assets/{Analytics-D-7EvH4S.js → Analytics-CsvKzTot.js} +3 -3
  8. package/src/assets/web-panel/assets/{AppLayout-DDEWVr-d.js → AppLayout-C2z1h1Pv.js} +3 -3
  9. package/src/assets/web-panel/assets/{Artifacts-CAlJAjRB.js → Artifacts-C5Q0ZhNb.js} +1 -1
  10. package/src/assets/web-panel/assets/{Audit-tuaWDOAM.js → Audit-DXnfPKsI.js} +1 -1
  11. package/src/assets/web-panel/assets/{BackgroundAgents-CIwRFJ50.js → BackgroundAgents-DjbGcLm3.js} +1 -1
  12. package/src/assets/web-panel/assets/{Backup-CpBepJlH.js → Backup-9bVrKvXE.js} +1 -1
  13. package/src/assets/web-panel/assets/{BaseInput-eKguCzQc.js → BaseInput-bx5ri7BD.js} +1 -1
  14. package/src/assets/web-panel/assets/{Chat-CFrUfXDV.js → Chat-CCFmeUMY.js} +4 -4
  15. package/src/assets/web-panel/assets/ChatBubbleRenderer-ClqsIfTT.js +1 -0
  16. package/src/assets/web-panel/assets/{Checkbox-o5FEn6a1.js → Checkbox-ei7MQXe9.js} +1 -1
  17. package/src/assets/web-panel/assets/{Codegen-Dih8HogC.js → Codegen-CXgBJ9iu.js} +1 -1
  18. package/src/assets/web-panel/assets/{Col-D5r-6WZe.js → Col-DMkMsizS.js} +1 -1
  19. package/src/assets/web-panel/assets/{Community-jeQ6bGey.js → Community-CvpB_UFR.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compact-CEP7QsWp.js → Compact-gay5hbFX.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compliance-B8NydOHd.js → Compliance-dG8JQyRL.js} +1 -1
  22. package/src/assets/web-panel/assets/{Cowork-Bmi1zuPy.js → Cowork-BA3xfaTW.js} +4 -4
  23. package/src/assets/web-panel/assets/{Cron-BAL9bwSp.js → Cron-D1_HsxyY.js} +2 -2
  24. package/src/assets/web-panel/assets/{Crosschain-D8WUi2dS.js → Crosschain-DZqoqoy5.js} +1 -1
  25. package/src/assets/web-panel/assets/{DID-CQCkYcLY.js → DID-Cwnr8hCO.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dashboard-DogXg6kh.js → Dashboard-CxevypiI.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dropdown-BJBJyQJn.js → Dropdown-Bs7-yWgr.js} +1 -1
  28. package/src/assets/web-panel/assets/{EmailListRenderer-CxPWquJx.js → EmailListRenderer-CcEs2Puo.js} +1 -1
  29. package/src/assets/web-panel/assets/{EvolutionSettings-BijH1x8G.js → EvolutionSettings-C9ipBzAQ.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DStwf-00.js → FamilyGuardDashboard-Dt6--mGn.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-LU9_sNxC.js → Federation-CD0JN5FF.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-DNfWhj7P.js → FormItemContext-POypz3Xv.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-DWYsPjzk.js → GenericCardRenderer-Dv9f-0F5.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-CA0O9ulB.js → Git-BbTh5Atv.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-ECShL7BJ.js → Governance-C71dgoir.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-9GA2542e.js → Inference-DwSHxbJB.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-nC5mTbEH.js → KnowledgeGraph-Cx1oQYQZ.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-B7IqgnDy.js → Logs-BAJWe00o.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-CD8a7lHT.js +1 -0
  40. package/src/assets/web-panel/assets/{Marketplace-CEr0suM8.js → Marketplace-fwuY7P7Y.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-DhTPQKOE.js → McpTools-Di4693uc.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-mHRZP1w4.js → Memory-Cz3L-GUB.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-BNfX6Rdo.js → MobileBridge-BuXKIkSb.js} +1 -1
  44. package/src/assets/web-panel/assets/MobileProjects-Bk-lK4Um.js +1 -0
  45. package/src/assets/web-panel/assets/{Mtc-CzkV3wsn.js → Mtc-LnACMFd0.js} +5 -5
  46. package/src/assets/web-panel/assets/{MtcAudit-DWYljJik.js → MtcAudit-BZy7yE0t.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-kHgtvZY_.js → Multisig-BFG5iVrE.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-56uQiPiY.js → NLProgramming-DgXi04vJ.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-DxGOZqRf.js → Notes-BAj0Kgg4.js} +4 -4
  50. package/src/assets/web-panel/assets/{NotificationSettings-DabaCpmo.js → NotificationSettings-AixrsHw5.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer-SzeQNlGT.js → OrderTableRenderer-Dwqk3FlG.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-D9gnPpK2.js → Organization-UNCBC3ux.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-Cqp_bXm4.js → Overflow-Drh8oAFT.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-DTeK-r0G.js → P2P-bc2q0xEB.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-CsjG85GT.js → PdhVaultBrowser-BGwKxcDM.js} +3 -3
  56. package/src/assets/web-panel/assets/{Permissions-B4ofZq3j.js → Permissions-DOnUxUzu.js} +3 -3
  57. package/src/assets/web-panel/assets/{PersonalDataHub-Cvas5cYd.js → PersonalDataHub-B3qPZkrr.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-Er3wmAew.js → Pipeline-B6hfa4-i.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-DMFYeZox.js → Privacy-Bun3c35F.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-DaVaYPy0.js → ProjectInit-CCObdzk9.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-dPx5q2Ac.js → ProjectSettings-C6lKrSXY.js} +2 -2
  62. package/src/assets/web-panel/assets/{Projects-N0JNeCui.js → Projects-DYcta7MZ.js} +1 -1
  63. package/src/assets/web-panel/assets/{Providers-BPQo_gQa.js → Providers-BtykxKCe.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-CD-sEsWh.js → QrScannerModal-D9CDyroY.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-LOKRJiN_.js → QuickAsk-CJauCxXx.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-BwPXqcWy.js → Recommend-BbiYZofR.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-BjotndxF.js → RemoteSession-BymCrgFW.js} +3 -3
  68. package/src/assets/web-panel/assets/{Reputation-BlZ2gcbU.js → Reputation-BWrGpMpF.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-D399HM-d.js → Row-9pOYIhiW.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-C7H-ibnY.js → RssFeed-D3bQJ4qB.js} +2 -2
  71. package/src/assets/web-panel/assets/{Search-CNHCVmM1.js → Search-BpKOOFE0.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-C70eDGYY.js → Security-DC6C0NSz.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-6xfq9G-H.js → Services-Lkf1046J.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-BbY2zeZV.js → Skeleton-eCQ_SsDb.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-DYtyZ7_6.js → Skills-BvDSQ3-d.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-CkWcl5OH.js → Sla-BQbKy8XJ.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-CiknYk2P.js → SpeechSettings-DHsRx-XA.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-D-8JVip4.js → SyncSettings-CC281X_J.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-B2KnR9XC.js → Tasks-CHHSWHom.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-CipXkOE-.js → Templates-Cr2vC0tg.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-DbwgIMmx.js → Tenant-DeYO5pIG.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-OtCriHEE.js → Terminal-DBNgbfPr.js} +2 -2
  83. package/src/assets/web-panel/assets/TimelineRenderer-MoxgaF3x.js +1 -0
  84. package/src/assets/web-panel/assets/{Tokens-B4iFVxvH.js → Tokens-CT2QfAN7.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-BegM2v8q.js → Trigger-DPUnomxs.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-COkqnnFj.js → Trust-DnufuaTx.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-BzqKPChj.js → UkeySign-CJ6iOdPs.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-D-F2XL3C.js → VideoEditing-xBWkH8bL.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-22PzRY9T.js → Wallet-WYV-4hMR.js} +3 -3
  90. package/src/assets/web-panel/assets/{WebAuthn-eSMq_qtz.js → WebAuthn-DJ5Mhd9L.js} +4 -4
  91. package/src/assets/web-panel/assets/{WorkflowEditor-BvdAakpq.js → WorkflowEditor-B__UdOjN.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-C6jhpF7E.js → chat-CLlmBVhp.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-BbbW2Pfv.js → colors-B8zoPQYm.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-CVFeLTqt.js → compact-item-CvJ-dUsH.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-DS0m7NZc.js → createContext-LAtMm8SC.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-DgZ4fdGI.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-DVouP0t6.js → hasIn-CLYvg42H.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Be12HoCS.js → index-09YzRb6Z.js} +3 -3
  99. package/src/assets/web-panel/assets/{index-CPS8E-J6.js → index-4rr5jFOL.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BXw6ivB8.js → index-B02qgCao.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BaUah6TO.js → index-B1hHp9_M.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Ds4wbrgk.js → index-B6zlMzZz.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-JeJqXbUU.js → index-BCzyJ1xS.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-54aHB4FL.js → index-BGrQ2uEl.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-eaXEQFuP.js → index-BM2HqHdT.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-C74emdKD.js → index-BUVrYDNV.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BrIv7C9a.js → index-B_RPWP_5.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DGYX17YW.js → index-BkcQJvsz.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DotRL126.js → index-Bu7-I2L4.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BvCgzl0X.js → index-C2KFnUsv.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-C4xmLgLa.js → index-C3uD5H7u.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BwozzG6k.js → index-C5v7cEtm.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-C6q2MkIG.js → index-C6PETyel.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-DUBkae39.js → index-CUcE6rIa.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BwZnPiFd.js → index-CVDcRQuX.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CIk7mg6G.js → index-C_dq-6_3.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-B8a_c_qZ.js → index-CnLXLaEF.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CIg4nqyp.js → index-Cpda0SAz.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BhvnNFfd.js → index-Cv7vwX3z.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CpTbnbUI.js → index-CxJ4NPVq.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BzL3LWFX.js → index-D0Z13xcZ.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BE_u-rXr.js → index-DDm9m4vC.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-ByLsH9CL.js → index-DHq2Of6z.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BOOsA080.js → index-DNQUdPam.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-SdFkCtiC.js → index-DX0aHEHL.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Bw-ZGQCH.js → index-DX99e8Xy.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CkdYB0_g.js → index-DYW4Z3y-.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DuVjTDcW.js → index-DfOOACfd.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DpReoAGb.js → index-Di9vKira.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CwYFDMn_.js → index-DpeV-6y4.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DR6TDz5H.js → index-GR3omApz.js} +1 -1
  132. package/src/assets/web-panel/assets/{index--AdzSlH7.js → index-f1ptYBMr.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-8QbwhE3i.js → index-iJ6s24bA.js} +1 -1
  134. package/src/assets/web-panel/assets/index-jLEdASOV.js +1 -0
  135. package/src/assets/web-panel/assets/index-jesGo4rl.js +1 -0
  136. package/src/assets/web-panel/assets/{index-B5JRIZXi.js → index-oX216HRH.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-CB5cUnQ5.js → initDefaultProps-QD-koeaw.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-bn_fWhQV.js → motion-CTItLW4-.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-DY4-Odtu.js → move-CGsdTOdN.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-B5l5mjoh.js → mtc-parser-C1kW_CNU.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-rJ-Ci8CW.js → omit-B4PilaYy.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-BMwGJd6C.js → pickAttrs-DBGUE12e.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-CaiQyGXG.js → placementArrow-DZxCLjuo.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-DQTO5wDH.js → responsiveObserve-CZjQiocN.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-Dg6ZDtbz.js → slide-Btz5hh2_.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-bvjrktKc.js → statusUtils-CfuxuLIt.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-C1geIU_x.js → styleChecker-DdO0Obo_.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-BzU3RJRi.js → useFlexGapSupport-CxftJ-Zs.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-B7l6OwLe.js → useFs-pVJChh6K.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-BYF1L_8x.js → usePersonalDataHub-wv7Q0uNE.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-6c2GVkT-.js → vnode-DVOVfAqC.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-BYAY6QeD.js → zoom-B5tebZuT.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/commands/permissions.js +27 -2
  155. package/src/data/changelog.json +26 -0
  156. package/src/lib/background-interaction-resolver.js +4 -0
  157. package/src/lib/context-memory-kernel/durable-memory-port.js +68 -15
  158. package/src/lib/packer/pack-update-applier.js +28 -9
  159. package/src/lib/packer/pkg-config-generator.js +119 -2
  160. package/src/lib/packer/pkg-runner.js +7 -0
  161. package/src/lib/packer/pkg-targets.js +83 -0
  162. package/src/lib/packer/web-panel-builder.js +7 -0
  163. package/src/lib/permission-authority.js +56 -7
  164. package/src/lib/process-execution-broker/index.js +11 -0
  165. package/src/lib/process-execution-broker/linux-subreaper-helper.js +87 -13
  166. package/src/lib/process-execution-broker/packer-build-policy.js +31 -0
  167. package/src/lib/process-execution-broker/platform-sandbox.js +11 -3
  168. package/src/lib/scoped-permission-store.js +241 -121
  169. package/src/lib/settings-authority-domain.cjs +966 -0
  170. package/src/lib/settings-authority-record.cjs +701 -0
  171. package/src/lib/settings-loader.cjs +45 -8
  172. package/src/lib/settings-source-observation.cjs +8 -0
  173. package/src/assets/web-panel/assets/ChatBubbleRenderer-DaRtY87l.js +0 -1
  174. package/src/assets/web-panel/assets/MarkdownRenderer-BqlqrJkY.js +0 -1
  175. package/src/assets/web-panel/assets/MobileProjects-8zG8f5li.js +0 -1
  176. package/src/assets/web-panel/assets/TimelineRenderer-B9LU5qrl.js +0 -1
  177. package/src/assets/web-panel/assets/devWarning-D50gzPS9.js +0 -1
  178. package/src/assets/web-panel/assets/index-CgensV-n.js +0 -1
  179. package/src/assets/web-panel/assets/index-Ctkf06Z_.js +0 -1
@@ -13,9 +13,10 @@ import projectRoot from "./project-root.cjs";
13
13
  import permissionRules from "./permission-rules.cjs";
14
14
  import { getMachineSecurityAnchorDir } from "./paths.js";
15
15
  import {
16
- mutateSecurityStore,
17
16
  readSecurityStore,
17
+ writeSecurityStore,
18
18
  } from "./durable-security-store.js";
19
+ import { withFileLock } from "./with-file-lock.js";
19
20
 
20
21
  const { findGitProjectRoot } = projectRoot;
21
22
  const { parseRule } = permissionRules;
@@ -27,6 +28,58 @@ const RULE_ID_PATTERN = /^spr_[0-9a-f]{32}$/;
27
28
  const DECISIONS = new Set(["allow", "ask", "deny"]);
28
29
  const DEFAULT_MAX_RECORDS = 1000;
29
30
 
31
+ // Official mutations revoke every provider in this module instance. Path
32
+ // matching alone misses aliases and files that did not exist at admission.
33
+ // This is an in-process event owner, not a cross-process notification journal.
34
+ const ownerId = randomUUID();
35
+ let permissionRevision = Object.freeze({
36
+ ownerId,
37
+ revision: 0,
38
+ state: "ready",
39
+ });
40
+ const revisionListeners = new Set();
41
+
42
+ export function getScopedPermissionRevision() {
43
+ if (permissionRevision.state !== "ready") {
44
+ throw scopedPermissionError(
45
+ "CC_SCOPED_PERMISSION_AUTHORITY_UNAVAILABLE",
46
+ "Scoped permission authority is unavailable",
47
+ );
48
+ }
49
+ return permissionRevision;
50
+ }
51
+
52
+ export function subscribeScopedPermissionRevision(listener) {
53
+ if (typeof listener !== "function")
54
+ throw new TypeError(
55
+ "scoped permission revision listener must be a function",
56
+ );
57
+ revisionListeners.add(listener);
58
+ return () => revisionListeners.delete(listener);
59
+ }
60
+
61
+ function beginMutation() {
62
+ getScopedPermissionRevision();
63
+ const revision = permissionRevision.revision + 1;
64
+ permissionRevision = Object.freeze({
65
+ ownerId,
66
+ revision,
67
+ state: Number.isSafeInteger(revision) ? "mutating" : "invalid",
68
+ });
69
+ for (const listener of [...revisionListeners]) {
70
+ try {
71
+ listener(permissionRevision);
72
+ } catch {
73
+ // One observer must not prevent the other shells from being revoked.
74
+ }
75
+ }
76
+ if (permissionRevision.state === "invalid") getScopedPermissionRevision();
77
+ }
78
+
79
+ function finishMutation(state) {
80
+ permissionRevision = Object.freeze({ ...permissionRevision, state });
81
+ }
82
+
30
83
  export const SCOPED_PERMISSION_ERROR_CODES = Object.freeze({
31
84
  INVALID: "CC_SCOPED_PERMISSION_INVALID",
32
85
  CONFLICT: "CC_SCOPED_PERMISSION_CONFLICT",
@@ -90,7 +143,7 @@ function validateRecord(record, seenIds) {
90
143
  seenIds.has(record.id) ||
91
144
  !DECISIONS.has(record.decision) ||
92
145
  !parseRule(record.rule) ||
93
- !Number.isInteger(record.revision) ||
146
+ !Number.isSafeInteger(record.revision) ||
94
147
  record.revision < 1 ||
95
148
  !Number.isFinite(record.createdAt) ||
96
149
  !Number.isFinite(record.expiresAt) ||
@@ -112,7 +165,7 @@ function validateState(value, workspace) {
112
165
  !value ||
113
166
  value.schema !== STATE_SCHEMA ||
114
167
  value.schemaVersion !== STATE_SCHEMA_VERSION ||
115
- !Number.isInteger(value.generation) ||
168
+ !Number.isSafeInteger(value.generation) ||
116
169
  value.generation < 0 ||
117
170
  !value.workspace ||
118
171
  value.workspace.id !== workspace.id ||
@@ -135,6 +188,16 @@ function statusFor(record, now) {
135
188
  return "active";
136
189
  }
137
190
 
191
+ function nextVersion(value, label) {
192
+ if (value === Number.MAX_SAFE_INTEGER) {
193
+ throw scopedPermissionError(
194
+ SCOPED_PERMISSION_ERROR_CODES.INVALID,
195
+ `Scoped permission ${label} is exhausted`,
196
+ );
197
+ }
198
+ return value + 1;
199
+ }
200
+
138
201
  function projectRecord(record, now, filePath) {
139
202
  return Object.freeze({
140
203
  ...structuredClone(record),
@@ -181,24 +244,89 @@ export class ScopedPermissionStore {
181
244
  this._randomId = randomId;
182
245
  }
183
246
 
184
- _read() {
185
- const stored = readSecurityStore(this.filePath, STORE_LABEL);
186
- if (Object.keys(stored).length === 0) return emptyState(this.workspace);
187
- return validateState(stored, this.workspace);
247
+ _capture() {
248
+ const revision = getScopedPermissionRevision();
249
+ const captured = Object.freeze({
250
+ filePath: path.resolve(this.filePath),
251
+ workspace: Object.freeze({ ...this.workspace }),
252
+ now: this._now.bind(this),
253
+ randomId: this._randomId.bind(this),
254
+ maxRecords: this.maxRecords,
255
+ lock: this._lock || withFileLock,
256
+ });
257
+ if (getScopedPermissionRevision() !== revision)
258
+ throw scopedPermissionError(
259
+ "CC_SCOPED_PERMISSION_AUTHORITY_CHANGED",
260
+ "Scoped permission authority changed while capturing the operation",
261
+ );
262
+ return captured;
263
+ }
264
+
265
+ _read(captured = this._capture()) {
266
+ getScopedPermissionRevision();
267
+ const stored = readSecurityStore(captured.filePath, STORE_LABEL);
268
+ if (Object.keys(stored).length === 0) return emptyState(captured.workspace);
269
+ return validateState(stored, captured.workspace);
270
+ }
271
+
272
+ _mutate(captured, update) {
273
+ getScopedPermissionRevision(); // Reject observer reentry before the lock.
274
+ fs.mkdirSync(path.dirname(captured.filePath), {
275
+ recursive: true,
276
+ mode: 0o700,
277
+ });
278
+ let started = false;
279
+ let committed = false;
280
+ try {
281
+ captured.lock(
282
+ captured.filePath,
283
+ () => {
284
+ const draft = this._read(captured);
285
+ if (update(draft) === false) return; // Idempotent revoke does no I/O.
286
+ validateState(draft, captured.workspace);
287
+ started = true;
288
+ beginMutation();
289
+ // No draft field aliases the public instance during notification.
290
+ validateState(draft, captured.workspace);
291
+ writeSecurityStore(captured.filePath, STORE_LABEL, draft);
292
+ committed = true;
293
+ },
294
+ { timeoutMs: 2000, staleMs: 30000, failIfUnavailable: true },
295
+ );
296
+ if (started) finishMutation("ready");
297
+ } catch (error) {
298
+ if (!error.commitState)
299
+ error.commitState = committed ? "committed" : "not-committed";
300
+ if (started)
301
+ finishMutation(
302
+ error.commitState === "unknown" ||
303
+ permissionRevision.state === "invalid"
304
+ ? "invalid"
305
+ : "ready",
306
+ );
307
+ throw error;
308
+ }
188
309
  }
189
310
 
190
311
  list() {
191
- const state = this._read();
192
- const now = this._now();
312
+ const revision = getScopedPermissionRevision();
313
+ const captured = this._capture();
314
+ const state = this._read(captured);
315
+ const now = captured.now();
316
+ if (getScopedPermissionRevision() !== revision)
317
+ throw scopedPermissionError(
318
+ "CC_SCOPED_PERMISSION_AUTHORITY_CHANGED",
319
+ "Scoped permission authority changed while reading rules",
320
+ );
193
321
  return Object.freeze({
194
322
  schema: state.schema,
195
323
  schemaVersion: state.schemaVersion,
196
324
  generation: state.generation,
197
325
  updatedAt: state.updatedAt,
198
326
  workspace: structuredClone(state.workspace),
199
- file: this.filePath,
327
+ file: captured.filePath,
200
328
  rules: state.rules.map((record) =>
201
- projectRecord(record, now, this.filePath),
329
+ projectRecord(record, now, captured.filePath),
202
330
  ),
203
331
  });
204
332
  }
@@ -210,12 +338,13 @@ export class ScopedPermissionStore {
210
338
  reason = "",
211
339
  expectedGeneration = null,
212
340
  } = {}) {
341
+ const captured = this._capture();
213
342
  const normalizedDecision = String(decision || "")
214
343
  .trim()
215
344
  .toLowerCase();
216
345
  const normalizedRule = String(rule || "").trim();
217
346
  const normalizedReason = String(reason || "").trim();
218
- const now = this._now();
347
+ const now = captured.now();
219
348
  const expiry = Number(expiresAt);
220
349
  if (!DECISIONS.has(normalizedDecision)) {
221
350
  throw scopedPermissionError(
@@ -243,71 +372,67 @@ export class ScopedPermissionStore {
243
372
  }
244
373
 
245
374
  let created = null;
246
- mutateSecurityStore(
247
- this.filePath,
248
- STORE_LABEL,
249
- (draft) => {
250
- const current =
251
- Object.keys(draft).length === 0
252
- ? emptyState(this.workspace)
253
- : validateState(draft, this.workspace);
254
- if (
255
- expectedGeneration !== null &&
256
- Number(expectedGeneration) !== current.generation
257
- ) {
258
- throw scopedPermissionError(
259
- SCOPED_PERMISSION_ERROR_CODES.CONFLICT,
260
- `Scoped permission generation changed (expected ${expectedGeneration}, current ${current.generation})`,
261
- );
262
- }
375
+ this._mutate(captured, (draft) => {
376
+ const current =
377
+ Object.keys(draft).length === 0
378
+ ? emptyState(captured.workspace)
379
+ : validateState(draft, captured.workspace);
380
+ if (
381
+ expectedGeneration !== null &&
382
+ Number(expectedGeneration) !== current.generation
383
+ ) {
384
+ throw scopedPermissionError(
385
+ SCOPED_PERMISSION_ERROR_CODES.CONFLICT,
386
+ `Scoped permission generation changed (expected ${expectedGeneration}, current ${current.generation})`,
387
+ );
388
+ }
263
389
 
264
- const retained = [...current.rules];
265
- while (retained.length >= this.maxRecords) {
266
- const inactiveIndex = retained.findIndex(
267
- (record) => statusFor(record, now) !== "active",
268
- );
269
- if (inactiveIndex < 0) break;
270
- retained.splice(inactiveIndex, 1);
271
- }
272
- if (retained.length >= this.maxRecords) {
273
- throw scopedPermissionError(
274
- SCOPED_PERMISSION_ERROR_CODES.INVALID,
275
- `Scoped permission store is full (${this.maxRecords} retained rules)`,
276
- );
277
- }
278
- const id = this._randomId();
279
- if (
280
- !RULE_ID_PATTERN.test(id) ||
281
- retained.some((item) => item.id === id)
282
- ) {
283
- throw scopedPermissionError(
284
- SCOPED_PERMISSION_ERROR_CODES.INVALID,
285
- "Could not allocate a valid unique scoped permission id",
286
- );
287
- }
288
- created = {
289
- id,
290
- decision: normalizedDecision,
291
- rule: normalizedRule,
292
- revision: 1,
293
- createdAt: now,
294
- expiresAt: expiry,
295
- revokedAt: null,
296
- reason: normalizedReason,
297
- };
298
- Object.assign(draft, current, {
299
- generation: current.generation + 1,
300
- updatedAt: now,
301
- rules: [...retained, created],
302
- });
303
- validateState(draft, this.workspace);
304
- },
305
- this._lock ? { lock: this._lock } : undefined,
306
- );
307
- return projectRecord(created, now, this.filePath);
390
+ const retained = [...current.rules];
391
+ while (retained.length >= captured.maxRecords) {
392
+ const inactiveIndex = retained.findIndex(
393
+ (record) => statusFor(record, now) !== "active",
394
+ );
395
+ if (inactiveIndex < 0) break;
396
+ retained.splice(inactiveIndex, 1);
397
+ }
398
+ if (retained.length >= captured.maxRecords) {
399
+ throw scopedPermissionError(
400
+ SCOPED_PERMISSION_ERROR_CODES.INVALID,
401
+ `Scoped permission store is full (${captured.maxRecords} retained rules)`,
402
+ );
403
+ }
404
+ const id = captured.randomId();
405
+ if (
406
+ !RULE_ID_PATTERN.test(id) ||
407
+ retained.some((item) => item.id === id)
408
+ ) {
409
+ throw scopedPermissionError(
410
+ SCOPED_PERMISSION_ERROR_CODES.INVALID,
411
+ "Could not allocate a valid unique scoped permission id",
412
+ );
413
+ }
414
+ created = {
415
+ id,
416
+ decision: normalizedDecision,
417
+ rule: normalizedRule,
418
+ revision: 1,
419
+ createdAt: now,
420
+ expiresAt: expiry,
421
+ revokedAt: null,
422
+ reason: normalizedReason,
423
+ };
424
+ Object.assign(draft, current, {
425
+ generation: nextVersion(current.generation, "generation"),
426
+ updatedAt: now,
427
+ rules: [...retained, created],
428
+ });
429
+ validateState(draft, captured.workspace);
430
+ });
431
+ return projectRecord(created, now, captured.filePath);
308
432
  }
309
433
 
310
434
  revoke({ id, expectedRevision = null } = {}) {
435
+ const captured = this._capture();
311
436
  const normalizedId = String(id || "").trim();
312
437
  if (!RULE_ID_PATTERN.test(normalizedId)) {
313
438
  throw scopedPermissionError(
@@ -315,55 +440,50 @@ export class ScopedPermissionStore {
315
440
  "A valid scoped permission id is required",
316
441
  );
317
442
  }
318
- const now = this._now();
443
+ const now = captured.now();
319
444
  let revoked = null;
320
- mutateSecurityStore(
321
- this.filePath,
322
- STORE_LABEL,
323
- (draft) => {
324
- const current =
325
- Object.keys(draft).length === 0
326
- ? emptyState(this.workspace)
327
- : validateState(draft, this.workspace);
328
- const index = current.rules.findIndex(
329
- (record) => record.id === normalizedId,
445
+ this._mutate(captured, (draft) => {
446
+ const current =
447
+ Object.keys(draft).length === 0
448
+ ? emptyState(captured.workspace)
449
+ : validateState(draft, captured.workspace);
450
+ const index = current.rules.findIndex(
451
+ (record) => record.id === normalizedId,
452
+ );
453
+ if (index < 0) {
454
+ throw scopedPermissionError(
455
+ SCOPED_PERMISSION_ERROR_CODES.NOT_FOUND,
456
+ `Scoped permission rule not found: ${normalizedId}`,
330
457
  );
331
- if (index < 0) {
332
- throw scopedPermissionError(
333
- SCOPED_PERMISSION_ERROR_CODES.NOT_FOUND,
334
- `Scoped permission rule not found: ${normalizedId}`,
335
- );
336
- }
337
- const record = current.rules[index];
338
- if (
339
- expectedRevision !== null &&
340
- Number(expectedRevision) !== record.revision
341
- ) {
342
- throw scopedPermissionError(
343
- SCOPED_PERMISSION_ERROR_CODES.CONFLICT,
344
- `Scoped permission revision changed (expected ${expectedRevision}, current ${record.revision})`,
345
- );
346
- }
347
- if (record.revokedAt !== null) {
348
- revoked = record;
349
- return;
350
- }
351
- revoked = {
352
- ...record,
353
- revision: record.revision + 1,
354
- revokedAt: now,
355
- };
356
- const rules = [...current.rules];
357
- rules[index] = revoked;
358
- Object.assign(draft, current, {
359
- generation: current.generation + 1,
360
- updatedAt: now,
361
- rules,
362
- });
363
- validateState(draft, this.workspace);
364
- },
365
- this._lock ? { lock: this._lock } : undefined,
366
- );
367
- return projectRecord(revoked, now, this.filePath);
458
+ }
459
+ const record = current.rules[index];
460
+ if (
461
+ expectedRevision !== null &&
462
+ Number(expectedRevision) !== record.revision
463
+ ) {
464
+ throw scopedPermissionError(
465
+ SCOPED_PERMISSION_ERROR_CODES.CONFLICT,
466
+ `Scoped permission revision changed (expected ${expectedRevision}, current ${record.revision})`,
467
+ );
468
+ }
469
+ if (record.revokedAt !== null) {
470
+ revoked = record;
471
+ return false;
472
+ }
473
+ revoked = {
474
+ ...record,
475
+ revision: nextVersion(record.revision, "revision"),
476
+ revokedAt: now,
477
+ };
478
+ const rules = [...current.rules];
479
+ rules[index] = revoked;
480
+ Object.assign(draft, current, {
481
+ generation: nextVersion(current.generation, "generation"),
482
+ updatedAt: now,
483
+ rules,
484
+ });
485
+ validateState(draft, captured.workspace);
486
+ });
487
+ return projectRecord(revoked, now, captured.filePath);
368
488
  }
369
489
  }