fix(mcp): обновить снимок приёмки маршрутизации и базы данных
This commit is contained in:
1 parent
bf3d50e1ff
commit
6a286a1f66
79 files changed
+10015
-109
No files matched your search
@@ -10,6 +10,7 @@ import ts from 'typescript'
|
||||
export const OPERATOR_SOURCES=Object.freeze([
|
||||
'database-writer-bootstrap-operator.mjs','database-writer-profile-operator.mts',
|
||||
'writer-profile/operator-files.mjs','writer-profile/operator-contract.mjs','writer-profile/publisher.mjs','writer-profile/context.mjs',
|
||||
'writer-profile/protected-source-registry.mts','writer-profile/maintenance-resolution.mts',
|
||||
'writer-profile/catalogue.mts','writer-profile/catalogue-value-bridge.ts','writer-profile/catalogue-projection.ts','writer-profile/catalogue-leaves.ts',
|
||||
'writer-profile/application-writer-profile-catalogue.ts','writer-profile/application-writer-profile-catalogue-values.ts',
|
||||
'writer-profile/application-writer-profile-catalogue-parent-policy.ts','writer-profile/application-writer-profile-canonical.ts',
|
||||
|
||||
@@ -19,7 +19,7 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
|
||||
const packageJson=JSON.parse(await readFile(path.join(backend,'package.json'),'utf8'))
|
||||
await mkdir(projectBackend,{recursive:true});await writeFile(path.join(projectBackend,'package.json'),JSON.stringify({type:'module',dependencies:packageJson.dependencies}))
|
||||
const first=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output}),second=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output})
|
||||
assert.deepEqual(first,second);assert.equal(first.modules.length,OPERATOR_SOURCES.length);assert.equal(first.modules.length,17);assert.deepEqual(first.runtimeDependencies,['dotenv','pg','zod']);assert.equal(first.backendDistModules.length,9)
|
||||
assert.deepEqual(first,second);assert.equal(first.modules.length,OPERATOR_SOURCES.length);assert.equal(first.modules.length,19);assert.deepEqual(first.runtimeDependencies,['dotenv','pg','zod']);assert.equal(first.backendDistModules.length,12)
|
||||
const copied=new Set()
|
||||
async function copyDist(relative){
|
||||
if(copied.has(relative))return;copied.add(relative)
|
||||
@@ -37,7 +37,7 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
|
||||
for(const file of OPERATOR_SOURCES){const target=path.join(projectBackend,'scripts',file);await mkdir(path.dirname(target),{recursive:true});await copyFile(path.join(backend,'scripts',file),target)}
|
||||
const buildScript=path.join(projectBackend,'scripts','build-database-operator.mjs');await copyFile(fileURLToPath(new URL('./build-database-operator.mjs',import.meta.url)),buildScript)
|
||||
const built=spawnSync(process.execPath,[buildScript],{cwd:temporary,env:{...process.env,NODE_OPTIONS:''},encoding:'utf8',timeout:30000})
|
||||
assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,17)
|
||||
assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,19)
|
||||
assert.deepEqual(JSON.parse(await readFile(path.join(output,'build-manifest.json'),'utf8')),first)
|
||||
const configFile=path.join(projectBackend,'dist','config.js'),originalConfig=await readFile(configFile,'utf8')
|
||||
for(const injected of ["import 'tsx';","import '../src/config.js';","import '../outside.js';"]){
|
||||
@@ -59,10 +59,10 @@ test('fixed artifact is reproducible, resolves the compiled backend graph, and r
|
||||
const modulesToImport=first.modules.map(item=>path.join(output,item.file))
|
||||
const importer=path.join(temporary,'import-all.mjs');await writeFile(importer,`import {pathToFileURL} from 'node:url';for(const file of ${JSON.stringify(modulesToImport)})await import(pathToFileURL(file).href);console.log(JSON.stringify({imported:${modulesToImport.length},readyAuthorized:false}));`)
|
||||
const imported=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,importer],{cwd:temporary,env,encoding:'utf8',timeout:30000})
|
||||
assert.equal(imported.status,0,imported.stderr);assert.equal(JSON.parse(imported.stdout.trim()).imported,17);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0)
|
||||
assert.equal(imported.status,0,imported.stderr);assert.equal(JSON.parse(imported.stdout.trim()).imported,19);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0)
|
||||
const refusals=[]
|
||||
for(const [entry,event] of [['database-writer-bootstrap-operator.mjs','application-writer-bootstrap-refused'],['database-writer-profile-operator.mjs','application-writer-profile-publication-refused']]){
|
||||
const child=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,path.join(output,entry),'invalid'],{cwd:temporary,env,encoding:'utf8',timeout:30000})
|
||||
for(const [entry,event,args] of [['database-writer-bootstrap-operator.mjs','application-writer-bootstrap-refused',['invalid']],['database-writer-profile-operator.mjs','application-writer-profile-publication-refused',['invalid']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-inspect']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-resolve']]]){
|
||||
const child=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,path.join(output,entry),...args],{cwd:temporary,env,encoding:'utf8',timeout:30000})
|
||||
assert.equal(child.status,1,child.stderr);assert.equal(child.stdout,'');assert.equal(JSON.parse(child.stderr.trim()).event,event);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0);refusals.push(event)
|
||||
}
|
||||
const receipt={node:process.version,projection:temporary,modules:first.modules.length,compiledBackendModules:copied.size,buildCliExitCode:built.status,importedModules:modulesToImport.length,invalidArgumentsRefused:refusals,developmentDependenciesBlocked:denied,networkConnections:0,productionActivated:false,readyAuthorized:false}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {mkdtemp,mkdir,writeFile,readFile,readdir,rm,unlink,lstat} from 'node:fs/promises'
|
||||
import {createHash,randomUUID} from 'node:crypto'
|
||||
import {rootCertificates} from 'node:tls'
|
||||
import {EventEmitter} from 'node:events'
|
||||
import path from 'node:path'
|
||||
import {fileURLToPath,pathToFileURL} from 'node:url'
|
||||
import {runInNewContext} from 'node:vm'
|
||||
import {createRequire} from 'node:module'
|
||||
import * as strictReader from '../src/services/database-maintenance-startup.js'
|
||||
import * as grammar from '../src/services/database-maintenance-resolution.js'
|
||||
import * as manifest from '../src/db/migration-manifest.js'
|
||||
import * as contract from './writer-profile/operator-contract.mjs'
|
||||
const backend=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'),require=createRequire(import.meta.url),ts=require('typescript'),hash=(value:Buffer|string)=>createHash('sha256').update(value).digest('hex')
|
||||
async function own(work:(root:string)=>Promise<void>){const temp=path.resolve(backend,'../tmp');await mkdir(temp,{recursive:true});const root=await mkdtemp(path.join(temp,'owned-resolution-command-'));try{await work(root)}finally{assert.equal(path.dirname(root),temp);assert(path.basename(root).startsWith('owned-resolution-command-'));await rm(root,{recursive:true,force:true})}}
|
||||
async function model(root:string,localHost?:string){
|
||||
let owners=0,sql=0,releaseFailure=false,poolEndFailure=false,published=0,wrongOwner=false,controlSchema=false,observationFailure:Error|undefined
|
||||
const capturedScopes:unknown[]=[]
|
||||
const state=path.join(root,'state');await mkdir(state);await mkdir(path.join(state,'restore-vault'));await writeFile(path.join(state,'access-generation'),Buffer.alloc(32,7));const uncertain=randomUUID(),at=new Date().toISOString(),record=(value:string)=>JSON.stringify({operationId:uncertain,state:value,at})+'\n';await writeFile(path.join(state,'database-maintenance.jsonl'),record('intent')+record('unconfirmed'))
|
||||
const source={systemIdentifier:'1234567890',databaseOid:'12345',databaseName:'managed',serverVersionNum:180004,inRecovery:false}
|
||||
const schema={sha256:'a'.repeat(64),observedRaw:{sha256:'b'.repeat(64)},observedPhysical:{sha256:'c'.repeat(64)},sections:{relations:manifest.CURRENT_REQUIRED_TABLES.map(name=>({namespace:'public',name,kind:'r',persistence:'p',row_security:false,force_row_security:false})),columns:[{namespace:'public',relation:'schema_migrations',name:'id',dropped:false,type:'text'}]}}
|
||||
class Pool {on(){};constructor(options:unknown){const ssl=(options as {ssl:false|{rejectUnauthorized:boolean}}).ssl;if(ssl===false)assert(['127.0.0.1','::1'].includes((options as {host:string}).host));else assert(ssl.rejectUnauthorized)}async connect(){owners++;const client=new EventEmitter();return Object.assign(client,{release:()=>{owners--;if(releaseFailure)throw Error('Synthetic release uncertainty')},query:async(text:string)=>{sql++;if(text.includes('pg_try_advisory_lock'))return {rows:[{locked:true}],command:'SELECT'};if(text.includes('pg_advisory_unlock'))return {rows:[{unlocked:true}],command:'SELECT'};if(text.includes('pg_namespace'))return {rows:controlSchema?[{nspname:'mmapp_control'}]:[],command:'SELECT'};if(text.includes('pg_control_system')){if(observationFailure)throw observationFailure;return {rows:[source],command:'SELECT'}};if(text.startsWith('SELECT id FROM public.schema_migrations'))return {rows:manifest.MIGRATION_MANIFEST.map(pin=>({id:pin.file.slice(0,-4)})),command:'SELECT'};return {rows:[],command:text.split(' ')[0]}}})}async end(){assert.equal(owners,0);if(poolEndFailure)throw Error('Synthetic pool retirement uncertainty')}}
|
||||
const protectedSource=await readFile(path.join(backend,'scripts/writer-profile/operator-files.mjs'),'utf8'),protectedExports:Record<string,unknown>={},actualUid=(await lstat(state)).uid
|
||||
const protectedCode=ts.transpileModule(protectedSource,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
|
||||
runInNewContext(protectedCode,{exports:protectedExports,require,Buffer,process:{platform:'linux',getuid:()=>actualUid+1},Object,Error})
|
||||
const protectedDirectory=async(directory:string)=>{assert(path.resolve(directory).startsWith(root+path.sep)||path.resolve(directory)===root);if(wrongOwner)await (protectedExports.protectedDirectory as (path:string)=>Promise<void>)(directory);await readdir(directory)}
|
||||
// Explicit MODELED protection adapter: native POSIX mode/ownership and hardlink
|
||||
// fsync publication remain separate Linux acceptance, never a Windows claim.
|
||||
const files={sha:hash,protectedDirectory,readProtected:async(file:string,maximum:number,pin?:string)=>{assert(file.startsWith(root+path.sep));const raw=await readFile(file);assert(raw.length>0&&raw.length<=maximum);if(pin)assert.equal(hash(raw),pin);return raw},writeFile:async(file:string,raw:Buffer)=>{assert(file.startsWith(root+path.sep));await writeFile(file,raw,{flag:'wx'});published++},withJournalLease:async(directory:string,work:()=>Promise<unknown>)=>{const lock=path.join(directory,'publisher-lease.lock');await writeFile(lock,'owned',{flag:'wx'});try{return await work()}finally{await unlink(lock)}}}
|
||||
const readRecords=async(directory:string)=>{const records=path.join(directory,'maintenance-acceptances');let names:string[];try{names=await readdir(records)}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return [];throw error};assert(names.every(name=>/^[a-f0-9-]{36}\.json$/.test(name)));return Promise.all(names.sort().map(name=>readFile(path.join(records,name))))}
|
||||
const moduleFile=path.join(backend,'scripts/writer-profile/maintenance-resolution.mts'),sourceText=(await readFile(moduleFile,'utf8')).replaceAll('import.meta.url',JSON.stringify(pathToFileURL(moduleFile).href)),exports:Record<string,unknown>={}
|
||||
const dependencies:Record<string,unknown>={'./operator-files.mjs':files,'./operator-contract.mjs':contract,'../../src/services/database-maintenance-startup.js':{...strictReader,readMaintenanceAcceptanceRecords:readRecords},'../../src/services/database-maintenance-resolution.js':grammar,'../../src/db/migration-schema-inventory.js':{captureMigrationSchema:async(_client:unknown,options:{scope:unknown})=>{capturedScopes.push(options.scope);return schema}},'../../src/db/migration-manifest.js':manifest,pg:{Pool}}
|
||||
const code=ts.transpileModule(sourceText,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
|
||||
runInNewContext(code,{exports,require:(name:string)=>name in dependencies?dependencies[name]:require(name),Buffer,URL,process:{env:{}},Date,Object,Map,Set,Error,console})
|
||||
const command=exports.maintenanceMain as (args:string[])=>Promise<Record<string,unknown>>
|
||||
const tlsTransport={format:'database-maintenance-transport-v1',connection:{host:'db.example.invalid',port:5432,user:'existing-maintenance-user',password:'synthetic-only',database:'managed'},tls:{serverName:'db.example.invalid',spkiSha256:'f'.repeat(64)}},localTransport=(host:string)=>({format:'database-maintenance-transport-v1',mode:'local-loopback',connection:{...tlsTransport.connection,host}})
|
||||
const configs={transport:localHost===undefined?tlsTransport:localTransport(localHost),'runtime-target':{format:'database-maintenance-runtime-target-v1',databaseUrl:'postgresql://db.example.invalid:5432/managed?sslmode=require'}}
|
||||
const args=['--state-dir',state]
|
||||
for(const [name,value]of Object.entries(configs)){const raw=Buffer.from(JSON.stringify(value)),file=path.join(root,name+'.json');await writeFile(file,raw);args.push('--'+name,file,'--'+name+'-sha256',hash(raw))}
|
||||
const ca=Buffer.from(rootCertificates[0]),caFile=path.join(root,'ca.pem');await writeFile(caFile,ca);if(localHost===undefined)args.push('--ca',caFile,'--ca-sha256',hash(ca))
|
||||
const output=path.join(root,'inspection.json')
|
||||
async function inspect(destination=output){return command(['maintenance-inspect',...args,'--output',destination])}
|
||||
async function approve(change?:(value:Record<string,unknown>)=>void){const raw=await readFile(output),inspection=JSON.parse(raw.toString()),approval={format:'database-maintenance-current-state-approval-v1',resolutionId:randomUUID(),decision:'accept-observed-current-state',actorId:'host-human-reviewer',inspectionSha256:hash(raw),issuedAt:new Date().toISOString(),expiresAt:new Date(Date.now()+60000).toISOString(),managedBackendStopped:true,historicOutcomesRemainUnknown:true,automaticRestore:false};assert(inspection.createdAt<=approval.issuedAt);change?.(approval);const bytes=Buffer.from(JSON.stringify(approval)),file=path.join(root,'approval.json');await writeFile(file,bytes);return ['maintenance-resolve',...args,'--inspection',output,'--inspection-sha256',hash(raw),'--approval',file,'--approval-sha256',hash(bytes)]}
|
||||
const toLocal=async(host='127.0.0.1')=>{const raw=Buffer.from(JSON.stringify(localTransport(host)));await writeFile(path.join(root,'transport.json'),raw);args[args.indexOf('--transport-sha256')+1]=hash(raw);for(const flag of ['--ca','--ca-sha256']){const at=args.indexOf(flag);if(at>=0)args.splice(at,2)}}
|
||||
const configuration=exports.maintenancePoolConfiguration as (value:unknown,ca?:Buffer,pin?:string)=>{options:Record<string,unknown>;transportBinding:{mode:string;endpointSha256:string}}
|
||||
return {command,inspect,approve,toLocal,configuration,readRecords,state,output,sql:()=>sql,published:()=>published,failRelease:()=>{releaseFailure=true},failEnd:()=>{poolEndFailure=true},driftSchema:()=>{schema.sha256='0'.repeat(64)},driftTarget:()=>{source.databaseOid='54321'},wrongOwner:()=>{wrongOwner=true},withControlSchema:()=>{controlSchema=true},scopes:()=>capturedScopes,failObservation:(error:Error)=>{observationFailure=error}}
|
||||
}
|
||||
test('actual operator commands inspect then separately approved resolve; old uncertain bytes never rewritten',()=>own(async root=>{
|
||||
const fixture=await model(root),journal=path.join(fixture.state,'database-maintenance.jsonl'),before=await readFile(journal),inspected=await fixture.inspect();assert.equal(inspected.humanApprovalRequired,true);assert.equal((await fixture.readRecords(fixture.state)).length,0)
|
||||
const result=await fixture.command(await fixture.approve());assert.equal(result.outcome,'committed');assert.equal(result.historicOutcomesChanged,false);assert.equal(result.archiveDataEquivalenceProved,false);assert.equal(result.automaticRestore,false)
|
||||
assert.deepEqual(await readFile(journal),before);const records=await fixture.readRecords(fixture.state),facts=await strictReader.readMaintenanceRecoveryFacts(fixture.state);assert.equal(records.length,1);const accepted=grammar.evaluateMaintenanceAcceptances(records,facts.journal,facts.vault.evidence,grammar.runtimeDatabaseTargetSha256('postgresql://db.example.invalid:5432/managed?sslmode=require'));assert.equal(accepted.count,1)
|
||||
fixture.driftSchema();const afterStatus=await fixture.inspect(path.join(root,'after-inspection.json'));assert.equal(afterStatus.state,'historically-accepted');assert.equal(afterStatus.humanApprovalRequired,false);assert.equal(afterStatus.historicOutcomesChanged,false)
|
||||
await assert.rejects(fixture.command(await fixture.approve()),/No new unresolved|already|assert|Assertion/i)
|
||||
}))
|
||||
for(const type of ['expired','not-stopped','wrong-inspection'])test('actual resolve refuses '+type+' approval before connecting SQL',()=>own(async root=>{
|
||||
const fixture=await model(root);await fixture.inspect();const args=await fixture.approve(value=>{if(type==='expired')value.expiresAt=new Date(Date.now()-1).toISOString();if(type==='not-stopped')value.managedBackendStopped=false;if(type==='wrong-inspection')value.inspectionSha256='0'.repeat(64)}),before=fixture.sql();await assert.rejects(fixture.command(args));assert.equal(fixture.sql(),before);assert.equal((await fixture.readRecords(fixture.state)).length,0)
|
||||
}))
|
||||
for(const type of ['release','pool-end','schema-drift','target-drift','external-drift'])test('actual resolve refuses '+type+' before acceptance publication',()=>own(async root=>{
|
||||
const fixture=await model(root);await fixture.inspect();const args=await fixture.approve();if(type==='release')fixture.failRelease();if(type==='pool-end')fixture.failEnd()
|
||||
if(type==='schema-drift')fixture.driftSchema();if(type==='target-drift')fixture.driftTarget()
|
||||
if(type==='external-drift')await appendFileSafe(path.join(fixture.state,'database-maintenance.jsonl'))
|
||||
await assert.rejects(fixture.command(args));assert.equal((await fixture.readRecords(fixture.state)).length,0)
|
||||
}))
|
||||
async function appendFileSafe(file:string){await writeFile(file,await readFile(file,'utf8')+JSON.stringify({operationId:randomUUID(),state:'intent',at:new Date().toISOString()})+'\n')}
|
||||
test('existing shipped main dispatches both commands before writer-profile role/source flags',async()=>{
|
||||
const source=await readFile(path.join(backend,'scripts/database-writer-profile-operator.mts'),'utf8');assert(source.indexOf("['maintenance-inspect','maintenance-resolve']")<source.indexOf("const flags="));assert(source.includes("import('./writer-profile/maintenance-resolution.mjs')"))
|
||||
})
|
||||
|
||||
test('existing protectedDirectory owner contract refuses a foreign CLI UID before SQL or writes',()=>own(async root=>{
|
||||
const fixture=await model(root);fixture.wrongOwner();await assert.rejects(fixture.inspect());assert.equal(fixture.sql(),0);assert.equal(fixture.published(),0);assert.equal((await fixture.readRecords(fixture.state)).length,0)
|
||||
}))
|
||||
|
||||
test('explicit local-loopback supports both IP families and binds selected mode in actual inspect/resolve',async()=>{
|
||||
for(const host of ['127.0.0.1','::1'])await own(async root=>{const fixture=await model(root,host);await fixture.inspect();const result=await fixture.command(await fixture.approve());assert.equal(result.outcome,'committed');const records=await fixture.readRecords(fixture.state);assert.equal(JSON.parse(records[0].toString()).knownCurrent.transport.mode,'local-loopback')})
|
||||
})
|
||||
test('plaintext remote/DNS/unspecified/mapped hosts refuse before SQL and never fall back',async()=>{
|
||||
for(const host of ['db.example.invalid','localhost','0.0.0.0','192.0.2.1','[::1]','::ffff:127.0.0.1'])await own(async root=>{const fixture=await model(root,host);await assert.rejects(fixture.inspect());assert.equal(fixture.sql(),0);assert.equal(fixture.published(),0)})
|
||||
})
|
||||
test('an approval for TLS observation cannot resolve through a changed local-loopback transport',()=>own(async root=>{
|
||||
const fixture=await model(root);await fixture.inspect();await fixture.toLocal();await assert.rejects(fixture.command(await fixture.approve()));assert.equal((await fixture.readRecords(fixture.state)).length,0)
|
||||
}))
|
||||
test('local mode explicitly disables SSL/environment substitution; TLS remains verified default without fallback',()=>own(async root=>{
|
||||
const fixture=await model(root),ConnectionParameters=require('pg/lib/connection-parameters'),raw={format:'database-maintenance-transport-v1',mode:'local-loopback',connection:{host:'127.0.0.1',port:5432,user:'existing-user',password:'synthetic-only',database:'managed'}}
|
||||
const previous={PGHOST:process.env.PGHOST,PGSSLMODE:process.env.PGSSLMODE,PGOPTIONS:process.env.PGOPTIONS};process.env.PGHOST='remote.invalid';process.env.PGSSLMODE='require';process.env.PGOPTIONS='-c role=foreign'
|
||||
try{const config=fixture.configuration(raw),actual=new ConnectionParameters(config.options);assert.equal(actual.host,'127.0.0.1');assert.equal(actual.ssl,false);assert.equal(actual.options,contract.STARTUP_OPTIONS);assert.equal(config.transportBinding.mode,'local-loopback')}
|
||||
finally{for(const [key,value]of Object.entries(previous)){if(value===undefined)delete process.env[key];else process.env[key]=value}}
|
||||
const ca=Buffer.from(rootCertificates[0]),tls={format:'database-maintenance-transport-v1',connection:{...raw.connection,host:'db.example.invalid'},tls:{serverName:'db.example.invalid',spkiSha256:'f'.repeat(64)}}
|
||||
assert.throws(()=>fixture.configuration(tls));const secure=fixture.configuration(tls,ca,hash(ca));assert.equal(secure.transportBinding.mode,'tls');assert.equal((secure.options.ssl as {rejectUnauthorized:boolean}).rejectUnauthorized,true)
|
||||
assert.throws(()=>fixture.configuration({...raw,mode:'plaintext'}));assert.throws(()=>fixture.configuration({...tls,mode:null},ca,hash(ca)));assert.throws(()=>fixture.configuration(raw,ca,hash(ca)))
|
||||
}))
|
||||
|
||||
// A normal installation has no optional writer-profile schema. Observation must
|
||||
// select its exact namespace set without requiring installation of that system.
|
||||
test('maintenance observes with and without the optional control namespace',async()=>{
|
||||
for(const present of [false,true])await own(async root=>{const fixture=await model(root);if(present)fixture.withControlSchema();await fixture.inspect();assert.equal(JSON.stringify(fixture.scopes()),JSON.stringify([{applicationNamespaces:['public'],excludedControlNamespaces:present?['mmapp_control']:[]}]))})
|
||||
})
|
||||
test('original observation failure survives confirmed rollback/unlock/release',()=>own(async root=>{
|
||||
const fixture=await model(root),failure=Error('Synthetic original observation failure');fixture.failObservation(failure);await assert.rejects(fixture.inspect(),error=>error===failure);assert.equal(fixture.published(),0);assert.equal((await fixture.readRecords(fixture.state)).length,0)
|
||||
}))
|
||||
@@ -9,6 +9,7 @@ import {deploymentRequest} from './database-writer-bootstrap-operator.mjs'
|
||||
const hex=(v:unknown)=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v)
|
||||
const parse=(b:Buffer)=>JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(b))
|
||||
export async function main(args:string[]){
|
||||
if(['maintenance-inspect','maintenance-resolve'].includes(args[0]))return (await import('./writer-profile/maintenance-resolution.mjs')).maintenanceMain(args)
|
||||
const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal']
|
||||
assert(['publish','reconcile','preflight'].includes(args[0])&&args.length===1+2*flags.length)
|
||||
assertJsDriverEnvironment(process.env)
|
||||
@@ -38,7 +39,12 @@ export async function main(args:string[]){
|
||||
assertJsDriverEnvironment(process.env)
|
||||
assert.equal(approvedDefinition(definition),pinned.definitionSha256)
|
||||
const installation=Object.freeze({...subject,roles:Object.fromEntries(roleKeys.map(k=>[k,request.roles[k].signedOid])),roleBindings:request.roles,expectedSource:request.source})
|
||||
const verifier=createDatabaseSourceBindingVerifier({registryDirectory:get('source-registry'),installationId:subject.installationId,publicKeySpkiSha256:get('source-spki-sha256'),currentHeadSha256:request.sourceHeadSha256})
|
||||
const sourcePin={registryDirectory:get('source-registry'),installationId:subject.installationId,publicKeySpkiSha256:get('source-spki-sha256'),currentHeadSha256:request.sourceHeadSha256}
|
||||
// Preflight is the read-only lifecycle entry for protected registry evidence.
|
||||
// A refused protection check never falls back to the signature-only verifier.
|
||||
const verifier=args[0]==='preflight'
|
||||
?(await import('./writer-profile/protected-source-registry.mjs')).createProtectedDatabaseSourceBindingVerifier(sourcePin)
|
||||
:createDatabaseSourceBindingVerifier(sourcePin)
|
||||
const publisher=createConnectedPublisher(installation,(s:Parameters<typeof collectSource44Catalogue>[0])=>collectSource44Catalogue(s,installation),verifier,pinned)
|
||||
const pool=new pg.Pool(options)
|
||||
// A pool-level async error never becomes raw stdout or a retry signal.
|
||||
@@ -47,7 +53,7 @@ export async function main(args:string[]){
|
||||
try{result=args[0]==='preflight'?await publisher.preflightCurrentProfile(pool,{deadlineAt:Date.now()+120000}):args[0]==='reconcile'?await publisher.reconcilePendingPublication(pool,{journalDirectory:get('journal'),deadlineAt:Date.now()+120000}):await publisher.publishCurrentProfile(pool,{privateKeyFile,expectedHeadSha256:pinned.expectedHeadSha256,journalDirectory:get('journal'),validityMs:pinned.validityMs,deadlineAt:Date.now()+120000})}
|
||||
catch(error){primary=error;failed=true}
|
||||
try{await pool.end()}catch(error){primary??=error;failed=true}
|
||||
if(args[0]==='preflight'&&result?.state==='matched'&&!failed&&!asyncFailure)return {event:'application-writer-profile-preflight',outcome:'matched',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,sourceMatched:true,rolesMatched:true,catalogueMatched:true,writerAuthorized:false,cleanupUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
|
||||
if(args[0]==='preflight'&&result?.state==='matched'&&result.platformProtectionVerified===true&&!failed&&!asyncFailure)return {event:'application-writer-profile-preflight',outcome:'matched',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,sourceMatched:true,rolesMatched:true,catalogueMatched:true,platformProtectionVerified:true,writerAuthorized:false,cleanupUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
|
||||
if(result?.outcome==='committed')return {event:'application-writer-profile-publication',outcome:'committed',epoch:result.epoch,headSha256:result.headSha256,notAfter:result.notAfter,
|
||||
cleanupUnconfirmed:result.cleanupUnconfirmed||asyncFailure||failed,journalUnconfirmed:!!result.journalUnconfirmed,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
|
||||
if(args[0]==='reconcile'&&result&&!failed&&!asyncFailure&&['idle','confirmed','not-committed'].includes(result.state))return {event:'application-writer-profile-reconciliation',outcome:result.state,headSha256:result.headSha256??null,cleanupUnconfirmed:false,journalUnconfirmed:false,automaticReplay:false,generationAuthenticated:false,readyAuthorized:false}
|
||||
@@ -55,5 +61,5 @@ export async function main(args:string[]){
|
||||
}
|
||||
if(process.argv[1]&&import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href){
|
||||
try{const result=await main(process.argv.slice(2));console.log(JSON.stringify(result));if(result.cleanupUnconfirmed||result.journalUnconfirmed)process.exitCode=2}
|
||||
catch{console.error(JSON.stringify({event:'application-writer-profile-publication-refused',automaticReplay:false,inspectPendingBeforeRetry:true,readyAuthorized:false}));process.exitCode=1}
|
||||
catch{console.error(JSON.stringify({event:['maintenance-inspect','maintenance-resolve'].includes(process.argv[2])?'database-maintenance-command-refused':'application-writer-profile-publication-refused',automaticReplay:false,inspectPendingBeforeRetry:true,readyAuthorized:false}));process.exitCode=1}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ import {readCurrentApplicationWriterProfile} from '../src/db/application-writer-
|
||||
import {syntheticFixture,stagedModelClient} from '../src/db/application-writer-profile-registry-read-model.test.ts'
|
||||
const require=createRequire(new URL('../package.json',import.meta.url)),ts=require('typescript'),pg=require('pg')
|
||||
const candidate=await fs.readFile(new URL('./writer-profile/publisher.mjs',import.meta.url),'utf8')
|
||||
interface PreflightDiagnostics {state:string;headSha256:string;sourceMatched:boolean;rolesMatched:boolean;catalogueMatched:boolean;writerAuthorized:boolean;generationAuthenticated:boolean;readyAuthorized:boolean}
|
||||
interface PublisherModule {createConnectedPublisher(installation:unknown,collect:()=>Promise<unknown>,sourceVerifier:{verify:()=>Promise<void>},approval:unknown):{preflightCurrentProfile(pool:unknown,options:{deadlineAt:number}):Promise<PreflightDiagnostics>}}
|
||||
interface OperatorModule {main(args:string[]):Promise<{event:string;outcome:string;writerAuthorized:boolean;readyAuthorized:boolean}>}
|
||||
interface PreflightDiagnostics {state:string;headSha256:string;sourceMatched:boolean;rolesMatched:boolean;catalogueMatched:boolean;platformProtectionVerified:boolean;writerAuthorized:boolean;generationAuthenticated:boolean;readyAuthorized:boolean}
|
||||
interface PublisherModule {createConnectedPublisher(installation:unknown,collect:()=>Promise<unknown>,sourceVerifier:{verify:()=>Promise<{platformProtectionVerified:boolean}>},approval:unknown):{preflightCurrentProfile(pool:unknown,options:{deadlineAt:number}):Promise<PreflightDiagnostics>}}
|
||||
interface OperatorModule {main(args:string[]):Promise<{event:string;outcome:string;platformProtectionVerified:boolean;writerAuthorized:boolean;readyAuthorized:boolean}>}
|
||||
function compile<T>(source:string,modules:Record<string,unknown>):T{
|
||||
const exports:Record<string,unknown>={};const compiled=ts.transpileModule(source.split('\nif(process.argv[1]')[0].replaceAll('import.meta.url',JSON.stringify('file:///candidate-not-cli.mts')),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText
|
||||
runInNewContext(compiled,{exports,require:(name:string)=>{assert(name in modules,'Unapproved import '+name);return modules[name]},Buffer,URL,TextDecoder,Date,setTimeout,clearTimeout,Object,Error,AggregateError,Promise,Map,Set,process:{env:{},argv:[]}}, {filename:'candidate.mjs'})
|
||||
@@ -56,17 +56,23 @@ function fixture(fault=''){
|
||||
'../../src/db/application-writer-profile-format.js':format,
|
||||
'./context.mjs':{ownedContext:async()=>{contexts++;if(fault==='late-actor'&&contexts===2)throw Error('Actor changed');return {pid:1,source}},sameOwned:(a:unknown,b:unknown)=>assert.deepEqual(a,b),sourceCoordinates:(v:{source:typeof source})=>v.source,protectedStructure:async()=>{events.push('roles');if(fault==='role-structure')throw Error('Unsafe role');return security},readCurrent:readCurrentApplicationWriterProfile,HISTORY:'history',CURRENT:'current'},
|
||||
}
|
||||
const api=compile<PublisherModule>(candidate,modules).createConnectedPublisher(installation,async()=>{events.push('catalogue');if(fault==='catalogue-eof')throw Error('Incomplete EOF');return catalogue},{verify:async()=>{verifications++;events.push('verify-source');if(fault==='source-verifier')throw Error('Unsigned source');if(fault==='late-expiry'&&verifications===2)approval.expiresAt=1}},approval)
|
||||
const api=compile<PublisherModule>(candidate,modules).createConnectedPublisher(installation,async()=>{events.push('catalogue');if(fault==='catalogue-eof')throw Error('Incomplete EOF');return catalogue},{verify:async()=>{verifications++;events.push('verify-source');if(fault==='source-verifier')throw Error('Unsigned source');if(fault==='late-expiry'&&verifications===2)approval.expiresAt=1;return {platformProtectionVerified:!(fault==='initial-unprotected'&&verifications===1||fault==='terminal-unprotected'&&verifications===2)}}},approval)
|
||||
return {run:()=>api.preflightCurrentProfile(pool,{deadlineAt:Date.now()+30000}),queries,events,f}
|
||||
}
|
||||
test('current44 preflight authenticates signed current and actual component inputs on one read-only lease',async()=>{
|
||||
const f=fixture(),result=await f.run();assert.equal(result.state,'matched');assert.equal(result.headSha256,f.f.state.current[0].head_sha256)
|
||||
assert.equal(result.sourceMatched,true);assert.equal(result.rolesMatched,true);assert.equal(result.catalogueMatched,true)
|
||||
assert.equal(result.platformProtectionVerified,true)
|
||||
assert.equal(result.writerAuthorized,false);assert.equal(result.generationAuthenticated,false);assert.equal(result.readyAuthorized,false)
|
||||
assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1);assert(f.queries.includes('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'));assert.equal(f.queries.filter(q=>q==='ROLLBACK').length,1)
|
||||
assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.events.at(-1),'release:false')
|
||||
assert.deepEqual(f.events.slice(0,6),['capture-source','verify-source','roles','catalogue','verify-source','fresh-source'])
|
||||
})
|
||||
for(const fault of ['initial-unprotected','terminal-unprotected'])test('preflight cannot promote '+fault+' into protected evidence',async()=>{
|
||||
const f=fixture(fault),result=await f.run();assert.equal(result.platformProtectionVerified,false)
|
||||
assert.equal(result.writerAuthorized,false);assert.equal(result.readyAuthorized,false)
|
||||
assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)))
|
||||
})
|
||||
for(const fault of ['tamper','empty','expired','signed-source','signed-role','signed-catalogue','signed-definition','signed-manifest','role-structure','catalogue-eof','source-verifier','late-source','late-actor','late-expiry','late-clock','rollback-ack'])test('preflight refuses '+fault+' without any write or replay',async()=>{
|
||||
const f=fixture(fault);await assert.rejects(f.run());assert(!f.queries.some(q=>/^(?:INSERT|UPDATE|DELETE|COMMIT|CREATE|ALTER|DROP)\b/.test(q)));assert.equal(f.queries.filter(q=>q.startsWith('BEGIN')).length,1)
|
||||
})
|
||||
@@ -74,23 +80,28 @@ test('existing operator main dispatches preflight only after protected input val
|
||||
const cli=await fs.readFile(new URL('./database-writer-profile-operator.mts',import.meta.url),'utf8'),f=syntheticFixture('1'),reads:string[]=[],events:string[]=[]
|
||||
const request={installationId:f.subject.installationId,roles:Object.fromEntries(['runtime','operator','capture','publisher','control'].map((k,i)=>[k,{name:k,signedOid:String(i+11)}])),source:f.policy.source,sourceHeadSha256:'a'.repeat(64),tls:{caSha256:'a'.repeat(64)}}
|
||||
const files:Record<string,unknown>={bootstrap:{format:'application-writer-bootstrap-v1',publicationAuthorized:false,readyAuthorized:false,deploymentRequest:request,verificationSubject:f.subject},approval:{expiresAt:Date.now()+60000,definitionSha256:'b'.repeat(64)},definition:{},transport:{},ca:'fixture'}
|
||||
let failEnd=false,asyncError=false
|
||||
let failEnd=false,asyncError=false,protectedRefused=false
|
||||
const retainedPools:Pool[]=[]
|
||||
class Pool extends EventEmitter{constructor(){super();retainedPools.push(this)}async end(){events.push('pool-end');if(failEnd)throw Error('Pool cleanup unconfirmed')}}
|
||||
const result={state:'matched',epoch:'1',headSha256:'c'.repeat(64),notAfter:Date.now()+60000}
|
||||
const result={state:'matched',epoch:'1',headSha256:'c'.repeat(64),notAfter:Date.now()+60000,platformProtectionVerified:true}
|
||||
const protectedVerifier={verify:async()=>({platformProtectionVerified:true})}
|
||||
const modules={
|
||||
'node:assert/strict':assert,'node:path':require('node:path'),'node:url':require('node:url'),'node:crypto':require('node:crypto'),
|
||||
'./writer-profile/operator-files.mjs':{readProtected:async(file:string)=>{reads.push(file);assert(file in files);return Buffer.from(JSON.stringify(files[file]))},protectedDirectory:async()=>{},sha:format.sha},
|
||||
'./writer-profile/operator-contract.mjs':{approval:(value:unknown)=>{events.push('approval');return value},reconciliationApproval:()=>{throw Error('No expired reconciliation bypass')},poolOptions:()=>{events.push('transport');return {}},roleKeys:['runtime','operator','capture','publisher','control'],assertJsDriverEnvironment:()=>{}},
|
||||
'./database-writer-bootstrap-operator.mjs':{deploymentRequest:(value:unknown)=>value},'../src/db/database-source-binding.js':{createDatabaseSourceBindingVerifier:()=>({})},
|
||||
pg:{Pool},'./writer-profile/publisher.mjs':{createConnectedPublisher:()=>({preflightCurrentProfile:async()=>{events.push('preflight');if(asyncError){const activePool=retainedPools.at(-1);assert(activePool);activePool.emit('error',Error('Async pool fault'))}return result},publishCurrentProfile:()=>{throw Error('No publish')},reconcilePendingPublication:()=>{throw Error('No reconcile')}})},
|
||||
'./database-writer-bootstrap-operator.mjs':{deploymentRequest:(value:unknown)=>value},'../src/db/database-source-binding.js':{createDatabaseSourceBindingVerifier:()=>{throw Error('Preflight must never fall back to unprotected source verification')}},
|
||||
'./writer-profile/protected-source-registry.mjs':{createProtectedDatabaseSourceBindingVerifier:(pin:{registryDirectory:string;currentHeadSha256:string})=>{events.push('protected-source');assert.equal(pin.registryDirectory,'source-registry');assert.equal(pin.currentHeadSha256,request.sourceHeadSha256);if(protectedRefused)throw Error('Protected source refused');return protectedVerifier}},
|
||||
pg:{Pool},'./writer-profile/publisher.mjs':{createConnectedPublisher:(_installation:unknown,_collect:unknown,verifier:unknown)=>{assert.equal(verifier,protectedVerifier);return {preflightCurrentProfile:async()=>{events.push('preflight');if(asyncError){const activePool=retainedPools.at(-1);assert(activePool);activePool.emit('error',Error('Async pool fault'))}return result},publishCurrentProfile:()=>{throw Error('No publish')},reconcilePendingPublication:()=>{throw Error('No reconcile')}}}},
|
||||
'./writer-profile/catalogue.mjs':{collectSource44Catalogue:()=>{},approvedDefinition:()=> 'b'.repeat(64)},
|
||||
}
|
||||
const flags=['bootstrap','bootstrap-sha256','approval','approval-sha256','definition','definition-sha256','transport','transport-sha256','ca','source-registry','source-spki-sha256','journal'],args=['preflight',...flags.flatMap(flag=>['--'+flag,flag.endsWith('sha256')?'a'.repeat(64):flag])]
|
||||
const output=await compile<OperatorModule>(cli,modules).main(args)
|
||||
assert.equal(output.event,'application-writer-profile-preflight');assert.equal(output.outcome,'matched');assert.equal(output.writerAuthorized,false);assert.equal(output.readyAuthorized,false)
|
||||
assert.deepEqual(reads,['bootstrap','approval','definition','transport','ca']);assert.deepEqual(events,['approval','transport','preflight','pool-end'])
|
||||
assert.equal(output.event,'application-writer-profile-preflight');assert.equal(output.outcome,'matched');assert.equal(output.platformProtectionVerified,true);assert.equal(output.writerAuthorized,false);assert.equal(output.readyAuthorized,false)
|
||||
assert.deepEqual(reads,['bootstrap','approval','definition','transport','ca']);assert.deepEqual(events,['approval','transport','protected-source','preflight','pool-end'])
|
||||
failEnd=true;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Pool cleanup unconfirmed/)
|
||||
failEnd=false;asyncError=true;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Publisher terminal unconfirmed/)
|
||||
asyncError=false;result.platformProtectionVerified=false;await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Publisher terminal unconfirmed/)
|
||||
result.platformProtectionVerified=true;protectedRefused=true;const before=retainedPools.length
|
||||
await assert.rejects(compile<OperatorModule>(cli,modules).main(args),/Protected source refused/);assert.equal(retainedPools.length,before,'Protection failure must not open a Pool or fall back')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/** Explicit host-operator inspection/acceptance. No restore, service restart, role
|
||||
* enrollment or source/READY issuer. Populated data is accepted, not certified. */
|
||||
import assert from 'node:assert/strict'
|
||||
import path from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
import {mkdir,readdir} from 'node:fs/promises'
|
||||
import {readProtected,protectedDirectory,writeFile,withJournalLease,sha} from './operator-files.mjs'
|
||||
import {poolOptions,STARTUP_OPTIONS,assertJsDriverEnvironment} from './operator-contract.mjs'
|
||||
import {readMaintenanceRecoveryFacts,readMaintenanceAcceptanceRecords} from '../../src/services/database-maintenance-startup.js'
|
||||
import {parseMaintenanceJSON,runtimeDatabaseTargetSha256,evaluateMaintenanceAcceptances,validateMaintenanceKnownCurrent,validateMaintenanceAcceptance,MAINTENANCE_ACCEPTANCE_FORMAT,type MaintenanceKnownCurrent} from '../../src/services/database-maintenance-resolution.js'
|
||||
import {captureMigrationSchema} from '../../src/db/migration-schema-inventory.js'
|
||||
import {MIGRATION_MANIFEST,CURRENT_REQUIRED_TABLES,migrationManifestDigest,inventoryMigrationDirectory} from '../../src/db/migration-manifest.js'
|
||||
import type {Pool,PoolClient} from 'pg'
|
||||
const hex=(v:unknown):v is string=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v)
|
||||
function object(v:unknown):asserts v is Record<string,unknown>{assert(v&&typeof v==='object'&&!Array.isArray(v))}
|
||||
function exact(v:unknown,keys:string[]):asserts v is Record<string,unknown>{object(v);assert.equal(Object.keys(v).sort().join('\0'),keys.sort().join('\0'))}
|
||||
const stamp=(v:unknown):v is string=>typeof v==='string'&&Number.isFinite(Date.parse(v))&&new Date(v).toISOString()===v
|
||||
const stable=(v:unknown)=>JSON.stringify(v)
|
||||
const flags=['state-dir','transport','transport-sha256','runtime-target','runtime-target-sha256']
|
||||
const validityMs=900000
|
||||
/** Explicit local-container transport never falls back from TLS or environment.
|
||||
* Caller launch review supplies --network container:<exact reviewed PG id>;
|
||||
* this module has no Docker socket/API and claims only loopback + physical SQL proof. */
|
||||
export function maintenancePoolConfiguration(value:unknown,ca?:Buffer,caSha256?:string){
|
||||
object(value);const mode=value.mode===undefined?'tls':value.mode;assert(mode==='tls'||mode==='local-loopback')
|
||||
exact(value,mode==='local-loopback'?['format','mode','connection']:value.mode===undefined?['format','connection','tls']:['format','mode','connection','tls'])
|
||||
assert.equal(value.format,'database-maintenance-transport-v1');exact(value.connection,['host','port','user','password','database']);const connection=value.connection
|
||||
assert(typeof connection.host==='string'&&typeof connection.user==='string'&&/^[^\x00-\x1f\x7f]{1,63}$/.test(connection.user)&&typeof connection.password==='string'&&connection.password.length>0&&connection.password.length<=4096&&!connection.password.includes('\0')&&typeof connection.database==='string'&&/^[^\x00-\x1f\x7f]{1,63}$/.test(connection.database)&&typeof connection.port==='number'&&Number.isSafeInteger(connection.port)&&Number(connection.port)>0&&Number(connection.port)<=65535)
|
||||
let options,identity:unknown
|
||||
if(mode==='tls'){
|
||||
exact(value.tls,['serverName','spkiSha256']);assert(typeof value.tls.serverName==='string'&&/^[a-zA-Z0-9][a-zA-Z0-9.-]{0,252}$/.test(value.tls.serverName)&&hex(value.tls.spkiSha256));assert(ca&&hex(caSha256))
|
||||
options=poolOptions(connection,{roles:{publisher:{name:connection.user}},source:{databaseName:connection.database},tls:{...value.tls,caSha256}},ca)
|
||||
identity={mode,host:connection.host,port:connection.port,database:connection.database,tls:{...value.tls,caSha256}}
|
||||
}else{
|
||||
assert(connection.host==='127.0.0.1'||connection.host==='::1','Plaintext maintenance requires exact loopback and reviewed PostgreSQL container namespace');assert(ca===undefined&&caSha256===undefined)
|
||||
options={host:connection.host,port:connection.port,user:connection.user,password:connection.password,database:connection.database,max:1,connectionTimeoutMillis:5000,idleTimeoutMillis:5000,statement_timeout:10000,lock_timeout:5000,options:STARTUP_OPTIONS,application_name:'mmapp-maintenance-acceptance',client_encoding:'UTF8',ssl:false}
|
||||
identity={mode,host:connection.host,port:connection.port,database:connection.database}
|
||||
}
|
||||
return {options,transportBinding:{mode,endpointSha256:sha(stable(identity))} as MaintenanceKnownCurrent['transport']}
|
||||
}
|
||||
export function maintenanceApproval(value:unknown,inspectionSha256:string,inspectionCreatedAt:string){
|
||||
exact(value,['format','resolutionId','decision','actorId','inspectionSha256','issuedAt','expiresAt','managedBackendStopped','historicOutcomesRemainUnknown','automaticRestore'])
|
||||
assert(value.format==='database-maintenance-current-state-approval-v1'&&value.decision==='accept-observed-current-state'&&typeof value.resolutionId==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value.resolutionId))
|
||||
assert(typeof value.actorId==='string'&&/^[^\x00-\x1f\x7f]{1,160}$/.test(value.actorId)&&value.inspectionSha256===inspectionSha256&&stamp(value.issuedAt)&&stamp(value.expiresAt))
|
||||
const now=Date.now();assert(Date.parse(value.issuedAt)>=Date.parse(inspectionCreatedAt)&&Date.parse(value.issuedAt)<=now&&now<Date.parse(value.expiresAt)&&Date.parse(value.expiresAt)-Date.parse(value.issuedAt)<=validityMs)
|
||||
assert(value.managedBackendStopped===true&&value.historicOutcomesRemainUnknown===true&&value.automaticRestore===false)
|
||||
return value as unknown as {resolutionId:string;actorId:string;issuedAt:string;expiresAt:string}
|
||||
}
|
||||
/** Genuine observation, not a supplied snapshot factory. Owner/transaction ends
|
||||
* must be confirmed before a caller can publish an acceptance. Exclusive key
|
||||
* protects cooperative observations; stopped-backend assumption spans publication. */
|
||||
export async function observeMaintenanceCurrent(pool:Pick<Pool,'connect'>,transport:MaintenanceKnownCurrent['transport']){
|
||||
let client:PoolClient|undefined,locked=false,begun=false,bad=false,faulted=false
|
||||
const fault=()=>{faulted=true},live=()=>{assert(!faulted)}
|
||||
try{
|
||||
client=await pool.connect();client.on('error',fault);client.on('end',fault)
|
||||
const query=async(text:string,values:unknown[]=[])=>{live();const result=await client!.query(text,values);live();return result}
|
||||
await query("SET statement_timeout='10s'");await query("SET lock_timeout='5s'")
|
||||
const lease=await query('SELECT pg_catalog.pg_try_advisory_lock(8723110::bigint) AS locked');assert(lease.rows.length===1&&lease.rows[0].locked===true);locked=true
|
||||
await query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');begun=true
|
||||
for(const sql of ["SET LOCAL search_path=pg_catalog","SET LOCAL TimeZone='UTC'","SET LOCAL DateStyle='ISO, YMD'","SET LOCAL standard_conforming_strings=on","SET LOCAL statement_timeout='10s'"])await query(sql)
|
||||
const identity=await query(`SELECT c.system_identifier::text AS "systemIdentifier",d.oid::text AS "databaseOid",d.datname AS "databaseName",pg_catalog.current_setting('server_version_num')::int AS "serverVersionNum",pg_catalog.pg_is_in_recovery() AS "inRecovery" FROM pg_catalog.pg_database d CROSS JOIN pg_catalog.pg_control_system() c WHERE d.datname=pg_catalog.current_database()`)
|
||||
assert.equal(identity.rows.length,1)
|
||||
// The optional writer-profile control schema is not installed by ordinary
|
||||
// application migrations. Observe its presence in this same transaction;
|
||||
// detailed capture still refuses every other unexpected namespace.
|
||||
const control=await query("SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname='mmapp_control'")
|
||||
assert(control.rows.length<=1&&control.rows.every(row=>row.nspname==='mmapp_control'))
|
||||
const schema=await captureMigrationSchema({query:async(text,values)=>query(text,values)},{scope:{applicationNamespaces:['public'],excludedControlNamespaces:control.rows.length?['mmapp_control']:[]},deadlineAt:Date.now()+60000})
|
||||
const marker=schema.sections.relations.find(row=>row.namespace==='public'&&row.name==='schema_migrations'),column=schema.sections.columns.find(row=>row.namespace==='public'&&row.relation==='schema_migrations'&&row.name==='id'&&row.dropped===false)
|
||||
assert(marker?.kind==='r'&&marker.persistence==='p'&&marker.row_security===false&&marker.force_row_security===false&&column?.type==='text')
|
||||
for(const table of CURRENT_REQUIRED_TABLES)assert(schema.sections.relations.some(row=>row.namespace==='public'&&row.name===table&&['r','p'].includes(String(row.kind))&&row.row_security===false&&row.force_row_security===false))
|
||||
const markers=(await query('SELECT id FROM public.schema_migrations ORDER BY id LIMIT $1',[MIGRATION_MANIFEST.length+1])).rows
|
||||
assert.equal(stable(markers),stable(MIGRATION_MANIFEST.map(entry=>({id:entry.file.slice(0,-4)}))))
|
||||
const knownCurrent={transport,source:identity.rows[0],schemaSha256:schema.sha256,rawSchemaSha256:schema.observedRaw.sha256,physicalSchemaSha256:schema.observedPhysical.sha256,manifestSha256:migrationManifestDigest(),markersSha256:sha(stable(markers))}
|
||||
validateMaintenanceKnownCurrent(knownCurrent)
|
||||
const end=await query('ROLLBACK');assert.equal(end.command,'ROLLBACK');begun=false
|
||||
const unlock=await query('SELECT pg_catalog.pg_advisory_unlock(8723110::bigint) AS unlocked');assert(unlock.rows.length===1&&unlock.rows[0].unlocked===true);locked=false
|
||||
return {knownCurrent,schema}
|
||||
}catch(error){bad=true;throw error}finally{
|
||||
if(client){let cleanupFailed=false
|
||||
try{if(begun&&!faulted){const end=await client.query('ROLLBACK');assert.equal(end.command,'ROLLBACK')}if(locked&&!faulted){const end=await client.query('SELECT pg_catalog.pg_advisory_unlock(8723110::bigint) AS unlocked');assert(end.rows[0]?.unlocked===true)}}catch{cleanupFailed=true}
|
||||
try{client.off('error',fault);client.off('end',fault)}catch{cleanupFailed=true}
|
||||
try{client.release(bad||faulted||cleanupFailed)}catch{cleanupFailed=true}
|
||||
assert(!cleanupFailed&&!faulted,'Read-only maintenance owner cleanup unconfirmed')
|
||||
}
|
||||
}
|
||||
}
|
||||
async function snapshot(stateDirectory:string,target:string,records:readonly Buffer[]){
|
||||
const state=await readMaintenanceRecoveryFacts(stateDirectory);assert(state.kind==='journal'&&state.generation)
|
||||
const accepted=evaluateMaintenanceAcceptances(records,state.journal,state.vault.evidence,target)
|
||||
const unresolvedOperations=state.operations.filter(op=>op.state!=='applied'),vaultCoverage=state.vault.evidence.filter(marker=>state.vault.unresolvedFiles.includes(marker.file))
|
||||
const newUnresolved=unresolvedOperations.some(op=>!accepted.acceptedCoreIds.has(op.operationId))||state.vault.unresolvedFiles.some(file=>!accepted.acceptedVaultFiles.has(file))
|
||||
return {newUnresolved,external:{journalCoverage:{prefixBytes:state.journal.length,sha256:sha(state.journal),unresolvedOperations},vaultCoverage,generationObservedSha256:sha(state.generation),previousResolutionSha256:accepted.headSha256}}
|
||||
}
|
||||
export async function maintenanceMain(args:string[]){
|
||||
assertJsDriverEnvironment(process.env);const resolve=args[0]==='maintenance-resolve';assert(resolve||args[0]==='maintenance-inspect')
|
||||
const required=[...flags,...(resolve?['inspection','inspection-sha256','approval','approval-sha256']:['output'])],names=[...required,'ca','ca-sha256'],values=new Map<string,string>()
|
||||
assert(args.length===1+required.length*2||args.length===1+(required.length+2)*2)
|
||||
for(let i=1;i<args.length;i+=2){const name=args[i].slice(2);assert(args[i].startsWith('--')&&names.includes(name)&&!values.has(name)&&args[i+1]);values.set(name,args[i+1])}
|
||||
for(const name of required)assert(values.has(name));assert(values.has('ca')===values.has('ca-sha256'));const get=(name:string)=>values.get(name)!
|
||||
for(const name of [...values.keys()].filter(name=>name.endsWith('sha256')))assert(hex(get(name)))
|
||||
const stateDirectory=get('state-dir');await protectedDirectory(stateDirectory)
|
||||
const transport=parseMaintenanceJSON(await readProtected(get('transport'),16384,get('transport-sha256')));object(transport);const mode=transport.mode===undefined?'tls':transport.mode;assert(mode==='tls'||mode==='local-loopback');assert(values.has('ca')===(mode==='tls'))
|
||||
const ca=mode==='tls'?await readProtected(get('ca'),65536,get('ca-sha256')):undefined
|
||||
const runtime=parseMaintenanceJSON(await readProtected(get('runtime-target'),16384,get('runtime-target-sha256')));exact(runtime,['format','databaseUrl']);assert(runtime.format==='database-maintenance-runtime-target-v1'&&typeof runtime.databaseUrl==='string');const runtimeUrl=new URL(runtime.databaseUrl);assert(!runtimeUrl.username&&!runtimeUrl.password)
|
||||
const target=runtimeDatabaseTargetSha256(runtime.databaseUrl),{options,transportBinding}=maintenancePoolConfiguration(transport,ca,mode==='tls'?get('ca-sha256'):undefined)
|
||||
exact(transport.connection,['host','port','user','password','database']);assert.equal(decodeURIComponent(runtimeUrl.pathname.slice(1)),transport.connection.database)
|
||||
const records=await readMaintenanceAcceptanceRecords(stateDirectory);if(resolve)assert(records.length<64,'Maintenance acceptance history bound reached');const currentState=await snapshot(stateDirectory,target,records),before=currentState.external;if(resolve)assert(currentState.newUnresolved,'No new unresolved history to accept')
|
||||
await inventoryMigrationDirectory(path.join(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))),'drizzle'))
|
||||
let inspection:Record<string,unknown>|undefined,approved:ReturnType<typeof maintenanceApproval>|undefined
|
||||
if(resolve){inspection=parseMaintenanceJSON(await readProtected(get('inspection'),67108864,get('inspection-sha256'))) as Record<string,unknown>;exact(inspection,['format','createdAt','expiresAt','runtimeTargetSha256','acceptanceRequired','external','knownCurrent','schema']);assert(inspection.format==='database-maintenance-current-state-inspection-v1'&&stamp(inspection.createdAt)&&stamp(inspection.expiresAt)&&Date.parse(inspection.createdAt)<=Date.now()&&Date.now()<Date.parse(inspection.expiresAt)&&Date.parse(inspection.expiresAt)-Date.parse(inspection.createdAt)<=validityMs&&inspection.runtimeTargetSha256===target&&inspection.acceptanceRequired===true);validateMaintenanceKnownCurrent(inspection.knownCurrent);assert.equal(stable(inspection.external),stable(before));approved=maintenanceApproval(parseMaintenanceJSON(await readProtected(get('approval'),16384,get('approval-sha256'))),get('inspection-sha256'),inspection.createdAt)}
|
||||
const {default:pg}=await import('pg');assertJsDriverEnvironment(process.env)
|
||||
const runProof=async()=>{const pool=new pg.Pool(options);let failed=false;pool.on('error',()=>{failed=true});try{const result=await observeMaintenanceCurrent(pool,transportBinding);await pool.end();assert(!failed);return result}catch(error){try{await pool.end()}catch{/* preserve refusal */}throw error}}
|
||||
if(!resolve){const result=await runProof();assert.equal(stable((await snapshot(stateDirectory,target,await readMaintenanceAcceptanceRecords(stateDirectory))).external),stable(before));const nowMs=Date.now(),now=new Date(nowMs).toISOString(),out={format:'database-maintenance-current-state-inspection-v1',createdAt:now,expiresAt:new Date(nowMs+validityMs).toISOString(),runtimeTargetSha256:target,acceptanceRequired:currentState.newUnresolved,external:before,...result};const raw=Buffer.from(stable(out));assert(raw.length<=67108864);await writeFile(get('output'),raw,{flag:'wx',mode:0o600});return {event:'database-maintenance-inspection',inspectionSha256:sha(raw),unresolvedCoreOperations:before.journalCoverage.unresolvedOperations.length,unresolvedVaultFiles:before.vaultCoverage.length,state:currentState.newUnresolved?'inspection-required':before.previousResolutionSha256?'historically-accepted':'no-unresolved-history',historicalAcceptanceHeadSha256:before.previousResolutionSha256,humanApprovalRequired:currentState.newUnresolved,knownCurrentObserved:true,archiveDataEquivalenceProved:false,historicOutcomesChanged:false,automaticRestore:false}}
|
||||
const resolutionDirectory=path.join(stateDirectory,'maintenance-acceptances');await mkdir(resolutionDirectory,{recursive:true,mode:0o700});await protectedDirectory(resolutionDirectory)
|
||||
return withJournalLease(resolutionDirectory,async()=>{
|
||||
// Existing immutable records are reread while our proven O_EXCL lease is held.
|
||||
// Runtime reader refuses this lease; never infer stale ownership from PID/time.
|
||||
const expected=records.map(raw=>{const v=parseMaintenanceJSON(raw) as {resolutionId:string};return v.resolutionId+'.json'}).sort()
|
||||
assert.equal((await readdir(resolutionDirectory)).sort().join('\0'),[...expected,'publisher-lease.lock'].sort().join('\0'))
|
||||
for(let i=0;i<records.length;i++){const value=parseMaintenanceJSON(records[i]) as {resolutionId:string};assert.equal(sha(await readProtected(path.join(resolutionDirectory,value.resolutionId+'.json'),1048576)),sha(records[i]))}
|
||||
const observed=await runProof();assert.equal(stable(observed.knownCurrent),stable(inspection!.knownCurrent));assert.equal(stable(observed.schema),stable(inspection!.schema))
|
||||
assert.equal(stable((await snapshot(stateDirectory,target,records)).external),stable(before));assert(Date.now()<Date.parse(approved!.expiresAt)&&Date.now()<Date.parse(inspection!.expiresAt as string))
|
||||
const record={format:MAINTENANCE_ACCEPTANCE_FORMAT,resolutionId:approved!.resolutionId,decision:'accept-observed-current-state',historicOutcomeChanged:false,automaticRestore:false,actor:{kind:'host-operator',id:approved!.actorId},approvalSha256:get('approval-sha256'),inspectionSha256:get('inspection-sha256'),approvedAt:approved!.issuedAt,approvalExpiresAt:approved!.expiresAt,acceptedAt:new Date().toISOString(),runtimeTargetSha256:target,...before,knownCurrent:observed.knownCurrent}
|
||||
validateMaintenanceAcceptance(record);const raw=Buffer.from(stable(record));assert(raw.length<=1048576,'Maintenance acceptance byte bound exceeded');await writeFile(path.join(resolutionDirectory,record.resolutionId+'.json'),raw,{flag:'wx',mode:0o600})
|
||||
return {event:'database-maintenance-current-state-accepted',outcome:'committed',resolutionId:record.resolutionId,acceptanceSha256:sha(raw),archiveDataEquivalenceProved:false,historicOutcomesChanged:false,automaticRestore:false}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/** Explicit operator/lifecycle read component. No startup, grants or READY. */
|
||||
import assert from 'node:assert/strict'
|
||||
import {lstat} from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import {protectedDirectory,readProtected} from './operator-files.mjs'
|
||||
import {createDatabaseSourceBindingVerifier,readDatabaseSourceBindingRegistry,
|
||||
type DatabaseSourceDeploymentPin} from '../../src/db/database-source-binding.js'
|
||||
import type {DatabaseSourceObservation,DatabaseSourceObservationOptions} from '../../src/db/database-source-observation.js'
|
||||
import type {ProbeStorageSql} from '../../src/services/server-probe-journal-maintenance.js'
|
||||
|
||||
const hex=(value:unknown):value is string=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value)
|
||||
function fence(options:DatabaseSourceObservationOptions){
|
||||
assert(Number.isSafeInteger(options.deadlineAt)&&Date.now()<options.deadlineAt&&!options.signal?.aborted,'Protected source lifetime refused')
|
||||
}
|
||||
async function identity(filename:string,content=false){
|
||||
const s=await lstat(filename,{bigint:true})
|
||||
// BigInt identity is never narrowed to Number. Content changes/permission
|
||||
// changes are checked around the whole verified read, not just open().
|
||||
return [s.dev,s.ino,s.mode,s.uid,s.gid,s.nlink,...(content?[s.size,s.mtimeNs,s.ctimeNs]:[])].map(String).join(':')
|
||||
}
|
||||
async function protectedState(pin:DatabaseSourceDeploymentPin){
|
||||
// Proven POSIX protection refuses unsupported Windows ACL BEFORE file reads.
|
||||
await protectedDirectory(pin.registryDirectory)
|
||||
const directories:Record<string,string>={}
|
||||
for(let p=pin.registryDirectory;;p=path.dirname(p)){
|
||||
directories[p]=await identity(p,p===pin.registryDirectory)
|
||||
if(p===path.dirname(p))break
|
||||
}
|
||||
const names=['operator-public.pem','head-'+pin.currentHeadSha256+'.json']
|
||||
const content:Record<string,Buffer>={},identities:Record<string,string>={}
|
||||
const collect=async(name:string,maximum:number,expected?:string)=>{
|
||||
const filename=path.join(pin.registryDirectory,name)
|
||||
const before=await identity(filename,true)
|
||||
content[name]=await readProtected(filename,maximum,expected)
|
||||
assert.equal(await identity(filename,true),before,'Protected source file changed during read')
|
||||
identities[name]=before
|
||||
}
|
||||
await collect(names[0],4096)
|
||||
await collect(names[1],8192,pin.currentHeadSha256)
|
||||
const head=JSON.parse(new TextDecoder('utf8',{fatal:true}).decode(content[names[1]]))
|
||||
assert(hex(head?.body?.bindingSha256),'Pinned source binding digest required')
|
||||
const bindingName='binding-'+head.body.bindingSha256+'.json'
|
||||
await collect(bindingName,8192,head.body.bindingSha256)
|
||||
await protectedDirectory(pin.registryDirectory)
|
||||
for(const [directory,wanted]of Object.entries(directories))assert.equal(await identity(directory,directory===pin.registryDirectory),wanted,'Protected source directory changed')
|
||||
return {directories,identities,content}
|
||||
}
|
||||
function same(before:Awaited<ReturnType<typeof protectedState>>,after:Awaited<ReturnType<typeof protectedState>>){
|
||||
assert.deepEqual(after.directories,before.directories,'Protected source directory drift')
|
||||
assert.deepEqual(after.identities,before.identities,'Protected source identity/mode drift')
|
||||
assert.deepEqual(after.content,before.content,'Protected source content drift')
|
||||
}
|
||||
function captured(raw:DatabaseSourceDeploymentPin){
|
||||
const pin=Object.freeze({...raw})
|
||||
// Existing strict deployment schema rejects unknown/malformed pin fields.
|
||||
const verifier=createDatabaseSourceBindingVerifier(pin)
|
||||
return {pin,verifier}
|
||||
}
|
||||
|
||||
/** Signature/head/source DATA plus actual POSIX protection of the pinned files.
|
||||
* The supplied pin still needs independent deployment approval; protection
|
||||
* cannot distinguish a restored clone/generation or authenticate all writers. */
|
||||
export async function readProtectedDatabaseSourceBindingRegistry(raw:DatabaseSourceDeploymentPin,options:DatabaseSourceObservationOptions){
|
||||
const {pin}=captured(raw);const lifetime={...options};fence(lifetime)
|
||||
const before=await protectedState(pin);fence(lifetime)
|
||||
const result=await readDatabaseSourceBindingRegistry(pin);fence(lifetime)
|
||||
const after=await protectedState(pin);fence(lifetime);same(before,after)
|
||||
return Object.freeze({...result,platformProtectionVerified:true as const,
|
||||
generationAuthenticated:false as const,allWriterDrainAuthenticated:false as const,readyAuthorized:false as const})
|
||||
}
|
||||
|
||||
/** Same-facade source verification is retained; filesystem JSON is never a
|
||||
* source observation, writer capability or independent generation authority. */
|
||||
export function createProtectedDatabaseSourceBindingVerifier(raw:DatabaseSourceDeploymentPin){
|
||||
const {pin,verifier}=captured(raw)
|
||||
return Object.freeze({async verify(sql:ProbeStorageSql,observation:DatabaseSourceObservation,options:DatabaseSourceObservationOptions){
|
||||
const lifetime={...options};fence(lifetime)
|
||||
const before=await protectedState(pin);fence(lifetime)
|
||||
const result=await verifier.verify(sql,observation,lifetime);fence(lifetime)
|
||||
const after=await protectedState(pin);fence(lifetime);same(before,after)
|
||||
return Object.freeze({...result,platformProtectionVerified:true as const,
|
||||
generationAuthenticated:false as const,allWriterDrainAuthenticated:false as const,readyAuthorized:false as const})
|
||||
}})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/** Genuine Ed25519/files; POSIX permission metadata is modeled on Windows.
|
||||
* No SQL, service, deployment or real POSIX acceptance is claimed. */
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
import crypto from 'node:crypto'
|
||||
import {createRequire} from 'node:module'
|
||||
import type {Stats,BigIntStats} from 'node:fs'
|
||||
import type {DatabaseSourceDeploymentPin} from '../../src/db/database-source-binding.ts'
|
||||
import type * as ProtectedApi from './protected-source-registry.mts'
|
||||
import {runInNewContext} from 'node:vm'
|
||||
import {readProtectedDatabaseSourceBindingRegistry} from './protected-source-registry.mts'
|
||||
import {readDatabaseSourceBindingRegistry,sourceBindingBytes,sourceBindingCanonical,sourceBindingSha256,DATABASE_SOURCE_SIGNATURE_DOMAINS} from '../../src/db/database-source-binding.ts'
|
||||
const require=createRequire(import.meta.url),ts=require('typescript')
|
||||
const helperSource=await fs.readFile(new URL('./operator-files.mjs',import.meta.url),'utf8')
|
||||
const source=await fs.readFile(new URL('./protected-source-registry.mts',import.meta.url),'utf8')
|
||||
const deadline=()=>({deadlineAt:Date.now()+10000})
|
||||
function compile(text:string){return ts.transpileModule(text,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,esModuleInterop:true}}).outputText}
|
||||
function evaluate<T=Record<string,unknown>>(text:string,imports:Record<string,unknown>,processModel:unknown=process):T{
|
||||
const exports:Record<string,unknown>={}
|
||||
runInNewContext(compile(text),{exports,require:(name:string)=>imports[name]??require(name),process:processModel,Buffer,TextDecoder,console})
|
||||
return exports as T
|
||||
}
|
||||
interface Fixture {pin:DatabaseSourceDeploymentPin;faults:Record<string,boolean>;api:typeof ProtectedApi;verificationReads:()=>number;root:string}
|
||||
async function own(work:(f:Fixture)=>Promise<void>){
|
||||
const root=await fs.mkdtemp(path.join(os.tmpdir(),'mm-protected-source-'))
|
||||
try{
|
||||
const key=crypto.generateKeyPairSync('ed25519'),installationId=crypto.randomUUID()
|
||||
const signed=(body:unknown,domain:string)=>sourceBindingBytes({body,signature:crypto.sign(null,Buffer.from(domain+sourceBindingCanonical(body)),key.privateKey).toString('base64url')})
|
||||
const body={format:'database-source-binding-v1',installationId,sourceIdentityId:crypto.randomUUID(),generation:crypto.randomUUID(),ordinal:1,
|
||||
source:{systemIdentifier:'123',databaseOid:16384,databaseName:'synthetic',sessionUser:'capture',currentUser:'capture',inRecovery:false,serverVersionNum:180004}}
|
||||
const binding=signed(body,DATABASE_SOURCE_SIGNATURE_DOMAINS.binding),bindingSha256=sourceBindingSha256(binding)
|
||||
const head=signed({format:'database-source-registry-head-v1',installationId,sourceIdentityId:body.sourceIdentityId,generation:body.generation,ordinal:1,bindingSha256,previousHeadSha256:null},DATABASE_SOURCE_SIGNATURE_DOMAINS.head)
|
||||
const pin={registryDirectory:root,installationId,publicKeySpkiSha256:sourceBindingSha256(key.publicKey.export({format:'der',type:'spki'})),currentHeadSha256:sourceBindingSha256(head)}
|
||||
await fs.writeFile(path.join(root,'operator-public.pem'),key.publicKey.export({format:'pem',type:'spki'}),{mode:0o600})
|
||||
await fs.writeFile(path.join(root,'head-'+pin.currentHeadSha256+'.json'),head,{mode:0o600})
|
||||
await fs.writeFile(path.join(root,'binding-'+bindingSha256+'.json'),binding,{mode:0o600})
|
||||
const faults={modeFile:false,modeDirectory:false,symlink:false,replacement:false,lateMode:false,changed:false};let verificationReads=0
|
||||
const metadata=(s:Stats|BigIntStats,filename:string,big:boolean)=>{
|
||||
const n=(value:number)=>big?BigInt(value):value,isRoot=filename===root,isFile=s.isFile()
|
||||
const mode=(isFile?0o100600:0o40700)|((isFile&&faults.modeFile||isRoot&&faults.modeDirectory||isFile&&faults.lateMode&&faults.changed)?0o022:0)
|
||||
return {...s,mode:n(mode),uid:n(1000),gid:n(1000),ino:faults.replacement&&faults.changed&&isFile?(typeof s.ino==='bigint'?s.ino+1n:s.ino+1):s.ino,
|
||||
isFile:()=>isFile,isDirectory:()=>s.isDirectory(),isSymbolicLink:()=>faults.symlink&&isRoot}
|
||||
}
|
||||
const modelFs={...fs,lstat:async(filename:string,options?:{bigint?:boolean})=>metadata(await fs.lstat(filename,{bigint:options?.bigint===true}),filename,options?.bigint===true),open:async(filename:string,flags:number)=>{
|
||||
const file=await fs.open(filename,flags)
|
||||
return {stat:async()=>metadata(await file.stat(),filename,false),read:file.read.bind(file),close:file.close.bind(file)}
|
||||
}}
|
||||
const processModel={platform:'linux',getuid:()=>1000}
|
||||
const proven=evaluate(helperSource,{'node:fs/promises':modelFs},processModel)
|
||||
const bindingModule={readDatabaseSourceBindingRegistry:async(p:DatabaseSourceDeploymentPin)=>{
|
||||
verificationReads++;const result=await readDatabaseSourceBindingRegistry(p);faults.changed=true;return result
|
||||
},createDatabaseSourceBindingVerifier:()=>({verify:async()=>{verificationReads++;faults.changed=true;return {genuineSourceFactsMatched:true,generationAuthenticated:false,readyAuthorized:false}}})}
|
||||
const api=evaluate<typeof ProtectedApi>(source,{'node:fs/promises':modelFs,'./operator-files.mjs':proven,'../../src/db/database-source-binding.js':bindingModule},processModel)
|
||||
await work({pin,faults,api,verificationReads:()=>verificationReads,root})
|
||||
}finally{const rel=path.relative(os.tmpdir(),root);assert(rel&&!rel.startsWith('..')&&!path.isAbsolute(rel));await fs.rm(root,{recursive:true,force:true})}
|
||||
}
|
||||
|
||||
test('unsupported Windows refuses before registry signature reads',{skip:process.platform!=='win32'},async()=>{
|
||||
await assert.rejects(readProtectedDatabaseSourceBindingRegistry({registryDirectory:path.resolve('not-created-protected-source'),installationId:crypto.randomUUID(),publicKeySpkiSha256:'a'.repeat(64),currentHeadSha256:'b'.repeat(64)},deadline()),/Windows ACL/)
|
||||
})
|
||||
test('proven POSIX helpers plus genuine signature/head bytes retain false generation/READY',()=>own(async f=>{
|
||||
const value=await f.api.readProtectedDatabaseSourceBindingRegistry(f.pin,deadline())
|
||||
assert.equal(value.platformProtectionVerified,true);assert.equal(value.generationAuthenticated,false);assert.equal(value.allWriterDrainAuthenticated,false);assert.equal(value.readyAuthorized,false)
|
||||
assert.equal(value.headSha256,f.pin.currentHeadSha256);assert.equal(f.verificationReads(),1)
|
||||
}))
|
||||
for(const fault of ['modeFile','modeDirectory','symlink'])test(fault+' refuses before cryptographic registry read',()=>own(async f=>{
|
||||
f.faults[fault]=true;await assert.rejects(f.api.readProtectedDatabaseSourceBindingRegistry(f.pin,deadline()));assert.equal(f.verificationReads(),0)
|
||||
}))
|
||||
for(const fault of ['replacement','lateMode'])test(fault+' during genuine signature verification refuses protected output',()=>own(async f=>{
|
||||
f.faults[fault]=true;await assert.rejects(f.api.readProtectedDatabaseSourceBindingRegistry(f.pin,deadline()));assert.equal(f.verificationReads(),1)
|
||||
}))
|
||||
test('expired/aborted lifetime refuses before filesystem or source access',()=>own(async f=>{
|
||||
const abort=new AbortController();abort.abort()
|
||||
for(const options of [{deadlineAt:Date.now()-1},{deadlineAt:Date.now()+1000,signal:abort.signal}])await assert.rejects(f.api.readProtectedDatabaseSourceBindingRegistry(f.pin,options))
|
||||
assert.equal(f.verificationReads(),0)
|
||||
}))
|
||||
test('source verifier drift cannot grant protected output',()=>own(async f=>{
|
||||
f.faults.lateMode=true
|
||||
const verify=f.api.createProtectedDatabaseSourceBindingVerifier(f.pin).verify
|
||||
await assert.rejects(verify({} as Parameters<typeof verify>[0],{} as Parameters<typeof verify>[1],deadline()))
|
||||
assert.equal(f.verificationReads(),1)
|
||||
}))
|
||||
@@ -141,14 +141,14 @@ async function preflightCurrentProfile(pool,{deadlineAt=Date.now()+120000}={}){
|
||||
const first=await ownedContext(s,installation,PUBLISHER,'publisher-read')
|
||||
if(canonical(sourceCoordinates(first))!==canonical(installation.expectedSource))throw Error('Pinned preflight source mismatch')
|
||||
const facade=Object.freeze({query:(sql,values)=>s.query(sql,values)}),options={deadlineAt}
|
||||
const observation=await captureDatabaseSourceObservation(facade,options);await sourceVerifier.verify(facade,observation,options)
|
||||
const observation=await captureDatabaseSourceObservation(facade,options),initialSource=await sourceVerifier.verify(facade,observation,options)
|
||||
const security=await protectedStructure(s,installation),catalogue=await collectFixedCatalogue(s)
|
||||
const current=await readCurrent(s,installation)
|
||||
if(!current)throw Error('Current signed policy required for preflight')
|
||||
const catalogSha256=sha(canonical({base:catalogue.catalogSha256,security:security.sha256})),manifestSha256=migrationManifestDigest()
|
||||
if(approval.expiresAt<=Date.now()||approval.manifestSha256!==manifestSha256||approval.definitionSha256!==catalogue.definitionSha256||approval.catalogSha256!==catalogSha256)throw Error('Independent preflight approval differs from actual source44 catalogue')
|
||||
if(canonical(current.policy.source)!==canonical(sourceCoordinates(first))||canonical(current.policy.roles)!==canonical(security.roles)||current.policy.manifestSha256!==manifestSha256||current.policy.definitionSha256!==catalogue.definitionSha256||current.policy.catalogSha256!==catalogSha256)throw Error('Current signed policy differs from actual source/roles/catalogue')
|
||||
await sourceVerifier.verify(facade,observation,options);await assertDatabaseSourceObservationCurrent(facade,observation,options)
|
||||
const terminalSource=await sourceVerifier.verify(facade,observation,options);await assertDatabaseSourceObservationCurrent(facade,observation,options)
|
||||
sameOwned(first,await ownedContext(s,installation,PUBLISHER,'publisher-read'))
|
||||
// Recheck the live interval after the potentially lengthy catalogue walk.
|
||||
const clock=(await s.query('SELECT (EXTRACT(EPOCH FROM pg_catalog.clock_timestamp())*1000)::bigint::text AS now')).rows
|
||||
@@ -157,7 +157,7 @@ async function preflightCurrentProfile(pool,{deadlineAt=Date.now()+120000}={}){
|
||||
if(approval.expiresAt<=Date.now())throw Error('Preflight approval expired before terminal')
|
||||
const terminal=await s.query('ROLLBACK');if(terminal.command!=='ROLLBACK')throw Error('Preflight terminal unconfirmed');s.begun(false)
|
||||
return Object.freeze({state:'matched',epoch:current.epoch,headSha256:current.headSha256,notAfter:current.policy.notAfter,
|
||||
sourceMatched:true,rolesMatched:true,catalogueMatched:true,writerAuthorized:false,generationAuthenticated:false,readyAuthorized:false})
|
||||
sourceMatched:true,rolesMatched:true,catalogueMatched:true,platformProtectionVerified:initialSource?.platformProtectionVerified===true&&terminalSource?.platformProtectionVerified===true,writerAuthorized:false,generationAuthenticated:false,readyAuthorized:false})
|
||||
})
|
||||
}
|
||||
async function boundedPending(filename){
|
||||
|
||||
Reference in new issue
Block a user