/** Governed application policy for complete mutation graphs. */ import { MutationIntent } from './request-intent'; export const MISSING_MUTATION_POLICY = 'MUTATION-POLICY-001'; export const MISSING_MUTATION_POLICY_APPROVAL = 'MUTATION-POLICY-002'; export type MutationOperationKind = 'create' | 'update' | 'delete' | 'recover'; export type MutationPolicyIdentity = Readonly<{ policyId: string; version: string; fingerprint: string; }>; export type MutationOperation = Readonly<{ kind: MutationOperationKind; entity: string; entityId?: unknown; originalVersion?: number; changedValues: Readonly>; }>; export type MutationPlan = Readonly<{ executionId: string; requestKey: string; rootEntityType: string; auditReason: string; operations: readonly MutationOperation[]; }>; export type MutationDecision = Readonly<{ verdict: 'allow' | 'deny'; code?: string; message?: string; fieldPaths?: readonly string[]; }>; export interface MutationPolicy { readonly identity: MutationPolicyIdentity; review(context: unknown, plan: MutationPlan): MutationDecision; } export interface MutationPolicyRegistry { resolve(requestKey: string): MutationPolicy | undefined; } export type MutationPolicyApproval = Readonly<{ policy: MutationPolicyIdentity; approvedBy: string; approvedAt: Date; }>; export interface MutationPolicyApprovalProvider { findApproval(identity: MutationPolicyIdentity): MutationPolicyApproval | undefined; } export type MutationPolicySource = 'generated_default' | 'customer'; export type MutationPolicyApprovalStatus = 'not_applicable' | 'missing' | 'approved'; export type MutationOperationSummary = Readonly<{ kind: MutationOperationKind; entity: string; entityId?: unknown; changedFields: readonly string[]; }>; export type MutationGovernanceSnapshot = Readonly<{ executionId: string; requestKey: string; source: MutationPolicySource; policy?: MutationPolicyIdentity; approvalStatus: MutationPolicyApprovalStatus; warningCodes: readonly string[]; operations: readonly MutationOperationSummary[]; }>; export type MutationGovernanceWarning = Readonly<{ snapshot: MutationGovernanceSnapshot; warningCode: string; firstOccurrence: boolean; }>; export interface MutationGovernanceSink { onWarning(context: unknown, warning: MutationGovernanceWarning): void; } export class MutationPolicyError extends Error { constructor(message: string) { super(message); this.name = 'MutationPolicyError'; } } export class DelegatingMutationPolicyRegistry implements MutationPolicyRegistry { constructor(private readonly resolver: (requestKey: string) => MutationPolicy | undefined) {} resolve(requestKey: string): MutationPolicy | undefined { return this.resolver(requestKey); } } export class DelegatingMutationPolicyApprovalProvider implements MutationPolicyApprovalProvider { constructor(private readonly finder: (identity: MutationPolicyIdentity) => MutationPolicyApproval | undefined) {} findApproval(identity: MutationPolicyIdentity): MutationPolicyApproval | undefined { return this.finder(identity); } } export class DelegatingMutationGovernanceSink implements MutationGovernanceSink { constructor(private readonly consumer: (context: unknown, warning: MutationGovernanceWarning) => void) {} onWarning(context: unknown, warning: MutationGovernanceWarning): void { this.consumer(context, warning); } } class ConsoleMutationGovernanceSink implements MutationGovernanceSink { onWarning(_context: unknown, warning: MutationGovernanceWarning): void { if (!warning.firstOccurrence || typeof console === 'undefined') return; console.warn( `TeaQL mutation policy warning code=${warning.warningCode} ` + `requestKey=${warning.snapshot.requestKey} source=${warning.snapshot.source} ` + `approval=${warning.snapshot.approvalStatus}`, ); } } type MutableMutationPolicyProfile = { registry?: MutationPolicyRegistry; approvalProvider?: MutationPolicyApprovalProvider; warningSink: MutationGovernanceSink; }; type MutationExecution = Readonly<{ snapshot: MutationGovernanceSnapshot; }>; let executionSequence = 0; /** @internal Runtime state owned by one UserContext. */ export class MutationPolicyRuntimeState { private readonly profile: MutableMutationPolicyProfile = { warningSink: new ConsoleMutationGovernanceSink(), }; private readonly emittedWarnings = new Set(); private graphActive = false; private graphReviewed = false; private preflight: MutationOperation[] = []; private preflightKeys: string[] = []; private rootEntity?: string; private auditReason?: string; private remaining = new Map(); private graphSnapshot?: MutationGovernanceSnapshot; setRegistry(registry: MutationPolicyRegistry): void { this.profile.registry = registry; } setApprovalProvider(provider: MutationPolicyApprovalProvider): void { this.profile.approvalProvider = provider; } setWarningSink(sink: MutationGovernanceSink): void { this.profile.warningSink = sink; } beginGraph(): void { if (this.graphActive) throw new MutationPolicyError('mutation policy graph is already active'); this.graphActive = true; this.graphReviewed = false; this.preflight = []; this.preflightKeys = []; this.rootEntity = undefined; this.auditReason = undefined; this.remaining.clear(); this.graphSnapshot = undefined; } endGraph(): void { this.graphActive = false; this.graphReviewed = false; this.preflight = []; this.preflightKeys = []; this.rootEntity = undefined; this.auditReason = undefined; this.remaining.clear(); this.graphSnapshot = undefined; } recordPreflight(mutation: unknown): void { if (!this.graphActive) return; if (this.graphReviewed) { throw new MutationPolicyError('mutation preflight cannot change after policy review'); } const operation = operationFromMutation(mutation); this.rootEntity ??= operation.entity; this.auditReason ??= mutationComment(mutation); this.preflight.push(operation); this.preflightKeys.push(operationMatchKey(mutation, operation)); } enterMutation(context: unknown, mutation: unknown): MutationExecution { const operation = operationFromMutation(mutation); if (!this.graphActive) { return { snapshot: this.review(context, this.plan(operation.entity, mutationComment(mutation), [operation])) }; } if (!this.graphReviewed) { if (this.profile.registry && this.preflight.length === 0) { throw new MutationPolicyError( 'customer mutation policy requires complete graph preflight before provider mutation', ); } // Legacy/manual graph callers without a customer policy remain usable. // Generated graphs provide the complete preflight and therefore share // one reviewed plan; the generated-default fallback reviews each legacy // operation independently and emits the stable missing-policy warning. if (!this.profile.registry && this.preflight.length === 0) { return { snapshot: this.review(context, this.plan( operation.entity, mutationComment(mutation), [operation], )) }; } const operations = this.preflight.length ? this.preflight : [operation]; this.graphSnapshot = this.review(context, this.plan( this.rootEntity ?? operations[0].entity, this.auditReason ?? mutationComment(mutation), operations, )); this.remaining.clear(); const plannedKeys = this.preflight.length ? this.preflightKeys : operations.map(operationSignature); for (const signature of plannedKeys) { this.remaining.set(signature, (this.remaining.get(signature) ?? 0) + 1); } this.graphReviewed = true; } this.consume(operationMatchKey(mutation, operation)); return { snapshot: this.graphSnapshot! }; } ensureGraphComplete(): void { if (this.graphReviewed && this.remaining.size) { throw new MutationPolicyError( 'reviewed mutation plan contains operations that were not executed', ); } } review(context: unknown, input: MutationPlan): MutationGovernanceSnapshot { validatePlan(input); const plan = freezePlan(input); const policy = this.profile.registry?.resolve(plan.requestKey); let source: MutationPolicySource; let identity: MutationPolicyIdentity | undefined; let approvalStatus: MutationPolicyApprovalStatus; let warningCodes: readonly string[]; if (!policy) { source = 'generated_default'; approvalStatus = 'not_applicable'; warningCodes = [MISSING_MUTATION_POLICY]; } else { identity = freezeIdentity(policy.identity); const decision = policy.review(context, plan); if (!decision || (decision.verdict !== 'allow' && decision.verdict !== 'deny')) { throw new MutationPolicyError('customer mutation policy returned an invalid decision'); } if (decision.verdict === 'deny') { throw new MutationPolicyError( `[MUTATION POLICY DENIED] ${decision.code || 'MUTATION-POLICY-DENIED'}: ` + `${decision.message || 'mutation rejected'}`, ); } source = 'customer'; const approval = this.profile.approvalProvider?.findApproval(identity); approvalStatus = validApproval(approval, identity) ? 'approved' : 'missing'; warningCodes = approvalStatus === 'approved' ? [] : [MISSING_MUTATION_POLICY_APPROVAL]; } const snapshot: MutationGovernanceSnapshot = Object.freeze({ executionId: plan.executionId, requestKey: plan.requestKey, source, policy: identity, approvalStatus, warningCodes: Object.freeze([...warningCodes]), operations: Object.freeze(plan.operations.map(operation => Object.freeze({ kind: operation.kind, entity: operation.entity, entityId: cloneAndFreeze(operation.entityId), changedFields: Object.freeze(Object.keys(operation.changedValues).sort()), }))), }); for (const code of warningCodes) this.emitWarning(context, snapshot, code); return snapshot; } private plan(root: string, reason: string | undefined, operations: readonly MutationOperation[]): MutationPlan { executionSequence += 1; return freezePlan({ executionId: `teaql-mutation-${executionSequence}`, requestKey: `${root}.saveGraph`, rootEntityType: root, auditReason: new MutationIntent(reason).comment, operations, }); } private consume(signature: string): void { const count = this.remaining.get(signature) ?? 0; if (count < 1) { throw new MutationPolicyError( 'provider mutation is not present in the reviewed graph plan', ); } if (count === 1) this.remaining.delete(signature); else this.remaining.set(signature, count - 1); } private emitWarning(context: unknown, snapshot: MutationGovernanceSnapshot, warningCode: string): void { const policy = snapshot.policy ? `${snapshot.policy.policyId}:${snapshot.policy.version}:${snapshot.policy.fingerprint}` : 'none'; const key = `${snapshot.requestKey}|${policy}|${warningCode}`; const firstOccurrence = !this.emittedWarnings.has(key); this.emittedWarnings.add(key); try { this.profile.warningSink.onWarning(context, Object.freeze({ snapshot, warningCode, firstOccurrence, })); } catch { // Warning delivery must not change persistence semantics. } } } function operationFromMutation(value: any): MutationOperation { const entity = String(value?.entity ?? '').trim(); const action = String(value?.action ?? '').toLowerCase(); const kinds: Record = { create: 'create', update: 'update', delete: 'delete', recover: 'recover', }; if (!entity || !kinds[action]) { throw new MutationPolicyError('mutation must contain a supported entity operation'); } const payload = value?.payload && typeof value.payload === 'object' ? value.payload as Record : {}; return Object.freeze({ kind: kinds[action], entity, entityId: cloneAndFreeze(value?.id ?? payload.id), originalVersion: value?.version ?? value?.expectedVersion, changedValues: cloneRecord(payload), }); } function mutationComment(value: any): string { return new MutationIntent(value?.comment).comment; } function validatePlan(plan: MutationPlan): void { new MutationIntent(plan?.auditReason); if (!plan.executionId?.trim()) throw new MutationPolicyError('execution id is required'); if (!plan.requestKey?.trim()) throw new MutationPolicyError('request key is required'); if (!plan.rootEntityType?.trim()) throw new MutationPolicyError('root entity type is required'); if (!plan.operations.length) throw new MutationPolicyError('mutation plan is empty'); } function freezeIdentity(identity: MutationPolicyIdentity): MutationPolicyIdentity { if (!identity || !identity.policyId?.trim() || !identity.version?.trim() || !identity.fingerprint?.trim()) { throw new MutationPolicyError('customer mutation policy identity is invalid'); } return Object.freeze({ policyId: identity.policyId.trim(), version: identity.version.trim(), fingerprint: identity.fingerprint.trim(), }); } function validApproval(approval: MutationPolicyApproval | undefined, identity: MutationPolicyIdentity): boolean { return !!approval && sameIdentity(approval.policy, identity) && !!approval.approvedBy?.trim() && approval.approvedAt instanceof Date && Number.isFinite(approval.approvedAt.getTime()) && approval.approvedAt.getTime() !== 0; } function sameIdentity(left: MutationPolicyIdentity, right: MutationPolicyIdentity): boolean { return left.policyId === right.policyId && left.version === right.version && left.fingerprint === right.fingerprint; } function freezePlan(plan: MutationPlan): MutationPlan { return Object.freeze({ executionId: plan.executionId, requestKey: plan.requestKey, rootEntityType: plan.rootEntityType, auditReason: plan.auditReason, operations: Object.freeze(plan.operations.map(operation => Object.freeze({ kind: operation.kind, entity: operation.entity, entityId: cloneAndFreeze(operation.entityId), originalVersion: operation.originalVersion, changedValues: cloneRecord(operation.changedValues), }))), }); } function cloneAndFreeze(value: unknown, ancestors = new WeakSet()): unknown { if (value === null || value === undefined || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return value; if (value instanceof Date) return value.toISOString(); if (typeof value === 'object') { if (ancestors.has(value)) { throw new MutationPolicyError('mutation policy values must not contain cycles'); } ancestors.add(value); try { if (Array.isArray(value)) { return Object.freeze(value.map(item => cloneAndFreeze(item, ancestors))); } const record = value as Record; if ('id' in record && Object.keys(record).some(key => key !== 'id')) { return cloneAndFreeze(record.id, ancestors); } return Object.freeze(Object.fromEntries( Object.entries(record).map(([key, item]) => [key, cloneAndFreeze(item, ancestors)]), )); } finally { ancestors.delete(value); } } return String(value); } function cloneRecord(value: Readonly>): Readonly> { return Object.freeze(Object.fromEntries( Object.entries(value).map(([key, item]) => [key, cloneAndFreeze(item)]), )); } function operationSignature(operation: MutationOperation): string { return canonicalJSON({ kind: operation.kind, entity: operation.entity, entityId: operation.entityId, originalVersion: operation.originalVersion, changedValues: operation.changedValues, }); } function operationMatchKey(mutation: any, operation: MutationOperation): string { const ledgerKey = mutation?.ledgerKey; if (ledgerKey && typeof ledgerKey === 'object' && String(ledgerKey.entity ?? '').trim() && ledgerKey.id !== undefined && ledgerKey.id !== null) { return `ledger:${String(ledgerKey.entity).trim()}:${canonicalJSON(ledgerKey.id)}`; } return `operation:${operationSignature(operation)}`; } function canonicalJSON(value: unknown): string { if (typeof value === 'bigint') return JSON.stringify(`${value.toString()}n`); if (value === undefined) return '""'; if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(canonicalJSON).join(',')}]`; return `{${Object.entries(value as Record) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJSON(item)}`).join(',')}}`; }