57 lines
3.3 KiB
TypeScript
57 lines
3.3 KiB
TypeScript
export const SCRIPT_SANDBOX_RECOVERY_SCHEMA = 1 as const;
|
|
|
|
export interface ScriptSandboxRecoveryAuditEntry {
|
|
sequence: 1 | 2;
|
|
requestId: string;
|
|
previousEntrySha256: string | null;
|
|
entrySha256: string;
|
|
sourceSha256: string;
|
|
manifestSha256: string;
|
|
}
|
|
|
|
export interface ScriptSandboxRecoveryReceipt {
|
|
schemaVersion: typeof SCRIPT_SANDBOX_RECOVERY_SCHEMA;
|
|
operation: "SCRIPT_SANDBOX_RECOVERY";
|
|
previousGeneration: number;
|
|
nextGeneration: number;
|
|
mainRevisionBefore: number;
|
|
mainRevisionAfter: number;
|
|
sourceSha256: string;
|
|
manifestSha256: string;
|
|
audit: {
|
|
entries: 2;
|
|
first: ScriptSandboxRecoveryAuditEntry;
|
|
second: ScriptSandboxRecoveryAuditEntry;
|
|
};
|
|
recovered: true;
|
|
execution: "DISABLED";
|
|
}
|
|
|
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
const REQUEST_ID = /^[-A-Za-z0-9:_./]{1,256}$/;
|
|
|
|
function assertDigest(value: string, name: string): void {
|
|
if (!SHA256.test(value)) throw new Error(`SCRIPT_SANDBOX_RECOVERY_INVALID: ${name} must be a SHA-256 digest`);
|
|
}
|
|
|
|
function assertEntry(entry: ScriptSandboxRecoveryAuditEntry, expectedSequence: 1 | 2, sourceSha256: string, manifestSha256: string): void {
|
|
if (entry.sequence !== expectedSequence || !REQUEST_ID.test(entry.requestId)) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit sequence or request id is invalid");
|
|
if (expectedSequence === 1 ? entry.previousEntrySha256 !== null : !entry.previousEntrySha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit previous hash is invalid");
|
|
assertDigest(entry.entrySha256, `audit[${expectedSequence}].entrySha256`);
|
|
if (entry.previousEntrySha256 !== null) assertDigest(entry.previousEntrySha256, `audit[${expectedSequence}].previousEntrySha256`);
|
|
if (entry.sourceSha256 !== sourceSha256 || entry.manifestSha256 !== manifestSha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit source or manifest hash drifted");
|
|
}
|
|
|
|
export function createScriptSandboxRecoveryReceipt(input: Omit<ScriptSandboxRecoveryReceipt, "schemaVersion" | "operation" | "recovered" | "execution">): ScriptSandboxRecoveryReceipt {
|
|
if (!Number.isSafeInteger(input.previousGeneration) || !Number.isSafeInteger(input.nextGeneration) || input.nextGeneration !== input.previousGeneration + 1) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: generation must advance once");
|
|
if (!Number.isSafeInteger(input.mainRevisionBefore) || input.mainRevisionBefore < 0 || input.mainRevisionAfter !== input.mainRevisionBefore) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: Main revision changed during recovery");
|
|
assertDigest(input.sourceSha256, "sourceSha256");
|
|
assertDigest(input.manifestSha256, "manifestSha256");
|
|
if (input.audit.entries !== 2) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit entry count is invalid");
|
|
assertEntry(input.audit.first, 1, input.sourceSha256, input.manifestSha256);
|
|
assertEntry(input.audit.second, 2, input.sourceSha256, input.manifestSha256);
|
|
if (input.audit.second.previousEntrySha256 !== input.audit.first.entrySha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit hash chain is not continuous");
|
|
if (input.audit.first.requestId === input.audit.second.requestId) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit request id is replayed");
|
|
return { schemaVersion: SCRIPT_SANDBOX_RECOVERY_SCHEMA, operation: "SCRIPT_SANDBOX_RECOVERY", ...input, recovered: true, execution: "DISABLED" };
|
|
}
|