Files
MikrotikManager/backend/scripts/writer-profile/protected-source-registry.mts
T

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})
}})
}