From 16bcb563c18eb34a7e55e1d7dbb320f50708b09b Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:55:54 +0800 Subject: [PATCH 1/3] feat(spec)!: remove dead PolicySchema / definePolicy + stack `policies` (#1882, 11.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PolicySchema (org security policy: password/network/session/audit) was 100% unenforced (no runtime reader). Per ADR-0049 enforce-or-remove, removed: - spec: delete security/policy.zod.ts + drop the `policies` stack field and its collection wiring (MAP_SUPPORTED_FIELDS / METADATA_ALIASES in shared/metadata-collection.zod.ts + the stack key list). - downstream-contract: drop DcPolicy fixture + contract case. - examples app-crm/app-showcase: drop unused policy defs. - regen api-surface (only the 8 Policy exports removed). Verified downstream: ../hotcrm + ../templates have ZERO Policy-API usage (templates only imports SharingRule, untouched). spec security 123 + downstream-contract 14 + objectql 718 green; api-surface check ✓; spec/examples build green. Co-Authored-By: Claude Opus 4.8 --- .changeset/v11-remove-policyschema.md | 22 ++ examples/app-crm/objectstack.config.ts | 3 - examples/app-crm/src/security/crm-policy.ts | 59 --- examples/app-crm/src/security/index.ts | 1 - examples/app-showcase/objectstack.config.ts | 2 - examples/app-showcase/src/security/index.ts | 17 - .../src/additional-domains.fixtures.ts | 9 +- .../downstream-contract/test/contract.test.ts | 3 +- packages/spec/api-surface-signatures.json | 1 - packages/spec/api-surface.json | 9 - packages/spec/src/index.ts | 1 - packages/spec/src/security/index.ts | 1 - packages/spec/src/security/policy.test.ts | 347 ------------------ packages/spec/src/security/policy.zod.ts | 90 ----- .../src/shared/metadata-collection.zod.ts | 2 - packages/spec/src/stack.zod.ts | 3 - 16 files changed, 24 insertions(+), 546 deletions(-) create mode 100644 .changeset/v11-remove-policyschema.md delete mode 100644 examples/app-crm/src/security/crm-policy.ts delete mode 100644 packages/spec/src/security/policy.test.ts delete mode 100644 packages/spec/src/security/policy.zod.ts diff --git a/.changeset/v11-remove-policyschema.md b/.changeset/v11-remove-policyschema.md new file mode 100644 index 0000000000..79b7b1b784 --- /dev/null +++ b/.changeset/v11-remove-policyschema.md @@ -0,0 +1,22 @@ +--- +"@objectstack/spec": major +--- + +Remove the dead `PolicySchema` / `definePolicy` and the stack `policies` collection (#1882, ADR-0049). + +`PolicySchema` (password / network / session / audit "org security policy") was +**100% unenforced** — no runtime consumer ever read it. Per ADR-0049 +(enforce-or-remove) it is removed rather than implemented: + +- `@objectstack/spec`: delete `security/policy.zod.ts` (`PolicySchema`, + `Password/Network/Session/AuditPolicySchema`, `definePolicy`); drop the + `policies` field from the stack schema and the `policies` collection wiring + (`MAP_SUPPORTED_FIELDS`, `METADATA_ALIASES`). +- `@objectstack/downstream-contract`: drop the `DcPolicy` fixture/case (the + contract gate stays green — `SharingRule` / `PermissionSet` are unaffected). +- Examples (`app-crm`, `app-showcase`): drop their unused policy definitions. + +No migration needed for consumers: `policies` was never enforced. `SharingRule`, +`PermissionSet`, RLS, and all `*PolicySchema` siblings (retry/retention/RLS/etc.) +are unrelated and unchanged. Verified: hotcrm + templates have zero Policy-API +usage; downstream-contract gate green. diff --git a/examples/app-crm/objectstack.config.ts b/examples/app-crm/objectstack.config.ts index 89ddd6df8c..16a62f05cd 100644 --- a/examples/app-crm/objectstack.config.ts +++ b/examples/app-crm/objectstack.config.ts @@ -22,8 +22,6 @@ import { HighValueOpportunitySharingRule, RepLeadSharingRule, WonDealActivitySharingRule, - CrmDefaultPolicy, - CrmFinancePolicy, } from './src/security/index.js'; import { CrmSeedData } from './src/data/index.js'; import { LeadCsvImportMapping, ContactJsonSyncMapping } from './src/data/crm-mappings.js'; @@ -118,7 +116,6 @@ export default defineStack({ RepLeadSharingRule, WonDealActivitySharingRule, ], - policies: [CrmDefaultPolicy, CrmFinancePolicy], // API apis: [PipelineSummaryEndpoint, LeadConvertEndpoint, MarketingWebhookEndpoint], diff --git a/examples/app-crm/src/security/crm-policy.ts b/examples/app-crm/src/security/crm-policy.ts deleted file mode 100644 index 577c7d45ff..0000000000 --- a/examples/app-crm/src/security/crm-policy.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { definePolicy } from '@objectstack/spec/security'; - -/** - * Default CRM security policy applied to all users. - * Enforces password complexity, session limits, and audit logging. - */ -export const CrmDefaultPolicy = definePolicy({ - name: 'crm_default_policy', - password: { - minLength: 12, - requireUppercase: true, - requireLowercase: true, - requireNumbers: true, - requireSymbols: true, - expirationDays: 90, - historyCount: 5, - }, - session: { - idleTimeout: 30, - absoluteTimeout: 480, - forceMfa: false, - }, - audit: { - logRetentionDays: 365, - sensitiveFields: ['annual_revenue', 'discount_percent'], - captureRead: false, - }, - isDefault: true, -}); - -/** - * Strict policy for Finance Approvers — requires MFA and shorter sessions. - */ -export const CrmFinancePolicy = definePolicy({ - name: 'crm_finance_policy', - password: { - minLength: 16, - requireUppercase: true, - requireLowercase: true, - requireNumbers: true, - requireSymbols: true, - expirationDays: 60, - historyCount: 10, - }, - session: { - idleTimeout: 15, - absoluteTimeout: 240, - forceMfa: true, - }, - audit: { - logRetentionDays: 730, - sensitiveFields: ['amount', 'discount_percent', 'annual_revenue'], - captureRead: true, - }, - isDefault: false, - assignedProfiles: ['finance_approver'], -}); diff --git a/examples/app-crm/src/security/index.ts b/examples/app-crm/src/security/index.ts index adf35a6800..c4d70386a4 100644 --- a/examples/app-crm/src/security/index.ts +++ b/examples/app-crm/src/security/index.ts @@ -14,4 +14,3 @@ export { WonDealActivitySharingRule, } from './sharing-rules.js'; -export { CrmDefaultPolicy, CrmFinancePolicy } from './crm-policy.js'; diff --git a/examples/app-showcase/objectstack.config.ts b/examples/app-showcase/objectstack.config.ts index c92798eae4..395c34587f 100644 --- a/examples/app-showcase/objectstack.config.ts +++ b/examples/app-showcase/objectstack.config.ts @@ -33,7 +33,6 @@ import { allRoles, allPermissionSets, allSharingRules, - allPolicies, } from './src/security/index.js'; import { allThemes } from './src/themes/index.js'; import { ShowcaseTranslationBundle } from './src/translations/index.js'; @@ -174,7 +173,6 @@ export default defineStack({ roles: allRoles, permissions: allPermissionSets, sharingRules: allSharingRules, - policies: allPolicies, // Seed data data: ShowcaseSeedData, diff --git a/examples/app-showcase/src/security/index.ts b/examples/app-showcase/src/security/index.ts index 9e8ad3175f..3b40155dd3 100644 --- a/examples/app-showcase/src/security/index.ts +++ b/examples/app-showcase/src/security/index.ts @@ -172,24 +172,7 @@ export const ContributorTaskSharingRule = { active: true, }; -// ── Org security policy ───────────────────────────────────────────────────── -export const ShowcasePolicy = { - name: 'showcase_default_policy', - password: { - minLength: 12, - requireUppercase: true, - requireLowercase: true, - requireNumbers: true, - requireSymbols: true, - expirationDays: 90, - historyCount: 5, - }, - session: { idleTimeout: 30, absoluteTimeout: 480, forceMfa: false }, - audit: { logRetentionDays: 365, sensitiveFields: ['budget', 'spent'], captureRead: false }, - isDefault: true, -}; export const allRoles = [ContributorRole, ManagerRole, ExecRole]; export const allPermissionSets = [ContributorPermissionSet, MemberDefaultProfile]; export const allSharingRules = [RedProjectSharingRule, HighValueRedProjectRule, ContributorTaskSharingRule]; -export const allPolicies = [ShowcasePolicy]; diff --git a/packages/downstream-contract/src/additional-domains.fixtures.ts b/packages/downstream-contract/src/additional-domains.fixtures.ts index c61d9993c8..ae55841d6b 100644 --- a/packages/downstream-contract/src/additional-domains.fixtures.ts +++ b/packages/downstream-contract/src/additional-domains.fixtures.ts @@ -7,7 +7,7 @@ // and DO NOT edit them to make a failing spec change pass — see the README. import type { DatasourceInput, MappingInput, CubeInput, ObjectExtensionInput } from '@objectstack/spec/data'; import type { ConnectorInput } from '@objectstack/spec/integration'; -import type { PolicyInput, SharingRuleInput, PermissionSetInput } from '@objectstack/spec/security'; +import type { SharingRuleInput, PermissionSetInput } from '@objectstack/spec/security'; import type { RoleInput } from '@objectstack/spec/identity'; import type { EmailTemplateDefinitionInput, TranslationBundleInput } from '@objectstack/spec/system'; import type { WebhookInput } from '@objectstack/spec/automation'; @@ -43,13 +43,6 @@ export const DcConnector: ConnectorInput = { ], }; -export const DcPolicy: PolicyInput = { - name: 'dc_default_policy', - isDefault: true, - password: { minLength: 12, requireUppercase: true, requireNumbers: true }, - session: { idleTimeout: 30, absoluteTimeout: 480 }, -}; - export const DcSharingRule: SharingRuleInput = { type: 'criteria', name: 'dc_share_customers', diff --git a/packages/downstream-contract/test/contract.test.ts b/packages/downstream-contract/test/contract.test.ts index 364353d053..f8b57dacec 100644 --- a/packages/downstream-contract/test/contract.test.ts +++ b/packages/downstream-contract/test/contract.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest'; import { ActionSchema, ReportSchema, PageSchema, ThemeSchema } from '@objectstack/spec/ui'; import { DatasourceSchema, MappingSchema, CubeSchema, ObjectExtensionSchema } from '@objectstack/spec/data'; import { ConnectorSchema } from '@objectstack/spec/integration'; -import { PolicySchema, SharingRuleSchema, PermissionSetSchema } from '@objectstack/spec/security'; +import { SharingRuleSchema, PermissionSetSchema } from '@objectstack/spec/security'; import { RoleSchema } from '@objectstack/spec/identity'; import { EmailTemplateDefinitionSchema, TranslationBundleSchema } from '@objectstack/spec/system'; import { WebhookSchema } from '@objectstack/spec/automation'; @@ -35,7 +35,6 @@ describe('downstream consumer contract (#2035)', () => { const cases: Array<[string, { parse: (v: unknown) => unknown }, unknown]> = [ ['Datasource', DatasourceSchema, more.DcDatasource], ['Connector', ConnectorSchema, more.DcConnector], - ['Policy', PolicySchema, more.DcPolicy], ['SharingRule', SharingRuleSchema, more.DcSharingRule], ['Role', RoleSchema, more.DcRole], ['PermissionSet', PermissionSetSchema, more.DcPermissionSet], diff --git a/packages/spec/api-surface-signatures.json b/packages/spec/api-surface-signatures.json index bba6ab1e20..bca6990b5d 100644 --- a/packages/spec/api-surface-signatures.json +++ b/packages/spec/api-surface-signatures.json @@ -14,7 +14,6 @@ "defineObjectExtension": "sha256:5286253308912bb1", "definePage": "sha256:e80363c256809a11", "definePermissionSet": "sha256:67b188ae181994cd", - "definePolicy": "sha256:f66d2be4a3d5fe98", "defineReport": "sha256:f7e85ba0996a5824", "defineRole": "sha256:40b8e11786c4ecbb", "defineSharingRule": "sha256:03347645bbda2880", diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 8f905ef7c8..c49d3a5166 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -74,7 +74,6 @@ "defineObjectExtension (function)", "definePage (function)", "definePermissionSet (function)", - "definePolicy (function)", "defineReport (function)", "defineRole (function)", "defineSharingRule (function)", @@ -3777,12 +3776,10 @@ "vercelStaticSiteConnectorExample (const)" ], "./security": [ - "AuditPolicySchema (const)", "CriteriaSharingRule (type)", "CriteriaSharingRuleSchema (const)", "FieldPermission (type)", "FieldPermissionSchema (const)", - "NetworkPolicySchema (const)", "OWDModel (const)", "ObjectAccessScope (type)", "ObjectAccessScopeSchema (const)", @@ -3790,13 +3787,9 @@ "ObjectPermissionSchema (const)", "OwnerSharingRule (type)", "OwnerSharingRuleSchema (const)", - "PasswordPolicySchema (const)", "PermissionSet (type)", "PermissionSetInput (type)", "PermissionSetSchema (const)", - "Policy (type)", - "PolicyInput (type)", - "PolicySchema (const)", "RLS (const)", "RLSAuditConfig (type)", "RLSAuditConfigSchema (const)", @@ -3811,7 +3804,6 @@ "RLSUserContextSchema (const)", "RowLevelSecurityPolicy (type)", "RowLevelSecurityPolicySchema (const)", - "SessionPolicySchema (const)", "ShareRecipientType (const)", "SharingLevel (const)", "SharingRule (type)", @@ -3824,7 +3816,6 @@ "TerritorySchema (const)", "TerritoryType (const)", "definePermissionSet (function)", - "definePolicy (function)", "defineSharingRule (function)", "permissionForm (const)" ], diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index 6fc7cabdef..ea76a79fc9 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -87,7 +87,6 @@ export { defineSkill } from './ai/skill.zod'; // `any` (the #2023 failure mode). Input-shape config + runtime `.parse()`. export { defineDatasource } from './data/datasource.zod'; export { defineConnector } from './integration/connector.zod'; -export { definePolicy } from './security/policy.zod'; export { defineSharingRule } from './security/sharing.zod'; export { defineRole } from './identity/role.zod'; export { definePermissionSet } from './security/permission.zod'; diff --git a/packages/spec/src/security/index.ts b/packages/spec/src/security/index.ts index c9f54dc351..c6ba743609 100644 --- a/packages/spec/src/security/index.ts +++ b/packages/spec/src/security/index.ts @@ -15,4 +15,3 @@ export * from './permission.form'; export * from './sharing.zod'; export * from './territory.zod'; export * from './rls.zod'; -export * from './policy.zod'; diff --git a/packages/spec/src/security/policy.test.ts b/packages/spec/src/security/policy.test.ts deleted file mode 100644 index 967e6f2dc1..0000000000 --- a/packages/spec/src/security/policy.test.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - PolicySchema, - PasswordPolicySchema, - NetworkPolicySchema, - SessionPolicySchema, - AuditPolicySchema, - type Policy, -} from './policy.zod'; - -describe('PasswordPolicySchema', () => { - it('should accept valid minimal password policy', () => { - const policy = PasswordPolicySchema.parse({}); - - expect(policy.minLength).toBe(8); - expect(policy.requireUppercase).toBe(true); - expect(policy.requireLowercase).toBe(true); - expect(policy.requireNumbers).toBe(true); - expect(policy.requireSymbols).toBe(false); - expect(policy.historyCount).toBe(3); - }); - - it('should accept custom password policy', () => { - const policy = PasswordPolicySchema.parse({ - minLength: 12, - requireUppercase: true, - requireLowercase: true, - requireNumbers: true, - requireSymbols: true, - expirationDays: 90, - historyCount: 5, - }); - - expect(policy.minLength).toBe(12); - expect(policy.requireSymbols).toBe(true); - expect(policy.expirationDays).toBe(90); - }); - - it('should accept password expiration policy', () => { - const policy = PasswordPolicySchema.parse({ - expirationDays: 90, - }); - - expect(policy.expirationDays).toBe(90); - }); - - it('should accept password history policy', () => { - const policy = PasswordPolicySchema.parse({ - historyCount: 10, - }); - - expect(policy.historyCount).toBe(10); - }); - - it('should accept relaxed password policy', () => { - const policy = PasswordPolicySchema.parse({ - minLength: 6, - requireUppercase: false, - requireLowercase: false, - requireNumbers: false, - requireSymbols: false, - }); - - expect(policy.minLength).toBe(6); - expect(policy.requireUppercase).toBe(false); - }); -}); - -describe('NetworkPolicySchema', () => { - it('should accept valid network policy', () => { - const policy = NetworkPolicySchema.parse({ - trustedRanges: ['10.0.0.0/8', '192.168.0.0/16'], - }); - - expect(policy.trustedRanges).toEqual(['10.0.0.0/8', '192.168.0.0/16']); - expect(policy.blockUnknown).toBe(false); - expect(policy.vpnRequired).toBe(false); - }); - - it('should accept network policy with blocking', () => { - const policy = NetworkPolicySchema.parse({ - trustedRanges: ['10.0.0.0/8'], - blockUnknown: true, - }); - - expect(policy.blockUnknown).toBe(true); - }); - - it('should accept VPN requirement', () => { - const policy = NetworkPolicySchema.parse({ - trustedRanges: [], - vpnRequired: true, - }); - - expect(policy.vpnRequired).toBe(true); - }); - - it('should accept CIDR ranges', () => { - const policy = NetworkPolicySchema.parse({ - trustedRanges: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'], - }); - - expect(policy.trustedRanges).toHaveLength(3); - }); - - it('should accept specific IP addresses', () => { - const policy = NetworkPolicySchema.parse({ - trustedRanges: ['203.0.113.1/32', '198.51.100.42/32'], - }); - - expect(policy.trustedRanges).toContain('203.0.113.1/32'); - }); -}); - -describe('SessionPolicySchema', () => { - it('should accept valid minimal session policy', () => { - const policy = SessionPolicySchema.parse({}); - - expect(policy.idleTimeout).toBe(30); - expect(policy.absoluteTimeout).toBe(480); - expect(policy.forceMfa).toBe(false); - }); - - it('should accept custom idle timeout', () => { - const policy = SessionPolicySchema.parse({ - idleTimeout: 15, - }); - - expect(policy.idleTimeout).toBe(15); - }); - - it('should accept custom absolute timeout', () => { - const policy = SessionPolicySchema.parse({ - absoluteTimeout: 720, - }); - - expect(policy.absoluteTimeout).toBe(720); - }); - - it('should accept MFA requirement', () => { - const policy = SessionPolicySchema.parse({ - forceMfa: true, - }); - - expect(policy.forceMfa).toBe(true); - }); - - it('should accept strict session policy', () => { - const policy = SessionPolicySchema.parse({ - idleTimeout: 10, - absoluteTimeout: 60, - forceMfa: true, - }); - - expect(policy.idleTimeout).toBe(10); - expect(policy.absoluteTimeout).toBe(60); - expect(policy.forceMfa).toBe(true); - }); -}); - -describe('AuditPolicySchema', () => { - it('should accept valid minimal audit policy', () => { - const policy = AuditPolicySchema.parse({ - sensitiveFields: [], - }); - - expect(policy.logRetentionDays).toBe(180); - expect(policy.captureRead).toBe(false); - }); - - it('should accept custom retention period', () => { - const policy = AuditPolicySchema.parse({ - logRetentionDays: 365, - sensitiveFields: [], - }); - - expect(policy.logRetentionDays).toBe(365); - }); - - it('should accept sensitive field redaction', () => { - const policy = AuditPolicySchema.parse({ - sensitiveFields: ['password', 'ssn', 'credit_card'], - }); - - expect(policy.sensitiveFields).toEqual(['password', 'ssn', 'credit_card']); - }); - - it('should accept read capture policy', () => { - const policy = AuditPolicySchema.parse({ - sensitiveFields: [], - captureRead: true, - }); - - expect(policy.captureRead).toBe(true); - }); - - it('should accept compliance audit policy', () => { - const policy = AuditPolicySchema.parse({ - logRetentionDays: 2555, // 7 years for financial compliance - sensitiveFields: ['ssn', 'tax_id', 'bank_account', 'credit_card'], - captureRead: true, - }); - - expect(policy.logRetentionDays).toBe(2555); - expect(policy.sensitiveFields).toHaveLength(4); - }); -}); - -describe('PolicySchema', () => { - it('should accept valid minimal policy', () => { - const policy: Policy = { - name: 'default_policy', - }; - - expect(() => PolicySchema.parse(policy)).not.toThrow(); - }); - - it('should validate policy name format (snake_case)', () => { - expect(() => PolicySchema.parse({ - name: 'valid_policy_name', - })).not.toThrow(); - - expect(() => PolicySchema.parse({ - name: 'InvalidPolicy', - })).toThrow(); - - expect(() => PolicySchema.parse({ - name: 'invalid-policy', - })).toThrow(); - }); - - it('should accept policy with all sub-policies', () => { - const policy = PolicySchema.parse({ - name: 'comprehensive_policy', - password: { - minLength: 12, - requireSymbols: true, - expirationDays: 90, - }, - network: { - trustedRanges: ['10.0.0.0/8'], - blockUnknown: true, - }, - session: { - idleTimeout: 15, - forceMfa: true, - }, - audit: { - logRetentionDays: 365, - sensitiveFields: ['password', 'ssn'], - captureRead: false, - }, - }); - - expect(policy.password?.minLength).toBe(12); - expect(policy.network?.blockUnknown).toBe(true); - expect(policy.session?.forceMfa).toBe(true); - expect(policy.audit?.logRetentionDays).toBe(365); - }); - - it('should accept default policy flag', () => { - const policy = PolicySchema.parse({ - name: 'default_policy', - isDefault: true, - }); - - expect(policy.isDefault).toBe(true); - }); - - it('should accept profile assignments', () => { - const policy = PolicySchema.parse({ - name: 'admin_policy', - assignedProfiles: ['admin', 'super_admin'], - }); - - expect(policy.assignedProfiles).toEqual(['admin', 'super_admin']); - }); - - it('should handle enterprise security policy', () => { - const policy = PolicySchema.parse({ - name: 'enterprise_security', - password: { - minLength: 14, - requireUppercase: true, - requireLowercase: true, - requireNumbers: true, - requireSymbols: true, - expirationDays: 60, - historyCount: 10, - }, - network: { - trustedRanges: ['10.0.0.0/8'], - blockUnknown: true, - vpnRequired: true, - }, - session: { - idleTimeout: 10, - absoluteTimeout: 480, - forceMfa: true, - }, - audit: { - logRetentionDays: 2555, - sensitiveFields: ['password', 'ssn', 'tax_id', 'credit_card'], - captureRead: true, - }, - isDefault: false, - assignedProfiles: ['admin', 'finance'], - }); - - expect(policy.password?.minLength).toBe(14); - expect(policy.network?.vpnRequired).toBe(true); - expect(policy.session?.forceMfa).toBe(true); - expect(policy.audit?.captureRead).toBe(true); - }); - - it('should handle development policy', () => { - const policy = PolicySchema.parse({ - name: 'dev_policy', - password: { - minLength: 6, - requireUppercase: false, - requireSymbols: false, - }, - session: { - idleTimeout: 120, - forceMfa: false, - }, - isDefault: true, - }); - - expect(policy.password?.minLength).toBe(6); - expect(policy.session?.forceMfa).toBe(false); - expect(policy.isDefault).toBe(true); - }); - - it('should reject policy without required fields', () => { - expect(() => PolicySchema.parse({})).toThrow(); - }); - - it('should apply default values for isDefault', () => { - const policy = PolicySchema.parse({ - name: 'test_policy', - }); - - expect(policy.isDefault).toBe(false); - }); -}); diff --git a/packages/spec/src/security/policy.zod.ts b/packages/spec/src/security/policy.zod.ts deleted file mode 100644 index 565962d067..0000000000 --- a/packages/spec/src/security/policy.zod.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { z } from 'zod'; - -// ⚠️ EXPERIMENTAL — NOT ENFORCED (ADR-0049, #1882). The entire PolicySchema tree -// (password / network / session / audit) is parsed but has no runtime consumer; -// `better-auth` runs hardcoded defaults regardless. Every property below carries -// the `[EXPERIMENTAL — not enforced]` marker so the no-op is explicit in the -// generated reference docs and to the spec-liveness gate — authoring any of these -// does NOT change behaviour. Do not rely on them for compliance. - -/** - * Password Complexity Policy - */ -import { lazySchema } from '../shared/lazy-schema'; -export const PasswordPolicySchema = lazySchema(() => z.object({ - minLength: z.number().default(8).describe('[EXPERIMENTAL — not enforced] Minimum password length'), - requireUppercase: z.boolean().default(true).describe('[EXPERIMENTAL — not enforced] Require an uppercase letter'), - requireLowercase: z.boolean().default(true).describe('[EXPERIMENTAL — not enforced] Require a lowercase letter'), - requireNumbers: z.boolean().default(true).describe('[EXPERIMENTAL — not enforced] Require a number'), - requireSymbols: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Require a symbol'), - expirationDays: z.number().optional().describe('[EXPERIMENTAL — not enforced] Force password change every X days'), - historyCount: z.number().default(3).describe('[EXPERIMENTAL — not enforced] Prevent reusing last X passwords'), -})); - -/** - * Network Access Policy (IP Whitelisting) - */ -export const NetworkPolicySchema = lazySchema(() => z.object({ - trustedRanges: z.array(z.string()).describe('[EXPERIMENTAL — not enforced] CIDR ranges allowed to access (e.g. 10.0.0.0/8)'), - blockUnknown: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Block all IPs not in trusted ranges'), - vpnRequired: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Require VPN to access'), -})); - -/** - * Session Policy - */ -export const SessionPolicySchema = lazySchema(() => z.object({ - idleTimeout: z.number().default(30).describe('[EXPERIMENTAL — not enforced] Minutes before idle session logout'), - absoluteTimeout: z.number().default(480).describe('[EXPERIMENTAL — not enforced] Max session duration (minutes)'), - forceMfa: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Require 2FA for all users'), -})); - -/** - * Audit Retention Policy - */ -export const AuditPolicySchema = lazySchema(() => z.object({ - logRetentionDays: z.number().default(180).describe('[EXPERIMENTAL — not enforced] Days to retain audit logs'), - sensitiveFields: z.array(z.string()).describe('[EXPERIMENTAL — not enforced] Fields to redact in logs (e.g. password, ssn)'), - captureRead: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Log read access (High volume!)'), -})); - -/** - * Security Policy Schema - * "The Cloud Compliance Contract" - * - * ⚠️ EXPERIMENTAL — NOT ENFORCED (ADR-0049, #1882). - * This schema is currently a no-op: it is not registered as a metadata type and - * has no runtime consumer. Password complexity, session idle/absolute timeout, - * `forceMfa`, the IP allow-list (`trustedRanges`/`vpnRequired`) and audit - * retention/redaction are all parsed but enforced by nothing — `better-auth` - * runs hardcoded defaults regardless. Authoring a policy here does NOT change - * behaviour. Treat as a forward-looking contract only; do not rely on it for - * compliance. Enforcement (or removal) is tracked by #1882 for M2. - */ -export const PolicySchema = lazySchema(() => z.object({ - name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('[EXPERIMENTAL — not enforced] Policy Name'), - - password: PasswordPolicySchema.optional(), - network: NetworkPolicySchema.optional(), - session: SessionPolicySchema.optional(), - audit: AuditPolicySchema.optional(), - - /** Assignment */ - isDefault: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Apply to all users by default'), - assignedProfiles: z.array(z.string()).optional().describe('[EXPERIMENTAL — not enforced] Apply to specific profiles'), -})); - -export type Policy = z.infer; -/** Authoring input for {@link Policy} — defaulted fields are optional. */ -export type PolicyInput = z.input; - -/** - * Type-safe factory for a security / compliance policy. Validates at authoring time via - * `.parse()` and accepts input-shape config (optional defaults, CEL - * shorthand) — preferred over a bare `: Policy` literal. - */ -export function definePolicy(config: z.input): Policy { - return PolicySchema.parse(config); -} diff --git a/packages/spec/src/shared/metadata-collection.zod.ts b/packages/spec/src/shared/metadata-collection.zod.ts index 0b319732aa..378beaa8de 100644 --- a/packages/spec/src/shared/metadata-collection.zod.ts +++ b/packages/spec/src/shared/metadata-collection.zod.ts @@ -82,7 +82,6 @@ export const MAP_SUPPORTED_FIELDS = [ 'roles', 'permissions', 'sharingRules', - 'policies', 'apis', 'webhooks', 'agents', @@ -121,7 +120,6 @@ export const PLURAL_TO_SINGULAR: Record = { permissions: 'permission', profiles: 'profile', sharingRules: 'sharing_rule', - policies: 'policy', apis: 'api', webhooks: 'webhook', agents: 'agent', diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 4915e366cf..2816cea3a6 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -32,7 +32,6 @@ import { JobSchema } from './system/job.zod'; import { RoleSchema } from './identity/role.zod'; import { PermissionSetSchema } from './security/permission.zod'; import { SharingRuleSchema } from './security/sharing.zod'; -import { PolicySchema } from './security/policy.zod'; import { ApiEndpointSchema } from './api/endpoint.zod'; import { FeatureFlagSchema } from './kernel/feature.zod'; @@ -235,7 +234,6 @@ export const ObjectStackDefinitionSchema = lazySchema(() => z.object({ roles: z.array(RoleSchema).optional().describe('User Roles hierarchy'), permissions: z.array(PermissionSetSchema).optional().describe('Permission Sets and Profiles'), sharingRules: z.array(SharingRuleSchema).optional().describe('Record Sharing Rules'), - policies: z.array(PolicySchema).optional().describe('Security & Compliance Policies'), /** * ObjectAPI: API Layer @@ -949,7 +947,6 @@ const CONCAT_ARRAY_FIELDS = [ 'roles', 'permissions', 'sharingRules', - 'policies', 'apis', 'webhooks', 'agents', From b817e9343e1250e3e060f7517ed60d1b4e53f305 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:01:05 +0800 Subject: [PATCH 2/3] =?UTF-8?q?test(spec):=20drop=20policies=E2=86=94polic?= =?UTF-8?q?y=20mapping=20assertions=20after=20PolicySchema=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit metadata-collection.test.ts asserted the now-removed `policies`/`policy` collection alias (PLURAL_TO_SINGULAR / pluralToSingular / singularToPlural). Full spec suite (6595) now green. Co-Authored-By: Claude Opus 4.8 --- packages/spec/src/shared/metadata-collection.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/spec/src/shared/metadata-collection.test.ts b/packages/spec/src/shared/metadata-collection.test.ts index 1d9a95afa3..3cae56fcca 100644 --- a/packages/spec/src/shared/metadata-collection.test.ts +++ b/packages/spec/src/shared/metadata-collection.test.ts @@ -288,7 +288,6 @@ describe('PLURAL_TO_SINGULAR / SINGULAR_TO_PLURAL', () => { }); it('should map irregular plural forms correctly', () => { - expect(PLURAL_TO_SINGULAR['policies']).toBe('policy'); expect(PLURAL_TO_SINGULAR['sharingRules']).toBe('sharing_rule'); expect(PLURAL_TO_SINGULAR['analyticsCubes']).toBe('analytics_cube'); expect(PLURAL_TO_SINGULAR['ragPipelines']).toBe('rag_pipeline'); @@ -304,7 +303,6 @@ describe('PLURAL_TO_SINGULAR / SINGULAR_TO_PLURAL', () => { describe('pluralToSingular', () => { it('should convert known plural to singular', () => { expect(pluralToSingular('apps')).toBe('app'); - expect(pluralToSingular('policies')).toBe('policy'); expect(pluralToSingular('sharingRules')).toBe('sharing_rule'); }); @@ -317,7 +315,6 @@ describe('pluralToSingular', () => { describe('singularToPlural', () => { it('should convert known singular to plural', () => { expect(singularToPlural('app')).toBe('apps'); - expect(singularToPlural('policy')).toBe('policies'); expect(singularToPlural('sharing_rule')).toBe('sharingRules'); }); From 57fceabe7f5b121d5657487942da7fe100d02758 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:10:57 +0800 Subject: [PATCH 3/3] test(app-crm): drop the 'has security policies' smoke assertion after PolicySchema removal stack.policies no longer exists. Exhaustive repo grep confirms no remaining .policies collection access or definePolicy/DcPolicy usage. Co-Authored-By: Claude Opus 4.8 --- examples/app-crm/test/smoke.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/examples/app-crm/test/smoke.test.ts b/examples/app-crm/test/smoke.test.ts index b147c61da5..43c48c923a 100644 --- a/examples/app-crm/test/smoke.test.ts +++ b/examples/app-crm/test/smoke.test.ts @@ -109,11 +109,6 @@ describe('app-crm minimal metadata bundle', () => { expect(rules.some((r) => r.type === 'owner')).toBe(true); }); - it('has security policies', () => { - const policies = stack.policies ?? []; - expect(policies.length).toBeGreaterThanOrEqual(1); - expect(policies.some((p) => p.isDefault)).toBe(true); - }); it('has API endpoints', () => { expect((stack.apis ?? []).length).toBeGreaterThanOrEqual(2);