import type { IAppStoreObjectStorageRequest, IAppStoreStorageClass, } from '../appstore/types.js'; import { canonicalizeStrictJson, createCanonicalJsonSha256Hex, deepFreezeValue, strictCanonicalJsonRules, } from '../private/canonicaljson.js'; import type { IResolvedObjectStorageBinding, IResolvedStoragePolicyRef, IStorageGrantedCapabilities, } from './storage.js'; export const storageMigrationContractLimits = Object.freeze({ version: 2 as const, maximumControlJsonBytes: 256 * 1024, maximumConfiguredStringBytes: 2048, maximumIssueMessageBytes: 4096, maximumAbortReasonBytes: 4096, maximumEnvironmentKeyBytes: 256, maximumSecretPathBytes: 2048, }); export const storageMigrationCanonicalJsonRules = strictCanonicalJsonRules; export class StorageMigrationContractError extends Error { public constructor(reasonArg: string) { super(`storage migration contract error: ${reasonArg}`); this.name = 'StorageMigrationContractError'; } } export type TStagedObjectStorageBinding = Omit & { status: 'provisioning'; }; export type TActiveObjectStorageBinding = Omit & { status: 'ready'; }; export interface IStorageBindingMigrationOwner { migrationId: string; migrationDigest: string; } export interface IUnfencedObjectStorageBindingControlSnapshot { schemaVersion: 2; binding: TActiveObjectStorageBinding; recordRevision: number; snapshotDigest: string; capturedAt: number; activeMigration?: never; } export interface IFencedObjectStorageBindingControlSnapshot { schemaVersion: 2; binding: TActiveObjectStorageBinding; recordRevision: number; snapshotDigest: string; capturedAt: number; activeMigration: IStorageBindingMigrationOwner; } export type TUnfencedObjectStorageBindingControlSnapshotDigestPayload = Omit< IUnfencedObjectStorageBindingControlSnapshot, 'snapshotDigest' >; export type TFencedObjectStorageBindingControlSnapshotDigestPayload = Omit< IFencedObjectStorageBindingControlSnapshot, 'snapshotDigest' >; export interface IObjectStorageMigrationTarget { policy: IResolvedStoragePolicyRef; storageClass: IAppStoreStorageClass & { kind: 'objectStorage' }; request: IAppStoreObjectStorageRequest; credentialManagementScope: IResolvedObjectStorageBinding['credentialManagementScope']; } export interface IObjectStorageMigrationPrepareRequest { schemaVersion: 2; migrationId: string; sourceControlSnapshot: IUnfencedObjectStorageBindingControlSnapshot; workloadGeneration: number; target: IObjectStorageMigrationTarget; } export interface IStorageMigrationSourceEvidence { preparationSnapshot: IUnfencedObjectStorageBindingControlSnapshot; fencedSnapshot: IFencedObjectStorageBindingControlSnapshot; } export interface IStorageMigrationProgress { objectsCopied: number; logicalBytesCopied: number; physicalBytesCopied?: number; objectsTotal?: number; logicalBytesTotal?: number; physicalBytesTotal?: number; updatedAt: number; } export type TStorageMigrationPreCutoverRecoveryAction = 'retry' | 'abort'; export type TStorageMigrationPostCutoverRecoveryAction = 'retry' | 'rollForward'; export interface IStorageMigrationIssueBase { code: string; message: string; retryable: boolean; observedAt: number; } export interface IStorageMigrationPreCutoverIssue extends IStorageMigrationIssueBase { allowedRecoveryActions: TStorageMigrationPreCutoverRecoveryAction[]; } export interface IStorageMigrationPostCutoverIssue extends IStorageMigrationIssueBase { allowedRecoveryActions: TStorageMigrationPostCutoverRecoveryAction[]; } export interface IStorageMigrationMutationFence { schemaVersion: 2; migrationId: string; migrationDigest: string; expectedMigrationRevision: number; mutationId: string; } export interface IObjectStorageMigrationConsumerQuiesceRequest extends IStorageMigrationMutationFence { sourceRecordRevision: number; sourceControlSnapshotDigest: string; candidateDigest: string; workloadGeneration: number; persistedStagingDigest: string; persistedStagingRevision: number; consumerStoppedAt: number; } export type TObjectStorageMigrationPersistedStagingDigestPayload = Omit< IObjectStorageMigrationConsumerQuiesceRequest, 'persistedStagingDigest' >; export interface IStorageMigrationConsumerQuiesceEvidence { request: IObjectStorageMigrationConsumerQuiesceRequest; acceptedMigrationRevision: number; acceptedAt: number; } export interface IObjectStorageMigrationConsumerActivationRequest extends IStorageMigrationMutationFence { candidateDigest: string; activeBindingDigest: string; workloadGeneration: number; persistedActivationDigest: string; persistedActivationRevision: number; consumerStartedAt: number; } export type TObjectStorageMigrationPersistedActivationDigestPayload = Omit< IObjectStorageMigrationConsumerActivationRequest, 'persistedActivationDigest' >; export interface IStorageMigrationConsumerActivationEvidence { request: IObjectStorageMigrationConsumerActivationRequest; acceptedMigrationRevision: number; acceptedAt: number; } export interface IObjectStorageMigrationAbortRequest extends IStorageMigrationMutationFence { reason: string; } export interface IObjectStorageMigrationResumeRequest extends IStorageMigrationMutationFence { action: | TStorageMigrationPreCutoverRecoveryAction | TStorageMigrationPostCutoverRecoveryAction; } export interface IObjectStorageMigrationCleanupRequest extends IStorageMigrationMutationFence {} export interface IStorageMigrationStatusBase { schemaVersion: 2; migrationId: string; migrationDigest: string; migrationRevision: number; bindingId: string; serviceId: string; requestId: string; workloadGeneration: number; target: IObjectStorageMigrationTarget; source: IStorageMigrationSourceEvidence; progress?: IStorageMigrationProgress; createdAt: number; updatedAt: number; } interface IStorageMigrationLiveStatusBase extends IStorageMigrationStatusBase { bindingFenceActive: true; } interface IStorageMigrationTerminalStatusBase extends IStorageMigrationStatusBase { bindingFenceActive: false; terminalAt: number; } interface IStorageMigrationCandidateStatusBase extends IStorageMigrationLiveStatusBase { candidateIssued: true; candidateBinding: TStagedObjectStorageBinding; candidateDigest: string; } interface IStorageMigrationQuiescedCandidateStatusBase extends IStorageMigrationCandidateStatusBase { consumerQuiesceEvidence: IStorageMigrationConsumerQuiesceEvidence; } interface IStorageMigrationDestinationAuthorizedStatusBase extends IStorageMigrationQuiescedCandidateStatusBase { destinationBindingStartAuthorized: true; authority: 'destination'; activeBinding: TActiveObjectStorageBinding; activeBindingDigest: string; } export interface IStorageMigrationPreparingStatus extends IStorageMigrationLiveStatusBase { phase: 'preparing'; candidateIssued: false; candidateBinding?: never; candidateDigest?: never; consumerQuiesceEvidence?: never; consumerActivationEvidence?: never; commitPointCrossed: false; destinationBindingStartAuthorized: false; authority: 'source'; consumerAction: 'wait'; issue?: IStorageMigrationPreCutoverIssue; } export interface IStorageMigrationTransferringStatus extends IStorageMigrationLiveStatusBase { phase: 'transferring'; candidateIssued: false; candidateBinding?: never; candidateDigest?: never; consumerQuiesceEvidence?: never; consumerActivationEvidence?: never; commitPointCrossed: false; destinationBindingStartAuthorized: false; authority: 'source'; consumerAction: 'wait'; issue?: IStorageMigrationPreCutoverIssue; } export interface IStorageMigrationAwaitingConsumerQuiesceStatus extends IStorageMigrationCandidateStatusBase { phase: 'awaitingConsumerQuiesce'; consumerQuiesceEvidence?: never; consumerActivationEvidence?: never; commitPointCrossed: false; destinationBindingStartAuthorized: false; authority: 'source'; consumerAction: 'stageCandidateAndStop'; issue?: IStorageMigrationPreCutoverIssue; } export interface IStorageMigrationFinalizingStatus extends IStorageMigrationQuiescedCandidateStatusBase { phase: 'finalizing'; consumerActivationEvidence?: never; commitPointCrossed: false; destinationBindingStartAuthorized: false; authority: 'source'; consumerAction: 'wait'; issue?: IStorageMigrationPreCutoverIssue; } export interface IStorageMigrationCommittingStatus extends IStorageMigrationQuiescedCandidateStatusBase { phase: 'committing'; consumerActivationEvidence?: never; commitPointCrossed: true; destinationBindingStartAuthorized: false; authority: 'transitioning'; consumerAction: 'wait'; issue?: IStorageMigrationPostCutoverIssue; } /** * Destination authority is durable, but Corestore must continue returning * `startDestination` until the exact activation acknowledgement is accepted. */ export interface IStorageMigrationReadyToStartStatus extends IStorageMigrationDestinationAuthorizedStatusBase { phase: 'readyToStart'; consumerActivationEvidence?: never; commitPointCrossed: true; consumerAction: 'startDestination'; issue?: IStorageMigrationPostCutoverIssue; } export interface IStorageMigrationCleanupPendingStatus extends IStorageMigrationDestinationAuthorizedStatusBase { phase: 'cleanupPending'; consumerActivationEvidence: IStorageMigrationConsumerActivationEvidence; commitPointCrossed: true; consumerAction: 'none'; issue?: IStorageMigrationPostCutoverIssue; } export interface IStorageMigrationCompleteStatus extends IStorageMigrationTerminalStatusBase { phase: 'complete'; candidateIssued: true; candidateBinding: TStagedObjectStorageBinding; candidateDigest: string; consumerQuiesceEvidence: IStorageMigrationConsumerQuiesceEvidence; consumerActivationEvidence: IStorageMigrationConsumerActivationEvidence; commitPointCrossed: true; destinationBindingStartAuthorized: true; authority: 'destination'; consumerAction: 'none'; activeBinding: TActiveObjectStorageBinding; activeBindingDigest: string; } interface IStorageMigrationAbortingStatusBase extends IStorageMigrationLiveStatusBase { phase: 'aborting'; commitPointCrossed: false; destinationBindingStartAuthorized: false; authority: 'source'; consumerAction: 'wait'; consumerQuiesceEvidence?: IStorageMigrationConsumerQuiesceEvidence; consumerActivationEvidence?: never; issue?: IStorageMigrationPreCutoverIssue; } export interface IStorageMigrationAbortingWithoutCandidateStatus extends IStorageMigrationAbortingStatusBase { candidateIssued: false; candidateBinding?: never; candidateDigest?: never; consumerQuiesceEvidence?: never; } export interface IStorageMigrationAbortingWithCandidateStatus extends IStorageMigrationAbortingStatusBase { candidateIssued: true; candidateBinding?: never; candidateDigest: string; } interface IStorageMigrationAbortedStatusBase extends IStorageMigrationTerminalStatusBase { phase: 'aborted'; commitPointCrossed: false; destinationBindingStartAuthorized: false; authority: 'source'; consumerAction: 'startSource'; activeBinding: TActiveObjectStorageBinding; activeBindingDigest: string; consumerQuiesceEvidence?: IStorageMigrationConsumerQuiesceEvidence; consumerActivationEvidence?: never; } export interface IStorageMigrationAbortedWithoutCandidateStatus extends IStorageMigrationAbortedStatusBase { candidateIssued: false; candidateBinding?: never; candidateDigest?: never; consumerQuiesceEvidence?: never; } export interface IStorageMigrationAbortedWithCandidateStatus extends IStorageMigrationAbortedStatusBase { candidateIssued: true; candidateBinding?: never; candidateDigest: string; } export type TObjectStorageMigrationStatus = | IStorageMigrationPreparingStatus | IStorageMigrationTransferringStatus | IStorageMigrationAwaitingConsumerQuiesceStatus | IStorageMigrationFinalizingStatus | IStorageMigrationCommittingStatus | IStorageMigrationReadyToStartStatus | IStorageMigrationCleanupPendingStatus | IStorageMigrationCompleteStatus | IStorageMigrationAbortingWithoutCandidateStatus | IStorageMigrationAbortingWithCandidateStatus | IStorageMigrationAbortedWithoutCandidateStatus | IStorageMigrationAbortedWithCandidateStatus; const fail = (reasonArg: string): never => { throw new StorageMigrationContractError(reasonArg); }; const utf8ByteLength = (valueArg: string): number => { return new TextEncoder().encode(valueArg).byteLength; }; /** * Enforce this against Content-Length and the running request byte count * before buffering or decoding a storage-migration control body. */ export const assertStorageMigrationControlIngressByteLength = ( valueArg: unknown, ): number => { return readSafeInteger( valueArg, 'storage migration raw control ingress byte length', 0, storageMigrationContractLimits.maximumControlJsonBytes, ); }; const readRecord = ( valueArg: unknown, fieldNameArg: string, ): Record => { if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) { return fail(`${fieldNameArg} must be a plain object`); } const prototype = Object.getPrototypeOf(valueArg); if (prototype !== Object.prototype && prototype !== null) { return fail(`${fieldNameArg} must be a plain object`); } const record = valueArg as Record; const ownKeys = Reflect.ownKeys(record); if (ownKeys.some((keyArg) => typeof keyArg !== 'string')) { return fail(`${fieldNameArg} must not contain symbol keys`); } for (const key of ownKeys as string[]) { const descriptor = Object.getOwnPropertyDescriptor(record, key); if ( !descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value') || descriptor.value === undefined ) { return fail( `${fieldNameArg} must contain enumerable JSON data properties only`, ); } } return record; }; const readDenseArray = ( valueArg: unknown, fieldNameArg: string, minimumLengthArg: number, maximumLengthArg: number, ): unknown[] => { if ( !Array.isArray(valueArg) || Object.getPrototypeOf(valueArg) !== Array.prototype ) { return fail(`${fieldNameArg} must be a dense plain array`); } if ( valueArg.length < minimumLengthArg || valueArg.length > maximumLengthArg ) { return fail( `${fieldNameArg} must contain between ${minimumLengthArg} and ${maximumLengthArg} entries`, ); } for (let index = 0; index < valueArg.length; index++) { if (!Object.hasOwn(valueArg, index)) { return fail(`${fieldNameArg} must not contain sparse entries`); } const descriptor = Object.getOwnPropertyDescriptor(valueArg, String(index)); if ( !descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value') || descriptor.value === undefined ) { return fail( `${fieldNameArg} must contain enumerable JSON data entries only`, ); } } for (const key of Reflect.ownKeys(valueArg)) { if (key === 'length') continue; if ( typeof key !== 'string' || !/^(?:0|[1-9][0-9]*)$/.test(key) || Number(key) >= valueArg.length ) { return fail(`${fieldNameArg} must not contain extra properties`); } } return valueArg; }; const assertKeys = ( recordArg: Record, requiredKeysArg: readonly string[], optionalKeysArg: readonly string[], fieldNameArg: string, ): void => { const actualKeys = Object.keys(recordArg).sort(); const allowedKeys = new Set([...requiredKeysArg, ...optionalKeysArg]); if ( requiredKeysArg.some((keyArg) => !Object.hasOwn(recordArg, keyArg)) || actualKeys.some((keyArg) => !allowedKeys.has(keyArg)) ) { fail(`${fieldNameArg} does not match its exact versioned schema`); } }; const assertExactKeys = ( recordArg: Record, expectedKeysArg: readonly string[], fieldNameArg: string, ): void => { assertKeys(recordArg, expectedKeysArg, [], fieldNameArg); }; const readString = ( valueArg: unknown, fieldNameArg: string, maximumBytesArg: number = storageMigrationContractLimits.maximumConfiguredStringBytes, ): string => { if ( typeof valueArg !== 'string' || valueArg.length === 0 || utf8ByteLength(valueArg) > maximumBytesArg || /[\u0000-\u001f\u007f]/u.test(valueArg) ) { return fail(`${fieldNameArg} must be a bounded non-empty JSON string`); } for (let index = 0; index < valueArg.length; index++) { const codeUnit = valueArg.charCodeAt(index); if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { const following = valueArg.charCodeAt(index + 1); if (following < 0xdc00 || following > 0xdfff) { return fail(`${fieldNameArg} contains an unpaired UTF-16 surrogate`); } index++; } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { return fail(`${fieldNameArg} contains an unpaired UTF-16 surrogate`); } } return valueArg; }; const canonicalIdPattern = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,191}$/; const environmentKeyPattern = /^[A-Z_][A-Z0-9_]{0,255}$/; const sha256Pattern = /^[a-f0-9]{64}$/; const capacityPattern = /^[1-9][0-9]*(?:KiB|MiB|GiB|TiB)$/; const readIdentifier = (valueArg: unknown, fieldNameArg: string): string => { if (typeof valueArg !== 'string' || !canonicalIdPattern.test(valueArg)) { return fail(`${fieldNameArg} must be a canonical identifier`); } return valueArg; }; const readSha256 = (valueArg: unknown, fieldNameArg: string): string => { if (typeof valueArg !== 'string' || !sha256Pattern.test(valueArg)) { return fail(`${fieldNameArg} must be a bare lowercase 64-hex SHA-256 digest`); } return valueArg; }; const readSafeInteger = ( valueArg: unknown, fieldNameArg: string, minimumArg: number, maximumArg = Number.MAX_SAFE_INTEGER, ): number => { if ( typeof valueArg !== 'number' || !Number.isSafeInteger(valueArg) || Object.is(valueArg, -0) || valueArg < minimumArg || valueArg > maximumArg ) { return fail( `${fieldNameArg} must be a safe integer between ${minimumArg} and ${maximumArg}`, ); } return valueArg; }; const readBoolean = (valueArg: unknown, fieldNameArg: string): boolean => { if (typeof valueArg !== 'boolean') { return fail(`${fieldNameArg} must be a boolean`); } return valueArg; }; const readLiteral = ( valueArg: unknown, allowedArg: readonly TLiteral[], fieldNameArg: string, ): TLiteral => { if ( typeof valueArg !== 'string' || !allowedArg.includes(valueArg as TLiteral) ) { return fail(`${fieldNameArg} is outside the supported versioned values`); } return valueArg as TLiteral; }; const canonicalize = (valueArg: unknown): string => { return canonicalizeStrictJson(valueArg, fail); }; const createCanonicalSha256 = async (valueArg: unknown): Promise => { return createCanonicalJsonSha256Hex(canonicalize(valueArg), fail); }; const assertControlJsonLimit = (valueArg: T, fieldNameArg: string): T => { const json = JSON.stringify(valueArg); if ( typeof json !== 'string' || utf8ByteLength(json) > storageMigrationContractLimits.maximumControlJsonBytes ) { return fail(`${fieldNameArg} exceeds the control JSON byte limit`); } return deepFreezeValue(valueArg); }; const normalizePolicy = ( valueArg: unknown, fieldNameArg: string, ): IResolvedStoragePolicyRef => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys(value, ['classId', 'revision'], fieldNameArg); return { classId: readIdentifier(value.classId, `${fieldNameArg}.classId`), revision: readIdentifier(value.revision, `${fieldNameArg}.revision`), }; }; const normalizeRequirement = ( valueArg: unknown, fieldNameArg: string, ): NonNullable => { const value = readRecord(valueArg, fieldNameArg); assertKeys( value, [], [ 'performanceTier', 'durability', 'topology', 'hardQuota', 'snapshots', 'backup', 'encryptedInTransit', ], fieldNameArg, ); if (Object.keys(value).length === 0) { return fail(`${fieldNameArg} must not be empty`); } return { ...(Object.hasOwn(value, 'performanceTier') ? { performanceTier: readLiteral( value.performanceTier, ['standard', 'highIops', 'capacity'] as const, `${fieldNameArg}.performanceTier`, ), } : {}), ...(Object.hasOwn(value, 'durability') ? { durability: readLiteral( value.durability, ['ephemeral', 'persistent'] as const, `${fieldNameArg}.durability`, ), } : {}), ...(Object.hasOwn(value, 'topology') ? { topology: readLiteral( value.topology, ['singleNode', 'multiNode'] as const, `${fieldNameArg}.topology`, ), } : {}), ...(Object.hasOwn(value, 'hardQuota') ? { hardQuota: value.hardQuota === true ? true as const : fail(`${fieldNameArg}.hardQuota must be true when present`), } : {}), ...(Object.hasOwn(value, 'snapshots') ? { snapshots: readLiteral( value.snapshots, ['portable', 'native'] as const, `${fieldNameArg}.snapshots`, ), } : {}), ...(Object.hasOwn(value, 'backup') ? { backup: value.backup === true ? true as const : fail(`${fieldNameArg}.backup must be true when present`), } : {}), ...(Object.hasOwn(value, 'encryptedInTransit') ? { encryptedInTransit: value.encryptedInTransit === true ? true as const : fail( `${fieldNameArg}.encryptedInTransit must be true when present`, ), } : {}), }; }; const normalizeStorageClass = ( valueArg: unknown, fieldNameArg: string, ): IAppStoreStorageClass & { kind: 'objectStorage' } => { const value = readRecord(valueArg, fieldNameArg); assertKeys( value, ['kind', 'purpose'], ['required', 'preferred'], fieldNameArg, ); if (value.kind !== 'objectStorage') { return fail(`${fieldNameArg}.kind must be objectStorage`); } return { kind: 'objectStorage', purpose: readLiteral( value.purpose, ['runtime', 'database', 'registry', 'backup'] as const, `${fieldNameArg}.purpose`, ), ...(Object.hasOwn(value, 'required') ? { required: normalizeRequirement( value.required, `${fieldNameArg}.required`, ), } : {}), ...(Object.hasOwn(value, 'preferred') ? { preferred: normalizeRequirement( value.preferred, `${fieldNameArg}.preferred`, ), } : {}), }; }; const readCapacityQuantity = ( valueArg: unknown, fieldNameArg: string, ): string => { if (typeof valueArg !== 'string' || !capacityPattern.test(valueArg)) { return fail(`${fieldNameArg} must be a canonical base-2 capacity quantity`); } const numericPart = valueArg.replace(/(?:KiB|MiB|GiB|TiB)$/, ''); if (!Number.isSafeInteger(Number(numericPart))) { return fail(`${fieldNameArg} exceeds the safe integer quantity range`); } return valueArg; }; const capacityQuantityBytes = (valueArg: string): bigint => { const units: Record = { KiB: 1024n, MiB: 1024n ** 2n, GiB: 1024n ** 3n, TiB: 1024n ** 4n, }; const unit = valueArg.slice(-3); return BigInt(valueArg.slice(0, -3)) * units[unit]; }; const normalizeCapacityRequest = ( valueArg: unknown, fieldNameArg: string, ): NonNullable => { const value = readRecord(valueArg, fieldNameArg); assertKeys(value, ['request'], ['limit'], fieldNameArg); const request = readCapacityQuantity(value.request, `${fieldNameArg}.request`); const limit = Object.hasOwn(value, 'limit') ? readCapacityQuantity(value.limit, `${fieldNameArg}.limit`) : undefined; if (limit && capacityQuantityBytes(limit) < capacityQuantityBytes(request)) { return fail(`${fieldNameArg}.limit must be at least request`); } return { request, ...(limit ? { limit } : {}), }; }; const normalizeResolvedCapacity = ( valueArg: unknown, fieldNameArg: string, ): NonNullable => { const value = readRecord(valueArg, fieldNameArg); assertKeys(value, [], ['requested', 'granted', 'limit'], fieldNameArg); if (Object.keys(value).length === 0) { return fail(`${fieldNameArg} must not be empty`); } const requested = Object.hasOwn(value, 'requested') ? readCapacityQuantity(value.requested, `${fieldNameArg}.requested`) : undefined; const granted = Object.hasOwn(value, 'granted') ? readCapacityQuantity(value.granted, `${fieldNameArg}.granted`) : undefined; const limit = Object.hasOwn(value, 'limit') ? readCapacityQuantity(value.limit, `${fieldNameArg}.limit`) : undefined; if ( requested && granted && capacityQuantityBytes(granted) < capacityQuantityBytes(requested) ) { return fail(`${fieldNameArg}.granted must be at least requested`); } if ( requested && limit && capacityQuantityBytes(limit) < capacityQuantityBytes(requested) ) { return fail(`${fieldNameArg}.limit must be at least requested`); } if ( granted && limit && capacityQuantityBytes(limit) < capacityQuantityBytes(granted) ) { return fail(`${fieldNameArg}.limit must be at least granted`); } return { ...(requested ? { requested } : {}), ...(granted ? { granted } : {}), ...(limit ? { limit } : {}), }; }; const readEnvironmentKey = ( valueArg: unknown, fieldNameArg: string, ): string => { if ( typeof valueArg !== 'string' || utf8ByteLength(valueArg) > storageMigrationContractLimits.maximumEnvironmentKeyBytes || !environmentKeyPattern.test(valueArg) ) { return fail(`${fieldNameArg} must be a canonical environment key`); } return valueArg; }; const normalizeDelivery = ( valueArg: unknown, fieldNameArg: string, ): IResolvedObjectStorageBinding['delivery'] => { const value = readRecord(valueArg, fieldNameArg); if (value.type === 'launcher-environment') { assertExactKeys(value, ['type', 'keys'], fieldNameArg); const keys = readRecord(value.keys, `${fieldNameArg}.keys`); assertKeys( keys, ['endpoint', 'bucket', 'region', 'accessKeyId', 'secretAccessKey'], ['sessionToken'], `${fieldNameArg}.keys`, ); const normalizedKeys = { endpoint: readEnvironmentKey( keys.endpoint, `${fieldNameArg}.keys.endpoint`, ), bucket: readEnvironmentKey(keys.bucket, `${fieldNameArg}.keys.bucket`), region: readEnvironmentKey(keys.region, `${fieldNameArg}.keys.region`), accessKeyId: readEnvironmentKey( keys.accessKeyId, `${fieldNameArg}.keys.accessKeyId`, ), secretAccessKey: readEnvironmentKey( keys.secretAccessKey, `${fieldNameArg}.keys.secretAccessKey`, ), ...(Object.hasOwn(keys, 'sessionToken') ? { sessionToken: readEnvironmentKey( keys.sessionToken, `${fieldNameArg}.keys.sessionToken`, ), } : {}), }; if ( new Set(Object.values(normalizedKeys)).size !== Object.keys(normalizedKeys).length ) { return fail(`${fieldNameArg}.keys values must be distinct`); } return { type: 'launcher-environment', keys: normalizedKeys }; } if (value.type === 'file') { assertExactKeys(value, ['type', 'targetPath', 'format', 'uid', 'gid', 'mode'], fieldNameArg); const targetPath = readString( value.targetPath, `${fieldNameArg}.targetPath`, storageMigrationContractLimits.maximumSecretPathBytes, ); if ( !targetPath.startsWith('/run/secrets/') || targetPath.includes('\\') || targetPath.split('/').some((segmentArg) => segmentArg === '.' || segmentArg === '..' ) ) { return fail( `${fieldNameArg}.targetPath must be a canonical path below /run/secrets`, ); } if (value.format !== 'servezone-object-storage-v1') { return fail(`${fieldNameArg}.format is unsupported`); } return { type: 'file', targetPath, format: 'servezone-object-storage-v1', uid: readSafeInteger(value.uid, `${fieldNameArg}.uid`, 0, 2_147_483_647), gid: readSafeInteger(value.gid, `${fieldNameArg}.gid`, 0, 2_147_483_647), mode: value.mode === 0o400 || value.mode === 0o440 ? value.mode : fail(`${fieldNameArg}.mode must be 0400 or 0440`), }; } return fail(`${fieldNameArg}.type is unsupported`); }; const normalizeCredentialManagementScope = ( valueArg: unknown, fieldNameArg: string, ): IResolvedObjectStorageBinding['credentialManagementScope'] => { const value = readString(valueArg, fieldNameArg); if (!/^platform:[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(value)) { return fail(`${fieldNameArg} must be a canonical platform management scope`); } return value as IResolvedObjectStorageBinding['credentialManagementScope']; }; const normalizeObjectStorageProtection = ( valueArg: unknown, fieldNameArg: string, ): NonNullable => { const value = readRecord(valueArg, fieldNameArg); assertKeys( value, [], ['backup', 'versioning', 'retentionDays'], fieldNameArg, ); if (Object.keys(value).length === 0) { return fail(`${fieldNameArg} must not be empty`); } return { ...(Object.hasOwn(value, 'backup') ? { backup: value.backup === 'required' ? 'required' as const : fail(`${fieldNameArg}.backup must be required`), } : {}), ...(Object.hasOwn(value, 'versioning') ? { versioning: value.versioning === 'required' ? 'required' as const : fail(`${fieldNameArg}.versioning must be required`), } : {}), ...(Object.hasOwn(value, 'retentionDays') ? { retentionDays: readSafeInteger( value.retentionDays, `${fieldNameArg}.retentionDays`, 1, 365_000, ), } : {}), }; }; const normalizeObjectStorageRequest = ( valueArg: unknown, fieldNameArg: string, ): IAppStoreObjectStorageRequest => { const value = readRecord(valueArg, fieldNameArg); assertKeys( value, [ 'id', 'kind', 'storageClass', 'accessMode', 'reclaimPolicy', 'delivery', ], ['capacity', 'protection'], fieldNameArg, ); if (value.kind !== 'objectStorage') { return fail(`${fieldNameArg}.kind must be objectStorage`); } return { id: readIdentifier(value.id, `${fieldNameArg}.id`), kind: 'objectStorage', storageClass: readIdentifier( value.storageClass, `${fieldNameArg}.storageClass`, ), accessMode: readLiteral( value.accessMode, ['readOnly', 'readWrite'] as const, `${fieldNameArg}.accessMode`, ), reclaimPolicy: readLiteral( value.reclaimPolicy, ['retain', 'delete'] as const, `${fieldNameArg}.reclaimPolicy`, ), delivery: normalizeDelivery(value.delivery, `${fieldNameArg}.delivery`), ...(Object.hasOwn(value, 'capacity') ? { capacity: normalizeCapacityRequest( value.capacity, `${fieldNameArg}.capacity`, ), } : {}), ...(Object.hasOwn(value, 'protection') ? { protection: normalizeObjectStorageProtection( value.protection, `${fieldNameArg}.protection`, ), } : {}), }; }; const normalizeCapabilities = ( valueArg: unknown, fieldNameArg: string, ): IStorageGrantedCapabilities => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys( value, [ 'performanceTier', 'durability', 'topology', 'hardQuota', 'snapshots', 'backup', 'encryptedInTransit', ], fieldNameArg, ); return { performanceTier: readLiteral( value.performanceTier, ['standard', 'highIops', 'capacity'] as const, `${fieldNameArg}.performanceTier`, ), durability: readLiteral( value.durability, ['ephemeral', 'persistent'] as const, `${fieldNameArg}.durability`, ), topology: readLiteral( value.topology, ['singleNode', 'multiNode'] as const, `${fieldNameArg}.topology`, ), hardQuota: readBoolean(value.hardQuota, `${fieldNameArg}.hardQuota`), snapshots: readLiteral( value.snapshots, ['none', 'portable', 'native'] as const, `${fieldNameArg}.snapshots`, ), backup: readBoolean(value.backup, `${fieldNameArg}.backup`), encryptedInTransit: readBoolean( value.encryptedInTransit, `${fieldNameArg}.encryptedInTransit`, ), }; }; const normalizeConnection = ( valueArg: unknown, fieldNameArg: string, ): IResolvedObjectStorageBinding['connection'] => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys(value, ['endpoint', 'bucket', 'region'], fieldNameArg); return { endpoint: readString(value.endpoint, `${fieldNameArg}.endpoint`), bucket: readIdentifier(value.bucket, `${fieldNameArg}.bucket`), region: readIdentifier(value.region, `${fieldNameArg}.region`), }; }; const normalizeObjectStorageBinding = ( valueArg: unknown, expectedStatusArg: 'provisioning' | 'ready', fieldNameArg: string, ): TStagedObjectStorageBinding | TActiveObjectStorageBinding => { const value = readRecord(valueArg, fieldNameArg); assertKeys( value, [ 'schemaVersion', 'id', 'serviceId', 'requestId', 'requestDigest', 'kind', 'generation', 'observedGeneration', 'status', 'policy', 'capabilities', 'resourceRef', 'accessMode', 'connection', 'credentialManagementScope', 'delivery', 'versioning', ], ['capacity', 'createdAt', 'updatedAt', 'retentionDays'], fieldNameArg, ); if (value.schemaVersion !== 2) { return fail(`${fieldNameArg}.schemaVersion must be 2`); } if (value.kind !== 'objectStorage') { return fail(`${fieldNameArg}.kind must be objectStorage`); } if (value.status !== expectedStatusArg) { return fail(`${fieldNameArg}.status must be ${expectedStatusArg}`); } const generation = readSafeInteger( value.generation, `${fieldNameArg}.generation`, 1, ); const observedGeneration = readSafeInteger( value.observedGeneration, `${fieldNameArg}.observedGeneration`, 0, generation, ); if (expectedStatusArg === 'ready' && observedGeneration !== generation) { return fail( `${fieldNameArg}.observedGeneration must equal generation when ready`, ); } const createdAt = Object.hasOwn(value, 'createdAt') ? readSafeInteger(value.createdAt, `${fieldNameArg}.createdAt`, 1) : undefined; const updatedAt = Object.hasOwn(value, 'updatedAt') ? readSafeInteger(value.updatedAt, `${fieldNameArg}.updatedAt`, 1) : undefined; if (createdAt && updatedAt && updatedAt < createdAt) { return fail(`${fieldNameArg}.updatedAt must not precede createdAt`); } return { schemaVersion: 2, id: readIdentifier(value.id, `${fieldNameArg}.id`), serviceId: readIdentifier(value.serviceId, `${fieldNameArg}.serviceId`), requestId: readIdentifier(value.requestId, `${fieldNameArg}.requestId`), requestDigest: readSha256( value.requestDigest, `${fieldNameArg}.requestDigest`, ), kind: 'objectStorage', generation, observedGeneration, status: expectedStatusArg, policy: normalizePolicy(value.policy, `${fieldNameArg}.policy`), capabilities: normalizeCapabilities( value.capabilities, `${fieldNameArg}.capabilities`, ), resourceRef: readIdentifier( value.resourceRef, `${fieldNameArg}.resourceRef`, ), ...(Object.hasOwn(value, 'capacity') ? { capacity: normalizeResolvedCapacity( value.capacity, `${fieldNameArg}.capacity`, ), } : {}), accessMode: readLiteral( value.accessMode, ['readOnly', 'readWrite'] as const, `${fieldNameArg}.accessMode`, ), connection: normalizeConnection( value.connection, `${fieldNameArg}.connection`, ), credentialManagementScope: normalizeCredentialManagementScope( value.credentialManagementScope, `${fieldNameArg}.credentialManagementScope`, ), delivery: normalizeDelivery(value.delivery, `${fieldNameArg}.delivery`), versioning: readBoolean(value.versioning, `${fieldNameArg}.versioning`), ...(Object.hasOwn(value, 'retentionDays') ? { retentionDays: readSafeInteger( value.retentionDays, `${fieldNameArg}.retentionDays`, 1, 365_000, ), } : {}), ...(createdAt ? { createdAt } : {}), ...(updatedAt ? { updatedAt } : {}), } as TStagedObjectStorageBinding | TActiveObjectStorageBinding; }; const normalizeTarget = ( valueArg: unknown, fieldNameArg: string, ): IObjectStorageMigrationTarget => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys( value, ['policy', 'storageClass', 'request', 'credentialManagementScope'], fieldNameArg, ); return { policy: normalizePolicy(value.policy, `${fieldNameArg}.policy`), storageClass: normalizeStorageClass( value.storageClass, `${fieldNameArg}.storageClass`, ), request: normalizeObjectStorageRequest( value.request, `${fieldNameArg}.request`, ), credentialManagementScope: normalizeCredentialManagementScope( value.credentialManagementScope, `${fieldNameArg}.credentialManagementScope`, ), }; }; const normalizeMigrationOwner = ( valueArg: unknown, fieldNameArg: string, ): IStorageBindingMigrationOwner => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys(value, ['migrationId', 'migrationDigest'], fieldNameArg); return { migrationId: readIdentifier( value.migrationId, `${fieldNameArg}.migrationId`, ), migrationDigest: readSha256( value.migrationDigest, `${fieldNameArg}.migrationDigest`, ), }; }; export const normalizeStorageBindingMigrationOwner = ( valueArg: unknown, ): Readonly => { return assertControlJsonLimit( normalizeMigrationOwner(valueArg, 'storage binding migration owner'), 'storage binding migration owner', ); }; const normalizeUnfencedSnapshotShape = ( valueArg: unknown, fieldNameArg: string, ): IUnfencedObjectStorageBindingControlSnapshot => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys( value, [ 'schemaVersion', 'binding', 'recordRevision', 'snapshotDigest', 'capturedAt', ], fieldNameArg, ); if (value.schemaVersion !== 2) { return fail(`${fieldNameArg}.schemaVersion must be 2`); } return { schemaVersion: 2, binding: normalizeObjectStorageBinding( value.binding, 'ready', `${fieldNameArg}.binding`, ) as TActiveObjectStorageBinding, recordRevision: readSafeInteger( value.recordRevision, `${fieldNameArg}.recordRevision`, 1, ), snapshotDigest: readSha256( value.snapshotDigest, `${fieldNameArg}.snapshotDigest`, ), capturedAt: readSafeInteger( value.capturedAt, `${fieldNameArg}.capturedAt`, 1, ), }; }; const normalizeFencedSnapshotShape = ( valueArg: unknown, fieldNameArg: string, ): IFencedObjectStorageBindingControlSnapshot => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys( value, [ 'schemaVersion', 'binding', 'recordRevision', 'snapshotDigest', 'capturedAt', 'activeMigration', ], fieldNameArg, ); if (value.schemaVersion !== 2) { return fail(`${fieldNameArg}.schemaVersion must be 2`); } return { schemaVersion: 2, binding: normalizeObjectStorageBinding( value.binding, 'ready', `${fieldNameArg}.binding`, ) as TActiveObjectStorageBinding, recordRevision: readSafeInteger( value.recordRevision, `${fieldNameArg}.recordRevision`, 1, ), snapshotDigest: readSha256( value.snapshotDigest, `${fieldNameArg}.snapshotDigest`, ), capturedAt: readSafeInteger( value.capturedAt, `${fieldNameArg}.capturedAt`, 1, ), activeMigration: normalizeMigrationOwner( value.activeMigration, `${fieldNameArg}.activeMigration`, ), }; }; const createUnfencedSnapshotDigestPayload = ( snapshotArg: IUnfencedObjectStorageBindingControlSnapshot, ): TUnfencedObjectStorageBindingControlSnapshotDigestPayload => ({ schemaVersion: 2, binding: snapshotArg.binding, recordRevision: snapshotArg.recordRevision, capturedAt: snapshotArg.capturedAt, }); const createFencedSnapshotDigestPayload = ( snapshotArg: IFencedObjectStorageBindingControlSnapshot, ): TFencedObjectStorageBindingControlSnapshotDigestPayload => ({ schemaVersion: 2, binding: snapshotArg.binding, recordRevision: snapshotArg.recordRevision, capturedAt: snapshotArg.capturedAt, activeMigration: snapshotArg.activeMigration, }); export const canonicalizeUnfencedObjectStorageBindingControlSnapshotPayload = ( valueArg: unknown, ): string => { const value = readRecord( valueArg, 'unfenced object-storage control snapshot digest payload', ); assertExactKeys( value, ['schemaVersion', 'binding', 'recordRevision', 'capturedAt'], 'unfenced object-storage control snapshot digest payload', ); const normalized = normalizeUnfencedSnapshotShape({ ...value, snapshotDigest: '0'.repeat(64), }, 'unfenced object-storage control snapshot digest payload'); return canonicalize(createUnfencedSnapshotDigestPayload(normalized)); }; export const canonicalizeFencedObjectStorageBindingControlSnapshotPayload = ( valueArg: unknown, ): string => { const value = readRecord( valueArg, 'fenced object-storage control snapshot digest payload', ); assertExactKeys( value, [ 'schemaVersion', 'binding', 'recordRevision', 'capturedAt', 'activeMigration', ], 'fenced object-storage control snapshot digest payload', ); const normalized = normalizeFencedSnapshotShape({ ...value, snapshotDigest: '0'.repeat(64), }, 'fenced object-storage control snapshot digest payload'); return canonicalize(createFencedSnapshotDigestPayload(normalized)); }; export const createUnfencedObjectStorageBindingControlSnapshotSha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256Hex( canonicalizeUnfencedObjectStorageBindingControlSnapshotPayload(valueArg), fail, ); }; export const createFencedObjectStorageBindingControlSnapshotSha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256Hex( canonicalizeFencedObjectStorageBindingControlSnapshotPayload(valueArg), fail, ); }; export const normalizeUnfencedObjectStorageBindingControlSnapshot = async ( valueArg: unknown, ): Promise> => { const normalized = normalizeUnfencedSnapshotShape( valueArg, 'unfenced object-storage control snapshot', ); const actualDigest = await createCanonicalSha256( createUnfencedSnapshotDigestPayload(normalized), ); if (normalized.snapshotDigest !== actualDigest) { return fail( 'unfenced object-storage control snapshot.snapshotDigest does not match its canonical payload', ); } return assertControlJsonLimit( normalized, 'unfenced object-storage control snapshot', ); }; export const normalizeFencedObjectStorageBindingControlSnapshot = async ( valueArg: unknown, ): Promise> => { const normalized = normalizeFencedSnapshotShape( valueArg, 'fenced object-storage control snapshot', ); const actualDigest = await createCanonicalSha256( createFencedSnapshotDigestPayload(normalized), ); if (normalized.snapshotDigest !== actualDigest) { return fail( 'fenced object-storage control snapshot.snapshotDigest does not match its canonical payload', ); } return assertControlJsonLimit( normalized, 'fenced object-storage control snapshot', ); }; export const normalizeObjectStorageMigrationTarget = ( valueArg: unknown, ): Readonly => { return assertControlJsonLimit( normalizeTarget(valueArg, 'object-storage migration target'), 'object-storage migration target', ); }; export const normalizeStagedObjectStorageBinding = ( valueArg: unknown, ): Readonly => { return assertControlJsonLimit( normalizeObjectStorageBinding( valueArg, 'provisioning', 'staged object-storage binding', ) as TStagedObjectStorageBinding, 'staged object-storage binding', ); }; export const normalizeActiveObjectStorageBinding = ( valueArg: unknown, ): Readonly => { return assertControlJsonLimit( normalizeObjectStorageBinding( valueArg, 'ready', 'active object-storage binding', ) as TActiveObjectStorageBinding, 'active object-storage binding', ); }; export const canonicalizeObjectStorageMigrationTargetRequest = ( valueArg: unknown, ): string => { return canonicalize( normalizeObjectStorageRequest( valueArg, 'object-storage migration target request', ), ); }; export const createObjectStorageMigrationTargetRequestSha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256Hex( canonicalizeObjectStorageMigrationTargetRequest(valueArg), fail, ); }; export const canonicalizeObjectStorageMigrationBinding = ( valueArg: unknown, ): string => { const value = readRecord(valueArg, 'object-storage migration binding'); const status = readLiteral( value.status, ['provisioning', 'ready'] as const, 'object-storage migration binding.status', ); return canonicalize( normalizeObjectStorageBinding( value, status, 'object-storage migration binding', ), ); }; export const createObjectStorageMigrationBindingSha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256Hex( canonicalizeObjectStorageMigrationBinding(valueArg), fail, ); }; const assertTargetIdentity = async ( targetArg: IObjectStorageMigrationTarget, sourceBindingArg: TActiveObjectStorageBinding, fieldNameArg: string, ): Promise => { if (targetArg.request.id !== sourceBindingArg.requestId) { fail(`${fieldNameArg}.request.id must equal the source binding requestId`); } if (targetArg.request.storageClass.length === 0) { fail(`${fieldNameArg}.request.storageClass must not be empty`); } const required = targetArg.storageClass.required; if (required?.durability === 'ephemeral') { fail(`${fieldNameArg}.storageClass cannot request ephemeral migration storage`); } }; const normalizePrepareRequestShape = ( valueArg: unknown, ): IObjectStorageMigrationPrepareRequest => { const value = readRecord(valueArg, 'object-storage migration prepare request'); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'sourceControlSnapshot', 'workloadGeneration', 'target', ], 'object-storage migration prepare request', ); if (value.schemaVersion !== 2) { return fail('object-storage migration prepare request.schemaVersion must be 2'); } return { schemaVersion: 2, migrationId: readIdentifier( value.migrationId, 'object-storage migration prepare request.migrationId', ), sourceControlSnapshot: normalizeUnfencedSnapshotShape( value.sourceControlSnapshot, 'object-storage migration prepare request.sourceControlSnapshot', ), workloadGeneration: readSafeInteger( value.workloadGeneration, 'object-storage migration prepare request.workloadGeneration', 1, ), target: normalizeTarget( value.target, 'object-storage migration prepare request.target', ), }; }; export const normalizeObjectStorageMigrationPrepareRequest = async ( valueArg: unknown, ): Promise> => { const normalized = normalizePrepareRequestShape(valueArg); await normalizeUnfencedObjectStorageBindingControlSnapshot( normalized.sourceControlSnapshot, ); await assertTargetIdentity( normalized.target, normalized.sourceControlSnapshot.binding, 'object-storage migration prepare request.target', ); return assertControlJsonLimit( normalized, 'object-storage migration prepare request', ); }; export const canonicalizeObjectStorageMigrationPrepareRequest = async ( valueArg: unknown, ): Promise => { return canonicalize( await normalizeObjectStorageMigrationPrepareRequest(valueArg), ); }; export const createObjectStorageMigrationSha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256Hex( await canonicalizeObjectStorageMigrationPrepareRequest(valueArg), fail, ); }; const normalizeProgress = ( valueArg: unknown, fieldNameArg: string, ): IStorageMigrationProgress => { const value = readRecord(valueArg, fieldNameArg); assertKeys( value, ['objectsCopied', 'logicalBytesCopied', 'updatedAt'], [ 'physicalBytesCopied', 'objectsTotal', 'logicalBytesTotal', 'physicalBytesTotal', ], fieldNameArg, ); const objectsCopied = readSafeInteger( value.objectsCopied, `${fieldNameArg}.objectsCopied`, 0, ); const logicalBytesCopied = readSafeInteger( value.logicalBytesCopied, `${fieldNameArg}.logicalBytesCopied`, 0, ); const physicalBytesCopied = Object.hasOwn(value, 'physicalBytesCopied') ? readSafeInteger( value.physicalBytesCopied, `${fieldNameArg}.physicalBytesCopied`, 0, ) : undefined; const objectsTotal = Object.hasOwn(value, 'objectsTotal') ? readSafeInteger(value.objectsTotal, `${fieldNameArg}.objectsTotal`, 0) : undefined; const logicalBytesTotal = Object.hasOwn(value, 'logicalBytesTotal') ? readSafeInteger( value.logicalBytesTotal, `${fieldNameArg}.logicalBytesTotal`, 0, ) : undefined; const physicalBytesTotal = Object.hasOwn(value, 'physicalBytesTotal') ? readSafeInteger( value.physicalBytesTotal, `${fieldNameArg}.physicalBytesTotal`, 0, ) : undefined; if (objectsTotal !== undefined && objectsCopied > objectsTotal) { return fail(`${fieldNameArg}.objectsCopied exceeds objectsTotal`); } if ( logicalBytesTotal !== undefined && logicalBytesCopied > logicalBytesTotal ) { return fail( `${fieldNameArg}.logicalBytesCopied exceeds logicalBytesTotal`, ); } if ( physicalBytesTotal !== undefined && physicalBytesCopied !== undefined && physicalBytesCopied > physicalBytesTotal ) { return fail( `${fieldNameArg}.physicalBytesCopied exceeds physicalBytesTotal`, ); } return { objectsCopied, logicalBytesCopied, ...(physicalBytesCopied !== undefined ? { physicalBytesCopied } : {}), ...(objectsTotal !== undefined ? { objectsTotal } : {}), ...(logicalBytesTotal !== undefined ? { logicalBytesTotal } : {}), ...(physicalBytesTotal !== undefined ? { physicalBytesTotal } : {}), updatedAt: readSafeInteger(value.updatedAt, `${fieldNameArg}.updatedAt`, 1), }; }; const normalizeIssue = ( valueArg: unknown, kindArg: 'pre' | 'post', fieldNameArg: string, ): IStorageMigrationPreCutoverIssue | IStorageMigrationPostCutoverIssue => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys( value, ['code', 'message', 'retryable', 'observedAt', 'allowedRecoveryActions'], fieldNameArg, ); const entries = readDenseArray( value.allowedRecoveryActions, `${fieldNameArg}.allowedRecoveryActions`, 1, 2, ); const allowed = kindArg === 'pre' ? (['retry', 'abort'] as const) : (['retry', 'rollForward'] as const); const actions = entries.map((entryArg, indexArg) => readLiteral( entryArg, allowed, `${fieldNameArg}.allowedRecoveryActions[${indexArg}]`, ) ); if (new Set(actions).size !== actions.length) { return fail(`${fieldNameArg}.allowedRecoveryActions must be distinct`); } const retryable = readBoolean(value.retryable, `${fieldNameArg}.retryable`); if (retryable !== actions.includes('retry')) { return fail( `${fieldNameArg}.retryable must match whether retry is an allowed action`, ); } return { code: readIdentifier(value.code, `${fieldNameArg}.code`), message: readString( value.message, `${fieldNameArg}.message`, storageMigrationContractLimits.maximumIssueMessageBytes, ), retryable, observedAt: readSafeInteger( value.observedAt, `${fieldNameArg}.observedAt`, 1, ), allowedRecoveryActions: actions, } as IStorageMigrationPreCutoverIssue | IStorageMigrationPostCutoverIssue; }; const normalizeMutationFence = ( valueArg: Record, fieldNameArg: string, ): IStorageMigrationMutationFence => { if (valueArg.schemaVersion !== 2) { return fail(`${fieldNameArg}.schemaVersion must be 2`); } return { schemaVersion: 2, migrationId: readIdentifier( valueArg.migrationId, `${fieldNameArg}.migrationId`, ), migrationDigest: readSha256( valueArg.migrationDigest, `${fieldNameArg}.migrationDigest`, ), expectedMigrationRevision: readSafeInteger( valueArg.expectedMigrationRevision, `${fieldNameArg}.expectedMigrationRevision`, 1, ), mutationId: readIdentifier( valueArg.mutationId, `${fieldNameArg}.mutationId`, ), }; }; export const normalizeStorageMigrationMutationFence = ( valueArg: unknown, ): Readonly => { const fieldName = 'storage migration mutation fence'; const value = readRecord(valueArg, fieldName); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'migrationDigest', 'expectedMigrationRevision', 'mutationId', ], fieldName, ); return assertControlJsonLimit(normalizeMutationFence(value, fieldName), fieldName); }; const normalizeQuiesceRequestShape = ( valueArg: unknown, ): IObjectStorageMigrationConsumerQuiesceRequest => { const fieldName = 'object-storage migration consumer quiesce request'; const value = readRecord(valueArg, fieldName); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'migrationDigest', 'expectedMigrationRevision', 'mutationId', 'sourceRecordRevision', 'sourceControlSnapshotDigest', 'candidateDigest', 'workloadGeneration', 'persistedStagingDigest', 'persistedStagingRevision', 'consumerStoppedAt', ], fieldName, ); return { ...normalizeMutationFence(value, fieldName), sourceRecordRevision: readSafeInteger( value.sourceRecordRevision, `${fieldName}.sourceRecordRevision`, 1, ), sourceControlSnapshotDigest: readSha256( value.sourceControlSnapshotDigest, `${fieldName}.sourceControlSnapshotDigest`, ), candidateDigest: readSha256( value.candidateDigest, `${fieldName}.candidateDigest`, ), workloadGeneration: readSafeInteger( value.workloadGeneration, `${fieldName}.workloadGeneration`, 1, ), persistedStagingDigest: readSha256( value.persistedStagingDigest, `${fieldName}.persistedStagingDigest`, ), persistedStagingRevision: readSafeInteger( value.persistedStagingRevision, `${fieldName}.persistedStagingRevision`, 1, ), consumerStoppedAt: readSafeInteger( value.consumerStoppedAt, `${fieldName}.consumerStoppedAt`, 1, ), }; }; const createPersistedStagingDigestPayload = ( requestArg: IObjectStorageMigrationConsumerQuiesceRequest, ): TObjectStorageMigrationPersistedStagingDigestPayload => ({ schemaVersion: 2, migrationId: requestArg.migrationId, migrationDigest: requestArg.migrationDigest, expectedMigrationRevision: requestArg.expectedMigrationRevision, mutationId: requestArg.mutationId, sourceRecordRevision: requestArg.sourceRecordRevision, sourceControlSnapshotDigest: requestArg.sourceControlSnapshotDigest, candidateDigest: requestArg.candidateDigest, workloadGeneration: requestArg.workloadGeneration, persistedStagingRevision: requestArg.persistedStagingRevision, consumerStoppedAt: requestArg.consumerStoppedAt, }); export const createObjectStorageMigrationPersistedStagingSha256 = async ( valueArg: unknown, ): Promise => { const value = readRecord(valueArg, 'persisted staging digest payload'); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'migrationDigest', 'expectedMigrationRevision', 'mutationId', 'sourceRecordRevision', 'sourceControlSnapshotDigest', 'candidateDigest', 'workloadGeneration', 'persistedStagingRevision', 'consumerStoppedAt', ], 'persisted staging digest payload', ); const request = normalizeQuiesceRequestShape({ ...value, persistedStagingDigest: '0'.repeat(64), }); return createCanonicalSha256(createPersistedStagingDigestPayload(request)); }; export const normalizeObjectStorageMigrationConsumerQuiesceRequest = async ( valueArg: unknown, ): Promise> => { const normalized = normalizeQuiesceRequestShape(valueArg); const actualDigest = await createCanonicalSha256( createPersistedStagingDigestPayload(normalized), ); if (normalized.persistedStagingDigest !== actualDigest) { return fail( 'object-storage migration consumer quiesce request.persistedStagingDigest does not match its canonical payload', ); } return assertControlJsonLimit( normalized, 'object-storage migration consumer quiesce request', ); }; const normalizeActivationRequestShape = ( valueArg: unknown, ): IObjectStorageMigrationConsumerActivationRequest => { const fieldName = 'object-storage migration consumer activation request'; const value = readRecord(valueArg, fieldName); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'migrationDigest', 'expectedMigrationRevision', 'mutationId', 'candidateDigest', 'activeBindingDigest', 'workloadGeneration', 'persistedActivationDigest', 'persistedActivationRevision', 'consumerStartedAt', ], fieldName, ); return { ...normalizeMutationFence(value, fieldName), candidateDigest: readSha256( value.candidateDigest, `${fieldName}.candidateDigest`, ), activeBindingDigest: readSha256( value.activeBindingDigest, `${fieldName}.activeBindingDigest`, ), workloadGeneration: readSafeInteger( value.workloadGeneration, `${fieldName}.workloadGeneration`, 1, ), persistedActivationDigest: readSha256( value.persistedActivationDigest, `${fieldName}.persistedActivationDigest`, ), persistedActivationRevision: readSafeInteger( value.persistedActivationRevision, `${fieldName}.persistedActivationRevision`, 1, ), consumerStartedAt: readSafeInteger( value.consumerStartedAt, `${fieldName}.consumerStartedAt`, 1, ), }; }; const createPersistedActivationDigestPayload = ( requestArg: IObjectStorageMigrationConsumerActivationRequest, ): TObjectStorageMigrationPersistedActivationDigestPayload => ({ schemaVersion: 2, migrationId: requestArg.migrationId, migrationDigest: requestArg.migrationDigest, expectedMigrationRevision: requestArg.expectedMigrationRevision, mutationId: requestArg.mutationId, candidateDigest: requestArg.candidateDigest, activeBindingDigest: requestArg.activeBindingDigest, workloadGeneration: requestArg.workloadGeneration, persistedActivationRevision: requestArg.persistedActivationRevision, consumerStartedAt: requestArg.consumerStartedAt, }); export const createObjectStorageMigrationPersistedActivationSha256 = async ( valueArg: unknown, ): Promise => { const value = readRecord(valueArg, 'persisted activation digest payload'); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'migrationDigest', 'expectedMigrationRevision', 'mutationId', 'candidateDigest', 'activeBindingDigest', 'workloadGeneration', 'persistedActivationRevision', 'consumerStartedAt', ], 'persisted activation digest payload', ); const request = normalizeActivationRequestShape({ ...value, persistedActivationDigest: '0'.repeat(64), }); return createCanonicalSha256(createPersistedActivationDigestPayload(request)); }; export const normalizeObjectStorageMigrationConsumerActivationRequest = async ( valueArg: unknown, ): Promise> => { const normalized = normalizeActivationRequestShape(valueArg); const actualDigest = await createCanonicalSha256( createPersistedActivationDigestPayload(normalized), ); if (normalized.persistedActivationDigest !== actualDigest) { return fail( 'object-storage migration consumer activation request.persistedActivationDigest does not match its canonical payload', ); } return assertControlJsonLimit( normalized, 'object-storage migration consumer activation request', ); }; const normalizeQuiesceEvidence = async ( valueArg: unknown, fieldNameArg: string, ): Promise => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys( value, ['request', 'acceptedMigrationRevision', 'acceptedAt'], fieldNameArg, ); const request = await normalizeObjectStorageMigrationConsumerQuiesceRequest( value.request, ); const acceptedMigrationRevision = readSafeInteger( value.acceptedMigrationRevision, `${fieldNameArg}.acceptedMigrationRevision`, 1, ); if (acceptedMigrationRevision <= request.expectedMigrationRevision) { return fail( `${fieldNameArg}.acceptedMigrationRevision must advance the requested revision`, ); } const acceptedAt = readSafeInteger( value.acceptedAt, `${fieldNameArg}.acceptedAt`, request.consumerStoppedAt, ); return { request, acceptedMigrationRevision, acceptedAt }; }; const normalizeActivationEvidence = async ( valueArg: unknown, fieldNameArg: string, ): Promise => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys( value, ['request', 'acceptedMigrationRevision', 'acceptedAt'], fieldNameArg, ); const request = await normalizeObjectStorageMigrationConsumerActivationRequest( value.request, ); const acceptedMigrationRevision = readSafeInteger( value.acceptedMigrationRevision, `${fieldNameArg}.acceptedMigrationRevision`, 1, ); if (acceptedMigrationRevision <= request.expectedMigrationRevision) { return fail( `${fieldNameArg}.acceptedMigrationRevision must advance the requested revision`, ); } const acceptedAt = readSafeInteger( value.acceptedAt, `${fieldNameArg}.acceptedAt`, request.consumerStartedAt, ); return { request, acceptedMigrationRevision, acceptedAt }; }; export const normalizeStorageMigrationConsumerQuiesceEvidence = async ( valueArg: unknown, ): Promise> => { return assertControlJsonLimit( await normalizeQuiesceEvidence( valueArg, 'storage migration consumer quiesce evidence', ), 'storage migration consumer quiesce evidence', ); }; export const normalizeStorageMigrationConsumerActivationEvidence = async ( valueArg: unknown, ): Promise> => { return assertControlJsonLimit( await normalizeActivationEvidence( valueArg, 'storage migration consumer activation evidence', ), 'storage migration consumer activation evidence', ); }; export const normalizeObjectStorageMigrationAbortRequest = ( valueArg: unknown, ): Readonly => { const fieldName = 'object-storage migration abort request'; const value = readRecord(valueArg, fieldName); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'migrationDigest', 'expectedMigrationRevision', 'mutationId', 'reason', ], fieldName, ); return assertControlJsonLimit({ ...normalizeMutationFence(value, fieldName), reason: readString( value.reason, `${fieldName}.reason`, storageMigrationContractLimits.maximumAbortReasonBytes, ), }, fieldName); }; export const normalizeObjectStorageMigrationResumeRequest = ( valueArg: unknown, ): Readonly => { const fieldName = 'object-storage migration resume request'; const value = readRecord(valueArg, fieldName); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'migrationDigest', 'expectedMigrationRevision', 'mutationId', 'action', ], fieldName, ); return assertControlJsonLimit({ ...normalizeMutationFence(value, fieldName), action: readLiteral( value.action, ['retry', 'abort', 'rollForward'] as const, `${fieldName}.action`, ), }, fieldName); }; export const normalizeObjectStorageMigrationCleanupRequest = ( valueArg: unknown, ): Readonly => { const fieldName = 'object-storage migration cleanup request'; const value = readRecord(valueArg, fieldName); assertExactKeys( value, [ 'schemaVersion', 'migrationId', 'migrationDigest', 'expectedMigrationRevision', 'mutationId', ], fieldName, ); return assertControlJsonLimit(normalizeMutationFence(value, fieldName), fieldName); }; const assertCapabilitiesSatisfyTarget = ( bindingArg: TStagedObjectStorageBinding, targetArg: IObjectStorageMigrationTarget, fieldNameArg: string, ): void => { const required = targetArg.storageClass.required; const capabilities = bindingArg.capabilities; if (required) { for (const key of [ 'performanceTier', 'durability', 'topology', 'snapshots', ] as const) { const requirement = required[key]; if (requirement !== undefined && capabilities[key] !== requirement) { fail(`${fieldNameArg}.capabilities.${key} does not satisfy target requirements`); } } for (const key of [ 'hardQuota', 'backup', 'encryptedInTransit', ] as const) { if (required[key] === true && capabilities[key] !== true) { fail(`${fieldNameArg}.capabilities.${key} does not satisfy target requirements`); } } } }; const assertCandidateIdentity = async ( candidateArg: TStagedObjectStorageBinding, sourceArg: TActiveObjectStorageBinding, targetArg: IObjectStorageMigrationTarget, fieldNameArg: string, ): Promise => { if ( candidateArg.id !== sourceArg.id || candidateArg.serviceId !== sourceArg.serviceId || candidateArg.requestId !== sourceArg.requestId ) { fail(`${fieldNameArg} must preserve binding, service, and request identity`); } if (candidateArg.requestId !== targetArg.request.id) { fail(`${fieldNameArg}.requestId must equal target.request.id`); } const targetRequestDigest = await createObjectStorageMigrationTargetRequestSha256(targetArg.request); if (candidateArg.requestDigest !== targetRequestDigest) { fail(`${fieldNameArg}.requestDigest does not match target.request`); } if ( canonicalize(candidateArg.policy) !== canonicalize(targetArg.policy) || candidateArg.credentialManagementScope !== targetArg.credentialManagementScope || canonicalize(candidateArg.delivery) !== canonicalize(targetArg.request.delivery) ) { fail(`${fieldNameArg} does not match the target policy and delivery intent`); } if (candidateArg.accessMode !== targetArg.request.accessMode) { fail(`${fieldNameArg}.accessMode does not match target.request.accessMode`); } if (candidateArg.resourceRef === sourceArg.resourceRef) { fail(`${fieldNameArg}.resourceRef must differ from the source resource`); } if (candidateArg.generation <= sourceArg.generation) { fail(`${fieldNameArg}.generation must advance the source generation`); } if (targetArg.request.capacity) { if ( candidateArg.capacity?.requested !== targetArg.request.capacity.request || candidateArg.capacity?.limit !== targetArg.request.capacity.limit ) { fail(`${fieldNameArg}.capacity must preserve the target request bounds`); } } if ( targetArg.request.protection?.backup === 'required' && candidateArg.capabilities.backup !== true ) { fail(`${fieldNameArg}.capabilities.backup does not satisfy target protection`); } if ( targetArg.request.capacity?.limit !== undefined && candidateArg.capabilities.hardQuota !== true ) { fail(`${fieldNameArg}.capabilities.hardQuota is required by target capacity.limit`); } if ( targetArg.request.protection?.versioning === 'required' && candidateArg.versioning !== true ) { fail(`${fieldNameArg}.versioning does not satisfy target protection`); } if ( targetArg.request.protection?.retentionDays !== undefined && (candidateArg.retentionDays ?? 0) < targetArg.request.protection.retentionDays ) { fail(`${fieldNameArg}.retentionDays does not satisfy target protection`); } assertCapabilitiesSatisfyTarget(candidateArg, targetArg, fieldNameArg); }; const assertActiveMatchesCandidate = ( activeArg: TActiveObjectStorageBinding, candidateArg: TStagedObjectStorageBinding, fieldNameArg: string, ): void => { const immutableKeys: readonly (keyof TActiveObjectStorageBinding)[] = [ 'schemaVersion', 'id', 'serviceId', 'requestId', 'requestDigest', 'kind', 'generation', 'policy', 'capabilities', 'resourceRef', 'capacity', 'accessMode', 'connection', 'credentialManagementScope', 'delivery', 'versioning', 'retentionDays', 'createdAt', ]; for (const key of immutableKeys) { if (canonicalize(activeArg[key] ?? null) !== canonicalize(candidateArg[key] ?? null)) { fail(`${fieldNameArg}.${key} must match the issued candidate`); } } }; const normalizeSourceEvidence = async ( valueArg: unknown, fieldNameArg: string, ): Promise => { const value = readRecord(valueArg, fieldNameArg); assertExactKeys( value, ['preparationSnapshot', 'fencedSnapshot'], fieldNameArg, ); const preparationSnapshot = await normalizeUnfencedObjectStorageBindingControlSnapshot( value.preparationSnapshot, ); const fencedSnapshot = await normalizeFencedObjectStorageBindingControlSnapshot( value.fencedSnapshot, ); if ( fencedSnapshot.recordRevision <= preparationSnapshot.recordRevision || fencedSnapshot.capturedAt < preparationSnapshot.capturedAt ) { return fail( `${fieldNameArg}.fencedSnapshot must be captured at a later record revision`, ); } if ( canonicalize(fencedSnapshot.binding) !== canonicalize(preparationSnapshot.binding) ) { return fail( `${fieldNameArg}.fencedSnapshot.binding must exactly preserve the prepared source binding`, ); } return { preparationSnapshot, fencedSnapshot, }; }; export const normalizeStorageMigrationSourceEvidence = async ( valueArg: unknown, ): Promise> => { return assertControlJsonLimit( await normalizeSourceEvidence(valueArg, 'storage migration source evidence'), 'storage migration source evidence', ); }; const assertEvidenceFence = ( fenceArg: IStorageMigrationMutationFence, migrationIdArg: string, migrationDigestArg: string, migrationRevisionArg: number, fieldNameArg: string, ): void => { if ( fenceArg.migrationId !== migrationIdArg || fenceArg.migrationDigest !== migrationDigestArg ) { fail(`${fieldNameArg} does not match the migration identity`); } if (fenceArg.expectedMigrationRevision >= migrationRevisionArg) { fail(`${fieldNameArg}.expectedMigrationRevision must precede status revision`); } }; const statusBaseRequiredKeys = [ 'schemaVersion', 'migrationId', 'migrationDigest', 'migrationRevision', 'bindingId', 'serviceId', 'requestId', 'workloadGeneration', 'target', 'source', 'createdAt', 'updatedAt', 'phase', 'bindingFenceActive', 'candidateIssued', 'commitPointCrossed', 'destinationBindingStartAuthorized', 'authority', 'consumerAction', ] as const; const assertFieldValue = ( recordArg: Record, keyArg: string, expectedArg: unknown, fieldNameArg: string, ): void => { if (recordArg[keyArg] !== expectedArg) { fail(`${fieldNameArg}.${keyArg} must be ${String(expectedArg)}`); } }; export const normalizeObjectStorageMigrationStatus = async ( valueArg: unknown, ): Promise> => { const fieldName = 'object-storage migration status'; const value = readRecord(valueArg, fieldName); const phase = readLiteral( value.phase, [ 'preparing', 'transferring', 'awaitingConsumerQuiesce', 'finalizing', 'committing', 'readyToStart', 'cleanupPending', 'complete', 'aborting', 'aborted', ] as const, `${fieldName}.phase`, ); const candidateIssued = readBoolean( value.candidateIssued, `${fieldName}.candidateIssued`, ); const phaseRequired: string[] = []; const phaseOptional: string[] = ['progress']; if (phase !== 'complete' && phase !== 'aborted') { phaseOptional.push('issue'); } if ( ['awaitingConsumerQuiesce', 'finalizing', 'committing', 'readyToStart', 'cleanupPending', 'complete'].includes(phase) ) { phaseRequired.push('candidateBinding', 'candidateDigest'); } else if ( (phase === 'aborting' || phase === 'aborted') && candidateIssued ) { phaseRequired.push('candidateDigest'); } if ( ['finalizing', 'committing', 'readyToStart', 'cleanupPending', 'complete'] .includes(phase) ) { phaseRequired.push('consumerQuiesceEvidence'); } else if ( (phase === 'aborting' || phase === 'aborted') && candidateIssued ) { phaseOptional.push('consumerQuiesceEvidence'); } if (phase === 'cleanupPending' || phase === 'complete') { phaseRequired.push('consumerActivationEvidence'); } if ( phase === 'readyToStart' || phase === 'cleanupPending' || phase === 'complete' || phase === 'aborted' ) { phaseRequired.push('activeBinding', 'activeBindingDigest'); } if (phase === 'complete' || phase === 'aborted') { phaseRequired.push('terminalAt'); } assertKeys( value, [...statusBaseRequiredKeys, ...phaseRequired], phaseOptional, fieldName, ); if (value.schemaVersion !== 2) { return fail(`${fieldName}.schemaVersion must be 2`); } const migrationId = readIdentifier(value.migrationId, `${fieldName}.migrationId`); const migrationDigest = readSha256( value.migrationDigest, `${fieldName}.migrationDigest`, ); const migrationRevision = readSafeInteger( value.migrationRevision, `${fieldName}.migrationRevision`, 1, ); const bindingId = readIdentifier(value.bindingId, `${fieldName}.bindingId`); const serviceId = readIdentifier(value.serviceId, `${fieldName}.serviceId`); const requestId = readIdentifier(value.requestId, `${fieldName}.requestId`); const workloadGeneration = readSafeInteger( value.workloadGeneration, `${fieldName}.workloadGeneration`, 1, ); const target = normalizeTarget(value.target, `${fieldName}.target`); const source = await normalizeSourceEvidence( value.source, `${fieldName}.source`, ); const sourceBinding = source.preparationSnapshot.binding; if ( bindingId !== sourceBinding.id || serviceId !== sourceBinding.serviceId || requestId !== sourceBinding.requestId || target.request.id !== requestId ) { return fail(`${fieldName} identity must match source and target bindings`); } await assertTargetIdentity(target, sourceBinding, `${fieldName}.target`); const recomputedMigrationDigest = await createObjectStorageMigrationSha256({ schemaVersion: 2, migrationId, sourceControlSnapshot: source.preparationSnapshot, workloadGeneration, target, }); if ( migrationDigest !== recomputedMigrationDigest || source.fencedSnapshot.activeMigration.migrationId !== migrationId || source.fencedSnapshot.activeMigration.migrationDigest !== migrationDigest ) { return fail( `${fieldName}.migrationDigest and fenced owner must match canonical prepare intent`, ); } const createdAt = readSafeInteger(value.createdAt, `${fieldName}.createdAt`, 1); const updatedAt = readSafeInteger( value.updatedAt, `${fieldName}.updatedAt`, createdAt, ); const progress = Object.hasOwn(value, 'progress') ? normalizeProgress(value.progress, `${fieldName}.progress`) : undefined; if (progress && (progress.updatedAt < createdAt || progress.updatedAt > updatedAt)) { return fail(`${fieldName}.progress.updatedAt must be within status time bounds`); } const preIssuePhase = [ 'preparing', 'transferring', 'awaitingConsumerQuiesce', 'finalizing', 'aborting', ].includes(phase); const issue = Object.hasOwn(value, 'issue') ? normalizeIssue( value.issue, preIssuePhase ? 'pre' : 'post', `${fieldName}.issue`, ) : undefined; if (issue && (issue.observedAt < createdAt || issue.observedAt > updatedAt)) { return fail(`${fieldName}.issue.observedAt must be within status time bounds`); } const expectedByPhase = { preparing: [true, false, false, false, 'source', 'wait'], transferring: [true, false, false, false, 'source', 'wait'], awaitingConsumerQuiesce: [ true, true, false, false, 'source', 'stageCandidateAndStop', ], finalizing: [true, true, false, false, 'source', 'wait'], committing: [true, true, true, false, 'transitioning', 'wait'], readyToStart: [true, true, true, true, 'destination', 'startDestination'], cleanupPending: [true, true, true, true, 'destination', 'none'], complete: [false, true, true, true, 'destination', 'none'], aborting: [true, candidateIssued, false, false, 'source', 'wait'], aborted: [false, candidateIssued, false, false, 'source', 'startSource'], } as const; const expected = expectedByPhase[phase]; assertFieldValue(value, 'bindingFenceActive', expected[0], fieldName); assertFieldValue(value, 'candidateIssued', expected[1], fieldName); assertFieldValue(value, 'commitPointCrossed', expected[2], fieldName); assertFieldValue( value, 'destinationBindingStartAuthorized', expected[3], fieldName, ); assertFieldValue(value, 'authority', expected[4], fieldName); assertFieldValue(value, 'consumerAction', expected[5], fieldName); let candidateBinding: TStagedObjectStorageBinding | undefined; let candidateDigest: string | undefined; if (Object.hasOwn(value, 'candidateBinding')) { candidateBinding = normalizeObjectStorageBinding( value.candidateBinding, 'provisioning', `${fieldName}.candidateBinding`, ) as TStagedObjectStorageBinding; candidateDigest = readSha256( value.candidateDigest, `${fieldName}.candidateDigest`, ); const recomputedCandidateDigest = await createObjectStorageMigrationBindingSha256(candidateBinding); if (candidateDigest !== recomputedCandidateDigest) { return fail(`${fieldName}.candidateDigest does not match candidateBinding`); } await assertCandidateIdentity( candidateBinding, sourceBinding, target, `${fieldName}.candidateBinding`, ); } else if (Object.hasOwn(value, 'candidateDigest')) { candidateDigest = readSha256( value.candidateDigest, `${fieldName}.candidateDigest`, ); } const quiesceEvidence = Object.hasOwn(value, 'consumerQuiesceEvidence') ? await normalizeQuiesceEvidence( value.consumerQuiesceEvidence, `${fieldName}.consumerQuiesceEvidence`, ) : undefined; if (quiesceEvidence) { assertEvidenceFence( quiesceEvidence.request, migrationId, migrationDigest, migrationRevision, `${fieldName}.consumerQuiesceEvidence.request`, ); if ( quiesceEvidence.acceptedMigrationRevision > migrationRevision || quiesceEvidence.request.sourceRecordRevision !== source.fencedSnapshot.recordRevision || quiesceEvidence.request.sourceControlSnapshotDigest !== source.fencedSnapshot.snapshotDigest || quiesceEvidence.request.candidateDigest !== candidateDigest || quiesceEvidence.request.workloadGeneration !== workloadGeneration || quiesceEvidence.acceptedAt < createdAt || quiesceEvidence.acceptedAt > updatedAt ) { return fail(`${fieldName}.consumerQuiesceEvidence does not match status`); } } let activeBinding: TActiveObjectStorageBinding | undefined; let activeBindingDigest: string | undefined; if (Object.hasOwn(value, 'activeBinding')) { activeBinding = normalizeObjectStorageBinding( value.activeBinding, 'ready', `${fieldName}.activeBinding`, ) as TActiveObjectStorageBinding; activeBindingDigest = readSha256( value.activeBindingDigest, `${fieldName}.activeBindingDigest`, ); const recomputedActiveDigest = await createObjectStorageMigrationBindingSha256(activeBinding); if (activeBindingDigest !== recomputedActiveDigest) { return fail(`${fieldName}.activeBindingDigest does not match activeBinding`); } if (phase === 'aborted') { if (canonicalize(activeBinding) !== canonicalize(sourceBinding)) { return fail( `${fieldName}.activeBinding must be the exact source binding after abort`, ); } } else { if (!candidateBinding) { return fail(`${fieldName}.activeBinding requires candidateBinding`); } assertActiveMatchesCandidate( activeBinding, candidateBinding, `${fieldName}.activeBinding`, ); } } const activationEvidence = Object.hasOwn(value, 'consumerActivationEvidence') ? await normalizeActivationEvidence( value.consumerActivationEvidence, `${fieldName}.consumerActivationEvidence`, ) : undefined; if (activationEvidence) { assertEvidenceFence( activationEvidence.request, migrationId, migrationDigest, migrationRevision, `${fieldName}.consumerActivationEvidence.request`, ); if ( activationEvidence.acceptedMigrationRevision > migrationRevision || activationEvidence.request.candidateDigest !== candidateDigest || activationEvidence.request.activeBindingDigest !== activeBindingDigest || activationEvidence.request.workloadGeneration !== workloadGeneration || !quiesceEvidence || activationEvidence.request.expectedMigrationRevision < quiesceEvidence.acceptedMigrationRevision || activationEvidence.request.consumerStartedAt < quiesceEvidence.acceptedAt || activationEvidence.acceptedAt < createdAt || activationEvidence.acceptedAt > updatedAt ) { return fail(`${fieldName}.consumerActivationEvidence does not match status`); } } const terminalAt = Object.hasOwn(value, 'terminalAt') ? readSafeInteger(value.terminalAt, `${fieldName}.terminalAt`, createdAt) : undefined; if (terminalAt !== undefined && terminalAt > updatedAt) { return fail(`${fieldName}.terminalAt must not exceed updatedAt`); } const normalized = { schemaVersion: 2 as const, migrationId, migrationDigest, migrationRevision, bindingId, serviceId, requestId, workloadGeneration, target, source, ...(progress ? { progress } : {}), createdAt, updatedAt, phase, bindingFenceActive: expected[0], candidateIssued: expected[1], ...(candidateBinding ? { candidateBinding } : {}), ...(candidateDigest ? { candidateDigest } : {}), ...(quiesceEvidence ? { consumerQuiesceEvidence: quiesceEvidence } : {}), ...(activationEvidence ? { consumerActivationEvidence: activationEvidence } : {}), commitPointCrossed: expected[2], destinationBindingStartAuthorized: expected[3], authority: expected[4], consumerAction: expected[5], ...(activeBinding ? { activeBinding } : {}), ...(activeBindingDigest ? { activeBindingDigest } : {}), ...(terminalAt !== undefined ? { terminalAt } : {}), ...(issue ? { issue } : {}), } as unknown as TObjectStorageMigrationStatus; return assertControlJsonLimit(normalized, fieldName); }; const assertCurrentMutationFence = ( requestArg: IStorageMigrationMutationFence, statusArg: TObjectStorageMigrationStatus, fieldNameArg: string, ): void => { if ( requestArg.migrationId !== statusArg.migrationId || requestArg.migrationDigest !== statusArg.migrationDigest ) { fail(`${fieldNameArg} does not match current migration identity`); } if (requestArg.expectedMigrationRevision !== statusArg.migrationRevision) { fail(`${fieldNameArg}.expectedMigrationRevision is stale`); } }; export const bindObjectStorageMigrationConsumerQuiesceRequest = async ( requestArg: unknown, statusArg: unknown, ): Promise> => { const request = await normalizeObjectStorageMigrationConsumerQuiesceRequest(requestArg); const status = await normalizeObjectStorageMigrationStatus(statusArg); if (status.phase !== 'awaitingConsumerQuiesce') { return fail( 'consumer quiesce request is accepted only while awaiting consumer quiescence', ); } assertCurrentMutationFence(request, status, 'consumer quiesce request'); if ( request.sourceRecordRevision !== status.source.fencedSnapshot.recordRevision || request.sourceControlSnapshotDigest !== status.source.fencedSnapshot.snapshotDigest || request.candidateDigest !== status.candidateDigest || request.workloadGeneration !== status.workloadGeneration ) { return fail('consumer quiesce request does not match the current fences'); } return request; }; export const bindObjectStorageMigrationConsumerActivationRequest = async ( requestArg: unknown, statusArg: unknown, ): Promise> => { const request = await normalizeObjectStorageMigrationConsumerActivationRequest(requestArg); const status = await normalizeObjectStorageMigrationStatus(statusArg); if ( status.phase !== 'readyToStart' || status.consumerAction !== 'startDestination' || status.destinationBindingStartAuthorized !== true ) { return fail( 'consumer activation request is accepted only for an authorized ready-to-start destination', ); } assertCurrentMutationFence(request, status, 'consumer activation request'); if ( request.candidateDigest !== status.candidateDigest || request.activeBindingDigest !== status.activeBindingDigest || request.workloadGeneration !== status.workloadGeneration || request.consumerStartedAt < status.consumerQuiesceEvidence.request.consumerStoppedAt ) { return fail('consumer activation request does not match destination fences'); } return request; }; export const bindObjectStorageMigrationAbortRequest = async ( requestArg: unknown, statusArg: unknown, ): Promise> => { const request = normalizeObjectStorageMigrationAbortRequest(requestArg); const status = await normalizeObjectStorageMigrationStatus(statusArg); if ( status.bindingFenceActive !== true || status.commitPointCrossed !== false || ![ 'preparing', 'transferring', 'awaitingConsumerQuiesce', 'finalizing', 'aborting', ].includes(status.phase) ) { return fail('abort request is forbidden after the migration commit point'); } assertCurrentMutationFence(request, status, 'abort request'); return request; }; export const bindObjectStorageMigrationResumeRequest = async ( requestArg: unknown, statusArg: unknown, ): Promise> => { const request = normalizeObjectStorageMigrationResumeRequest(requestArg); const status = await normalizeObjectStorageMigrationStatus(statusArg); if (status.bindingFenceActive !== true) { return fail( 'resume request requires a live nonterminal migration binding fence', ); } assertCurrentMutationFence(request, status, 'resume request'); const issue = 'issue' in status ? status.issue : undefined; if (!issue) { return fail('resume request requires a current migration issue'); } if ( !issue.allowedRecoveryActions.some( (actionArg) => actionArg === request.action, ) ) { return fail('resume request action is not advertised by the current issue'); } return request; }; export const bindObjectStorageMigrationCleanupRequest = async ( requestArg: unknown, statusArg: unknown, ): Promise> => { const request = normalizeObjectStorageMigrationCleanupRequest(requestArg); const status = await normalizeObjectStorageMigrationStatus(statusArg); if ( status.phase !== 'cleanupPending' || !status.consumerActivationEvidence ) { return fail( 'cleanup request is accepted only after destination activation acknowledgement', ); } assertCurrentMutationFence(request, status, 'cleanup request'); return request; }; export const normalizeStorageMigrationProgress = ( valueArg: unknown, ): Readonly => { return assertControlJsonLimit( normalizeProgress(valueArg, 'storage migration progress'), 'storage migration progress', ); }; export const normalizeStorageMigrationPreCutoverIssue = ( valueArg: unknown, ): Readonly => { return assertControlJsonLimit( normalizeIssue( valueArg, 'pre', 'storage migration pre-cutover issue', ) as IStorageMigrationPreCutoverIssue, 'storage migration pre-cutover issue', ); }; export const normalizeStorageMigrationPostCutoverIssue = ( valueArg: unknown, ): Readonly => { return assertControlJsonLimit( normalizeIssue( valueArg, 'post', 'storage migration post-cutover issue', ) as IStorageMigrationPostCutoverIssue, 'storage migration post-cutover issue', ); }; export { storageMigrationCanonicalDigestGoldenVectors, type IStorageMigrationCanonicalDigestGoldenVector, } from './storagemigration.golden.js';