import { createCanonicalJsonSha256Hex } from '../private/canonicaljson.js'; import { validatePlatformObjectStorageBucketName } from './objectstorage.js'; export const objectStorageRetentionLimits = Object.freeze({ maximumPolicyIdLength: 96, maximumRetentionDurationSeconds: 100 * 366 * 24 * 60 * 60, }); export interface IObjectStorageRetentionIntentV1 { mode: 'compliance'; policyId: string; retentionDurationSeconds: number; } export interface IObjectStorageRetentionCapabilityEvidenceV1 { version: 1; supported: true; backend: 'standalone' | 'clustered'; modes: ['compliance']; atomicCreateOnly: true; retainedMultipartSupported: false; } export interface IObjectStorageRetentionAuthorityV1 { serviceId: string; bindingId: string; reconciliationGeneration: number; bindingRequestDigest: string; } export interface IObjectStorageRetentionSentinelEvidenceV1 extends IObjectStorageRetentionIntentV1 { version: 1; createdAt: number; retainUntil: number; payloadSha256: string; metadataSha256: string; etag: string; } export interface IObjectStorageRetentionReceiptV1 extends IObjectStorageRetentionIntentV1 { bucketName: string; configuredAt: number; sentinelKey: string; sentinel: IObjectStorageRetentionSentinelEvidenceV1; } export interface IObjectStorageRetentionEvidenceV1 { intent: IObjectStorageRetentionIntentV1; intentSha256: string; authority: IObjectStorageRetentionAuthorityV1; capability: IObjectStorageRetentionCapabilityEvidenceV1; receipt: IObjectStorageRetentionReceiptV1; verifiedAt: number; } /** Value-free desired intent with optional exact provider evidence. */ export interface IObjectStorageRetentionBindingV1 { schemaVersion: 1; intent: IObjectStorageRetentionIntentV1; evidence?: IObjectStorageRetentionEvidenceV1; } export interface IObjectStorageRetentionEvidenceExpectationV1 extends IObjectStorageRetentionAuthorityV1 { intent: IObjectStorageRetentionIntentV1; bucketName: string; } const policyIdPattern = /^[A-Za-z0-9._-]{1,96}$/; const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/; const sha256Pattern = /^[a-f0-9]{64}$/; const etagPattern = /^[a-f0-9]{32}$/; const sentinelKeyPattern = /^\.smartstorage-retention\/v1\/sentinel-[a-f0-9]{64}\.json$/; const isRecord = (valueArg: unknown): valueArg is Record => ( Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg) && (Object.getPrototypeOf(valueArg) === Object.prototype || Object.getPrototypeOf(valueArg) === null) ); const hasExactKeys = ( valueArg: Record, keysArg: readonly string[], ): boolean => { const actualKeys = Object.keys(valueArg).sort(); const expectedKeys = [...keysArg].sort(); return actualKeys.length === expectedKeys.length && actualKeys.every((keyArg, indexArg) => keyArg === expectedKeys[indexArg]); }; const isPositiveSafeInteger = (valueArg: unknown): valueArg is number => ( Number.isSafeInteger(valueArg) && (valueArg as number) > 0 ); export const validateObjectStorageRetentionIntent = ( intentArg: unknown, pathArg = 'objectstorage retention intent', ): string[] => { if (!isRecord(intentArg) || !hasExactKeys(intentArg, ['mode', 'policyId', 'retentionDurationSeconds'])) { return [`${pathArg} must use its exact schema`]; } const errors: string[] = []; if (intentArg.mode !== 'compliance' || typeof intentArg.policyId !== 'string' || !policyIdPattern.test(intentArg.policyId)) { errors.push(`${pathArg} must identify a canonical compliance policy`); } if (!isPositiveSafeInteger(intentArg.retentionDurationSeconds) || (intentArg.retentionDurationSeconds as number) > objectStorageRetentionLimits.maximumRetentionDurationSeconds) { errors.push(`${pathArg} duration must be a positive bounded safe integer`); } return errors; }; export const createObjectStorageRetentionRetainUntil = ( createdAtArg: number, retentionDurationSecondsArg: number, ): number => { if (!isPositiveSafeInteger(createdAtArg) || !isPositiveSafeInteger(retentionDurationSecondsArg) || retentionDurationSecondsArg > objectStorageRetentionLimits.maximumRetentionDurationSeconds) { throw new Error('objectstorage retention arithmetic requires positive bounded safe integers'); } const retainUntil = createdAtArg + retentionDurationSecondsArg * 1000; if (!Number.isSafeInteger(retainUntil) || retainUntil <= createdAtArg) { throw new Error('objectstorage retention retainUntil exceeds safe integer bounds'); } return retainUntil; }; export const createObjectStorageRetentionIntentDigestInput = ( intentArg: IObjectStorageRetentionIntentV1, ): string => { const errors = validateObjectStorageRetentionIntent(intentArg); if (errors.length > 0) throw new Error(errors[0]); return JSON.stringify({ capability: 'objectstorage', retention: { mode: intentArg.mode, policyId: intentArg.policyId, retentionDurationSeconds: intentArg.retentionDurationSeconds, }, version: 1, }); }; export const computeObjectStorageRetentionIntentSha256 = async ( intentArg: IObjectStorageRetentionIntentV1, ): Promise => createCanonicalJsonSha256Hex( createObjectStorageRetentionIntentDigestInput(intentArg), (reasonArg) => { throw new Error(reasonArg); }, ); export const objectStorageRetentionIntentsEqual = ( leftArg: unknown, rightArg: unknown, ): boolean => validateObjectStorageRetentionIntent(leftArg).length === 0 && validateObjectStorageRetentionIntent(rightArg).length === 0 && (leftArg as IObjectStorageRetentionIntentV1).mode === (rightArg as IObjectStorageRetentionIntentV1).mode && (leftArg as IObjectStorageRetentionIntentV1).policyId === (rightArg as IObjectStorageRetentionIntentV1).policyId && (leftArg as IObjectStorageRetentionIntentV1).retentionDurationSeconds === (rightArg as IObjectStorageRetentionIntentV1).retentionDurationSeconds; const retentionIntentFieldsEqual = ( leftArg: unknown, rightArg: unknown, ): boolean => isRecord(leftArg) && isRecord(rightArg) && leftArg.mode === 'compliance' && rightArg.mode === 'compliance' && typeof leftArg.policyId === 'string' && leftArg.policyId === rightArg.policyId && isPositiveSafeInteger(leftArg.retentionDurationSeconds) && leftArg.retentionDurationSeconds === rightArg.retentionDurationSeconds; export const validateObjectStorageRetentionEvidence = async ( evidenceArg: unknown, expectedArg: IObjectStorageRetentionEvidenceExpectationV1, ): Promise => { if (!isRecord(evidenceArg) || !hasExactKeys(evidenceArg, [ 'intent', 'intentSha256', 'authority', 'capability', 'receipt', 'verifiedAt', ])) { return ['objectstorage retention evidence must use its exact schema']; } const errors = validateObjectStorageRetentionIntent(evidenceArg.intent); const intent = evidenceArg.intent as IObjectStorageRetentionIntentV1; if (!retentionIntentFieldsEqual(intent, expectedArg.intent)) { errors.push('objectstorage retention evidence intent must equal the expected intent'); } if (typeof evidenceArg.intentSha256 !== 'string' || !sha256Pattern.test(evidenceArg.intentSha256) || errors.length === 0 && evidenceArg.intentSha256 !== await computeObjectStorageRetentionIntentSha256(intent)) { errors.push('objectstorage retention intent digest must exactly match the intent'); } const authority = evidenceArg.authority; if (!isRecord(authority) || !hasExactKeys(authority, [ 'serviceId', 'bindingId', 'reconciliationGeneration', 'bindingRequestDigest', ]) || typeof authority.serviceId !== 'string' || !identifierPattern.test(authority.serviceId) || authority.serviceId !== expectedArg.serviceId || typeof authority.bindingId !== 'string' || !identifierPattern.test(authority.bindingId) || authority.bindingId !== expectedArg.bindingId || !isPositiveSafeInteger(authority.reconciliationGeneration) || authority.reconciliationGeneration !== expectedArg.reconciliationGeneration || typeof authority.bindingRequestDigest !== 'string' || !sha256Pattern.test(authority.bindingRequestDigest) || authority.bindingRequestDigest !== expectedArg.bindingRequestDigest) { errors.push('objectstorage retention evidence authority must match the trusted binding fence'); } const capability = evidenceArg.capability; if (!isRecord(capability) || !hasExactKeys(capability, [ 'version', 'supported', 'backend', 'modes', 'atomicCreateOnly', 'retainedMultipartSupported', ]) || capability.version !== 1 || capability.supported !== true || (capability.backend !== 'standalone' && capability.backend !== 'clustered') || !Array.isArray(capability.modes) || capability.modes.length !== 1 || capability.modes[0] !== 'compliance' || capability.atomicCreateOnly !== true || capability.retainedMultipartSupported !== false) { errors.push('objectstorage retention capability evidence must be exact'); } const receipt = evidenceArg.receipt; if (!isRecord(receipt) || !hasExactKeys(receipt, [ 'mode', 'policyId', 'retentionDurationSeconds', 'bucketName', 'configuredAt', 'sentinelKey', 'sentinel', ])) { errors.push('objectstorage retention receipt must use its exact schema'); return errors; } if (!retentionIntentFieldsEqual(receipt, intent)) { errors.push('objectstorage retention receipt intent must equal its evidence intent'); } if (validatePlatformObjectStorageBucketName( receipt.bucketName, 'objectstorage retention receipt bucket', ).length > 0 || receipt.bucketName !== expectedArg.bucketName) { errors.push('objectstorage retention receipt bucket must exactly match'); } if (!isPositiveSafeInteger(receipt.configuredAt) || typeof receipt.sentinelKey !== 'string' || !sentinelKeyPattern.test(receipt.sentinelKey)) { errors.push('objectstorage retention receipt configuration evidence must be canonical'); } const sentinel = receipt.sentinel; if (!isRecord(sentinel) || !hasExactKeys(sentinel, [ 'version', 'mode', 'policyId', 'retentionDurationSeconds', 'createdAt', 'retainUntil', 'payloadSha256', 'metadataSha256', 'etag', ])) { errors.push('objectstorage retention sentinel evidence must use its exact schema'); return errors; } if (sentinel.version !== 1 || !retentionIntentFieldsEqual(sentinel, intent)) { errors.push('objectstorage retention sentinel intent must equal its evidence intent'); } if (!isPositiveSafeInteger(sentinel.createdAt) || !isPositiveSafeInteger(sentinel.retainUntil) || !isPositiveSafeInteger(receipt.configuredAt) || (sentinel.createdAt as number) < (receipt.configuredAt as number)) { errors.push('objectstorage retention timestamps must be positive and ordered'); } else { try { if (sentinel.retainUntil !== createObjectStorageRetentionRetainUntil( sentinel.createdAt, intent.retentionDurationSeconds, )) { errors.push('objectstorage retention retainUntil must equal createdAt plus duration'); } } catch { errors.push('objectstorage retention retainUntil arithmetic must be safe'); } } if (typeof sentinel.payloadSha256 !== 'string' || !sha256Pattern.test(sentinel.payloadSha256) || typeof sentinel.metadataSha256 !== 'string' || !sha256Pattern.test(sentinel.metadataSha256) || typeof sentinel.etag !== 'string' || !etagPattern.test(sentinel.etag)) { errors.push('objectstorage retention sentinel digests and ETag must be canonical'); } if (!isPositiveSafeInteger(evidenceArg.verifiedAt) || !isPositiveSafeInteger(sentinel.createdAt) || (evidenceArg.verifiedAt as number) < (sentinel.createdAt as number)) { errors.push('objectstorage retention verification timestamp must follow sentinel creation'); } return errors; }; export const validateObjectStorageRetentionBinding = async ( bindingArg: unknown, expectedArg: IObjectStorageRetentionEvidenceExpectationV1, ): Promise => { if (!isRecord(bindingArg) || !Object.hasOwn(bindingArg, 'schemaVersion') || !Object.hasOwn(bindingArg, 'intent') || Object.keys(bindingArg).some((keyArg) => !['schemaVersion', 'intent', 'evidence'].includes(keyArg))) { return ['objectstorage retention binding must use its exact schema']; } const errors = bindingArg.schemaVersion === 1 ? validateObjectStorageRetentionIntent(bindingArg.intent) : ['objectstorage retention binding schemaVersion must be 1']; if (!retentionIntentFieldsEqual(bindingArg.intent, expectedArg.intent)) { errors.push('objectstorage retention binding intent must equal the trusted expected intent'); } if (bindingArg.evidence !== undefined) { errors.push(...await validateObjectStorageRetentionEvidence( bindingArg.evidence, expectedArg, )); } return errors; }; export const validatePlatformBindingObjectStorageRetention = async ( bindingArg: unknown, expectedArg: IObjectStorageRetentionEvidenceExpectationV1, ): Promise => { if (!isRecord(bindingArg)) { return ['platform objectstorage retention binding must be an object']; } if (validatePlatformObjectStorageBucketName(expectedArg.bucketName).length > 0 || validatePlatformObjectStorageBucketName(bindingArg.objectstorageBucketName).length > 0) { return ['platform objectstorage retention must use canonical bucket authority']; } if (bindingArg.capability !== 'objectstorage' || bindingArg.id !== expectedArg.bindingId || bindingArg.serviceId !== expectedArg.serviceId || bindingArg.objectstorageBucketName !== expectedArg.bucketName) { return ['platform objectstorage retention must match an objectstorage binding authority']; } if (bindingArg.objectstorageRetention === undefined) { return ['platform objectstorage retention binding must contain retention intent']; } return validateObjectStorageRetentionBinding(bindingArg.objectstorageRetention, expectedArg); };