From ec162432eaa296e4bde3c3cf9eb042eb86828647 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 16:25:17 +0000 Subject: [PATCH] refactor(spec): remove dead compliance subsystems (ADR-0056 D8) + mark agent visibility EXPERIMENTAL (#1901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0056 D8 said "design + enforce, or remove" for the declared-but-never- enforced surface. Dispositions: REMOVED (no runtime path ever read them; compliance-grade config must never merely look live): - system/compliance.zod.ts (GDPR/HIPAA/PCI ComplianceConfig) + tests - system/masking.zod.ts (MaskingRule/MaskingConfig) + tests — FLS is the enforced field-visibility path; a masking/deny layer arrives with ADR-0066 ⑦/⑧ if needed - security/rls.zod.ts: RLSConfigSchema / RLSAuditEventSchema / RLSAuditConfigSchema — the enforced RLS path (computeRlsFilter) never read the global config; per-policy RowLevelSecurityPolicySchema is unchanged KEPT [EXPERIMENTAL]: system/encryption.zod.ts — real enterprise roadmap item with a stable shape; carrying it marked costs less than remove-and-re-add (ADR-0087). MARKED [EXPERIMENTAL — NOT ENFORCED] (#1901): AgentSchema.visibility — the chat-access evaluator deliberately excludes it and the list route does not filter by it, so `private` does not hide an agent. Schema description + the authoring form now say so; access/permissions (enforced since #1884) are the real gates. Ledger: authz-conformance matrix rows flipped (3× removed with disposition notes, field-encryption note records the keep decision, new agent-visibility experimental row). Generated reference docs regenerated (compliance/masking pages deleted); hand-written security/types/authorization docs synced. Tests: spec 6659 passed (246 files) after removal; api-surface regenerated; liveness ✓; conformance-matrix companion test ✓; full turbo build green. Refs #2561, ADR-0056 D8, #1901 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0187GeNqezxV6g5jiLiggfbt --- .../remove-dead-compliance-subsystems.md | 36 ++ content/docs/permissions/authorization.mdx | 2 +- content/docs/protocol/objectql/security.mdx | 77 +--- content/docs/protocol/objectql/types.mdx | 7 +- content/docs/references/ai/agent.mdx | 2 +- content/docs/references/cloud/app-store.mdx | 7 +- .../references/cloud/developer-portal.mdx | 7 +- content/docs/references/cloud/marketplace.mdx | 18 +- content/docs/references/data/field.mdx | 18 +- content/docs/references/data/object.mdx | 46 +- content/docs/references/data/seed-loader.mdx | 21 +- .../references/kernel/execution-context.mdx | 1 + .../references/kernel/package-upgrade.mdx | 21 +- content/docs/references/security/rls.mdx | 62 +-- content/docs/references/system/compliance.mdx | 184 -------- content/docs/references/system/index.mdx | 2 - content/docs/references/system/masking.mdx | 76 ---- content/docs/references/system/meta.json | 2 - .../dogfood/test/authz-conformance.matrix.ts | 16 +- packages/spec/api-surface.json | 35 -- packages/spec/src/ai/agent.form.ts | 2 +- packages/spec/src/ai/agent.zod.ts | 9 +- packages/spec/src/security/rls.test.ts | 218 ---------- packages/spec/src/security/rls.zod.ts | 206 +-------- packages/spec/src/system/compliance.test.ts | 407 ------------------ packages/spec/src/system/compliance.zod.ts | 297 ------------- packages/spec/src/system/index.ts | 8 +- packages/spec/src/system/masking.test.ts | 96 ----- packages/spec/src/system/masking.zod.ts | 46 -- 29 files changed, 183 insertions(+), 1746 deletions(-) create mode 100644 .changeset/remove-dead-compliance-subsystems.md delete mode 100644 content/docs/references/system/compliance.mdx delete mode 100644 content/docs/references/system/masking.mdx delete mode 100644 packages/spec/src/system/compliance.test.ts delete mode 100644 packages/spec/src/system/compliance.zod.ts delete mode 100644 packages/spec/src/system/masking.test.ts delete mode 100644 packages/spec/src/system/masking.zod.ts diff --git a/.changeset/remove-dead-compliance-subsystems.md b/.changeset/remove-dead-compliance-subsystems.md new file mode 100644 index 0000000000..096e23071c --- /dev/null +++ b/.changeset/remove-dead-compliance-subsystems.md @@ -0,0 +1,36 @@ +--- +'@objectstack/spec': minor +--- + +BREAKING (pre-launch): remove the three declared-but-never-enforced compliance +subsystems per ADR-0056 D8 ("design + enforce, or remove"), and mark the AI +agent `visibility` property EXPERIMENTAL (#1901). + +Removed — none of these were read by any runtime path, and compliance-grade +configuration must never merely look live: + +- `ComplianceConfigSchema` / `GDPRConfigSchema` / `HIPAAConfigSchema` (and the + rest of `system/compliance.zod.ts`) — there is no data-subject-rights engine, + retention enforcer, or BAA gate. FROM `import { ComplianceConfigSchema } from + '@objectstack/spec/system'` TO: delete the reference — a real compliance + subsystem will be designed top-down when scheduled. +- `MaskingConfigSchema` / `MaskingRuleSchema` (`system/masking.zod.ts`) — no + redaction layer applies them. FROM masking config TO: field-level security + (permission-set field rules, enforced by plugin-security's field masker); a + subtractive masking/deny layer arrives with ADR-0066 ⑦/⑧ if needed. +- `RLSConfigSchema` / `RLSAuditEventSchema` / `RLSAuditConfigSchema` + (`security/rls.zod.ts`) — the enforced RLS path never read the global config. + FROM global `RLSConfig` TO: per-policy `RowLevelSecurityPolicySchema` (the + live, enforced surface — unchanged). + +Kept, still `[EXPERIMENTAL]`: `EncryptionConfigSchema` (at-rest field +encryption) — a real enterprise roadmap item with a stable shape; carrying it +marked costs less than remove-and-re-add (ADR-0087). + +Marked `[EXPERIMENTAL — NOT ENFORCED]` (#1901): `AgentSchema.visibility` — the +chat-access evaluator deliberately excludes it and the agent list route does +not filter by it, so `private` does not hide an agent. The schema description +and the authoring form now say so; use `access` / `permissions` (both enforced +at the chat route since #1884) for real gating. The ADR-0056 D10 conformance +matrix tracks all dispositions (`agent-visibility` experimental; +`compliance-configs` / `data-masking` / `rls-config-global` removed). diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx index 14ac3ce36d..3314abedd1 100644 --- a/content/docs/permissions/authorization.mdx +++ b/content/docs/permissions/authorization.mdx @@ -143,7 +143,7 @@ one of two doors, each writing only what it owns: | 2 · Distribution / install / upgrade / uninstall | Install-consent scopes (ADR-0025 — consent ≠ RBAC grants); namespaced, collision-free composition; provenance axis makes uninstall well-defined | ADR-0025/0028/0048/0086 | | 3 · Environment composition / assignment | Platform-owned assignment records (`sys_user_role` etc.); anti-escalation; union semantics | ADR-0057 D4 | | 4 · Runtime enforcement | The six-gate chain above; ~18 primitives enforced and CI-guarded | ADR-0056 D10 matrix | -| 5 · Production / enterprise | Honest `[EXPERIMENTAL]` markers on the unenforced surface (field encryption, masking, compliance configs); enterprise authentication hardening staged per ADR-0069 | ADR-0049/0056 D8, ADR-0069 | +| 5 · Production / enterprise | ADR-0056 D8 dispositions settled (2026-07): compliance configs, data masking, and the global RLSConfig were **removed** (never enforced); field encryption stays honestly `[EXPERIMENTAL]` (roadmap); agent `visibility` is marked `[EXPERIMENTAL]` pending #1901. Enterprise authentication hardening staged per ADR-0069 | ADR-0049/0056 D8, ADR-0069 | ## Governance: how "declared = enforced" is kept true diff --git a/content/docs/protocol/objectql/security.mdx b/content/docs/protocol/objectql/security.mdx index c891e54948..f288271183 100644 --- a/content/docs/protocol/objectql/security.mdx +++ b/content/docs/protocol/objectql/security.mdx @@ -198,7 +198,7 @@ rowLevelSecurity: using: "status == 'active'" # date-window/function predicates (NOW(), arithmetic) are NOT pushdown-able (ADR-0055) — enforce time windows in the app layer or a pre-resolved set ``` -> RLS conditions are compiled to parameterized queries. The default policy when no rule matches is **deny** (`RLSConfigSchema.defaultPolicy`). +> RLS conditions are compiled to parameterized queries, and the enforcement path fails **closed**: an applicable policy that cannot be compiled denies (returns zero rows) rather than being dropped. (The old global `RLSConfigSchema` was removed in 2026-07 — it was never read by the enforced path; per-policy `RowLevelSecurityPolicySchema` is the live surface.) --- @@ -251,71 +251,14 @@ fields: ## 4. Data Masking -> **Status: schema exists, field wiring removed.** `MaskingRuleSchema` -> (`packages/spec/src/system/masking.zod.ts`, strategies `redact`, `partial`, -> `hash`, `tokenize`, `randomize`, `nullify`, `substitute`) remains in the -> System namespace, but the per-field `maskingRule` property was **pruned from -> the Field schema in 2026-06** as dead surface — it was never read by the -> runtime (see `docs/audits/2026-06-dead-surface-disposition-plan.md`). To keep -> a value out of reader hands today, use FLS (`readable: false`), `hidden`, or -> a `type: 'secret'` field. The examples below show the *schema shapes* for -> implementers, not a wired runtime capability. - -### Partial Masking - -```yaml -# user.object.yml -fields: - ssn: - type: text - label: SSN - maskingRule: - field: ssn - strategy: partial - pattern: '\d{5}$' # regex selecting the visible portion - preserveFormat: true - preserveLength: true - exemptRoles: [hr_admin] # roles that see the unmasked value -``` - -### Full Redaction - -```yaml -fields: - credit_card: - type: text - label: Credit Card - maskingRule: - field: credit_card - strategy: redact -``` - -### Role-Scoped Masking - -`roles` lists the roles that see the *masked* value; `exemptRoles` lists those that see the original: - -```yaml -fields: - salary: - type: currency - label: Salary - maskingRule: - field: salary - strategy: partial - exemptRoles: [hr_manager, hr_admin] -``` - -### Hash Masking - -```yaml -fields: - email: - type: email - label: Email - maskingRule: - field: email - strategy: hash -``` +> **Status: REMOVED (2026-07, ADR-0056 D8 "design + enforce, or remove").** +> `MaskingRuleSchema` / `MaskingConfigSchema` were deleted from the spec: no +> redaction layer ever applied them, and the per-field `maskingRule` property +> had already been pruned in 2026-06. To keep a value out of reader hands, use +> **FLS** (`readable: false` field permissions — enforced by plugin-security's +> field masker), `hidden`, or a `type: 'secret'` field. A subtractive +> masking/deny layer, if needed, arrives with the ADR-0066 ⑦/⑧ muting work. +> Disposition tracked in the ADR-0056 D10 conformance matrix (`data-masking`). --- @@ -421,7 +364,7 @@ Object- and field-level history is opt-in metadata, not a free-form `enable.audi - **Field history** — set `trackHistory: true` on the object (`ObjectCapabilities` in `object.zod.ts`) to record per-field changes. - **Per-field audit trail** — set `auditTrail: true` on an individual field to track every change with user and timestamp. -- **RLS audit** — RLS policy evaluations can be logged via `RLSAuditConfigSchema` (`enabled`, `logLevel`, `destination`, `retentionDays`, …). The recorded event shape is `RLSAuditEventSchema` (timestamp, userId, operation, object, policyName, granted, evaluationDurationMs). +- **RLS audit** — removed (2026-07, ADR-0056 D8): the `RLSAuditConfigSchema`/`RLSAuditEventSchema` shapes were never emitted or read by the runtime RLS path and were deleted from the spec. Enforcement decisions surface today through the security plugin's fail-closed warnings and the standard audit log (`plugin-audit`), not a dedicated RLS event stream. ```yaml # account.object.yml diff --git a/content/docs/protocol/objectql/types.mdx b/content/docs/protocol/objectql/types.mdx index fff208d5a6..3c5e7a6a72 100644 --- a/content/docs/protocol/objectql/types.mdx +++ b/content/docs/protocol/objectql/types.mdx @@ -950,9 +950,10 @@ api_key: > The per-field `maskingRule` and `encryptionConfig` properties were **pruned > from the Field schema in 2026-06** — they were declared surface with no -> runtime consumer. The `MaskingRuleSchema`/`EncryptionConfigSchema` shapes -> remain in the System namespace for implementers; see -> [Security & Access Control](/docs/protocol/objectql/security) for their status. +> runtime consumer. In 2026-07 the masking shapes were removed from the spec +> entirely (ADR-0056 D8); `EncryptionConfigSchema` remains in the System +> namespace as `[EXPERIMENTAL]` roadmap surface. See +> [Security & Access Control](/docs/protocol/objectql/security) for status. ## Type Selection Guide diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index 820ea3651e..efb111c10e 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -84,7 +84,7 @@ const result = AIKnowledge.parse(data); | **access** | `string[]` | optional | Who can chat with this agent | | **permissions** | `string[]` | optional | Required permissions or roles | | **tenantId** | `string` | optional | Tenant/Organization ID | -| **visibility** | `Enum<'global' \| 'organization' \| 'private'>` | ✅ | | +| **visibility** | `Enum<'global' \| 'organization' \| 'private'>` | ✅ | [EXPERIMENTAL — NOT ENFORCED, #1901] Intended listing scope. No runtime consumer yet; use access/permissions for real gating. | | **planning** | `Object` | optional | Autonomous reasoning and planning configuration | | **memory** | `Object` | optional | Agent memory management | | **guardrails** | `Object` | optional | Safety guardrails for the agent | diff --git a/content/docs/references/cloud/app-store.mdx b/content/docs/references/cloud/app-store.mdx index 8ccccfb6ac..0077cbc064 100644 --- a/content/docs/references/cloud/app-store.mdx +++ b/content/docs/references/cloud/app-store.mdx @@ -21,9 +21,10 @@ installing, and managing marketplace apps from within ObjectOS. ## Customer Journey -```mermaid -flowchart LR - Discover --> Evaluate --> Install --> Configure --> Use --> Rate["Rate / Review"] --> Manage +``` + +Discover → Evaluate → Install → Configure → Use → Rate/Review → Manage + ``` ## Key Concepts diff --git a/content/docs/references/cloud/developer-portal.mdx b/content/docs/references/cloud/developer-portal.mdx index 142a6c4ded..6dd328f84e 100644 --- a/content/docs/references/cloud/developer-portal.mdx +++ b/content/docs/references/cloud/developer-portal.mdx @@ -11,9 +11,10 @@ Defines schemas for the developer-facing side of the marketplace ecosystem. Covers the complete developer journey: -```mermaid -flowchart LR - Register --> Create["Create App"] --> Develop --> Validate --> Build --> Submit --> Monitor --> Iterate +``` + +Register → Create App → Develop → Validate → Build → Submit → Monitor → Iterate + ``` ## Architecture Alignment diff --git a/content/docs/references/cloud/marketplace.mdx b/content/docs/references/cloud/marketplace.mdx index 47ddbdc459..025b13d553 100644 --- a/content/docs/references/cloud/marketplace.mdx +++ b/content/docs/references/cloud/marketplace.mdx @@ -47,12 +47,18 @@ platform, and customers who install plugins. ## Platform Management Flow -```mermaid -flowchart LR - A["1 · Receive
accept submissions from verified publishers"] --> B["2 · Scan
automated security + compatibility check"] - B --> C["3 · Review
human review for quality + policy"] - C --> D["4 · Catalog
index in marketplace search"] - D --> E["5 · Monitor
track installs, ratings, issues, SLAs"] +``` + +1. Receive → Accept submissions from verified publishers + +2. Scan → Automated security scan and compatibility check + +3. Review → Human review for quality and policy compliance + +4. Catalog → Index in marketplace search catalog + +5. Monitor → Track installs, ratings, issues, and enforce SLAs + ``` diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index cab5a45d7f..6ab68ff49e 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -14,8 +14,8 @@ Field Type Enum ## TypeScript Usage ```typescript -import { Address, ComputedFieldCache, CurrencyConfig, CurrencyValue, DataQualityRules, FieldType, FileAttachmentConfig, LocationCoordinates, SelectOption, VectorConfig } from '@objectstack/spec/data'; -import type { Address, ComputedFieldCache, CurrencyConfig, CurrencyValue, DataQualityRules, FieldType, FileAttachmentConfig, LocationCoordinates, SelectOption, VectorConfig } from '@objectstack/spec/data'; +import { Address, ComputedFieldCache, CurrencyConfig, CurrencyValue, DataQualityRules, FieldType, FileAttachmentConfig, LocationCoordinates, VectorConfig } from '@objectstack/spec/data'; +import type { Address, ComputedFieldCache, CurrencyConfig, CurrencyValue, DataQualityRules, FieldType, FileAttachmentConfig, LocationCoordinates, VectorConfig } from '@objectstack/spec/data'; // Validate data const result = Address.parse(data); @@ -194,20 +194,6 @@ const result = Address.parse(data); | **accuracy** | `number` | optional | Accuracy in meters | ---- - -## SelectOption - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **label** | `string` | ✅ | Display label (human-readable, any case allowed) | -| **value** | `string` | ✅ | Stored value (lowercase machine identifier) | -| **color** | `string` | optional | Color code for badges/charts | -| **default** | `boolean` | optional | Is default option | - - --- ## VectorConfig diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 66549bf583..6dc1cd6d87 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -14,8 +14,8 @@ API Operations Enum ## TypeScript Usage ```typescript -import { ApiMethod, Index, ObjectAccessConfig, ObjectCapabilities, ObjectExternalBinding, ObjectFieldGroup, ObjectOwnershipEnum, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; -import type { ApiMethod, Index, ObjectAccessConfig, ObjectCapabilities, ObjectExternalBinding, ObjectFieldGroup, ObjectOwnershipEnum, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; +import { ApiMethod, Index, ObjectAccessConfig, ObjectCapabilities, ObjectExternalBinding, ObjectFieldGroup, ObjectOwnershipEnum, ObjectRequiredPermissions, PerOperationRequiredPermissions, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; +import type { ApiMethod, Index, ObjectAccessConfig, ObjectCapabilities, ObjectExternalBinding, ObjectFieldGroup, ObjectOwnershipEnum, ObjectRequiredPermissions, PerOperationRequiredPermissions, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; // Validate data const result = ApiMethod.parse(data); @@ -135,6 +135,48 @@ External datasource binding (ADR-0015) * `extend` +--- + +## ObjectRequiredPermissions + +### Union Options + +This schema accepts one of the following structures: + +#### Option 1 + +Type: `string[]` + +--- + +#### Option 2 + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **read** | `string[]` | optional | Capabilities required to read (find/findOne/count/aggregate). | +| **create** | `string[]` | optional | Capabilities required to create (insert). | +| **update** | `string[]` | optional | Capabilities required to update (update/transfer/restore). | +| **delete** | `string[]` | optional | Capabilities required to delete (delete/purge). | + +--- + + +--- + +## PerOperationRequiredPermissions + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **read** | `string[]` | optional | Capabilities required to read (find/findOne/count/aggregate). | +| **create** | `string[]` | optional | Capabilities required to create (insert). | +| **update** | `string[]` | optional | Capabilities required to update (update/transfer/restore). | +| **delete** | `string[]` | optional | Capabilities required to delete (delete/purge). | + + --- ## SoftDeleteConfig diff --git a/content/docs/references/data/seed-loader.mdx b/content/docs/references/data/seed-loader.mdx index cd479c7a1f..6986738a48 100644 --- a/content/docs/references/data/seed-loader.mdx +++ b/content/docs/references/data/seed-loader.mdx @@ -21,13 +21,20 @@ relationship resolution, dependency ordering, and multi-pass insertion. ## Loading Flow -```mermaid -flowchart TD - A["1 · Build object dependency graph
from field metadata (lookup / master_detail)"] --> B["2 · Topological sort
→ insert order (parents before children)"] - B --> C["3 · Pass 1 — insert/upsert records,
resolve references via externalId"] - C --> D["4 · Pass 2 — fill deferred references
(circular / delayed dependencies)"] - D --> E["5 · Validate & report unresolved references"] - E --> F["6 · Return structured result
with per-object stats"] +``` + +1. Build object dependency graph from field metadata (lookup/master_detail) + +2. Topological sort → determine insert order (parents before children) + +3. Pass 1: Insert/upsert records, resolve references via externalId + +4. Pass 2: Fill deferred references (circular/delayed dependencies) + +5. Validate & report unresolved references + +6. Return structured result with per-object stats + ``` diff --git a/content/docs/references/kernel/execution-context.mdx b/content/docs/references/kernel/execution-context.mdx index 578058afdf..9e988379d2 100644 --- a/content/docs/references/kernel/execution-context.mdx +++ b/content/docs/references/kernel/execution-context.mdx @@ -61,6 +61,7 @@ const result = ExecutionContext.parse(data); | **org_user_ids** | `string[]` | optional | | | **rlsMembership** | `Record` | optional | | | **isSystem** | `boolean` | ✅ | | +| **skipTriggers** | `boolean` | optional | | | **accessToken** | `string` | optional | | | **transaction** | `any` | optional | | | **traceId** | `string` | optional | | diff --git a/content/docs/references/kernel/package-upgrade.mdx b/content/docs/references/kernel/package-upgrade.mdx index ae99a8c1f0..f1d3ca6423 100644 --- a/content/docs/references/kernel/package-upgrade.mdx +++ b/content/docs/references/kernel/package-upgrade.mdx @@ -25,13 +25,20 @@ and rollback capabilities. ## Upgrade Flow -```mermaid -flowchart LR - A["1 · PreCheck
validate compatibility, deps"] --> B["2 · Plan
upgrade plan + metadata diff"] - B --> C["3 · Snapshot
backup metadata + customizations"] - C --> D["4 · Execute
apply new metadata (3-way merge)"] - D --> E["5 · Validate
post-upgrade health checks"] - E --> F["6 · Commit
finalize (or Rollback on failure)"] +``` + +1. PreCheck → Validate compatibility, check dependencies + +2. Plan → Generate upgrade plan with metadata diff + +3. Snapshot → Backup current state (metadata + customizations) + +4. Execute → Apply new package metadata with 3-way merge + +5. Validate → Run post-upgrade health checks + +6. Commit → Finalize upgrade (or Rollback on failure) + ``` diff --git a/content/docs/references/security/rls.mdx b/content/docs/references/security/rls.mdx index 0bba7d4c77..5a37bc3f5f 100644 --- a/content/docs/references/security/rls.mdx +++ b/content/docs/references/security/rls.mdx @@ -150,69 +150,13 @@ ObjectStack RLS: ## TypeScript Usage ```typescript -import { RLSAuditConfig, RLSAuditEvent, RLSConfig, RLSEvaluationResult, RLSOperation, RLSUserContext, RowLevelSecurityPolicy } from '@objectstack/spec/security'; -import type { RLSAuditConfig, RLSAuditEvent, RLSConfig, RLSEvaluationResult, RLSOperation, RLSUserContext, RowLevelSecurityPolicy } from '@objectstack/spec/security'; +import { RLSEvaluationResult, RLSOperation, RLSUserContext, RowLevelSecurityPolicy } from '@objectstack/spec/security'; +import type { RLSEvaluationResult, RLSOperation, RLSUserContext, RowLevelSecurityPolicy } from '@objectstack/spec/security'; // Validate data -const result = RLSAuditConfig.parse(data); +const result = RLSEvaluationResult.parse(data); ``` ---- - -## RLSAuditConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable RLS audit logging | -| **logLevel** | `Enum<'all' \| 'denied_only' \| 'granted_only' \| 'none'>` | ✅ | Which evaluations to log | -| **destination** | `Enum<'system_log' \| 'audit_trail' \| 'external'>` | ✅ | Audit log destination | -| **sampleRate** | `number` | ✅ | Sampling rate (0-1) for high-traffic environments | -| **retentionDays** | `integer` | ✅ | Audit log retention period in days | -| **includeRowData** | `boolean` | ✅ | Include row data in audit logs (security-sensitive) | -| **alertOnDenied** | `boolean` | ✅ | Send alerts when access is denied | - - ---- - -## RLSAuditEvent - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **timestamp** | `string` | ✅ | ISO 8601 timestamp of the evaluation | -| **userId** | `string` | ✅ | User ID whose access was evaluated | -| **operation** | `Enum<'select' \| 'insert' \| 'update' \| 'delete'>` | ✅ | Database operation being performed | -| **object** | `string` | ✅ | Target object name | -| **policyName** | `string` | ✅ | Name of the RLS policy evaluated | -| **granted** | `boolean` | ✅ | Whether access was granted | -| **evaluationDurationMs** | `number` | ✅ | Policy evaluation duration in milliseconds | -| **matchedCondition** | `string` | optional | Which USING/CHECK clause matched | -| **rowCount** | `number` | optional | Number of rows affected | -| **metadata** | `Record` | optional | Additional audit event metadata | - - ---- - -## RLSConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable RLS enforcement globally | -| **defaultPolicy** | `Enum<'deny' \| 'allow'>` | ✅ | Default action when no policies match | -| **allowSuperuserBypass** | `boolean` | ✅ | Allow superusers to bypass RLS | -| **bypassRoles** | `string[]` | optional | Roles that bypass RLS (see all data) | -| **logEvaluations** | `boolean` | ✅ | Log RLS policy evaluations for debugging | -| **cacheResults** | `boolean` | ✅ | Cache RLS evaluation results | -| **cacheTtlSeconds** | `integer` | ✅ | Cache TTL in seconds | -| **prefetchUserContext** | `boolean` | ✅ | Pre-fetch user context for performance | -| **audit** | `Object` | optional | RLS audit logging configuration | - - --- ## RLSEvaluationResult diff --git a/content/docs/references/system/compliance.mdx b/content/docs/references/system/compliance.mdx deleted file mode 100644 index 9a25838179..0000000000 --- a/content/docs/references/system/compliance.mdx +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: Compliance -description: Compliance protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -Compliance protocol for GDPR, CCPA, HIPAA, SOX, PCI-DSS - - -**Source:** `packages/spec/src/system/compliance.zod.ts` - - -## TypeScript Usage - -```typescript -import { AuditFinding, AuditFindingSeverity, AuditFindingStatus, AuditLogConfig, AuditSchedule, ComplianceConfig, GDPRConfig, HIPAAConfig, PCIDSSConfig } from '@objectstack/spec/system'; -import type { AuditFinding, AuditFindingSeverity, AuditFindingStatus, AuditLogConfig, AuditSchedule, ComplianceConfig, GDPRConfig, HIPAAConfig, PCIDSSConfig } from '@objectstack/spec/system'; - -// Validate data -const result = AuditFinding.parse(data); -``` - ---- - -## AuditFinding - -Audit finding with remediation tracking per ISO 27001:2022 A.5.35 - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Unique finding identifier | -| **title** | `string` | ✅ | Finding title | -| **description** | `string` | ✅ | Finding description | -| **severity** | `Enum<'critical' \| 'major' \| 'minor' \| 'observation'>` | ✅ | Finding severity | -| **status** | `Enum<'open' \| 'in_remediation' \| 'remediated' \| 'verified' \| 'accepted_risk' \| 'closed'>` | ✅ | Finding status | -| **controlReference** | `string` | optional | ISO 27001 control reference | -| **framework** | `Enum<'gdpr' \| 'hipaa' \| 'sox' \| 'pci_dss' \| 'ccpa' \| 'iso27001'>` | optional | Related compliance framework | -| **identifiedAt** | `number` | ✅ | Identification timestamp | -| **identifiedBy** | `string` | ✅ | Identifier (auditor name or system) | -| **remediationPlan** | `string` | optional | Remediation plan | -| **remediationDeadline** | `number` | optional | Remediation deadline timestamp | -| **verifiedAt** | `number` | optional | Verification timestamp | -| **verifiedBy** | `string` | optional | Verifier name or role | -| **notes** | `string` | optional | Additional notes | - - ---- - -## AuditFindingSeverity - -### Allowed Values - -* `critical` -* `major` -* `minor` -* `observation` - - ---- - -## AuditFindingStatus - -### Allowed Values - -* `open` -* `in_remediation` -* `remediated` -* `verified` -* `accepted_risk` -* `closed` - - ---- - -## AuditLogConfig - -Audit log configuration for compliance and security monitoring - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable audit logging | -| **retentionDays** | `number` | ✅ | Number of days to retain audit logs | -| **immutable** | `boolean` | ✅ | Prevent modification or deletion of audit logs | -| **signLogs** | `boolean` | ✅ | Cryptographically sign log entries for tamper detection | -| **events** | `Enum<'create' \| 'read' \| 'update' \| 'delete' \| 'export' \| 'permission-change' \| 'login' \| 'logout' \| 'failed-login'>[]` | ✅ | Event types to capture in the audit log | - - ---- - -## AuditSchedule - -Audit schedule for independent security reviews per ISO 27001:2022 A.5.35 - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Unique audit schedule identifier | -| **title** | `string` | ✅ | Audit title | -| **scope** | `string[]` | ✅ | Audit scope areas | -| **framework** | `Enum<'gdpr' \| 'hipaa' \| 'sox' \| 'pci_dss' \| 'ccpa' \| 'iso27001'>` | ✅ | Target compliance framework | -| **scheduledAt** | `number` | ✅ | Scheduled audit timestamp | -| **completedAt** | `number` | optional | Completion timestamp | -| **assessor** | `string` | ✅ | Assessor or audit team | -| **isExternal** | `boolean` | ✅ | Whether this is an external audit | -| **recurrenceMonths** | `number` | ✅ | Recurrence interval in months (0 = one-time) | -| **findings** | `Object[]` | optional | Audit findings | - - ---- - -## ComplianceConfig - -Unified compliance configuration spanning GDPR, HIPAA, PCI-DSS, and audit governance - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **gdpr** | `Object` | optional | GDPR compliance settings | -| **hipaa** | `Object` | optional | HIPAA compliance settings | -| **pciDss** | `Object` | optional | PCI-DSS compliance settings | -| **auditLog** | `Object` | ✅ | Audit log configuration | -| **auditSchedules** | `Object[]` | optional | Scheduled compliance audits (A.5.35) | - - ---- - -## GDPRConfig - -GDPR (General Data Protection Regulation) compliance configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable GDPR compliance controls | -| **dataSubjectRights** | `Object` | ✅ | Data subject rights configuration per GDPR Articles 15-21 | -| **legalBasis** | `Enum<'consent' \| 'contract' \| 'legal-obligation' \| 'vital-interests' \| 'public-task' \| 'legitimate-interests'>` | ✅ | Legal basis for data processing under GDPR Article 6 | -| **consentTracking** | `boolean` | ✅ | Track and record user consent | -| **dataRetentionDays** | `number` | optional | Maximum data retention period in days | -| **dataProcessingAgreement** | `string` | optional | URL or reference to the data processing agreement | - - ---- - -## HIPAAConfig - -HIPAA (Health Insurance Portability and Accountability Act) compliance configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable HIPAA compliance controls | -| **phi** | `Object` | ✅ | Protected Health Information safeguards | -| **businessAssociateAgreement** | `boolean` | ✅ | BAA is in place with third-party processors | - - ---- - -## PCIDSSConfig - -PCI-DSS (Payment Card Industry Data Security Standard) compliance configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable PCI-DSS compliance controls | -| **level** | `Enum<'1' \| '2' \| '3' \| '4'>` | ✅ | PCI-DSS compliance level (1 = highest) | -| **cardDataFields** | `string[]` | ✅ | Field names containing cardholder data | -| **tokenization** | `boolean` | ✅ | Replace card data with secure tokens | -| **encryptionInTransit** | `boolean` | ✅ | Encrypt cardholder data during transmission | -| **encryptionAtRest** | `boolean` | ✅ | Encrypt stored cardholder data | - - ---- - diff --git a/content/docs/references/system/index.mdx b/content/docs/references/system/index.mdx index 294120d50a..d416d817d8 100644 --- a/content/docs/references/system/index.mdx +++ b/content/docs/references/system/index.mdx @@ -13,7 +13,6 @@ This section contains all protocol schemas for the system layer of ObjectStack. - @@ -27,7 +26,6 @@ This section contains all protocol schemas for the system layer of ObjectStack. - diff --git a/content/docs/references/system/masking.mdx b/content/docs/references/system/masking.mdx deleted file mode 100644 index c3efcb2d36..0000000000 --- a/content/docs/references/system/masking.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Masking -description: Masking protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -Data masking protocol for PII protection - - -**Source:** `packages/spec/src/system/masking.zod.ts` - - -## TypeScript Usage - -```typescript -import { MaskingConfig, MaskingRule, MaskingStrategy } from '@objectstack/spec/system'; -import type { MaskingConfig, MaskingRule, MaskingStrategy } from '@objectstack/spec/system'; - -// Validate data -const result = MaskingConfig.parse(data); -``` - ---- - -## MaskingConfig - -Top-level data masking configuration for PII protection - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable data masking | -| **rules** | `Object[]` | ✅ | List of field-level masking rules | -| **auditUnmasking** | `boolean` | ✅ | Log when masked data is accessed unmasked | - - ---- - -## MaskingRule - -Masking rule for a single field - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **field** | `string` | ✅ | Field name to apply masking to | -| **strategy** | `Enum<'redact' \| 'partial' \| 'hash' \| 'tokenize' \| 'randomize' \| 'nullify' \| 'substitute'>` | ✅ | Masking strategy to use | -| **pattern** | `string` | optional | Regex pattern for partial masking | -| **preserveFormat** | `boolean` | ✅ | Keep the original data format after masking | -| **preserveLength** | `boolean` | ✅ | Keep the original data length after masking | -| **roles** | `string[]` | optional | Roles that see masked data | -| **exemptRoles** | `string[]` | optional | Roles that see unmasked data | - - ---- - -## MaskingStrategy - -Data masking strategy for PII protection - -### Allowed Values - -* `redact` -* `partial` -* `hash` -* `tokenize` -* `randomize` -* `nullify` -* `substitute` - - ---- - diff --git a/content/docs/references/system/meta.json b/content/docs/references/system/meta.json index 0cb248a689..cba7445bd7 100644 --- a/content/docs/references/system/meta.json +++ b/content/docs/references/system/meta.json @@ -8,7 +8,6 @@ "cache", "change-management", "collaboration", - "compliance", "core-services", "deploy-bundle", "disaster-recovery", @@ -22,7 +21,6 @@ "job", "license", "logging", - "masking", "message-queue", "metadata-loader", "metadata-persistence", diff --git a/packages/dogfood/test/authz-conformance.matrix.ts b/packages/dogfood/test/authz-conformance.matrix.ts index 31a672c8df..ab107c4041 100644 --- a/packages/dogfood/test/authz-conformance.matrix.ts +++ b/packages/dogfood/test/authz-conformance.matrix.ts @@ -80,10 +80,18 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ note: 'Primitive enforcement unit-proven in plugin-security/security-plugin.test.ts (ADR-0066 posture suite); the per-object declarations are pinned by platform-objects.test.ts "secure-by-default posture" so dropping the flag from a secret store fails CI, not review. Member self-service objects (sys_session, sys_api_key, sys_oauth_application, sys_two_factor) deliberately stay public-posture — the Account app reads them with a member context; row scoping (owner/tenant RLS + _self carve-outs) is their guard.' }, // ── Experimental — declared, NOT enforced (ADR-0049/0056 D8) ─────────── - { id: 'compliance-configs', summary: 'GDPR/HIPAA/PCI configs', state: 'experimental', note: 'no runtime consumer; marked [EXPERIMENTAL] (D8)' }, - { id: 'field-encryption', summary: 'at-rest field encryption', state: 'experimental', note: 'no crypto provider reads the config; marked [EXPERIMENTAL] (D8)' }, - { id: 'data-masking', summary: 'role-based data masking', state: 'experimental', note: 'FLS is the enforced field-visibility path; marked [EXPERIMENTAL] (D8)' }, - { id: 'rls-config-global', summary: 'global RLSConfig / RLSAuditEvent', state: 'experimental', note: 'not read by the RLS path; marked [EXPERIMENTAL] (D8)' }, + { id: 'field-encryption', summary: 'at-rest field encryption', state: 'experimental', + note: 'no crypto provider reads the config; marked [EXPERIMENTAL] (D8). Deliberately KEPT (2026-07 D8 disposition): at-rest encryption is a real enterprise roadmap item with a stable schema shape — removing and re-adding would cost more (ADR-0087) than carrying it marked.' }, + { id: 'agent-visibility', summary: 'AI agent `visibility` listing scope (#1901)', state: 'experimental', + note: 'Intentionally NOT enforced — the chat-access evaluator excludes it (service-ai agent-access.ts) and the agent list route does not filter by it. Schema + authoring form carry EXPERIMENTAL banners (2026-07) so authors are told `private` does not hide the agent; `access`/`permissions` ARE enforced at the chat route (#1884). Enforce when the agent listing surface gains owner/org semantics — #1901.' }, + + // ── Removed — by ADR-0056 D8 "design+enforce or remove" (2026-07) ────── + { id: 'compliance-configs', summary: 'GDPR/HIPAA/PCI configs', state: 'removed', + note: 'REMOVED from spec (system/compliance.zod.ts deleted). Compliance-grade config must never merely look live: a parsed-but-dead `gdpr:` block is a liability in an audit. A real compliance subsystem will be designed top-down (data-subject rights engine, retention enforcer) when scheduled.' }, + { id: 'data-masking', summary: 'role-based data masking', state: 'removed', + note: 'REMOVED from spec (system/masking.zod.ts deleted). FLS (plugin-security field-masker) is the enforced field-visibility path; a masking/deny layer would be redesigned with the ADR-0066 ⑦/⑧ muting work anyway, so the dead config was pure drift risk.' }, + { id: 'rls-config-global', summary: 'global RLSConfig / RLSAuditEvent', state: 'removed', + note: 'REMOVED from spec (rls.zod.ts — RLSConfigSchema/RLSAuditEventSchema/RLSAuditConfigSchema deleted). The enforced RLS path (plugin-security computeRlsFilter) never read them; per-policy RowLevelSecurityPolicySchema is the live surface and is unchanged.' }, { id: 'requireAuth-default-flip', summary: 'global requireAuth default is secure-by-default (deny anonymous)', state: 'enforced', enforcement: 'spec/api/rest-server.zod.ts requireAuth default(true) + rest/rest-server.ts normalizeConfig ?? true; explicit requireAuth:false opt-out warns at boot (rest-api-plugin)', proof: 'showcase-anonymous-deny.dogfood.test.ts', diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 8d0970a454..f706fa4934 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -528,17 +528,8 @@ "AuditEventTarget (type)", "AuditEventTargetSchema (const)", "AuditEventType (type)", - "AuditFinding (type)", - "AuditFindingSchema (const)", - "AuditFindingSeveritySchema (const)", - "AuditFindingStatusSchema (const)", - "AuditLogConfig (type)", - "AuditLogConfigInput (type)", - "AuditLogConfigSchema (const)", "AuditRetentionPolicy (type)", "AuditRetentionPolicySchema (const)", - "AuditSchedule (type)", - "AuditScheduleSchema (const)", "AuditStorageConfig (type)", "AuditStorageConfigSchema (const)", "AuthConfig (type)", @@ -622,9 +613,6 @@ "CollaborativeCursorSchema (const)", "ComplianceAuditRequirement (type)", "ComplianceAuditRequirementSchema (const)", - "ComplianceConfig (type)", - "ComplianceConfigInput (type)", - "ComplianceConfigSchema (const)", "ComplianceEncryptionRequirement (type)", "ComplianceEncryptionRequirementSchema (const)", "ComplianceFramework (type)", @@ -752,12 +740,6 @@ "FileMetadataSchema (const)", "GCounter (type)", "GCounterSchema (const)", - "GDPRConfig (type)", - "GDPRConfigInput (type)", - "GDPRConfigSchema (const)", - "HIPAAConfig (type)", - "HIPAAConfigInput (type)", - "HIPAAConfigSchema (const)", "HistogramBucketConfig (type)", "HistogramBucketConfigSchema (const)", "HttpDestinationConfig (type)", @@ -826,14 +808,6 @@ "LoggingConfig (type)", "LoggingConfigSchema (const)", "METADATA_FORM_REGISTRY (const)", - "MaskingConfig (type)", - "MaskingConfigInput (type)", - "MaskingConfigSchema (const)", - "MaskingRule (type)", - "MaskingRuleInput (type)", - "MaskingRuleSchema (const)", - "MaskingStrategy (type)", - "MaskingStrategySchema (const)", "MaskingVisibilityRule (type)", "MaskingVisibilityRuleSchema (const)", "MessageFormat (type)", @@ -955,9 +929,6 @@ "OpenTelemetryCompatibility (type)", "OpenTelemetryCompatibilitySchema (const)", "OtelExporterType (type)", - "PCIDSSConfig (type)", - "PCIDSSConfigInput (type)", - "PCIDSSConfigSchema (const)", "PKG_CONVENTIONS (const)", "PNCounter (type)", "PNCounterSchema (const)", @@ -3880,12 +3851,6 @@ "PermissionSetSchema (const)", "PlatformCapability (interface)", "RLS (const)", - "RLSAuditConfig (type)", - "RLSAuditConfigSchema (const)", - "RLSAuditEvent (type)", - "RLSAuditEventSchema (const)", - "RLSConfig (type)", - "RLSConfigSchema (const)", "RLSEvaluationResult (type)", "RLSEvaluationResultSchema (const)", "RLSOperation (type)", diff --git a/packages/spec/src/ai/agent.form.ts b/packages/spec/src/ai/agent.form.ts index 9c56665e9d..18c8cf4576 100644 --- a/packages/spec/src/ai/agent.form.ts +++ b/packages/spec/src/ai/agent.form.ts @@ -53,7 +53,7 @@ export const agentForm = defineForm({ collapsible: true, collapsed: true, fields: [ - { field: 'visibility', helpText: 'Scope: global, organization, or private' }, + { field: 'visibility', helpText: 'EXPERIMENTAL — not enforced yet (#1901): setting "private" does not hide the agent. Use Access / Permissions below for real gating.' }, { field: 'access', widget: 'string-tags', helpText: 'User IDs or role names who can chat with this agent' }, { field: 'permissions', widget: 'string-tags', helpText: 'Required permissions to use this agent' }, { field: 'tenantId', helpText: 'Restrict to specific organization ID' }, diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts index 0ceb84c8c6..9d7d7dddb4 100644 --- a/packages/spec/src/ai/agent.zod.ts +++ b/packages/spec/src/ai/agent.zod.ts @@ -169,7 +169,14 @@ export const AgentSchema = lazySchema(() => z.object({ /** Multi-tenancy & Visibility */ tenantId: z.string().optional().describe('Tenant/Organization ID'), - visibility: z.enum(['global', 'organization', 'private']).default('organization'), + // ⚠️ EXPERIMENTAL — NOT ENFORCED (#1901, ADR-0049). The chat-access evaluator + // deliberately excludes `visibility` (agent-access.ts) and the agent list + // route does not filter by it — setting `private` does NOT hide the agent. + // Use `access` / `permissions` (both ENFORCED at the chat route, #1884) to + // actually restrict who can use an agent. Enforcement needs owner/org + // semantics on the listing surface first; tracked in #1901. + visibility: z.enum(['global', 'organization', 'private']).default('organization') + .describe('[EXPERIMENTAL — NOT ENFORCED, #1901] Intended listing scope. No runtime consumer yet; use access/permissions for real gating.'), /** Autonomous Reasoning */ planning: z.object({ diff --git a/packages/spec/src/security/rls.test.ts b/packages/spec/src/security/rls.test.ts index d52a39481e..28ce5be808 100644 --- a/packages/spec/src/security/rls.test.ts +++ b/packages/spec/src/security/rls.test.ts @@ -1,11 +1,8 @@ import { describe, it, expect } from 'vitest'; import { RowLevelSecurityPolicySchema, - RLSConfigSchema, RLSUserContextSchema, RLSEvaluationResultSchema, - RLSAuditEventSchema, - RLSAuditConfigSchema, RLSOperation, RLS, type RowLevelSecurityPolicy, @@ -172,68 +169,6 @@ describe('Row-Level Security (RLS) Protocol', () => { }); }); - describe('RLSConfigSchema', () => { - it('should validate minimal config', () => { - const config = {}; - const result = RLSConfigSchema.parse(config); - - expect(result.enabled).toBe(true); - expect(result.defaultPolicy).toBe('deny'); - expect(result.allowSuperuserBypass).toBe(true); - expect(result.logEvaluations).toBe(false); - expect(result.cacheResults).toBe(true); - expect(result.cacheTtlSeconds).toBe(300); - expect(result.prefetchUserContext).toBe(true); - }); - - it('should validate complete config', () => { - const config = { - enabled: true, - defaultPolicy: 'deny' as const, - allowSuperuserBypass: true, - bypassRoles: ['system_admin', 'data_auditor'], - logEvaluations: true, - cacheResults: true, - cacheTtlSeconds: 600, - prefetchUserContext: true, - }; - - const result = RLSConfigSchema.parse(config); - expect(result).toEqual(config); - }); - - it('should validate defaultPolicy options', () => { - const denyConfig = { defaultPolicy: 'deny' as const }; - expect(RLSConfigSchema.parse(denyConfig).defaultPolicy).toBe('deny'); - - const allowConfig = { defaultPolicy: 'allow' as const }; - expect(RLSConfigSchema.parse(allowConfig).defaultPolicy).toBe('allow'); - - const invalidConfig = { defaultPolicy: 'reject' }; - expect(() => RLSConfigSchema.parse(invalidConfig)).toThrow(); - }); - - it('should validate bypass roles', () => { - const config = { - bypassRoles: ['admin', 'superuser', 'auditor'], - }; - - const result = RLSConfigSchema.parse(config); - expect(result.bypassRoles).toEqual(['admin', 'superuser', 'auditor']); - }); - - it('should validate cache TTL as positive integer', () => { - const validConfig = { cacheTtlSeconds: 600 }; - expect(() => RLSConfigSchema.parse(validConfig)).not.toThrow(); - - const invalidConfig1 = { cacheTtlSeconds: -100 }; - expect(() => RLSConfigSchema.parse(invalidConfig1)).toThrow(); - - const invalidConfig2 = { cacheTtlSeconds: 0 }; - expect(() => RLSConfigSchema.parse(invalidConfig2)).toThrow(); - }); - }); - describe('RLSUserContextSchema', () => { it('should validate minimal user context', () => { const context = { @@ -569,157 +504,4 @@ describe('Row-Level Security (RLS) Protocol', () => { expect(() => RowLevelSecurityPolicySchema.parse(policy)).not.toThrow(); }); }); - - // ========================================== - // RLS Audit Event & Config Tests - // ========================================== - - describe('RLSAuditEventSchema', () => { - it('should accept minimal audit event', () => { - const event = RLSAuditEventSchema.parse({ - timestamp: '2024-06-15T10:30:00Z', - userId: 'user_123', - operation: 'select', - object: 'account', - policyName: 'tenant_isolation', - granted: true, - evaluationDurationMs: 2.5, - }); - - expect(event.granted).toBe(true); - expect(event.evaluationDurationMs).toBe(2.5); - }); - - it('should accept full audit event', () => { - const event = RLSAuditEventSchema.parse({ - timestamp: '2024-06-15T10:30:00Z', - userId: 'user_456', - operation: 'delete', - object: 'customer', - policyName: 'owner_access', - granted: false, - evaluationDurationMs: 15.3, - matchedCondition: 'owner_id == current_user.id', - rowCount: 0, - metadata: { source: 'api', requestId: 'req-789' }, - }); - - expect(event.granted).toBe(false); - expect(event.matchedCondition).toBe('owner_id == current_user.id'); - expect(event.rowCount).toBe(0); - expect(event.metadata?.source).toBe('api'); - }); - - it('should accept all operation types', () => { - const ops = ['select', 'insert', 'update', 'delete'] as const; - ops.forEach(operation => { - expect(() => RLSAuditEventSchema.parse({ - timestamp: '2024-01-01T00:00:00Z', - userId: 'u1', - operation, - object: 'test', - policyName: 'test_policy', - granted: true, - evaluationDurationMs: 1, - })).not.toThrow(); - }); - }); - }); - - describe('RLSAuditConfigSchema', () => { - it('should accept full audit config', () => { - const config = RLSAuditConfigSchema.parse({ - enabled: true, - logLevel: 'all', - destination: 'audit_trail', - sampleRate: 1.0, - retentionDays: 365, - includeRowData: false, - alertOnDenied: true, - }); - - expect(config.enabled).toBe(true); - expect(config.logLevel).toBe('all'); - expect(config.destination).toBe('audit_trail'); - expect(config.sampleRate).toBe(1.0); - expect(config.retentionDays).toBe(365); - }); - - it('should accept all log levels', () => { - const levels = ['all', 'denied_only', 'granted_only', 'none'] as const; - levels.forEach(logLevel => { - const config = RLSAuditConfigSchema.parse({ - enabled: true, - logLevel, - destination: 'system_log', - sampleRate: 0.5, - }); - expect(config.logLevel).toBe(logLevel); - }); - }); - - it('should accept all destinations', () => { - const destinations = ['system_log', 'audit_trail', 'external'] as const; - destinations.forEach(destination => { - const config = RLSAuditConfigSchema.parse({ - enabled: true, - logLevel: 'all', - destination, - sampleRate: 1, - }); - expect(config.destination).toBe(destination); - }); - }); - - it('should enforce sampleRate range', () => { - expect(() => RLSAuditConfigSchema.parse({ - enabled: true, - logLevel: 'all', - destination: 'system_log', - sampleRate: -0.1, - })).toThrow(); - - expect(() => RLSAuditConfigSchema.parse({ - enabled: true, - logLevel: 'all', - destination: 'system_log', - sampleRate: 1.1, - })).toThrow(); - }); - - it('should apply defaults for includeRowData and alertOnDenied', () => { - const config = RLSAuditConfigSchema.parse({ - enabled: true, - logLevel: 'denied_only', - destination: 'external', - sampleRate: 0.1, - }); - - expect(config.includeRowData).toBe(false); - expect(config.alertOnDenied).toBe(true); - expect(config.retentionDays).toBe(90); - }); - }); - - describe('RLSConfigSchema with audit', () => { - it('should accept config with audit field', () => { - const config = RLSConfigSchema.parse({ - enabled: true, - audit: { - enabled: true, - logLevel: 'denied_only', - destination: 'audit_trail', - sampleRate: 0.5, - }, - }); - - expect(config.audit?.enabled).toBe(true); - expect(config.audit?.logLevel).toBe('denied_only'); - }); - - it('should accept config without audit field', () => { - const config = RLSConfigSchema.parse({}); - expect(config.audit).toBeUndefined(); - }); - }); }); diff --git a/packages/spec/src/security/rls.zod.ts b/packages/spec/src/security/rls.zod.ts index dbbf5119ef..555d9b594c 100644 --- a/packages/spec/src/security/rls.zod.ts +++ b/packages/spec/src/security/rls.zod.ts @@ -417,205 +417,12 @@ export const RowLevelSecurityPolicySchema = lazySchema(() => z.object({ // since 'all' and mixed operation types are valid })); -/** - * RLS Audit Event Schema - * - * Records a single RLS policy evaluation event for compliance and debugging. - */ -// ⚠️ EXPERIMENTAL — NOT ENFORCED (ADR-0056). RLS audit events are not emitted by the -// runtime RLS path — no consumer writes these. Authoring does NOT change behaviour (per ADR-0049). -export const RLSAuditEventSchema = lazySchema(() => z.object({ - /** ISO 8601 timestamp of the evaluation */ - timestamp: z.string() - .describe('ISO 8601 timestamp of the evaluation'), - - /** ID of the user whose access was evaluated */ - userId: z.string() - .describe('User ID whose access was evaluated'), - - /** Database operation being performed */ - operation: z.enum(['select', 'insert', 'update', 'delete']) - .describe('Database operation being performed'), - - /** Target object (table) name */ - object: z.string() - .describe('Target object name'), - - /** Name of the RLS policy evaluated */ - policyName: z.string() - .describe('Name of the RLS policy evaluated'), - - /** Whether access was granted */ - granted: z.boolean() - .describe('Whether access was granted'), - - /** Time taken to evaluate the policy in milliseconds */ - evaluationDurationMs: z.number() - .describe('Policy evaluation duration in milliseconds'), - - /** Which USING/CHECK clause matched */ - matchedCondition: z.string() - .optional() - .describe('Which USING/CHECK clause matched'), - - /** Number of rows affected by the operation */ - rowCount: z.number() - .optional() - .describe('Number of rows affected'), - - /** Additional metadata for the audit event */ - metadata: z.record(z.string(), z.unknown()) - .optional() - .describe('Additional audit event metadata'), -})); - -export type RLSAuditEvent = z.infer; - -/** - * RLS Audit Configuration Schema - * - * Controls how RLS policy evaluations are logged and monitored. - */ -export const RLSAuditConfigSchema = lazySchema(() => z.object({ - /** Enable RLS audit logging */ - enabled: z.boolean() - .describe('Enable RLS audit logging'), - - /** Which evaluations to log */ - logLevel: z.enum(['all', 'denied_only', 'granted_only', 'none']) - .describe('Which evaluations to log'), - - /** Where to send audit logs */ - destination: z.enum(['system_log', 'audit_trail', 'external']) - .describe('Audit log destination'), - - /** Sampling rate for high-traffic environments (0-1) */ - sampleRate: z.number() - .min(0) - .max(1) - .describe('Sampling rate (0-1) for high-traffic environments'), - - /** Number of days to retain audit logs */ - retentionDays: z.number() - .int() - .default(90) - .describe('Audit log retention period in days'), - - /** Whether to include row data in audit logs (security-sensitive) */ - includeRowData: z.boolean() - .default(false) - .describe('Include row data in audit logs (security-sensitive)'), - - /** Alert when access is denied */ - alertOnDenied: z.boolean() - .default(true) - .describe('Send alerts when access is denied'), -})); - -export type RLSAuditConfig = z.infer; - -/** - * RLS Configuration Schema - * - * Global configuration for the Row-Level Security system. - * Defines how RLS is enforced across the entire platform. - */ -// ⚠️ EXPERIMENTAL — NOT ENFORCED (ADR-0056). Global RLSConfig (defaultPolicy / bypassRoles / -// caching / audit) is not read by the SecurityPlugin RLS path. Authoring does NOT change behaviour (per ADR-0049). -export const RLSConfigSchema = lazySchema(() => z.object({ - /** - * Global RLS enable/disable flag. - * When false, all RLS policies are ignored (use with caution!). - * - * @default true - */ - enabled: z.boolean() - .default(true) - .describe('Enable RLS enforcement globally'), - - /** - * Default behavior when no policies match. - * - * - **deny**: Deny access (secure default) - * - **allow**: Allow access (permissive mode, not recommended) - * - * @default "deny" - */ - defaultPolicy: z.enum(['deny', 'allow']) - .default('deny') - .describe('Default action when no policies match'), - - /** - * Whether to allow superusers to bypass RLS. - * Superusers include system administrators and service accounts. - * - * @default true - */ - allowSuperuserBypass: z.boolean() - .default(true) - .describe('Allow superusers to bypass RLS'), - - /** - * List of roles that can bypass RLS. - * Users with these roles see all records regardless of policies. - * - * @example ["system_admin", "data_auditor"] - */ - bypassRoles: z.array(z.string()) - .optional() - .describe('Roles that bypass RLS (see all data)'), - - /** - * Whether to log RLS policy evaluations. - * Useful for debugging and auditing. - * Can impact performance if enabled globally. - * - * @default false - */ - logEvaluations: z.boolean() - .default(false) - .describe('Log RLS policy evaluations for debugging'), - - /** - * Cache RLS policy evaluation results. - * Can improve performance for frequently accessed records. - * Cache is invalidated when policies change or user context changes. - * - * @default true - */ - cacheResults: z.boolean() - .default(true) - .describe('Cache RLS evaluation results'), - - /** - * Cache TTL in seconds. - * How long to cache RLS evaluation results. - * - * @default 300 (5 minutes) - */ - cacheTtlSeconds: z.number() - .int() - .positive() - .default(300) - .describe('Cache TTL in seconds'), - - /** - * Performance optimization: Pre-fetch user context. - * Load user context once per request instead of per-query. - * - * @default true - */ - prefetchUserContext: z.boolean() - .default(true) - .describe('Pre-fetch user context for performance'), - - /** - * Audit logging configuration for RLS evaluations. - */ - audit: RLSAuditConfigSchema - .optional() - .describe('RLS audit logging configuration'), -})); +// RLSAuditEventSchema / RLSAuditConfigSchema / RLSConfigSchema were REMOVED +// per ADR-0056 D8 "design+enforce or remove": the global RLSConfig +// (defaultPolicy/bypassRoles/caching/audit) and the audit-event shapes were +// never read by the enforced RLS path (plugin-security computeRlsFilter) — +// declared-but-inert config. Per-policy RLS (RowLevelSecurityPolicySchema, +// below/above) is the live, enforced surface and is unchanged. /** * User Context Schema @@ -723,7 +530,6 @@ export const RLSEvaluationResultSchema = lazySchema(() => z.object({ * Type exports */ export type RowLevelSecurityPolicy = z.infer; -export type RLSConfig = z.infer; export type RLSUserContext = z.infer; export type RLSEvaluationResult = z.infer; diff --git a/packages/spec/src/system/compliance.test.ts b/packages/spec/src/system/compliance.test.ts deleted file mode 100644 index b027a412a3..0000000000 --- a/packages/spec/src/system/compliance.test.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - GDPRConfigSchema, - HIPAAConfigSchema, - PCIDSSConfigSchema, - AuditLogConfigSchema, - ComplianceConfigSchema, - AuditFindingSeveritySchema, - AuditFindingStatusSchema, - AuditFindingSchema, - AuditScheduleSchema, -} from './compliance.zod'; - -describe('GDPRConfigSchema', () => { - it('should accept valid GDPR config with defaults', () => { - const config = GDPRConfigSchema.parse({ - enabled: true, - dataSubjectRights: {}, - legalBasis: 'consent', - }); - - expect(config.enabled).toBe(true); - expect(config.dataSubjectRights.rightToAccess).toBe(true); - expect(config.dataSubjectRights.rightToRectification).toBe(true); - expect(config.dataSubjectRights.rightToErasure).toBe(true); - expect(config.dataSubjectRights.rightToRestriction).toBe(true); - expect(config.dataSubjectRights.rightToPortability).toBe(true); - expect(config.dataSubjectRights.rightToObjection).toBe(true); - expect(config.consentTracking).toBe(true); - }); - - it('should accept all legal basis values', () => { - const bases = [ - 'consent', 'contract', 'legal-obligation', - 'vital-interests', 'public-task', 'legitimate-interests', - ]; - - bases.forEach((basis) => { - expect(() => GDPRConfigSchema.parse({ - enabled: true, - dataSubjectRights: {}, - legalBasis: basis, - })).not.toThrow(); - }); - }); - - it('should accept optional fields', () => { - const config = GDPRConfigSchema.parse({ - enabled: true, - dataSubjectRights: {}, - legalBasis: 'consent', - dataRetentionDays: 365, - dataProcessingAgreement: 'https://example.com/dpa', - }); - - expect(config.dataRetentionDays).toBe(365); - expect(config.dataProcessingAgreement).toBe('https://example.com/dpa'); - }); - - it('should reject invalid legal basis', () => { - expect(() => GDPRConfigSchema.parse({ - enabled: true, - dataSubjectRights: {}, - legalBasis: 'invalid', - })).toThrow(); - }); - - it('should reject missing required fields', () => { - expect(() => GDPRConfigSchema.parse({})).toThrow(); - expect(() => GDPRConfigSchema.parse({ enabled: true })).toThrow(); - }); -}); - -describe('HIPAAConfigSchema', () => { - it('should accept valid HIPAA config with defaults', () => { - const config = HIPAAConfigSchema.parse({ - enabled: true, - phi: {}, - }); - - expect(config.enabled).toBe(true); - expect(config.phi.encryption).toBe(true); - expect(config.phi.accessControl).toBe(true); - expect(config.phi.auditTrail).toBe(true); - expect(config.phi.backupAndRecovery).toBe(true); - expect(config.businessAssociateAgreement).toBe(false); - }); - - it('should accept full configuration', () => { - const config = HIPAAConfigSchema.parse({ - enabled: true, - phi: { - encryption: false, - accessControl: true, - auditTrail: true, - backupAndRecovery: false, - }, - businessAssociateAgreement: true, - }); - - expect(config.phi.encryption).toBe(false); - expect(config.phi.backupAndRecovery).toBe(false); - expect(config.businessAssociateAgreement).toBe(true); - }); - - it('should reject missing required fields', () => { - expect(() => HIPAAConfigSchema.parse({})).toThrow(); - expect(() => HIPAAConfigSchema.parse({ enabled: true })).toThrow(); - }); -}); - -describe('PCIDSSConfigSchema', () => { - it('should accept valid PCI-DSS config with defaults', () => { - const config = PCIDSSConfigSchema.parse({ - enabled: true, - level: '1', - cardDataFields: ['card_number', 'cvv'], - }); - - expect(config.enabled).toBe(true); - expect(config.level).toBe('1'); - expect(config.cardDataFields).toEqual(['card_number', 'cvv']); - expect(config.tokenization).toBe(true); - expect(config.encryptionInTransit).toBe(true); - expect(config.encryptionAtRest).toBe(true); - }); - - it('should accept all compliance levels', () => { - const levels = ['1', '2', '3', '4']; - - levels.forEach((level) => { - expect(() => PCIDSSConfigSchema.parse({ - enabled: true, - level, - cardDataFields: [], - })).not.toThrow(); - }); - }); - - it('should reject invalid level', () => { - expect(() => PCIDSSConfigSchema.parse({ - enabled: true, - level: '5', - cardDataFields: [], - })).toThrow(); - }); - - it('should reject missing required fields', () => { - expect(() => PCIDSSConfigSchema.parse({})).toThrow(); - expect(() => PCIDSSConfigSchema.parse({ enabled: true })).toThrow(); - expect(() => PCIDSSConfigSchema.parse({ enabled: true, level: '1' })).toThrow(); - }); -}); - -describe('AuditLogConfigSchema', () => { - it('should accept valid config with defaults', () => { - const config = AuditLogConfigSchema.parse({ - events: ['create', 'update', 'delete'], - }); - - expect(config.enabled).toBe(true); - expect(config.retentionDays).toBe(365); - expect(config.immutable).toBe(true); - expect(config.signLogs).toBe(false); - expect(config.events).toEqual(['create', 'update', 'delete']); - }); - - it('should accept all event types', () => { - const events = [ - 'create', 'read', 'update', 'delete', 'export', - 'permission-change', 'login', 'logout', 'failed-login', - ]; - - expect(() => AuditLogConfigSchema.parse({ events })).not.toThrow(); - }); - - it('should reject invalid event type', () => { - expect(() => AuditLogConfigSchema.parse({ - events: ['invalid-event'], - })).toThrow(); - }); - - it('should reject missing events', () => { - expect(() => AuditLogConfigSchema.parse({})).toThrow(); - }); -}); - -describe('ComplianceConfigSchema', () => { - it('should accept minimal configuration with required auditLog', () => { - const config = ComplianceConfigSchema.parse({ - auditLog: { - events: ['create', 'update'], - }, - }); - - expect(config.gdpr).toBeUndefined(); - expect(config.hipaa).toBeUndefined(); - expect(config.pciDss).toBeUndefined(); - expect(config.auditLog).toBeDefined(); - expect(config.auditLog.events).toEqual(['create', 'update']); - }); - - it('should accept full configuration', () => { - const config = ComplianceConfigSchema.parse({ - gdpr: { - enabled: true, - dataSubjectRights: {}, - legalBasis: 'consent', - }, - hipaa: { - enabled: true, - phi: {}, - }, - pciDss: { - enabled: true, - level: '1', - cardDataFields: ['card_number'], - }, - auditLog: { - events: ['create', 'read', 'update', 'delete'], - }, - }); - - expect(config.gdpr?.enabled).toBe(true); - expect(config.hipaa?.enabled).toBe(true); - expect(config.pciDss?.enabled).toBe(true); - expect(config.auditLog.events).toHaveLength(4); - }); - - it('should reject missing auditLog', () => { - expect(() => ComplianceConfigSchema.parse({})).toThrow(); - }); - - it('should accept configuration with audit schedules', () => { - const config = ComplianceConfigSchema.parse({ - auditLog: { - events: ['create', 'update'], - }, - auditSchedules: [ - { - id: 'AUDIT-2024-Q1', - title: 'Q1 ISO 27001 Internal Audit', - scope: ['access_control', 'encryption'], - framework: 'iso27001', - scheduledAt: 1711929600000, - assessor: 'internal_audit_team', - }, - ], - }); - - expect(config.auditSchedules).toHaveLength(1); - expect(config.auditSchedules![0].framework).toBe('iso27001'); - }); -}); - -describe('AuditFindingSeveritySchema', () => { - it('should accept all valid severities', () => { - const severities = ['critical', 'major', 'minor', 'observation']; - - severities.forEach((severity) => { - expect(() => AuditFindingSeveritySchema.parse(severity)).not.toThrow(); - }); - }); - - it('should reject invalid severity', () => { - expect(() => AuditFindingSeveritySchema.parse('warning')).toThrow(); - }); -}); - -describe('AuditFindingStatusSchema', () => { - it('should accept all valid statuses', () => { - const statuses = ['open', 'in_remediation', 'remediated', 'verified', 'accepted_risk', 'closed']; - - statuses.forEach((status) => { - expect(() => AuditFindingStatusSchema.parse(status)).not.toThrow(); - }); - }); - - it('should reject invalid status', () => { - expect(() => AuditFindingStatusSchema.parse('pending')).toThrow(); - }); -}); - -describe('AuditFindingSchema', () => { - it('should accept valid finding', () => { - const finding = AuditFindingSchema.parse({ - id: 'FIND-2024-001', - title: 'Insufficient access logging', - description: 'PHI access events are not being logged for HIPAA compliance', - severity: 'major', - status: 'in_remediation', - controlReference: 'A.8.15', - framework: 'iso27001', - identifiedAt: 1704067200000, - identifiedBy: 'external_auditor', - remediationPlan: 'Implement audit logging for all PHI access events', - remediationDeadline: 1706745600000, - }); - - expect(finding.severity).toBe('major'); - expect(finding.framework).toBe('iso27001'); - }); - - it('should accept minimal finding', () => { - const finding = AuditFindingSchema.parse({ - id: 'FIND-2024-002', - title: 'Missing encryption', - description: 'Field-level encryption not enabled', - severity: 'minor', - status: 'open', - identifiedAt: Date.now(), - identifiedBy: 'internal_audit', - }); - - expect(finding.controlReference).toBeUndefined(); - expect(finding.remediationPlan).toBeUndefined(); - }); - - it('should accept verified finding', () => { - const finding = AuditFindingSchema.parse({ - id: 'FIND-2024-003', - title: 'Weak password policy', - description: 'Password minimum length below 12 characters', - severity: 'observation', - status: 'verified', - identifiedAt: 1704067200000, - identifiedBy: 'auditor', - verifiedAt: 1706745600000, - verifiedBy: 'senior_auditor', - notes: 'Password policy updated and verified', - }); - - expect(finding.verifiedAt).toBe(1706745600000); - expect(finding.verifiedBy).toBe('senior_auditor'); - }); - - it('should reject missing required fields', () => { - expect(() => AuditFindingSchema.parse({})).toThrow(); - }); -}); - -describe('AuditScheduleSchema', () => { - it('should accept valid schedule with defaults', () => { - const schedule = AuditScheduleSchema.parse({ - id: 'AUDIT-2024-Q1', - title: 'Q1 ISO 27001 Internal Audit', - scope: ['access_control', 'encryption', 'incident_response'], - framework: 'iso27001', - scheduledAt: 1711929600000, - assessor: 'internal_audit_team', - }); - - expect(schedule.isExternal).toBe(false); - expect(schedule.recurrenceMonths).toBe(0); - expect(schedule.findings).toBeUndefined(); - }); - - it('should accept full schedule with findings', () => { - const schedule = AuditScheduleSchema.parse({ - id: 'AUDIT-2024-EXT', - title: 'Annual External ISO 27001 Audit', - scope: ['all_controls'], - framework: 'iso27001', - scheduledAt: 1711929600000, - completedAt: 1712534400000, - assessor: 'External Audit Firm LLC', - isExternal: true, - recurrenceMonths: 12, - findings: [ - { - id: 'FIND-001', - title: 'Missing incident response plan', - description: 'No documented incident response procedure', - severity: 'major', - status: 'open', - controlReference: 'A.5.24', - framework: 'iso27001', - identifiedAt: 1712534400000, - identifiedBy: 'External Audit Firm LLC', - }, - ], - }); - - expect(schedule.isExternal).toBe(true); - expect(schedule.recurrenceMonths).toBe(12); - expect(schedule.findings).toHaveLength(1); - }); - - it('should accept all framework values', () => { - const frameworks = ['gdpr', 'hipaa', 'sox', 'pci_dss', 'ccpa', 'iso27001']; - - frameworks.forEach((framework) => { - expect(() => AuditScheduleSchema.parse({ - id: `AUDIT-${framework}`, - title: `${framework} audit`, - scope: ['general'], - framework, - scheduledAt: Date.now(), - assessor: 'auditor', - })).not.toThrow(); - }); - }); - - it('should reject missing required fields', () => { - expect(() => AuditScheduleSchema.parse({})).toThrow(); - }); -}); diff --git a/packages/spec/src/system/compliance.zod.ts b/packages/spec/src/system/compliance.zod.ts deleted file mode 100644 index e186a1d371..0000000000 --- a/packages/spec/src/system/compliance.zod.ts +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { z } from 'zod'; - -// ⚠️ EXPERIMENTAL — NOT ENFORCED (ADR-0056). The GDPR/HIPAA/PCI-DSS schemas below -// are parsed but have NO runtime consumer — no data-subject-rights engine, -// retention enforcer, BAA gate, or tokenizer reads them. Authoring them does NOT -// change behaviour; recorded as roadmap intent (M2+). Do not rely on them for -// compliance until a consumer lands (per ADR-0049 enforce-or-remove). -import { ComplianceFrameworkSchema } from './security-context.zod'; - -/** - * Compliance protocol for GDPR, CCPA, HIPAA, SOX, PCI-DSS - */ -import { lazySchema } from '../shared/lazy-schema'; -export const GDPRConfigSchema = lazySchema(() => z.object({ - enabled: z.boolean().describe('Enable GDPR compliance controls'), - dataSubjectRights: z.object({ - rightToAccess: z.boolean().default(true).describe('Allow data subjects to access their data'), - rightToRectification: z.boolean().default(true).describe('Allow data subjects to correct their data'), - rightToErasure: z.boolean().default(true).describe('Allow data subjects to request deletion'), - rightToRestriction: z.boolean().default(true).describe('Allow data subjects to restrict processing'), - rightToPortability: z.boolean().default(true).describe('Allow data subjects to export their data'), - rightToObjection: z.boolean().default(true).describe('Allow data subjects to object to processing'), - }).describe('Data subject rights configuration per GDPR Articles 15-21'), - legalBasis: z.enum([ - 'consent', - 'contract', - 'legal-obligation', - 'vital-interests', - 'public-task', - 'legitimate-interests', - ]).describe('Legal basis for data processing under GDPR Article 6'), - consentTracking: z.boolean().default(true).describe('Track and record user consent'), - dataRetentionDays: z.number().optional().describe('Maximum data retention period in days'), - dataProcessingAgreement: z.string().optional().describe('URL or reference to the data processing agreement'), -}).describe('GDPR (General Data Protection Regulation) compliance configuration')); - -export type GDPRConfig = z.infer; -export type GDPRConfigInput = z.input; - -export const HIPAAConfigSchema = lazySchema(() => z.object({ - enabled: z.boolean().describe('Enable HIPAA compliance controls'), - phi: z.object({ - encryption: z.boolean().default(true).describe('Encrypt Protected Health Information at rest'), - accessControl: z.boolean().default(true).describe('Enforce role-based access to PHI'), - auditTrail: z.boolean().default(true).describe('Log all PHI access events'), - backupAndRecovery: z.boolean().default(true).describe('Enable PHI backup and disaster recovery'), - }).describe('Protected Health Information safeguards'), - businessAssociateAgreement: z.boolean().default(false).describe('BAA is in place with third-party processors'), -}).describe('HIPAA (Health Insurance Portability and Accountability Act) compliance configuration')); - -export type HIPAAConfig = z.infer; -export type HIPAAConfigInput = z.input; - -export const PCIDSSConfigSchema = lazySchema(() => z.object({ - enabled: z.boolean().describe('Enable PCI-DSS compliance controls'), - level: z.enum(['1', '2', '3', '4']).describe('PCI-DSS compliance level (1 = highest)'), - cardDataFields: z.array(z.string()).describe('Field names containing cardholder data'), - tokenization: z.boolean().default(true).describe('Replace card data with secure tokens'), - encryptionInTransit: z.boolean().default(true).describe('Encrypt cardholder data during transmission'), - encryptionAtRest: z.boolean().default(true).describe('Encrypt stored cardholder data'), -}).describe('PCI-DSS (Payment Card Industry Data Security Standard) compliance configuration')); - -export type PCIDSSConfig = z.infer; -export type PCIDSSConfigInput = z.input; - -export const AuditLogConfigSchema = lazySchema(() => z.object({ - enabled: z.boolean().default(true).describe('Enable audit logging'), - retentionDays: z.number().default(365).describe('Number of days to retain audit logs'), - immutable: z.boolean().default(true).describe('Prevent modification or deletion of audit logs'), - signLogs: z.boolean().default(false).describe('Cryptographically sign log entries for tamper detection'), - events: z.array(z.enum([ - 'create', - 'read', - 'update', - 'delete', - 'export', - 'permission-change', - 'login', - 'logout', - 'failed-login', - ])).describe('Event types to capture in the audit log'), -}).describe('Audit log configuration for compliance and security monitoring')); - -export type AuditLogConfig = z.infer; -export type AuditLogConfigInput = z.input; - -/** - * Audit Finding Severity Schema - * - * Severity classification for audit findings. - */ -export const AuditFindingSeveritySchema = lazySchema(() => z.enum([ - 'critical', // Immediate remediation required - 'major', // Significant non-conformity - 'minor', // Minor non-conformity - 'observation', // Improvement opportunity -])); - -/** - * Audit Finding Status Schema - * - * Lifecycle status of an audit finding. - */ -export const AuditFindingStatusSchema = lazySchema(() => z.enum([ - 'open', // Finding identified, not yet addressed - 'in_remediation', // Remediation in progress - 'remediated', // Remediation completed, pending verification - 'verified', // Remediation verified and accepted - 'accepted_risk', // Risk accepted by management - 'closed', // Finding closed -])); - -/** - * Audit Finding Schema (A.5.35) - * - * Individual finding from a compliance or security audit. - * Supports tracking from discovery through remediation and verification. - * - * @example - * ```json - * { - * "id": "FIND-2024-001", - * "title": "Insufficient access logging", - * "description": "PHI access events are not being logged for HIPAA compliance", - * "severity": "major", - * "status": "in_remediation", - * "controlReference": "A.8.15", - * "framework": "iso27001", - * "identifiedAt": 1704067200000, - * "identifiedBy": "external_auditor", - * "remediationPlan": "Implement audit logging for all PHI access events", - * "remediationDeadline": 1706745600000 - * } - * ``` - */ -export const AuditFindingSchema = lazySchema(() => z.object({ - /** - * Unique finding identifier - */ - id: z.string().describe('Unique finding identifier'), - - /** - * Short descriptive title - */ - title: z.string().describe('Finding title'), - - /** - * Detailed description of the finding - */ - description: z.string().describe('Finding description'), - - /** - * Finding severity - */ - severity: AuditFindingSeveritySchema.describe('Finding severity'), - - /** - * Current status - */ - status: AuditFindingStatusSchema.describe('Finding status'), - - /** - * ISO 27001 control reference (e.g., "A.5.35", "A.8.15") - */ - controlReference: z.string().optional().describe('ISO 27001 control reference'), - - /** - * Compliance framework - */ - framework: ComplianceFrameworkSchema.optional() - .describe('Related compliance framework'), - - /** - * Timestamp when finding was identified (Unix milliseconds) - */ - identifiedAt: z.number().describe('Identification timestamp'), - - /** - * User or entity who identified the finding - */ - identifiedBy: z.string().describe('Identifier (auditor name or system)'), - - /** - * Planned remediation actions - */ - remediationPlan: z.string().optional().describe('Remediation plan'), - - /** - * Remediation deadline (Unix milliseconds) - */ - remediationDeadline: z.number().optional().describe('Remediation deadline timestamp'), - - /** - * Timestamp when remediation was verified (Unix milliseconds) - */ - verifiedAt: z.number().optional().describe('Verification timestamp'), - - /** - * Verifier name or role - */ - verifiedBy: z.string().optional().describe('Verifier name or role'), - - /** - * Notes or comments - */ - notes: z.string().optional().describe('Additional notes'), -}).describe('Audit finding with remediation tracking per ISO 27001:2022 A.5.35')); - -export type AuditFinding = z.infer; - -/** - * Audit Schedule Schema (A.5.35) - * - * Defines audit scheduling for independent information security reviews. - * Supports recurring audits, scope definition, and assessor assignment. - * - * @example - * ```json - * { - * "id": "AUDIT-2024-Q1", - * "title": "Q1 ISO 27001 Internal Audit", - * "scope": ["access_control", "encryption", "incident_response"], - * "framework": "iso27001", - * "scheduledAt": 1711929600000, - * "assessor": "internal_audit_team", - * "recurrenceMonths": 3 - * } - * ``` - */ -export const AuditScheduleSchema = lazySchema(() => z.object({ - /** - * Unique audit schedule identifier - */ - id: z.string().describe('Unique audit schedule identifier'), - - /** - * Audit title or name - */ - title: z.string().describe('Audit title'), - - /** - * Scope of areas to audit - */ - scope: z.array(z.string()).describe('Audit scope areas'), - - /** - * Target compliance framework - */ - framework: ComplianceFrameworkSchema - .describe('Target compliance framework'), - - /** - * Scheduled audit date (Unix milliseconds) - */ - scheduledAt: z.number().describe('Scheduled audit timestamp'), - - /** - * Actual completion date (Unix milliseconds) - */ - completedAt: z.number().optional().describe('Completion timestamp'), - - /** - * Assessor name, team, or external firm - */ - assessor: z.string().describe('Assessor or audit team'), - - /** - * Whether this is an external (independent) audit - */ - isExternal: z.boolean().default(false).describe('Whether this is an external audit'), - - /** - * Recurrence interval in months (0 = one-time) - */ - recurrenceMonths: z.number().default(0).describe('Recurrence interval in months (0 = one-time)'), - - /** - * Findings from this audit - */ - findings: z.array(AuditFindingSchema).optional().describe('Audit findings'), -}).describe('Audit schedule for independent security reviews per ISO 27001:2022 A.5.35')); - -export type AuditSchedule = z.infer; - -export const ComplianceConfigSchema = lazySchema(() => z.object({ - gdpr: GDPRConfigSchema.optional().describe('GDPR compliance settings'), - hipaa: HIPAAConfigSchema.optional().describe('HIPAA compliance settings'), - pciDss: PCIDSSConfigSchema.optional().describe('PCI-DSS compliance settings'), - auditLog: AuditLogConfigSchema.describe('Audit log configuration'), - auditSchedules: z.array(AuditScheduleSchema).optional() - .describe('Scheduled compliance audits (A.5.35)'), -}).describe('Unified compliance configuration spanning GDPR, HIPAA, PCI-DSS, and audit governance')); - -export type ComplianceConfig = z.infer; -export type ComplianceConfigInput = z.input; diff --git a/packages/spec/src/system/index.ts b/packages/spec/src/system/index.ts index 0ea9265a35..b8fdb07243 100644 --- a/packages/spec/src/system/index.ts +++ b/packages/spec/src/system/index.ts @@ -34,9 +34,13 @@ export * from './email-config.zod'; export * from './email-template.zod'; export * from './email-template.form'; export * from './metadata-form-registry'; -export * from './compliance.zod'; +// compliance.zod (GDPR/HIPAA/PCI configs) and masking.zod (role-based data +// masking) were REMOVED per ADR-0056 D8 "design+enforce or remove": both were +// declared-but-never-enforced (no runtime consumer), and compliance-grade +// configuration must never merely LOOK live. FLS (plugin-security) is the +// enforced field-visibility mechanism; a masking/deny layer arrives with +// ADR-0066 ⑦/⑧ if needed. encryption.zod stays (EXPERIMENTAL — roadmap). export * from './encryption.zod'; -export * from './masking.zod'; export * from './security-context.zod'; export * from './incident-response.zod'; export * from './supplier-security.zod'; diff --git a/packages/spec/src/system/masking.test.ts b/packages/spec/src/system/masking.test.ts deleted file mode 100644 index 8832c10a42..0000000000 --- a/packages/spec/src/system/masking.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - MaskingStrategySchema, - MaskingRuleSchema, - MaskingConfigSchema, -} from './masking.zod'; - -describe('MaskingStrategySchema', () => { - it('should accept valid strategies', () => { - const strategies = ['redact', 'partial', 'hash', 'tokenize', 'randomize', 'nullify', 'substitute']; - - strategies.forEach((strategy) => { - expect(() => MaskingStrategySchema.parse(strategy)).not.toThrow(); - }); - }); - - it('should reject invalid strategies', () => { - expect(() => MaskingStrategySchema.parse('invalid')).toThrow(); - expect(() => MaskingStrategySchema.parse('encrypt')).toThrow(); - }); -}); - -describe('MaskingRuleSchema', () => { - it('should accept valid rule with defaults', () => { - const rule = MaskingRuleSchema.parse({ - field: 'ssn', - strategy: 'redact', - }); - - expect(rule.field).toBe('ssn'); - expect(rule.strategy).toBe('redact'); - expect(rule.preserveFormat).toBe(true); - expect(rule.preserveLength).toBe(true); - }); - - it('should accept full rule configuration', () => { - const rule = MaskingRuleSchema.parse({ - field: 'phone_number', - strategy: 'partial', - pattern: '^(\\d{3}).*$', - preserveFormat: false, - preserveLength: false, - roles: ['viewer', 'analyst'], - exemptRoles: ['admin', 'compliance_officer'], - }); - - expect(rule.field).toBe('phone_number'); - expect(rule.strategy).toBe('partial'); - expect(rule.pattern).toBe('^(\\d{3}).*$'); - expect(rule.preserveFormat).toBe(false); - expect(rule.preserveLength).toBe(false); - expect(rule.roles).toEqual(['viewer', 'analyst']); - expect(rule.exemptRoles).toEqual(['admin', 'compliance_officer']); - }); - - it('should reject missing required fields', () => { - expect(() => MaskingRuleSchema.parse({})).toThrow(); - expect(() => MaskingRuleSchema.parse({ field: 'ssn' })).toThrow(); - }); - - it('should reject invalid strategy', () => { - expect(() => MaskingRuleSchema.parse({ field: 'ssn', strategy: 'invalid' })).toThrow(); - }); -}); - -describe('MaskingConfigSchema', () => { - it('should accept valid config with defaults', () => { - const config = MaskingConfigSchema.parse({ - rules: [{ field: 'email', strategy: 'redact' }], - }); - - expect(config.enabled).toBe(false); - expect(config.auditUnmasking).toBe(true); - expect(config.rules).toHaveLength(1); - }); - - it('should accept full configuration', () => { - const config = MaskingConfigSchema.parse({ - enabled: true, - rules: [ - { field: 'ssn', strategy: 'redact' }, - { field: 'phone', strategy: 'partial', pattern: '^(\\d{3})' }, - { field: 'email', strategy: 'hash' }, - ], - auditUnmasking: false, - }); - - expect(config.enabled).toBe(true); - expect(config.rules).toHaveLength(3); - expect(config.auditUnmasking).toBe(false); - }); - - it('should reject missing required rules', () => { - expect(() => MaskingConfigSchema.parse({})).toThrow(); - }); -}); diff --git a/packages/spec/src/system/masking.zod.ts b/packages/spec/src/system/masking.zod.ts deleted file mode 100644 index ef7cc124aa..0000000000 --- a/packages/spec/src/system/masking.zod.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { z } from 'zod'; - -// ⚠️ EXPERIMENTAL — NOT ENFORCED (ADR-0056). Data-masking rules are declared but no -// redaction layer applies them — Field-Level Security (PermissionSet.fields) is the -// enforced field-visibility mechanism today. Authoring masking does NOT change -// behaviour (roadmap M2+; per ADR-0049). - -/** - * Data masking protocol for PII protection - */ -import { lazySchema } from '../shared/lazy-schema'; -export const MaskingStrategySchema = lazySchema(() => z.enum([ - 'redact', // Complete redaction: **** - 'partial', // Partial masking: 138****5678 - 'hash', // Hash value: sha256(value) - 'tokenize', // Tokenization: token-12345 - 'randomize', // Randomize: generate random value - 'nullify', // Null value: null - 'substitute', // Substitute with dummy data -]).describe('Data masking strategy for PII protection')); - -export type MaskingStrategy = z.infer; - -export const MaskingRuleSchema = lazySchema(() => z.object({ - field: z.string().describe('Field name to apply masking to'), - strategy: MaskingStrategySchema.describe('Masking strategy to use'), - pattern: z.string().optional().describe('Regex pattern for partial masking'), - preserveFormat: z.boolean().default(true).describe('Keep the original data format after masking'), - preserveLength: z.boolean().default(true).describe('Keep the original data length after masking'), - roles: z.array(z.string()).optional().describe('Roles that see masked data'), - exemptRoles: z.array(z.string()).optional().describe('Roles that see unmasked data'), -}).describe('Masking rule for a single field')); - -export type MaskingRule = z.infer; -export type MaskingRuleInput = z.input; - -export const MaskingConfigSchema = lazySchema(() => z.object({ - enabled: z.boolean().default(false).describe('Enable data masking'), - rules: z.array(MaskingRuleSchema).describe('List of field-level masking rules'), - auditUnmasking: z.boolean().default(true).describe('Log when masked data is accessed unmasked'), -}).describe('Top-level data masking configuration for PII protection')); - -export type MaskingConfig = z.infer; -export type MaskingConfigInput = z.input;