canopycms 0.0.0 → 0.0.2
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/dist/auth/plugin.d.ts +8 -0
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/build-mode.d.ts +15 -5
- package/dist/build-mode.d.ts.map +1 -1
- package/dist/build-mode.js +18 -8
- package/dist/build-mode.js.map +1 -1
- package/dist/cli/init.d.ts +2 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +37 -36
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/template-files/ai-config.ts.template +21 -0
- package/dist/cli/template-files/ai-route.ts.template +10 -0
- package/dist/cli/template-files/canopy.ts.template +24 -0
- package/dist/cli/templates.d.ts +5 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +9 -2
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/schemas/config.d.ts +4 -0
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content-reader.js +2 -2
- package/dist/content-reader.js.map +1 -1
- package/dist/context.js +5 -5
- package/dist/context.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +15 -18
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/init.ts +43 -38
- 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/canopy.ts.template +0 -55
- 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
- /package/{src/cli/templates → dist/cli/template-files}/Dockerfile.cms.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/canopycms.config.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/deploy-cms.yml.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/edit-page.tsx.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/route.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/schemas.ts.template +0 -0
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { createCheckBranchAccess, createCheckContentAccess, RESERVED_GROUPS } from '../'
|
|
4
|
-
import { unsafeAsPermissionPath } from '../test-utils'
|
|
5
|
-
import type { PathPermission } from '../../config'
|
|
6
|
-
import { unsafeAsPhysicalPath } from '../../paths/test-utils'
|
|
7
|
-
|
|
8
|
-
const branchContext = {
|
|
9
|
-
baseRoot: '/tmp/base',
|
|
10
|
-
branchRoot: '/tmp/base/feature-x',
|
|
11
|
-
branch: {
|
|
12
|
-
name: 'feature/x',
|
|
13
|
-
status: 'editing' as const,
|
|
14
|
-
access: {},
|
|
15
|
-
createdBy: 'u1',
|
|
16
|
-
createdAt: new Date().toISOString(),
|
|
17
|
-
updatedAt: new Date().toISOString(),
|
|
18
|
-
},
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Path permission rules (from .canopycms/permissions.json)
|
|
22
|
-
// Rule with explicit constraints - only Admins group can edit admin paths
|
|
23
|
-
const pathRules: PathPermission[] = [
|
|
24
|
-
{
|
|
25
|
-
path: unsafeAsPermissionPath('content/admin/**'),
|
|
26
|
-
edit: { allowedGroups: ['Admins'] },
|
|
27
|
-
},
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
describe('checkContentAccess', () => {
|
|
31
|
-
it('denies when branch ACL defaults to deny and no allowlist', async () => {
|
|
32
|
-
const mockLoadPermissions = vi.fn().mockResolvedValue(pathRules)
|
|
33
|
-
const checkContent = createCheckContentAccess({
|
|
34
|
-
checkBranchAccess: createCheckBranchAccess('deny'),
|
|
35
|
-
loadPathPermissions: mockLoadPermissions,
|
|
36
|
-
defaultPathAccess: 'allow',
|
|
37
|
-
mode: 'dev',
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const res = await checkContent(
|
|
41
|
-
branchContext,
|
|
42
|
-
'/repo',
|
|
43
|
-
unsafeAsPhysicalPath('content/pages/foo.md'),
|
|
44
|
-
{ type: 'authenticated', userId: 'u1', groups: [] },
|
|
45
|
-
'edit',
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
expect(mockLoadPermissions).toHaveBeenCalledWith('/repo', 'dev')
|
|
49
|
-
expect(res.allowed).toBe(false)
|
|
50
|
-
expect(res.branch.reason).toBe('no_acl')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('allows Reviewer override even if branch default deny', async () => {
|
|
54
|
-
const mockLoadPermissions = vi.fn().mockResolvedValue(pathRules)
|
|
55
|
-
const checkContent = createCheckContentAccess({
|
|
56
|
-
checkBranchAccess: createCheckBranchAccess('deny'),
|
|
57
|
-
loadPathPermissions: mockLoadPermissions,
|
|
58
|
-
defaultPathAccess: 'allow',
|
|
59
|
-
mode: 'dev',
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const res = await checkContent(
|
|
63
|
-
branchContext,
|
|
64
|
-
'/repo',
|
|
65
|
-
unsafeAsPhysicalPath('content/pages/foo.md'),
|
|
66
|
-
{
|
|
67
|
-
type: 'authenticated',
|
|
68
|
-
userId: 'u1',
|
|
69
|
-
groups: [RESERVED_GROUPS.REVIEWERS],
|
|
70
|
-
},
|
|
71
|
-
'edit',
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
expect(res.allowed).toBe(true)
|
|
75
|
-
expect(res.branch.reason).toBe('privileged')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('denies path access for regular users hitting admin paths', async () => {
|
|
79
|
-
const mockLoadPermissions = vi.fn().mockResolvedValue(pathRules)
|
|
80
|
-
const checkContent = createCheckContentAccess({
|
|
81
|
-
checkBranchAccess: createCheckBranchAccess('allow'),
|
|
82
|
-
loadPathPermissions: mockLoadPermissions,
|
|
83
|
-
defaultPathAccess: 'allow',
|
|
84
|
-
mode: 'dev',
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const res = await checkContent(
|
|
88
|
-
branchContext,
|
|
89
|
-
'/repo',
|
|
90
|
-
unsafeAsPhysicalPath('content/admin/secret.md'),
|
|
91
|
-
{ type: 'authenticated', userId: 'u1', groups: [] },
|
|
92
|
-
'edit',
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
expect(res.allowed).toBe(false)
|
|
96
|
-
expect(res.path.allowed).toBe(false)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('respects defaultPathAccess when no rule matches', async () => {
|
|
100
|
-
const mockLoadPermissions = vi.fn().mockResolvedValue([])
|
|
101
|
-
const checkContent = createCheckContentAccess({
|
|
102
|
-
checkBranchAccess: createCheckBranchAccess('allow'),
|
|
103
|
-
loadPathPermissions: mockLoadPermissions,
|
|
104
|
-
defaultPathAccess: 'deny',
|
|
105
|
-
mode: 'dev',
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
const res = await checkContent(
|
|
109
|
-
branchContext,
|
|
110
|
-
'/repo',
|
|
111
|
-
unsafeAsPhysicalPath('content/open/page.md'),
|
|
112
|
-
{ type: 'authenticated', userId: 'u1', groups: [] },
|
|
113
|
-
'edit',
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
expect(res.allowed).toBe(false)
|
|
117
|
-
expect(res.path.allowed).toBe(false)
|
|
118
|
-
expect(res.path.reason).toBe('no_rule_match')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('allows access when defaultPathAccess is allow and no rules match', async () => {
|
|
122
|
-
const mockLoadPermissions = vi.fn().mockResolvedValue([])
|
|
123
|
-
const checkContent = createCheckContentAccess({
|
|
124
|
-
checkBranchAccess: createCheckBranchAccess('allow'),
|
|
125
|
-
loadPathPermissions: mockLoadPermissions,
|
|
126
|
-
defaultPathAccess: 'allow',
|
|
127
|
-
mode: 'dev',
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const res = await checkContent(
|
|
131
|
-
branchContext,
|
|
132
|
-
'/repo',
|
|
133
|
-
unsafeAsPhysicalPath('content/open/page.md'),
|
|
134
|
-
{ type: 'authenticated', userId: 'u1', groups: [] },
|
|
135
|
-
'edit',
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
expect(res.allowed).toBe(true)
|
|
139
|
-
expect(res.path.allowed).toBe(true)
|
|
140
|
-
expect(res.path.reason).toBe('no_rule_match')
|
|
141
|
-
})
|
|
142
|
-
})
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { checkPathAccess, RESERVED_GROUPS } from '../'
|
|
4
|
-
import { unsafeAsPermissionPath } from '../test-utils'
|
|
5
|
-
import type { PathPermission } from '../../config'
|
|
6
|
-
import type { CanopyUser } from '../../user'
|
|
7
|
-
import { unsafeAsPhysicalPath } from '../../paths/test-utils'
|
|
8
|
-
|
|
9
|
-
// Path permission rules (previously from config.pathPermissions, now from .canopycms/permissions.json)
|
|
10
|
-
const rules: PathPermission[] = [
|
|
11
|
-
{ path: unsafeAsPermissionPath('content/admin/**'), edit: {} },
|
|
12
|
-
{
|
|
13
|
-
path: unsafeAsPermissionPath('content/partners/**'),
|
|
14
|
-
edit: { allowedGroups: ['partner-org'] },
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
path: unsafeAsPermissionPath('content/restricted/**'),
|
|
18
|
-
edit: { allowedUsers: ['user-a'] },
|
|
19
|
-
},
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
// Helper to create authenticated users
|
|
23
|
-
const createUser = (userId: string, groups: string[] = []): CanopyUser => ({
|
|
24
|
-
type: 'authenticated',
|
|
25
|
-
userId,
|
|
26
|
-
groups,
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
describe('path permissions', () => {
|
|
30
|
-
it('allows admin', () => {
|
|
31
|
-
const result = checkPathAccess({
|
|
32
|
-
rules,
|
|
33
|
-
relativePath: unsafeAsPhysicalPath('content/admin/secret.md'),
|
|
34
|
-
user: createUser('any', [RESERVED_GROUPS.ADMINS]),
|
|
35
|
-
defaultAccess: 'deny',
|
|
36
|
-
level: 'edit',
|
|
37
|
-
})
|
|
38
|
-
expect(result.allowed).toBe(true)
|
|
39
|
-
expect(result.reason).toBe('admin')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('allows user when edit rule has no constraints', () => {
|
|
43
|
-
const result = checkPathAccess({
|
|
44
|
-
rules,
|
|
45
|
-
relativePath: unsafeAsPhysicalPath('content/admin/secret.md'),
|
|
46
|
-
user: createUser('any', []),
|
|
47
|
-
defaultAccess: 'deny',
|
|
48
|
-
level: 'edit',
|
|
49
|
-
})
|
|
50
|
-
expect(result.allowed).toBe(true)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('denies when user does not match allowedUsers or allowedGroups', () => {
|
|
54
|
-
const result = checkPathAccess({
|
|
55
|
-
rules,
|
|
56
|
-
relativePath: unsafeAsPhysicalPath('content/restricted/secret.md'),
|
|
57
|
-
user: createUser('user-b', ['partner-org']),
|
|
58
|
-
defaultAccess: 'deny',
|
|
59
|
-
level: 'edit',
|
|
60
|
-
})
|
|
61
|
-
expect(result.allowed).toBe(false)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('allows group membership', () => {
|
|
65
|
-
const result = checkPathAccess({
|
|
66
|
-
rules,
|
|
67
|
-
relativePath: unsafeAsPhysicalPath('content/partners/page.md'),
|
|
68
|
-
user: createUser('user-x', ['partner-org']),
|
|
69
|
-
defaultAccess: 'deny',
|
|
70
|
-
level: 'edit',
|
|
71
|
-
})
|
|
72
|
-
expect(result.allowed).toBe(true)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('denies missing group membership', () => {
|
|
76
|
-
const result = checkPathAccess({
|
|
77
|
-
rules,
|
|
78
|
-
relativePath: unsafeAsPhysicalPath('content/partners/page.md'),
|
|
79
|
-
user: createUser('user-x', ['other-org']),
|
|
80
|
-
defaultAccess: 'deny',
|
|
81
|
-
level: 'edit',
|
|
82
|
-
})
|
|
83
|
-
expect(result.allowed).toBe(false)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('defaults to allow when no rule matches', () => {
|
|
87
|
-
const result = checkPathAccess({
|
|
88
|
-
rules,
|
|
89
|
-
relativePath: unsafeAsPhysicalPath('content/open/page.md'),
|
|
90
|
-
user: createUser('user-x'),
|
|
91
|
-
defaultAccess: 'allow',
|
|
92
|
-
level: 'edit',
|
|
93
|
-
})
|
|
94
|
-
expect(result.allowed).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('uses defaultAccess=allow when no rule matches and explicitly set', () => {
|
|
98
|
-
const result = checkPathAccess({
|
|
99
|
-
rules,
|
|
100
|
-
relativePath: unsafeAsPhysicalPath('content/open/page.md'),
|
|
101
|
-
user: createUser('user-x'),
|
|
102
|
-
defaultAccess: 'allow',
|
|
103
|
-
level: 'edit',
|
|
104
|
-
})
|
|
105
|
-
expect(result.allowed).toBe(true)
|
|
106
|
-
expect(result.reason).toBe('no_rule_match')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('uses defaultAccess=deny when no rule matches', () => {
|
|
110
|
-
const result = checkPathAccess({
|
|
111
|
-
rules,
|
|
112
|
-
relativePath: unsafeAsPhysicalPath('content/open/page.md'),
|
|
113
|
-
user: createUser('user-x'),
|
|
114
|
-
defaultAccess: 'deny',
|
|
115
|
-
level: 'edit',
|
|
116
|
-
})
|
|
117
|
-
expect(result.allowed).toBe(false)
|
|
118
|
-
expect(result.reason).toBe('no_rule_match')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('applies matching rule regardless of defaultAccess', () => {
|
|
122
|
-
// Even with defaultAccess=allow, a matching rule that denies should deny
|
|
123
|
-
const result = checkPathAccess({
|
|
124
|
-
rules,
|
|
125
|
-
relativePath: unsafeAsPhysicalPath('content/restricted/secret.md'),
|
|
126
|
-
user: createUser('regular-user', []),
|
|
127
|
-
defaultAccess: 'allow',
|
|
128
|
-
level: 'edit',
|
|
129
|
-
})
|
|
130
|
-
expect(result.allowed).toBe(false)
|
|
131
|
-
expect(result.reason).toBe('denied_by_rule')
|
|
132
|
-
})
|
|
133
|
-
})
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import { loadPathPermissions, savePathPermissions, ensurePermissionsFile } from '../permissions'
|
|
6
|
-
import { unsafeAsPermissionPath } from '../test-utils'
|
|
7
|
-
import { mockConsole } from '../../test-utils/console-spy.js'
|
|
8
|
-
|
|
9
|
-
describe('permissions loader', () => {
|
|
10
|
-
let testRoot: string
|
|
11
|
-
|
|
12
|
-
beforeEach(async () => {
|
|
13
|
-
testRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'canopy-perms-test-'))
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
afterEach(async () => {
|
|
17
|
-
await fs.rm(testRoot, { recursive: true, force: true })
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
describe('loadPathPermissions', () => {
|
|
21
|
-
it('loads from file when it exists', async () => {
|
|
22
|
-
// Create permissions file
|
|
23
|
-
const canopyDir = testRoot
|
|
24
|
-
await fs.mkdir(canopyDir, { recursive: true })
|
|
25
|
-
await fs.writeFile(
|
|
26
|
-
path.join(canopyDir, 'permissions.json'),
|
|
27
|
-
JSON.stringify({
|
|
28
|
-
version: 1,
|
|
29
|
-
updatedAt: new Date().toISOString(),
|
|
30
|
-
updatedBy: 'admin-user',
|
|
31
|
-
pathPermissions: [
|
|
32
|
-
{
|
|
33
|
-
path: 'content/admin/**',
|
|
34
|
-
edit: {},
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
path: 'content/partners/**',
|
|
38
|
-
edit: { allowedGroups: ['partner-org'] },
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
}),
|
|
42
|
-
'utf-8',
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
const permissions = await loadPathPermissions(testRoot, 'prod')
|
|
46
|
-
|
|
47
|
-
expect(permissions).toHaveLength(2)
|
|
48
|
-
expect(permissions[0]).toEqual({
|
|
49
|
-
path: 'content/admin/**',
|
|
50
|
-
edit: {},
|
|
51
|
-
})
|
|
52
|
-
expect(permissions[1]).toEqual({
|
|
53
|
-
path: 'content/partners/**',
|
|
54
|
-
edit: { allowedGroups: ['partner-org'] },
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('returns empty array when file does not exist', async () => {
|
|
59
|
-
const permissions = await loadPathPermissions(testRoot, 'prod')
|
|
60
|
-
expect(permissions).toEqual([])
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('throws error on invalid JSON', async () => {
|
|
64
|
-
const consoleSpy = mockConsole()
|
|
65
|
-
|
|
66
|
-
// Create invalid permissions file in new location
|
|
67
|
-
const canopyDir = testRoot
|
|
68
|
-
await fs.mkdir(canopyDir, { recursive: true })
|
|
69
|
-
await fs.writeFile(path.join(canopyDir, 'permissions.json'), 'invalid json', 'utf-8')
|
|
70
|
-
|
|
71
|
-
await expect(loadPathPermissions(testRoot, 'prod')).rejects.toThrow(
|
|
72
|
-
'Invalid permissions file',
|
|
73
|
-
)
|
|
74
|
-
expect(consoleSpy).toHaveErrored('Failed to parse permissions file')
|
|
75
|
-
consoleSpy.restore()
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('throws error on invalid schema', async () => {
|
|
79
|
-
const consoleSpy = mockConsole()
|
|
80
|
-
|
|
81
|
-
// Create file with wrong version in new location
|
|
82
|
-
const canopyDir = testRoot
|
|
83
|
-
await fs.mkdir(canopyDir, { recursive: true })
|
|
84
|
-
await fs.writeFile(
|
|
85
|
-
path.join(canopyDir, 'permissions.json'),
|
|
86
|
-
JSON.stringify({
|
|
87
|
-
version: 2, // Wrong version
|
|
88
|
-
updatedAt: new Date().toISOString(),
|
|
89
|
-
updatedBy: 'admin',
|
|
90
|
-
pathPermissions: [],
|
|
91
|
-
}),
|
|
92
|
-
'utf-8',
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
await expect(loadPathPermissions(testRoot, 'prod')).rejects.toThrow(
|
|
96
|
-
'Invalid permissions file',
|
|
97
|
-
)
|
|
98
|
-
expect(consoleSpy).toHaveErrored('Failed to parse permissions file')
|
|
99
|
-
consoleSpy.restore()
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('savePathPermissions', () => {
|
|
104
|
-
it('saves permissions to file', async () => {
|
|
105
|
-
const permissions = [
|
|
106
|
-
{
|
|
107
|
-
path: unsafeAsPermissionPath('content/admin/**'),
|
|
108
|
-
edit: {},
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
path: unsafeAsPermissionPath('content/users/**'),
|
|
112
|
-
edit: { allowedUsers: ['user-1', 'user-2'] },
|
|
113
|
-
},
|
|
114
|
-
]
|
|
115
|
-
|
|
116
|
-
await savePathPermissions(testRoot, permissions, 'admin-user', 'prod')
|
|
117
|
-
|
|
118
|
-
const filePath = path.join(testRoot, 'permissions.json')
|
|
119
|
-
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
120
|
-
const parsed = JSON.parse(fileContent)
|
|
121
|
-
|
|
122
|
-
expect(parsed.version).toBe(1)
|
|
123
|
-
expect(parsed.updatedBy).toBe('admin-user')
|
|
124
|
-
expect(parsed.updatedAt).toBeTruthy()
|
|
125
|
-
expect(parsed.pathPermissions).toHaveLength(2)
|
|
126
|
-
expect(parsed.pathPermissions[0]).toEqual({
|
|
127
|
-
path: 'content/admin/**',
|
|
128
|
-
edit: {},
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('validates permissions before saving', async () => {
|
|
133
|
-
const invalidPermissions: any = [
|
|
134
|
-
{
|
|
135
|
-
path: '', // Invalid empty path
|
|
136
|
-
edit: {},
|
|
137
|
-
},
|
|
138
|
-
]
|
|
139
|
-
|
|
140
|
-
await expect(
|
|
141
|
-
savePathPermissions(testRoot, invalidPermissions, 'admin', 'prod'),
|
|
142
|
-
).rejects.toThrow()
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('overwrites existing file', async () => {
|
|
146
|
-
const firstPermissions = [
|
|
147
|
-
{
|
|
148
|
-
path: unsafeAsPermissionPath('content/first/**'),
|
|
149
|
-
edit: { allowedUsers: ['user-1'] },
|
|
150
|
-
},
|
|
151
|
-
]
|
|
152
|
-
await savePathPermissions(testRoot, firstPermissions, 'admin-1', 'prod')
|
|
153
|
-
|
|
154
|
-
const secondPermissions = [
|
|
155
|
-
{
|
|
156
|
-
path: unsafeAsPermissionPath('content/second/**'),
|
|
157
|
-
edit: { allowedUsers: ['user-2'] },
|
|
158
|
-
},
|
|
159
|
-
]
|
|
160
|
-
await savePathPermissions(testRoot, secondPermissions, 'admin-2', 'prod')
|
|
161
|
-
|
|
162
|
-
const loaded = await loadPathPermissions(testRoot, 'prod')
|
|
163
|
-
|
|
164
|
-
expect(loaded).toHaveLength(1)
|
|
165
|
-
expect(loaded[0].path).toBe('content/second/**')
|
|
166
|
-
})
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
describe('ensurePermissionsFile', () => {
|
|
170
|
-
it('creates default file if it does not exist', async () => {
|
|
171
|
-
await ensurePermissionsFile(testRoot, 'admin-user', 'prod')
|
|
172
|
-
|
|
173
|
-
const filePath = path.join(testRoot, 'permissions.json')
|
|
174
|
-
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
175
|
-
const parsed = JSON.parse(fileContent)
|
|
176
|
-
|
|
177
|
-
expect(parsed.version).toBe(1)
|
|
178
|
-
expect(parsed.updatedBy).toBe('admin-user')
|
|
179
|
-
expect(parsed.pathPermissions).toEqual([])
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('does nothing if file already exists', async () => {
|
|
183
|
-
const existingPermissions = [
|
|
184
|
-
{
|
|
185
|
-
path: unsafeAsPermissionPath('content/**'),
|
|
186
|
-
edit: { allowedUsers: ['existing'] },
|
|
187
|
-
},
|
|
188
|
-
]
|
|
189
|
-
await savePathPermissions(testRoot, existingPermissions, 'original-admin', 'prod')
|
|
190
|
-
|
|
191
|
-
await ensurePermissionsFile(testRoot, 'new-admin', 'prod')
|
|
192
|
-
|
|
193
|
-
const loaded = await loadPathPermissions(testRoot, 'prod')
|
|
194
|
-
|
|
195
|
-
// Original permissions should still be there
|
|
196
|
-
expect(loaded).toHaveLength(1)
|
|
197
|
-
expect(loaded[0].path).toBe('content/**')
|
|
198
|
-
})
|
|
199
|
-
})
|
|
200
|
-
})
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Branch-level authorization
|
|
3
|
-
*
|
|
4
|
-
* Handles checking if a user can access a branch based on ACLs.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { BranchContext } from '../types'
|
|
8
|
-
import type { DefaultBranchAccess } from '../config'
|
|
9
|
-
import { isAdmin, isReviewer } from './helpers'
|
|
10
|
-
import type { CanopyUser } from '../user'
|
|
11
|
-
import type { BranchAccessResult } from './types'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Check if user has access to a branch with explicit default behavior.
|
|
15
|
-
*/
|
|
16
|
-
export function checkBranchAccessWithDefault(
|
|
17
|
-
context: BranchContext,
|
|
18
|
-
user: CanopyUser,
|
|
19
|
-
defaultAccess: DefaultBranchAccess = 'deny',
|
|
20
|
-
): BranchAccessResult {
|
|
21
|
-
// Admins and Reviewers have full branch access
|
|
22
|
-
if (isAdmin(user.groups) || isReviewer(user.groups)) {
|
|
23
|
-
return { allowed: true, reason: 'privileged' }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const access = context.branch.access
|
|
27
|
-
const hasUserConstraint = !!access.allowedUsers?.length
|
|
28
|
-
const hasGroupConstraint = !!access.allowedGroups?.length
|
|
29
|
-
const managerOrAdminAllowed = access.managerOrAdminAllowed ?? false
|
|
30
|
-
|
|
31
|
-
if (!hasUserConstraint && !hasGroupConstraint) {
|
|
32
|
-
if (managerOrAdminAllowed) {
|
|
33
|
-
return { allowed: false, reason: 'denied_by_acl' }
|
|
34
|
-
}
|
|
35
|
-
const allowed = defaultAccess === 'allow'
|
|
36
|
-
return { allowed, reason: 'no_acl' }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const userAllowed = hasUserConstraint && access.allowedUsers?.includes(user.userId)
|
|
40
|
-
const groupAllowed =
|
|
41
|
-
hasGroupConstraint && user.groups?.some((g) => access.allowedGroups?.includes(g))
|
|
42
|
-
|
|
43
|
-
const allowed = Boolean(userAllowed || groupAllowed)
|
|
44
|
-
return { allowed, reason: allowed ? 'allowed_by_acl' : 'denied_by_acl' }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Create a branch access checker with bound default access.
|
|
49
|
-
*/
|
|
50
|
-
export function createCheckBranchAccess(defaultAccess: DefaultBranchAccess = 'deny') {
|
|
51
|
-
return (context: BranchContext, user: CanopyUser): BranchAccessResult =>
|
|
52
|
-
checkBranchAccessWithDefault(context, user, defaultAccess)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Check if user can perform workflow actions (submit/withdraw) on a branch.
|
|
57
|
-
* Allowed if: user is creator OR user has ACL access OR (system branch AND user has general access).
|
|
58
|
-
*
|
|
59
|
-
* This implements a hybrid permission model:
|
|
60
|
-
* - Branch creators can always submit/withdraw their branches
|
|
61
|
-
* - Users explicitly listed in branch ACLs can also submit/withdraw
|
|
62
|
-
* - For system branches (createdBy: 'canopycms-system'), anyone with general access can submit/withdraw
|
|
63
|
-
* - Admins and Reviewers always have access (via checkBranchAccess)
|
|
64
|
-
*/
|
|
65
|
-
export function canPerformWorkflowAction(
|
|
66
|
-
context: BranchContext,
|
|
67
|
-
user: CanopyUser,
|
|
68
|
-
defaultAccess: DefaultBranchAccess = 'deny',
|
|
69
|
-
): boolean {
|
|
70
|
-
// Check if user has general branch access (handles admins, reviewers, ACLs)
|
|
71
|
-
const accessResult = checkBranchAccessWithDefault(context, user, defaultAccess)
|
|
72
|
-
|
|
73
|
-
// If user doesn't have basic branch access, deny immediately
|
|
74
|
-
if (!accessResult.allowed) {
|
|
75
|
-
return false
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Check if user is the branch creator
|
|
79
|
-
const userIsCreator = context.branch.createdBy === user.userId
|
|
80
|
-
|
|
81
|
-
// Check if this is a system-created branch
|
|
82
|
-
const isSystemBranch = context.branch.createdBy === 'canopycms-system'
|
|
83
|
-
|
|
84
|
-
// Allow if:
|
|
85
|
-
// 1. User is the creator, OR
|
|
86
|
-
// 2. User has ACL access (reason: 'privileged' or 'allowed_by_acl'), OR
|
|
87
|
-
// 3. System branch with general access
|
|
88
|
-
return (
|
|
89
|
-
userIsCreator ||
|
|
90
|
-
accessResult.reason === 'privileged' ||
|
|
91
|
-
accessResult.reason === 'allowed_by_acl' ||
|
|
92
|
-
(isSystemBranch && accessResult.allowed)
|
|
93
|
-
)
|
|
94
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content access authorization
|
|
3
|
-
*
|
|
4
|
-
* This is the main entry point for authorization checks. It combines
|
|
5
|
-
* branch-level and path-level access checks into a single API.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* ```ts
|
|
9
|
-
* import { checkContentAccess } from './authorization'
|
|
10
|
-
*
|
|
11
|
-
* const result = await checkContentAccess(deps, context, branchRoot, 'content/posts/my-post.mdx', user, 'edit')
|
|
12
|
-
* if (result.allowed) {
|
|
13
|
-
* // User can edit the file
|
|
14
|
-
* }
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { BranchContext } from '../types'
|
|
19
|
-
import type { PermissionLevel } from '../config'
|
|
20
|
-
import type { CanopyUser } from '../user'
|
|
21
|
-
import { operatingStrategy } from '../operating-mode'
|
|
22
|
-
import { createCheckPathAccess } from './path'
|
|
23
|
-
import type { ContentAccessResult, ContentAccessDeps } from './types'
|
|
24
|
-
import type { PhysicalPath } from '../paths/types'
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Check content access by evaluating both branch and path permissions.
|
|
28
|
-
* Path permissions are loaded dynamically from the branch root.
|
|
29
|
-
*
|
|
30
|
-
* @param deps - Dependencies including branch access checker and path permissions loader
|
|
31
|
-
* @param context - Branch context containing branch metadata
|
|
32
|
-
* @param branchRoot - Root directory of the branch
|
|
33
|
-
* @param relativePath - Physical path relative to branch root (with embedded IDs)
|
|
34
|
-
* @param user - User to check access for
|
|
35
|
-
* @param level - Permission level to check ('read', 'edit', or 'review')
|
|
36
|
-
*/
|
|
37
|
-
export async function checkContentAccess(
|
|
38
|
-
deps: ContentAccessDeps,
|
|
39
|
-
context: BranchContext,
|
|
40
|
-
branchRoot: string,
|
|
41
|
-
relativePath: PhysicalPath,
|
|
42
|
-
user: CanopyUser,
|
|
43
|
-
level: PermissionLevel,
|
|
44
|
-
): Promise<ContentAccessResult> {
|
|
45
|
-
const branch = deps.checkBranchAccess(context, user)
|
|
46
|
-
|
|
47
|
-
// Load permissions from appropriate location based on operating mode
|
|
48
|
-
// Modes with separate settings branch: load from settings branch
|
|
49
|
-
// Other modes: load from the current branch
|
|
50
|
-
let permissionsRoot = branchRoot
|
|
51
|
-
const mode = deps.mode
|
|
52
|
-
const strategy = operatingStrategy(mode)
|
|
53
|
-
|
|
54
|
-
if (strategy.usesSeparateSettingsBranch()) {
|
|
55
|
-
if (!deps.getSettingsBranchRoot) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
'getSettingsBranchRoot is required for modes that use separate settings branch',
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
// getSettingsBranchRoot must throw if it cannot load the settings branch
|
|
61
|
-
// This ensures we never fall back to reading permissions from the current branch
|
|
62
|
-
permissionsRoot = await deps.getSettingsBranchRoot()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const rules = await deps.loadPathPermissions(permissionsRoot, deps.mode)
|
|
66
|
-
const pathChecker = createCheckPathAccess(rules, deps.defaultPathAccess)
|
|
67
|
-
|
|
68
|
-
const path = pathChecker({
|
|
69
|
-
relativePath,
|
|
70
|
-
user,
|
|
71
|
-
level,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
allowed: branch.allowed && path.allowed,
|
|
76
|
-
branch,
|
|
77
|
-
path,
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Create a content access checker with bound dependencies.
|
|
83
|
-
*/
|
|
84
|
-
export function createCheckContentAccess(deps: ContentAccessDeps) {
|
|
85
|
-
return (
|
|
86
|
-
context: BranchContext,
|
|
87
|
-
branchRoot: string,
|
|
88
|
-
relativePath: PhysicalPath,
|
|
89
|
-
user: CanopyUser,
|
|
90
|
-
level: PermissionLevel,
|
|
91
|
-
): Promise<ContentAccessResult> =>
|
|
92
|
-
checkContentAccess(deps, context, branchRoot, relativePath, user, level)
|
|
93
|
-
}
|