canopycms 0.0.0 → 0.0.1

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 (430) hide show
  1. package/package.json +2 -3
  2. package/dist/__integration__/fixtures/content-seeds.d.ts +0 -43
  3. package/dist/__integration__/fixtures/content-seeds.d.ts.map +0 -1
  4. package/dist/__integration__/fixtures/content-seeds.js +0 -99
  5. package/dist/__integration__/fixtures/content-seeds.js.map +0 -1
  6. package/dist/__integration__/fixtures/schemas.d.ts +0 -12
  7. package/dist/__integration__/fixtures/schemas.d.ts.map +0 -1
  8. package/dist/__integration__/fixtures/schemas.js +0 -65
  9. package/dist/__integration__/fixtures/schemas.js.map +0 -1
  10. package/dist/__integration__/test-utils/api-client.d.ts +0 -123
  11. package/dist/__integration__/test-utils/api-client.d.ts.map +0 -1
  12. package/dist/__integration__/test-utils/api-client.js +0 -118
  13. package/dist/__integration__/test-utils/api-client.js.map +0 -1
  14. package/dist/__integration__/test-utils/multi-user.d.ts +0 -25
  15. package/dist/__integration__/test-utils/multi-user.d.ts.map +0 -1
  16. package/dist/__integration__/test-utils/multi-user.js +0 -105
  17. package/dist/__integration__/test-utils/multi-user.js.map +0 -1
  18. package/dist/__integration__/test-utils/test-workspace.d.ts +0 -25
  19. package/dist/__integration__/test-utils/test-workspace.d.ts.map +0 -1
  20. package/dist/__integration__/test-utils/test-workspace.js +0 -102
  21. package/dist/__integration__/test-utils/test-workspace.js.map +0 -1
  22. package/dist/editor/BranchManager.stories.d.ts +0 -8
  23. package/dist/editor/BranchManager.stories.d.ts.map +0 -1
  24. package/dist/editor/BranchManager.stories.js +0 -74
  25. package/dist/editor/BranchManager.stories.js.map +0 -1
  26. package/dist/editor/CanopyEditor.stories.d.ts +0 -7
  27. package/dist/editor/CanopyEditor.stories.d.ts.map +0 -1
  28. package/dist/editor/CanopyEditor.stories.js +0 -99
  29. package/dist/editor/CanopyEditor.stories.js.map +0 -1
  30. package/dist/editor/CommentsPanel.stories.d.ts +0 -10
  31. package/dist/editor/CommentsPanel.stories.d.ts.map +0 -1
  32. package/dist/editor/CommentsPanel.stories.js +0 -175
  33. package/dist/editor/CommentsPanel.stories.js.map +0 -1
  34. package/dist/editor/Editor.stories.d.ts +0 -7
  35. package/dist/editor/Editor.stories.d.ts.map +0 -1
  36. package/dist/editor/Editor.stories.js +0 -95
  37. package/dist/editor/Editor.stories.js.map +0 -1
  38. package/dist/editor/EditorPanes.stories.d.ts +0 -7
  39. package/dist/editor/EditorPanes.stories.d.ts.map +0 -1
  40. package/dist/editor/EditorPanes.stories.js +0 -116
  41. package/dist/editor/EditorPanes.stories.js.map +0 -1
  42. package/dist/editor/EntryNavigator.stories.d.ts +0 -8
  43. package/dist/editor/EntryNavigator.stories.d.ts.map +0 -1
  44. package/dist/editor/EntryNavigator.stories.js +0 -42
  45. package/dist/editor/EntryNavigator.stories.js.map +0 -1
  46. package/dist/editor/FormRenderer.stories.d.ts +0 -7
  47. package/dist/editor/FormRenderer.stories.d.ts.map +0 -1
  48. package/dist/editor/FormRenderer.stories.js +0 -115
  49. package/dist/editor/FormRenderer.stories.js.map +0 -1
  50. package/dist/editor/GroupManager.stories.d.ts +0 -19
  51. package/dist/editor/GroupManager.stories.d.ts.map +0 -1
  52. package/dist/editor/GroupManager.stories.js +0 -265
  53. package/dist/editor/GroupManager.stories.js.map +0 -1
  54. package/dist/editor/PermissionManager.stories.d.ts +0 -20
  55. package/dist/editor/PermissionManager.stories.d.ts.map +0 -1
  56. package/dist/editor/PermissionManager.stories.js +0 -506
  57. package/dist/editor/PermissionManager.stories.js.map +0 -1
  58. package/dist/editor/comments/FieldWrapper.stories.d.ts +0 -10
  59. package/dist/editor/comments/FieldWrapper.stories.d.ts.map +0 -1
  60. package/dist/editor/comments/FieldWrapper.stories.js +0 -173
  61. package/dist/editor/comments/FieldWrapper.stories.js.map +0 -1
  62. package/dist/editor/fields/BlockField.stories.d.ts +0 -7
  63. package/dist/editor/fields/BlockField.stories.d.ts.map +0 -1
  64. package/dist/editor/fields/BlockField.stories.js +0 -50
  65. package/dist/editor/fields/BlockField.stories.js.map +0 -1
  66. package/dist/editor/fields/fields.stories.d.ts +0 -8
  67. package/dist/editor/fields/fields.stories.d.ts.map +0 -1
  68. package/dist/editor/fields/fields.stories.js +0 -34
  69. package/dist/editor/fields/fields.stories.js.map +0 -1
  70. package/dist/test-utils/api-test-helpers.d.ts +0 -238
  71. package/dist/test-utils/api-test-helpers.d.ts.map +0 -1
  72. package/dist/test-utils/api-test-helpers.js +0 -347
  73. package/dist/test-utils/api-test-helpers.js.map +0 -1
  74. package/dist/test-utils/console-spy.d.ts +0 -56
  75. package/dist/test-utils/console-spy.d.ts.map +0 -1
  76. package/dist/test-utils/console-spy.js +0 -81
  77. package/dist/test-utils/console-spy.js.map +0 -1
  78. package/dist/test-utils/git-helpers.d.ts +0 -21
  79. package/dist/test-utils/git-helpers.d.ts.map +0 -1
  80. package/dist/test-utils/git-helpers.js +0 -23
  81. package/dist/test-utils/git-helpers.js.map +0 -1
  82. package/dist/test-utils/index.d.ts +0 -5
  83. package/dist/test-utils/index.d.ts.map +0 -1
  84. package/dist/test-utils/index.js +0 -4
  85. package/dist/test-utils/index.js.map +0 -1
  86. package/src/__integration__/errors/invalid-content.test.ts +0 -238
  87. package/src/__integration__/errors/permission-denied.test.ts +0 -220
  88. package/src/__integration__/fixtures/content-seeds.ts +0 -105
  89. package/src/__integration__/fixtures/schemas.ts +0 -67
  90. package/src/__integration__/initialization/prod-sim-init.test.ts +0 -139
  91. package/src/__integration__/permissions/path-permissions.test.ts +0 -314
  92. package/src/__integration__/permissions/role-permissions.test.ts +0 -354
  93. package/src/__integration__/permissions/settings-branch-isolation.test.ts +0 -317
  94. package/src/__integration__/settings/groups-api.test.ts +0 -403
  95. package/src/__integration__/test-utils/api-client.ts +0 -167
  96. package/src/__integration__/test-utils/multi-user.ts +0 -129
  97. package/src/__integration__/test-utils/test-workspace.ts +0 -130
  98. package/src/__integration__/user/user-context.test.ts +0 -174
  99. package/src/__integration__/validation/input-validation.test.ts +0 -166
  100. package/src/__integration__/workflows/api-editing-workflow.test.ts +0 -244
  101. package/src/__integration__/workflows/conflict-resolution.test.ts +0 -259
  102. package/src/__integration__/workflows/editing-workflow.test.ts +0 -205
  103. package/src/__integration__/workflows/review-workflow.test.ts +0 -260
  104. package/src/ai/__tests__/build.integration.test.ts +0 -224
  105. package/src/ai/__tests__/generate.integration.test.ts +0 -495
  106. package/src/ai/__tests__/handler.integration.test.ts +0 -212
  107. package/src/ai/__tests__/json-to-markdown.test.ts +0 -553
  108. package/src/ai/generate.ts +0 -410
  109. package/src/ai/handler.ts +0 -123
  110. package/src/ai/index.ts +0 -26
  111. package/src/ai/json-to-markdown.ts +0 -424
  112. package/src/ai/resolve-branch.ts +0 -34
  113. package/src/ai/types.ts +0 -160
  114. package/src/api/AGENTS.md +0 -81
  115. package/src/api/__test__/mock-client.ts +0 -404
  116. package/src/api/assets.test.ts +0 -140
  117. package/src/api/assets.ts +0 -154
  118. package/src/api/branch-merge.test.ts +0 -163
  119. package/src/api/branch-merge.ts +0 -113
  120. package/src/api/branch-review.test.ts +0 -297
  121. package/src/api/branch-review.ts +0 -136
  122. package/src/api/branch-status.test.ts +0 -85
  123. package/src/api/branch-status.ts +0 -153
  124. package/src/api/branch-withdraw.test.ts +0 -146
  125. package/src/api/branch-withdraw.ts +0 -81
  126. package/src/api/branch-workflow.integration.test.ts +0 -578
  127. package/src/api/branch.test.ts +0 -620
  128. package/src/api/branch.ts +0 -492
  129. package/src/api/client.test.ts +0 -349
  130. package/src/api/client.ts +0 -506
  131. package/src/api/comments.test.ts +0 -285
  132. package/src/api/comments.ts +0 -210
  133. package/src/api/content.test.ts +0 -345
  134. package/src/api/content.ts +0 -454
  135. package/src/api/entries.test.ts +0 -1339
  136. package/src/api/entries.ts +0 -650
  137. package/src/api/github-sync.ts +0 -144
  138. package/src/api/groups.test.ts +0 -1013
  139. package/src/api/groups.ts +0 -375
  140. package/src/api/guards.test.ts +0 -533
  141. package/src/api/guards.ts +0 -271
  142. package/src/api/index.ts +0 -87
  143. package/src/api/permissions.test.ts +0 -766
  144. package/src/api/permissions.ts +0 -334
  145. package/src/api/reference-options.ts +0 -118
  146. package/src/api/resolve-references.ts +0 -107
  147. package/src/api/route-builder.ts +0 -289
  148. package/src/api/schema.test.ts +0 -840
  149. package/src/api/schema.ts +0 -936
  150. package/src/api/security.test.ts +0 -233
  151. package/src/api/settings-helpers.ts +0 -84
  152. package/src/api/types.ts +0 -40
  153. package/src/api/user.test.ts +0 -127
  154. package/src/api/user.ts +0 -42
  155. package/src/api/validators.test.ts +0 -275
  156. package/src/api/validators.ts +0 -176
  157. package/src/asset-store.test.ts +0 -37
  158. package/src/asset-store.ts +0 -110
  159. package/src/auth/cache.ts +0 -7
  160. package/src/auth/caching-auth-plugin.test.ts +0 -154
  161. package/src/auth/caching-auth-plugin.ts +0 -109
  162. package/src/auth/context-helpers.ts +0 -75
  163. package/src/auth/file-based-auth-cache.test.ts +0 -257
  164. package/src/auth/file-based-auth-cache.ts +0 -279
  165. package/src/auth/index.ts +0 -12
  166. package/src/auth/plugin.ts +0 -51
  167. package/src/auth/types.ts +0 -38
  168. package/src/authorization/__tests__/branch.test.ts +0 -260
  169. package/src/authorization/__tests__/content.test.ts +0 -142
  170. package/src/authorization/__tests__/path.test.ts +0 -133
  171. package/src/authorization/__tests__/permissions-loader.test.ts +0 -200
  172. package/src/authorization/branch.ts +0 -94
  173. package/src/authorization/content.ts +0 -93
  174. package/src/authorization/groups/index.ts +0 -11
  175. package/src/authorization/groups/loader.ts +0 -127
  176. package/src/authorization/groups/schema.ts +0 -48
  177. package/src/authorization/helpers.ts +0 -48
  178. package/src/authorization/index.ts +0 -84
  179. package/src/authorization/path.ts +0 -112
  180. package/src/authorization/permissions/index.ts +0 -11
  181. package/src/authorization/permissions/loader.ts +0 -116
  182. package/src/authorization/permissions/schema.ts +0 -66
  183. package/src/authorization/test-utils.ts +0 -15
  184. package/src/authorization/types.ts +0 -66
  185. package/src/authorization/validation.test.ts +0 -100
  186. package/src/authorization/validation.ts +0 -62
  187. package/src/branch-metadata.test.ts +0 -168
  188. package/src/branch-metadata.ts +0 -166
  189. package/src/branch-registry.test.ts +0 -248
  190. package/src/branch-registry.ts +0 -152
  191. package/src/branch-schema-cache.test.ts +0 -275
  192. package/src/branch-schema-cache.ts +0 -189
  193. package/src/branch-workspace.test.ts +0 -183
  194. package/src/branch-workspace.ts +0 -124
  195. package/src/build/generate-ai-content.ts +0 -78
  196. package/src/build/index.ts +0 -8
  197. package/src/build-mode.ts +0 -27
  198. package/src/cli/generate-ai-content.ts +0 -100
  199. package/src/cli/init.test.ts +0 -240
  200. package/src/cli/templates/Dockerfile.cms.template +0 -19
  201. package/src/cli/templates/canopy.ts.template +0 -55
  202. package/src/cli/templates/canopycms.config.ts.template +0 -11
  203. package/src/cli/templates/deploy-cms.yml.template +0 -27
  204. package/src/cli/templates/edit-page.tsx.template +0 -32
  205. package/src/cli/templates/route.ts.template +0 -12
  206. package/src/cli/templates/schemas.ts.template +0 -16
  207. package/src/cli/templates.ts +0 -47
  208. package/src/client.ts +0 -12
  209. package/src/comment-store.test.ts +0 -442
  210. package/src/comment-store.ts +0 -301
  211. package/src/config/__tests__/config.test.ts +0 -513
  212. package/src/config/flatten.ts +0 -174
  213. package/src/config/helpers.ts +0 -167
  214. package/src/config/index.ts +0 -86
  215. package/src/config/schemas/collection.ts +0 -67
  216. package/src/config/schemas/config.ts +0 -77
  217. package/src/config/schemas/field.ts +0 -108
  218. package/src/config/schemas/media.ts +0 -27
  219. package/src/config/schemas/permissions.ts +0 -21
  220. package/src/config/types.ts +0 -321
  221. package/src/config/validation.ts +0 -70
  222. package/src/config-test.ts +0 -65
  223. package/src/config.ts +0 -11
  224. package/src/content-id-index.test.ts +0 -512
  225. package/src/content-id-index.ts +0 -479
  226. package/src/content-reader.test.ts +0 -478
  227. package/src/content-reader.ts +0 -214
  228. package/src/content-store.test.ts +0 -1126
  229. package/src/content-store.ts +0 -793
  230. package/src/context.ts +0 -111
  231. package/src/editor/BranchManager.stories.tsx +0 -80
  232. package/src/editor/BranchManager.test.tsx +0 -324
  233. package/src/editor/BranchManager.tsx +0 -461
  234. package/src/editor/CanopyEditor.stories.tsx +0 -128
  235. package/src/editor/CanopyEditor.test.tsx +0 -81
  236. package/src/editor/CanopyEditor.tsx +0 -73
  237. package/src/editor/CanopyEditorPage.test.tsx +0 -59
  238. package/src/editor/CanopyEditorPage.tsx +0 -25
  239. package/src/editor/CommentsPanel.stories.tsx +0 -184
  240. package/src/editor/CommentsPanel.tsx +0 -338
  241. package/src/editor/Editor.integration.test.tsx +0 -227
  242. package/src/editor/Editor.stories.tsx +0 -119
  243. package/src/editor/Editor.tsx +0 -1221
  244. package/src/editor/EditorPanes.stories.tsx +0 -256
  245. package/src/editor/EditorPanes.test.tsx +0 -77
  246. package/src/editor/EditorPanes.tsx +0 -180
  247. package/src/editor/EntryNavigator.stories.tsx +0 -65
  248. package/src/editor/EntryNavigator.test.tsx +0 -598
  249. package/src/editor/EntryNavigator.tsx +0 -665
  250. package/src/editor/FormRenderer.stories.tsx +0 -212
  251. package/src/editor/FormRenderer.test.tsx +0 -194
  252. package/src/editor/FormRenderer.tsx +0 -432
  253. package/src/editor/GroupManager.stories.tsx +0 -301
  254. package/src/editor/GroupManager.test.tsx +0 -682
  255. package/src/editor/GroupManager.tsx +0 -9
  256. package/src/editor/PermissionManager.stories.tsx +0 -539
  257. package/src/editor/PermissionManager.test.tsx +0 -864
  258. package/src/editor/PermissionManager.tsx +0 -12
  259. package/src/editor/canopy-path.test.ts +0 -23
  260. package/src/editor/canopy-path.ts +0 -52
  261. package/src/editor/client-reference-resolver.ts +0 -118
  262. package/src/editor/comments/BranchComments.tsx +0 -93
  263. package/src/editor/comments/EntryComments.tsx +0 -94
  264. package/src/editor/comments/FieldWrapper.stories.tsx +0 -210
  265. package/src/editor/comments/FieldWrapper.tsx +0 -129
  266. package/src/editor/comments/InlineCommentThread.test.tsx +0 -384
  267. package/src/editor/comments/InlineCommentThread.tsx +0 -246
  268. package/src/editor/comments/ThreadCarousel.test.tsx +0 -393
  269. package/src/editor/comments/ThreadCarousel.tsx +0 -525
  270. package/src/editor/components/ConfirmDeleteModal.tsx +0 -49
  271. package/src/editor/components/EditorContext.tsx +0 -49
  272. package/src/editor/components/EditorFooter.tsx +0 -47
  273. package/src/editor/components/EditorHeader.tsx +0 -492
  274. package/src/editor/components/EditorSidebar.tsx +0 -193
  275. package/src/editor/components/EntryCreateModal.tsx +0 -193
  276. package/src/editor/components/RenameEntryModal.tsx +0 -152
  277. package/src/editor/components/UserBadge.test.tsx +0 -274
  278. package/src/editor/components/UserBadge.tsx +0 -240
  279. package/src/editor/components/index.ts +0 -6
  280. package/src/editor/context/ApiClientContext.tsx +0 -56
  281. package/src/editor/context/EditorStateContext.tsx +0 -221
  282. package/src/editor/context/index.ts +0 -40
  283. package/src/editor/editor-config.test.ts +0 -385
  284. package/src/editor/editor-config.ts +0 -94
  285. package/src/editor/editor-utils.test.ts +0 -772
  286. package/src/editor/editor-utils.ts +0 -303
  287. package/src/editor/env.ts +0 -4
  288. package/src/editor/fields/BlockField.stories.tsx +0 -79
  289. package/src/editor/fields/BlockField.tsx +0 -267
  290. package/src/editor/fields/CodeField.tsx +0 -41
  291. package/src/editor/fields/MarkdownField.tsx +0 -205
  292. package/src/editor/fields/ObjectField.tsx +0 -71
  293. package/src/editor/fields/ReferenceField.tsx +0 -138
  294. package/src/editor/fields/SelectField.tsx +0 -76
  295. package/src/editor/fields/TextField.tsx +0 -35
  296. package/src/editor/fields/ToggleField.tsx +0 -37
  297. package/src/editor/fields/fields.stories.tsx +0 -40
  298. package/src/editor/group-manager/ExternalGroupsTab.tsx +0 -114
  299. package/src/editor/group-manager/GroupCard.tsx +0 -102
  300. package/src/editor/group-manager/GroupForm.tsx +0 -66
  301. package/src/editor/group-manager/InternalGroupsTab.tsx +0 -147
  302. package/src/editor/group-manager/MemberList.tsx +0 -184
  303. package/src/editor/group-manager/hooks/useExternalGroupSearch.ts +0 -63
  304. package/src/editor/group-manager/hooks/useGroupState.ts +0 -134
  305. package/src/editor/group-manager/hooks/useUserSearch.ts +0 -84
  306. package/src/editor/group-manager/index.tsx +0 -210
  307. package/src/editor/group-manager/types.ts +0 -28
  308. package/src/editor/hooks/README.md +0 -26
  309. package/src/editor/hooks/__test__/test-utils.tsx +0 -183
  310. package/src/editor/hooks/index.ts +0 -23
  311. package/src/editor/hooks/useBranchActions.test.tsx +0 -267
  312. package/src/editor/hooks/useBranchActions.tsx +0 -121
  313. package/src/editor/hooks/useBranchManager.test.tsx +0 -391
  314. package/src/editor/hooks/useBranchManager.tsx +0 -326
  315. package/src/editor/hooks/useCommentSystem.test.ts +0 -615
  316. package/src/editor/hooks/useCommentSystem.ts +0 -347
  317. package/src/editor/hooks/useDraftManager.test.ts +0 -375
  318. package/src/editor/hooks/useDraftManager.ts +0 -259
  319. package/src/editor/hooks/useEditorLayout.test.ts +0 -147
  320. package/src/editor/hooks/useEditorLayout.ts +0 -67
  321. package/src/editor/hooks/useEntryManager.test.ts +0 -588
  322. package/src/editor/hooks/useEntryManager.ts +0 -387
  323. package/src/editor/hooks/useGroupManager.test.ts +0 -277
  324. package/src/editor/hooks/useGroupManager.ts +0 -139
  325. package/src/editor/hooks/usePermissionManager.test.ts +0 -211
  326. package/src/editor/hooks/usePermissionManager.ts +0 -113
  327. package/src/editor/hooks/useReferenceResolution.ts +0 -248
  328. package/src/editor/hooks/useSchemaManager.test.ts +0 -370
  329. package/src/editor/hooks/useSchemaManager.ts +0 -310
  330. package/src/editor/hooks/useUserContext.tsx +0 -57
  331. package/src/editor/hooks/useUserMetadata.test.ts +0 -191
  332. package/src/editor/hooks/useUserMetadata.ts +0 -71
  333. package/src/editor/permission-manager/GroupSelector.tsx +0 -73
  334. package/src/editor/permission-manager/PermissionEditor.tsx +0 -321
  335. package/src/editor/permission-manager/PermissionLevelBadge.tsx +0 -53
  336. package/src/editor/permission-manager/PermissionTree.tsx +0 -237
  337. package/src/editor/permission-manager/UserSelector.tsx +0 -95
  338. package/src/editor/permission-manager/constants.tsx +0 -18
  339. package/src/editor/permission-manager/hooks/useGroupsAndUsers.ts +0 -153
  340. package/src/editor/permission-manager/hooks/usePermissionTree.ts +0 -200
  341. package/src/editor/permission-manager/index.tsx +0 -294
  342. package/src/editor/permission-manager/types.ts +0 -58
  343. package/src/editor/permission-manager/utils.ts +0 -179
  344. package/src/editor/preview-bridge.test.tsx +0 -50
  345. package/src/editor/preview-bridge.tsx +0 -294
  346. package/src/editor/schema-editor/CollectionEditor.test.tsx +0 -238
  347. package/src/editor/schema-editor/CollectionEditor.tsx +0 -520
  348. package/src/editor/schema-editor/EntryTypeEditor.test.tsx +0 -215
  349. package/src/editor/schema-editor/EntryTypeEditor.tsx +0 -367
  350. package/src/editor/schema-editor/index.ts +0 -19
  351. package/src/editor/setup-test-dom.ts +0 -10
  352. package/src/editor/test-setup.ts +0 -33
  353. package/src/editor/theme.tsx +0 -119
  354. package/src/editor/utils/env.ts +0 -39
  355. package/src/entry-schema-registry.test.ts +0 -281
  356. package/src/entry-schema-registry.ts +0 -121
  357. package/src/entry-schema.ts +0 -84
  358. package/src/git-manager.test.ts +0 -552
  359. package/src/git-manager.ts +0 -667
  360. package/src/github-service.test.ts +0 -312
  361. package/src/github-service.ts +0 -295
  362. package/src/http/handler.test.ts +0 -275
  363. package/src/http/handler.ts +0 -280
  364. package/src/http/index.ts +0 -11
  365. package/src/http/router.ts +0 -164
  366. package/src/http/types.ts +0 -44
  367. package/src/id.test.ts +0 -48
  368. package/src/id.ts +0 -22
  369. package/src/index.ts +0 -26
  370. package/src/operating-mode/__tests__/strategies.test.ts +0 -511
  371. package/src/operating-mode/client-safe-strategy.ts +0 -184
  372. package/src/operating-mode/client-unsafe-strategy.ts +0 -303
  373. package/src/operating-mode/client.ts +0 -13
  374. package/src/operating-mode/index.ts +0 -34
  375. package/src/operating-mode/types.ts +0 -186
  376. package/src/paths/__tests__/branch.test.ts +0 -53
  377. package/src/paths/__tests__/normalize.test.ts +0 -141
  378. package/src/paths/__tests__/resolve.test.ts +0 -207
  379. package/src/paths/__tests__/validation.test.ts +0 -61
  380. package/src/paths/branch.ts +0 -115
  381. package/src/paths/index.ts +0 -73
  382. package/src/paths/normalize-server.ts +0 -40
  383. package/src/paths/normalize.ts +0 -107
  384. package/src/paths/resolve.ts +0 -61
  385. package/src/paths/test-utils.ts +0 -37
  386. package/src/paths/types.ts +0 -68
  387. package/src/paths/validation.test.ts +0 -480
  388. package/src/paths/validation.ts +0 -391
  389. package/src/reference-resolver.test.ts +0 -107
  390. package/src/reference-resolver.ts +0 -157
  391. package/src/schema/index.ts +0 -29
  392. package/src/schema/meta-loader.ts +0 -366
  393. package/src/schema/resolver.ts +0 -83
  394. package/src/schema/schema-store-types.ts +0 -56
  395. package/src/schema/schema-store.test.ts +0 -816
  396. package/src/schema/schema-store.ts +0 -795
  397. package/src/schema/types.ts +0 -33
  398. package/src/schema-meta-loader.test.ts +0 -447
  399. package/src/server.ts +0 -15
  400. package/src/services.test.ts +0 -559
  401. package/src/services.ts +0 -373
  402. package/src/settings-branch-utils.ts +0 -53
  403. package/src/settings-workspace.ts +0 -156
  404. package/src/task-queue/README.md +0 -144
  405. package/src/task-queue/index.ts +0 -14
  406. package/src/task-queue/task-queue.test.ts +0 -524
  407. package/src/task-queue/task-queue.ts +0 -514
  408. package/src/task-queue/types.ts +0 -41
  409. package/src/test-utils/api-test-helpers.ts +0 -445
  410. package/src/test-utils/console-spy.test.ts +0 -14
  411. package/src/test-utils/console-spy.ts +0 -125
  412. package/src/test-utils/git-helpers.ts +0 -31
  413. package/src/test-utils/index.ts +0 -4
  414. package/src/types.ts +0 -54
  415. package/src/user.ts +0 -118
  416. package/src/utils/debug.test.ts +0 -114
  417. package/src/utils/debug.ts +0 -127
  418. package/src/utils/error.test.ts +0 -92
  419. package/src/utils/error.ts +0 -83
  420. package/src/utils/format.ts +0 -12
  421. package/src/validation/__tests__/field-traversal.test.ts +0 -263
  422. package/src/validation/deletion-checker.ts +0 -234
  423. package/src/validation/field-traversal.ts +0 -146
  424. package/src/validation/reference-validator.ts +0 -168
  425. package/src/worker/cms-worker-rebase.test.ts +0 -473
  426. package/src/worker/cms-worker.ts +0 -777
  427. package/src/worker/integration.test.ts +0 -289
  428. package/src/worker/task-queue-config.ts +0 -25
  429. package/src/worker/task-queue.test.ts +0 -452
  430. package/src/worker/task-queue.ts +0 -58
@@ -1,1013 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest'
2
- import type { ApiContext, ApiRequest } from './types'
3
- import type { InternalGroup } from '../authorization'
4
- import type { CanopyGroupId, CanopyUserId } from '../types'
5
- import { RESERVED_GROUPS } from '../authorization'
6
- import { createMockApiContext, createMockBranchContext, createMockGitManager } from '../test-utils'
7
-
8
- // Mock authorization module (specifically the groups loader functions)
9
- vi.mock('../authorization', async (importOriginal) => {
10
- const { vi } = await import('vitest')
11
- const original = await importOriginal<typeof import('../authorization')>()
12
- return {
13
- ...original,
14
- loadInternalGroups: vi.fn(),
15
- loadGroupsFile: vi.fn(),
16
- saveInternalGroups: vi.fn(),
17
- }
18
- })
19
-
20
- import {
21
- GROUP_ROUTES,
22
- validateAdminGroupUpdate,
23
- validateReservedGroups,
24
- type UpdateInternalGroupsBody,
25
- type SearchExternalGroupsParams,
26
- } from './groups'
27
- import * as authorization from '../authorization'
28
-
29
- // Alias for convenience (tests reference groupsLoader)
30
- const groupsLoader = {
31
- loadInternalGroups: authorization.loadInternalGroups,
32
- loadGroupsFile: authorization.loadGroupsFile,
33
- saveInternalGroups: authorization.saveInternalGroups,
34
- }
35
-
36
- // Extract handlers for testing
37
- const getInternalGroups = GROUP_ROUTES.getInternal.handler
38
- const updateInternalGroups = GROUP_ROUTES.updateInternal.handler
39
- const searchExternalGroups = GROUP_ROUTES.searchExternal.handler
40
-
41
- describe('groups API', () => {
42
- let mockContext: ApiContext
43
- let mockGit: ReturnType<typeof createMockGitManager>
44
-
45
- beforeEach(() => {
46
- mockGit = createMockGitManager()
47
-
48
- mockContext = createMockApiContext({
49
- services: {
50
- config: {
51
- defaultBaseBranch: 'main',
52
- mode: 'dev',
53
- gitBotAuthorName: 'Canopy Bot',
54
- gitBotAuthorEmail: 'bot@example.com',
55
- sourceRoot: '/test/workspace',
56
- } as any,
57
- createGitManagerFor: vi.fn(() => mockGit) as any,
58
- },
59
- branchContext: createMockBranchContext({
60
- branchName: 'main',
61
- createdBy: 'admin-1' as CanopyUserId,
62
- baseRoot: '/test',
63
- branchRoot: '/test/main',
64
- createdAt: '2024-01-01T00:00:00.000Z',
65
- updatedAt: '2024-01-01T00:00:00.000Z',
66
- }),
67
- })
68
- })
69
-
70
- describe('getInternalGroups', () => {
71
- it('should return 403 for non-admin users', async () => {
72
- const req: ApiRequest<undefined> = {
73
- user: {
74
- type: 'authenticated',
75
- userId: 'user-1' as CanopyUserId,
76
- groups: [],
77
- },
78
- }
79
-
80
- const result = await getInternalGroups(mockContext, req)
81
-
82
- expect(result).toEqual({
83
- ok: false,
84
- status: 403,
85
- error: 'Admin access required',
86
- })
87
- })
88
-
89
- it('should return empty array when groups file does not exist', async () => {
90
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
91
-
92
- const req: ApiRequest<undefined> = {
93
- user: {
94
- type: 'authenticated',
95
- userId: 'admin-1' as CanopyUserId,
96
- groups: [RESERVED_GROUPS.ADMINS],
97
- },
98
- }
99
-
100
- const result = await getInternalGroups(mockContext, req)
101
-
102
- expect(result.ok).toBe(true)
103
- expect(result.status).toBe(200)
104
- if (result.ok) {
105
- expect(result.data?.groups).toEqual([])
106
- }
107
- })
108
- })
109
-
110
- describe('updateInternalGroups', () => {
111
- it('should return 403 for non-admin users', async () => {
112
- const req: ApiRequest = {
113
- user: {
114
- type: 'authenticated',
115
- userId: 'user-1' as CanopyUserId,
116
- groups: [],
117
- },
118
- }
119
-
120
- const result = await updateInternalGroups(mockContext, req, {
121
- groups: [],
122
- })
123
-
124
- expect(result).toEqual({
125
- ok: false,
126
- status: 403,
127
- error: 'Admin access required',
128
- })
129
- })
130
-
131
- it('should return 400 if groups not provided', async () => {
132
- const req: ApiRequest = {
133
- user: {
134
- type: 'authenticated',
135
- userId: 'admin-1' as CanopyUserId,
136
- groups: [RESERVED_GROUPS.ADMINS],
137
- },
138
- }
139
-
140
- const result = await updateInternalGroups(mockContext, req, {} as UpdateInternalGroupsBody)
141
-
142
- expect(result).toEqual({
143
- ok: false,
144
- status: 400,
145
- error: 'groups array required',
146
- })
147
- })
148
-
149
- it('should save groups and commit changes for admin', async () => {
150
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
151
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
152
- // Add bootstrap admin so validation passes
153
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
154
-
155
- const groups: InternalGroup[] = [
156
- {
157
- id: '' as CanopyGroupId, // Empty ID for new group
158
- name: 'Content Editors',
159
- description: 'Team members who can edit content',
160
- members: ['user-1' as CanopyUserId, 'user-2' as CanopyUserId],
161
- },
162
- ]
163
-
164
- const req: ApiRequest = {
165
- user: {
166
- type: 'authenticated',
167
- userId: 'admin-1' as CanopyUserId,
168
- groups: [RESERVED_GROUPS.ADMINS],
169
- },
170
- }
171
-
172
- const result = await updateInternalGroups(mockContext, req, { groups })
173
-
174
- expect(result.ok).toBe(true)
175
- expect(result.status).toBe(200)
176
-
177
- // Verify groups were saved with generated IDs
178
- // In dev mode, uses workspace root instead of branch root
179
- expect(groupsLoader.saveInternalGroups).toHaveBeenCalledWith(
180
- '/test/workspace',
181
- expect.arrayContaining([
182
- expect.objectContaining({
183
- name: 'Content Editors',
184
- description: 'Team members who can edit content',
185
- members: ['user-1', 'user-2'],
186
- id: expect.any(String), // Should have generated ID
187
- }),
188
- ]),
189
- 'admin-1',
190
- 'dev',
191
- 1, // contentVersion starts at 1 when file doesn't exist
192
- )
193
-
194
- // Verify the generated ID is not empty
195
- const savedGroups = vi.mocked(groupsLoader.saveInternalGroups).mock.calls[0][1]
196
- expect(savedGroups[0].id).not.toBe('')
197
- expect(savedGroups[0].id.length).toBeGreaterThan(0)
198
-
199
- // In dev mode (default), no git operations are performed
200
- expect(mockContext.services.commitFiles).not.toHaveBeenCalled()
201
- })
202
- })
203
-
204
- describe('searchExternalGroups', () => {
205
- it('should return 403 for non-admin users', async () => {
206
- const req: ApiRequest<undefined> = {
207
- user: {
208
- type: 'authenticated',
209
- userId: 'user-1' as CanopyUserId,
210
- groups: [],
211
- },
212
- }
213
-
214
- const params: SearchExternalGroupsParams = { query: 'test' }
215
-
216
- const result = await searchExternalGroups(mockContext, req, params)
217
-
218
- expect(result).toEqual({
219
- ok: false,
220
- status: 403,
221
- error: 'Admin access required',
222
- })
223
- })
224
-
225
- it('should return 501 if auth plugin not configured', async () => {
226
- mockContext.services.config.authPlugin = undefined
227
-
228
- const req: ApiRequest<undefined> = {
229
- user: {
230
- type: 'authenticated',
231
- userId: 'admin-1' as CanopyUserId,
232
- groups: [RESERVED_GROUPS.ADMINS],
233
- },
234
- }
235
-
236
- const params: SearchExternalGroupsParams = { query: 'test' }
237
-
238
- const result = await searchExternalGroups(mockContext, req, params)
239
-
240
- expect(result).toEqual({
241
- ok: false,
242
- status: 501,
243
- error: 'External group search not configured',
244
- })
245
- })
246
-
247
- it('should return 501 if searchExternalGroups method not available', async () => {
248
- mockContext.services.config.authPlugin = {
249
- searchUsers: vi.fn(),
250
- // searchExternalGroups not provided
251
- } as any
252
-
253
- const req: ApiRequest<undefined> = {
254
- user: {
255
- type: 'authenticated',
256
- userId: 'admin-1' as CanopyUserId,
257
- groups: [RESERVED_GROUPS.ADMINS],
258
- },
259
- }
260
-
261
- const params: SearchExternalGroupsParams = { query: 'test' }
262
-
263
- const result = await searchExternalGroups(mockContext, req, params)
264
-
265
- expect(result).toEqual({
266
- ok: false,
267
- status: 501,
268
- error: 'External group search not configured',
269
- })
270
- })
271
-
272
- it('should return search results from auth plugin', async () => {
273
- const mockGroups = [
274
- { id: 'org_123' as CanopyGroupId, name: 'Acme Corporation' },
275
- { id: 'org_456' as CanopyGroupId, name: 'Partner Organization' },
276
- ]
277
-
278
- mockContext.authPlugin = {
279
- searchExternalGroups: vi.fn(async () => mockGroups),
280
- } as any
281
-
282
- const req: ApiRequest<undefined> = {
283
- user: {
284
- type: 'authenticated',
285
- userId: 'admin-1' as CanopyUserId,
286
- groups: [RESERVED_GROUPS.ADMINS],
287
- },
288
- }
289
-
290
- const params: SearchExternalGroupsParams = { query: 'test' }
291
-
292
- const result = await searchExternalGroups(mockContext, req, params)
293
-
294
- expect(result.ok).toBe(true)
295
- expect(result.status).toBe(200)
296
- if (result.ok) {
297
- expect(result.data?.groups).toEqual(mockGroups)
298
- }
299
- })
300
-
301
- it('should return 500 on auth plugin error', async () => {
302
- mockContext.authPlugin = {
303
- searchExternalGroups: vi.fn(async () => {
304
- throw new Error('Search failed')
305
- }),
306
- } as any
307
-
308
- const req: ApiRequest<undefined> = {
309
- user: {
310
- type: 'authenticated',
311
- userId: 'admin-1' as CanopyUserId,
312
- groups: [RESERVED_GROUPS.ADMINS],
313
- },
314
- }
315
-
316
- const params: SearchExternalGroupsParams = { query: 'test' }
317
-
318
- const result = await searchExternalGroups(mockContext, req, params)
319
-
320
- expect(result).toEqual({
321
- ok: false,
322
- status: 500,
323
- error: 'Search failed',
324
- })
325
- })
326
- })
327
-
328
- describe('validateAdminGroupUpdate', () => {
329
- it('should return valid when Admins group has members', () => {
330
- const groups: InternalGroup[] = [
331
- {
332
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
333
- name: 'Admins',
334
- members: ['admin-1' as CanopyUserId],
335
- },
336
- ]
337
- const result = validateAdminGroupUpdate(groups, new Set())
338
- expect(result.valid).toBe(true)
339
- })
340
-
341
- it('should return valid when bootstrap admins exist even if Admins group is empty', () => {
342
- const groups: InternalGroup[] = [
343
- {
344
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
345
- name: 'Admins',
346
- members: [],
347
- },
348
- ]
349
- const result = validateAdminGroupUpdate(groups, new Set(['bootstrap-admin']))
350
- expect(result.valid).toBe(true)
351
- })
352
-
353
- it('should return valid when bootstrap admins exist and Admins group is missing', () => {
354
- const groups: InternalGroup[] = []
355
- const result = validateAdminGroupUpdate(groups, new Set(['bootstrap-admin']))
356
- expect(result.valid).toBe(true)
357
- })
358
-
359
- it('should return invalid when no admins exist', () => {
360
- const groups: InternalGroup[] = [
361
- {
362
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
363
- name: 'Admins',
364
- members: [],
365
- },
366
- ]
367
- const result = validateAdminGroupUpdate(groups, new Set())
368
- expect(result.valid).toBe(false)
369
- expect(result.error).toBe('Cannot remove last admin - at least one admin is required')
370
- })
371
-
372
- it('should return invalid when Admins group is missing and no bootstrap admins', () => {
373
- const groups: InternalGroup[] = []
374
- const result = validateAdminGroupUpdate(groups, new Set())
375
- expect(result.valid).toBe(false)
376
- expect(result.error).toBe('Cannot remove last admin - at least one admin is required')
377
- })
378
-
379
- it('should not double count when bootstrap admin is also in Admins group', () => {
380
- const groups: InternalGroup[] = [
381
- {
382
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
383
- name: 'Admins',
384
- members: ['admin-1' as CanopyUserId],
385
- },
386
- ]
387
- // Same user is bootstrap admin
388
- const result = validateAdminGroupUpdate(groups, new Set(['admin-1']))
389
- expect(result.valid).toBe(true)
390
- // Still valid but only counts as 1 admin, not 2
391
- })
392
- })
393
-
394
- describe('validateReservedGroups', () => {
395
- it('should return valid for non-reserved groups', () => {
396
- const groups: InternalGroup[] = [
397
- {
398
- id: 'editors' as CanopyGroupId,
399
- name: 'Content Editors',
400
- members: [],
401
- },
402
- ]
403
- const result = validateReservedGroups(groups)
404
- expect(result.valid).toBe(true)
405
- })
406
-
407
- it('should return valid when reserved group name matches ID', () => {
408
- const groups: InternalGroup[] = [
409
- {
410
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
411
- name: 'Admins',
412
- members: [],
413
- },
414
- {
415
- id: RESERVED_GROUPS.REVIEWERS as CanopyGroupId,
416
- name: 'Reviewers',
417
- members: [],
418
- },
419
- ]
420
- const result = validateReservedGroups(groups)
421
- expect(result.valid).toBe(true)
422
- })
423
-
424
- it('should return invalid when Admins group is renamed', () => {
425
- const groups: InternalGroup[] = [
426
- {
427
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
428
- name: 'Administrators',
429
- members: [],
430
- },
431
- ]
432
- const result = validateReservedGroups(groups)
433
- expect(result.valid).toBe(false)
434
- expect(result.error).toBe("Reserved group 'Admins' cannot be renamed")
435
- })
436
-
437
- it('should return invalid when Reviewers group is renamed', () => {
438
- const groups: InternalGroup[] = [
439
- {
440
- id: RESERVED_GROUPS.REVIEWERS as CanopyGroupId,
441
- name: 'Content Reviewers',
442
- members: [],
443
- },
444
- ]
445
- const result = validateReservedGroups(groups)
446
- expect(result.valid).toBe(false)
447
- expect(result.error).toBe("Reserved group 'Reviewers' cannot be renamed")
448
- })
449
- })
450
-
451
- describe('updateInternalGroups safety validations', () => {
452
- it('should reject update that removes last admin', async () => {
453
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
454
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
455
-
456
- const groups: InternalGroup[] = [
457
- {
458
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
459
- name: 'Admins',
460
- members: [],
461
- },
462
- ]
463
-
464
- const req: ApiRequest = {
465
- user: {
466
- type: 'authenticated',
467
- userId: 'admin-1' as CanopyUserId,
468
- groups: [RESERVED_GROUPS.ADMINS],
469
- },
470
- }
471
-
472
- const result = await updateInternalGroups(mockContext, req, { groups })
473
-
474
- expect(result.ok).toBe(false)
475
- expect(result.status).toBe(400)
476
- expect(result.error).toBe('Cannot remove last admin - at least one admin is required')
477
- })
478
-
479
- it('should reject update that renames reserved group', async () => {
480
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
481
- // Mock existing Admins group so it's recognized as existing
482
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([
483
- {
484
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
485
- name: RESERVED_GROUPS.ADMINS,
486
- members: ['admin-1' as CanopyUserId],
487
- },
488
- ])
489
-
490
- const groups: InternalGroup[] = [
491
- {
492
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
493
- name: 'Super Admins',
494
- members: ['admin-1' as CanopyUserId],
495
- },
496
- ]
497
-
498
- const req: ApiRequest = {
499
- user: {
500
- type: 'authenticated',
501
- userId: 'admin-1' as CanopyUserId,
502
- groups: [RESERVED_GROUPS.ADMINS],
503
- },
504
- }
505
-
506
- const result = await updateInternalGroups(mockContext, req, { groups })
507
-
508
- expect(result.ok).toBe(false)
509
- expect(result.status).toBe(400)
510
- expect(result.error).toBe("Reserved group 'Admins' cannot be renamed")
511
- })
512
-
513
- it('should allow update when bootstrap admin exists even with empty Admins group', async () => {
514
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
515
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
516
-
517
- // Add bootstrap admin
518
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
519
-
520
- const groups: InternalGroup[] = [
521
- {
522
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
523
- name: 'Admins',
524
- members: [],
525
- },
526
- ]
527
-
528
- const req: ApiRequest = {
529
- user: {
530
- type: 'authenticated',
531
- userId: 'admin-1' as CanopyUserId,
532
- groups: [RESERVED_GROUPS.ADMINS],
533
- },
534
- }
535
-
536
- const result = await updateInternalGroups(mockContext, req, { groups })
537
-
538
- expect(result.ok).toBe(true)
539
- expect(result.status).toBe(200)
540
- })
541
- })
542
-
543
- describe('collision detection', () => {
544
- it('should reject duplicate group names', async () => {
545
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
546
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
547
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
548
-
549
- const groups: InternalGroup[] = [
550
- {
551
- id: '' as CanopyGroupId,
552
- name: 'Editors',
553
- members: [],
554
- },
555
- {
556
- id: '' as CanopyGroupId,
557
- name: 'Editors', // Duplicate name
558
- members: [],
559
- },
560
- ]
561
-
562
- const req: ApiRequest = {
563
- user: {
564
- type: 'authenticated',
565
- userId: 'admin-1' as CanopyUserId,
566
- groups: [RESERVED_GROUPS.ADMINS],
567
- },
568
- }
569
-
570
- const result = await updateInternalGroups(mockContext, req, { groups })
571
-
572
- expect(result.ok).toBe(false)
573
- expect(result.status).toBe(400)
574
- expect(result.error).toBe('Duplicate group name detected: Editors')
575
- })
576
-
577
- it('should reject duplicate group names case-insensitively', async () => {
578
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
579
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
580
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
581
-
582
- const groups: InternalGroup[] = [
583
- {
584
- id: '' as CanopyGroupId,
585
- name: 'Editors',
586
- members: [],
587
- },
588
- {
589
- id: '' as CanopyGroupId,
590
- name: 'editors', // Same name, different case
591
- members: [],
592
- },
593
- ]
594
-
595
- const req: ApiRequest = {
596
- user: {
597
- type: 'authenticated',
598
- userId: 'admin-1' as CanopyUserId,
599
- groups: [RESERVED_GROUPS.ADMINS],
600
- },
601
- }
602
-
603
- const result = await updateInternalGroups(mockContext, req, { groups })
604
-
605
- expect(result.ok).toBe(false)
606
- expect(result.status).toBe(400)
607
- expect(result.error).toBe('Duplicate group name detected: editors')
608
- })
609
-
610
- it('should reject groups with manually specified duplicate IDs', async () => {
611
- const existingGroups: InternalGroup[] = [
612
- {
613
- id: 'existing-1' as CanopyGroupId,
614
- name: 'Existing Group',
615
- members: [],
616
- },
617
- ]
618
-
619
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
620
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue(existingGroups)
621
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
622
-
623
- const groups: InternalGroup[] = [
624
- {
625
- id: 'existing-1' as CanopyGroupId,
626
- name: 'Existing Group',
627
- members: [],
628
- },
629
- {
630
- id: 'existing-1' as CanopyGroupId, // Duplicate ID
631
- name: 'Another Group',
632
- members: [],
633
- },
634
- ]
635
-
636
- const req: ApiRequest = {
637
- user: {
638
- type: 'authenticated',
639
- userId: 'admin-1' as CanopyUserId,
640
- groups: [RESERVED_GROUPS.ADMINS],
641
- },
642
- }
643
-
644
- const result = await updateInternalGroups(mockContext, req, { groups })
645
-
646
- expect(result.ok).toBe(false)
647
- expect(result.status).toBe(400)
648
- expect(result.error).toBe('Duplicate group ID detected: existing-1')
649
- })
650
- })
651
-
652
- describe('autogenerated group IDs', () => {
653
- it('should generate unique IDs for new groups', async () => {
654
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
655
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
656
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
657
-
658
- const groups: InternalGroup[] = [
659
- {
660
- id: '' as CanopyGroupId,
661
- name: 'Group 1',
662
- members: [],
663
- },
664
- {
665
- id: '' as CanopyGroupId,
666
- name: 'Group 2',
667
- members: [],
668
- },
669
- ]
670
-
671
- const req: ApiRequest = {
672
- user: {
673
- type: 'authenticated',
674
- userId: 'admin-1' as CanopyUserId,
675
- groups: [RESERVED_GROUPS.ADMINS],
676
- },
677
- }
678
-
679
- const result = await updateInternalGroups(mockContext, req, { groups })
680
-
681
- expect(result.ok).toBe(true)
682
-
683
- // Check that saveInternalGroups was called
684
- expect(groupsLoader.saveInternalGroups).toHaveBeenCalled()
685
- const calls = vi.mocked(groupsLoader.saveInternalGroups).mock.calls
686
- const savedGroups = calls[calls.length - 1][1] // Get last call
687
- expect(savedGroups).toBeDefined()
688
- expect(savedGroups.length).toBe(2)
689
- expect(savedGroups[0].id).not.toBe('')
690
- expect(savedGroups[1].id).not.toBe('')
691
- expect(savedGroups[0].id).not.toBe(savedGroups[1].id) // Different IDs
692
- })
693
-
694
- it('should preserve IDs for existing groups', async () => {
695
- const existingGroups: InternalGroup[] = [
696
- {
697
- id: 'existing-group-id' as CanopyGroupId,
698
- name: 'Existing Group',
699
- members: [],
700
- },
701
- ]
702
-
703
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
704
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue(existingGroups)
705
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
706
-
707
- const groups: InternalGroup[] = [
708
- {
709
- id: 'existing-group-id' as CanopyGroupId,
710
- name: 'Existing Group (updated)',
711
- members: ['user-1' as CanopyUserId],
712
- },
713
- ]
714
-
715
- const req: ApiRequest = {
716
- user: {
717
- type: 'authenticated',
718
- userId: 'admin-1' as CanopyUserId,
719
- groups: [RESERVED_GROUPS.ADMINS],
720
- },
721
- }
722
-
723
- const result = await updateInternalGroups(mockContext, req, { groups })
724
-
725
- expect(result.ok).toBe(true)
726
-
727
- const calls = vi.mocked(groupsLoader.saveInternalGroups).mock.calls
728
- const savedGroups = calls[calls.length - 1][1]
729
- expect(savedGroups.length).toBe(1)
730
- expect(savedGroups[0].id).toBe('existing-group-id') // ID preserved
731
- expect(savedGroups[0].name).toBe('Existing Group (updated)') // But name updated
732
- })
733
-
734
- it('should use group name as ID for reserved groups', async () => {
735
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
736
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
737
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
738
-
739
- const groups: InternalGroup[] = [
740
- {
741
- id: '' as CanopyGroupId,
742
- name: 'Admins',
743
- members: ['admin-1' as CanopyUserId],
744
- },
745
- {
746
- id: '' as CanopyGroupId,
747
- name: 'Reviewers',
748
- members: [],
749
- },
750
- ]
751
-
752
- const req: ApiRequest = {
753
- user: {
754
- type: 'authenticated',
755
- userId: 'admin-1' as CanopyUserId,
756
- groups: [RESERVED_GROUPS.ADMINS],
757
- },
758
- }
759
-
760
- const result = await updateInternalGroups(mockContext, req, { groups })
761
-
762
- expect(result.ok).toBe(true)
763
-
764
- const calls = vi.mocked(groupsLoader.saveInternalGroups).mock.calls
765
- const savedGroups = calls[calls.length - 1][1]
766
- expect(savedGroups.length).toBe(2)
767
- expect(savedGroups[0].id).toBe('Admins') // Reserved group uses name as ID
768
- expect(savedGroups[1].id).toBe('Reviewers') // Reserved group uses name as ID
769
- })
770
-
771
- it('should mix existing groups with new groups correctly', async () => {
772
- const existingGroups: InternalGroup[] = [
773
- {
774
- id: 'existing-1' as CanopyGroupId,
775
- name: 'Existing Group',
776
- members: [],
777
- },
778
- ]
779
-
780
- vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
781
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue(existingGroups)
782
- ;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
783
-
784
- const groups: InternalGroup[] = [
785
- {
786
- id: 'existing-1' as CanopyGroupId,
787
- name: 'Existing Group',
788
- members: [],
789
- },
790
- {
791
- id: '' as CanopyGroupId,
792
- name: 'New Group',
793
- members: [],
794
- },
795
- ]
796
-
797
- const req: ApiRequest = {
798
- user: {
799
- type: 'authenticated',
800
- userId: 'admin-1' as CanopyUserId,
801
- groups: [RESERVED_GROUPS.ADMINS],
802
- },
803
- }
804
-
805
- const result = await updateInternalGroups(mockContext, req, { groups })
806
-
807
- expect(result.ok).toBe(true)
808
-
809
- const calls = vi.mocked(groupsLoader.saveInternalGroups).mock.calls
810
- const savedGroups = calls[calls.length - 1][1]
811
- expect(savedGroups.length).toBe(2)
812
- expect(savedGroups[0].id).toBe('existing-1') // Existing ID preserved
813
- expect(savedGroups[1].id).not.toBe('') // New group got generated ID
814
- expect(savedGroups[1].id).not.toBe('existing-1') // Different from existing
815
- })
816
- })
817
-
818
- describe('optimistic locking with contentVersion', () => {
819
- it('should return 409 when expectedContentVersion does not match current version', async () => {
820
- // Mock loadGroupsFile to return a file with contentVersion 5
821
- vi.mocked(groupsLoader.loadGroupsFile).mockResolvedValue({
822
- version: 1,
823
- contentVersion: 5,
824
- updatedAt: '2024-01-01T00:00:00Z',
825
- updatedBy: 'other-admin' as CanopyUserId,
826
- groups: [
827
- {
828
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
829
- name: 'Admins',
830
- members: ['admin-1' as CanopyUserId],
831
- },
832
- ],
833
- })
834
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([
835
- {
836
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
837
- name: 'Admins',
838
- members: ['admin-1' as CanopyUserId],
839
- },
840
- ])
841
-
842
- const req: ApiRequest = {
843
- user: {
844
- type: 'authenticated',
845
- userId: 'admin-1' as CanopyUserId,
846
- groups: [RESERVED_GROUPS.ADMINS],
847
- },
848
- }
849
-
850
- const body = {
851
- groups: [
852
- {
853
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
854
- name: 'Admins',
855
- members: ['admin-1' as CanopyUserId],
856
- },
857
- ],
858
- expectedContentVersion: 3, // Client thinks version is 3, but it's actually 5
859
- }
860
-
861
- const result = await updateInternalGroups(mockContext, req, body)
862
-
863
- expect(result.ok).toBe(false)
864
- expect(result.status).toBe(409)
865
- expect(result.error).toBe(
866
- 'Groups were modified by another user. Please reload and try again.',
867
- )
868
- })
869
-
870
- it('should succeed when expectedContentVersion matches current version', async () => {
871
- // Mock loadGroupsFile to return a file with contentVersion 5
872
- vi.mocked(groupsLoader.loadGroupsFile).mockResolvedValue({
873
- version: 1,
874
- contentVersion: 5,
875
- updatedAt: '2024-01-01T00:00:00Z',
876
- updatedBy: 'admin-1' as CanopyUserId,
877
- groups: [
878
- {
879
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
880
- name: 'Admins',
881
- members: ['admin-1' as CanopyUserId],
882
- },
883
- ],
884
- })
885
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([
886
- {
887
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
888
- name: 'Admins',
889
- members: ['admin-1' as CanopyUserId],
890
- },
891
- ])
892
-
893
- const req: ApiRequest = {
894
- user: {
895
- type: 'authenticated',
896
- userId: 'admin-1' as CanopyUserId,
897
- groups: [RESERVED_GROUPS.ADMINS],
898
- },
899
- }
900
-
901
- const body = {
902
- groups: [
903
- {
904
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
905
- name: 'Admins',
906
- members: ['admin-1' as CanopyUserId],
907
- },
908
- ],
909
- expectedContentVersion: 5, // Matches current version
910
- }
911
-
912
- const result = await updateInternalGroups(mockContext, req, body)
913
-
914
- expect(result.ok).toBe(true)
915
- expect(result.status).toBe(200)
916
-
917
- // Verify saveInternalGroups was called with incremented version
918
- expect(groupsLoader.saveInternalGroups).toHaveBeenCalledWith(
919
- expect.any(String), // branchRoot varies by test context
920
- [{ id: RESERVED_GROUPS.ADMINS, name: 'Admins', members: ['admin-1'] }],
921
- 'admin-1',
922
- 'dev',
923
- 6, // Should be 5 + 1
924
- )
925
- })
926
-
927
- it('should allow update when expectedContentVersion is not provided (backward compatible)', async () => {
928
- // Mock loadGroupsFile to return a file with contentVersion 5
929
- vi.mocked(groupsLoader.loadGroupsFile).mockResolvedValue({
930
- version: 1,
931
- contentVersion: 5,
932
- updatedAt: '2024-01-01T00:00:00Z',
933
- updatedBy: 'admin-1' as CanopyUserId,
934
- groups: [
935
- {
936
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
937
- name: 'Admins',
938
- members: ['admin-1' as CanopyUserId],
939
- },
940
- ],
941
- })
942
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([
943
- {
944
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
945
- name: 'Admins',
946
- members: ['admin-1' as CanopyUserId],
947
- },
948
- ])
949
-
950
- const req: ApiRequest = {
951
- user: {
952
- type: 'authenticated',
953
- userId: 'admin-1' as CanopyUserId,
954
- groups: [RESERVED_GROUPS.ADMINS],
955
- },
956
- }
957
-
958
- const body = {
959
- groups: [
960
- {
961
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
962
- name: 'Admins',
963
- members: ['admin-1' as CanopyUserId],
964
- },
965
- ],
966
- // No expectedContentVersion provided
967
- }
968
-
969
- const result = await updateInternalGroups(mockContext, req, body)
970
-
971
- expect(result.ok).toBe(true)
972
- expect(result.status).toBe(200)
973
- })
974
-
975
- it('should start at version 1 for new files without contentVersion', async () => {
976
- // Mock loadGroupsFile to return null (file doesn't exist)
977
- vi.mocked(groupsLoader.loadGroupsFile).mockResolvedValue(null)
978
- vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
979
-
980
- const req: ApiRequest = {
981
- user: {
982
- type: 'authenticated',
983
- userId: 'admin-1' as CanopyUserId,
984
- groups: [RESERVED_GROUPS.ADMINS],
985
- },
986
- }
987
-
988
- const body = {
989
- groups: [
990
- {
991
- id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
992
- name: 'Admins',
993
- members: ['admin-1' as CanopyUserId],
994
- },
995
- ],
996
- }
997
-
998
- const result = await updateInternalGroups(mockContext, req, body)
999
-
1000
- expect(result.ok).toBe(true)
1001
- expect(result.status).toBe(200)
1002
-
1003
- // Verify saveInternalGroups was called with version 1 (0 + 1)
1004
- expect(groupsLoader.saveInternalGroups).toHaveBeenCalledWith(
1005
- expect.any(String), // branchRoot varies by test context
1006
- [{ id: RESERVED_GROUPS.ADMINS, name: 'Admins', members: ['admin-1'] }],
1007
- 'admin-1',
1008
- 'dev',
1009
- 1,
1010
- )
1011
- })
1012
- })
1013
- })