Skip to content

Reference source · 0.1.0-dev.11

recovery.ts

scripts/recovery.ts · Download original file

import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fixture, PATIENT, lab } from '../src/reference/fixtures.ts';
import { SCOPES } from '../src/authorization.ts';
import { encryptBackup, decryptBackup, verifyArchive } from '../src/archive.ts';
import { makeIdentity, signCredential, signStatus, TrustStore } from '../src/identity.ts';
import { ReferenceHost } from '../src/host.ts';
import { Client } from '../src/sdk.ts';

const directory = mkdtempSync(join(tmpdir(), 'oprp-recovery-'));
const sourcePath = join(directory, 'source.sqlite'); const f = fixture(sourcePath);
let sourceClosed = false, destination: ReferenceHost | undefined, offline: TrustStore | undefined;
try {
  const pending = await f.client.requestAccess({ patient: PATIENT, scopes: Object.values(SCOPES), requested_duration_seconds: 1000, refresh_requested: true });
  assert.equal(pending.status, 'pending'); if (pending.status !== 'pending') throw new Error('Expected approval');
  f.host.authorization.approve(f.app, pending.request_id, PATIENT, f.clock()); await f.client.approvalStatus(pending.request_id);
  const originalLab = await f.client.createLab(PATIENT, lab(), 'recoverable-lab');
  await f.client.updateDemographics(PATIENT, 'Synthetic Portable Example', 1, 'recoverable-demographics');
  const sealed = encryptBackup(await f.client.exportRecord(PATIENT));
  const backupPath = join(directory, 'record.oprp-backup.json'), recoveryKeyPath = join(directory, 'owner-recovery.key');
  writeFileSync(backupPath, JSON.stringify(sealed.backup), { mode: 0o600 }); writeFileSync(recoveryKeyPath, sealed.recoveryKey, { mode: 0o600 }); sealed.recoveryKey.fill(0);
  console.log('PASS: a separately authorized full export becomes an authenticated encrypted backup with an independent recovery key.');
  f.host.close(); sourceClosed = true;
  for (const suffix of ['', '-wal', '-shm']) rmSync(sourcePath + suffix, { force: true });
  f.advance(90000);
  offline = new TrustStore(f.trust.policies); // Independent pinned public roots; no status fetch or source private key.
  const recoveryKey = readFileSync(recoveryKeyPath); let archive;
  try { archive = decryptBackup(JSON.parse(readFileSync(backupPath, 'utf8')), recoveryKey); } finally { recoveryKey.fill(0); }
  const manifest = verifyArchive(archive, offline, f.clock(), PATIENT); assert.equal(manifest.resources.length, 2);
  console.log('PASS: offline verification succeeds after source shutdown, database removal and source credential expiry.');
  const identity = makeIdentity('urn:oprp:demo:recovery-host'), credential = signCredential(f.hostingAuthority, identity.public, 'host', f.clock());
  destination = new ReferenceHost(join(directory, 'destination.sqlite'), identity, credential, offline, f.policy);
  const imported = destination.importRecord(archive, PATIENT, f.clock()); assert.equal(imported.imported, true);
  assert.equal(destination.importRecord(archive, PATIENT, f.clock()).imported, false);
  for (const authority of [f.hostingAuthority, f.applicationAuthority]) offline.ingestStatus(signStatus(authority, 20, f.clock()), f.clock());
  const client = new Client({ identity: f.appIdentity, credential: signCredential(f.applicationAuthority, f.appIdentity.public, 'application', f.clock()), hostId: identity.public.subject, hostCredential: credential, trust: offline, transport: async envelope => destination!.handle(envelope, f.clock()), clock: f.clock });
  const request = await client.requestAccess({ patient: PATIENT, scopes: Object.values(SCOPES), requested_duration_seconds: 1000, refresh_requested: true });
  assert.equal(request.status, 'pending'); if (request.status !== 'pending') throw new Error('Expected new authorization');
  destination.authorization.approve(offline.validate(client.credential, f.clock()), request.request_id, PATIENT, f.clock()); await client.approvalStatus(request.request_id);
  const recoveredLab = await client.createLab(PATIENT, lab(), 'recoverable-lab');
  assert.equal(recoveredLab.resource.id, originalLab.resource.id); assert.equal((await client.listLabs(PATIENT)).resource.total, 1);
  assert.equal((await client.updateDemographics(PATIENT, 'Synthetic Portable Example', 1, 'recoverable-demographics')).version, 2);
  console.log('PASS: destination authorization is established separately; late retries recover original IDs and versions without duplicate mutations.');
  const nextArchive = verifyArchive(await client.exportRecord(PATIENT), offline, f.clock()); assert.equal(nextArchive.prior_archives.length, 1);
  console.log('PASS: re-export preserves the original signed archive as history evidence without copying source grants or service secrets.');
  console.log('Synthetic recovery complete. Temporary files are removed; no private keys, tokens or clinical contents were logged.');
} finally { if (!sourceClosed) f.host.close(); destination?.close(); offline?.close(); f.trust.close(); rmSync(directory, { recursive: true, force: true }); }