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,154 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { CachingAuthPlugin } from './caching-auth-plugin'
|
|
3
|
-
import type { AuthCacheProvider, TokenVerifier } from './caching-auth-plugin'
|
|
4
|
-
import type { UserSearchResult, GroupMetadata } from './types'
|
|
5
|
-
|
|
6
|
-
function createMockCache(overrides: Partial<AuthCacheProvider> = {}): AuthCacheProvider {
|
|
7
|
-
const users: UserSearchResult[] = [
|
|
8
|
-
{
|
|
9
|
-
id: 'user_1',
|
|
10
|
-
name: 'Alice Smith',
|
|
11
|
-
email: 'alice@example.com',
|
|
12
|
-
avatarUrl: 'https://avatar/alice',
|
|
13
|
-
},
|
|
14
|
-
{ id: 'user_2', name: 'Bob Jones', email: 'bob@example.com' },
|
|
15
|
-
]
|
|
16
|
-
const groups: GroupMetadata[] = [
|
|
17
|
-
{ id: 'org_1', name: 'Engineering', memberCount: 5 },
|
|
18
|
-
{ id: 'org_2', name: 'Design', memberCount: 3 },
|
|
19
|
-
]
|
|
20
|
-
const memberships: Record<string, string[]> = {
|
|
21
|
-
user_1: ['org_1', 'org_2'],
|
|
22
|
-
user_2: ['org_1'],
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
getUser: vi.fn(async (userId) => users.find((u) => u.id === userId) ?? null),
|
|
27
|
-
getGroup: vi.fn(async (groupId) => groups.find((g) => g.id === groupId) ?? null),
|
|
28
|
-
getAllUsers: vi.fn(async () => users),
|
|
29
|
-
getAllGroups: vi.fn(async () => groups),
|
|
30
|
-
getUserExternalGroups: vi.fn(async (userId) => memberships[userId] ?? []),
|
|
31
|
-
...overrides,
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('CachingAuthPlugin', () => {
|
|
36
|
-
let mockVerifier: TokenVerifier
|
|
37
|
-
let mockCache: AuthCacheProvider
|
|
38
|
-
let plugin: CachingAuthPlugin
|
|
39
|
-
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
mockVerifier = vi.fn(async () => ({ userId: 'user_1' }))
|
|
42
|
-
mockCache = createMockCache()
|
|
43
|
-
plugin = new CachingAuthPlugin(mockVerifier, mockCache)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('authenticate', () => {
|
|
47
|
-
it('returns user with cached metadata on successful token verification', async () => {
|
|
48
|
-
const result = await plugin.authenticate({})
|
|
49
|
-
|
|
50
|
-
expect(result.success).toBe(true)
|
|
51
|
-
expect(result.user).toEqual({
|
|
52
|
-
userId: 'user_1',
|
|
53
|
-
name: 'Alice Smith',
|
|
54
|
-
email: 'alice@example.com',
|
|
55
|
-
avatarUrl: 'https://avatar/alice',
|
|
56
|
-
externalGroups: ['org_1', 'org_2'],
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('returns failure when token verification fails', async () => {
|
|
61
|
-
mockVerifier = vi.fn(async () => null)
|
|
62
|
-
plugin = new CachingAuthPlugin(mockVerifier, mockCache)
|
|
63
|
-
|
|
64
|
-
const result = await plugin.authenticate({})
|
|
65
|
-
|
|
66
|
-
expect(result.success).toBe(false)
|
|
67
|
-
expect(result.error).toBe('No valid authentication token')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('returns userId as name when user not in cache', async () => {
|
|
71
|
-
mockVerifier = vi.fn(async () => ({ userId: 'unknown_user' }))
|
|
72
|
-
plugin = new CachingAuthPlugin(mockVerifier, mockCache)
|
|
73
|
-
|
|
74
|
-
const result = await plugin.authenticate({})
|
|
75
|
-
|
|
76
|
-
expect(result.success).toBe(true)
|
|
77
|
-
expect(result.user?.userId).toBe('unknown_user')
|
|
78
|
-
expect(result.user?.name).toBe('unknown_user')
|
|
79
|
-
expect(result.user?.externalGroups).toEqual([])
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('passes context to verifier', async () => {
|
|
83
|
-
const context = { headers: new Headers({ Authorization: 'Bearer test' }) }
|
|
84
|
-
await plugin.authenticate(context)
|
|
85
|
-
|
|
86
|
-
expect(mockVerifier).toHaveBeenCalledWith(context)
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
describe('searchUsers', () => {
|
|
91
|
-
it('filters users by name', async () => {
|
|
92
|
-
const results = await plugin.searchUsers('alice')
|
|
93
|
-
expect(results).toHaveLength(1)
|
|
94
|
-
expect(results[0].name).toBe('Alice Smith')
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('filters users by email', async () => {
|
|
98
|
-
const results = await plugin.searchUsers('bob@')
|
|
99
|
-
expect(results).toHaveLength(1)
|
|
100
|
-
expect(results[0].email).toBe('bob@example.com')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('is case insensitive', async () => {
|
|
104
|
-
const results = await plugin.searchUsers('ALICE')
|
|
105
|
-
expect(results).toHaveLength(1)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('respects limit', async () => {
|
|
109
|
-
const results = await plugin.searchUsers('', 1)
|
|
110
|
-
expect(results).toHaveLength(1)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('returns empty for no matches', async () => {
|
|
114
|
-
const results = await plugin.searchUsers('nonexistent')
|
|
115
|
-
expect(results).toHaveLength(0)
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe('getUserMetadata', () => {
|
|
120
|
-
it('returns user from cache', async () => {
|
|
121
|
-
const user = await plugin.getUserMetadata('user_1')
|
|
122
|
-
expect(user?.name).toBe('Alice Smith')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('returns null for unknown user', async () => {
|
|
126
|
-
const user = await plugin.getUserMetadata('unknown')
|
|
127
|
-
expect(user).toBeNull()
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
describe('getGroupMetadata', () => {
|
|
132
|
-
it('returns group from cache', async () => {
|
|
133
|
-
const group = await plugin.getGroupMetadata('org_1')
|
|
134
|
-
expect(group?.name).toBe('Engineering')
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('returns null for unknown group', async () => {
|
|
138
|
-
const group = await plugin.getGroupMetadata('unknown')
|
|
139
|
-
expect(group).toBeNull()
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
describe('listGroups', () => {
|
|
144
|
-
it('returns all groups', async () => {
|
|
145
|
-
const groups = await plugin.listGroups()
|
|
146
|
-
expect(groups).toHaveLength(2)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('respects limit', async () => {
|
|
150
|
-
const groups = await plugin.listGroups(1)
|
|
151
|
-
expect(groups).toHaveLength(1)
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
})
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import type { AuthPlugin } from './plugin'
|
|
2
|
-
import type { UserSearchResult, GroupMetadata, AuthenticationResult } from './types'
|
|
3
|
-
import type { CanopyUserId, CanopyGroupId } from '../types'
|
|
4
|
-
import { createDebugLogger } from '../utils/debug'
|
|
5
|
-
|
|
6
|
-
const log = createDebugLogger({ prefix: 'CachingAuthPlugin' })
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Generic cache provider interface for auth metadata.
|
|
10
|
-
* Any auth system can implement its own cache backend
|
|
11
|
-
* (file-based, Redis, in-memory, etc.)
|
|
12
|
-
*/
|
|
13
|
-
export interface AuthCacheProvider {
|
|
14
|
-
getUser(userId: CanopyUserId): Promise<UserSearchResult | null>
|
|
15
|
-
getGroup(groupId: CanopyGroupId): Promise<GroupMetadata | null>
|
|
16
|
-
getAllUsers(): Promise<UserSearchResult[]>
|
|
17
|
-
getAllGroups(): Promise<GroupMetadata[]>
|
|
18
|
-
getUserExternalGroups(userId: CanopyUserId): Promise<CanopyGroupId[]>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Token verifier function type.
|
|
23
|
-
* Given a request context, extracts and verifies the auth token,
|
|
24
|
-
* returning the user ID on success.
|
|
25
|
-
*/
|
|
26
|
-
export type TokenVerifier = (context: unknown) => Promise<{ userId: CanopyUserId } | null>
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Auth plugin that wraps a token verifier with cached metadata lookups.
|
|
30
|
-
*
|
|
31
|
-
* Used in environments where the auth provider API is not reachable
|
|
32
|
-
* (e.g., Lambda with no internet). JWT verification is done locally,
|
|
33
|
-
* and user/group metadata comes from a cache populated externally
|
|
34
|
-
* (e.g., by an EC2 worker).
|
|
35
|
-
*/
|
|
36
|
-
export class CachingAuthPlugin implements AuthPlugin {
|
|
37
|
-
constructor(
|
|
38
|
-
private readonly verifyToken: TokenVerifier,
|
|
39
|
-
private readonly cache: AuthCacheProvider,
|
|
40
|
-
) {}
|
|
41
|
-
|
|
42
|
-
async authenticate(context: unknown): Promise<AuthenticationResult> {
|
|
43
|
-
const identity = await this.verifyToken(context)
|
|
44
|
-
if (!identity) {
|
|
45
|
-
return { success: false, error: 'No valid authentication token' }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const user = await this.cache.getUser(identity.userId)
|
|
50
|
-
const externalGroups = await this.cache.getUserExternalGroups(identity.userId)
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
success: true,
|
|
54
|
-
user: {
|
|
55
|
-
userId: identity.userId,
|
|
56
|
-
name: user?.name ?? identity.userId,
|
|
57
|
-
email: user?.email,
|
|
58
|
-
avatarUrl: user?.avatarUrl,
|
|
59
|
-
externalGroups,
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
// Cache error — still return authenticated with minimal info
|
|
64
|
-
log.debug('auth', 'Cache lookup failed, returning minimal user', {
|
|
65
|
-
userId: identity.userId,
|
|
66
|
-
})
|
|
67
|
-
return {
|
|
68
|
-
success: true,
|
|
69
|
-
user: {
|
|
70
|
-
userId: identity.userId,
|
|
71
|
-
name: identity.userId,
|
|
72
|
-
externalGroups: [],
|
|
73
|
-
},
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async searchUsers(query: string, limit = 10): Promise<UserSearchResult[]> {
|
|
79
|
-
try {
|
|
80
|
-
const allUsers = await this.cache.getAllUsers()
|
|
81
|
-
const lowerQuery = query.toLowerCase()
|
|
82
|
-
return allUsers
|
|
83
|
-
.filter(
|
|
84
|
-
(u) =>
|
|
85
|
-
u.name.toLowerCase().includes(lowerQuery) || u.email.toLowerCase().includes(lowerQuery),
|
|
86
|
-
)
|
|
87
|
-
.slice(0, limit)
|
|
88
|
-
} catch {
|
|
89
|
-
return []
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async getUserMetadata(userId: CanopyUserId): Promise<UserSearchResult | null> {
|
|
94
|
-
return this.cache.getUser(userId)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async getGroupMetadata(groupId: CanopyGroupId): Promise<GroupMetadata | null> {
|
|
98
|
-
return this.cache.getGroup(groupId)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async listGroups(limit = 50): Promise<GroupMetadata[]> {
|
|
102
|
-
try {
|
|
103
|
-
const allGroups = await this.cache.getAllGroups()
|
|
104
|
-
return allGroups.slice(0, limit)
|
|
105
|
-
} catch {
|
|
106
|
-
return []
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type { CanopyRequest } from '../http/types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Headers-like interface for auth context.
|
|
5
|
-
* Framework-agnostic - matches Web Headers, Next.js Headers, and any similar interface.
|
|
6
|
-
*/
|
|
7
|
-
export interface HeadersLike {
|
|
8
|
-
get(name: string): string | null
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Type guard to check if context is a CanopyRequest.
|
|
13
|
-
* CanopyRequest has both 'header' method and 'method' property.
|
|
14
|
-
*/
|
|
15
|
-
export function isCanopyRequest(context: unknown): context is CanopyRequest {
|
|
16
|
-
return (
|
|
17
|
-
typeof context === 'object' &&
|
|
18
|
-
context !== null &&
|
|
19
|
-
'header' in context &&
|
|
20
|
-
'method' in context &&
|
|
21
|
-
typeof (context as Record<string, unknown>).header === 'function'
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Type guard to check if context is a headers-like object.
|
|
27
|
-
* Headers have a 'get' method for retrieving header values.
|
|
28
|
-
*/
|
|
29
|
-
export function isHeadersLike(context: unknown): context is HeadersLike {
|
|
30
|
-
return (
|
|
31
|
-
typeof context === 'object' &&
|
|
32
|
-
context !== null &&
|
|
33
|
-
'get' in context &&
|
|
34
|
-
typeof (context as Record<string, unknown>).get === 'function'
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Extract headers from various auth context types.
|
|
40
|
-
* Supports CanopyRequest (API routes) and any headers-like object (server components).
|
|
41
|
-
*
|
|
42
|
-
* @returns HeadersLike object or null if context type is unsupported
|
|
43
|
-
*/
|
|
44
|
-
export function extractHeaders(context: unknown): HeadersLike | null {
|
|
45
|
-
if (isCanopyRequest(context)) {
|
|
46
|
-
// Wrap CanopyRequest.header() as HeadersLike.get()
|
|
47
|
-
return {
|
|
48
|
-
get: (name: string) => context.header(name),
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (isHeadersLike(context)) {
|
|
53
|
-
return context
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return null
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Validate auth context and throw helpful error if unsupported.
|
|
61
|
-
* Use this in auth plugins to provide clear error messages.
|
|
62
|
-
*/
|
|
63
|
-
export function validateAuthContext(context: unknown): HeadersLike {
|
|
64
|
-
const headers = extractHeaders(context)
|
|
65
|
-
|
|
66
|
-
if (!headers) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
'Invalid auth context: expected CanopyRequest or Headers object. ' +
|
|
69
|
-
'Received: ' +
|
|
70
|
-
(context === null ? 'null' : typeof context),
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return headers
|
|
75
|
-
}
|
|
@@ -1,257 +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 { FileBasedAuthCache, writeAuthCacheSnapshot } from './file-based-auth-cache'
|
|
6
|
-
|
|
7
|
-
describe('FileBasedAuthCache', () => {
|
|
8
|
-
let tmpDir: string
|
|
9
|
-
let cache: FileBasedAuthCache
|
|
10
|
-
|
|
11
|
-
const testUsers = {
|
|
12
|
-
users: [
|
|
13
|
-
{
|
|
14
|
-
id: 'user_1',
|
|
15
|
-
name: 'Alice',
|
|
16
|
-
email: 'alice@test.com',
|
|
17
|
-
avatarUrl: 'https://avatar/alice',
|
|
18
|
-
},
|
|
19
|
-
{ id: 'user_2', name: 'Bob', email: 'bob@test.com' },
|
|
20
|
-
],
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const testGroups = {
|
|
24
|
-
groups: [
|
|
25
|
-
{ id: 'org_1', name: 'Engineering', memberCount: 5 },
|
|
26
|
-
{ id: 'org_2', name: 'Design', memberCount: 3 },
|
|
27
|
-
],
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const testMemberships = {
|
|
31
|
-
memberships: {
|
|
32
|
-
user_1: ['org_1', 'org_2'],
|
|
33
|
-
user_2: ['org_1'],
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
beforeEach(async () => {
|
|
38
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'canopy-cache-test-'))
|
|
39
|
-
cache = new FileBasedAuthCache(tmpDir)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
afterEach(async () => {
|
|
43
|
-
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
async function writeCache() {
|
|
47
|
-
await fs.writeFile(path.join(tmpDir, 'users.json'), JSON.stringify(testUsers))
|
|
48
|
-
await fs.writeFile(path.join(tmpDir, 'orgs.json'), JSON.stringify(testGroups))
|
|
49
|
-
await fs.writeFile(path.join(tmpDir, 'memberships.json'), JSON.stringify(testMemberships))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe('with populated cache', () => {
|
|
53
|
-
beforeEach(writeCache)
|
|
54
|
-
|
|
55
|
-
it('getUser returns user by ID', async () => {
|
|
56
|
-
const user = await cache.getUser('user_1')
|
|
57
|
-
expect(user).toEqual(testUsers.users[0])
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('getUser returns null for unknown ID', async () => {
|
|
61
|
-
const user = await cache.getUser('unknown')
|
|
62
|
-
expect(user).toBeNull()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('getGroup returns group by ID', async () => {
|
|
66
|
-
const group = await cache.getGroup('org_1')
|
|
67
|
-
expect(group).toEqual(testGroups.groups[0])
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('getGroup returns null for unknown ID', async () => {
|
|
71
|
-
const group = await cache.getGroup('unknown')
|
|
72
|
-
expect(group).toBeNull()
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('getAllUsers returns all users', async () => {
|
|
76
|
-
const users = await cache.getAllUsers()
|
|
77
|
-
expect(users).toHaveLength(2)
|
|
78
|
-
expect(users[0].name).toBe('Alice')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('getAllGroups returns all groups', async () => {
|
|
82
|
-
const groups = await cache.getAllGroups()
|
|
83
|
-
expect(groups).toHaveLength(2)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('getUserExternalGroups returns group IDs for user', async () => {
|
|
87
|
-
const groups = await cache.getUserExternalGroups('user_1')
|
|
88
|
-
expect(groups).toEqual(['org_1', 'org_2'])
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('getUserExternalGroups returns empty array for unknown user', async () => {
|
|
92
|
-
const groups = await cache.getUserExternalGroups('unknown')
|
|
93
|
-
expect(groups).toEqual([])
|
|
94
|
-
})
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
describe('with empty cache directory', () => {
|
|
98
|
-
it('returns empty results when no cache files exist', async () => {
|
|
99
|
-
const users = await cache.getAllUsers()
|
|
100
|
-
expect(users).toEqual([])
|
|
101
|
-
|
|
102
|
-
const groups = await cache.getAllGroups()
|
|
103
|
-
expect(groups).toEqual([])
|
|
104
|
-
|
|
105
|
-
const user = await cache.getUser('user_1')
|
|
106
|
-
expect(user).toBeNull()
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe('cache invalidation', () => {
|
|
111
|
-
it('re-reads cache when file mtime changes', async () => {
|
|
112
|
-
await writeCache()
|
|
113
|
-
|
|
114
|
-
// First read
|
|
115
|
-
const users1 = await cache.getAllUsers()
|
|
116
|
-
expect(users1).toHaveLength(2)
|
|
117
|
-
|
|
118
|
-
// Wait a moment so mtime changes
|
|
119
|
-
await new Promise((r) => setTimeout(r, 50))
|
|
120
|
-
|
|
121
|
-
// Update cache with different data
|
|
122
|
-
const updatedUsers = {
|
|
123
|
-
users: [{ id: 'user_3', name: 'Charlie', email: 'charlie@test.com' }],
|
|
124
|
-
}
|
|
125
|
-
await fs.writeFile(path.join(tmpDir, 'users.json'), JSON.stringify(updatedUsers))
|
|
126
|
-
|
|
127
|
-
// Should pick up new data
|
|
128
|
-
const users2 = await cache.getAllUsers()
|
|
129
|
-
expect(users2).toHaveLength(1)
|
|
130
|
-
expect(users2[0].name).toBe('Charlie')
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('uses cached data when mtime unchanged', async () => {
|
|
134
|
-
await writeCache()
|
|
135
|
-
|
|
136
|
-
// Read twice — should use in-memory cache on second read
|
|
137
|
-
const users1 = await cache.getAllUsers()
|
|
138
|
-
const users2 = await cache.getAllUsers()
|
|
139
|
-
expect(users1).toEqual(users2)
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
describe('partial cache files', () => {
|
|
144
|
-
it('handles missing orgs.json gracefully', async () => {
|
|
145
|
-
await fs.writeFile(path.join(tmpDir, 'users.json'), JSON.stringify(testUsers))
|
|
146
|
-
// No orgs.json or memberships.json
|
|
147
|
-
|
|
148
|
-
const users = await cache.getAllUsers()
|
|
149
|
-
expect(users).toHaveLength(2)
|
|
150
|
-
|
|
151
|
-
const groups = await cache.getAllGroups()
|
|
152
|
-
expect(groups).toEqual([])
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('handles malformed JSON gracefully', async () => {
|
|
156
|
-
await fs.writeFile(path.join(tmpDir, 'users.json'), 'not json')
|
|
157
|
-
|
|
158
|
-
const users = await cache.getAllUsers()
|
|
159
|
-
expect(users).toEqual([])
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
describe('snapshot layout (symlink)', () => {
|
|
164
|
-
it('reads from snapshot directory via current symlink', async () => {
|
|
165
|
-
await writeAuthCacheSnapshot(tmpDir, {
|
|
166
|
-
'users.json': testUsers,
|
|
167
|
-
'orgs.json': testGroups,
|
|
168
|
-
'memberships.json': testMemberships,
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
const snapshotCache = new FileBasedAuthCache(tmpDir)
|
|
172
|
-
const users = await snapshotCache.getAllUsers()
|
|
173
|
-
expect(users).toHaveLength(2)
|
|
174
|
-
expect(users[0].name).toBe('Alice')
|
|
175
|
-
|
|
176
|
-
const groups = await snapshotCache.getAllGroups()
|
|
177
|
-
expect(groups).toHaveLength(2)
|
|
178
|
-
|
|
179
|
-
const memberships = await snapshotCache.getUserExternalGroups('user_1')
|
|
180
|
-
expect(memberships).toEqual(['org_1', 'org_2'])
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('picks up new snapshot after symlink swap', async () => {
|
|
184
|
-
await writeAuthCacheSnapshot(tmpDir, {
|
|
185
|
-
'users.json': testUsers,
|
|
186
|
-
'orgs.json': testGroups,
|
|
187
|
-
'memberships.json': testMemberships,
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const snapshotCache = new FileBasedAuthCache(tmpDir)
|
|
191
|
-
const users1 = await snapshotCache.getAllUsers()
|
|
192
|
-
expect(users1).toHaveLength(2)
|
|
193
|
-
|
|
194
|
-
// Wait so mtime changes
|
|
195
|
-
await new Promise((r) => setTimeout(r, 50))
|
|
196
|
-
|
|
197
|
-
// Write new snapshot with different data
|
|
198
|
-
const updatedUsers = {
|
|
199
|
-
users: [{ id: 'user_3', name: 'Charlie', email: 'charlie@test.com' }],
|
|
200
|
-
}
|
|
201
|
-
await writeAuthCacheSnapshot(tmpDir, {
|
|
202
|
-
'users.json': updatedUsers,
|
|
203
|
-
'orgs.json': { groups: [] },
|
|
204
|
-
'memberships.json': { memberships: {} },
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
const users2 = await snapshotCache.getAllUsers()
|
|
208
|
-
expect(users2).toHaveLength(1)
|
|
209
|
-
expect(users2[0].name).toBe('Charlie')
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('falls back to flat layout when symlink target escapes cache directory', async () => {
|
|
213
|
-
// Create a symlink pointing outside the cache directory
|
|
214
|
-
const currentLink = path.join(tmpDir, 'current')
|
|
215
|
-
try {
|
|
216
|
-
await fs.unlink(currentLink)
|
|
217
|
-
} catch {
|
|
218
|
-
// May not exist
|
|
219
|
-
}
|
|
220
|
-
await fs.symlink('/tmp', currentLink)
|
|
221
|
-
|
|
222
|
-
// Write users.json to the flat layout (in tmpDir directly)
|
|
223
|
-
await fs.writeFile(path.join(tmpDir, 'users.json'), JSON.stringify(testUsers))
|
|
224
|
-
|
|
225
|
-
const escapedCache = new FileBasedAuthCache(tmpDir)
|
|
226
|
-
// Should fall back to flat layout instead of following the escape symlink
|
|
227
|
-
const users = await escapedCache.getAllUsers()
|
|
228
|
-
expect(users).toHaveLength(2)
|
|
229
|
-
expect(users[0].name).toBe('Alice')
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('cleans up old snapshots keeping only 2', async () => {
|
|
233
|
-
// Write 3 snapshots
|
|
234
|
-
await writeAuthCacheSnapshot(tmpDir, {
|
|
235
|
-
'users.json': { users: [] },
|
|
236
|
-
'orgs.json': { groups: [] },
|
|
237
|
-
'memberships.json': { memberships: {} },
|
|
238
|
-
})
|
|
239
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
240
|
-
await writeAuthCacheSnapshot(tmpDir, {
|
|
241
|
-
'users.json': { users: [] },
|
|
242
|
-
'orgs.json': { groups: [] },
|
|
243
|
-
'memberships.json': { memberships: {} },
|
|
244
|
-
})
|
|
245
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
246
|
-
await writeAuthCacheSnapshot(tmpDir, {
|
|
247
|
-
'users.json': { users: [] },
|
|
248
|
-
'orgs.json': { groups: [] },
|
|
249
|
-
'memberships.json': { memberships: {} },
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
const entries = await fs.readdir(tmpDir)
|
|
253
|
-
const snapshots = entries.filter((e) => e.startsWith('snapshot-'))
|
|
254
|
-
expect(snapshots).toHaveLength(2)
|
|
255
|
-
})
|
|
256
|
-
})
|
|
257
|
-
})
|