@aphexcms/cms-core 2.0.9 → 2.0.11
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/LICENSE +21 -0
- package/dist/api/api-keys.d.ts +8 -7
- package/dist/api/api-keys.d.ts.map +1 -1
- package/dist/api/assets.d.ts +7 -22
- package/dist/api/assets.d.ts.map +1 -1
- package/dist/api/assets.js +1 -10
- package/dist/api/documents.d.ts +17 -16
- package/dist/api/documents.d.ts.map +1 -1
- package/dist/api/documents.js +2 -2
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -0
- package/dist/api/organizations.d.ts +15 -35
- package/dist/api/organizations.d.ts.map +1 -1
- package/dist/api/roles.d.ts +22 -0
- package/dist/api/roles.d.ts.map +1 -0
- package/dist/api/roles.js +26 -0
- package/dist/api/schemas/api-keys.d.ts +62 -0
- package/dist/api/schemas/api-keys.d.ts.map +1 -0
- package/dist/api/schemas/api-keys.js +28 -0
- package/dist/api/schemas/assets.d.ts +174 -0
- package/dist/api/schemas/assets.d.ts.map +1 -0
- package/dist/api/schemas/assets.js +99 -0
- package/dist/api/schemas/documents.d.ts +325 -0
- package/dist/api/schemas/documents.d.ts.map +1 -0
- package/dist/api/schemas/documents.js +161 -0
- package/dist/api/schemas/organizations.d.ts +41 -0
- package/dist/api/schemas/organizations.d.ts.map +1 -0
- package/dist/api/schemas/organizations.js +56 -0
- package/dist/api/schemas/roles.d.ts +77 -0
- package/dist/api/schemas/roles.d.ts.map +1 -0
- package/dist/api/schemas/roles.js +42 -0
- package/dist/api/schemas/user.d.ts +10 -0
- package/dist/api/schemas/user.d.ts.map +1 -0
- package/dist/api/schemas/user.js +11 -0
- package/dist/api/user.d.ts +4 -6
- package/dist/api/user.d.ts.map +1 -1
- package/dist/app-augment.d.ts +32 -0
- package/dist/app-augment.d.ts.map +1 -0
- package/dist/app-augment.js +18 -0
- package/dist/auth/auth-hooks.d.ts +2 -1
- package/dist/auth/auth-hooks.d.ts.map +1 -1
- package/dist/auth/auth-hooks.js +22 -2
- package/dist/auth/provider.d.ts +5 -0
- package/dist/auth/provider.d.ts.map +1 -1
- package/dist/cli/index.js +0 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +7 -0
- package/dist/components/AdminApp.svelte +306 -142
- package/dist/components/AdminApp.svelte.d.ts +8 -0
- package/dist/components/AdminApp.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentEditor.svelte +653 -365
- package/dist/components/admin/DocumentEditor.svelte.d.ts +2 -0
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentVersionPanel.svelte +57 -44
- package/dist/components/admin/DocumentVersionPanel.svelte.d.ts.map +1 -1
- package/dist/components/admin/MediaBrowser.svelte +70 -30
- package/dist/components/admin/MediaBrowser.svelte.d.ts.map +1 -1
- package/dist/components/admin/ObjectModal.svelte +70 -95
- package/dist/components/admin/ObjectModal.svelte.d.ts +1 -2
- package/dist/components/admin/ObjectModal.svelte.d.ts.map +1 -1
- package/dist/components/admin/PermissionsDebug.svelte +119 -0
- package/dist/components/admin/PermissionsDebug.svelte.d.ts +4 -0
- package/dist/components/admin/PermissionsDebug.svelte.d.ts.map +1 -0
- package/dist/components/admin/SchemaField.svelte +3 -1
- package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -1
- package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte +35 -0
- package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte.d.ts +19 -0
- package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte.d.ts.map +1 -0
- package/dist/components/admin/confirm-dialog/confirm-dialog.svelte.d.ts +16 -0
- package/dist/components/admin/confirm-dialog/confirm-dialog.svelte.d.ts.map +1 -0
- package/dist/components/admin/confirm-dialog/confirm-dialog.svelte.js +29 -0
- package/dist/components/admin/fields/ArrayField.svelte +104 -108
- package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/FileField.svelte +22 -17
- package/dist/components/admin/fields/FileField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ImageField.svelte +21 -15
- package/dist/components/admin/fields/ImageField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ReferenceField.svelte +142 -112
- package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/TextareaField.svelte +4 -3
- package/dist/components/admin/fields/TextareaField.svelte.d.ts +2 -2
- package/dist/components/admin/fields/TextareaField.svelte.d.ts.map +1 -1
- package/dist/components/layout/Sidebar.svelte +21 -11
- package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -1
- package/dist/components/layout/sidebar/NavMain.svelte +1 -0
- package/dist/db/interfaces/document.d.ts.map +1 -1
- package/dist/db/interfaces/index.d.ts +3 -1
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/db/interfaces/organization.d.ts +1 -1
- package/dist/db/interfaces/organization.d.ts.map +1 -1
- package/dist/db/interfaces/role.d.ts +25 -0
- package/dist/db/interfaces/role.d.ts.map +1 -0
- package/dist/db/interfaces/role.js +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +5 -0
- package/dist/field-access.d.ts +30 -0
- package/dist/field-access.d.ts.map +1 -0
- package/dist/field-access.js +79 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +23 -13
- package/dist/lib/api/api-keys.d.ts +8 -7
- package/dist/lib/api/api-keys.d.ts.map +1 -1
- package/dist/lib/api/api-keys.js.map +1 -1
- package/dist/lib/api/assets.d.ts +7 -22
- package/dist/lib/api/assets.d.ts.map +1 -1
- package/dist/lib/api/assets.js +1 -10
- package/dist/lib/api/assets.js.map +1 -1
- package/dist/lib/api/documents.d.ts +17 -16
- package/dist/lib/api/documents.d.ts.map +1 -1
- package/dist/lib/api/documents.js +2 -2
- package/dist/lib/api/documents.js.map +1 -1
- package/dist/lib/api/index.d.ts +1 -0
- package/dist/lib/api/index.d.ts.map +1 -1
- package/dist/lib/api/index.js +1 -0
- package/dist/lib/api/index.js.map +1 -1
- package/dist/lib/api/organizations.d.ts +15 -35
- package/dist/lib/api/organizations.d.ts.map +1 -1
- package/dist/lib/api/organizations.js.map +1 -1
- package/dist/lib/api/roles.d.ts +22 -0
- package/dist/lib/api/roles.d.ts.map +1 -0
- package/dist/lib/api/roles.js +27 -0
- package/dist/lib/api/roles.js.map +1 -0
- package/dist/lib/api/schemas/api-keys.d.ts +62 -0
- package/dist/lib/api/schemas/api-keys.d.ts.map +1 -0
- package/dist/lib/api/schemas/api-keys.js +29 -0
- package/dist/lib/api/schemas/api-keys.js.map +1 -0
- package/dist/lib/api/schemas/assets.d.ts +174 -0
- package/dist/lib/api/schemas/assets.d.ts.map +1 -0
- package/dist/lib/api/schemas/assets.js +100 -0
- package/dist/lib/api/schemas/assets.js.map +1 -0
- package/dist/lib/api/schemas/documents.d.ts +325 -0
- package/dist/lib/api/schemas/documents.d.ts.map +1 -0
- package/dist/lib/api/schemas/documents.js +162 -0
- package/dist/lib/api/schemas/documents.js.map +1 -0
- package/dist/lib/api/schemas/organizations.d.ts +41 -0
- package/dist/lib/api/schemas/organizations.d.ts.map +1 -0
- package/dist/lib/api/schemas/organizations.js +57 -0
- package/dist/lib/api/schemas/organizations.js.map +1 -0
- package/dist/lib/api/schemas/roles.d.ts +77 -0
- package/dist/lib/api/schemas/roles.d.ts.map +1 -0
- package/dist/lib/api/schemas/roles.js +43 -0
- package/dist/lib/api/schemas/roles.js.map +1 -0
- package/dist/lib/api/schemas/user.d.ts +10 -0
- package/dist/lib/api/schemas/user.d.ts.map +1 -0
- package/dist/lib/api/schemas/user.js +12 -0
- package/dist/lib/api/schemas/user.js.map +1 -0
- package/dist/lib/api/user.d.ts +4 -6
- package/dist/lib/api/user.d.ts.map +1 -1
- package/dist/lib/api/user.js.map +1 -1
- package/dist/lib/app-augment.d.ts +32 -0
- package/dist/lib/app-augment.d.ts.map +1 -0
- package/dist/lib/app-augment.js +19 -0
- package/dist/lib/app-augment.js.map +1 -0
- package/dist/lib/auth/auth-hooks.d.ts +2 -1
- package/dist/lib/auth/auth-hooks.d.ts.map +1 -1
- package/dist/lib/auth/auth-hooks.js +22 -2
- package/dist/lib/auth/auth-hooks.js.map +1 -1
- package/dist/lib/auth/provider.d.ts +5 -0
- package/dist/lib/auth/provider.d.ts.map +1 -1
- package/dist/lib/client/index.d.ts +4 -0
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +7 -0
- package/dist/lib/client/index.js.map +1 -1
- package/dist/lib/components/admin/confirm-dialog/confirm-dialog.svelte.d.ts +16 -0
- package/dist/lib/components/admin/confirm-dialog/confirm-dialog.svelte.d.ts.map +1 -0
- package/dist/lib/components/admin/confirm-dialog/confirm-dialog.svelte.js +30 -0
- package/dist/lib/components/admin/confirm-dialog/confirm-dialog.svelte.js.map +1 -0
- package/dist/lib/db/interfaces/document.d.ts.map +1 -1
- package/dist/lib/db/interfaces/index.d.ts +3 -1
- package/dist/lib/db/interfaces/index.d.ts.map +1 -1
- package/dist/lib/db/interfaces/organization.d.ts +1 -1
- package/dist/lib/db/interfaces/organization.d.ts.map +1 -1
- package/dist/lib/db/interfaces/role.d.ts +25 -0
- package/dist/lib/db/interfaces/role.d.ts.map +1 -0
- package/dist/lib/db/interfaces/role.js +2 -0
- package/dist/lib/db/interfaces/role.js.map +1 -0
- package/dist/lib/engine.d.ts.map +1 -1
- package/dist/lib/engine.js +5 -0
- package/dist/lib/engine.js.map +1 -1
- package/dist/lib/field-access.d.ts +30 -0
- package/dist/lib/field-access.d.ts.map +1 -0
- package/dist/lib/field-access.js +80 -0
- package/dist/lib/field-access.js.map +1 -0
- package/dist/lib/hooks.d.ts +2 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/hooks.js +23 -13
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/local-api/collection-api.d.ts +2 -0
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +98 -30
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/local-api/index.d.ts.map +1 -1
- package/dist/lib/local-api/index.js.map +1 -1
- package/dist/lib/local-api/permissions.d.ts +26 -26
- package/dist/lib/local-api/permissions.d.ts.map +1 -1
- package/dist/lib/local-api/permissions.js +122 -80
- package/dist/lib/local-api/permissions.js.map +1 -1
- package/dist/lib/permissions-context.svelte.d.ts +44 -0
- package/dist/lib/permissions-context.svelte.d.ts.map +1 -0
- package/dist/lib/permissions-context.svelte.js +80 -0
- package/dist/lib/permissions-context.svelte.js.map +1 -0
- package/dist/lib/routes/assets-bulk.d.ts.map +1 -1
- package/dist/lib/routes/assets-bulk.js +10 -3
- package/dist/lib/routes/assets-bulk.js.map +1 -1
- package/dist/lib/routes/assets-by-id.d.ts.map +1 -1
- package/dist/lib/routes/assets-by-id.js +11 -1
- package/dist/lib/routes/assets-by-id.js.map +1 -1
- package/dist/lib/routes/assets-references-counts.d.ts.map +1 -1
- package/dist/lib/routes/assets-references-counts.js +12 -2
- package/dist/lib/routes/assets-references-counts.js.map +1 -1
- package/dist/lib/routes/assets.d.ts.map +1 -1
- package/dist/lib/routes/assets.js +14 -13
- package/dist/lib/routes/assets.js.map +1 -1
- package/dist/lib/routes/document-versions.d.ts.map +1 -1
- package/dist/lib/routes/document-versions.js +16 -2
- package/dist/lib/routes/document-versions.js.map +1 -1
- package/dist/lib/routes/documents-by-id.d.ts.map +1 -1
- package/dist/lib/routes/documents-by-id.js +15 -3
- package/dist/lib/routes/documents-by-id.js.map +1 -1
- package/dist/lib/routes/documents-query.d.ts.map +1 -1
- package/dist/lib/routes/documents-query.js +13 -10
- package/dist/lib/routes/documents-query.js.map +1 -1
- package/dist/lib/routes/documents.d.ts.map +1 -1
- package/dist/lib/routes/documents.js +29 -26
- package/dist/lib/routes/documents.js.map +1 -1
- package/dist/lib/routes/organizations-by-id.d.ts.map +1 -1
- package/dist/lib/routes/organizations-by-id.js +17 -4
- package/dist/lib/routes/organizations-by-id.js.map +1 -1
- package/dist/lib/routes/organizations-invitations.d.ts.map +1 -1
- package/dist/lib/routes/organizations-invitations.js +48 -18
- package/dist/lib/routes/organizations-invitations.js.map +1 -1
- package/dist/lib/routes/organizations-members.d.ts.map +1 -1
- package/dist/lib/routes/organizations-members.js +24 -18
- package/dist/lib/routes/organizations-members.js.map +1 -1
- package/dist/lib/routes/organizations-switch.d.ts.map +1 -1
- package/dist/lib/routes/organizations-switch.js +7 -3
- package/dist/lib/routes/organizations-switch.js.map +1 -1
- package/dist/lib/routes/organizations.d.ts.map +1 -1
- package/dist/lib/routes/organizations.js +11 -5
- package/dist/lib/routes/organizations.js.map +1 -1
- package/dist/lib/routes/roles-by-name.d.ts +4 -0
- package/dist/lib/routes/roles-by-name.d.ts.map +1 -0
- package/dist/lib/routes/roles-by-name.js +112 -0
- package/dist/lib/routes/roles-by-name.js.map +1 -0
- package/dist/lib/routes/roles.d.ts +4 -0
- package/dist/lib/routes/roles.d.ts.map +1 -0
- package/dist/lib/routes/roles.js +91 -0
- package/dist/lib/routes/roles.js.map +1 -0
- package/dist/lib/routes/user-preferences.d.ts.map +1 -1
- package/dist/lib/routes/user-preferences.js +7 -13
- package/dist/lib/routes/user-preferences.js.map +1 -1
- package/dist/lib/routes-exports.d.ts +2 -0
- package/dist/lib/routes-exports.d.ts.map +1 -1
- package/dist/lib/routes-exports.js +3 -0
- package/dist/lib/routes-exports.js.map +1 -1
- package/dist/lib/save-state-context.svelte.d.ts +8 -0
- package/dist/lib/save-state-context.svelte.d.ts.map +1 -0
- package/dist/lib/save-state-context.svelte.js +9 -0
- package/dist/lib/save-state-context.svelte.js.map +1 -0
- package/dist/lib/schema-utils/validator.d.ts.map +1 -1
- package/dist/lib/schema-utils/validator.js +14 -0
- package/dist/lib/schema-utils/validator.js.map +1 -1
- package/dist/lib/server/index.d.ts +1 -0
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +1 -0
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/services/index.d.ts +1 -0
- package/dist/lib/services/index.d.ts.map +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/services/index.js.map +1 -1
- package/dist/lib/services/roles-service.d.ts +35 -0
- package/dist/lib/services/roles-service.d.ts.map +1 -0
- package/dist/lib/services/roles-service.js +91 -0
- package/dist/lib/services/roles-service.js.map +1 -0
- package/dist/lib/services/version-service.d.ts.map +1 -1
- package/dist/lib/services/version-service.js.map +1 -1
- package/dist/lib/types/auth.d.ts +43 -9
- package/dist/lib/types/auth.d.ts.map +1 -1
- package/dist/lib/types/auth.js +31 -26
- package/dist/lib/types/auth.js.map +1 -1
- package/dist/lib/types/capabilities.d.ts +104 -0
- package/dist/lib/types/capabilities.d.ts.map +1 -0
- package/dist/lib/types/capabilities.js +217 -0
- package/dist/lib/types/capabilities.js.map +1 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/organization.d.ts +13 -4
- package/dist/lib/types/organization.d.ts.map +1 -1
- package/dist/lib/types/schemas.d.ts +71 -0
- package/dist/lib/types/schemas.d.ts.map +1 -1
- package/dist/lib/types/sidebar.d.ts +1 -1
- package/dist/lib/types/sidebar.d.ts.map +1 -1
- package/dist/lib/utils/mime-detect.d.ts.map +1 -1
- package/dist/lib/utils/mime-detect.js +28 -6
- package/dist/lib/utils/mime-detect.js.map +1 -1
- package/dist/local-api/collection-api.d.ts +2 -0
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +98 -30
- package/dist/local-api/index.d.ts.map +1 -1
- package/dist/local-api/permissions.d.ts +26 -26
- package/dist/local-api/permissions.d.ts.map +1 -1
- package/dist/local-api/permissions.js +122 -80
- package/dist/permissions-context.svelte.d.ts +44 -0
- package/dist/permissions-context.svelte.d.ts.map +1 -0
- package/dist/permissions-context.svelte.js +79 -0
- package/dist/plugins/README.md +20 -20
- package/dist/routes/assets-bulk.d.ts.map +1 -1
- package/dist/routes/assets-bulk.js +10 -3
- package/dist/routes/assets-by-id.d.ts.map +1 -1
- package/dist/routes/assets-by-id.js +11 -1
- package/dist/routes/assets-references-counts.d.ts.map +1 -1
- package/dist/routes/assets-references-counts.js +12 -2
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/assets.js +14 -13
- package/dist/routes/document-versions.d.ts.map +1 -1
- package/dist/routes/document-versions.js +16 -2
- package/dist/routes/documents-by-id.d.ts.map +1 -1
- package/dist/routes/documents-by-id.js +15 -3
- package/dist/routes/documents-query.d.ts.map +1 -1
- package/dist/routes/documents-query.js +13 -10
- package/dist/routes/documents.d.ts.map +1 -1
- package/dist/routes/documents.js +29 -26
- package/dist/routes/organizations-by-id.d.ts.map +1 -1
- package/dist/routes/organizations-by-id.js +17 -4
- package/dist/routes/organizations-invitations.d.ts.map +1 -1
- package/dist/routes/organizations-invitations.js +48 -18
- package/dist/routes/organizations-members.d.ts.map +1 -1
- package/dist/routes/organizations-members.js +24 -18
- package/dist/routes/organizations-switch.d.ts.map +1 -1
- package/dist/routes/organizations-switch.js +7 -3
- package/dist/routes/organizations.d.ts.map +1 -1
- package/dist/routes/organizations.js +11 -5
- package/dist/routes/roles-by-name.d.ts +4 -0
- package/dist/routes/roles-by-name.d.ts.map +1 -0
- package/dist/routes/roles-by-name.js +111 -0
- package/dist/routes/roles.d.ts +4 -0
- package/dist/routes/roles.d.ts.map +1 -0
- package/dist/routes/roles.js +90 -0
- package/dist/routes/user-preferences.d.ts.map +1 -1
- package/dist/routes/user-preferences.js +7 -13
- package/dist/routes-exports.d.ts +2 -0
- package/dist/routes-exports.d.ts.map +1 -1
- package/dist/routes-exports.js +3 -0
- package/dist/save-state-context.svelte.d.ts +8 -0
- package/dist/save-state-context.svelte.d.ts.map +1 -0
- package/dist/save-state-context.svelte.js +8 -0
- package/dist/schema-utils/validator.d.ts.map +1 -1
- package/dist/schema-utils/validator.js +14 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/roles-service.d.ts +35 -0
- package/dist/services/roles-service.d.ts.map +1 -0
- package/dist/services/roles-service.js +90 -0
- package/dist/services/version-service.d.ts.map +1 -1
- package/dist/types/auth.d.ts +43 -9
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/auth.js +31 -26
- package/dist/types/capabilities.d.ts +104 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +216 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/organization.d.ts +13 -4
- package/dist/types/organization.d.ts.map +1 -1
- package/dist/types/schemas.d.ts +71 -0
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/sidebar.d.ts +1 -1
- package/dist/types/sidebar.d.ts.map +1 -1
- package/dist/utils/mime-detect.d.ts.map +1 -1
- package/dist/utils/mime-detect.js +28 -6
- package/package.json +142 -138
|
@@ -2,12 +2,41 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Collection API - provides type-safe CRUD operations for a single collection
|
|
4
4
|
import { validateDocumentData } from '../field-validation/utils.js';
|
|
5
|
+
import { hiddenReadFields, hiddenWriteFields, dropLockedWrites, stripHiddenFields } from '../field-access.js';
|
|
6
|
+
const EMPTY_SET = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Re-project a FindResult through a hidden-fields filter without mutating
|
|
9
|
+
* the shared (potentially cached) original.
|
|
10
|
+
*/
|
|
11
|
+
function applyHiddenToResult(result, hidden) {
|
|
12
|
+
if (hidden.size === 0)
|
|
13
|
+
return result;
|
|
14
|
+
return {
|
|
15
|
+
...result,
|
|
16
|
+
docs: result.docs.map((d) => applyHiddenToDoc(d, hidden))
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function applyHiddenToDoc(doc, hidden) {
|
|
20
|
+
if (!doc || typeof doc !== 'object')
|
|
21
|
+
return doc;
|
|
22
|
+
const copy = {};
|
|
23
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
24
|
+
if (hidden.has(key))
|
|
25
|
+
continue;
|
|
26
|
+
copy[key] = value;
|
|
27
|
+
}
|
|
28
|
+
return copy;
|
|
29
|
+
}
|
|
5
30
|
/**
|
|
6
31
|
* Transform a raw database document into a typed document with data extracted
|
|
7
|
-
* based on perspective (draft or published)
|
|
32
|
+
* based on perspective (draft or published). Optionally strips field-level
|
|
33
|
+
* read-restricted fields from the data payload.
|
|
8
34
|
*/
|
|
9
|
-
function transformDocument(doc, perspective = 'draft') {
|
|
10
|
-
const
|
|
35
|
+
function transformDocument(doc, perspective = 'draft', hiddenFields) {
|
|
36
|
+
const raw = perspective === 'draft' ? doc.draftData : doc.publishedData;
|
|
37
|
+
const data = hiddenFields && hiddenFields.size > 0
|
|
38
|
+
? stripHiddenFields(raw, hiddenFields)
|
|
39
|
+
: raw;
|
|
11
40
|
// Merge document metadata with the content data
|
|
12
41
|
return {
|
|
13
42
|
id: doc.id,
|
|
@@ -77,11 +106,15 @@ export class CollectionAPI {
|
|
|
77
106
|
// Permission check (unless overrideAccess)
|
|
78
107
|
await this.permissions.canRead(context, this.collectionName);
|
|
79
108
|
const perspective = options.perspective || 'draft';
|
|
109
|
+
// Field-level access: compute per-request so two users with different
|
|
110
|
+
// roles see different projections even when the underlying cache hit
|
|
111
|
+
// is shared. Cached payloads are stored unfiltered for this reason.
|
|
112
|
+
const hidden = this.resolveHiddenReadFields(context);
|
|
80
113
|
// Check cache for published queries
|
|
81
114
|
if (perspective === 'published' && this.documentCache) {
|
|
82
115
|
const cached = await this.documentCache.getQuery(context.organizationId, this.collectionName, options);
|
|
83
116
|
if (cached)
|
|
84
|
-
return cached;
|
|
117
|
+
return applyHiddenToResult(cached, hidden);
|
|
85
118
|
}
|
|
86
119
|
// Resolve org IDs via hierarchy service (cached) and pass directly —
|
|
87
120
|
// this avoids the adapter opening a transaction just to set RLS context
|
|
@@ -91,17 +124,28 @@ export class CollectionAPI {
|
|
|
91
124
|
findOptions.filterOrganizationIds = orgIds;
|
|
92
125
|
}
|
|
93
126
|
const result = await this.databaseAdapter.findManyDocAdvanced(context.organizationId, this.collectionName, findOptions);
|
|
94
|
-
// Transform documents to extract data based on perspective
|
|
95
|
-
|
|
96
|
-
const
|
|
127
|
+
// Transform documents to extract data based on perspective.
|
|
128
|
+
// Cache the unfiltered version; return the filtered one.
|
|
129
|
+
const unfilteredDocs = result.docs.map((doc) => transformDocument(doc, perspective));
|
|
130
|
+
const unfilteredResult = {
|
|
97
131
|
...result,
|
|
98
|
-
docs:
|
|
132
|
+
docs: unfilteredDocs
|
|
99
133
|
};
|
|
100
|
-
// Populate cache for published queries
|
|
134
|
+
// Populate cache for published queries (unfiltered payload)
|
|
101
135
|
if (perspective === 'published' && this.documentCache) {
|
|
102
|
-
await this.documentCache.setQuery(context.organizationId, this.collectionName, options,
|
|
136
|
+
await this.documentCache.setQuery(context.organizationId, this.collectionName, options, unfilteredResult);
|
|
103
137
|
}
|
|
104
|
-
return
|
|
138
|
+
return applyHiddenToResult(unfilteredResult, hidden);
|
|
139
|
+
}
|
|
140
|
+
resolveHiddenReadFields(context) {
|
|
141
|
+
if (context.overrideAccess)
|
|
142
|
+
return EMPTY_SET;
|
|
143
|
+
return hiddenReadFields(this._schema, context.auth);
|
|
144
|
+
}
|
|
145
|
+
resolveHiddenWriteFields(context) {
|
|
146
|
+
if (context.overrideAccess)
|
|
147
|
+
return EMPTY_SET;
|
|
148
|
+
return hiddenWriteFields(this._schema, context.auth);
|
|
105
149
|
}
|
|
106
150
|
/**
|
|
107
151
|
* Find a single document by ID
|
|
@@ -119,11 +163,12 @@ export class CollectionAPI {
|
|
|
119
163
|
// Permission check (unless overrideAccess)
|
|
120
164
|
await this.permissions.canRead(context, this.collectionName);
|
|
121
165
|
const perspective = options?.perspective || 'draft';
|
|
166
|
+
const hidden = this.resolveHiddenReadFields(context);
|
|
122
167
|
// Check cache for published lookups
|
|
123
168
|
if (perspective === 'published' && this.documentCache) {
|
|
124
169
|
const cached = await this.documentCache.getDocument(context.organizationId, id);
|
|
125
170
|
if (cached)
|
|
126
|
-
return cached;
|
|
171
|
+
return applyHiddenToDoc(cached, hidden);
|
|
127
172
|
}
|
|
128
173
|
// Resolve org IDs via hierarchy service (cached) — avoids RLS transaction
|
|
129
174
|
const findOptions = { ...options };
|
|
@@ -135,11 +180,13 @@ export class CollectionAPI {
|
|
|
135
180
|
if (!result) {
|
|
136
181
|
return null;
|
|
137
182
|
}
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
// Cache the unfiltered projection; apply hidden fields per-caller.
|
|
184
|
+
const unfiltered = transformDocument(result, perspective);
|
|
185
|
+
// Populate cache for published lookups (unfiltered payload)
|
|
140
186
|
if (perspective === 'published' && this.documentCache) {
|
|
141
|
-
await this.documentCache.setDocument(context.organizationId, id,
|
|
187
|
+
await this.documentCache.setDocument(context.organizationId, id, unfiltered);
|
|
142
188
|
}
|
|
189
|
+
const transformed = applyHiddenToDoc(unfiltered, hidden);
|
|
143
190
|
return transformed;
|
|
144
191
|
}
|
|
145
192
|
/**
|
|
@@ -177,9 +224,16 @@ export class CollectionAPI {
|
|
|
177
224
|
*/
|
|
178
225
|
async create(context, data, options) {
|
|
179
226
|
// Permission check (unless overrideAccess)
|
|
180
|
-
await this.permissions.
|
|
227
|
+
await this.permissions.canCreate(context, this.collectionName);
|
|
228
|
+
// Drop any writes targeting field-locked names before validation.
|
|
229
|
+
// A caller with collection-level update may still lack write access on
|
|
230
|
+
// a specific field; silently dropping keeps the server authoritative.
|
|
231
|
+
const locked = this.resolveHiddenWriteFields(context);
|
|
232
|
+
const filteredData = dropLockedWrites(data, locked);
|
|
181
233
|
// Validate and normalize data (dates converted to ISO)
|
|
182
|
-
const validationResult = await validateDocumentData(this._schema,
|
|
234
|
+
const validationResult = await validateDocumentData(this._schema, filteredData, {
|
|
235
|
+
context: filteredData
|
|
236
|
+
});
|
|
183
237
|
if (options?.publish) {
|
|
184
238
|
await this.permissions.canPublish(context, this.collectionName);
|
|
185
239
|
// Block publish if validation fails
|
|
@@ -234,13 +288,19 @@ export class CollectionAPI {
|
|
|
234
288
|
* ```
|
|
235
289
|
*/
|
|
236
290
|
async update(context, id, data, options) {
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
//
|
|
291
|
+
// Fetch the doc first so ownership policies have access to the target.
|
|
292
|
+
// A missing doc still returns null below; capability/role rules stay
|
|
293
|
+
// unaffected by the reorder.
|
|
240
294
|
const existingDoc = await this.databaseAdapter.findByDocIdAdvanced(context.organizationId, id);
|
|
241
295
|
if (!existingDoc) {
|
|
242
296
|
return null;
|
|
243
297
|
}
|
|
298
|
+
await this.permissions.canUpdate(context, this.collectionName, existingDoc);
|
|
299
|
+
// Drop writes targeting field-locked names so a caller with
|
|
300
|
+
// collection-level update can't bypass field-level restrictions.
|
|
301
|
+
// Field-locked values stay at their existing stored value via the merge.
|
|
302
|
+
const locked = this.resolveHiddenWriteFields(context);
|
|
303
|
+
const filteredData = dropLockedWrites(data, locked);
|
|
244
304
|
// Merge existing data with updates, but only keep fields defined in schema
|
|
245
305
|
// This prevents orphaned fields from persisting when schema changes
|
|
246
306
|
const schemaFieldNames = new Set(this._schema.fields.map((f) => f.name));
|
|
@@ -251,8 +311,8 @@ export class CollectionAPI {
|
|
|
251
311
|
cleanedExisting[key] = value;
|
|
252
312
|
}
|
|
253
313
|
}
|
|
254
|
-
// Merge cleaned existing data with
|
|
255
|
-
const mergedData = { ...cleanedExisting, ...
|
|
314
|
+
// Merge cleaned existing data with filtered updates
|
|
315
|
+
const mergedData = { ...cleanedExisting, ...filteredData };
|
|
256
316
|
// Validate and normalize the merged data
|
|
257
317
|
const validationResult = await validateDocumentData(this._schema, mergedData, mergedData);
|
|
258
318
|
// Update draft with normalized data (dates in ISO format)
|
|
@@ -264,7 +324,7 @@ export class CollectionAPI {
|
|
|
264
324
|
return null;
|
|
265
325
|
}
|
|
266
326
|
if (options?.publish) {
|
|
267
|
-
await this.permissions.canPublish(context, this.collectionName);
|
|
327
|
+
await this.permissions.canPublish(context, this.collectionName, document);
|
|
268
328
|
// Block publish if validation fails
|
|
269
329
|
if (!validationResult.isValid) {
|
|
270
330
|
const errorMessage = validationResult.errors
|
|
@@ -304,8 +364,13 @@ export class CollectionAPI {
|
|
|
304
364
|
* ```
|
|
305
365
|
*/
|
|
306
366
|
async delete(context, id) {
|
|
307
|
-
//
|
|
308
|
-
|
|
367
|
+
// Fetch the target so ownership policies have a doc to inspect. For
|
|
368
|
+
// role/capability rules this extra read is a small cost; ergonomic
|
|
369
|
+
// wins outweigh it for operations that are rare by nature.
|
|
370
|
+
const existing = await this.databaseAdapter.findByDocIdAdvanced(context.organizationId, id);
|
|
371
|
+
if (!existing)
|
|
372
|
+
return false;
|
|
373
|
+
await this.permissions.canDelete(context, this.collectionName, existing);
|
|
309
374
|
const result = await this.databaseAdapter.deleteDocById(context.organizationId, id);
|
|
310
375
|
// Invalidate cache for deleted document
|
|
311
376
|
if (result && this.documentCache) {
|
|
@@ -326,13 +391,13 @@ export class CollectionAPI {
|
|
|
326
391
|
* ```
|
|
327
392
|
*/
|
|
328
393
|
async publish(context, id) {
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
// Get the document to access draft data for validation
|
|
394
|
+
// Fetch first so policies can inspect the target. Order matters here:
|
|
395
|
+
// 404s beat 403s for missing docs.
|
|
332
396
|
const document = await this.databaseAdapter.findByDocIdAdvanced(context.organizationId, id);
|
|
333
397
|
if (!document || !document.draftData) {
|
|
334
398
|
throw new Error('Document not found or has no draft content to publish');
|
|
335
399
|
}
|
|
400
|
+
await this.permissions.canPublish(context, this.collectionName, document);
|
|
336
401
|
// Validate draft data (dates already in ISO, will be converted for validation)
|
|
337
402
|
const validationResult = await validateDocumentData(this._schema, document.draftData, document.draftData);
|
|
338
403
|
if (!validationResult.isValid) {
|
|
@@ -367,8 +432,11 @@ export class CollectionAPI {
|
|
|
367
432
|
* ```
|
|
368
433
|
*/
|
|
369
434
|
async unpublish(context, id) {
|
|
370
|
-
//
|
|
371
|
-
await this.
|
|
435
|
+
// Fetch target for policy inspection before mutating.
|
|
436
|
+
const existing = await this.databaseAdapter.findByDocIdAdvanced(context.organizationId, id);
|
|
437
|
+
if (!existing)
|
|
438
|
+
return null;
|
|
439
|
+
await this.permissions.canUnpublish(context, this.collectionName, existing);
|
|
372
440
|
const document = await this.databaseAdapter.unpublishDoc(context.organizationId, id);
|
|
373
441
|
if (!document) {
|
|
374
442
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/local-api/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C;;;GAGG;AACH,MAAM,WAAW,WAAW;IAE3B,CAAC,cAAc,EAAE,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;CAGjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,QAAQ;IAWnB,OAAO,CAAC,MAAM;IAVR,WAAW,EAAE,WAAW,CAAqB;IACpD,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,gBAAgB,CAAmB;IACpC,cAAc,EAAE,cAAc,CAAC;IACtC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,OAAO,CAA0B;gBAGhC,MAAM,EAAE,SAAS,EACzB,WAAW,EAAE,eAAe,EAC5B,aAAa,CAAC,EAAE,eAAe;IAwBhC;;OAEG;IACH,OAAO,CAAC,qBAAqB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/local-api/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C;;;GAGG;AACH,MAAM,WAAW,WAAW;IAE3B,CAAC,cAAc,EAAE,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;CAGjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,QAAQ;IAWnB,OAAO,CAAC,MAAM;IAVR,WAAW,EAAE,WAAW,CAAqB;IACpD,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,gBAAgB,CAAmB;IACpC,cAAc,EAAE,cAAc,CAAC;IACtC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,OAAO,CAA0B;gBAGhC,MAAM,EAAE,SAAS,EACzB,WAAW,EAAE,eAAe,EAC5B,aAAa,CAAC,EAAE,eAAe;IAwBhC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAgD7B;;;OAGG;IACH,OAAO,CAAC,UAAU;IAOlB;;OAEG;IACH,kBAAkB,IAAI,MAAM,EAAE;IAI9B;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIpC;;OAEG;IACH,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAIzD;;;;OAIG;IACG,gBAAgB,CACrB,OAAO,EAAE,eAAe,EACxB,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,GACrC,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CAetD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAC7B,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,eAAe,EAC5B,aAAa,CAAC,EAAE,eAAe,GAC7B,QAAQ,CAGV;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,IAAI,QAAQ,CAKtC;AAGD,OAAO,EAAE,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
import type { LocalAPIContext } from './types.js';
|
|
2
2
|
import type { SchemaType } from '../types/schemas.js';
|
|
3
|
+
import type { Document } from '../types/document.js';
|
|
3
4
|
import type { CMSConfig } from '../types/config.js';
|
|
4
|
-
/**
|
|
5
|
-
* Permission error thrown when access is denied
|
|
6
|
-
*/
|
|
7
5
|
export declare class PermissionError extends Error {
|
|
8
6
|
readonly operation: string;
|
|
9
7
|
readonly resource: string;
|
|
10
8
|
constructor(message: string, operation: string, resource: string);
|
|
11
9
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Checks permissions for Local API operations
|
|
14
|
-
*/
|
|
15
10
|
export declare class PermissionChecker {
|
|
16
11
|
private _config;
|
|
17
12
|
private schemas;
|
|
18
13
|
constructor(_config: CMSConfig, schemas: Map<string, SchemaType>);
|
|
19
|
-
/**
|
|
20
|
-
* Get the CMS config
|
|
21
|
-
* Available for future per-collection permission logic
|
|
22
|
-
*/
|
|
23
14
|
get config(): CMSConfig;
|
|
15
|
+
canRead(context: LocalAPIContext, collectionName: string, doc?: Document): Promise<void>;
|
|
16
|
+
canCreate(context: LocalAPIContext, collectionName: string): Promise<void>;
|
|
17
|
+
canUpdate(context: LocalAPIContext, collectionName: string, doc?: Document): Promise<void>;
|
|
24
18
|
/**
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if the current context has write permissions (create/update)
|
|
30
|
-
*/
|
|
31
|
-
canWrite(context: LocalAPIContext, collectionName: string): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Check if the current context has delete permissions
|
|
34
|
-
*/
|
|
35
|
-
canDelete(context: LocalAPIContext, collectionName: string): Promise<void>;
|
|
36
|
-
/**
|
|
37
|
-
* Check if the current context has publish permissions
|
|
19
|
+
* @deprecated Prefer `canCreate` or `canUpdate` — this method conflates the
|
|
20
|
+
* two and was only kept for legacy call sites. It now aliases `canUpdate`
|
|
21
|
+
* for safety (update is the more restrictive default for mutation).
|
|
38
22
|
*/
|
|
39
|
-
|
|
23
|
+
canWrite(context: LocalAPIContext, collectionName: string, doc?: Document): Promise<void>;
|
|
24
|
+
canDelete(context: LocalAPIContext, collectionName: string, doc?: Document): Promise<void>;
|
|
25
|
+
canPublish(context: LocalAPIContext, collectionName: string, doc?: Document): Promise<void>;
|
|
26
|
+
canUnpublish(context: LocalAPIContext, collectionName: string, doc?: Document): Promise<void>;
|
|
27
|
+
validateCollection(collectionName: string): void;
|
|
28
|
+
private assert;
|
|
29
|
+
private logDenial;
|
|
30
|
+
private requireAuth;
|
|
40
31
|
/**
|
|
41
|
-
*
|
|
32
|
+
* Evaluate an access rule for a given operation.
|
|
33
|
+
*
|
|
34
|
+
* Three kinds of declared rules:
|
|
35
|
+
* - `OrganizationRole[]` — role allowlist (as before).
|
|
36
|
+
* - `(ctx) => boolean` — arbitrary policy, receives auth + optional doc.
|
|
37
|
+
* Use for ownership rules like `doc.createdBy === auth.user.id`.
|
|
38
|
+
* - `undefined` — fall back to capability check.
|
|
39
|
+
*
|
|
40
|
+
* A declared-but-excluded role/policy for a session caller is an explicit
|
|
41
|
+
* deny; the capability map does not re-grant access.
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
private isAllowed;
|
|
44
44
|
}
|
|
45
45
|
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/lib/local-api/permissions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/lib/local-api/permissions.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,kBAAkB,CAAC;AACjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKjD,qBAAa,eAAgB,SAAQ,KAAK;aAGxB,SAAS,EAAE,MAAM;aACjB,QAAQ,EAAE,MAAM;gBAFhC,OAAO,EAAE,MAAM,EACC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM;CAKjC;AAmCD,qBAAa,iBAAiB;IAE5B,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,OAAO;gBADP,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC;IAGzC,IAAI,MAAM,IAAI,SAAS,CAEtB;IAEK,OAAO,CAAC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxF,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1E,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhG;;;;OAIG;IACG,QAAQ,CAAC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzF,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1F,UAAU,CACf,OAAO,EAAE,eAAe,EACxB,cAAc,EAAE,MAAM,EACtB,GAAG,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,IAAI,CAAC;IAIV,YAAY,CACjB,OAAO,EAAE,eAAe,EACxB,cAAc,EAAE,MAAM,EACtB,GAAG,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,IAAI,CAAC;IAIhB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;YAYlC,MAAM;IAmBpB,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,WAAW;IAenB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,SAAS;CAmCjB"}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// local-api/permissions.ts
|
|
2
2
|
//
|
|
3
|
-
// Permission checking for the Local API layer
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
// Permission checking for the Local API layer.
|
|
4
|
+
//
|
|
5
|
+
// Every operation resolves to a capability. A check passes when either:
|
|
6
|
+
// 1. The collection schema declares `access[operation]` and the caller's
|
|
7
|
+
// effective organization role appears in the list, OR
|
|
8
|
+
// 2. `auth.capabilities` (hydrated by the auth hook via RolesService)
|
|
9
|
+
// contains the capability.
|
|
10
|
+
//
|
|
11
|
+
// `overrideAccess` short-circuits everything for system operations.
|
|
12
|
+
import { hasCapability, effectiveOrganizationRole } from '../types/capabilities.js';
|
|
13
|
+
import { cmsLogger } from '../utils/logger.js';
|
|
7
14
|
export class PermissionError extends Error {
|
|
8
15
|
operation;
|
|
9
16
|
resource;
|
|
@@ -14,9 +21,33 @@ export class PermissionError extends Error {
|
|
|
14
21
|
this.name = 'PermissionError';
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
const OPERATION_CAPABILITY = {
|
|
25
|
+
read: 'document.read',
|
|
26
|
+
create: 'document.create',
|
|
27
|
+
update: 'document.update',
|
|
28
|
+
delete: 'document.delete',
|
|
29
|
+
publish: 'document.publish',
|
|
30
|
+
unpublish: 'document.unpublish'
|
|
31
|
+
};
|
|
32
|
+
// Human-readable action phrasing for error messages. Keep in sync with
|
|
33
|
+
// OPERATION_CAPABILITY and any new Operation keys.
|
|
34
|
+
const OPERATION_LABEL = {
|
|
35
|
+
read: 'view',
|
|
36
|
+
create: 'create',
|
|
37
|
+
update: 'edit',
|
|
38
|
+
delete: 'delete',
|
|
39
|
+
publish: 'publish',
|
|
40
|
+
unpublish: 'unpublish'
|
|
41
|
+
};
|
|
42
|
+
function denialMessage(operations, collectionName) {
|
|
43
|
+
const actions = operations.map((op) => OPERATION_LABEL[op]);
|
|
44
|
+
const action = actions.length === 1
|
|
45
|
+
? actions[0]
|
|
46
|
+
: actions.length === 2
|
|
47
|
+
? `${actions[0]} or ${actions[1]}`
|
|
48
|
+
: `${actions.slice(0, -1).join(', ')}, or ${actions[actions.length - 1]}`;
|
|
49
|
+
return `You don't have permission to ${action} ${collectionName} documents. Ask an admin if you need access.`;
|
|
50
|
+
}
|
|
20
51
|
export class PermissionChecker {
|
|
21
52
|
_config;
|
|
22
53
|
schemas;
|
|
@@ -24,93 +55,104 @@ export class PermissionChecker {
|
|
|
24
55
|
this._config = _config;
|
|
25
56
|
this.schemas = schemas;
|
|
26
57
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Get the CMS config
|
|
29
|
-
* Available for future per-collection permission logic
|
|
30
|
-
*/
|
|
31
58
|
get config() {
|
|
32
59
|
return this._config;
|
|
33
60
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
async canRead(context, collectionName) {
|
|
38
|
-
// Always allow if overrideAccess is true (system operations)
|
|
39
|
-
if (context.overrideAccess) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
// Require authentication for read operations
|
|
43
|
-
if (!context.user) {
|
|
44
|
-
throw new PermissionError('Authentication required for read operations', 'read', collectionName);
|
|
45
|
-
}
|
|
46
|
-
// All authenticated users can read (role-based restrictions handled by RLS)
|
|
47
|
-
// Additional per-collection permission logic can be added here in the future
|
|
48
|
-
return;
|
|
61
|
+
async canRead(context, collectionName, doc) {
|
|
62
|
+
await this.assert(context, collectionName, 'read', doc);
|
|
49
63
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
// Require authentication for write operations
|
|
59
|
-
if (!context.user) {
|
|
60
|
-
throw new PermissionError('Authentication required for write operations', 'write', collectionName);
|
|
61
|
-
}
|
|
62
|
-
// Check if user has write role (not a viewer)
|
|
63
|
-
// Viewers have read-only access
|
|
64
|
-
if (context.user.role === 'viewer') {
|
|
65
|
-
throw new PermissionError('Write permission denied. Viewers have read-only access.', 'write', collectionName);
|
|
66
|
-
}
|
|
67
|
-
// Additional per-collection permission logic can be added here in the future
|
|
68
|
-
return;
|
|
64
|
+
async canCreate(context, collectionName) {
|
|
65
|
+
// `create` has no target doc by definition.
|
|
66
|
+
await this.assert(context, collectionName, 'create');
|
|
67
|
+
}
|
|
68
|
+
async canUpdate(context, collectionName, doc) {
|
|
69
|
+
await this.assert(context, collectionName, 'update', doc);
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
+
* @deprecated Prefer `canCreate` or `canUpdate` — this method conflates the
|
|
73
|
+
* two and was only kept for legacy call sites. It now aliases `canUpdate`
|
|
74
|
+
* for safety (update is the more restrictive default for mutation).
|
|
72
75
|
*/
|
|
73
|
-
async
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
async canWrite(context, collectionName, doc) {
|
|
77
|
+
await this.canUpdate(context, collectionName, doc);
|
|
78
|
+
}
|
|
79
|
+
async canDelete(context, collectionName, doc) {
|
|
80
|
+
await this.assert(context, collectionName, 'delete', doc);
|
|
81
|
+
}
|
|
82
|
+
async canPublish(context, collectionName, doc) {
|
|
83
|
+
await this.assert(context, collectionName, 'publish', doc);
|
|
84
|
+
}
|
|
85
|
+
async canUnpublish(context, collectionName, doc) {
|
|
86
|
+
await this.assert(context, collectionName, 'unpublish', doc);
|
|
87
|
+
}
|
|
88
|
+
validateCollection(collectionName) {
|
|
89
|
+
if (!this.schemas.has(collectionName)) {
|
|
90
|
+
throw new Error(`Collection "${collectionName}" not found in schema. Available collections: ${Array.from(this.schemas.keys()).join(', ')}`);
|
|
85
91
|
}
|
|
86
|
-
// Additional per-collection permission logic can be added here in the future
|
|
87
|
-
return;
|
|
88
92
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
async canPublish(context, collectionName) {
|
|
93
|
-
// Always allow if overrideAccess is true (system operations)
|
|
94
|
-
if (context.overrideAccess) {
|
|
93
|
+
// ---- internals -----------------------------------------------------------
|
|
94
|
+
async assert(context, collectionName, operation, doc) {
|
|
95
|
+
if (context.overrideAccess)
|
|
95
96
|
return;
|
|
97
|
+
const auth = this.requireAuth(context, operation, collectionName);
|
|
98
|
+
if (this.isAllowed(auth, collectionName, operation, doc))
|
|
99
|
+
return;
|
|
100
|
+
this.logDenial(auth, operation, collectionName, [OPERATION_CAPABILITY[operation]]);
|
|
101
|
+
throw new PermissionError(denialMessage([operation], collectionName), operation, collectionName);
|
|
102
|
+
}
|
|
103
|
+
logDenial(auth, operation, collectionName, missingCapabilities) {
|
|
104
|
+
const who = auth.type === 'session'
|
|
105
|
+
? `user=${auth.user.id} role="${auth.organizationRole}"`
|
|
106
|
+
: auth.type === 'api_key'
|
|
107
|
+
? `apiKey=${auth.keyId}`
|
|
108
|
+
: auth.type;
|
|
109
|
+
const caps = 'capabilities' in auth && Array.isArray(auth.capabilities)
|
|
110
|
+
? auth.capabilities.join(',')
|
|
111
|
+
: '(none)';
|
|
112
|
+
cmsLogger.warn('[RBAC]', `DENY ${operation} on "${collectionName}" — ${who} has=[${caps}] needs=[${missingCapabilities.join('|')}]`);
|
|
113
|
+
}
|
|
114
|
+
requireAuth(context, operation, collectionName) {
|
|
115
|
+
if (!context.auth) {
|
|
116
|
+
throw new PermissionError(`You must be signed in to ${OPERATION_LABEL[operation]} ${collectionName} documents.`, operation, collectionName);
|
|
96
117
|
}
|
|
97
|
-
|
|
98
|
-
if (!context.user) {
|
|
99
|
-
throw new PermissionError('Authentication required for publish operations', 'publish', collectionName);
|
|
100
|
-
}
|
|
101
|
-
// Check if user has write role (not a viewer)
|
|
102
|
-
if (context.user.role === 'viewer') {
|
|
103
|
-
throw new PermissionError('Publish permission denied. Viewers have read-only access.', 'publish', collectionName);
|
|
104
|
-
}
|
|
105
|
-
// Additional per-collection permission logic can be added here in the future
|
|
106
|
-
return;
|
|
118
|
+
return context.auth;
|
|
107
119
|
}
|
|
108
120
|
/**
|
|
109
|
-
*
|
|
121
|
+
* Evaluate an access rule for a given operation.
|
|
122
|
+
*
|
|
123
|
+
* Three kinds of declared rules:
|
|
124
|
+
* - `OrganizationRole[]` — role allowlist (as before).
|
|
125
|
+
* - `(ctx) => boolean` — arbitrary policy, receives auth + optional doc.
|
|
126
|
+
* Use for ownership rules like `doc.createdBy === auth.user.id`.
|
|
127
|
+
* - `undefined` — fall back to capability check.
|
|
128
|
+
*
|
|
129
|
+
* A declared-but-excluded role/policy for a session caller is an explicit
|
|
130
|
+
* deny; the capability map does not re-grant access.
|
|
110
131
|
*/
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
isAllowed(auth, collectionName, operation, doc) {
|
|
133
|
+
const schema = this.schemas.get(collectionName);
|
|
134
|
+
const declared = schema?.access?.[operation];
|
|
135
|
+
if (declared) {
|
|
136
|
+
if (typeof declared === 'function') {
|
|
137
|
+
try {
|
|
138
|
+
return declared({ auth, doc });
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
// A throwing policy is a bug, not a grant — log and deny.
|
|
142
|
+
cmsLogger.error('[RBAC]', `access policy for "${collectionName}.${operation}" threw:`, err);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Array-shaped rule (role allowlist)
|
|
147
|
+
const role = effectiveOrganizationRole(auth);
|
|
148
|
+
if (role && declared.includes(role))
|
|
149
|
+
return true;
|
|
150
|
+
if (auth.type === 'session' && role) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
// Unrecognized role (custom roles, API keys) → fall through to
|
|
154
|
+
// capability check so they can still gain access via capabilities.
|
|
114
155
|
}
|
|
156
|
+
return hasCapability(auth, OPERATION_CAPABILITY[operation]);
|
|
115
157
|
}
|
|
116
158
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Capability } from './types/capabilities.js';
|
|
2
|
+
export interface PermissionsContext {
|
|
3
|
+
/** Current capability list (reactive). */
|
|
4
|
+
readonly capabilities: readonly string[];
|
|
5
|
+
/**
|
|
6
|
+
* Effective organization role name (reactive). `null` when the caller has
|
|
7
|
+
* no role (API key, unauthenticated). Used for role-name-based checks
|
|
8
|
+
* such as field-level access lists.
|
|
9
|
+
*/
|
|
10
|
+
readonly role: string | null;
|
|
11
|
+
/** True if the session has the named capability. */
|
|
12
|
+
can(cap: Capability): boolean;
|
|
13
|
+
/** True if the session has at least one of the capabilities. */
|
|
14
|
+
canAny(...caps: Capability[]): boolean;
|
|
15
|
+
/** True if the session has every one of the capabilities. */
|
|
16
|
+
canAll(...caps: Capability[]): boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Publish a capability context to descendants.
|
|
20
|
+
*
|
|
21
|
+
* `getCapabilities` is a getter rather than a static array so reactive
|
|
22
|
+
* sources (Svelte state, `$page.data.rbac.capabilities`) propagate: every
|
|
23
|
+
* `can()` call re-reads through the closure.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```svelte
|
|
27
|
+
* <script>
|
|
28
|
+
* import { page } from '$app/state';
|
|
29
|
+
* import { setPermissionsContext } from '@aphexcms/cms-core';
|
|
30
|
+
* setPermissionsContext(() => page.data.rbac?.capabilities ?? []);
|
|
31
|
+
* </script>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function setPermissionsContext(getCapabilities: () => readonly string[], getRole?: () => string | null): PermissionsContext;
|
|
35
|
+
/**
|
|
36
|
+
* Read the capability context set by an ancestor.
|
|
37
|
+
*
|
|
38
|
+
* When no provider is present (e.g. a component rendered in isolation during
|
|
39
|
+
* tests or Storybook), returns a deny-all stub with a one-time dev warning
|
|
40
|
+
* rather than throwing — that way consumers degrade to "no affordances shown"
|
|
41
|
+
* instead of crashing the tree.
|
|
42
|
+
*/
|
|
43
|
+
export declare function usePermissions(): PermissionsContext;
|
|
44
|
+
//# sourceMappingURL=permissions-context.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions-context.svelte.d.ts","sourceRoot":"","sources":["../src/lib/permissions-context.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD,MAAM,WAAW,kBAAkB;IAClC,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oDAAoD;IACpD,GAAG,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC;IAC9B,gEAAgE;IAChE,MAAM,CAAC,GAAG,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACvC,6DAA6D;IAC7D,MAAM,CAAC,GAAG,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;CACvC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACpC,eAAe,EAAE,MAAM,SAAS,MAAM,EAAE,EACxC,OAAO,GAAE,MAAM,MAAM,GAAG,IAAiB,GACvC,kBAAkB,CAsBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,IAAI,kBAAkB,CAKnD"}
|