85 lines
5.1 KiB
TypeScript
85 lines
5.1 KiB
TypeScript
/** 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})
|
|
}})
|
|
}
|