/** * Mosaic Native Kanban — frozen recovery-posture contract v1. * Recovery posture is configurable; SOT, write-health, Coordinator authority, * and gate semantics are not fields and cannot be overridden. */ export const RECOVERY_POSTURE_CONTRACT_VERSION = '1.0.0' as const; export const recoveryTiers = ['lite', 'standard', 'high-assurance'] as const; export type RecoveryTier = (typeof recoveryTiers)[number]; export interface OffClusterStorageV1 { required: true; encrypted: true; separateFailureDomain: true; minimumCopies: number; storageClass: 'encrypted-object-storage' | 'encrypted-backup-target'; } export interface RecoveryPostureV1 { contractVersion: typeof RECOVERY_POSTURE_CONTRACT_VERSION; tier: RecoveryTier; targetRpoMinutes: number; targetRtoMinutes: number; baseBackupIntervalHours: number; /** null means WAL archival/PITR is disabled. */ walArchiveIntervalMinutes: number | null; /** 0 means PITR is disabled. */ pitrRetentionDays: number; restoreTestIntervalDays: number; breakGlassDrillIntervalDays: number; offClusterStorage: OffClusterStorageV1; } export const recoveryPostureDefaults: Readonly> = { lite: { contractVersion: RECOVERY_POSTURE_CONTRACT_VERSION, tier: 'lite', targetRpoMinutes: 24 * 60, targetRtoMinutes: 24 * 60, baseBackupIntervalHours: 24, walArchiveIntervalMinutes: null, pitrRetentionDays: 0, restoreTestIntervalDays: 90, breakGlassDrillIntervalDays: 365, offClusterStorage: { required: true, encrypted: true, separateFailureDomain: true, minimumCopies: 1, storageClass: 'encrypted-backup-target', }, }, standard: { contractVersion: RECOVERY_POSTURE_CONTRACT_VERSION, tier: 'standard', targetRpoMinutes: 60, targetRtoMinutes: 8 * 60, baseBackupIntervalHours: 24, walArchiveIntervalMinutes: 15, pitrRetentionDays: 14, restoreTestIntervalDays: 90, breakGlassDrillIntervalDays: 180, offClusterStorage: { required: true, encrypted: true, separateFailureDomain: true, minimumCopies: 1, storageClass: 'encrypted-object-storage', }, }, 'high-assurance': { contractVersion: RECOVERY_POSTURE_CONTRACT_VERSION, tier: 'high-assurance', targetRpoMinutes: 15, targetRtoMinutes: 4 * 60, baseBackupIntervalHours: 24, walArchiveIntervalMinutes: 5, pitrRetentionDays: 35, restoreTestIntervalDays: 30, breakGlassDrillIntervalDays: 90, offClusterStorage: { required: true, encrypted: true, separateFailureDomain: true, minimumCopies: 1, storageClass: 'encrypted-object-storage', }, }, }; /** Shape schema. Normative cross-field semantics are enforced by validateRecoveryPostureV1. */ export const recoveryPostureJsonSchemaV1 = { $id: 'https://mosaicstack.dev/contracts/recovery-posture.v1.schema.json', $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object', additionalProperties: false, required: [ 'contractVersion', 'tier', 'targetRpoMinutes', 'targetRtoMinutes', 'baseBackupIntervalHours', 'walArchiveIntervalMinutes', 'pitrRetentionDays', 'restoreTestIntervalDays', 'breakGlassDrillIntervalDays', 'offClusterStorage', ], properties: { contractVersion: { const: RECOVERY_POSTURE_CONTRACT_VERSION }, tier: { enum: recoveryTiers }, targetRpoMinutes: { type: 'integer', minimum: 1 }, targetRtoMinutes: { type: 'integer', minimum: 1 }, baseBackupIntervalHours: { type: 'integer', minimum: 1 }, walArchiveIntervalMinutes: { anyOf: [{ type: 'integer', minimum: 1 }, { type: 'null' }], }, pitrRetentionDays: { type: 'integer', minimum: 0 }, restoreTestIntervalDays: { type: 'integer', minimum: 1 }, breakGlassDrillIntervalDays: { type: 'integer', minimum: 1 }, offClusterStorage: { type: 'object', additionalProperties: false, required: ['required', 'encrypted', 'separateFailureDomain', 'minimumCopies', 'storageClass'], properties: { required: { const: true }, encrypted: { const: true }, separateFailureDomain: { const: true }, minimumCopies: { type: 'integer', minimum: 1 }, storageClass: { enum: ['encrypted-object-storage', 'encrypted-backup-target'], }, }, }, }, } as const; export const recoveryValidationCodes = [ 'INVALID_SHAPE', 'UNKNOWN_FIELD', 'PITR_REQUIRES_WAL', 'WAL_REQUIRES_PITR', 'RPO_BETTER_THAN_MECHANISM', 'OFF_CLUSTER_REQUIRED', 'HIGH_ASSURANCE_WEAKENED', ] as const; export type RecoveryValidationCode = (typeof recoveryValidationCodes)[number]; export interface RecoveryValidationIssueV1 { code: RecoveryValidationCode; path: string; message: string; } export type RecoveryValidationResultV1 = | { ok: true; value: RecoveryPostureV1 } | { ok: false; issues: RecoveryValidationIssueV1[] }; const topLevelFields = new Set([ 'contractVersion', 'tier', 'targetRpoMinutes', 'targetRtoMinutes', 'baseBackupIntervalHours', 'walArchiveIntervalMinutes', 'pitrRetentionDays', 'restoreTestIntervalDays', 'breakGlassDrillIntervalDays', 'offClusterStorage', ]); const storageFields = new Set([ 'required', 'encrypted', 'separateFailureDomain', 'minimumCopies', 'storageClass', ]); function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function isPositiveInteger(value: unknown): value is number { return Number.isInteger(value) && Number(value) > 0; } function isNonnegativeInteger(value: unknown): value is number { return Number.isInteger(value) && Number(value) >= 0; } /** * Normative parser/refinement. Deployment code MUST call this function (or a * byte-for-byte behaviorally equivalent generated validator), not JSON Schema * shape validation alone. */ export function validateRecoveryPostureV1(input: unknown): RecoveryValidationResultV1 { const issues: RecoveryValidationIssueV1[] = []; if (!isRecord(input)) { return { ok: false, issues: [{ code: 'INVALID_SHAPE', path: '$', message: 'posture must be an object' }], }; } for (const key of Object.keys(input)) { if (!topLevelFields.has(key)) { issues.push({ code: 'UNKNOWN_FIELD', path: `$.${key}`, message: 'unknown field' }); } } const tier = input['tier']; const storage = input['offClusterStorage']; const integerFields = [ 'targetRpoMinutes', 'targetRtoMinutes', 'baseBackupIntervalHours', 'restoreTestIntervalDays', 'breakGlassDrillIntervalDays', ] as const; if (input['contractVersion'] !== RECOVERY_POSTURE_CONTRACT_VERSION) { issues.push({ code: 'INVALID_SHAPE', path: '$.contractVersion', message: `must equal ${RECOVERY_POSTURE_CONTRACT_VERSION}`, }); } if (!recoveryTiers.includes(tier as RecoveryTier)) { issues.push({ code: 'INVALID_SHAPE', path: '$.tier', message: 'unknown recovery tier' }); } for (const field of integerFields) { if (!isPositiveInteger(input[field])) { issues.push({ code: 'INVALID_SHAPE', path: `$.${field}`, message: 'must be a positive integer', }); } } if (!isNonnegativeInteger(input['pitrRetentionDays'])) { issues.push({ code: 'INVALID_SHAPE', path: '$.pitrRetentionDays', message: 'must be a nonnegative integer', }); } if ( input['walArchiveIntervalMinutes'] !== null && !isPositiveInteger(input['walArchiveIntervalMinutes']) ) { issues.push({ code: 'INVALID_SHAPE', path: '$.walArchiveIntervalMinutes', message: 'must be null or a positive integer', }); } if (!isRecord(storage)) { issues.push({ code: 'INVALID_SHAPE', path: '$.offClusterStorage', message: 'must be an object', }); } else { for (const key of Object.keys(storage)) { if (!storageFields.has(key)) { issues.push({ code: 'UNKNOWN_FIELD', path: `$.offClusterStorage.${key}`, message: 'unknown field', }); } } if ( storage['required'] !== true || storage['encrypted'] !== true || storage['separateFailureDomain'] !== true ) { issues.push({ code: 'OFF_CLUSTER_REQUIRED', path: '$.offClusterStorage', message: 'storage must be required, encrypted, and in a separate failure domain', }); } if (!isPositiveInteger(storage['minimumCopies'])) { issues.push({ code: 'INVALID_SHAPE', path: '$.offClusterStorage.minimumCopies', message: 'must be a positive integer', }); } if ( storage['storageClass'] !== 'encrypted-object-storage' && storage['storageClass'] !== 'encrypted-backup-target' ) { issues.push({ code: 'INVALID_SHAPE', path: '$.offClusterStorage.storageClass', message: 'unsupported storage class', }); } } const wal = input['walArchiveIntervalMinutes']; const pitr = input['pitrRetentionDays']; if (pitr !== 0 && wal === null) { issues.push({ code: 'PITR_REQUIRES_WAL', path: '$.pitrRetentionDays', message: 'PITR retention requires WAL archival', }); } if (wal !== null && pitr === 0) { issues.push({ code: 'WAL_REQUIRES_PITR', path: '$.walArchiveIntervalMinutes', message: 'WAL archival requires positive PITR retention', }); } if ( isPositiveInteger(input['targetRpoMinutes']) && isPositiveInteger(input['baseBackupIntervalHours']) && (wal === null || isPositiveInteger(wal)) ) { const mechanismMinutes = wal === null ? input['baseBackupIntervalHours'] * 60 : wal; if (mechanismMinutes > input['targetRpoMinutes']) { issues.push({ code: 'RPO_BETTER_THAN_MECHANISM', path: '$.targetRpoMinutes', message: `configured mechanism can only support ${mechanismMinutes} minutes`, }); } } if (tier === 'high-assurance') { const weakened = !isPositiveInteger(input['targetRpoMinutes']) || input['targetRpoMinutes'] > 15 || !isPositiveInteger(input['targetRtoMinutes']) || input['targetRtoMinutes'] > 4 * 60 || !isPositiveInteger(input['baseBackupIntervalHours']) || input['baseBackupIntervalHours'] > 24 || !isPositiveInteger(wal) || wal > 5 || !isNonnegativeInteger(pitr) || pitr < 35 || !isPositiveInteger(input['restoreTestIntervalDays']) || input['restoreTestIntervalDays'] > 30 || !isPositiveInteger(input['breakGlassDrillIntervalDays']) || input['breakGlassDrillIntervalDays'] > 90; if (weakened) { issues.push({ code: 'HIGH_ASSURANCE_WEAKENED', path: '$', message: 'high-assurance posture may be strengthened but not weakened', }); } } if (issues.length > 0) return { ok: false, issues }; return { ok: true, value: input as unknown as RecoveryPostureV1 }; } export interface RecoveryPostureOverrideAuditV1 { actorId: string; reason: string; effectiveAt: string; policyRevision: string; previous: RecoveryPostureV1; next: RecoveryPostureV1; }