canopycms 0.0.59 → 0.0.60

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 (109) hide show
  1. package/dist/api/__test__/mock-client.js +4 -4
  2. package/dist/api/__test__/mock-client.js.map +1 -1
  3. package/dist/api/branch-merge.d.ts.map +1 -1
  4. package/dist/api/branch-merge.js +2 -6
  5. package/dist/api/branch-merge.js.map +1 -1
  6. package/dist/api/branch-withdraw.d.ts.map +1 -1
  7. package/dist/api/branch-withdraw.js +19 -4
  8. package/dist/api/branch-withdraw.js.map +1 -1
  9. package/dist/api/entries.d.ts.map +1 -1
  10. package/dist/api/entries.js +22 -3
  11. package/dist/api/entries.js.map +1 -1
  12. package/dist/api/groups.d.ts +3 -0
  13. package/dist/api/groups.d.ts.map +1 -1
  14. package/dist/api/groups.js +89 -68
  15. package/dist/api/groups.js.map +1 -1
  16. package/dist/api/permissions.d.ts +1 -0
  17. package/dist/api/permissions.d.ts.map +1 -1
  18. package/dist/api/permissions.js +23 -20
  19. package/dist/api/permissions.js.map +1 -1
  20. package/dist/api/schema.d.ts.map +1 -1
  21. package/dist/api/schema.js +29 -15
  22. package/dist/api/schema.js.map +1 -1
  23. package/dist/authorization/groups/index.d.ts +1 -1
  24. package/dist/authorization/groups/index.d.ts.map +1 -1
  25. package/dist/authorization/groups/index.js +1 -1
  26. package/dist/authorization/groups/index.js.map +1 -1
  27. package/dist/authorization/groups/loader.d.ts +25 -4
  28. package/dist/authorization/groups/loader.d.ts.map +1 -1
  29. package/dist/authorization/groups/loader.js +39 -20
  30. package/dist/authorization/groups/loader.js.map +1 -1
  31. package/dist/authorization/groups/schema.d.ts +8 -7
  32. package/dist/authorization/groups/schema.d.ts.map +1 -1
  33. package/dist/authorization/groups/schema.js +8 -4
  34. package/dist/authorization/groups/schema.js.map +1 -1
  35. package/dist/authorization/index.d.ts +4 -3
  36. package/dist/authorization/index.d.ts.map +1 -1
  37. package/dist/authorization/index.js +6 -3
  38. package/dist/authorization/index.js.map +1 -1
  39. package/dist/authorization/path.d.ts +8 -0
  40. package/dist/authorization/path.d.ts.map +1 -1
  41. package/dist/authorization/path.js +16 -1
  42. package/dist/authorization/path.js.map +1 -1
  43. package/dist/authorization/permissions/index.d.ts +1 -1
  44. package/dist/authorization/permissions/index.d.ts.map +1 -1
  45. package/dist/authorization/permissions/index.js +1 -1
  46. package/dist/authorization/permissions/index.js.map +1 -1
  47. package/dist/authorization/permissions/loader.d.ts +18 -4
  48. package/dist/authorization/permissions/loader.d.ts.map +1 -1
  49. package/dist/authorization/permissions/loader.js +35 -24
  50. package/dist/authorization/permissions/loader.js.map +1 -1
  51. package/dist/authorization/permissions/schema.d.ts +8 -7
  52. package/dist/authorization/permissions/schema.d.ts.map +1 -1
  53. package/dist/authorization/permissions/schema.js +8 -4
  54. package/dist/authorization/permissions/schema.js.map +1 -1
  55. package/dist/authorization/settings-file-store.d.ts +127 -0
  56. package/dist/authorization/settings-file-store.d.ts.map +1 -0
  57. package/dist/authorization/settings-file-store.js +156 -0
  58. package/dist/authorization/settings-file-store.js.map +1 -0
  59. package/dist/branch-metadata.d.ts +11 -0
  60. package/dist/branch-metadata.d.ts.map +1 -1
  61. package/dist/branch-metadata.js +22 -0
  62. package/dist/branch-metadata.js.map +1 -1
  63. package/dist/cli/cli.js +245 -188
  64. package/dist/cli/generate-ai-content.js +18 -3
  65. package/dist/cli/init.js +12 -2
  66. package/dist/cli/migrate.d.ts.map +1 -1
  67. package/dist/cli/migrate.js +39 -11
  68. package/dist/cli/migrate.js.map +1 -1
  69. package/dist/config/index.d.ts +1 -1
  70. package/dist/config/index.d.ts.map +1 -1
  71. package/dist/config/index.js.map +1 -1
  72. package/dist/config/schemas/config.d.ts +55 -5
  73. package/dist/config/schemas/config.d.ts.map +1 -1
  74. package/dist/config/schemas/config.js +18 -1
  75. package/dist/config/schemas/config.js.map +1 -1
  76. package/dist/config/types.d.ts +11 -1
  77. package/dist/config/types.d.ts.map +1 -1
  78. package/dist/context.d.ts +3 -0
  79. package/dist/context.d.ts.map +1 -1
  80. package/dist/context.js +25 -3
  81. package/dist/context.js.map +1 -1
  82. package/dist/editor/BranchManager.d.ts +3 -0
  83. package/dist/editor/BranchManager.d.ts.map +1 -1
  84. package/dist/editor/BranchManager.js +18 -5
  85. package/dist/editor/BranchManager.js.map +1 -1
  86. package/dist/editor/hooks/useBranchManager.d.ts +3 -1
  87. package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
  88. package/dist/editor/hooks/useBranchManager.js +2 -0
  89. package/dist/editor/hooks/useBranchManager.js.map +1 -1
  90. package/dist/editor/hooks/useGroupManager.d.ts.map +1 -1
  91. package/dist/editor/hooks/useGroupManager.js +13 -2
  92. package/dist/editor/hooks/useGroupManager.js.map +1 -1
  93. package/dist/editor/hooks/usePermissionManager.d.ts.map +1 -1
  94. package/dist/editor/hooks/usePermissionManager.js +13 -2
  95. package/dist/editor/hooks/usePermissionManager.js.map +1 -1
  96. package/dist/git-manager.d.ts.map +1 -1
  97. package/dist/git-manager.js +6 -0
  98. package/dist/git-manager.js.map +1 -1
  99. package/dist/schema/schema-store.d.ts +98 -0
  100. package/dist/schema/schema-store.d.ts.map +1 -1
  101. package/dist/schema/schema-store.js +187 -29
  102. package/dist/schema/schema-store.js.map +1 -1
  103. package/dist/types.d.ts +13 -0
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/worker/cms-worker.d.ts +36 -0
  106. package/dist/worker/cms-worker.d.ts.map +1 -1
  107. package/dist/worker/cms-worker.js +218 -8
  108. package/dist/worker/cms-worker.js.map +1 -1
  109. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/authorization/groups/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACrB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,yBAAyB;IAChE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAA6B;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CACb,CAAC,CAAC,MAAM,CAAC;QACP,EAAE,EAAE,CAAC,CAAC,MAAM,EAA8B;QAC1C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACvB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAClC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA6B,CAAC;KACxD,CAAC,CACH;CACF,CAAC,CAAA;AAcF;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAoB;IAC1D,OAAO;QACL,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,MAAM;QACjB,MAAM,EAAE,EAAE;KACX,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/authorization/groups/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,2EAA2E;IAC3E,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IAClD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAA6B;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CACb,CAAC,CAAC,MAAM,CAAC;QACP,EAAE,EAAE,CAAC,CAAC,MAAM,EAA8B;QAC1C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACvB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAClC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA6B,CAAC;KACxD,CAAC,CACH;CACF,CAAC,CAAA;AAcF;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAoB;IAC1D,OAAO;QACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,MAAM;QACjB,MAAM,EAAE,EAAE;KACX,CAAA;AACH,CAAC"}
@@ -29,8 +29,9 @@ export type { BranchAccessResult, PathPermissionResult, ContentAccessResult, Con
29
29
  export { parsePermissionPath } from './validation';
30
30
  export { checkContentAccess, createCheckContentAccess, createContentAccessChecker, type ContentAccessChecker, } from './content';
31
31
  export { checkBranchAccessWithDefault, createCheckBranchAccess, canPerformWorkflowAction, } from './branch';
32
- export { checkPathAccess, createCheckPathAccess } from './path';
32
+ export { checkPathAccess, createCheckPathAccess, resolveDefaultPathAccess } from './path';
33
33
  export { RESERVED_GROUPS, type ReservedGroupId, isReservedGroup, stripReservedGroups, isAdmin, isReviewer, isPrivileged, } from './helpers';
34
- export { PermissionsFileSchema, createDefaultPermissionsFile, type PermissionsFile, loadPermissionsFile, loadPathPermissions, savePathPermissions, ensurePermissionsFile, } from './permissions';
35
- export { GroupsFileSchema, createDefaultGroupsFile, type GroupsFile, type InternalGroup, loadGroupsFile, loadInternalGroups, saveInternalGroups, } from './groups';
34
+ export { PermissionsFileSchema, createDefaultPermissionsFile, type PermissionsFile, loadPermissionsFile, loadPathPermissions, mutatePermissionsFile, ensurePermissionsFile, } from './permissions';
35
+ export { GroupsFileSchema, createDefaultGroupsFile, type GroupsFile, type InternalGroup, loadGroupsFile, loadInternalGroups, deriveInternalGroups, mutateGroupsFile, } from './groups';
36
+ export { mutateSettingsJsonFile, SettingsFileConflictError, SettingsVersionConflictError, type MutateSettingsFileOptions, } from './settings-file-store';
36
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/authorization/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,YAAY,EACV,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,GACf,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAA;AAGlB,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,UAAU,CAAA;AAGjB,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAA;AAG/D,OAAO,EACL,eAAe,EACf,KAAK,eAAe,EACpB,eAAe,EACf,mBAAmB,EACnB,OAAO,EACP,UAAU,EACV,YAAY,GACb,MAAM,WAAW,CAAA;AAGlB,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAC5B,KAAK,eAAe,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/authorization/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,YAAY,EACV,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,GACf,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAA;AAGlB,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,UAAU,CAAA;AAGjB,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,QAAQ,CAAA;AAGzF,OAAO,EACL,eAAe,EACf,KAAK,eAAe,EACpB,eAAe,EACf,mBAAmB,EACnB,OAAO,EACP,UAAU,EACV,YAAY,GACb,MAAM,WAAW,CAAA;AAGlB,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAC5B,KAAK,eAAe,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,UAAU,CAAA;AAIjB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,4BAA4B,EAC5B,KAAK,yBAAyB,GAC/B,MAAM,uBAAuB,CAAA"}
@@ -32,11 +32,14 @@ export { checkContentAccess, createCheckContentAccess, createContentAccessChecke
32
32
  // Branch-level access
33
33
  export { checkBranchAccessWithDefault, createCheckBranchAccess, canPerformWorkflowAction, } from './branch.js';
34
34
  // Path-level access
35
- export { checkPathAccess, createCheckPathAccess } from './path.js';
35
+ export { checkPathAccess, createCheckPathAccess, resolveDefaultPathAccess } from './path.js';
36
36
  // Helper functions
37
37
  export { RESERVED_GROUPS, isReservedGroup, stripReservedGroups, isAdmin, isReviewer, isPrivileged, } from './helpers.js';
38
38
  // Permissions file handling
39
- export { PermissionsFileSchema, createDefaultPermissionsFile, loadPermissionsFile, loadPathPermissions, savePathPermissions, ensurePermissionsFile, } from './permissions/index.js';
39
+ export { PermissionsFileSchema, createDefaultPermissionsFile, loadPermissionsFile, loadPathPermissions, mutatePermissionsFile, ensurePermissionsFile, } from './permissions/index.js';
40
40
  // Groups file handling
41
- export { GroupsFileSchema, createDefaultGroupsFile, loadGroupsFile, loadInternalGroups, saveInternalGroups, } from './groups/index.js';
41
+ export { GroupsFileSchema, createDefaultGroupsFile, loadGroupsFile, loadInternalGroups, deriveInternalGroups, mutateGroupsFile, } from './groups/index.js';
42
+ // Settings file store — cross-host layered locking shared by
43
+ // permissions.json and groups.json (see authorization/settings-file-store.ts)
44
+ export { mutateSettingsJsonFile, SettingsFileConflictError, SettingsVersionConflictError, } from './settings-file-store.js';
42
45
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/authorization/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAWH,aAAa;AACb,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAElD,mDAAmD;AACnD,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,GAE3B,MAAM,WAAW,CAAA;AAElB,sBAAsB;AACtB,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,UAAU,CAAA;AAEjB,oBAAoB;AACpB,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAA;AAE/D,mBAAmB;AACnB,OAAO,EACL,eAAe,EAEf,eAAe,EACf,mBAAmB,EACnB,OAAO,EACP,UAAU,EACV,YAAY,GACb,MAAM,WAAW,CAAA;AAElB,4BAA4B;AAC5B,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAE5B,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,eAAe,CAAA;AAEtB,uBAAuB;AACvB,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EAGvB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/authorization/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAWH,aAAa;AACb,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAElD,mDAAmD;AACnD,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,GAE3B,MAAM,WAAW,CAAA;AAElB,sBAAsB;AACtB,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,UAAU,CAAA;AAEjB,oBAAoB;AACpB,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,QAAQ,CAAA;AAEzF,mBAAmB;AACnB,OAAO,EACL,eAAe,EAEf,eAAe,EACf,mBAAmB,EACnB,OAAO,EACP,UAAU,EACV,YAAY,GACb,MAAM,WAAW,CAAA;AAElB,4BAA4B;AAC5B,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAE5B,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,eAAe,CAAA;AAEtB,uBAAuB;AACvB,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EAGvB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,UAAU,CAAA;AAEjB,6DAA6D;AAC7D,8EAA8E;AAC9E,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,4BAA4B,GAE7B,MAAM,uBAAuB,CAAA"}
@@ -7,6 +7,14 @@ import type { PathPermission, DefaultPathAccess, PermissionLevel } from '../conf
7
7
  import type { CanopyUser } from '../user';
8
8
  import type { PathPermissionResult } from './types';
9
9
  import type { PhysicalPath } from '../paths/types';
10
+ /**
11
+ * Resolve a `defaultPathAccess` config value to an 'allow'/'deny' verdict for one
12
+ * permission level. String form applies the same value to every level (unchanged
13
+ * behavior). Object form looks up the level; an absent level resolves to 'deny'
14
+ * (fail-closed), so scoping e.g. `{ read: 'allow' }` doesn't accidentally open
15
+ * edit/review.
16
+ */
17
+ export declare function resolveDefaultPathAccess(defaultAccess: DefaultPathAccess, level: PermissionLevel): 'allow' | 'deny';
10
18
  /**
11
19
  * Evaluate access for a relative path against config-defined rules.
12
20
  * Uses defaultAccess when no rule matches. First matching rule wins.
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/authorization/path.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EAEhB,MAAM,WAAW,CAAA;AAElB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAoClD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,YAAY,EACZ,IAAI,EACJ,aAAa,EACb,KAAK,GACN,EAAE;IACD,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,YAAY,EAAE,YAAY,CAAA;IAC1B,IAAI,EAAE,UAAU,CAAA;IAChB,aAAa,EAAE,iBAAiB,CAAA;IAChC,KAAK,EAAE,eAAe,CAAA;CACvB,GAAG,oBAAoB,CA6BvB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,aAAa,EAAE,iBAAiB,IACrF,OAAO;IACb,YAAY,EAAE,YAAY,CAAA;IAC1B,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,eAAe,CAAA;CACvB,KAAG,oBAAoB,CACzB"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/authorization/path.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EAEhB,MAAM,WAAW,CAAA;AAElB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAoClD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,iBAAiB,EAChC,KAAK,EAAE,eAAe,GACrB,OAAO,GAAG,MAAM,CAGlB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,YAAY,EACZ,IAAI,EACJ,aAAa,EACb,KAAK,GACN,EAAE;IACD,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,YAAY,EAAE,YAAY,CAAA;IAC1B,IAAI,EAAE,UAAU,CAAA;IAChB,aAAa,EAAE,iBAAiB,CAAA;IAChC,KAAK,EAAE,eAAe,CAAA;CACvB,GAAG,oBAAoB,CAgCvB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,aAAa,EAAE,iBAAiB,IACrF,OAAO;IACb,YAAY,EAAE,YAAY,CAAA;IAC1B,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,eAAe,CAAA;CACvB,KAAG,oBAAoB,CACzB"}
@@ -33,6 +33,18 @@ function isAllowedByTarget(target, user) {
33
33
  const matchesGroup = hasGroupConstraint && user.groups?.some((gid) => target.allowedGroups?.includes(gid));
34
34
  return Boolean(matchesUser || matchesGroup);
35
35
  }
36
+ /**
37
+ * Resolve a `defaultPathAccess` config value to an 'allow'/'deny' verdict for one
38
+ * permission level. String form applies the same value to every level (unchanged
39
+ * behavior). Object form looks up the level; an absent level resolves to 'deny'
40
+ * (fail-closed), so scoping e.g. `{ read: 'allow' }` doesn't accidentally open
41
+ * edit/review.
42
+ */
43
+ export function resolveDefaultPathAccess(defaultAccess, level) {
44
+ if (typeof defaultAccess === 'string')
45
+ return defaultAccess;
46
+ return defaultAccess[level] ?? 'deny';
47
+ }
36
48
  /**
37
49
  * Evaluate access for a relative path against config-defined rules.
38
50
  * Uses defaultAccess when no rule matches. First matching rule wins.
@@ -60,7 +72,10 @@ export function checkPathAccess({ rules, relativePath, user, defaultAccess, leve
60
72
  reason: allowed ? 'allowed_by_rule' : 'denied_by_rule',
61
73
  };
62
74
  }
63
- return { allowed: defaultAccess === 'allow', reason: 'no_rule_match' };
75
+ return {
76
+ allowed: resolveDefaultPathAccess(defaultAccess, level) === 'allow',
77
+ reason: 'no_rule_match',
78
+ };
64
79
  }
65
80
  /**
66
81
  * Factory to bind rules and defaultAccess once.
@@ -1 +1 @@
1
- {"version":3,"file":"path.js","sourceRoot":"","sources":["../../src/authorization/path.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAQrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKnC;;GAEG;AACH,SAAS,SAAS,CAAC,CAAe;IAChC,MAAM,UAAU,GAAI,CAAY,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC1D,OAAO,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAiB,CAAA;AAC1D,CAAC;AAED;;GAEG;AACH,SAAS,WAAW,CAAC,IAAoB,EAAE,YAA0B;IACnE,OAAO,SAAS,CAAC,YAAsB,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;AACpE,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB,CAAC,MAAwB,EAAE,IAAgB;IACnE,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAA;IACvD,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAA;IAEzD,0DAA0D;IAC1D,IAAI,CAAC,iBAAiB,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC9C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,WAAW,GAAG,iBAAiB,IAAI,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACnF,MAAM,YAAY,GAChB,kBAAkB,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;IAEvF,OAAO,OAAO,CAAC,WAAW,IAAI,YAAY,CAAC,CAAA;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,KAAK,EACL,YAAY,EACZ,IAAI,EACJ,aAAa,EACb,KAAK,GAON;IACC,MAAM,cAAc,GAAG,SAAS,CAAC,YAAY,CAAC,CAAA;IAE9C,0CAA0C;IAC1C,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IAC3C,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,cAAc,CAAC,EAAE,CAAC;YACvC,SAAQ;QACV,CAAC;QAED,2CAA2C;QAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;QAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,4EAA4E;YAC5E,SAAQ;QACV,CAAC;QAED,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QAC/C,OAAO;YACL,OAAO;YACP,WAAW,EAAE,IAAI;YACjB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,gBAAgB;SACvD,CAAA;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;AACxE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAuB,EAAE,aAAgC;IAC7F,OAAO,CAAC,KAIP,EAAwB,EAAE,CAAC,eAAe,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAA;AACjF,CAAC"}
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../../src/authorization/path.ts"],"names":[],"mappings":"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"}
@@ -2,5 +2,5 @@
2
2
  * Permissions module exports
3
3
  */
4
4
  export { PermissionsFileSchema, createDefaultPermissionsFile, type PermissionsFile } from './schema';
5
- export { loadPermissionsFile, loadPathPermissions, savePathPermissions, ensurePermissionsFile, } from './loader';
5
+ export { loadPermissionsFile, loadPathPermissions, mutatePermissionsFile, ensurePermissionsFile, } from './loader';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/authorization/permissions/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AACpG,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/authorization/permissions/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AACpG,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAA"}
@@ -2,5 +2,5 @@
2
2
  * Permissions module exports
3
3
  */
4
4
  export { PermissionsFileSchema, createDefaultPermissionsFile } from './schema.js';
5
- export { loadPermissionsFile, loadPathPermissions, savePathPermissions, ensurePermissionsFile, } from './loader.js';
5
+ export { loadPermissionsFile, loadPathPermissions, mutatePermissionsFile, ensurePermissionsFile, } from './loader.js';
6
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/authorization/permissions/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAwB,MAAM,UAAU,CAAA;AACpG,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/authorization/permissions/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAwB,MAAM,UAAU,CAAA;AACpG,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAA"}
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * Permissions file loader
3
3
  *
4
- * Handles loading and saving path permissions from the filesystem.
4
+ * Handles loading path permissions from the filesystem and mutating
5
+ * permissions.json under the cross-host layered lock in
6
+ * authorization/settings-file-store.ts.
5
7
  */
6
8
  import type { PathPermission } from '../../config';
7
9
  import type { PermissionsFile } from './schema';
8
10
  import type { OperatingMode } from '../../operating-mode';
11
+ import type { OccWriteResult } from '../../utils/occ-json-write';
9
12
  /**
10
13
  * Load full permissions file (for version checking)
11
14
  * Returns null if file doesn't exist.
@@ -23,11 +26,22 @@ export declare function loadPermissionsFile(repoRoot: string, mode: OperatingMod
23
26
  */
24
27
  export declare function loadPathPermissions(repoRoot: string, mode: OperatingMode): Promise<PathPermission[]>;
25
28
  /**
26
- * Save path permissions to .canopycms/permissions.json (or .local.json in dev mode)
29
+ * Mutate permissions.json (or .local.json in dev mode) under the full
30
+ * cross-host lock + OCC-retry stack (see
31
+ * authorization/settings-file-store.ts). `mutate` is called with the
32
+ * current parsed file (`null` if it doesn't exist yet) and the version to
33
+ * write under; it returns the next raw payload, or `null` for a deliberate
34
+ * no-op. The returned payload is validated against
35
+ * {@link PermissionsFileSchema} before being written, preserving the
36
+ * previous validate-before-write behavior of the old `savePathPermissions`.
27
37
  */
28
- export declare function savePathPermissions(repoRoot: string, permissions: PathPermission[], updatedBy: string, mode: OperatingMode, contentVersion?: number): Promise<void>;
38
+ export declare function mutatePermissionsFile(repoRoot: string, mode: OperatingMode, mutate: (current: PermissionsFile | null, version: number) => Record<string, unknown> | null, options?: {
39
+ settleMs?: number;
40
+ maxAttempts?: number;
41
+ }): Promise<OccWriteResult | null>;
29
42
  /**
30
- * Initialize permissions file if it doesn't exist
43
+ * Initialize permissions file if it doesn't exist. A no-op when the file is
44
+ * already present (the mutator returns `null`, so no write happens).
31
45
  */
32
46
  export declare function ensurePermissionsFile(repoRoot: string, userId: string, mode: OperatingMode): Promise<void>;
33
47
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/authorization/permissions/loader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAWzD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAoBjC;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAG3B;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,cAAc,EAAE,EAC7B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,aAAa,EACnB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAUf"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/authorization/permissions/loader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAShE;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAoBjC;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAG3B;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EAC5F,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACpD,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAahC;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAWf"}
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Permissions file loader
3
3
  *
4
- * Handles loading and saving path permissions from the filesystem.
4
+ * Handles loading path permissions from the filesystem and mutating
5
+ * permissions.json under the cross-host layered lock in
6
+ * authorization/settings-file-store.ts.
5
7
  */
6
8
  import fs from 'node:fs/promises';
7
9
  import { PermissionsFileSchema } from './schema.js';
8
10
  import { operatingStrategy } from '../../operating-mode/index.js';
9
- import { atomicWriteFile } from '../../utils/atomic-write.js';
11
+ import { mutateSettingsJsonFile } from '../settings-file-store.js';
10
12
  /**
11
13
  * Get the appropriate permissions file path based on mode
12
14
  */
@@ -50,33 +52,42 @@ export async function loadPathPermissions(repoRoot, mode) {
50
52
  return file?.pathPermissions ?? [];
51
53
  }
52
54
  /**
53
- * Save path permissions to .canopycms/permissions.json (or .local.json in dev mode)
55
+ * Mutate permissions.json (or .local.json in dev mode) under the full
56
+ * cross-host lock + OCC-retry stack (see
57
+ * authorization/settings-file-store.ts). `mutate` is called with the
58
+ * current parsed file (`null` if it doesn't exist yet) and the version to
59
+ * write under; it returns the next raw payload, or `null` for a deliberate
60
+ * no-op. The returned payload is validated against
61
+ * {@link PermissionsFileSchema} before being written, preserving the
62
+ * previous validate-before-write behavior of the old `savePathPermissions`.
54
63
  */
55
- export async function savePathPermissions(repoRoot, permissions, updatedBy, mode, contentVersion) {
64
+ export async function mutatePermissionsFile(repoRoot, mode, mutate, options) {
56
65
  const permissionsPath = getPermissionsFilePath(repoRoot, mode);
57
- const permissionsFile = {
58
- version: 1,
59
- contentVersion: contentVersion ?? 1,
60
- updatedAt: new Date().toISOString(),
61
- updatedBy,
62
- pathPermissions: permissions,
63
- };
64
- // Validate before writing
65
- PermissionsFileSchema.parse(permissionsFile);
66
- await atomicWriteFile(permissionsPath, JSON.stringify(permissionsFile, null, 2));
66
+ return mutateSettingsJsonFile({
67
+ filePath: permissionsPath,
68
+ parse: (raw) => PermissionsFileSchema.parse(JSON.parse(raw)),
69
+ mutate: (current, version) => {
70
+ const payload = mutate(current, version);
71
+ return payload === null ? null : PermissionsFileSchema.parse(payload);
72
+ },
73
+ settleMs: options?.settleMs,
74
+ maxAttempts: options?.maxAttempts,
75
+ });
67
76
  }
68
77
  /**
69
- * Initialize permissions file if it doesn't exist
78
+ * Initialize permissions file if it doesn't exist. A no-op when the file is
79
+ * already present (the mutator returns `null`, so no write happens).
70
80
  */
71
81
  export async function ensurePermissionsFile(repoRoot, userId, mode) {
72
- const permissionsPath = getPermissionsFilePath(repoRoot, mode);
73
- try {
74
- await fs.access(permissionsPath);
75
- // File exists, nothing to do
76
- }
77
- catch {
78
- // File doesn't exist, create default
79
- await savePathPermissions(repoRoot, [], userId, mode);
80
- }
82
+ await mutatePermissionsFile(repoRoot, mode, (current) => {
83
+ if (current) {
84
+ return null;
85
+ }
86
+ return {
87
+ updatedAt: new Date().toISOString(),
88
+ updatedBy: userId,
89
+ pathPermissions: [],
90
+ };
91
+ });
81
92
  }
82
93
  //# sourceMappingURL=loader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/authorization/permissions/loader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAGjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAEhD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAE1D;;GAEG;AACH,SAAS,sBAAsB,CAAC,QAAgB,EAAE,IAAmB;IACnE,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAA;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,IAAmB;IAEnB,MAAM,eAAe,GAAG,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAE9D,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,CAAA;QAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;QACtC,MAAM,SAAS,GAAG,qBAAqB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACrD,OAAO,SAAS,CAAA;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,qBAAqB;QACrB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACvD,OAAO,IAAI,CAAA;QACb,CAAC;QAED,gDAAgD;QAChD,OAAO,CAAC,KAAK,CAAC,6CAA6C,EAAE,KAAK,CAAC,CAAA;QACnE,MAAM,IAAI,KAAK,CACb,6BAA6B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,CACxF,CAAA;IACH,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,IAAmB;IAEnB,MAAM,IAAI,GAAG,MAAM,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACtD,OAAO,IAAI,EAAE,eAAe,IAAI,EAAE,CAAA;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,WAA6B,EAC7B,SAAiB,EACjB,IAAmB,EACnB,cAAuB;IAEvB,MAAM,eAAe,GAAG,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAE9D,MAAM,eAAe,GAAoB;QACvC,OAAO,EAAE,CAAC;QACV,cAAc,EAAE,cAAc,IAAI,CAAC;QACnC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS;QACT,eAAe,EAAE,WAAW;KAC7B,CAAA;IAED,0BAA0B;IAC1B,qBAAqB,CAAC,KAAK,CAAC,eAAe,CAAC,CAAA;IAE5C,MAAM,eAAe,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;AAClF,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,QAAgB,EAChB,MAAc,EACd,IAAmB;IAEnB,MAAM,eAAe,GAAG,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAE9D,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;QAChC,6BAA6B;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,qCAAqC;QACrC,MAAM,mBAAmB,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;IACvD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/authorization/permissions/loader.ts"],"names":[],"mappings":"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"}
@@ -11,8 +11,8 @@ import type { PermissionPath } from '../types';
11
11
  * Any path containing '..' or other traversal sequences will be rejected.
12
12
  */
13
13
  export declare const PermissionsFileSchema: z.ZodObject<{
14
- version: z.ZodLiteral<1>;
15
- contentVersion: z.ZodOptional<z.ZodNumber>;
14
+ version: z.ZodOptional<z.ZodNumber>;
15
+ writeId: z.ZodOptional<z.ZodString>;
16
16
  updatedAt: z.ZodString;
17
17
  updatedBy: z.ZodType<CanopyUserId>;
18
18
  pathPermissions: z.ZodArray<z.ZodObject<{
@@ -81,7 +81,6 @@ export declare const PermissionsFileSchema: z.ZodObject<{
81
81
  } | undefined;
82
82
  }>, "many">;
83
83
  }, "strip", z.ZodTypeAny, {
84
- version: 1;
85
84
  updatedAt: string;
86
85
  updatedBy: string;
87
86
  pathPermissions: {
@@ -101,9 +100,9 @@ export declare const PermissionsFileSchema: z.ZodObject<{
101
100
  allowedGroups?: string[] | undefined;
102
101
  } | undefined;
103
102
  }[];
104
- contentVersion?: number | undefined;
103
+ version?: number | undefined;
104
+ writeId?: string | undefined;
105
105
  }, {
106
- version: 1;
107
106
  updatedAt: string;
108
107
  updatedBy: string;
109
108
  pathPermissions: {
@@ -123,11 +122,13 @@ export declare const PermissionsFileSchema: z.ZodObject<{
123
122
  allowedGroups?: string[] | undefined;
124
123
  } | undefined;
125
124
  }[];
126
- contentVersion?: number | undefined;
125
+ version?: number | undefined;
126
+ writeId?: string | undefined;
127
127
  }>;
128
128
  export type PermissionsFile = z.infer<typeof PermissionsFileSchema>;
129
129
  /**
130
- * Default permissions file
130
+ * Default permissions file. Omits `version`/`writeId` — the writer
131
+ * (mutateSettingsJsonFile via writeOccJsonFile) manages those.
131
132
  */
132
133
  export declare function createDefaultPermissionsFile(userId: CanopyUserId): PermissionsFile;
133
134
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/authorization/permissions/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,aAAa,CAAA;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAyB9C;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;eAIP,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAShD,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAEnE;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,CAOlF"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/authorization/permissions/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,aAAa,CAAA;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAyB9C;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;eAQP,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAShD,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAEnE;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,CAMlF"}
@@ -31,8 +31,12 @@ const permissionPathSchema = z
31
31
  * Any path containing '..' or other traversal sequences will be rejected.
32
32
  */
33
33
  export const PermissionsFileSchema = z.object({
34
- version: z.literal(1),
35
- contentVersion: z.number().optional(), // For optimistic locking
34
+ // Managed by writeOccJsonFile (see authorization/settings-file-store.ts) —
35
+ // the single OCC counter for this file. Optional so a hand-written file
36
+ // with no version field parses as version 0. A legacy `contentVersion`
37
+ // field (if present) is silently stripped by this non-strict zod parse.
38
+ version: z.number().int().nonnegative().optional(),
39
+ writeId: z.string().optional(),
36
40
  updatedAt: z.string().datetime(),
37
41
  updatedBy: z.string(),
38
42
  pathPermissions: z.array(z.object({
@@ -43,11 +47,11 @@ export const PermissionsFileSchema = z.object({
43
47
  })),
44
48
  });
45
49
  /**
46
- * Default permissions file
50
+ * Default permissions file. Omits `version`/`writeId` — the writer
51
+ * (mutateSettingsJsonFile via writeOccJsonFile) manages those.
47
52
  */
48
53
  export function createDefaultPermissionsFile(userId) {
49
54
  return {
50
- version: 1,
51
55
  updatedAt: new Date().toISOString(),
52
56
  updatedBy: userId,
53
57
  pathPermissions: [],
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/authorization/permissions/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAGnD,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA6B,CAAC,CAAC,QAAQ,EAAE;IACvE,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA8B,CAAC,CAAC,QAAQ,EAAE;CAC1E,CAAC,CAAA;AAEF;;GAEG;AACH,MAAM,oBAAoB,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,GAAG,CAAC,CAAC,CAAC;KACN,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACtB,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAA;IACvC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,OAAO,EAAE,MAAM,CAAC,KAAK;SACtB,CAAC,CAAA;QACF,OAAO,CAAC,CAAC,KAAK,CAAA;IAChB,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAA;AACpB,CAAC,CAAyC,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACrB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,yBAAyB;IAChE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAA6B;IAChD,eAAe,EAAE,CAAC,CAAC,KAAK,CACtB,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,oBAAoB;QAC1B,IAAI,EAAE,sBAAsB,CAAC,QAAQ,EAAE;QACvC,IAAI,EAAE,sBAAsB,CAAC,QAAQ,EAAE;QACvC,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE;KAC1C,CAAC,CACH;CACF,CAAC,CAAA;AAIF;;GAEG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAoB;IAC/D,OAAO;QACL,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,MAAM;QACjB,eAAe,EAAE,EAAE;KACpB,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/authorization/permissions/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAGnD,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA6B,CAAC,CAAC,QAAQ,EAAE;IACvE,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAA8B,CAAC,CAAC,QAAQ,EAAE;CAC1E,CAAC,CAAA;AAEF;;GAEG;AACH,MAAM,oBAAoB,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,GAAG,CAAC,CAAC,CAAC;KACN,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACtB,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAA;IACvC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,OAAO,EAAE,MAAM,CAAC,KAAK;SACtB,CAAC,CAAA;QACF,OAAO,CAAC,CAAC,KAAK,CAAA;IAChB,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAA;AACpB,CAAC,CAAyC,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,2EAA2E;IAC3E,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IAClD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAA6B;IAChD,eAAe,EAAE,CAAC,CAAC,KAAK,CACtB,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,oBAAoB;QAC1B,IAAI,EAAE,sBAAsB,CAAC,QAAQ,EAAE;QACvC,IAAI,EAAE,sBAAsB,CAAC,QAAQ,EAAE;QACvC,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE;KAC1C,CAAC,CACH;CACF,CAAC,CAAA;AAIF;;;GAGG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAoB;IAC/D,OAAO;QACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,MAAM;QACjB,eAAe,EAAE,EAAE;KACpB,CAAA;AACH,CAAC"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Cross-host layered locking for settings JSON files (permissions.json,
3
+ * groups.json) in the settings workspace — a single global orphan-git-branch
4
+ * checkout at `{settingsRoot}` shared by every branch (see
5
+ * `api/settings-helpers.ts`'s `getSettingsBranchContext`).
6
+ *
7
+ * Without this helper, the write path is a classic unprotected TOCTOU: load
8
+ * -> compare -> mutate -> write with no lock spanning the cycle, so two warm
9
+ * Lambda containers (separate NFS clients on EFS) can each read the same
10
+ * pre-mutation file and have the second write silently clobber the first.
11
+ * `mutateSettingsJsonFile` closes that with the standard 3-layer recipe from
12
+ * docs/concurrency.md, structured identically to {@link CommentStore}'s
13
+ * `withMutation` (see comment-store.ts) and `BranchMetadataFileManager.save()`
14
+ * (see branch-metadata.ts):
15
+ *
16
+ * 1. {@link withLock} - an in-process FIFO mutex keyed by the resolved file
17
+ * path. Deterministic same-process serialization.
18
+ * 2. {@link withOccFileLock} - a server-enforced, cross-process/cross-host
19
+ * lock (proper-lockfile, mkdir-based), immune to NFS client dentry/
20
+ * attribute caching. This is the actual fix for a lost permission/group
21
+ * edit across two warm Lambda containers on EFS.
22
+ * 3. {@link withOccRetry} around {@link writeOccJsonFile} - version/writeId
23
+ * based optimistic concurrency control, reloading the file fresh on
24
+ * EVERY retry attempt. With layers 1-2 in place this is defense-in-depth
25
+ * (e.g. a stale process from a rolling deploy writing without the lock),
26
+ * not the primary safety mechanism.
27
+ *
28
+ * `writeOccJsonFile`'s managed `version`/`writeId` pair is now THE single
29
+ * counter for these files: the old hand-rolled `version: 1` format literal
30
+ * and the separate `contentVersion` field are gone. A pre-existing file on
31
+ * disk that still says `"version": 1` simply reads as OCC version 1 and
32
+ * continues incrementing from there; a leftover `contentVersion` key is
33
+ * silently stripped by the (non-strict, extra-keys-stripped) zod parse.
34
+ *
35
+ * Three caveats specific to these files, on top of the generic guarantee
36
+ * documented on `utils/occ-json-write.ts`:
37
+ *
38
+ * (a) UNLIKE comments.json/branch.json (which never leave the branch
39
+ * workspace), permissions.json/groups.json are git-committed on the
40
+ * settings orphan branch. `commitSettings()` (api/settings-helpers.ts)
41
+ * calls `commitToSettingsBranch`, whose `pullCurrentBranch()` merge runs
42
+ * AFTER this helper has released its lock, and that merge can rewrite
43
+ * the file's `version` from upstream. So, unlike branch.json, `version`
44
+ * here is NOT guaranteed monotonic — it remains a correctness aid (it
45
+ * still catches the common same-host and short-window cross-host races)
46
+ * but is advisory/defense-in-depth, not a hard guarantee. The LOCKFILE
47
+ * (layer 2) is the actual cross-host correctness mechanism.
48
+ * (b) The git commit+push deliberately happens OUTSIDE this lock (mirrors
49
+ * branch-metadata.ts keeping registry invalidation outside the lock it
50
+ * protects): committing/pushing is comparatively slow network/process
51
+ * I/O, and holding a mkdir-based lock across it would serialize
52
+ * unrelated requests behind that I/O for no correctness gain — only the
53
+ * write to the working tree needs the lock.
54
+ * (c) Because commit+push is outside the lock, two writers' save-then-commit
55
+ * sequences can interleave (A saves+commits+pushes, B's save lands and
56
+ * commits before A's push is visible, or similar), and the second
57
+ * `git commit` can run against an already-clean tree. Verified benign
58
+ * with simple-git 3.36: its task-error detection depends on stderr
59
+ * output, and a clean-tree `git commit` exits 1 with "nothing to
60
+ * commit" on STDOUT only, so `git.commit()` resolves rather than
61
+ * throwing. Re-verify this on any simple-git upgrade.
62
+ */
63
+ import { type OccWriteResult } from '../utils/occ-json-write';
64
+ /**
65
+ * Thrown when the lock/OCC-retry stack in {@link mutateSettingsJsonFile}
66
+ * exhausts every attempt without landing a write — the file is busy with
67
+ * another writer. Callers translate this into a 409 ("try again").
68
+ */
69
+ export declare class SettingsFileConflictError extends Error {
70
+ constructor(message?: string);
71
+ }
72
+ /**
73
+ * Thrown by a caller's `mutate` callback when an app-level
74
+ * `expectedContentVersion` sent by a client doesn't match the file's current
75
+ * `version`. A different concern from {@link SettingsFileConflictError}:
76
+ * this is a real edit conflict the user must resolve by reloading, not
77
+ * transient lock contention a retry can fix — and indeed it never IS
78
+ * retried, since {@link withOccRetry} only recognizes
79
+ * {@link OccWriteConflictError} as retryable, so this propagates on the
80
+ * very first attempt.
81
+ */
82
+ export declare class SettingsVersionConflictError extends Error {
83
+ constructor(message?: string);
84
+ }
85
+ /**
86
+ * Structural shape `mutateSettingsJsonFile` needs from a parsed settings
87
+ * file: just enough to read the OCC version off it without resorting to
88
+ * `any`. Concrete file types (e.g. `PermissionsFile`, `GroupsFile`) satisfy
89
+ * this automatically since `version` is optional on both.
90
+ */
91
+ interface VersionedSettingsFile {
92
+ version?: number;
93
+ }
94
+ export interface MutateSettingsFileOptions<TFile extends VersionedSettingsFile> {
95
+ /** Path to the settings JSON file (resolved internally; need not be absolute). */
96
+ filePath: string;
97
+ /** JSON.parse + zod-parse the raw file contents. Throws propagate untouched (never retried). */
98
+ parse: (raw: string) => TFile;
99
+ /**
100
+ * Compute the next payload from the current parsed file (`null` on
101
+ * ENOENT) and the version to write it under. Return `null` for a
102
+ * deliberate no-op — the write is skipped entirely. Called once per
103
+ * retry attempt against freshly reloaded state, so it must be safe to
104
+ * call more than once; anything it throws (besides the retried
105
+ * `OccWriteConflictError`, which it should never throw itself) propagates
106
+ * out of `mutateSettingsJsonFile` untouched.
107
+ */
108
+ mutate: (current: TFile | null, version: number) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
109
+ /** Forwarded to writeOccJsonFile. Pass 0 in tests. */
110
+ settleMs?: number;
111
+ /** Forwarded to withOccRetry. */
112
+ maxAttempts?: number;
113
+ }
114
+ /**
115
+ * Run a load -> mutate -> write cycle for a settings JSON file under the
116
+ * full lock + OCC-retry stack described in the module doc comment above. A
117
+ * conflict that survives every retry surfaces as
118
+ * {@link SettingsFileConflictError}; everything else — including
119
+ * {@link SettingsVersionConflictError} thrown by `mutate`, and any
120
+ * parse/validation error — propagates untouched.
121
+ *
122
+ * Returns the `writeOccJsonFile` result, or `null` if `mutate` chose a
123
+ * no-op (no write happened).
124
+ */
125
+ export declare function mutateSettingsJsonFile<TFile extends VersionedSettingsFile>(opts: MutateSettingsFileOptions<TFile>): Promise<OccWriteResult | null>;
126
+ export {};
127
+ //# sourceMappingURL=settings-file-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-file-store.d.ts","sourceRoot":"","sources":["../../src/authorization/settings-file-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAMH,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAA;AAGhC;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,SAAwC;CAI5D;AAED;;;;;;;;;GASG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;gBACzC,OAAO,SAAyE;CAI7F;AAED;;;;;GAKG;AACH,UAAU,qBAAqB;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,yBAAyB,CAAC,KAAK,SAAS,qBAAqB;IAC5E,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAA;IAChB,gGAAgG;IAChG,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,KAAK,CAAA;IAC7B;;;;;;;;OAQG;IACH,MAAM,EAAE,CACN,OAAO,EAAE,KAAK,GAAG,IAAI,EACrB,OAAO,EAAE,MAAM,KACZ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;IAC7E,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAgCD;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,SAAS,qBAAqB,EAC9E,IAAI,EAAE,yBAAyB,CAAC,KAAK,CAAC,GACrC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA6BhC"}