import type { IBackupArchiveManifest, IBackupArchiveObject, TIsolatedRestoreResourceMapping, } from './backup.js'; import { canonicalizeStrictJson, createCanonicalJsonSha256Hex, createSha256Hex, deepFreezeValue, strictCanonicalJsonRules, } from '../private/canonicaljson.js'; /** Operations carried by a short-lived, single-operation restore grant. */ export const isolatedRestoreGrantOperations = Object.freeze([ 'prepare', 'write-object', 'execute', 'cleanup', 'status', ] as const); export type TIsolatedRestoreGrantOperation = (typeof isolatedRestoreGrantOperations)[number]; /** * Versioned limits shared by signers, relays, and Corestore. * * V1 uses an inline descriptor manifest inside a 1 MiB control request. Its * 4,096 object slots reserve 256 entries for config, snapshots, keys, and * index segments; the remaining 3,840 slots fit 1,920 default 8 MiB pack/index * pairs. The 15 GiB total envelope is therefore reachable with the default * ContainerArchive packing profile instead of advertising impossible capacity. */ export const isolatedRestoreContractLimits = Object.freeze({ version: 1 as const, maximumControlJsonBytes: 1024 * 1024, maximumRestoreGrantBytes: 64 * 1024, maximumConfiguredStringBytes: 2048, maximumPathBytes: 4096, maximumResourceMappings: 256, maximumArchiveObjects: 4096, reservedArchiveMetadataObjects: 256, defaultContainerArchivePackTargetBytes: 8 * 1024 * 1024, maximumArchiveObjectBytes: 64 * 1024 * 1024, maximumArchiveTotalBytes: 15 * 1024 * 1024 * 1024, maximumWriteChunkBytes: 512 * 1024, maximumWriteChunkBase64Characters: 699_052, maximumObjectChunks: 4096, }); /** ContainerArchive v1 paths that may be replicated into restore staging. */ export const isolatedRestoreRepositoryPathProfile = Object.freeze({ version: 1 as const, rootObject: 'config.json' as const, topLevelDirectories: Object.freeze([ 'packs', 'snapshots', 'index', 'keys', ] as const), temporaryFilesAllowed: false as const, ancestorConflictsAllowed: false as const, }); /** * Exact normalized payload of a signed isolated restore grant. * Persist this projection for audit/recovery, never the compact bearer JWT. */ export interface IIsolatedRestoreGrantClaims { grantVersion: 1; iss: string; aud: string; sub: string; jti: string; authorizationId: string; tenantId: string; clusterId: string; targetNodeName: string; restoreId: string; sourceBackupId: string; sourceServiceId: string; scratchNamespaceId: string; stagingArchiveId: string; operation: TIsolatedRestoreGrantOperation; resourceMappingsSha256: string; archiveManifestSha256: string; iat: number; nbf: number; exp: number; } declare const isolatedRestoreVerifiedGrantBrand: unique symbol; /** * Runtime-branded result of invoking a cryptographic verifier for a compact * restore grant. The bearer itself remains in module-private storage so this * value cannot disclose it through serialization or reflection. It cannot be * reconstructed from claims alone and is accepted only by the same module * instance that performed verification. */ export interface IVerifiedIsolatedRestoreGrant { readonly claims: Readonly; readonly [isolatedRestoreVerifiedGrantBrand]: true; } export type TIsolatedRestoreGrantVerifier = ( compactArg: string, ) => unknown | Promise; /** The signed restoreGrant is the only authority accepted in control bodies. */ export interface IIsolatedRestoreControlForbiddenAuthority { authorizationId?: never; tenantId?: never; clusterId?: never; targetNodeName?: never; restoreId?: never; sourceBackupId?: never; sourceServiceId?: never; targetServiceId?: never; scratchNamespaceId?: never; stagingArchiveId?: never; operation?: never; resourceMappingsSha256?: never; archiveManifestSha256?: never; } export interface IIsolatedRestoreControlPrepareRequest extends IIsolatedRestoreControlForbiddenAuthority { restoreGrant: string; expectedRevision: number; resourceMappings: TIsolatedRestoreResourceMapping[]; archiveManifest: IBackupArchiveManifest; } /** * One bounded chunk of an immutable manifest object. `size` and `sha256` * describe the complete object; `chunkSha256` authenticates this request's * decoded bytes. Offset + decoded length === size means only that the chunk * reaches the object boundary; it does not prove object completion. */ export interface IIsolatedRestoreControlWriteRequest extends IIsolatedRestoreControlForbiddenAuthority { restoreGrant: string; expectedRevision: number; path: string; size: number; sha256: string; offset: number; chunkSha256: string; contentsBase64: string; } export interface IIsolatedRestoreControlExecuteRequest extends IIsolatedRestoreControlForbiddenAuthority { restoreGrant: string; expectedRevision: number; } export interface IIsolatedRestoreControlCleanupRequest extends IIsolatedRestoreControlForbiddenAuthority { restoreGrant: string; expectedRevision: number; } export interface IIsolatedRestoreControlStatusRequest extends IIsolatedRestoreControlForbiddenAuthority { restoreGrant: string; } export type TIsolatedRestoreControlRequest = | IIsolatedRestoreControlPrepareRequest | IIsolatedRestoreControlWriteRequest | IIsolatedRestoreControlExecuteRequest | IIsolatedRestoreControlCleanupRequest | IIsolatedRestoreControlStatusRequest; /** * Stable authority shared by every grant for one restore plan. Request-local * operation, jti, and validity times are intentionally excluded so separately * authorized write requests can contribute to the same object. */ export interface IIsolatedRestoreAuthorityProjection { version: 1; iss: string; aud: string; sub: string; authorizationId: string; tenantId: string; clusterId: string; targetNodeName: string; restoreId: string; sourceBackupId: string; sourceServiceId: string; scratchNamespaceId: string; stagingArchiveId: string; resourceMappingsSha256: string; archiveManifestSha256: string; } export type TIsolatedRestoreBoundPrepareRequest = Omit< IIsolatedRestoreControlPrepareRequest, 'restoreGrant' >; export type TIsolatedRestoreBoundWriteRequestData = Omit< IIsolatedRestoreControlWriteRequest, 'restoreGrant' >; export type TIsolatedRestoreBoundExecuteRequest = Omit< IIsolatedRestoreControlExecuteRequest, 'restoreGrant' >; export type TIsolatedRestoreBoundCleanupRequest = Omit< IIsolatedRestoreControlCleanupRequest, 'restoreGrant' >; export type TIsolatedRestoreBoundStatusRequest = Omit< IIsolatedRestoreControlStatusRequest, 'restoreGrant' >; export interface IIsolatedRestoreBoundPlan { authority: Readonly; authoritySha256: string; claims: Readonly; resourceMappings: readonly TIsolatedRestoreResourceMapping[]; archiveManifest: Readonly; } export interface IIsolatedRestoreBoundPrepareRequest { authority: Readonly; authoritySha256: string; claims: Readonly; request: Readonly; } export interface IIsolatedRestoreVerifiedChunkDescriptor { contentsBase64: string; decodedSize: number; sha256: string; } export interface IIsolatedRestoreBoundWriteRequest { authority: Readonly; authoritySha256: string; claims: Readonly; request: Readonly; descriptor: Readonly; chunk: Readonly; /** * The chunk reaches the declared size boundary. This is not object * completion: only bindIsolatedRestoreCompletedObject proves contiguous * coverage from offset 0 and the complete manifest-object SHA-256. */ endsAtObjectSize: boolean; } export interface IIsolatedRestoreBoundControlRequest< TRequest extends object, > { authority: Readonly; authoritySha256: string; claims: Readonly; request: Readonly; plan: IIsolatedRestoreBoundPlan; } declare const isolatedRestoreCompletedObjectBrand: unique symbol; export interface IIsolatedRestoreCompletedObjectBinding { readonly authority: Readonly; readonly authoritySha256: string; readonly descriptor: Readonly; readonly chunkCount: number; readonly contiguousBytes: number; readonly sha256: string; readonly [isolatedRestoreCompletedObjectBrand]: true; } /** * Bearer-free receipt suitable for trusted Corestore persistence. Its hashes * detect corruption and bind authority, descriptor, range, and chunk digest; * they do not replace grant authentication or authenticate an untrusted store. */ export interface IIsolatedRestoreDurableChunkReceipt { version: 1; authority: Readonly; authoritySha256: string; descriptor: Readonly; offset: number; decodedSize: number; chunkSha256: string; receiptSha256: string; } /** Durable, bearer-free proof regenerated from receipts and one file snapshot. */ export interface IIsolatedRestoreDurableCompletedObjectProof { version: 1; authority: Readonly; authoritySha256: string; descriptor: Readonly; chunkCount: number; contiguousBytes: number; sha256: string; receiptSetSha256: string; proofSha256: string; } export class IsolatedRestoreContractError extends Error { public constructor(reasonArg: string) { super(`isolated restore contract error: ${reasonArg}`); this.name = 'IsolatedRestoreContractError'; } } /** Cross-runtime rules for restore authority digests. */ export const isolatedRestoreCanonicalJsonRules = strictCanonicalJsonRules; const canonicalIdentifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,255}$/; const canonicalScopedIdPattern = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,191}$/; const canonicalVolumeNamePattern = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/; const canonicalResourceNamePattern = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/; const canonicalDatabaseNamePattern = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/; const canonicalBucketNamePattern = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/; const canonicalIpv4SegmentPattern = /^(?:0|[1-9][0-9]{0,2})$/; const providerReservedDatabaseNames = new Set(['admin', 'config', 'local']); const canonicalSha256Pattern = /^[a-f0-9]{64}$/; const standardBase64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; const base64UrlAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; const fail = (reasonArg: string): never => { throw new IsolatedRestoreContractError(reasonArg); }; const utf8ByteLength = (valueArg: string): number => { return new TextEncoder().encode(valueArg).byteLength; }; /** * Enforces the raw HTTP ingress limit before buffering or JSON parsing. * Apply it to Content-Length and to the running byte count after every chunk. */ export const assertIsolatedRestoreControlIngressByteLength = ( byteLengthArg: unknown, ): number => { if ( typeof byteLengthArg !== 'number' || !Number.isSafeInteger(byteLengthArg) || Object.is(byteLengthArg, -0) || byteLengthArg < 0 || byteLengthArg > isolatedRestoreContractLimits.maximumControlJsonBytes ) { return fail( `raw control ingress must contain between 0 and ${isolatedRestoreContractLimits.maximumControlJsonBytes} bytes`, ); } return byteLengthArg; }; const deepFreeze = deepFreezeValue; const verifiedIsolatedRestoreGrantCompacts = new WeakMap(); const verifiedIsolatedRestoreWriteBindings = new WeakSet(); const completedIsolatedRestoreObjects = new WeakSet(); const assertUnicodeScalarString = ( valueArg: string, fieldNameArg: string, ): void => { for (let index = 0; index < valueArg.length; index++) { const codeUnit = valueArg.charCodeAt(index); if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { const followingCodeUnit = valueArg.charCodeAt(index + 1); if ( !Number.isInteger(followingCodeUnit) || followingCodeUnit < 0xdc00 || followingCodeUnit > 0xdfff ) { fail(`${fieldNameArg} must not contain an unpaired UTF-16 surrogate`); } index++; continue; } if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { fail(`${fieldNameArg} must not contain an unpaired UTF-16 surrogate`); } } }; 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') ) { return fail(`${fieldNameArg} must contain enumerable 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') ) { return fail(`${fieldNameArg} must contain enumerable 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 assertExactKeys = ( recordArg: Record, expectedKeysArg: readonly string[], fieldNameArg: string, ): void => { const actualKeys = Object.keys(recordArg).sort(); const expectedKeys = [...expectedKeysArg].sort(); if ( actualKeys.length !== expectedKeys.length || actualKeys.some((keyArg, indexArg) => keyArg !== expectedKeys[indexArg]) ) { fail(`${fieldNameArg} does not match its exact versioned schema`); } }; const readString = ( valueArg: unknown, fieldNameArg: string, maximumBytesArg: number, ): string => { if (typeof valueArg !== 'string' || valueArg.length === 0) { return fail(`${fieldNameArg} must be a non-empty string`); } assertUnicodeScalarString(valueArg, fieldNameArg); if (utf8ByteLength(valueArg) > maximumBytesArg) { return fail(`${fieldNameArg} exceeds its UTF-8 byte limit`); } return valueArg; }; const readConfiguredString = ( valueArg: unknown, fieldNameArg: string, ): string => { const value = readString( valueArg, fieldNameArg, isolatedRestoreContractLimits.maximumConfiguredStringBytes, ); if ( value.trim() !== value || /\s/u.test(value) || /[\u0000-\u001f\u007f]/u.test(value) ) { return fail(`${fieldNameArg} must be a canonical string without whitespace`); } return value; }; const readIdentifier = (valueArg: unknown, fieldNameArg: string): string => { if (typeof valueArg !== 'string' || !canonicalIdentifierPattern.test(valueArg)) { return fail(`${fieldNameArg} must be a canonical identifier`); } return valueArg; }; const readScopedId = (valueArg: unknown, fieldNameArg: string): string => { if (typeof valueArg !== 'string' || !canonicalScopedIdPattern.test(valueArg)) { return fail(`${fieldNameArg} must be a canonical scoped identifier`); } return valueArg; }; const readPatternName = ( valueArg: unknown, fieldNameArg: string, patternArg: RegExp, descriptionArg: string, ): string => { if (typeof valueArg !== 'string' || !patternArg.test(valueArg)) { return fail(`${fieldNameArg} must be a canonical ${descriptionArg}`); } return valueArg; }; const readVolumeName = (valueArg: unknown, fieldNameArg: string): string => { return readPatternName( valueArg, fieldNameArg, canonicalVolumeNamePattern, 'volume name', ); }; const readResourceName = (valueArg: unknown, fieldNameArg: string): string => { return readPatternName( valueArg, fieldNameArg, canonicalResourceNamePattern, 'resource name', ); }; const readDatabaseName = (valueArg: unknown, fieldNameArg: string): string => { return readPatternName( valueArg, fieldNameArg, canonicalDatabaseNamePattern, 'database name', ); }; const readBucketName = (valueArg: unknown, fieldNameArg: string): string => { const value = readPatternName( valueArg, fieldNameArg, canonicalBucketNamePattern, 'bucket name', ); const ipv4Segments = value.split('.'); const isIpv4Address = ipv4Segments.length === 4 && ipv4Segments.every( (segmentArg) => canonicalIpv4SegmentPattern.test(segmentArg) && Number(segmentArg) <= 255, ); if (value.includes('..') || isIpv4Address) { return fail(`${fieldNameArg} must be a canonical bucket name`); } return value; }; const readSha256 = (valueArg: unknown, fieldNameArg: string): string => { if (typeof valueArg !== 'string' || !canonicalSha256Pattern.test(valueArg)) { return fail(`${fieldNameArg} must be a lowercase sha256 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 readMountPath = (valueArg: unknown, fieldNameArg: string): string => { const value = readString( valueArg, fieldNameArg, isolatedRestoreContractLimits.maximumPathBytes, ); if ( !value.startsWith('/') || value.includes('\\') || /[\u0000-\u001f\u007f]/u.test(value) ) { return fail(`${fieldNameArg} must be a canonical absolute POSIX path`); } if (value !== '/') { const segments = value.slice(1).split('/'); if ( segments.some( (segmentArg) => segmentArg.length === 0 || segmentArg === '.' || segmentArg === '..', ) ) { return fail(`${fieldNameArg} must be a canonical absolute POSIX path`); } } return value; }; const readRepositoryPath = (valueArg: unknown, fieldNameArg: string): string => { const value = readString( valueArg, fieldNameArg, isolatedRestoreContractLimits.maximumPathBytes, ); if ( value.startsWith('/') || value.includes('\\') || /[\u0000-\u001f\u007f]/u.test(value) ) { return fail(`${fieldNameArg} is outside the supported repository path profile`); } if (value === 'config.json') { return value; } let match = /^packs\/data\/([a-f0-9]{2})\/([a-f0-9]{32})\.(?:pack|idx)$/.exec(value); if (match) { if (match[1] !== match[2].slice(0, 2)) { return fail(`${fieldNameArg} pack shard does not match its object id`); } return value; } match = /^packs\/parity\/([a-f0-9]{2})\/([a-f0-9]{32})(?:(?:\.(0|[1-9]|[12][0-9]|3[01]))?\.par|\.parx)$/.exec(value); if (match) { if (match[1] !== match[2].slice(0, 2)) { return fail(`${fieldNameArg} parity shard does not match its object id`); } if (match[3] !== undefined && Number(match[3]) > 31) { return fail(`${fieldNameArg} parity index is outside the supported range`); } return value; } if (/^snapshots\/[A-Za-z0-9][A-Za-z0-9_.:-]{0,191}\.json$/.test(value)) { return value; } if (/^index\/[a-f0-9]{32}\.json$/.test(value)) { return value; } if (/^keys\/[A-Za-z0-9][A-Za-z0-9_.:-]{0,191}\.key$/.test(value)) { return value; } return fail(`${fieldNameArg} is outside the supported repository path profile`); }; const assertCanonicalBase64 = ( valueArg: unknown, fieldNameArg: string, ): string => { if (typeof valueArg !== 'string') { return fail(`${fieldNameArg} must be canonical padded base64`); } if ( valueArg.length > isolatedRestoreContractLimits.maximumWriteChunkBase64Characters ) { return fail(`${fieldNameArg} exceeds the encoded chunk character limit`); } if ( valueArg.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( valueArg, ) ) { return fail(`${fieldNameArg} must be canonical padded base64`); } if (valueArg.endsWith('==')) { const sextet = standardBase64Alphabet.indexOf(valueArg[valueArg.length - 3]); if (sextet < 0 || (sextet & 0x0f) !== 0) { return fail(`${fieldNameArg} has non-zero base64 pad bits`); } } else if (valueArg.endsWith('=')) { const sextet = standardBase64Alphabet.indexOf(valueArg[valueArg.length - 2]); if (sextet < 0 || (sextet & 0x03) !== 0) { return fail(`${fieldNameArg} has non-zero base64 pad bits`); } } return valueArg; }; const decodeCanonicalBase64 = ( valueArg: unknown, fieldNameArg: string, ): Uint8Array => { const value = assertCanonicalBase64(valueArg, fieldNameArg); const paddingLength = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0; const result = new Uint8Array(value.length / 4 * 3 - paddingLength); let outputOffset = 0; for (let inputOffset = 0; inputOffset < value.length; inputOffset += 4) { const first = standardBase64Alphabet.indexOf(value[inputOffset]); const second = standardBase64Alphabet.indexOf(value[inputOffset + 1]); const third = value[inputOffset + 2] === '=' ? 0 : standardBase64Alphabet.indexOf(value[inputOffset + 2]); const fourth = value[inputOffset + 3] === '=' ? 0 : standardBase64Alphabet.indexOf(value[inputOffset + 3]); const combined = (first << 18) | (second << 12) | (third << 6) | fourth; if (outputOffset < result.length) { result[outputOffset++] = combined >>> 16 & 0xff; } if (outputOffset < result.length) { result[outputOffset++] = combined >>> 8 & 0xff; } if (outputOffset < result.length) { result[outputOffset++] = combined & 0xff; } } return result; }; const assertCanonicalBase64UrlSegment = ( valueArg: string, fieldNameArg: string, ): void => { if ( valueArg.length === 0 || valueArg.length % 4 === 1 || !/^[A-Za-z0-9_-]+$/.test(valueArg) ) { fail(`${fieldNameArg} must be canonical unpadded base64url`); } const remainder = valueArg.length % 4; const finalSextet = base64UrlAlphabet.indexOf(valueArg[valueArg.length - 1]); if ( (remainder === 2 && (finalSextet & 0x0f) !== 0) || (remainder === 3 && (finalSextet & 0x03) !== 0) ) { fail(`${fieldNameArg} has non-zero base64url pad bits`); } }; const readCompactRestoreGrant = (valueArg: unknown): string => { const value = readString( valueArg, 'restoreGrant', isolatedRestoreContractLimits.maximumRestoreGrantBytes, ); const segments = value.split('.'); if (segments.length !== 3) { return fail('restoreGrant must be a three-segment compact JWT'); } segments.forEach((segmentArg, indexArg) => { assertCanonicalBase64UrlSegment( segmentArg, `restoreGrant segment ${indexArg + 1}`, ); }); return value; }; const assertControlJsonLimit = (valueArg: T, fieldNameArg: string): T => { const json = JSON.stringify(valueArg); if ( typeof json !== 'string' || utf8ByteLength(json) > isolatedRestoreContractLimits.maximumControlJsonBytes ) { return fail( `${fieldNameArg} exceeds the ${isolatedRestoreContractLimits.maximumControlJsonBytes}-byte control JSON limit`, ); } return deepFreeze(valueArg); }; const normalizeMapping = ( mappingArg: unknown, indexArg: number, ): TIsolatedRestoreResourceMapping => { const fieldName = `resourceMappings[${indexArg}]`; const mapping = readRecord(mappingArg, fieldName); assertExactKeys(mapping, ['id', 'type', 'source', 'target'], fieldName); const id = readScopedId(mapping.id, `${fieldName}.id`); if (mapping.type === 'volume') { const source = readRecord(mapping.source, `${fieldName}.source`); const target = readRecord(mapping.target, `${fieldName}.target`); assertExactKeys( source, ['snapshotId', 'volumeName', 'mountPath'], `${fieldName}.source`, ); assertExactKeys(target, ['volumeName', 'mountPath'], `${fieldName}.target`); const sourceVolumeName = readVolumeName( source.volumeName, `${fieldName}.source.volumeName`, ); const targetVolumeName = readVolumeName( target.volumeName, `${fieldName}.target.volumeName`, ); if (sourceVolumeName === targetVolumeName) { return fail(`${fieldName}.target.volumeName must differ from its source`); } return { id, type: 'volume', source: { snapshotId: readScopedId( source.snapshotId, `${fieldName}.source.snapshotId`, ), volumeName: sourceVolumeName, mountPath: readMountPath( source.mountPath, `${fieldName}.source.mountPath`, ), }, target: { volumeName: targetVolumeName, mountPath: readMountPath( target.mountPath, `${fieldName}.target.mountPath`, ), }, }; } if (mapping.type === 'database') { const source = readRecord(mapping.source, `${fieldName}.source`); const target = readRecord(mapping.target, `${fieldName}.target`); assertExactKeys( source, ['snapshotId', 'resourceName', 'databaseName'], `${fieldName}.source`, ); assertExactKeys( target, ['resourceName', 'databaseName'], `${fieldName}.target`, ); const sourceResourceName = readResourceName( source.resourceName, `${fieldName}.source.resourceName`, ); const sourceDatabaseName = readDatabaseName( source.databaseName, `${fieldName}.source.databaseName`, ); const targetResourceName = readResourceName( target.resourceName, `${fieldName}.target.resourceName`, ); const targetDatabaseName = readDatabaseName( target.databaseName, `${fieldName}.target.databaseName`, ); if ( sourceResourceName === targetResourceName || sourceDatabaseName === targetDatabaseName ) { return fail(`${fieldName}.target database namespace must differ from its source`); } return { id, type: 'database', source: { snapshotId: readScopedId( source.snapshotId, `${fieldName}.source.snapshotId`, ), resourceName: sourceResourceName, databaseName: sourceDatabaseName, }, target: { resourceName: targetResourceName, databaseName: targetDatabaseName, }, }; } if (mapping.type === 'objectstorage') { const source = readRecord(mapping.source, `${fieldName}.source`); const target = readRecord(mapping.target, `${fieldName}.target`); assertExactKeys( source, ['snapshotId', 'resourceName', 'bucketName'], `${fieldName}.source`, ); assertExactKeys( target, ['resourceName', 'bucketName'], `${fieldName}.target`, ); const sourceResourceName = readResourceName( source.resourceName, `${fieldName}.source.resourceName`, ); const sourceBucketName = readBucketName( source.bucketName, `${fieldName}.source.bucketName`, ); const targetResourceName = readResourceName( target.resourceName, `${fieldName}.target.resourceName`, ); const targetBucketName = readBucketName( target.bucketName, `${fieldName}.target.bucketName`, ); if ( sourceResourceName === targetResourceName || sourceBucketName === targetBucketName ) { return fail( `${fieldName}.target objectstorage namespace must differ from its source`, ); } return { id, type: 'objectstorage', source: { snapshotId: readScopedId( source.snapshotId, `${fieldName}.source.snapshotId`, ), resourceName: sourceResourceName, bucketName: sourceBucketName, }, target: { resourceName: targetResourceName, bucketName: targetBucketName, }, }; } return fail(`${fieldName}.type is unsupported`); }; export const normalizeIsolatedRestoreResourceMappings = ( valueArg: unknown, ): TIsolatedRestoreResourceMapping[] => { const entries = readDenseArray( valueArg, 'resourceMappings', 1, isolatedRestoreContractLimits.maximumResourceMappings, ); const mappings = entries.map(normalizeMapping); const mappingIds = new Set(); const sourceSnapshotIds = new Set(); const targetAuthorities = new Set(); const targetVolumeMountPaths = new Set(); for (const mapping of mappings) { if (mappingIds.has(mapping.id)) { return fail(`resourceMappings contains duplicate id ${mapping.id}`); } mappingIds.add(mapping.id); if (sourceSnapshotIds.has(mapping.source.snapshotId)) { return fail( `resourceMappings contains duplicate source snapshot ${mapping.source.snapshotId}`, ); } sourceSnapshotIds.add(mapping.source.snapshotId); if (mapping.type === 'volume') { if (targetVolumeMountPaths.has(mapping.target.mountPath)) { return fail( `resourceMappings contains duplicate target volume mount path ${mapping.target.mountPath}`, ); } targetVolumeMountPaths.add(mapping.target.mountPath); } const targetAuthority = mapping.type === 'volume' ? `volume:${mapping.target.volumeName}` : mapping.type === 'database' ? `database:${mapping.target.resourceName}:${mapping.target.databaseName}` : `objectstorage:${mapping.target.resourceName}:${mapping.target.bucketName}`; if (targetAuthorities.has(targetAuthority)) { return fail( `resourceMappings contains duplicate target authority ${targetAuthority}`, ); } targetAuthorities.add(targetAuthority); } return deepFreeze(mappings); }; /** * Rejects names that the exact restore providers cannot provision, replace, * or delete. Keep this separate from structural normalization so historical * coordinator state remains readable for cleanup after a contract upgrade. */ export const assertIsolatedRestoreProviderRestorableResourceMappings = ( mappingsArg: readonly TIsolatedRestoreResourceMapping[], ): void => { for (const [index, mapping] of mappingsArg.entries()) { if (mapping.type !== 'database') { continue; } for (const [side, databaseName] of [ ['source', mapping.source.databaseName], ['target', mapping.target.databaseName], ] as const) { if (providerReservedDatabaseNames.has(databaseName)) { fail( `resourceMappings[${index}].${side}.databaseName must not target a MongoDB system database`, ); } } } }; export const normalizeIsolatedRestoreArchiveManifest = ( valueArg: unknown, ): IBackupArchiveManifest => { const manifest = readRecord(valueArg, 'archiveManifest'); assertExactKeys( manifest, ['version', 'backupId', 'createdAt', 'objects', 'totalSize'], 'archiveManifest', ); if (manifest.version !== 1) { return fail('archiveManifest.version must be 1'); } const objectEntries = readDenseArray( manifest.objects, 'archiveManifest.objects', 1, isolatedRestoreContractLimits.maximumArchiveObjects, ); const paths = new Set(); let calculatedTotalSize = 0; const objects = objectEntries.map((objectArg, indexArg) => { const fieldName = `archiveManifest.objects[${indexArg}]`; const object = readRecord(objectArg, fieldName); assertExactKeys(object, ['path', 'size', 'sha256'], fieldName); const path = readRepositoryPath(object.path, `${fieldName}.path`); if (paths.has(path)) { return fail(`archiveManifest.objects contains duplicate path ${path}`); } paths.add(path); const size = readSafeInteger( object.size, `${fieldName}.size`, 0, isolatedRestoreContractLimits.maximumArchiveObjectBytes, ); if ( !Number.isSafeInteger(calculatedTotalSize + size) || calculatedTotalSize + size > isolatedRestoreContractLimits.maximumArchiveTotalBytes ) { return fail( 'archiveManifest object sizes exceed the versioned total-size limit', ); } calculatedTotalSize += size; return { path, size, sha256: readSha256(object.sha256, `${fieldName}.sha256`), }; }); const parityStylesByGroup = new Map(); for (const object of objects) { const parityMatch = /^packs\/parity\/([a-f0-9]{2})\/([a-f0-9]{32})(?:\.([0-9]{1,2}))?\.par$/.exec( object.path, ); if (!parityMatch) { continue; } const group = `${parityMatch[1]}/${parityMatch[2]}`; const style = parityMatch[3] === undefined ? 'unnumbered' : 'numbered'; const previousStyle = parityStylesByGroup.get(group); if (previousStyle && previousStyle !== style) { return fail( `archiveManifest parity group ${group} mixes numbered and unnumbered shards`, ); } parityStylesByGroup.set(group, style); } for (const path of paths) { let slashIndex = path.indexOf('/'); while (slashIndex >= 0) { const ancestor = path.slice(0, slashIndex); if (paths.has(ancestor)) { return fail( `archiveManifest path ${path} conflicts with object ancestor ${ancestor}`, ); } slashIndex = path.indexOf('/', slashIndex + 1); } } const totalSize = readSafeInteger( manifest.totalSize, 'archiveManifest.totalSize', 0, isolatedRestoreContractLimits.maximumArchiveTotalBytes, ); if (totalSize !== calculatedTotalSize) { return fail('archiveManifest.totalSize must equal the sum of object sizes'); } return deepFreeze({ version: 1, backupId: readScopedId(manifest.backupId, 'archiveManifest.backupId'), createdAt: readSafeInteger( manifest.createdAt, 'archiveManifest.createdAt', 0, ), objects, totalSize, }); }; export const normalizeIsolatedRestoreGrantClaims = ( valueArg: unknown, ): Readonly => { const claims = readRecord(valueArg, 'restore grant claims'); assertExactKeys( claims, [ 'grantVersion', 'iss', 'aud', 'sub', 'jti', 'authorizationId', 'tenantId', 'clusterId', 'targetNodeName', 'restoreId', 'sourceBackupId', 'sourceServiceId', 'scratchNamespaceId', 'stagingArchiveId', 'operation', 'resourceMappingsSha256', 'archiveManifestSha256', 'iat', 'nbf', 'exp', ], 'restore grant claims', ); if (claims.grantVersion !== 1) { return fail('grantVersion must be 1'); } if ( typeof claims.operation !== 'string' || !isolatedRestoreGrantOperations.includes( claims.operation as TIsolatedRestoreGrantOperation, ) ) { return fail('operation is unsupported'); } const iat = readSafeInteger(claims.iat, 'iat', 1); const nbf = readSafeInteger(claims.nbf, 'nbf', 1); const exp = readSafeInteger(claims.exp, 'exp', 1); if (exp <= iat || exp <= nbf) { return fail('exp must be later than both iat and nbf'); } const normalizedClaims: IIsolatedRestoreGrantClaims = { grantVersion: 1, iss: readConfiguredString(claims.iss, 'iss'), aud: readConfiguredString(claims.aud, 'aud'), sub: readIdentifier(claims.sub, 'sub'), jti: readIdentifier(claims.jti, 'jti'), authorizationId: readIdentifier(claims.authorizationId, 'authorizationId'), tenantId: readIdentifier(claims.tenantId, 'tenantId'), clusterId: readIdentifier(claims.clusterId, 'clusterId'), targetNodeName: readIdentifier(claims.targetNodeName, 'targetNodeName'), restoreId: readScopedId(claims.restoreId, 'restoreId'), sourceBackupId: readScopedId(claims.sourceBackupId, 'sourceBackupId'), sourceServiceId: readScopedId(claims.sourceServiceId, 'sourceServiceId'), scratchNamespaceId: readScopedId( claims.scratchNamespaceId, 'scratchNamespaceId', ), stagingArchiveId: readScopedId( claims.stagingArchiveId, 'stagingArchiveId', ), operation: claims.operation as TIsolatedRestoreGrantOperation, resourceMappingsSha256: readSha256( claims.resourceMappingsSha256, 'resourceMappingsSha256', ), archiveManifestSha256: readSha256( claims.archiveManifestSha256, 'archiveManifestSha256', ), iat, nbf, exp, }; if (normalizedClaims.scratchNamespaceId === normalizedClaims.sourceServiceId) { return fail('scratchNamespaceId must differ from sourceServiceId'); } if (normalizedClaims.stagingArchiveId === normalizedClaims.sourceBackupId) { return fail('stagingArchiveId must differ from sourceBackupId'); } return deepFreeze(normalizedClaims); }; const normalizeControlMutationRequest = ( valueArg: unknown, fieldNameArg: string, ): { restoreGrant: string; expectedRevision: number } => { const request = readRecord(valueArg, fieldNameArg); assertExactKeys(request, ['restoreGrant', 'expectedRevision'], fieldNameArg); return assertControlJsonLimit( { restoreGrant: readCompactRestoreGrant(request.restoreGrant), expectedRevision: readSafeInteger( request.expectedRevision, `${fieldNameArg}.expectedRevision`, 0, ), }, fieldNameArg, ); }; export const normalizeIsolatedRestoreControlPrepareRequest = ( valueArg: unknown, ): IIsolatedRestoreControlPrepareRequest => { const request = readRecord(valueArg, 'prepare request'); assertExactKeys( request, ['restoreGrant', 'expectedRevision', 'resourceMappings', 'archiveManifest'], 'prepare request', ); const resourceMappings = normalizeIsolatedRestoreResourceMappings( request.resourceMappings, ); assertIsolatedRestoreProviderRestorableResourceMappings(resourceMappings); return assertControlJsonLimit( { restoreGrant: readCompactRestoreGrant(request.restoreGrant), expectedRevision: readSafeInteger( request.expectedRevision, 'prepare request.expectedRevision', 0, ), resourceMappings, archiveManifest: normalizeIsolatedRestoreArchiveManifest( request.archiveManifest, ), }, 'prepare request', ); }; export const normalizeIsolatedRestoreControlWriteRequest = ( valueArg: unknown, ): IIsolatedRestoreControlWriteRequest => { const request = readRecord(valueArg, 'write request'); assertExactKeys( request, [ 'restoreGrant', 'expectedRevision', 'path', 'size', 'sha256', 'offset', 'chunkSha256', 'contentsBase64', ], 'write request', ); const size = readSafeInteger( request.size, 'write request.size', 0, isolatedRestoreContractLimits.maximumArchiveObjectBytes, ); const offset = readSafeInteger( request.offset, 'write request.offset', 0, size, ); const contentsBase64 = assertCanonicalBase64( request.contentsBase64, 'write request.contentsBase64', ); const contents = decodeCanonicalBase64( contentsBase64, 'write request.contentsBase64', ); if ( contents.byteLength > isolatedRestoreContractLimits.maximumWriteChunkBytes ) { return fail('write request decoded chunk exceeds the versioned chunk limit'); } if (size === 0) { if (offset !== 0 || contents.byteLength !== 0) { return fail( 'write request for an empty object must contain one empty chunk at offset 0', ); } } else if ( contents.byteLength === 0 || offset >= size || offset + contents.byteLength > size ) { return fail( 'write request chunk does not fit within the immutable object descriptor', ); } return assertControlJsonLimit( { restoreGrant: readCompactRestoreGrant(request.restoreGrant), expectedRevision: readSafeInteger( request.expectedRevision, 'write request.expectedRevision', 0, ), path: readRepositoryPath(request.path, 'write request.path'), size, sha256: readSha256(request.sha256, 'write request.sha256'), offset, chunkSha256: readSha256( request.chunkSha256, 'write request.chunkSha256', ), contentsBase64, }, 'write request', ); }; export const normalizeIsolatedRestoreControlExecuteRequest = ( valueArg: unknown, ): IIsolatedRestoreControlExecuteRequest => { return normalizeControlMutationRequest(valueArg, 'execute request'); }; export const normalizeIsolatedRestoreControlCleanupRequest = ( valueArg: unknown, ): IIsolatedRestoreControlCleanupRequest => { return normalizeControlMutationRequest(valueArg, 'cleanup request'); }; export const normalizeIsolatedRestoreControlStatusRequest = ( valueArg: unknown, ): IIsolatedRestoreControlStatusRequest => { const request = readRecord(valueArg, 'status request'); assertExactKeys(request, ['restoreGrant'], 'status request'); return assertControlJsonLimit( { restoreGrant: readCompactRestoreGrant(request.restoreGrant) }, 'status request', ); }; const canonicalizeJson = (valueArg: unknown): string => { return canonicalizeStrictJson(valueArg, fail); }; export const canonicalizeIsolatedRestoreResourceMappings = ( valueArg: readonly TIsolatedRestoreResourceMapping[], ): string => { return canonicalizeJson(normalizeIsolatedRestoreResourceMappings(valueArg)); }; export const canonicalizeIsolatedRestoreArchiveManifest = ( valueArg: IBackupArchiveManifest, ): string => { return canonicalizeJson(normalizeIsolatedRestoreArchiveManifest(valueArg)); }; export const canonicalizeIsolatedRestoreGrantClaims = ( valueArg: IIsolatedRestoreGrantClaims, ): string => { return canonicalizeJson(normalizeIsolatedRestoreGrantClaims(valueArg)); }; export const normalizeIsolatedRestoreAuthorityProjection = ( valueArg: unknown, ): Readonly => { const authority = readRecord(valueArg, 'restore authority projection'); assertExactKeys( authority, [ 'version', 'iss', 'aud', 'sub', 'authorizationId', 'tenantId', 'clusterId', 'targetNodeName', 'restoreId', 'sourceBackupId', 'sourceServiceId', 'scratchNamespaceId', 'stagingArchiveId', 'resourceMappingsSha256', 'archiveManifestSha256', ], 'restore authority projection', ); if (authority.version !== 1) { return fail('restore authority projection.version must be 1'); } const normalized: IIsolatedRestoreAuthorityProjection = { version: 1, iss: readConfiguredString(authority.iss, 'restore authority projection.iss'), aud: readConfiguredString(authority.aud, 'restore authority projection.aud'), sub: readIdentifier(authority.sub, 'restore authority projection.sub'), authorizationId: readIdentifier( authority.authorizationId, 'restore authority projection.authorizationId', ), tenantId: readIdentifier( authority.tenantId, 'restore authority projection.tenantId', ), clusterId: readIdentifier( authority.clusterId, 'restore authority projection.clusterId', ), targetNodeName: readIdentifier( authority.targetNodeName, 'restore authority projection.targetNodeName', ), restoreId: readScopedId( authority.restoreId, 'restore authority projection.restoreId', ), sourceBackupId: readScopedId( authority.sourceBackupId, 'restore authority projection.sourceBackupId', ), sourceServiceId: readScopedId( authority.sourceServiceId, 'restore authority projection.sourceServiceId', ), scratchNamespaceId: readScopedId( authority.scratchNamespaceId, 'restore authority projection.scratchNamespaceId', ), stagingArchiveId: readScopedId( authority.stagingArchiveId, 'restore authority projection.stagingArchiveId', ), resourceMappingsSha256: readSha256( authority.resourceMappingsSha256, 'restore authority projection.resourceMappingsSha256', ), archiveManifestSha256: readSha256( authority.archiveManifestSha256, 'restore authority projection.archiveManifestSha256', ), }; if (normalized.scratchNamespaceId === normalized.sourceServiceId) { return fail( 'restore authority projection scratchNamespaceId must differ from sourceServiceId', ); } if (normalized.stagingArchiveId === normalized.sourceBackupId) { return fail( 'restore authority projection stagingArchiveId must differ from sourceBackupId', ); } return deepFreeze(normalized); }; export const createIsolatedRestoreAuthorityProjection = ( claimsArg: IIsolatedRestoreGrantClaims, ): Readonly => { const claims = normalizeIsolatedRestoreGrantClaims(claimsArg); return normalizeIsolatedRestoreAuthorityProjection({ version: 1, iss: claims.iss, aud: claims.aud, sub: claims.sub, authorizationId: claims.authorizationId, tenantId: claims.tenantId, clusterId: claims.clusterId, targetNodeName: claims.targetNodeName, restoreId: claims.restoreId, sourceBackupId: claims.sourceBackupId, sourceServiceId: claims.sourceServiceId, scratchNamespaceId: claims.scratchNamespaceId, stagingArchiveId: claims.stagingArchiveId, resourceMappingsSha256: claims.resourceMappingsSha256, archiveManifestSha256: claims.archiveManifestSha256, }); }; export const canonicalizeIsolatedRestoreAuthorityProjection = ( valueArg: IIsolatedRestoreAuthorityProjection, ): string => { return canonicalizeJson(normalizeIsolatedRestoreAuthorityProjection(valueArg)); }; const createSha256 = async (contentsArg: Uint8Array): Promise => { return createSha256Hex(contentsArg, fail); }; const createCanonicalJsonSha256 = async ( canonicalJsonArg: string, ): Promise => { return createCanonicalJsonSha256Hex(canonicalJsonArg, fail); }; export const createIsolatedRestoreBytesSha256 = async ( contentsArg: Uint8Array, ): Promise => { if (!(contentsArg instanceof Uint8Array)) { return fail('contents must be a Uint8Array'); } return createSha256(contentsArg); }; export const createIsolatedRestoreResourceMappingsSha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256( canonicalizeIsolatedRestoreResourceMappings( valueArg as readonly TIsolatedRestoreResourceMapping[], ), ); }; export const createIsolatedRestoreArchiveManifestSha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256( canonicalizeIsolatedRestoreArchiveManifest( valueArg as IBackupArchiveManifest, ), ); }; export const createIsolatedRestoreGrantClaimsSha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256( canonicalizeIsolatedRestoreGrantClaims( valueArg as IIsolatedRestoreGrantClaims, ), ); }; export const createIsolatedRestoreAuthoritySha256 = async ( valueArg: unknown, ): Promise => { return createCanonicalJsonSha256( canonicalizeIsolatedRestoreAuthorityProjection( valueArg as IIsolatedRestoreAuthorityProjection, ), ); }; const requireVerifiedIsolatedRestoreGrant = ( grantArg: unknown, ): IVerifiedIsolatedRestoreGrant => { if ( !grantArg || typeof grantArg !== 'object' || !verifiedIsolatedRestoreGrantCompacts.has(grantArg) ) { return fail( 'restore grant must be a runtime-branded result of verifyIsolatedRestoreGrant', ); } return grantArg as IVerifiedIsolatedRestoreGrant; }; /** * Invokes the caller's cryptographic verifier for exactly `compact`, retains * that bearer in module-private storage, and brands the immutable claims * projection. Binding helpers reject claims supplied without this operation * and reject any control body carrying another compact JWT. */ export const verifyIsolatedRestoreGrant = async ( compactArg: unknown, verifierArg: TIsolatedRestoreGrantVerifier, ): Promise => { const compact = readCompactRestoreGrant(compactArg); if (typeof verifierArg !== 'function') { return fail('restore grant verifier must be a function'); } const claims = normalizeIsolatedRestoreGrantClaims( await verifierArg(compact), ); const grant = deepFreeze({ claims }) as unknown as IVerifiedIsolatedRestoreGrant; verifiedIsolatedRestoreGrantCompacts.set(grant, compact); return grant; }; export const assertIsolatedRestoreGrantOperation = ( grantArg: IVerifiedIsolatedRestoreGrant, expectedOperationArg: TIsolatedRestoreGrantOperation, ): Readonly => { if (!isolatedRestoreGrantOperations.includes(expectedOperationArg)) { return fail('expected operation is unsupported'); } const grant = requireVerifiedIsolatedRestoreGrant(grantArg); if (grant.claims.operation !== expectedOperationArg) { return fail( `restore grant operation ${grant.claims.operation} does not authorize ${expectedOperationArg}`, ); } return grant.claims; }; const assertControlRequestUsesVerifiedGrant = ( grantArg: IVerifiedIsolatedRestoreGrant, compactArg: string, ): void => { const grant = requireVerifiedIsolatedRestoreGrant(grantArg); if (compactArg !== verifiedIsolatedRestoreGrantCompacts.get(grant)) { fail('control request restoreGrant does not match the verified compact JWT'); } }; const assertPlanRelationships = ( claimsArg: Readonly, mappingsArg: readonly TIsolatedRestoreResourceMapping[], manifestArg: Readonly, ): void => { if (manifestArg.backupId !== claimsArg.sourceBackupId) { fail('archiveManifest.backupId does not match grant sourceBackupId'); } const paths = new Set( manifestArg.objects.map((objectArg) => objectArg.path), ); if (!paths.has('config.json')) { fail('archiveManifest must contain config.json'); } for (const mapping of mappingsArg) { const requiredSnapshotPath = `snapshots/${mapping.source.snapshotId}.json`; if (!paths.has(requiredSnapshotPath)) { fail( `archiveManifest does not contain required object ${requiredSnapshotPath}`, ); } } }; export const bindIsolatedRestoreGrantToPlan = async ( grantArg: IVerifiedIsolatedRestoreGrant, expectedOperationArg: TIsolatedRestoreGrantOperation, resourceMappingsArg: unknown, archiveManifestArg: unknown, ): Promise => { const grant = requireVerifiedIsolatedRestoreGrant(grantArg); const claims = assertIsolatedRestoreGrantOperation( grant, expectedOperationArg, ); const resourceMappings = normalizeIsolatedRestoreResourceMappings( resourceMappingsArg, ); if (expectedOperationArg === 'prepare') { assertIsolatedRestoreProviderRestorableResourceMappings(resourceMappings); } const archiveManifest = normalizeIsolatedRestoreArchiveManifest( archiveManifestArg, ); assertPlanRelationships(claims, resourceMappings, archiveManifest); const [resourceMappingsSha256, archiveManifestSha256] = await Promise.all([ createIsolatedRestoreResourceMappingsSha256(resourceMappings), createIsolatedRestoreArchiveManifestSha256(archiveManifest), ]); if (resourceMappingsSha256 !== claims.resourceMappingsSha256) { return fail('resourceMappings digest does not match the restore grant'); } if (archiveManifestSha256 !== claims.archiveManifestSha256) { return fail('archiveManifest digest does not match the restore grant'); } const authority = createIsolatedRestoreAuthorityProjection(claims); const authoritySha256 = await createIsolatedRestoreAuthoritySha256(authority); return deepFreeze({ authority, authoritySha256, claims, resourceMappings, archiveManifest, }); }; export const bindIsolatedRestorePrepareRequestToGrant = async ( grantArg: IVerifiedIsolatedRestoreGrant, requestArg: unknown, ): Promise => { const grant = requireVerifiedIsolatedRestoreGrant(grantArg); const normalizedRequest = normalizeIsolatedRestoreControlPrepareRequest( requestArg, ); assertControlRequestUsesVerifiedGrant(grant, normalizedRequest.restoreGrant); const plan = await bindIsolatedRestoreGrantToPlan( grant, 'prepare', normalizedRequest.resourceMappings, normalizedRequest.archiveManifest, ); const request: TIsolatedRestoreBoundPrepareRequest = { expectedRevision: normalizedRequest.expectedRevision, resourceMappings: plan.resourceMappings as TIsolatedRestoreResourceMapping[], archiveManifest: plan.archiveManifest as IBackupArchiveManifest, }; return deepFreeze({ authority: plan.authority, authoritySha256: plan.authoritySha256, claims: plan.claims, request, }); }; const bindSanitizedControlRequestToGrant = async < TRequest extends object, >( grantArg: IVerifiedIsolatedRestoreGrant, expectedOperationArg: TIsolatedRestoreGrantOperation, compactArg: string, requestArg: TRequest, resourceMappingsArg: unknown, archiveManifestArg: unknown, ): Promise> => { const grant = requireVerifiedIsolatedRestoreGrant(grantArg); assertControlRequestUsesVerifiedGrant(grant, compactArg); const plan = await bindIsolatedRestoreGrantToPlan( grant, expectedOperationArg, resourceMappingsArg, archiveManifestArg, ); return deepFreeze({ authority: plan.authority, authoritySha256: plan.authoritySha256, claims: plan.claims, request: requestArg, plan, }); }; export const bindIsolatedRestoreExecuteRequestToGrant = async ( grantArg: IVerifiedIsolatedRestoreGrant, requestArg: unknown, resourceMappingsArg: unknown, archiveManifestArg: unknown, ): Promise< IIsolatedRestoreBoundControlRequest > => { const normalizedRequest = normalizeIsolatedRestoreControlExecuteRequest(requestArg); const request: TIsolatedRestoreBoundExecuteRequest = { expectedRevision: normalizedRequest.expectedRevision, }; return bindSanitizedControlRequestToGrant( grantArg, 'execute', normalizedRequest.restoreGrant, request, resourceMappingsArg, archiveManifestArg, ); }; export const bindIsolatedRestoreCleanupRequestToGrant = async ( grantArg: IVerifiedIsolatedRestoreGrant, requestArg: unknown, resourceMappingsArg: unknown, archiveManifestArg: unknown, ): Promise< IIsolatedRestoreBoundControlRequest > => { const normalizedRequest = normalizeIsolatedRestoreControlCleanupRequest(requestArg); const request: TIsolatedRestoreBoundCleanupRequest = { expectedRevision: normalizedRequest.expectedRevision, }; return bindSanitizedControlRequestToGrant( grantArg, 'cleanup', normalizedRequest.restoreGrant, request, resourceMappingsArg, archiveManifestArg, ); }; export const bindIsolatedRestoreStatusRequestToGrant = async ( grantArg: IVerifiedIsolatedRestoreGrant, requestArg: unknown, resourceMappingsArg: unknown, archiveManifestArg: unknown, ): Promise< IIsolatedRestoreBoundControlRequest > => { const normalizedRequest = normalizeIsolatedRestoreControlStatusRequest(requestArg); const request: TIsolatedRestoreBoundStatusRequest = {}; return bindSanitizedControlRequestToGrant( grantArg, 'status', normalizedRequest.restoreGrant, request, resourceMappingsArg, archiveManifestArg, ); }; export const bindIsolatedRestoreWriteRequestToGrant = async ( grantArg: IVerifiedIsolatedRestoreGrant, requestArg: unknown, resourceMappingsArg: unknown, archiveManifestArg: unknown, ): Promise => { const grant = requireVerifiedIsolatedRestoreGrant(grantArg); const request = normalizeIsolatedRestoreControlWriteRequest(requestArg); assertControlRequestUsesVerifiedGrant(grant, request.restoreGrant); const plan = await bindIsolatedRestoreGrantToPlan( grant, 'write-object', resourceMappingsArg, archiveManifestArg, ); const descriptor = plan.archiveManifest.objects.find( (objectArg) => objectArg.path === request.path, ); if (!descriptor) { return fail( `write request path ${request.path} is absent from the immutable manifest`, ); } if (descriptor.size !== request.size || descriptor.sha256 !== request.sha256) { return fail( `write request descriptor for ${request.path} does not match the immutable manifest`, ); } const contents = decodeCanonicalBase64( request.contentsBase64, 'write request.contentsBase64', ); const actualChunkSha256 = await createIsolatedRestoreBytesSha256(contents); if (actualChunkSha256 !== request.chunkSha256) { return fail(`write request chunk checksum mismatch for ${request.path}`); } const sanitizedRequest: TIsolatedRestoreBoundWriteRequestData = { expectedRevision: request.expectedRevision, path: request.path, size: request.size, sha256: request.sha256, offset: request.offset, chunkSha256: request.chunkSha256, contentsBase64: request.contentsBase64, }; const bound = deepFreeze({ authority: plan.authority, authoritySha256: plan.authoritySha256, claims: plan.claims, request: sanitizedRequest, descriptor, chunk: { contentsBase64: request.contentsBase64, decodedSize: contents.byteLength, sha256: actualChunkSha256, }, endsAtObjectSize: request.offset + contents.byteLength === descriptor.size, }); verifiedIsolatedRestoreWriteBindings.add(bound); return bound; }; const requireBoundWriteRequest = ( bindingArg: unknown, ): IIsolatedRestoreBoundWriteRequest => { if ( !bindingArg || typeof bindingArg !== 'object' || !verifiedIsolatedRestoreWriteBindings.has(bindingArg) ) { return fail('write binding was not produced by bindIsolatedRestoreWriteRequestToGrant'); } return bindingArg as IIsolatedRestoreBoundWriteRequest; }; /** Returns a fresh caller-owned copy; verified bindings never expose mutable bytes. */ export const decodeIsolatedRestoreBoundWriteChunk = ( bindingArg: IIsolatedRestoreBoundWriteRequest, ): Uint8Array => { const binding = requireBoundWriteRequest(bindingArg); return decodeCanonicalBase64( binding.chunk.contentsBase64, 'verified write chunk.contentsBase64', ); }; type TIsolatedRestoreDurableChunkReceiptPayload = Omit< IIsolatedRestoreDurableChunkReceipt, 'receiptSha256' >; const createDurableChunkReceiptPayload = ( receiptArg: IIsolatedRestoreDurableChunkReceipt, ): TIsolatedRestoreDurableChunkReceiptPayload => { return { version: 1, authority: receiptArg.authority, authoritySha256: receiptArg.authoritySha256, descriptor: receiptArg.descriptor, offset: receiptArg.offset, decodedSize: receiptArg.decodedSize, chunkSha256: receiptArg.chunkSha256, }; }; const createDurableChunkReceiptSha256 = async ( payloadArg: TIsolatedRestoreDurableChunkReceiptPayload, ): Promise => { return createCanonicalJsonSha256(canonicalizeJson(payloadArg)); }; export const normalizeIsolatedRestoreDurableChunkReceipt = ( valueArg: unknown, ): Readonly => { const receipt = readRecord(valueArg, 'durable chunk receipt'); assertExactKeys( receipt, [ 'version', 'authority', 'authoritySha256', 'descriptor', 'offset', 'decodedSize', 'chunkSha256', 'receiptSha256', ], 'durable chunk receipt', ); if (receipt.version !== 1) { return fail('durable chunk receipt.version must be 1'); } const authority = normalizeIsolatedRestoreAuthorityProjection( receipt.authority, ); const descriptorRecord = readRecord( receipt.descriptor, 'durable chunk receipt.descriptor', ); assertExactKeys( descriptorRecord, ['path', 'size', 'sha256'], 'durable chunk receipt.descriptor', ); const descriptor = deepFreeze({ path: readRepositoryPath( descriptorRecord.path, 'durable chunk receipt.descriptor.path', ), size: readSafeInteger( descriptorRecord.size, 'durable chunk receipt.descriptor.size', 0, isolatedRestoreContractLimits.maximumArchiveObjectBytes, ), sha256: readSha256( descriptorRecord.sha256, 'durable chunk receipt.descriptor.sha256', ), }); const offset = readSafeInteger( receipt.offset, 'durable chunk receipt.offset', 0, descriptor.size, ); const decodedSize = readSafeInteger( receipt.decodedSize, 'durable chunk receipt.decodedSize', 0, isolatedRestoreContractLimits.maximumWriteChunkBytes, ); if (descriptor.size === 0) { if (offset !== 0 || decodedSize !== 0) { return fail( 'durable chunk receipt for an empty object must describe one empty range at offset 0', ); } } else if ( decodedSize === 0 || offset >= descriptor.size || offset + decodedSize > descriptor.size ) { return fail( 'durable chunk receipt range does not fit its immutable descriptor', ); } return deepFreeze({ version: 1, authority, authoritySha256: readSha256( receipt.authoritySha256, 'durable chunk receipt.authoritySha256', ), descriptor, offset, decodedSize, chunkSha256: readSha256( receipt.chunkSha256, 'durable chunk receipt.chunkSha256', ), receiptSha256: readSha256( receipt.receiptSha256, 'durable chunk receipt.receiptSha256', ), }); }; /** * Rehydrates a trusted-store receipt and verifies both consistency digests. * This does not replace verification of the grant that originally produced it. */ export const verifyIsolatedRestoreDurableChunkReceipt = async ( valueArg: unknown, ): Promise> => { const receipt = normalizeIsolatedRestoreDurableChunkReceipt(valueArg); const authoritySha256 = await createIsolatedRestoreAuthoritySha256( receipt.authority, ); if (authoritySha256 !== receipt.authoritySha256) { return fail('durable chunk receipt authority checksum mismatch'); } const receiptSha256 = await createDurableChunkReceiptSha256( createDurableChunkReceiptPayload(receipt), ); if (receiptSha256 !== receipt.receiptSha256) { return fail('durable chunk receipt checksum mismatch'); } return receipt; }; /** * Creates a bearer-free receipt only from a runtime-verified write binding. * Persist the result in authenticated/trusted Corestore state for recovery. */ export const createIsolatedRestoreDurableChunkReceipt = async ( bindingArg: IIsolatedRestoreBoundWriteRequest, ): Promise> => { const binding = requireBoundWriteRequest(bindingArg); const payload: TIsolatedRestoreDurableChunkReceiptPayload = { version: 1, authority: binding.authority, authoritySha256: binding.authoritySha256, descriptor: binding.descriptor, offset: binding.request.offset, decodedSize: binding.chunk.decodedSize, chunkSha256: binding.chunk.sha256, }; const receiptSha256 = await createDurableChunkReceiptSha256(payload); return deepFreeze({ ...payload, receiptSha256 }); }; /** * Rehydrates ordered trusted-store receipts and verifies them against one * defensive snapshot of the complete staged file. Per-range and full-file * SHA-256 checks bind the resulting proof to authority, object, and receipt set. */ export const verifyIsolatedRestoreDurableCompletedObject = async ( receiptsArg: readonly unknown[], contentsArg: Uint8Array, ): Promise> => { if (!(contentsArg instanceof Uint8Array)) { return fail('durable completed object contents must be a Uint8Array'); } if ( contentsArg.byteLength > isolatedRestoreContractLimits.maximumArchiveObjectBytes ) { return fail( `durable completed object snapshot exceeds the ${isolatedRestoreContractLimits.maximumArchiveObjectBytes}-byte object limit`, ); } const receiptEntries = readDenseArray( receiptsArg, 'durable completed object receipts', 1, isolatedRestoreContractLimits.maximumObjectChunks, ); const contentsSnapshot = new Uint8Array(contentsArg); const first = await verifyIsolatedRestoreDurableChunkReceipt( receiptEntries[0], ); if (first.descriptor.size === 0 && receiptEntries.length !== 1) { return fail( 'durable completed object for an empty descriptor must contain exactly one receipt', ); } const receipts: Readonly[] = [first]; for (let index = 1; index < receiptEntries.length; index++) { receipts.push( await verifyIsolatedRestoreDurableChunkReceipt(receiptEntries[index]), ); } const authority = first.authority; const authoritySha256 = first.authoritySha256; const authorityCanonicalJson = canonicalizeIsolatedRestoreAuthorityProjection(authority); const descriptor = first.descriptor; if (contentsSnapshot.byteLength !== descriptor.size) { return fail( `durable completed object snapshot contains ${contentsSnapshot.byteLength} of ${descriptor.size} bytes`, ); } let expectedOffset = 0; for (const receipt of receipts) { if ( receipt.authoritySha256 !== authoritySha256 || canonicalizeIsolatedRestoreAuthorityProjection(receipt.authority) !== authorityCanonicalJson ) { return fail( 'durable completed object receipts do not share one stable authority', ); } if ( receipt.descriptor.path !== descriptor.path || receipt.descriptor.size !== descriptor.size || receipt.descriptor.sha256 !== descriptor.sha256 ) { return fail( 'durable completed object receipts do not share one immutable descriptor', ); } if (receipt.offset !== expectedOffset) { return fail( `durable completed object receipts are not contiguous at offset ${expectedOffset}`, ); } const rangeEnd = receipt.offset + receipt.decodedSize; const rangeSha256 = await createIsolatedRestoreBytesSha256( contentsSnapshot.subarray(receipt.offset, rangeEnd), ); if (rangeSha256 !== receipt.chunkSha256) { return fail( `durable completed object chunk checksum mismatch at offset ${receipt.offset}`, ); } expectedOffset = rangeEnd; } if (expectedOffset !== descriptor.size) { return fail( `durable completed object receipts cover ${expectedOffset} of ${descriptor.size} bytes`, ); } const sha256 = await createIsolatedRestoreBytesSha256(contentsSnapshot); if (sha256 !== descriptor.sha256) { return fail( `durable completed object checksum mismatch for ${descriptor.path}`, ); } const receiptSetSha256 = await createCanonicalJsonSha256( canonicalizeJson(receipts), ); const proofPayload = { version: 1 as const, authority, authoritySha256, descriptor, chunkCount: receipts.length, contiguousBytes: expectedOffset, sha256, receiptSetSha256, }; const proofSha256 = await createCanonicalJsonSha256( canonicalizeJson(proofPayload), ); return deepFreeze({ ...proofPayload, proofSha256 }); }; /** * Process-local completion convenience for runtime-branded chunks. It does not * survive restart. Enterprise/Corestore workflows must persist durable chunk * receipts and call verifyIsolatedRestoreDurableCompletedObject after recovery. * Chunks must share one authority and descriptor, cover [0, size) contiguously, * and hash to the complete manifest descriptor. */ export const bindIsolatedRestoreCompletedObject = async ( bindingsArg: readonly IIsolatedRestoreBoundWriteRequest[], ): Promise => { const entries = readDenseArray( bindingsArg, 'completed object chunks', 1, isolatedRestoreContractLimits.maximumObjectChunks, ); const first = requireBoundWriteRequest(entries[0]); const authority = first.authority; const authoritySha256 = first.authoritySha256; const authorityCanonicalJson = canonicalizeIsolatedRestoreAuthorityProjection(authority); const descriptor = first.descriptor; if (descriptor.size === 0 && entries.length !== 1) { return fail( 'completed object for an empty descriptor must contain exactly one chunk', ); } const completeContents = new Uint8Array(descriptor.size); let expectedOffset = 0; for (const entry of entries) { const binding = requireBoundWriteRequest(entry); if ( binding.authoritySha256 !== authoritySha256 || canonicalizeIsolatedRestoreAuthorityProjection(binding.authority) !== authorityCanonicalJson ) { return fail('completed object chunks do not share one stable authority'); } if ( binding.descriptor.path !== descriptor.path || binding.descriptor.size !== descriptor.size || binding.descriptor.sha256 !== descriptor.sha256 ) { return fail('completed object chunks do not share one immutable descriptor'); } if (binding.request.offset !== expectedOffset) { return fail( `completed object chunks are not contiguous at offset ${expectedOffset}`, ); } const contents = decodeIsolatedRestoreBoundWriteChunk(binding); completeContents.set(contents, expectedOffset); expectedOffset += contents.byteLength; } if (expectedOffset !== descriptor.size) { return fail( `completed object chunks cover ${expectedOffset} of ${descriptor.size} bytes`, ); } const sha256 = await createIsolatedRestoreBytesSha256(completeContents); if (sha256 !== descriptor.sha256) { return fail(`completed object checksum mismatch for ${descriptor.path}`); } const completed = deepFreeze({ authority, authoritySha256, descriptor, chunkCount: entries.length, contiguousBytes: expectedOffset, sha256, }) as unknown as IIsolatedRestoreCompletedObjectBinding; completedIsolatedRestoreObjects.add(completed); return completed; }; export const assertIsolatedRestoreCompletedObject = ( completedArg: IIsolatedRestoreCompletedObjectBinding, ): IIsolatedRestoreCompletedObjectBinding => { if ( !completedArg || typeof completedArg !== 'object' || !completedIsolatedRestoreObjects.has(completedArg) ) { return fail( 'completed object must be produced by bindIsolatedRestoreCompletedObject', ); } return completedArg; };