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.
- package/package.json +2 -3
- package/dist/__integration__/fixtures/content-seeds.d.ts +0 -43
- package/dist/__integration__/fixtures/content-seeds.d.ts.map +0 -1
- package/dist/__integration__/fixtures/content-seeds.js +0 -99
- package/dist/__integration__/fixtures/content-seeds.js.map +0 -1
- package/dist/__integration__/fixtures/schemas.d.ts +0 -12
- package/dist/__integration__/fixtures/schemas.d.ts.map +0 -1
- package/dist/__integration__/fixtures/schemas.js +0 -65
- package/dist/__integration__/fixtures/schemas.js.map +0 -1
- package/dist/__integration__/test-utils/api-client.d.ts +0 -123
- package/dist/__integration__/test-utils/api-client.d.ts.map +0 -1
- package/dist/__integration__/test-utils/api-client.js +0 -118
- package/dist/__integration__/test-utils/api-client.js.map +0 -1
- package/dist/__integration__/test-utils/multi-user.d.ts +0 -25
- package/dist/__integration__/test-utils/multi-user.d.ts.map +0 -1
- package/dist/__integration__/test-utils/multi-user.js +0 -105
- package/dist/__integration__/test-utils/multi-user.js.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.d.ts +0 -25
- package/dist/__integration__/test-utils/test-workspace.d.ts.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.js +0 -102
- package/dist/__integration__/test-utils/test-workspace.js.map +0 -1
- package/dist/editor/BranchManager.stories.d.ts +0 -8
- package/dist/editor/BranchManager.stories.d.ts.map +0 -1
- package/dist/editor/BranchManager.stories.js +0 -74
- package/dist/editor/BranchManager.stories.js.map +0 -1
- package/dist/editor/CanopyEditor.stories.d.ts +0 -7
- package/dist/editor/CanopyEditor.stories.d.ts.map +0 -1
- package/dist/editor/CanopyEditor.stories.js +0 -99
- package/dist/editor/CanopyEditor.stories.js.map +0 -1
- package/dist/editor/CommentsPanel.stories.d.ts +0 -10
- package/dist/editor/CommentsPanel.stories.d.ts.map +0 -1
- package/dist/editor/CommentsPanel.stories.js +0 -175
- package/dist/editor/CommentsPanel.stories.js.map +0 -1
- package/dist/editor/Editor.stories.d.ts +0 -7
- package/dist/editor/Editor.stories.d.ts.map +0 -1
- package/dist/editor/Editor.stories.js +0 -95
- package/dist/editor/Editor.stories.js.map +0 -1
- package/dist/editor/EditorPanes.stories.d.ts +0 -7
- package/dist/editor/EditorPanes.stories.d.ts.map +0 -1
- package/dist/editor/EditorPanes.stories.js +0 -116
- package/dist/editor/EditorPanes.stories.js.map +0 -1
- package/dist/editor/EntryNavigator.stories.d.ts +0 -8
- package/dist/editor/EntryNavigator.stories.d.ts.map +0 -1
- package/dist/editor/EntryNavigator.stories.js +0 -42
- package/dist/editor/EntryNavigator.stories.js.map +0 -1
- package/dist/editor/FormRenderer.stories.d.ts +0 -7
- package/dist/editor/FormRenderer.stories.d.ts.map +0 -1
- package/dist/editor/FormRenderer.stories.js +0 -115
- package/dist/editor/FormRenderer.stories.js.map +0 -1
- package/dist/editor/GroupManager.stories.d.ts +0 -19
- package/dist/editor/GroupManager.stories.d.ts.map +0 -1
- package/dist/editor/GroupManager.stories.js +0 -265
- package/dist/editor/GroupManager.stories.js.map +0 -1
- package/dist/editor/PermissionManager.stories.d.ts +0 -20
- package/dist/editor/PermissionManager.stories.d.ts.map +0 -1
- package/dist/editor/PermissionManager.stories.js +0 -506
- package/dist/editor/PermissionManager.stories.js.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.d.ts +0 -10
- package/dist/editor/comments/FieldWrapper.stories.d.ts.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.js +0 -173
- package/dist/editor/comments/FieldWrapper.stories.js.map +0 -1
- package/dist/editor/fields/BlockField.stories.d.ts +0 -7
- package/dist/editor/fields/BlockField.stories.d.ts.map +0 -1
- package/dist/editor/fields/BlockField.stories.js +0 -50
- package/dist/editor/fields/BlockField.stories.js.map +0 -1
- package/dist/editor/fields/fields.stories.d.ts +0 -8
- package/dist/editor/fields/fields.stories.d.ts.map +0 -1
- package/dist/editor/fields/fields.stories.js +0 -34
- package/dist/editor/fields/fields.stories.js.map +0 -1
- package/dist/test-utils/api-test-helpers.d.ts +0 -238
- package/dist/test-utils/api-test-helpers.d.ts.map +0 -1
- package/dist/test-utils/api-test-helpers.js +0 -347
- package/dist/test-utils/api-test-helpers.js.map +0 -1
- package/dist/test-utils/console-spy.d.ts +0 -56
- package/dist/test-utils/console-spy.d.ts.map +0 -1
- package/dist/test-utils/console-spy.js +0 -81
- package/dist/test-utils/console-spy.js.map +0 -1
- package/dist/test-utils/git-helpers.d.ts +0 -21
- package/dist/test-utils/git-helpers.d.ts.map +0 -1
- package/dist/test-utils/git-helpers.js +0 -23
- package/dist/test-utils/git-helpers.js.map +0 -1
- package/dist/test-utils/index.d.ts +0 -5
- package/dist/test-utils/index.d.ts.map +0 -1
- package/dist/test-utils/index.js +0 -4
- package/dist/test-utils/index.js.map +0 -1
- package/src/__integration__/errors/invalid-content.test.ts +0 -238
- package/src/__integration__/errors/permission-denied.test.ts +0 -220
- package/src/__integration__/fixtures/content-seeds.ts +0 -105
- package/src/__integration__/fixtures/schemas.ts +0 -67
- package/src/__integration__/initialization/prod-sim-init.test.ts +0 -139
- package/src/__integration__/permissions/path-permissions.test.ts +0 -314
- package/src/__integration__/permissions/role-permissions.test.ts +0 -354
- package/src/__integration__/permissions/settings-branch-isolation.test.ts +0 -317
- package/src/__integration__/settings/groups-api.test.ts +0 -403
- package/src/__integration__/test-utils/api-client.ts +0 -167
- package/src/__integration__/test-utils/multi-user.ts +0 -129
- package/src/__integration__/test-utils/test-workspace.ts +0 -130
- package/src/__integration__/user/user-context.test.ts +0 -174
- package/src/__integration__/validation/input-validation.test.ts +0 -166
- package/src/__integration__/workflows/api-editing-workflow.test.ts +0 -244
- package/src/__integration__/workflows/conflict-resolution.test.ts +0 -259
- package/src/__integration__/workflows/editing-workflow.test.ts +0 -205
- package/src/__integration__/workflows/review-workflow.test.ts +0 -260
- package/src/ai/__tests__/build.integration.test.ts +0 -224
- package/src/ai/__tests__/generate.integration.test.ts +0 -495
- package/src/ai/__tests__/handler.integration.test.ts +0 -212
- package/src/ai/__tests__/json-to-markdown.test.ts +0 -553
- package/src/ai/generate.ts +0 -410
- package/src/ai/handler.ts +0 -123
- package/src/ai/index.ts +0 -26
- package/src/ai/json-to-markdown.ts +0 -424
- package/src/ai/resolve-branch.ts +0 -34
- package/src/ai/types.ts +0 -160
- package/src/api/AGENTS.md +0 -81
- package/src/api/__test__/mock-client.ts +0 -404
- package/src/api/assets.test.ts +0 -140
- package/src/api/assets.ts +0 -154
- package/src/api/branch-merge.test.ts +0 -163
- package/src/api/branch-merge.ts +0 -113
- package/src/api/branch-review.test.ts +0 -297
- package/src/api/branch-review.ts +0 -136
- package/src/api/branch-status.test.ts +0 -85
- package/src/api/branch-status.ts +0 -153
- package/src/api/branch-withdraw.test.ts +0 -146
- package/src/api/branch-withdraw.ts +0 -81
- package/src/api/branch-workflow.integration.test.ts +0 -578
- package/src/api/branch.test.ts +0 -620
- package/src/api/branch.ts +0 -492
- package/src/api/client.test.ts +0 -349
- package/src/api/client.ts +0 -506
- package/src/api/comments.test.ts +0 -285
- package/src/api/comments.ts +0 -210
- package/src/api/content.test.ts +0 -345
- package/src/api/content.ts +0 -454
- package/src/api/entries.test.ts +0 -1339
- package/src/api/entries.ts +0 -650
- package/src/api/github-sync.ts +0 -144
- package/src/api/groups.test.ts +0 -1013
- package/src/api/groups.ts +0 -375
- package/src/api/guards.test.ts +0 -533
- package/src/api/guards.ts +0 -271
- package/src/api/index.ts +0 -87
- package/src/api/permissions.test.ts +0 -766
- package/src/api/permissions.ts +0 -334
- package/src/api/reference-options.ts +0 -118
- package/src/api/resolve-references.ts +0 -107
- package/src/api/route-builder.ts +0 -289
- package/src/api/schema.test.ts +0 -840
- package/src/api/schema.ts +0 -936
- package/src/api/security.test.ts +0 -233
- package/src/api/settings-helpers.ts +0 -84
- package/src/api/types.ts +0 -40
- package/src/api/user.test.ts +0 -127
- package/src/api/user.ts +0 -42
- package/src/api/validators.test.ts +0 -275
- package/src/api/validators.ts +0 -176
- package/src/asset-store.test.ts +0 -37
- package/src/asset-store.ts +0 -110
- package/src/auth/cache.ts +0 -7
- package/src/auth/caching-auth-plugin.test.ts +0 -154
- package/src/auth/caching-auth-plugin.ts +0 -109
- package/src/auth/context-helpers.ts +0 -75
- package/src/auth/file-based-auth-cache.test.ts +0 -257
- package/src/auth/file-based-auth-cache.ts +0 -279
- package/src/auth/index.ts +0 -12
- package/src/auth/plugin.ts +0 -51
- package/src/auth/types.ts +0 -38
- package/src/authorization/__tests__/branch.test.ts +0 -260
- package/src/authorization/__tests__/content.test.ts +0 -142
- package/src/authorization/__tests__/path.test.ts +0 -133
- package/src/authorization/__tests__/permissions-loader.test.ts +0 -200
- package/src/authorization/branch.ts +0 -94
- package/src/authorization/content.ts +0 -93
- package/src/authorization/groups/index.ts +0 -11
- package/src/authorization/groups/loader.ts +0 -127
- package/src/authorization/groups/schema.ts +0 -48
- package/src/authorization/helpers.ts +0 -48
- package/src/authorization/index.ts +0 -84
- package/src/authorization/path.ts +0 -112
- package/src/authorization/permissions/index.ts +0 -11
- package/src/authorization/permissions/loader.ts +0 -116
- package/src/authorization/permissions/schema.ts +0 -66
- package/src/authorization/test-utils.ts +0 -15
- package/src/authorization/types.ts +0 -66
- package/src/authorization/validation.test.ts +0 -100
- package/src/authorization/validation.ts +0 -62
- package/src/branch-metadata.test.ts +0 -168
- package/src/branch-metadata.ts +0 -166
- package/src/branch-registry.test.ts +0 -248
- package/src/branch-registry.ts +0 -152
- package/src/branch-schema-cache.test.ts +0 -275
- package/src/branch-schema-cache.ts +0 -189
- package/src/branch-workspace.test.ts +0 -183
- package/src/branch-workspace.ts +0 -124
- package/src/build/generate-ai-content.ts +0 -78
- package/src/build/index.ts +0 -8
- package/src/build-mode.ts +0 -27
- package/src/cli/generate-ai-content.ts +0 -100
- package/src/cli/init.test.ts +0 -240
- package/src/cli/templates/Dockerfile.cms.template +0 -19
- package/src/cli/templates/canopy.ts.template +0 -55
- package/src/cli/templates/canopycms.config.ts.template +0 -11
- package/src/cli/templates/deploy-cms.yml.template +0 -27
- package/src/cli/templates/edit-page.tsx.template +0 -32
- package/src/cli/templates/route.ts.template +0 -12
- package/src/cli/templates/schemas.ts.template +0 -16
- package/src/cli/templates.ts +0 -47
- package/src/client.ts +0 -12
- package/src/comment-store.test.ts +0 -442
- package/src/comment-store.ts +0 -301
- package/src/config/__tests__/config.test.ts +0 -513
- package/src/config/flatten.ts +0 -174
- package/src/config/helpers.ts +0 -167
- package/src/config/index.ts +0 -86
- package/src/config/schemas/collection.ts +0 -67
- package/src/config/schemas/config.ts +0 -77
- package/src/config/schemas/field.ts +0 -108
- package/src/config/schemas/media.ts +0 -27
- package/src/config/schemas/permissions.ts +0 -21
- package/src/config/types.ts +0 -321
- package/src/config/validation.ts +0 -70
- package/src/config-test.ts +0 -65
- package/src/config.ts +0 -11
- package/src/content-id-index.test.ts +0 -512
- package/src/content-id-index.ts +0 -479
- package/src/content-reader.test.ts +0 -478
- package/src/content-reader.ts +0 -214
- package/src/content-store.test.ts +0 -1126
- package/src/content-store.ts +0 -793
- package/src/context.ts +0 -111
- package/src/editor/BranchManager.stories.tsx +0 -80
- package/src/editor/BranchManager.test.tsx +0 -324
- package/src/editor/BranchManager.tsx +0 -461
- package/src/editor/CanopyEditor.stories.tsx +0 -128
- package/src/editor/CanopyEditor.test.tsx +0 -81
- package/src/editor/CanopyEditor.tsx +0 -73
- package/src/editor/CanopyEditorPage.test.tsx +0 -59
- package/src/editor/CanopyEditorPage.tsx +0 -25
- package/src/editor/CommentsPanel.stories.tsx +0 -184
- package/src/editor/CommentsPanel.tsx +0 -338
- package/src/editor/Editor.integration.test.tsx +0 -227
- package/src/editor/Editor.stories.tsx +0 -119
- package/src/editor/Editor.tsx +0 -1221
- package/src/editor/EditorPanes.stories.tsx +0 -256
- package/src/editor/EditorPanes.test.tsx +0 -77
- package/src/editor/EditorPanes.tsx +0 -180
- package/src/editor/EntryNavigator.stories.tsx +0 -65
- package/src/editor/EntryNavigator.test.tsx +0 -598
- package/src/editor/EntryNavigator.tsx +0 -665
- package/src/editor/FormRenderer.stories.tsx +0 -212
- package/src/editor/FormRenderer.test.tsx +0 -194
- package/src/editor/FormRenderer.tsx +0 -432
- package/src/editor/GroupManager.stories.tsx +0 -301
- package/src/editor/GroupManager.test.tsx +0 -682
- package/src/editor/GroupManager.tsx +0 -9
- package/src/editor/PermissionManager.stories.tsx +0 -539
- package/src/editor/PermissionManager.test.tsx +0 -864
- package/src/editor/PermissionManager.tsx +0 -12
- package/src/editor/canopy-path.test.ts +0 -23
- package/src/editor/canopy-path.ts +0 -52
- package/src/editor/client-reference-resolver.ts +0 -118
- package/src/editor/comments/BranchComments.tsx +0 -93
- package/src/editor/comments/EntryComments.tsx +0 -94
- package/src/editor/comments/FieldWrapper.stories.tsx +0 -210
- package/src/editor/comments/FieldWrapper.tsx +0 -129
- package/src/editor/comments/InlineCommentThread.test.tsx +0 -384
- package/src/editor/comments/InlineCommentThread.tsx +0 -246
- package/src/editor/comments/ThreadCarousel.test.tsx +0 -393
- package/src/editor/comments/ThreadCarousel.tsx +0 -525
- package/src/editor/components/ConfirmDeleteModal.tsx +0 -49
- package/src/editor/components/EditorContext.tsx +0 -49
- package/src/editor/components/EditorFooter.tsx +0 -47
- package/src/editor/components/EditorHeader.tsx +0 -492
- package/src/editor/components/EditorSidebar.tsx +0 -193
- package/src/editor/components/EntryCreateModal.tsx +0 -193
- package/src/editor/components/RenameEntryModal.tsx +0 -152
- package/src/editor/components/UserBadge.test.tsx +0 -274
- package/src/editor/components/UserBadge.tsx +0 -240
- package/src/editor/components/index.ts +0 -6
- package/src/editor/context/ApiClientContext.tsx +0 -56
- package/src/editor/context/EditorStateContext.tsx +0 -221
- package/src/editor/context/index.ts +0 -40
- package/src/editor/editor-config.test.ts +0 -385
- package/src/editor/editor-config.ts +0 -94
- package/src/editor/editor-utils.test.ts +0 -772
- package/src/editor/editor-utils.ts +0 -303
- package/src/editor/env.ts +0 -4
- package/src/editor/fields/BlockField.stories.tsx +0 -79
- package/src/editor/fields/BlockField.tsx +0 -267
- package/src/editor/fields/CodeField.tsx +0 -41
- package/src/editor/fields/MarkdownField.tsx +0 -205
- package/src/editor/fields/ObjectField.tsx +0 -71
- package/src/editor/fields/ReferenceField.tsx +0 -138
- package/src/editor/fields/SelectField.tsx +0 -76
- package/src/editor/fields/TextField.tsx +0 -35
- package/src/editor/fields/ToggleField.tsx +0 -37
- package/src/editor/fields/fields.stories.tsx +0 -40
- package/src/editor/group-manager/ExternalGroupsTab.tsx +0 -114
- package/src/editor/group-manager/GroupCard.tsx +0 -102
- package/src/editor/group-manager/GroupForm.tsx +0 -66
- package/src/editor/group-manager/InternalGroupsTab.tsx +0 -147
- package/src/editor/group-manager/MemberList.tsx +0 -184
- package/src/editor/group-manager/hooks/useExternalGroupSearch.ts +0 -63
- package/src/editor/group-manager/hooks/useGroupState.ts +0 -134
- package/src/editor/group-manager/hooks/useUserSearch.ts +0 -84
- package/src/editor/group-manager/index.tsx +0 -210
- package/src/editor/group-manager/types.ts +0 -28
- package/src/editor/hooks/README.md +0 -26
- package/src/editor/hooks/__test__/test-utils.tsx +0 -183
- package/src/editor/hooks/index.ts +0 -23
- package/src/editor/hooks/useBranchActions.test.tsx +0 -267
- package/src/editor/hooks/useBranchActions.tsx +0 -121
- package/src/editor/hooks/useBranchManager.test.tsx +0 -391
- package/src/editor/hooks/useBranchManager.tsx +0 -326
- package/src/editor/hooks/useCommentSystem.test.ts +0 -615
- package/src/editor/hooks/useCommentSystem.ts +0 -347
- package/src/editor/hooks/useDraftManager.test.ts +0 -375
- package/src/editor/hooks/useDraftManager.ts +0 -259
- package/src/editor/hooks/useEditorLayout.test.ts +0 -147
- package/src/editor/hooks/useEditorLayout.ts +0 -67
- package/src/editor/hooks/useEntryManager.test.ts +0 -588
- package/src/editor/hooks/useEntryManager.ts +0 -387
- package/src/editor/hooks/useGroupManager.test.ts +0 -277
- package/src/editor/hooks/useGroupManager.ts +0 -139
- package/src/editor/hooks/usePermissionManager.test.ts +0 -211
- package/src/editor/hooks/usePermissionManager.ts +0 -113
- package/src/editor/hooks/useReferenceResolution.ts +0 -248
- package/src/editor/hooks/useSchemaManager.test.ts +0 -370
- package/src/editor/hooks/useSchemaManager.ts +0 -310
- package/src/editor/hooks/useUserContext.tsx +0 -57
- package/src/editor/hooks/useUserMetadata.test.ts +0 -191
- package/src/editor/hooks/useUserMetadata.ts +0 -71
- package/src/editor/permission-manager/GroupSelector.tsx +0 -73
- package/src/editor/permission-manager/PermissionEditor.tsx +0 -321
- package/src/editor/permission-manager/PermissionLevelBadge.tsx +0 -53
- package/src/editor/permission-manager/PermissionTree.tsx +0 -237
- package/src/editor/permission-manager/UserSelector.tsx +0 -95
- package/src/editor/permission-manager/constants.tsx +0 -18
- package/src/editor/permission-manager/hooks/useGroupsAndUsers.ts +0 -153
- package/src/editor/permission-manager/hooks/usePermissionTree.ts +0 -200
- package/src/editor/permission-manager/index.tsx +0 -294
- package/src/editor/permission-manager/types.ts +0 -58
- package/src/editor/permission-manager/utils.ts +0 -179
- package/src/editor/preview-bridge.test.tsx +0 -50
- package/src/editor/preview-bridge.tsx +0 -294
- package/src/editor/schema-editor/CollectionEditor.test.tsx +0 -238
- package/src/editor/schema-editor/CollectionEditor.tsx +0 -520
- package/src/editor/schema-editor/EntryTypeEditor.test.tsx +0 -215
- package/src/editor/schema-editor/EntryTypeEditor.tsx +0 -367
- package/src/editor/schema-editor/index.ts +0 -19
- package/src/editor/setup-test-dom.ts +0 -10
- package/src/editor/test-setup.ts +0 -33
- package/src/editor/theme.tsx +0 -119
- package/src/editor/utils/env.ts +0 -39
- package/src/entry-schema-registry.test.ts +0 -281
- package/src/entry-schema-registry.ts +0 -121
- package/src/entry-schema.ts +0 -84
- package/src/git-manager.test.ts +0 -552
- package/src/git-manager.ts +0 -667
- package/src/github-service.test.ts +0 -312
- package/src/github-service.ts +0 -295
- package/src/http/handler.test.ts +0 -275
- package/src/http/handler.ts +0 -280
- package/src/http/index.ts +0 -11
- package/src/http/router.ts +0 -164
- package/src/http/types.ts +0 -44
- package/src/id.test.ts +0 -48
- package/src/id.ts +0 -22
- package/src/index.ts +0 -26
- package/src/operating-mode/__tests__/strategies.test.ts +0 -511
- package/src/operating-mode/client-safe-strategy.ts +0 -184
- package/src/operating-mode/client-unsafe-strategy.ts +0 -303
- package/src/operating-mode/client.ts +0 -13
- package/src/operating-mode/index.ts +0 -34
- package/src/operating-mode/types.ts +0 -186
- package/src/paths/__tests__/branch.test.ts +0 -53
- package/src/paths/__tests__/normalize.test.ts +0 -141
- package/src/paths/__tests__/resolve.test.ts +0 -207
- package/src/paths/__tests__/validation.test.ts +0 -61
- package/src/paths/branch.ts +0 -115
- package/src/paths/index.ts +0 -73
- package/src/paths/normalize-server.ts +0 -40
- package/src/paths/normalize.ts +0 -107
- package/src/paths/resolve.ts +0 -61
- package/src/paths/test-utils.ts +0 -37
- package/src/paths/types.ts +0 -68
- package/src/paths/validation.test.ts +0 -480
- package/src/paths/validation.ts +0 -391
- package/src/reference-resolver.test.ts +0 -107
- package/src/reference-resolver.ts +0 -157
- package/src/schema/index.ts +0 -29
- package/src/schema/meta-loader.ts +0 -366
- package/src/schema/resolver.ts +0 -83
- package/src/schema/schema-store-types.ts +0 -56
- package/src/schema/schema-store.test.ts +0 -816
- package/src/schema/schema-store.ts +0 -795
- package/src/schema/types.ts +0 -33
- package/src/schema-meta-loader.test.ts +0 -447
- package/src/server.ts +0 -15
- package/src/services.test.ts +0 -559
- package/src/services.ts +0 -373
- package/src/settings-branch-utils.ts +0 -53
- package/src/settings-workspace.ts +0 -156
- package/src/task-queue/README.md +0 -144
- package/src/task-queue/index.ts +0 -14
- package/src/task-queue/task-queue.test.ts +0 -524
- package/src/task-queue/task-queue.ts +0 -514
- package/src/task-queue/types.ts +0 -41
- package/src/test-utils/api-test-helpers.ts +0 -445
- package/src/test-utils/console-spy.test.ts +0 -14
- package/src/test-utils/console-spy.ts +0 -125
- package/src/test-utils/git-helpers.ts +0 -31
- package/src/test-utils/index.ts +0 -4
- package/src/types.ts +0 -54
- package/src/user.ts +0 -118
- package/src/utils/debug.test.ts +0 -114
- package/src/utils/debug.ts +0 -127
- package/src/utils/error.test.ts +0 -92
- package/src/utils/error.ts +0 -83
- package/src/utils/format.ts +0 -12
- package/src/validation/__tests__/field-traversal.test.ts +0 -263
- package/src/validation/deletion-checker.ts +0 -234
- package/src/validation/field-traversal.ts +0 -146
- package/src/validation/reference-validator.ts +0 -168
- package/src/worker/cms-worker-rebase.test.ts +0 -473
- package/src/worker/cms-worker.ts +0 -777
- package/src/worker/integration.test.ts +0 -289
- package/src/worker/task-queue-config.ts +0 -25
- package/src/worker/task-queue.test.ts +0 -452
- package/src/worker/task-queue.ts +0 -58
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import type { AuthCacheProvider } from './caching-auth-plugin'
|
|
4
|
-
import type { UserSearchResult, GroupMetadata } from './types'
|
|
5
|
-
import type { CanopyUserId, CanopyGroupId } from '../types'
|
|
6
|
-
import { createDebugLogger } from '../utils/debug'
|
|
7
|
-
|
|
8
|
-
const log = createDebugLogger({ prefix: 'FileBasedAuthCache' })
|
|
9
|
-
|
|
10
|
-
interface CachedUsers {
|
|
11
|
-
users: UserSearchResult[]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface CachedGroups {
|
|
15
|
-
groups: GroupMetadata[]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface CachedMemberships {
|
|
19
|
-
memberships: Record<string, string[]> // userId -> groupId[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface LoadedCache {
|
|
23
|
-
users: Map<CanopyUserId, UserSearchResult>
|
|
24
|
-
groups: Map<CanopyGroupId, GroupMetadata>
|
|
25
|
-
memberships: Map<CanopyUserId, CanopyGroupId[]>
|
|
26
|
-
allUsers: UserSearchResult[]
|
|
27
|
-
allGroups: GroupMetadata[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolve the active cache directory.
|
|
32
|
-
*
|
|
33
|
-
* Supports two layouts:
|
|
34
|
-
* 1. Snapshot layout (preferred): {cachePath}/current → {cachePath}/snapshot-{ts}/
|
|
35
|
-
* The `current` symlink points to the active snapshot directory.
|
|
36
|
-
* 2. Flat layout (legacy/simple): files directly in {cachePath}/
|
|
37
|
-
*
|
|
38
|
-
* Returns the directory path where users.json, orgs.json, memberships.json live.
|
|
39
|
-
*/
|
|
40
|
-
async function resolveActiveCacheDir(cachePath: string): Promise<string> {
|
|
41
|
-
const currentLink = path.join(cachePath, 'current')
|
|
42
|
-
try {
|
|
43
|
-
const target = await fs.readlink(currentLink)
|
|
44
|
-
// Symlink target may be relative or absolute
|
|
45
|
-
const resolved = path.isAbsolute(target) ? target : path.resolve(cachePath, target)
|
|
46
|
-
// SECURITY: Validate that resolved target stays within the expected cache directory
|
|
47
|
-
const normalizedCache = path.resolve(cachePath)
|
|
48
|
-
const normalizedTarget = path.resolve(resolved)
|
|
49
|
-
if (
|
|
50
|
-
!normalizedTarget.startsWith(normalizedCache + path.sep) &&
|
|
51
|
-
normalizedTarget !== normalizedCache
|
|
52
|
-
) {
|
|
53
|
-
log.debug('cache', 'Symlink target escapes cache directory', {
|
|
54
|
-
cachePath: normalizedCache,
|
|
55
|
-
target: normalizedTarget,
|
|
56
|
-
})
|
|
57
|
-
return cachePath
|
|
58
|
-
}
|
|
59
|
-
return resolved
|
|
60
|
-
} catch {
|
|
61
|
-
// No symlink — fall back to flat layout
|
|
62
|
-
return cachePath
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* File-based auth cache provider.
|
|
68
|
-
* Reads JSON files from a directory that is populated externally
|
|
69
|
-
* (e.g., by an EC2 worker running refreshClerkCache).
|
|
70
|
-
*
|
|
71
|
-
* Supports two directory layouts:
|
|
72
|
-
* - Snapshot layout: {cachePath}/current/ symlink → snapshot-{ts}/ directory
|
|
73
|
-
* - Flat layout: files directly in {cachePath}/
|
|
74
|
-
*
|
|
75
|
-
* Expects:
|
|
76
|
-
* - users.json — { users: UserSearchResult[] }
|
|
77
|
-
* - orgs.json — { groups: GroupMetadata[] }
|
|
78
|
-
* - memberships.json — { memberships: { [userId]: groupId[] } }
|
|
79
|
-
*
|
|
80
|
-
* Caches in memory and re-reads when file mtime changes.
|
|
81
|
-
*/
|
|
82
|
-
export class FileBasedAuthCache implements AuthCacheProvider {
|
|
83
|
-
private cache: LoadedCache | null = null
|
|
84
|
-
private lastMtime = 0
|
|
85
|
-
|
|
86
|
-
constructor(private readonly cachePath: string) {}
|
|
87
|
-
|
|
88
|
-
async getUser(userId: CanopyUserId): Promise<UserSearchResult | null> {
|
|
89
|
-
const cache = await this.ensureLoaded()
|
|
90
|
-
return cache.users.get(userId) ?? null
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async getGroup(groupId: CanopyGroupId): Promise<GroupMetadata | null> {
|
|
94
|
-
const cache = await this.ensureLoaded()
|
|
95
|
-
return cache.groups.get(groupId) ?? null
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async getAllUsers(): Promise<UserSearchResult[]> {
|
|
99
|
-
const cache = await this.ensureLoaded()
|
|
100
|
-
return cache.allUsers
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async getAllGroups(): Promise<GroupMetadata[]> {
|
|
104
|
-
const cache = await this.ensureLoaded()
|
|
105
|
-
return cache.allGroups
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async getUserExternalGroups(userId: CanopyUserId): Promise<CanopyGroupId[]> {
|
|
109
|
-
const cache = await this.ensureLoaded()
|
|
110
|
-
return cache.memberships.get(userId) ?? []
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private async ensureLoaded(): Promise<LoadedCache> {
|
|
114
|
-
const activeDir = await resolveActiveCacheDir(this.cachePath)
|
|
115
|
-
|
|
116
|
-
const usersPath = path.join(activeDir, 'users.json')
|
|
117
|
-
const orgsPath = path.join(activeDir, 'orgs.json')
|
|
118
|
-
const membershipsPath = path.join(activeDir, 'memberships.json')
|
|
119
|
-
|
|
120
|
-
// Check max mtime across all three files for cache freshness
|
|
121
|
-
let maxMtime = 0
|
|
122
|
-
for (const filePath of [usersPath, orgsPath, membershipsPath]) {
|
|
123
|
-
try {
|
|
124
|
-
const stat = await fs.stat(filePath)
|
|
125
|
-
maxMtime = Math.max(maxMtime, stat.mtimeMs)
|
|
126
|
-
} catch {
|
|
127
|
-
// File doesn't exist — continue checking others
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (maxMtime === 0) {
|
|
132
|
-
// No cache files exist — return empty cache
|
|
133
|
-
if (!this.cache) {
|
|
134
|
-
this.cache = this.emptyCache()
|
|
135
|
-
}
|
|
136
|
-
return this.cache
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// If max mtime hasn't changed and we have a cache, return it
|
|
140
|
-
if (this.cache && maxMtime === this.lastMtime) {
|
|
141
|
-
return this.cache
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Load fresh data
|
|
145
|
-
this.cache = await this.loadFromDisk(activeDir)
|
|
146
|
-
this.lastMtime = maxMtime
|
|
147
|
-
return this.cache
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private async loadFromDisk(dir: string): Promise<LoadedCache> {
|
|
151
|
-
const usersPath = path.join(dir, 'users.json')
|
|
152
|
-
const orgsPath = path.join(dir, 'orgs.json')
|
|
153
|
-
const membershipsPath = path.join(dir, 'memberships.json')
|
|
154
|
-
|
|
155
|
-
const [usersData, orgsData, membershipsData] = await Promise.all([
|
|
156
|
-
this.readJsonFile<CachedUsers>(usersPath, { users: [] }),
|
|
157
|
-
this.readJsonFile<CachedGroups>(orgsPath, { groups: [] }),
|
|
158
|
-
this.readJsonFile<CachedMemberships>(membershipsPath, {
|
|
159
|
-
memberships: {},
|
|
160
|
-
}),
|
|
161
|
-
])
|
|
162
|
-
|
|
163
|
-
const users = new Map<CanopyUserId, UserSearchResult>()
|
|
164
|
-
for (const user of usersData.users) {
|
|
165
|
-
users.set(user.id, user)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const groups = new Map<CanopyGroupId, GroupMetadata>()
|
|
169
|
-
for (const group of orgsData.groups) {
|
|
170
|
-
groups.set(group.id, group)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const memberships = new Map<CanopyUserId, CanopyGroupId[]>()
|
|
174
|
-
for (const [userId, groupIds] of Object.entries(membershipsData.memberships)) {
|
|
175
|
-
memberships.set(userId as CanopyUserId, groupIds as CanopyGroupId[])
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
log.debug('cache', 'Loaded auth cache', {
|
|
179
|
-
dir,
|
|
180
|
-
users: users.size,
|
|
181
|
-
groups: groups.size,
|
|
182
|
-
memberships: memberships.size,
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
users,
|
|
187
|
-
groups,
|
|
188
|
-
memberships,
|
|
189
|
-
allUsers: usersData.users,
|
|
190
|
-
allGroups: orgsData.groups,
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private async readJsonFile<T>(filePath: string, fallback: T): Promise<T> {
|
|
195
|
-
try {
|
|
196
|
-
const content = await fs.readFile(filePath, 'utf-8')
|
|
197
|
-
return JSON.parse(content) as T
|
|
198
|
-
} catch {
|
|
199
|
-
log.debug('cache', 'Cache file not found or invalid', { path: filePath })
|
|
200
|
-
return fallback
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private emptyCache(): LoadedCache {
|
|
205
|
-
return {
|
|
206
|
-
users: new Map(),
|
|
207
|
-
groups: new Map(),
|
|
208
|
-
memberships: new Map(),
|
|
209
|
-
allUsers: [],
|
|
210
|
-
allGroups: [],
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Write auth cache files atomically using a snapshot directory and symlink swap.
|
|
217
|
-
*
|
|
218
|
-
* 1. Writes files to a timestamped snapshot directory: {cachePath}/snapshot-{ts}/
|
|
219
|
-
* 2. Creates a temporary symlink, then atomically renames it to {cachePath}/current
|
|
220
|
-
* 3. Cleans up old snapshot directories (keeps the 2 most recent)
|
|
221
|
-
*
|
|
222
|
-
* This ensures readers (FileBasedAuthCache) always see a consistent set of files:
|
|
223
|
-
* either the old snapshot or the new one, never a mix.
|
|
224
|
-
*/
|
|
225
|
-
export async function writeAuthCacheSnapshot(
|
|
226
|
-
cachePath: string,
|
|
227
|
-
files: Record<string, unknown>,
|
|
228
|
-
): Promise<string> {
|
|
229
|
-
await fs.mkdir(cachePath, { recursive: true })
|
|
230
|
-
|
|
231
|
-
const timestamp = Date.now()
|
|
232
|
-
const snapshotDir = path.join(cachePath, `snapshot-${timestamp}`)
|
|
233
|
-
await fs.mkdir(snapshotDir, { recursive: true })
|
|
234
|
-
|
|
235
|
-
// Write all files to the snapshot directory
|
|
236
|
-
for (const [fileName, data] of Object.entries(files)) {
|
|
237
|
-
const tmpPath = path.join(snapshotDir, `${fileName}.tmp`)
|
|
238
|
-
const finalPath = path.join(snapshotDir, fileName)
|
|
239
|
-
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8')
|
|
240
|
-
await fs.rename(tmpPath, finalPath)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Atomic symlink swap: create temp symlink, rename over current
|
|
244
|
-
const currentLink = path.join(cachePath, 'current')
|
|
245
|
-
const tmpLink = path.join(cachePath, `current-${timestamp}`)
|
|
246
|
-
await fs.symlink(snapshotDir, tmpLink)
|
|
247
|
-
await fs.rename(tmpLink, currentLink)
|
|
248
|
-
|
|
249
|
-
// Clean up old snapshots (keep the 2 most recent)
|
|
250
|
-
await cleanupOldSnapshots(cachePath, 2)
|
|
251
|
-
|
|
252
|
-
return snapshotDir
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async function cleanupOldSnapshots(cachePath: string, keepCount: number): Promise<void> {
|
|
256
|
-
let entries: string[]
|
|
257
|
-
try {
|
|
258
|
-
entries = await fs.readdir(cachePath)
|
|
259
|
-
} catch {
|
|
260
|
-
return
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const snapshots = entries
|
|
264
|
-
.filter((e) => e.startsWith('snapshot-'))
|
|
265
|
-
.sort()
|
|
266
|
-
.reverse()
|
|
267
|
-
|
|
268
|
-
// Skip the most recent `keepCount` snapshots
|
|
269
|
-
for (const snapshot of snapshots.slice(keepCount)) {
|
|
270
|
-
try {
|
|
271
|
-
await fs.rm(path.join(cachePath, snapshot), {
|
|
272
|
-
recursive: true,
|
|
273
|
-
force: true,
|
|
274
|
-
})
|
|
275
|
-
} catch {
|
|
276
|
-
// Best effort cleanup
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
package/src/auth/index.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export type { AuthPlugin, AuthPluginFactory } from './plugin'
|
|
2
|
-
export type { UserSearchResult, GroupMetadata, AuthenticationResult } from './types'
|
|
3
|
-
export {
|
|
4
|
-
isCanopyRequest,
|
|
5
|
-
isHeadersLike,
|
|
6
|
-
extractHeaders,
|
|
7
|
-
validateAuthContext,
|
|
8
|
-
} from './context-helpers'
|
|
9
|
-
export type { HeadersLike } from './context-helpers'
|
|
10
|
-
// Server-only implementations (CachingAuthPlugin, FileBasedAuthCache, writeAuthCacheSnapshot)
|
|
11
|
-
// are exported via 'canopycms/auth/cache' to avoid pulling Node.js APIs into client bundles.
|
|
12
|
-
export type { AuthCacheProvider, TokenVerifier } from './caching-auth-plugin'
|
package/src/auth/plugin.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { UserSearchResult, GroupMetadata, AuthenticationResult } from './types'
|
|
2
|
-
import type { CanopyUserId, CanopyGroupId } from '../types'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Abstract auth provider interface.
|
|
6
|
-
* Implement this to integrate different auth systems (Clerk, Auth0, NextAuth, etc.)
|
|
7
|
-
*/
|
|
8
|
-
export interface AuthPlugin {
|
|
9
|
-
/**
|
|
10
|
-
* Authenticate user from request context.
|
|
11
|
-
* Returns user identity (without final groups) - core will apply bootstrap admins.
|
|
12
|
-
*
|
|
13
|
-
* @param context - Framework-specific context (CanopyRequest, headers, etc.)
|
|
14
|
-
* @returns AuthenticationResult with user identity or error
|
|
15
|
-
*/
|
|
16
|
-
authenticate(context: unknown): Promise<AuthenticationResult>
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Search for users (for permission management UI)
|
|
20
|
-
* @param query - Search string (email, name, etc.)
|
|
21
|
-
* @param limit - Max results (default 10)
|
|
22
|
-
*/
|
|
23
|
-
searchUsers(query: string, limit?: number): Promise<UserSearchResult[]>
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Get detailed user metadata by ID
|
|
27
|
-
*/
|
|
28
|
-
getUserMetadata(userId: CanopyUserId): Promise<UserSearchResult | null>
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Get group/organization metadata by ID
|
|
32
|
-
*/
|
|
33
|
-
getGroupMetadata(groupId: CanopyGroupId): Promise<GroupMetadata | null>
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* List all groups (for permission UI dropdowns)
|
|
37
|
-
*/
|
|
38
|
-
listGroups(limit?: number): Promise<GroupMetadata[]>
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Search for external groups/organizations (for group management UI)
|
|
42
|
-
* Optional - only needed if auth provider supports external groups
|
|
43
|
-
* @param query - Search string (name, ID, etc.)
|
|
44
|
-
*/
|
|
45
|
-
searchExternalGroups?(query: string): Promise<Array<{ id: CanopyGroupId; name: string }>>
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Factory function type for creating auth plugins
|
|
50
|
-
*/
|
|
51
|
-
export type AuthPluginFactory<TConfig = unknown> = (config: TConfig) => AuthPlugin
|
package/src/auth/types.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { CanopyUserId, CanopyGroupId } from '../types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* User search result for permission UI
|
|
5
|
-
*/
|
|
6
|
-
export interface UserSearchResult {
|
|
7
|
-
id: CanopyUserId
|
|
8
|
-
name: string
|
|
9
|
-
email: string
|
|
10
|
-
avatarUrl?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Group metadata for permission UI
|
|
15
|
-
*/
|
|
16
|
-
export interface GroupMetadata {
|
|
17
|
-
id: CanopyGroupId
|
|
18
|
-
name: string
|
|
19
|
-
description?: string
|
|
20
|
-
memberCount?: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Authentication result from auth plugins.
|
|
25
|
-
* Returns user identity (without final groups) on success.
|
|
26
|
-
*/
|
|
27
|
-
export interface AuthenticationResult {
|
|
28
|
-
success: boolean
|
|
29
|
-
user?: {
|
|
30
|
-
userId: CanopyUserId
|
|
31
|
-
email?: string
|
|
32
|
-
name?: string
|
|
33
|
-
avatarUrl?: string
|
|
34
|
-
/** Groups from external auth provider (e.g., Clerk organizations) */
|
|
35
|
-
externalGroups?: CanopyGroupId[]
|
|
36
|
-
}
|
|
37
|
-
error?: string
|
|
38
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { checkBranchAccessWithDefault, canPerformWorkflowAction, RESERVED_GROUPS } from '../'
|
|
4
|
-
import type { BranchContext } from '../../types'
|
|
5
|
-
|
|
6
|
-
const baseContext: BranchContext = {
|
|
7
|
-
baseRoot: '/tmp/base',
|
|
8
|
-
branchRoot: '/tmp/base/feature-x',
|
|
9
|
-
branch: {
|
|
10
|
-
name: 'feature/x',
|
|
11
|
-
status: 'editing',
|
|
12
|
-
access: {},
|
|
13
|
-
createdBy: 'user-1',
|
|
14
|
-
createdAt: new Date().toISOString(),
|
|
15
|
-
updatedAt: new Date().toISOString(),
|
|
16
|
-
},
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('branch access', () => {
|
|
20
|
-
it('allows Admins', () => {
|
|
21
|
-
const res = checkBranchAccessWithDefault(baseContext, {
|
|
22
|
-
type: 'authenticated',
|
|
23
|
-
userId: 'u',
|
|
24
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
25
|
-
})
|
|
26
|
-
expect(res.allowed).toBe(true)
|
|
27
|
-
expect(res.reason).toBe('privileged')
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('allows Reviewers', () => {
|
|
31
|
-
const res = checkBranchAccessWithDefault(baseContext, {
|
|
32
|
-
type: 'authenticated',
|
|
33
|
-
userId: 'u',
|
|
34
|
-
groups: [RESERVED_GROUPS.REVIEWERS],
|
|
35
|
-
})
|
|
36
|
-
expect(res.allowed).toBe(true)
|
|
37
|
-
expect(res.reason).toBe('privileged')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('denies when no ACLs are set (default deny)', () => {
|
|
41
|
-
const res = checkBranchAccessWithDefault(baseContext, {
|
|
42
|
-
type: 'authenticated',
|
|
43
|
-
userId: 'u',
|
|
44
|
-
groups: [],
|
|
45
|
-
})
|
|
46
|
-
expect(res.allowed).toBe(false)
|
|
47
|
-
expect(res.reason).toBe('no_acl')
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('honors default allow override', () => {
|
|
51
|
-
const res = checkBranchAccessWithDefault(
|
|
52
|
-
baseContext,
|
|
53
|
-
{ type: 'authenticated', userId: 'u', groups: [] },
|
|
54
|
-
'allow',
|
|
55
|
-
)
|
|
56
|
-
expect(res.allowed).toBe(true)
|
|
57
|
-
expect(res.reason).toBe('no_acl')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('denies when managerOrAdminAllowed set but user is not privileged', () => {
|
|
61
|
-
const res = checkBranchAccessWithDefault(
|
|
62
|
-
{
|
|
63
|
-
...baseContext,
|
|
64
|
-
branch: {
|
|
65
|
-
...baseContext.branch,
|
|
66
|
-
access: { managerOrAdminAllowed: true },
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
{ type: 'authenticated', userId: 'u', groups: [] },
|
|
70
|
-
)
|
|
71
|
-
expect(res.allowed).toBe(false)
|
|
72
|
-
expect(res.reason).toBe('denied_by_acl')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('allows matching user', () => {
|
|
76
|
-
const res = checkBranchAccessWithDefault(
|
|
77
|
-
{
|
|
78
|
-
...baseContext,
|
|
79
|
-
branch: { ...baseContext.branch, access: { allowedUsers: ['user-1'] } },
|
|
80
|
-
},
|
|
81
|
-
{ type: 'authenticated', userId: 'user-1', groups: [] },
|
|
82
|
-
)
|
|
83
|
-
expect(res.allowed).toBe(true)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('allows matching group', () => {
|
|
87
|
-
const res = checkBranchAccessWithDefault(
|
|
88
|
-
{
|
|
89
|
-
...baseContext,
|
|
90
|
-
branch: {
|
|
91
|
-
...baseContext.branch,
|
|
92
|
-
access: { allowedGroups: ['group-1'] },
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
{ type: 'authenticated', userId: 'u', groups: ['group-1'] },
|
|
96
|
-
)
|
|
97
|
-
expect(res.allowed).toBe(true)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('denies when allowlists miss', () => {
|
|
101
|
-
const res = checkBranchAccessWithDefault(
|
|
102
|
-
{
|
|
103
|
-
...baseContext,
|
|
104
|
-
branch: { ...baseContext.branch, access: { allowedUsers: ['user-2'] } },
|
|
105
|
-
},
|
|
106
|
-
{ type: 'authenticated', userId: 'user-1', groups: [] },
|
|
107
|
-
)
|
|
108
|
-
expect(res.allowed).toBe(false)
|
|
109
|
-
expect(res.reason).toBe('denied_by_acl')
|
|
110
|
-
})
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
describe('canPerformWorkflowAction', () => {
|
|
114
|
-
const regularUser = {
|
|
115
|
-
type: 'authenticated' as const,
|
|
116
|
-
userId: 'user-1',
|
|
117
|
-
groups: [],
|
|
118
|
-
}
|
|
119
|
-
const admin = {
|
|
120
|
-
type: 'authenticated' as const,
|
|
121
|
-
userId: 'admin-1',
|
|
122
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
123
|
-
}
|
|
124
|
-
const reviewer = {
|
|
125
|
-
type: 'authenticated' as const,
|
|
126
|
-
userId: 'reviewer-1',
|
|
127
|
-
groups: [RESERVED_GROUPS.REVIEWERS],
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
describe('branch creator permissions', () => {
|
|
131
|
-
it('allows branch creator to perform workflow actions', () => {
|
|
132
|
-
const context: BranchContext = {
|
|
133
|
-
...baseContext,
|
|
134
|
-
branch: { ...baseContext.branch, createdBy: 'user-1', access: {} },
|
|
135
|
-
}
|
|
136
|
-
expect(canPerformWorkflowAction(context, regularUser, 'allow')).toBe(true)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('denies non-creator without ACL access', () => {
|
|
140
|
-
const context: BranchContext = {
|
|
141
|
-
...baseContext,
|
|
142
|
-
branch: { ...baseContext.branch, createdBy: 'user-2', access: {} },
|
|
143
|
-
}
|
|
144
|
-
expect(canPerformWorkflowAction(context, regularUser, 'deny')).toBe(false)
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
describe('ACL-based permissions', () => {
|
|
149
|
-
it('allows user in allowedUsers ACL', () => {
|
|
150
|
-
const context: BranchContext = {
|
|
151
|
-
...baseContext,
|
|
152
|
-
branch: {
|
|
153
|
-
...baseContext.branch,
|
|
154
|
-
createdBy: 'user-2',
|
|
155
|
-
access: { allowedUsers: ['user-1'] },
|
|
156
|
-
},
|
|
157
|
-
}
|
|
158
|
-
expect(canPerformWorkflowAction(context, regularUser, 'deny')).toBe(true)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('allows user in allowedGroups ACL', () => {
|
|
162
|
-
const userInGroup = {
|
|
163
|
-
type: 'authenticated' as const,
|
|
164
|
-
userId: 'user-1',
|
|
165
|
-
groups: ['team-a'],
|
|
166
|
-
}
|
|
167
|
-
const context: BranchContext = {
|
|
168
|
-
...baseContext,
|
|
169
|
-
branch: {
|
|
170
|
-
...baseContext.branch,
|
|
171
|
-
createdBy: 'user-2',
|
|
172
|
-
access: { allowedGroups: ['team-a'] },
|
|
173
|
-
},
|
|
174
|
-
}
|
|
175
|
-
expect(canPerformWorkflowAction(context, userInGroup, 'deny')).toBe(true)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('denies user not in ACL', () => {
|
|
179
|
-
const context: BranchContext = {
|
|
180
|
-
...baseContext,
|
|
181
|
-
branch: {
|
|
182
|
-
...baseContext.branch,
|
|
183
|
-
createdBy: 'user-2',
|
|
184
|
-
access: { allowedUsers: ['user-3'] },
|
|
185
|
-
},
|
|
186
|
-
}
|
|
187
|
-
expect(canPerformWorkflowAction(context, regularUser, 'deny')).toBe(false)
|
|
188
|
-
})
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
describe('system branch permissions', () => {
|
|
192
|
-
it('allows any user with general access on system branches', () => {
|
|
193
|
-
const context: BranchContext = {
|
|
194
|
-
...baseContext,
|
|
195
|
-
branch: {
|
|
196
|
-
...baseContext.branch,
|
|
197
|
-
createdBy: 'canopycms-system',
|
|
198
|
-
access: {},
|
|
199
|
-
},
|
|
200
|
-
}
|
|
201
|
-
expect(canPerformWorkflowAction(context, regularUser, 'allow')).toBe(true)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('denies user without general access on system branches', () => {
|
|
205
|
-
const context: BranchContext = {
|
|
206
|
-
...baseContext,
|
|
207
|
-
branch: {
|
|
208
|
-
...baseContext.branch,
|
|
209
|
-
createdBy: 'canopycms-system',
|
|
210
|
-
access: { allowedUsers: ['user-2'] },
|
|
211
|
-
},
|
|
212
|
-
}
|
|
213
|
-
expect(canPerformWorkflowAction(context, regularUser, 'deny')).toBe(false)
|
|
214
|
-
})
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
describe('privileged user permissions', () => {
|
|
218
|
-
it('allows admins to perform workflow actions', () => {
|
|
219
|
-
const context: BranchContext = {
|
|
220
|
-
...baseContext,
|
|
221
|
-
branch: { ...baseContext.branch, createdBy: 'user-2', access: {} },
|
|
222
|
-
}
|
|
223
|
-
expect(canPerformWorkflowAction(context, admin, 'deny')).toBe(true)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
it('allows reviewers to perform workflow actions', () => {
|
|
227
|
-
const context: BranchContext = {
|
|
228
|
-
...baseContext,
|
|
229
|
-
branch: { ...baseContext.branch, createdBy: 'user-2', access: {} },
|
|
230
|
-
}
|
|
231
|
-
expect(canPerformWorkflowAction(context, reviewer, 'deny')).toBe(true)
|
|
232
|
-
})
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
describe('combined scenarios', () => {
|
|
236
|
-
it('allows creator who is also in ACL', () => {
|
|
237
|
-
const context: BranchContext = {
|
|
238
|
-
...baseContext,
|
|
239
|
-
branch: {
|
|
240
|
-
...baseContext.branch,
|
|
241
|
-
createdBy: 'user-1',
|
|
242
|
-
access: { allowedUsers: ['user-1', 'user-2'] },
|
|
243
|
-
},
|
|
244
|
-
}
|
|
245
|
-
expect(canPerformWorkflowAction(context, regularUser, 'deny')).toBe(true)
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('denies user who lacks both creator and ACL access', () => {
|
|
249
|
-
const context: BranchContext = {
|
|
250
|
-
...baseContext,
|
|
251
|
-
branch: {
|
|
252
|
-
...baseContext.branch,
|
|
253
|
-
createdBy: 'user-2',
|
|
254
|
-
access: { allowedUsers: ['user-3'] },
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
expect(canPerformWorkflowAction(context, regularUser, 'deny')).toBe(false)
|
|
258
|
-
})
|
|
259
|
-
})
|
|
260
|
-
})
|