@beechcms/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/account-email.d.ts +3 -0
- package/dist/auth/account-email.d.ts.map +1 -0
- package/dist/auth/account-email.js +9 -0
- package/dist/auth/password-reset-token.repository.d.ts +13 -2
- package/dist/auth/password-reset-token.repository.d.ts.map +1 -1
- package/dist/auth/user.repository.d.ts +69 -4
- package/dist/auth/user.repository.d.ts.map +1 -1
- package/dist/automations-grammar-words.d.ts +2 -0
- package/dist/automations-grammar-words.d.ts.map +1 -0
- package/dist/automations-grammar-words.js +8 -0
- package/dist/automations.repository.interface.d.ts +19 -0
- package/dist/automations.repository.interface.d.ts.map +1 -0
- package/dist/automations.repository.interface.js +3 -0
- package/dist/automations.runner.interface.d.ts +10 -0
- package/dist/automations.runner.interface.d.ts.map +1 -0
- package/dist/automations.runner.interface.js +3 -0
- package/dist/automations.runner.stub.d.ts +5 -0
- package/dist/automations.runner.stub.d.ts.map +1 -0
- package/dist/automations.runner.stub.js +7 -0
- package/dist/automations.types.d.ts +75 -0
- package/dist/automations.types.d.ts.map +1 -0
- package/dist/automations.types.js +3 -0
- package/dist/clock.d.ts +25 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +11 -0
- package/dist/common/canonical-json.d.ts +31 -0
- package/dist/common/canonical-json.d.ts.map +1 -0
- package/dist/common/canonical-json.js +91 -0
- package/dist/content/content-scan.repository.d.ts +1 -1
- package/dist/content/content.repository.d.ts +154 -3
- package/dist/content/content.repository.d.ts.map +1 -1
- package/dist/content/content.repository.js +62 -0
- package/dist/content/deletion-ledger.d.ts +39 -0
- package/dist/content/deletion-ledger.d.ts.map +1 -0
- package/dist/content/deletion-ledger.js +3 -0
- package/dist/content/seed-media-purge.repository.d.ts +34 -0
- package/dist/content/seed-media-purge.repository.d.ts.map +1 -0
- package/dist/content/seed-media-purge.repository.js +3 -0
- package/dist/content/seed.repository.d.ts +14 -0
- package/dist/content/seed.repository.d.ts.map +1 -1
- package/dist/content-scan.repository.d.ts +9 -0
- package/dist/content-scan.repository.d.ts.map +1 -0
- package/dist/content-scan.repository.js +3 -0
- package/dist/content.repository.d.ts +247 -0
- package/dist/content.repository.d.ts.map +1 -0
- package/dist/content.repository.js +59 -0
- package/dist/dashboard-layout/content-view.d.ts +757 -0
- package/dist/dashboard-layout/content-view.d.ts.map +1 -0
- package/dist/dashboard-layout/content-view.js +285 -0
- package/dist/dashboard-layout/content-view.repository.d.ts +39 -0
- package/dist/dashboard-layout/content-view.repository.d.ts.map +1 -0
- package/dist/dashboard-layout/content-view.repository.js +3 -0
- package/dist/dashboard-layout/dashboard-layout.d.ts +5 -0
- package/dist/dashboard-layout/dashboard-layout.d.ts.map +1 -1
- package/dist/dashboard-layout/dashboard-layout.js +17 -9
- package/dist/dashboard-layout/kanban/kanban.d.ts +32 -18
- package/dist/dashboard-layout/kanban/kanban.d.ts.map +1 -1
- package/dist/dashboard-layout/kanban/kanban.js +45 -21
- package/dist/dashboard-layout/seed-layout.d.ts +22 -31
- package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
- package/dist/dashboard-layout/seed-layout.js +93 -18
- package/dist/dashboard-layout/seed-layout.repository.d.ts +1 -5
- package/dist/dashboard-layout/seed-layout.repository.d.ts.map +1 -1
- package/dist/dashboard-layout.d.ts +154 -0
- package/dist/dashboard-layout.d.ts.map +1 -0
- package/dist/dashboard-layout.js +186 -0
- package/dist/dashboard-layout.repository.d.ts +18 -0
- package/dist/dashboard-layout.repository.d.ts.map +1 -0
- package/dist/dashboard-layout.repository.js +3 -0
- package/dist/dashboard-permissions.d.ts +3 -0
- package/dist/dashboard-permissions.d.ts.map +1 -0
- package/dist/dashboard-permissions.js +11 -0
- package/dist/dashboard-scopes.d.ts +12 -0
- package/dist/dashboard-scopes.d.ts.map +1 -0
- package/dist/dashboard-scopes.js +30 -0
- package/dist/define-seed.d.ts +3 -0
- package/dist/define-seed.d.ts.map +1 -0
- package/dist/define-seed.js +5 -0
- package/dist/demo-data.repository.d.ts +5 -0
- package/dist/demo-data.repository.d.ts.map +1 -0
- package/dist/demo-data.repository.js +3 -0
- package/dist/email.provider.d.ts +32 -0
- package/dist/email.provider.d.ts.map +1 -0
- package/dist/email.provider.js +4 -0
- package/dist/engine/ddl/ddl.d.ts +247 -0
- package/dist/engine/ddl/ddl.d.ts.map +1 -0
- package/dist/engine/ddl/ddl.js +653 -0
- package/dist/engine/ddl/schema-mutator.d.ts +29 -0
- package/dist/engine/ddl/schema-mutator.d.ts.map +1 -0
- package/dist/engine/ddl/schema-mutator.js +3 -0
- package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
- package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
- package/dist/engine/ddl/seed-ddl.d.ts +43 -0
- package/dist/engine/ddl/seed-ddl.d.ts.map +1 -0
- package/dist/engine/ddl/seed-ddl.js +115 -0
- package/dist/engine/ddl/seed-purge-guards.d.ts +7 -0
- package/dist/engine/ddl/seed-purge-guards.d.ts.map +1 -0
- package/dist/engine/ddl/seed-purge-guards.js +19 -0
- package/dist/engine/engine.d.ts +4 -3
- package/dist/engine/engine.d.ts.map +1 -1
- package/dist/engine/engine.js +4 -3
- package/dist/engine/introspection/introspection.d.ts +101 -0
- package/dist/engine/introspection/introspection.d.ts.map +1 -0
- package/dist/engine/introspection/introspection.js +127 -0
- package/dist/engine/introspection/relation-audit.d.ts +46 -0
- package/dist/engine/introspection/relation-audit.d.ts.map +1 -0
- package/dist/engine/introspection/relation-audit.js +103 -0
- package/dist/engine/introspection/schema-fingerprint.d.ts +74 -0
- package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -0
- package/dist/engine/introspection/schema-fingerprint.js +102 -0
- package/dist/engine/introspection.d.ts +101 -0
- package/dist/engine/introspection.d.ts.map +1 -0
- package/dist/engine/introspection.js +127 -0
- package/dist/engine/localization/localization.d.ts +127 -0
- package/dist/engine/localization/localization.d.ts.map +1 -0
- package/dist/engine/localization/localization.js +212 -0
- package/dist/engine/localization.d.ts +127 -0
- package/dist/engine/localization.d.ts.map +1 -0
- package/dist/engine/localization.js +212 -0
- package/dist/engine/privacy/policies.d.ts +58 -0
- package/dist/engine/privacy/policies.d.ts.map +1 -0
- package/dist/engine/privacy/policies.js +191 -0
- package/dist/engine/privacy/privacy.service.d.ts +78 -0
- package/dist/engine/privacy/privacy.service.d.ts.map +1 -0
- package/dist/engine/privacy/privacy.service.js +138 -0
- package/dist/engine/privacy.service.d.ts.map +1 -1
- package/dist/engine/query/active-clause.d.ts +19 -0
- package/dist/engine/query/active-clause.d.ts.map +1 -0
- package/dist/engine/query/active-clause.js +27 -0
- package/dist/engine/query/query.d.ts +36 -0
- package/dist/engine/query/query.d.ts.map +1 -0
- package/dist/engine/query/query.js +393 -0
- package/dist/engine/query/relations.d.ts +28 -0
- package/dist/engine/query/relations.d.ts.map +1 -0
- package/dist/engine/query/relations.js +25 -0
- package/dist/engine/query/serialize.d.ts +22 -0
- package/dist/engine/query/serialize.d.ts.map +1 -0
- package/dist/engine/query/serialize.js +175 -0
- package/dist/engine/schema-fingerprint.d.ts +73 -0
- package/dist/engine/schema-fingerprint.d.ts.map +1 -0
- package/dist/engine/schema-fingerprint.js +97 -0
- package/dist/engine/seed-registry.d.ts.map +1 -1
- package/dist/engine/seeds/candidate-ids.d.ts +14 -0
- package/dist/engine/seeds/candidate-ids.d.ts.map +1 -0
- package/dist/engine/seeds/candidate-ids.js +40 -0
- package/dist/engine/seeds/define-seed.d.ts +3 -0
- package/dist/engine/seeds/define-seed.d.ts.map +1 -0
- package/dist/engine/seeds/define-seed.js +5 -0
- package/dist/engine/seeds/seed-definition-diff.d.ts +38 -0
- package/dist/engine/seeds/seed-definition-diff.d.ts.map +1 -0
- package/dist/engine/seeds/seed-definition-diff.js +51 -0
- package/dist/engine/seeds/seed-registry.d.ts +88 -0
- package/dist/engine/seeds/seed-registry.d.ts.map +1 -0
- package/dist/engine/seeds/seed-registry.js +194 -0
- package/dist/engine/seeds/seed-types-generator.d.ts +25 -0
- package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -0
- package/dist/engine/seeds/seed-types-generator.js +116 -0
- package/dist/engine/seeds/seed-validation.d.ts +35 -0
- package/dist/engine/seeds/seed-validation.d.ts.map +1 -0
- package/dist/engine/seeds/seed-validation.js +384 -0
- package/dist/engine/seeds/seeds.d.ts +20 -0
- package/dist/engine/seeds/seeds.d.ts.map +1 -0
- package/dist/engine/seeds/seeds.js +21 -0
- package/dist/engine/seeds/tag-migration.d.ts +73 -0
- package/dist/engine/seeds/tag-migration.d.ts.map +1 -0
- package/dist/engine/seeds/tag-migration.js +212 -0
- package/dist/engine/seeds/tag-options.d.ts +15 -0
- package/dist/engine/seeds/tag-options.d.ts.map +1 -0
- package/dist/engine/seeds/tag-options.js +111 -0
- package/dist/engine/seeds/tag-value.d.ts +31 -0
- package/dist/engine/seeds/tag-value.d.ts.map +1 -0
- package/dist/engine/seeds/tag-value.js +51 -0
- package/dist/engine/types.d.ts +59 -7
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/engine/validation/cache.d.ts.map +1 -1
- package/dist/engine/validation/cache.js +20 -4
- package/dist/engine/validation/emptiness.d.ts +12 -0
- package/dist/engine/validation/emptiness.d.ts.map +1 -0
- package/dist/engine/validation/emptiness.js +33 -0
- package/dist/engine/validation/index.d.ts +11 -0
- package/dist/engine/validation/index.d.ts.map +1 -1
- package/dist/engine/validation/index.js +29 -33
- package/dist/engine/validation/primitives.d.ts +12 -0
- package/dist/engine/validation/primitives.d.ts.map +1 -1
- package/dist/engine/validation/primitives.js +22 -0
- package/dist/engine/validation/richtext-sanitizer.d.ts +9 -1
- package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
- package/dist/engine/validation/richtext-sanitizer.js +36 -7
- package/dist/engine/validation/schema-builders.d.ts +16 -0
- package/dist/engine/validation/schema-builders.d.ts.map +1 -1
- package/dist/engine/validation/schema-builders.js +125 -18
- package/dist/engine.d.ts +144 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +741 -0
- package/dist/file-types.d.ts +13 -0
- package/dist/file-types.d.ts.map +1 -0
- package/dist/file-types.js +85 -0
- package/dist/hooks.d.ts +29 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +3 -0
- package/dist/id-generator.d.ts +26 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +12 -0
- package/dist/idempotency.repository.d.ts +13 -0
- package/dist/idempotency.repository.d.ts.map +1 -0
- package/dist/idempotency.repository.js +3 -0
- package/dist/index.d.ts +34 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -11
- package/dist/kanban-position.repository.d.ts +19 -0
- package/dist/kanban-position.repository.d.ts.map +1 -0
- package/dist/kanban-position.repository.js +3 -0
- package/dist/kanban.d.ts +75 -0
- package/dist/kanban.d.ts.map +1 -0
- package/dist/kanban.js +82 -0
- package/dist/layout-permissions.d.ts +3 -0
- package/dist/layout-permissions.d.ts.map +1 -0
- package/dist/layout-permissions.js +11 -0
- package/dist/media/file-types.d.ts +12 -0
- package/dist/media/file-types.d.ts.map +1 -1
- package/dist/media/file-types.js +24 -4
- package/dist/media/image-transformer.d.ts +34 -0
- package/dist/media/image-transformer.d.ts.map +1 -0
- package/dist/media/image-transformer.js +3 -0
- package/dist/media/magic-bytes.d.ts.map +1 -1
- package/dist/media/magic-bytes.js +19 -9
- package/dist/media/media-transform.d.ts +92 -0
- package/dist/media/media-transform.d.ts.map +1 -0
- package/dist/media/media-transform.js +197 -0
- package/dist/media/media.repository.d.ts +5 -0
- package/dist/media/media.repository.d.ts.map +1 -1
- package/dist/media.repository.d.ts +61 -0
- package/dist/media.repository.d.ts.map +1 -0
- package/dist/media.repository.js +3 -0
- package/dist/notifications/notification.repository.d.ts +11 -2
- package/dist/notifications/notification.repository.d.ts.map +1 -1
- package/dist/oauth/authorization-code.repository.d.ts +10 -5
- package/dist/oauth/authorization-code.repository.d.ts.map +1 -1
- package/dist/oauth/role-guard.d.ts +30 -11
- package/dist/oauth/role-guard.d.ts.map +1 -1
- package/dist/oauth/role-guard.js +27 -5
- package/dist/oauth/token.repository.d.ts +2 -0
- package/dist/oauth/token.repository.d.ts.map +1 -1
- package/dist/policies.d.ts +10 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +33 -0
- package/dist/queue/queue.interface.d.ts +9 -1
- package/dist/queue/queue.interface.d.ts.map +1 -1
- package/dist/queue/queue.stub.d.ts +1 -1
- package/dist/queue/queue.stub.d.ts.map +1 -1
- package/dist/queue/queue.stub.js +2 -3
- package/dist/queue.interface.d.ts +39 -0
- package/dist/queue.interface.d.ts.map +1 -0
- package/dist/queue.interface.js +3 -0
- package/dist/queue.stub.d.ts +6 -0
- package/dist/queue.stub.d.ts.map +1 -0
- package/dist/queue.stub.js +8 -0
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts +7 -0
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -1
- package/dist/rate-limit/token-bucket-rate-limiter.js +66 -0
- package/dist/rbac/evaluate.d.ts +57 -0
- package/dist/rbac/evaluate.d.ts.map +1 -0
- package/dist/rbac/evaluate.js +113 -0
- package/dist/rbac/invitation.repository.d.ts +84 -0
- package/dist/rbac/invitation.repository.d.ts.map +1 -0
- package/dist/rbac/invitation.repository.js +3 -0
- package/dist/rbac/permissions.d.ts +39 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +43 -0
- package/dist/rbac/types.d.ts +130 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +4 -0
- package/dist/relations.d.ts +28 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +25 -0
- package/dist/richtext-render.d.ts +11 -0
- package/dist/richtext-render.d.ts.map +1 -0
- package/dist/richtext-render.js +87 -0
- package/dist/richtext.d.ts +11 -0
- package/dist/richtext.d.ts.map +1 -0
- package/dist/richtext.js +13 -0
- package/dist/scheduler.interface.d.ts +4 -0
- package/dist/scheduler.interface.d.ts.map +1 -0
- package/dist/scheduler.interface.js +3 -0
- package/dist/scheduler.stub.d.ts +5 -0
- package/dist/scheduler.stub.d.ts.map +1 -0
- package/dist/scheduler.stub.js +7 -0
- package/dist/schema/canonical.d.ts +15 -0
- package/dist/schema/canonical.d.ts.map +1 -0
- package/dist/schema/canonical.js +37 -0
- package/dist/schema/define.d.ts +42 -0
- package/dist/schema/define.d.ts.map +1 -0
- package/dist/schema/define.js +56 -0
- package/dist/schema/emit.d.ts +14 -0
- package/dist/schema/emit.d.ts.map +1 -0
- package/dist/schema/emit.js +43 -0
- package/dist/schema/index.d.ts +12 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +13 -0
- package/dist/schema/manifest-seeds.d.ts +12 -0
- package/dist/schema/manifest-seeds.d.ts.map +1 -0
- package/dist/schema/manifest-seeds.js +48 -0
- package/dist/schema/manifest-validation.d.ts +13 -0
- package/dist/schema/manifest-validation.d.ts.map +1 -0
- package/dist/schema/manifest-validation.js +41 -0
- package/dist/schema/manifest.types.d.ts +54 -0
- package/dist/schema/manifest.types.d.ts.map +1 -0
- package/dist/schema/manifest.types.js +7 -0
- package/dist/schema-mutator.d.ts +26 -0
- package/dist/schema-mutator.d.ts.map +1 -0
- package/dist/schema-mutator.js +3 -0
- package/dist/search/search.repository.d.ts +5 -4
- package/dist/search/search.repository.d.ts.map +1 -1
- package/dist/search/vector-extractor.d.ts.map +1 -1
- package/dist/search/vector-extractor.js +23 -2
- package/dist/search/vector.repository.d.ts +2 -1
- package/dist/search/vector.repository.d.ts.map +1 -1
- package/dist/security/time-trap-token.repository.d.ts +5 -3
- package/dist/security/time-trap-token.repository.d.ts.map +1 -1
- package/dist/seed-ddl-destructive.d.ts +40 -0
- package/dist/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/seed-ddl-destructive.js +149 -0
- package/dist/seed-ddl.d.ts +40 -0
- package/dist/seed-ddl.d.ts.map +1 -0
- package/dist/seed-ddl.js +100 -0
- package/dist/seed-layout.d.ts +145 -0
- package/dist/seed-layout.d.ts.map +1 -0
- package/dist/seed-layout.js +192 -0
- package/dist/seed-layout.repository.d.ts +22 -0
- package/dist/seed-layout.repository.d.ts.map +1 -0
- package/dist/seed-layout.repository.js +3 -0
- package/dist/seed-registry.d.ts +71 -0
- package/dist/seed-registry.d.ts.map +1 -0
- package/dist/seed-registry.js +143 -0
- package/dist/seed-types-generator.d.ts +1 -1
- package/dist/seed-types-generator.d.ts.map +1 -1
- package/dist/seed-validation.d.ts +23 -0
- package/dist/seed-validation.d.ts.map +1 -0
- package/dist/seed-validation.js +224 -0
- package/dist/seed.repository.d.ts +37 -0
- package/dist/seed.repository.d.ts.map +1 -0
- package/dist/seed.repository.js +3 -0
- package/dist/seeds.d.ts +20 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +21 -0
- package/dist/settings/site-settings.repository.d.ts +8 -0
- package/dist/settings/site-settings.repository.d.ts.map +1 -1
- package/dist/site-settings.repository.d.ts +16 -0
- package/dist/site-settings.repository.d.ts.map +1 -0
- package/dist/site-settings.repository.js +3 -0
- package/dist/slug-utils.d.ts +18 -0
- package/dist/slug-utils.d.ts.map +1 -0
- package/dist/slug-utils.js +32 -0
- package/dist/storage.d.ts +46 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +3 -0
- package/dist/transfer/csv.d.ts +34 -0
- package/dist/transfer/csv.d.ts.map +1 -0
- package/dist/transfer/csv.js +135 -0
- package/dist/transfer/flat-seed.d.ts +13 -0
- package/dist/transfer/flat-seed.d.ts.map +1 -0
- package/dist/transfer/flat-seed.js +50 -0
- package/dist/transfer/index.d.ts +7 -0
- package/dist/transfer/index.d.ts.map +1 -0
- package/dist/transfer/index.js +8 -0
- package/dist/transfer/ndjson.d.ts +25 -0
- package/dist/transfer/ndjson.d.ts.map +1 -0
- package/dist/transfer/ndjson.js +62 -0
- package/dist/transfer/row-mapping.d.ts +36 -0
- package/dist/transfer/row-mapping.d.ts.map +1 -0
- package/dist/transfer/row-mapping.js +97 -0
- package/dist/transfer/transfer.constants.d.ts +20 -0
- package/dist/transfer/transfer.constants.d.ts.map +1 -0
- package/dist/transfer/transfer.constants.js +21 -0
- package/dist/transfer/transfer.types.d.ts +46 -0
- package/dist/transfer/transfer.types.d.ts.map +1 -0
- package/dist/transfer/transfer.types.js +6 -0
- package/dist/types.d.ts +243 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +50 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +660 -0
- package/dist/webhook-validation.d.ts +3 -0
- package/dist/webhook-validation.d.ts.map +1 -0
- package/dist/webhook-validation.js +6 -0
- package/package.json +11 -6
- package/dist/.tsbuildinfo +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { cleanString, isPlainObject } from './validation/primitives.js';
|
|
4
|
+
/**
|
|
5
|
+
* Locale code grammar (v1): ISO 639 language (2–3 lowercase letters), optionally followed by an ISO 3166
|
|
6
|
+
* region (2 uppercase letters) or a UN M.49 area (3 digits) — `it`, `en`, `pt-BR`, `es-419`. Script
|
|
7
|
+
* subtags (`zh-Hant`) are outside v1. The grammar doubles as an injection guard: validated codes are
|
|
8
|
+
* later interpolated into SQLite JSON paths.
|
|
9
|
+
*/
|
|
10
|
+
export const LOCALE_CODE_RE = /^[a-z]{2,3}(?:-(?:[A-Z]{2}|[0-9]{3}))?$/;
|
|
11
|
+
/** Branch types that may carry `localized: true`. */
|
|
12
|
+
export const LOCALIZABLE_BRANCH_TYPES = new Set(['text', 'richtext', 'json']);
|
|
13
|
+
/** True when `value` is a string matching {@link LOCALE_CODE_RE}. */
|
|
14
|
+
export function isLocaleCode(value) {
|
|
15
|
+
return typeof value === 'string' && LOCALE_CODE_RE.test(value);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True when the branch is localized AND its type supports it. A malformed definition that slipped past
|
|
19
|
+
* seed validation (e.g. `localized: true` on a `number`) is treated as not localized, never as a dictionary.
|
|
20
|
+
*/
|
|
21
|
+
export function isLocalizedBranch(branch) {
|
|
22
|
+
return branch.localized === true && LOCALIZABLE_BRANCH_TYPES.has(branch.type);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Structural check used on READ paths (no config available): a non-empty plain object whose every key
|
|
26
|
+
* matches the locale-code grammar. A richtext envelope (`schemaVersion`, `doc`) and a TipTap doc
|
|
27
|
+
* (`type`, `content`) never match.
|
|
28
|
+
*/
|
|
29
|
+
export function isLocaleDictionary(value) {
|
|
30
|
+
if (!isPlainObject(value))
|
|
31
|
+
return false;
|
|
32
|
+
const keys = Object.keys(value);
|
|
33
|
+
return keys.length > 0 && keys.every(isLocaleCode);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Stricter check used on WRITE paths: a locale dictionary with at least one REGISTERED locale key.
|
|
37
|
+
* Without the registration requirement a legitimate json value such as `{"url": "…", "alt": "…"}` —
|
|
38
|
+
* whose keys happen to match the grammar — would be read as a dictionary of unregistered locales and
|
|
39
|
+
* silently emptied.
|
|
40
|
+
*/
|
|
41
|
+
export function isLocalizedWriteDictionary(value, config) {
|
|
42
|
+
return isLocaleDictionary(value) && Object.keys(value).some((key) => config.locales.includes(key));
|
|
43
|
+
}
|
|
44
|
+
function isBlankLocaleValue(value) {
|
|
45
|
+
return value === null || value === undefined || (typeof value === 'string' && cleanString(value) === '');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalises any accepted write value of a localized branch into a {@link LocalizedPatch}:
|
|
49
|
+
* - a write dictionary keeps its registered locales, in `config.locales` order; unregistered locale
|
|
50
|
+
* keys are dropped;
|
|
51
|
+
* - any other value (string, richtext doc, non-dictionary json) is the default-locale value;
|
|
52
|
+
* - `null`, `undefined` and blank strings become `null` ("clear this locale").
|
|
53
|
+
* Idempotent: a patch passed back in yields the same patch.
|
|
54
|
+
*/
|
|
55
|
+
export function toLocalizedPatch(value, config) {
|
|
56
|
+
const source = isLocalizedWriteDictionary(value, config)
|
|
57
|
+
? value
|
|
58
|
+
: { [config.defaultLocale]: value };
|
|
59
|
+
const patch = {};
|
|
60
|
+
for (const locale of config.locales) {
|
|
61
|
+
if (!Object.hasOwn(source, locale))
|
|
62
|
+
continue;
|
|
63
|
+
patch[locale] = isBlankLocaleValue(source[locale]) ? null : source[locale];
|
|
64
|
+
}
|
|
65
|
+
return patch;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Removes `null` / `undefined` / blank-string entries. Returns `null` when nothing remains, so an
|
|
69
|
+
* all-cleared dictionary is stored as SQL NULL rather than `{}`. Keys are otherwise preserved as-is —
|
|
70
|
+
* including locales no longer registered, which must never be dropped by a write (brief §2, no data loss).
|
|
71
|
+
*/
|
|
72
|
+
export function compactLocalizedDictionary(value) {
|
|
73
|
+
const compact = {};
|
|
74
|
+
for (const [locale, localeValue] of Object.entries(value)) {
|
|
75
|
+
if (isBlankLocaleValue(localeValue))
|
|
76
|
+
continue;
|
|
77
|
+
compact[locale] = localeValue;
|
|
78
|
+
}
|
|
79
|
+
return Object.keys(compact).length > 0 ? compact : null;
|
|
80
|
+
}
|
|
81
|
+
/** Content locale used when neither stored `locales` nor a valid `defaultLanguage` is available. */
|
|
82
|
+
const FALLBACK_LOCALE = 'en';
|
|
83
|
+
/**
|
|
84
|
+
* Builds the project's {@link LocaleConfig}, repairing whatever a hand-edited settings row could break so
|
|
85
|
+
* the LocaleConfig invariant always holds:
|
|
86
|
+
* - `locales`: the stored list minus invalid codes and duplicates; when nothing remains (never configured,
|
|
87
|
+
* `[]`, corrupt), the implicit single-language config `[defaultLanguage]` (brief §2), which keeps the
|
|
88
|
+
* feature invisible to a mono-lingual project;
|
|
89
|
+
* - `defaultLocale`: the stored value when it belongs to `locales`, else `locales[0]`.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveLocaleConfig(settings) {
|
|
92
|
+
const stored = [...new Set((settings.locales ?? []).filter(isLocaleCode))];
|
|
93
|
+
const implicit = isLocaleCode(settings.defaultLanguage) ? settings.defaultLanguage : FALLBACK_LOCALE;
|
|
94
|
+
const locales = stored.length > 0 ? stored : [implicit];
|
|
95
|
+
const defaultLocale = settings.defaultLocale && locales.includes(settings.defaultLocale)
|
|
96
|
+
? settings.defaultLocale
|
|
97
|
+
: locales[0];
|
|
98
|
+
return { locales, defaultLocale };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* True when a STORED value of a localized branch is a locale dictionary. Text and richtext cannot hold an
|
|
102
|
+
* object keyed only by locale codes for any other reason; json can (`{"url": "…", "alt": "…"}`), so a json
|
|
103
|
+
* object counts only when at least one key is a registered locale — otherwise it is a legacy value written
|
|
104
|
+
* before the branch became localized.
|
|
105
|
+
*/
|
|
106
|
+
function isStoredLocaleDictionary(branch, value, config) {
|
|
107
|
+
if (!isLocalizedBranch(branch) || !isLocaleDictionary(value))
|
|
108
|
+
return false;
|
|
109
|
+
return branch.type !== 'json' || Object.keys(value).some((key) => config.locales.includes(key));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Merges a write into the value stored for a localized branch. The write path's only way to persist a
|
|
113
|
+
* localized value, so a translation the write does not mention is never dropped (brief §2):
|
|
114
|
+
* - `write === null` clears the whole field (an explicit null keeps its pre-localization meaning);
|
|
115
|
+
* - otherwise the write is normalised with {@link toLocalizedPatch}: every registered locale it names
|
|
116
|
+
* overwrites that locale, a `null` entry clears that locale only, and every other stored locale —
|
|
117
|
+
* including ones no longer registered — is kept as-is;
|
|
118
|
+
* - a stored legacy value (written before the branch became localized) is the default-locale value.
|
|
119
|
+
* @returns The compacted dictionary, or `null` when no locale keeps a value.
|
|
120
|
+
*/
|
|
121
|
+
export function applyLocalizedPatch(branch, stored, write, config) {
|
|
122
|
+
if (write === null)
|
|
123
|
+
return null;
|
|
124
|
+
const merged = isStoredLocaleDictionary(branch, stored, config)
|
|
125
|
+
? { ...stored }
|
|
126
|
+
: isBlankLocaleValue(stored) ? {} : { [config.defaultLocale]: stored };
|
|
127
|
+
Object.assign(merged, toLocalizedPatch(write, config));
|
|
128
|
+
return compactLocalizedDictionary(merged);
|
|
129
|
+
}
|
|
130
|
+
/** Aliases of the localized branches a write carries — the fields it must merge rather than replace. */
|
|
131
|
+
export function localizedAliasesIn(seed, data) {
|
|
132
|
+
return seed.branches
|
|
133
|
+
.filter((branch) => isLocalizedBranch(branch) && Object.hasOwn(data, branch.alias) && data[branch.alias] !== undefined)
|
|
134
|
+
.map((branch) => branch.alias);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Returns `data` with every localized branch it carries merged into `stored` via
|
|
138
|
+
* {@link applyLocalizedPatch}; other keys pass through untouched. `stored` is the entry's current values
|
|
139
|
+
* (`null` on create, which only compacts). Returns `data` itself when `config` is undefined or no localized
|
|
140
|
+
* branch is written.
|
|
141
|
+
*/
|
|
142
|
+
export function mergeLocalizedFields(seed, stored, data, config) {
|
|
143
|
+
if (!config)
|
|
144
|
+
return data;
|
|
145
|
+
const aliases = localizedAliasesIn(seed, data);
|
|
146
|
+
if (aliases.length === 0)
|
|
147
|
+
return data;
|
|
148
|
+
const merged = { ...data };
|
|
149
|
+
for (const branch of seed.branches) {
|
|
150
|
+
if (!aliases.includes(branch.alias))
|
|
151
|
+
continue;
|
|
152
|
+
merged[branch.alias] = applyLocalizedPatch(branch, stored?.[branch.alias], data[branch.alias], config);
|
|
153
|
+
}
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolves a stored localized value to one language: the requested locale, then the default locale, then the
|
|
158
|
+
* first stored translation (stored key order), then `null`. A legacy value that is not a dictionary is returned
|
|
159
|
+
* as-is. Non-localized branches return `value` unchanged. `buildSelectQuery` applies the same chain in SQL, so
|
|
160
|
+
* filters and ORDER BY compare exactly what a reader sees.
|
|
161
|
+
*/
|
|
162
|
+
export function resolveLocalizedValue(branch, value, locale, config) {
|
|
163
|
+
if (!isStoredLocaleDictionary(branch, value, config))
|
|
164
|
+
return value;
|
|
165
|
+
if (!isBlankLocaleValue(value[locale]))
|
|
166
|
+
return value[locale];
|
|
167
|
+
if (!isBlankLocaleValue(value[config.defaultLocale]))
|
|
168
|
+
return value[config.defaultLocale];
|
|
169
|
+
// An implicit config follows `defaultLanguage`; changing it before languages are configured leaves every
|
|
170
|
+
// dictionary without its default key. Returning null there would blank the public site (brief §2).
|
|
171
|
+
const firstStored = Object.values(value).find((translation) => !isBlankLocaleValue(translation));
|
|
172
|
+
return firstStored ?? null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Returns a copy of `data` with every localized branch resolved to `locale` (default: the default locale)
|
|
176
|
+
* via {@link resolveLocalizedValue}. Returns `data` itself when `config` is undefined.
|
|
177
|
+
*/
|
|
178
|
+
export function resolveLocalizedFields(seed, data, config, locale) {
|
|
179
|
+
if (!config)
|
|
180
|
+
return data;
|
|
181
|
+
const target = locale ?? config.defaultLocale;
|
|
182
|
+
const resolved = { ...data };
|
|
183
|
+
for (const branch of seed.branches) {
|
|
184
|
+
if (isLocalizedBranch(branch) && Object.hasOwn(resolved, branch.alias)) {
|
|
185
|
+
resolved[branch.alias] = resolveLocalizedValue(branch, resolved[branch.alias], target, config);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return resolved;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The whole dictionary of a stored localized value, for readers asking every language (`?lang=all`):
|
|
192
|
+
* a stored dictionary as-is — unregistered locales included, nothing is hidden (brief §2) — a legacy
|
|
193
|
+
* value as `{ [defaultLocale]: value }` (the write path's reading of it), a blank value as `null`.
|
|
194
|
+
* Non-localized branches return `value` unchanged.
|
|
195
|
+
*/
|
|
196
|
+
export function asLocaleDictionary(branch, value, config) {
|
|
197
|
+
if (!isLocalizedBranch(branch))
|
|
198
|
+
return value;
|
|
199
|
+
if (isStoredLocaleDictionary(branch, value, config))
|
|
200
|
+
return value;
|
|
201
|
+
return isBlankLocaleValue(value) ? null : { [config.defaultLocale]: value };
|
|
202
|
+
}
|
|
203
|
+
/** Returns a copy of `data` with every localized branch it carries passed through {@link asLocaleDictionary}. */
|
|
204
|
+
export function asLocaleDictionaries(seed, data, config) {
|
|
205
|
+
const expanded = { ...data };
|
|
206
|
+
for (const branch of seed.branches) {
|
|
207
|
+
if (isLocalizedBranch(branch) && Object.hasOwn(expanded, branch.alias)) {
|
|
208
|
+
expanded[branch.alias] = asLocaleDictionary(branch, expanded[branch.alias], config);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return expanded;
|
|
212
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ActorContext, Branch, DataClassification, Seed } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes an un-salted SHA-256 hex digest of a string value.
|
|
4
|
+
* Used for basic digest operations.
|
|
5
|
+
* @param value - The raw string value to hash.
|
|
6
|
+
* @returns A Promise resolving to a 64-character hex string.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sha256hex(value: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Verifies if a candidate string matches a stored SHA-256 hash digest, comparing in constant time.
|
|
11
|
+
* @param stored - The expected 64-character SHA-256 hex digest.
|
|
12
|
+
* @param candidate - The raw candidate string to verify.
|
|
13
|
+
* @returns A Promise resolving to true if candidate matches stored digest, false otherwise.
|
|
14
|
+
*/
|
|
15
|
+
export declare function verifyHashField(stored: string, candidate: string): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolved classification rules for a branch, bundling storage strategy and API visibility defaults.
|
|
18
|
+
*/
|
|
19
|
+
export interface ResolvedClassification {
|
|
20
|
+
/** Canonical data classification tier. */
|
|
21
|
+
classification: DataClassification;
|
|
22
|
+
/** Storage mechanism at rest (`plain`, `encrypt` via AES-GCM, or `hash` via HMAC-SHA256). */
|
|
23
|
+
storage: 'plain' | 'encrypt' | 'hash';
|
|
24
|
+
/** Default visibility rule for unauthenticated public API endpoints (`full` or `hidden`). */
|
|
25
|
+
publicVisibility: 'full' | 'hidden';
|
|
26
|
+
/** Default visibility rule for authenticated API endpoints (`full` or `hidden`). */
|
|
27
|
+
authVisibility: 'full' | 'hidden';
|
|
28
|
+
}
|
|
29
|
+
/** Normalizes raw policy strings to canonical DataClassification enum. */
|
|
30
|
+
export declare function normalizeClassification(raw?: string): DataClassification;
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the 4-tier DataClassification for a branch, bundling its storage
|
|
33
|
+
* strategy at rest and its API visibility rules (public vs auth context).
|
|
34
|
+
* @param branch - The seed branch definition to inspect.
|
|
35
|
+
* @returns The resolved classification details and visibility rules.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveClassification(branch: Branch): ResolvedClassification;
|
|
38
|
+
/**
|
|
39
|
+
* Resolves branch policies applying all defaults and classification rules.
|
|
40
|
+
* @param branch - The seed branch definition to inspect.
|
|
41
|
+
* @returns Complete required policies object for the branch.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolvePolicies(branch: Branch): Required<NonNullable<Branch['policies']>>;
|
|
44
|
+
/**
|
|
45
|
+
* Filters entry payload fields based on the caller's {@link ActorContext} and branch classification tier.
|
|
46
|
+
*
|
|
47
|
+
* Rules:
|
|
48
|
+
* - Public actor: receives `public` fields; `internal`, `confidential`, and `restricted` fields are omitted.
|
|
49
|
+
* - Authenticated actor: receives `public`, `internal`, and `confidential` fields; `restricted` fields are ALWAYS omitted.
|
|
50
|
+
* - System actor: receives all fields including `restricted` (used for internal worker/automation orchestration).
|
|
51
|
+
*
|
|
52
|
+
* @param data - Raw record fields object.
|
|
53
|
+
* @param seed - Content type seed definition containing branch definitions.
|
|
54
|
+
* @param actor - Context of the caller requesting the entry payload (defaults to authenticated).
|
|
55
|
+
* @returns Filtered data record containing only authorized fields for the given actor context.
|
|
56
|
+
*/
|
|
57
|
+
export declare function filterEntryForActor(data: Record<string, unknown>, seed: Seed, actor?: ActorContext): Record<string, unknown>;
|
|
58
|
+
//# sourceMappingURL=policies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../../../src/engine/privacy/policies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAIjF;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK9D;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzF;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,cAAc,EAAE,kBAAkB,CAAA;IAClC,6FAA6F;IAC7F,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;IACrC,6FAA6F;IAC7F,gBAAgB,EAAE,MAAM,GAAG,QAAQ,CAAA;IACnC,oFAAoF;IACpF,cAAc,EAAE,MAAM,GAAG,QAAQ,CAAA;CAClC;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAQxE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,CAmC5E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CA+BzF;AAuBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,EAAE,IAAI,EACV,KAAK,GAAE,YAAwC,GAC9C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiDzB"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { isLocaleDictionary, isLocalizedBranch } from '../localization/localization.js';
|
|
4
|
+
import { timingSafeEqual } from '../../webhooks/webhook-crypto.js';
|
|
5
|
+
/**
|
|
6
|
+
* Computes an un-salted SHA-256 hex digest of a string value.
|
|
7
|
+
* Used for basic digest operations.
|
|
8
|
+
* @param value - The raw string value to hash.
|
|
9
|
+
* @returns A Promise resolving to a 64-character hex string.
|
|
10
|
+
*/
|
|
11
|
+
export async function sha256hex(value) {
|
|
12
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
13
|
+
return Array.from(new Uint8Array(buf))
|
|
14
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
15
|
+
.join('');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Verifies if a candidate string matches a stored SHA-256 hash digest, comparing in constant time.
|
|
19
|
+
* @param stored - The expected 64-character SHA-256 hex digest.
|
|
20
|
+
* @param candidate - The raw candidate string to verify.
|
|
21
|
+
* @returns A Promise resolving to true if candidate matches stored digest, false otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export async function verifyHashField(stored, candidate) {
|
|
24
|
+
return timingSafeEqual(stored, await sha256hex(candidate));
|
|
25
|
+
}
|
|
26
|
+
/** Normalizes raw policy strings to canonical DataClassification enum. */
|
|
27
|
+
export function normalizeClassification(raw) {
|
|
28
|
+
if (!raw)
|
|
29
|
+
return 'public';
|
|
30
|
+
const lower = raw.toLowerCase();
|
|
31
|
+
if (lower === 'confidential')
|
|
32
|
+
return 'confidential';
|
|
33
|
+
if (lower === 'restricted' || lower === 'hash')
|
|
34
|
+
return 'restricted';
|
|
35
|
+
if (lower === 'encrypt')
|
|
36
|
+
return 'confidential';
|
|
37
|
+
if (lower === 'internal')
|
|
38
|
+
return 'internal';
|
|
39
|
+
return 'public';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the 4-tier DataClassification for a branch, bundling its storage
|
|
43
|
+
* strategy at rest and its API visibility rules (public vs auth context).
|
|
44
|
+
* @param branch - The seed branch definition to inspect.
|
|
45
|
+
* @returns The resolved classification details and visibility rules.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveClassification(branch) {
|
|
48
|
+
const rawClassification = branch.policies?.classification ?? branch.policies?.privacy;
|
|
49
|
+
const classification = normalizeClassification(rawClassification);
|
|
50
|
+
switch (classification) {
|
|
51
|
+
case 'confidential':
|
|
52
|
+
return {
|
|
53
|
+
classification: 'confidential',
|
|
54
|
+
storage: 'encrypt',
|
|
55
|
+
publicVisibility: 'hidden',
|
|
56
|
+
authVisibility: 'full',
|
|
57
|
+
};
|
|
58
|
+
case 'restricted':
|
|
59
|
+
return {
|
|
60
|
+
classification: 'restricted',
|
|
61
|
+
storage: 'hash',
|
|
62
|
+
publicVisibility: 'hidden',
|
|
63
|
+
authVisibility: 'hidden',
|
|
64
|
+
};
|
|
65
|
+
case 'internal':
|
|
66
|
+
return {
|
|
67
|
+
classification: 'internal',
|
|
68
|
+
storage: 'plain',
|
|
69
|
+
publicVisibility: 'hidden',
|
|
70
|
+
authVisibility: 'full',
|
|
71
|
+
};
|
|
72
|
+
case 'public':
|
|
73
|
+
default:
|
|
74
|
+
return {
|
|
75
|
+
classification: 'public',
|
|
76
|
+
storage: 'plain',
|
|
77
|
+
publicVisibility: 'full',
|
|
78
|
+
authVisibility: 'full',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolves branch policies applying all defaults and classification rules.
|
|
84
|
+
* @param branch - The seed branch definition to inspect.
|
|
85
|
+
* @returns Complete required policies object for the branch.
|
|
86
|
+
*/
|
|
87
|
+
export function resolvePolicies(branch) {
|
|
88
|
+
const resolved = resolveClassification(branch);
|
|
89
|
+
const isRepeater = branch.type === 'repeater';
|
|
90
|
+
const isEncryptedOrHashed = resolved.storage !== 'plain';
|
|
91
|
+
const defaultVisibility = resolved.authVisibility;
|
|
92
|
+
const isPublicAllowed = branch.policies?.public ?? (resolved.publicVisibility === 'full');
|
|
93
|
+
const visibility = branch.policies?.visibility ?? defaultVisibility;
|
|
94
|
+
// Concealed values must not be queryable: filter/sort/search leak them
|
|
95
|
+
const isConcealed = visibility === 'hidden' || visibility === 'masked';
|
|
96
|
+
const defaultFilter = branch.policies?.filter ?? (isRepeater || resolved.storage === 'hash' ? false : !isConcealed);
|
|
97
|
+
const defaultSort = branch.policies?.sort ?? (isRepeater || isEncryptedOrHashed ? false : !isConcealed);
|
|
98
|
+
const defaultSearch = branch.policies?.search ?? (isRepeater || isEncryptedOrHashed ? false : !isConcealed);
|
|
99
|
+
// publicEdit default: true for public fields (unless public: false), false for confidential/internal/restricted
|
|
100
|
+
const defaultPublicEdit = resolved.classification === 'public'
|
|
101
|
+
? (branch.policies?.publicEdit ?? isPublicAllowed)
|
|
102
|
+
: (branch.policies?.publicEdit ?? false);
|
|
103
|
+
return {
|
|
104
|
+
classification: resolved.classification,
|
|
105
|
+
privacy: resolved.storage,
|
|
106
|
+
visibility,
|
|
107
|
+
search: defaultSearch,
|
|
108
|
+
filter: defaultFilter,
|
|
109
|
+
sort: defaultSort,
|
|
110
|
+
public: isPublicAllowed,
|
|
111
|
+
publicEdit: defaultPublicEdit,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const MASK = '••••••••';
|
|
115
|
+
/**
|
|
116
|
+
* The masked form of a value. A localized dictionary (a `?lang=all` read) masks each translation, exactly as
|
|
117
|
+
* a single-language read masks the one translation it resolves to; before this, any object became `null`.
|
|
118
|
+
*/
|
|
119
|
+
function maskValue(branch, value) {
|
|
120
|
+
if (typeof value === 'string')
|
|
121
|
+
return value.length > 0 ? MASK : null;
|
|
122
|
+
if (isLocalizedBranch(branch) && isLocaleDictionary(value)) {
|
|
123
|
+
return Object.fromEntries(Object.entries(value).map(([locale, translation]) => [
|
|
124
|
+
locale,
|
|
125
|
+
typeof translation === 'string' && translation.length > 0 ? MASK : null,
|
|
126
|
+
]));
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const SYSTEM_FIELDS = new Set(['id', 'slug', 'status', 'created_at', 'updated_at', 'version', 'has_pending_draft']);
|
|
131
|
+
/**
|
|
132
|
+
* Filters entry payload fields based on the caller's {@link ActorContext} and branch classification tier.
|
|
133
|
+
*
|
|
134
|
+
* Rules:
|
|
135
|
+
* - Public actor: receives `public` fields; `internal`, `confidential`, and `restricted` fields are omitted.
|
|
136
|
+
* - Authenticated actor: receives `public`, `internal`, and `confidential` fields; `restricted` fields are ALWAYS omitted.
|
|
137
|
+
* - System actor: receives all fields including `restricted` (used for internal worker/automation orchestration).
|
|
138
|
+
*
|
|
139
|
+
* @param data - Raw record fields object.
|
|
140
|
+
* @param seed - Content type seed definition containing branch definitions.
|
|
141
|
+
* @param actor - Context of the caller requesting the entry payload (defaults to authenticated).
|
|
142
|
+
* @returns Filtered data record containing only authorized fields for the given actor context.
|
|
143
|
+
*/
|
|
144
|
+
export function filterEntryForActor(data, seed, actor = { type: 'authenticated' }) {
|
|
145
|
+
const result = {};
|
|
146
|
+
for (const [key, value] of Object.entries(data)) {
|
|
147
|
+
if (SYSTEM_FIELDS.has(key)) {
|
|
148
|
+
result[key] = value;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const branch = seed.branches.find((b) => b.alias === key);
|
|
152
|
+
if (!branch) {
|
|
153
|
+
// Pass through unknown/extra fields unless actor is public
|
|
154
|
+
if (actor.type !== 'public') {
|
|
155
|
+
result[key] = value;
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const resolved = resolveClassification(branch);
|
|
160
|
+
const isPublicActor = actor.type === 'public';
|
|
161
|
+
const isSystemActor = actor.type === 'system';
|
|
162
|
+
// System actor sees all fields including restricted
|
|
163
|
+
if (isSystemActor) {
|
|
164
|
+
result[key] = value;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Public actor check
|
|
168
|
+
if (isPublicActor) {
|
|
169
|
+
if (resolved.publicVisibility === 'hidden')
|
|
170
|
+
continue;
|
|
171
|
+
if (branch.policies?.public === false)
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Authenticated actor check
|
|
176
|
+
if (resolved.authVisibility === 'hidden')
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Explicit visibility override check
|
|
180
|
+
if (branch.policies?.visibility === 'hidden')
|
|
181
|
+
continue;
|
|
182
|
+
// Masking rule check
|
|
183
|
+
if (branch.policies?.visibility === 'masked') {
|
|
184
|
+
result[key] = maskValue(branch, value);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
result[key] = value;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract for the Application-Level Encryption (ALE) and hashing privacy service.
|
|
3
|
+
* Used by the Botanical Engine to secure fields classified as `confidential` or `restricted`.
|
|
4
|
+
*/
|
|
5
|
+
export interface IPrivacyService {
|
|
6
|
+
/**
|
|
7
|
+
* Symmetrically encrypts a plaintext string using AES-256-GCM.
|
|
8
|
+
* @param plaintext - The raw string value to encrypt.
|
|
9
|
+
* @returns A Promise resolving to formatted ciphertext (`v1:<iv_base64>:<ciphertext_base64>`).
|
|
10
|
+
*/
|
|
11
|
+
encrypt(plaintext: string): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Decrypts a formatted AES-256-GCM ciphertext payload.
|
|
14
|
+
* @param ciphertext - The formatted ciphertext string (`v1:<iv_base64>:<ciphertext_base64>`).
|
|
15
|
+
* @returns A Promise resolving to the original plaintext string.
|
|
16
|
+
* @throws Error if the ciphertext format is invalid or decryption fails (authentication tag mismatch).
|
|
17
|
+
*/
|
|
18
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Computes a deterministic HMAC SHA-256 digest of a plaintext string.
|
|
21
|
+
* Used for blind-indexing and one-way field digests (`restricted` classification).
|
|
22
|
+
* @param plaintext - The raw string value to hash.
|
|
23
|
+
* @returns A Promise resolving to a 64-character hexadecimal digest string.
|
|
24
|
+
*/
|
|
25
|
+
hash(plaintext: string): Promise<string>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Concrete Edge-native implementation of {@link IPrivacyService}.
|
|
29
|
+
* Employs Web Crypto API (`crypto.subtle`) for zero-dependency execution in Cloudflare Workers.
|
|
30
|
+
*/
|
|
31
|
+
export declare class PrivacyService implements IPrivacyService {
|
|
32
|
+
private readonly masterKey;
|
|
33
|
+
private aesKeyPromise?;
|
|
34
|
+
private hmacKeyPromise?;
|
|
35
|
+
/**
|
|
36
|
+
* Initializes the PrivacyService with a Master Encryption Key.
|
|
37
|
+
* @param masterKey - Secret key used to derive cryptographic keys for AES-GCM and HMAC.
|
|
38
|
+
* @throws Error if `masterKey` is empty or undefined.
|
|
39
|
+
*/
|
|
40
|
+
constructor(masterKey: string);
|
|
41
|
+
/**
|
|
42
|
+
* Lazy-loads and caches the derived AES-256-GCM {@link CryptoKey}.
|
|
43
|
+
* Derives a 256-bit key from `masterKey` via SHA-256 digest.
|
|
44
|
+
*/
|
|
45
|
+
private getAesKey;
|
|
46
|
+
/**
|
|
47
|
+
* Lazy-loads and caches the derived HMAC SHA-256 {@link CryptoKey}.
|
|
48
|
+
*/
|
|
49
|
+
private getHmacKey;
|
|
50
|
+
/**
|
|
51
|
+
* Encrypts a plaintext string with AES-256-GCM and a fresh 96-bit (12-byte) IV.
|
|
52
|
+
* Output is formatted as `v1:<iv_base64>:<ciphertext_base64>`.
|
|
53
|
+
*/
|
|
54
|
+
encrypt(plaintext: string): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Decrypts a `v1:<iv_base64>:<ciphertext_base64>` string back to its plaintext value.
|
|
57
|
+
* If the input string is not prefixed with `v1:`, returns the raw string as unencrypted legacy data.
|
|
58
|
+
*/
|
|
59
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Computes a deterministic HMAC SHA-256 hex digest for blind index generation.
|
|
62
|
+
*/
|
|
63
|
+
hash(plaintext: string): Promise<string>;
|
|
64
|
+
}
|
|
65
|
+
/** Thrown when a protected field is processed but no master key is configured. */
|
|
66
|
+
export declare class PrivacyNotConfiguredError extends Error {
|
|
67
|
+
constructor();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Fail-closed {@link IPrivacyService} for installs without a master key.
|
|
71
|
+
* Never returns plaintext as protected output; legacy non-`v1:` values still read through.
|
|
72
|
+
*/
|
|
73
|
+
export declare class UnconfiguredPrivacyService implements IPrivacyService {
|
|
74
|
+
encrypt(_plaintext: string): Promise<string>;
|
|
75
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
76
|
+
hash(_plaintext: string): Promise<string>;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=privacy.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privacy.service.d.ts","sourceRoot":"","sources":["../../../src/engine/privacy/privacy.service.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE3C;;;;;OAKG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE5C;;;;;OAKG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACzC;AA4BD;;;GAGG;AACH,qBAAa,cAAe,YAAW,eAAe;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,aAAa,CAAC,CAAoB;IAC1C,OAAO,CAAC,cAAc,CAAC,CAAoB;IAE3C;;;;OAIG;gBACS,SAAS,EAAE,MAAM;IAO7B;;;OAGG;YACW,SAAS;IAmBvB;;OAEG;YACW,UAAU;IAexB;;;OAGG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAcjD;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBlD;;OAEG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAa/C;AAED,kFAAkF;AAClF,qBAAa,yBAA0B,SAAQ,KAAK;;CAKnD;AAED;;;GAGG;AACH,qBAAa,0BAA2B,YAAW,eAAe;IAC1D,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI5C,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK5C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAGhD"}
|