Reference source · 0.1.0-dev.11
migration.ts
scripts/migration.ts · Download original file
import assert from 'node:assert/strict';
import { mkdtempSync, 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 { makeIdentity, signCredential } from '../src/identity.ts';
import { ReferenceHost } from '../src/host.ts';
import { ReferenceDirectory } from '../src/reference/directory.ts';
import { HostPeer } from '../src/host-peer.ts';
import { DirectoryTrust, discover, webfinger } from '../src/discovery.ts';
import { httpsTransport } from '../src/sdk.ts';
import { DurableQueue } from '../src/queue.ts';
import { startDemoServer } from '../src/reference/https-server.ts';
import { verifyArchive } from '../src/archive.ts';
const directoryPath = mkdtempSync(join(tmpdir(), 'oprp-migration-')), f = fixture(join(directoryPath, 'source.sqlite'));
const identity = makeIdentity('urn:oprp:demo:destination'), credential = signCredential(f.hostingAuthority, identity.public, 'host', f.clock());
const destination = new ReferenceHost(join(directoryPath, 'destination.sqlite'), identity, credential, f.trust, f.policy);
const bindings = new ReferenceDirectory(join(directoryPath, 'directory.sqlite'), f.hostingAuthority, ['patients.example'], f.trust);
const directory = new DirectoryTrust({ [f.hostingAuthority.issuer]: { publicKey: f.hostingAuthority.publicKey, domains: ['patients.example'] } }, f.trust);
const address = 'synthetic@patients.example';
let sourceServer: Awaited<ReturnType<typeof startDemoServer>> | undefined, destinationServer: Awaited<ReturnType<typeof startDemoServer>> | undefined, queue: DurableQueue | undefined;
try {
sourceServer = await startDemoServer(f.host, f.clock, resource => bindings.document(resource, f.clock()));
destinationServer = await startDemoServer(destination, f.clock);
bindings.enroll(address, PATIENT, f.hostCredential, sourceServer.endpoint, f.clock());
const discoverOptions = { ca: sourceServer.ca, origin: new URL(sourceServer.endpoint).origin };
const initial = await discover(address, directory, f.clock(), discoverOptions);
await f.client.followDiscovery(webfinger(address, initial.delegation), address, directory, httpsTransport(initial.endpoint, sourceServer.ca), PATIENT);
const request = await f.client.requestAccess({ patient: PATIENT, scopes: Object.values(SCOPES), requested_duration_seconds: 1800, refresh_requested: true });
if (request.status !== 'pending') throw new Error('Expected synthetic owner decision');
f.host.authorization.approve(f.app, request.request_id, PATIENT, f.clock()); await f.client.approvalStatus(request.request_id);
const oldSnapshot = await f.client.syncSnapshot(PATIENT);
const plan = f.host.migration.begin(PATIENT, credential, destinationServer.endpoint, webfinger(address, initial.delegation), directory, PATIENT, f.clock(), 'demo-live-transfer');
destination.migration.admit(plan, PATIENT, f.clock()); // Simulated destination owner admission, separate from source consent.
const peer = new HostPeer(f.hostIdentity, f.hostCredential, credential, f.trust, httpsTransport(destinationServer.endpoint, destinationServer.ca), f.clock);
await peer.exchange({ operation: 'migration_stage', plan, archive: f.host.exportRecord(PATIENT, f.clock()) });
console.log('PASS: an owner-approved plan stages a verified baseline over post-quantum protected HTTPS; destination application access remains disabled.');
queue = new DurableQueue(join(directoryPath, 'queue.sqlite'), f.client);
queue.createLab(PATIENT, lab(), 'demo-uncertain-lab'); queue.updateDemographics(PATIENT, 'Synthetic Moved Example', 1, 'demo-following-update');
const sourceNetwork = f.client.transport;
f.client.transport = async envelope => { await sourceNetwork(envelope); throw new Error('synthetic_lost_write_response'); };
await queue.flush(); f.client.transport = sourceNetwork;
assert.equal(queue.entries()[0].state, 'pending'); assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM labs').get() as any).n, 1);
const originalLab = (f.host.db.prepare('SELECT id FROM labs').get() as any).id;
f.advance(1);
const final = f.host.migration.freeze('demo-live-transfer', f.clock());
await assert.rejects(f.client.createLab(PATIENT, lab(), 'blocked-after-fence'), /migration_fenced/);
const ready = (await peer.exchange({ operation: 'migration_stage_final', plan, archive: final })).ready;
const release = f.host.migration.release('demo-live-transfer', ready, f.clock());
await assert.rejects(f.client.readDemographics(PATIENT), /record_moved/);
assert.throws(() => f.host.migration.abort('demo-live-transfer', PATIENT, f.clock()), /migration_irreversible/);
console.log('PASS: final catch-up includes the uncertain write; durable source fencing precedes irreversible signed release.');
const destinationNetwork = peer.transport;
peer.transport = async envelope => { await destinationNetwork(envelope); throw new Error('synthetic_lost_activation_response'); };
await assert.rejects(peer.activate('demo-live-transfer', release), /synthetic_lost_activation_response/);
peer.transport = destinationNetwork; const activation = await peer.activate('demo-live-transfer', release);
const handoff = f.host.migration.complete('demo-live-transfer', activation, f.clock());
bindings.publish(handoff, f.clock()); bindings.publish(handoff, f.clock());
console.log('PASS: a lost activation response recovers the saved receipt; discovery publication requires the complete verified handoff.');
const located = await discover(address, directory, f.clock(), { ...discoverOptions, minimumGeneration: 2 });
assert.equal(located.host, identity.public.subject); assert.equal(located.generation, 2);
await queue.followMigration(PATIENT, webfinger(address, located.delegation), address, directory, httpsTransport(located.endpoint, destinationServer.ca));
assert.equal(f.client.authorization, undefined); await queue.flush(); assert.equal(queue.entries()[0].error, 'no_authorization');
const newRequest = await f.client.requestAccess({ patient: PATIENT, scopes: Object.values(SCOPES), requested_duration_seconds: 1800, refresh_requested: true });
if (newRequest.status !== 'pending') throw new Error('Expected separate destination policy');
destination.authorization.approve(f.app, newRequest.request_id, PATIENT, f.clock()); await f.client.approvalStatus(newRequest.request_id);
await assert.rejects(f.client.changes(PATIENT, oldSnapshot.cursor), /invalid_opaque_value/);
const current = await f.client.syncSnapshot(PATIENT); await queue.flush();
assert.deepEqual(queue.entries().map(entry => entry.state), ['committed','committed']); assert.equal(queue.entries()[0].result!.resource.id, originalLab);
assert.equal((await f.client.listLabs(PATIENT)).resource.total, 1); assert.equal((await f.client.readDemographics(PATIENT)).version, 2);
assert.equal((await f.client.changes(PATIENT, current.cursor)).events.length, 1);
console.log('PASS: verified generation-2 routing clears source tokens; new authorization and snapshot precede duplicate-safe ordered queue recovery.');
const exported = verifyArchive(await f.client.exportRecord(PATIENT), f.trust, f.clock()); assert.equal(exported.handoffs!.length, 1);
assert.equal(f.host.migration.custody(PATIENT).state, 'moved'); assert.equal(destination.migration.custody(PATIENT).state, 'active');
console.log('PASS: destination re-export retains signed custody/history evidence; the retained source copy cannot accept further writes.');
console.log('Synthetic live migration complete. Temporary databases/certificates are removed; no keys, tokens or clinical contents were logged.');
} finally {
queue?.close(); if (sourceServer) await sourceServer.close(); if (destinationServer) await destinationServer.close();
bindings.close(); destination.close(); f.host.close(); f.trust.close(); rmSync(directoryPath, { recursive: true, force: true });
}