// Kill fixture for FORWARD HTTPS durability storage v3 child-process tests.
// Usage: node forward-https-storage-kill-fixture.mjs <mode> <baseDir>
// Modes:
// setup — create the four quota roots, open quota+target store, create one
// session, print the minted identities as JSON, exit cleanly.
// crash — reopen, append a durable FPR9 tombstone payload directly through
// the child store WAL, then process.exit(2) BEFORE any in-memory
// prune application, simulating a crash after complete fsync.
// verify — reopen fresh and print the recovered slot/head state as JSON;
// the durable tombstone must be applied exactly once.
import fs from 'node:fs/promises'
import path from 'node:path'
import b4a from 'b4a'
import {
openForwardHttpsAggregateQuotaV3,
mintForwardHttpsAggregateQuotaCapabilitiesV3,
closeForwardHttpsAggregateQuotaV3,
beginForwardHttpsAggregateQuotaRecoveryV3,
finishForwardHttpsAggregateQuotaRecoveryV3,
initializeForwardHttpsAggregateQuotaV3,
encodeForwardHttpsRetentionPrunedV3
} from '../forward-https-replay-journal-v4.js'
import {
openForwardHttpsTargetStoreV3,
acceptForwardedHttpsTargetTurnV3,
forwardHttpsTargetStoreV3Status,
closeForwardHttpsTargetStoreV3
} from '../forward-https-target-store-v3.js'
const [mode, base] = process.argv.slice(2)
if (!mode || !base) {
console.error('usage: fixture <setup|crash|verify> <baseDir>')
process.exit(64)
}
function fixed (byte) {
return b4a.alloc(32, byte)
}
async function main () {
const roots = {}
for (const name of ['source-replay', 'target-replay', 'source-store', 'target-store']) {
roots[name] = path.join(base, name)
await fs.mkdir(roots[name], { recursive: true, mode: 0o700 })
}
const quotaAuthority = await openForwardHttpsAggregateQuotaV3({
sourceReplayRoot: roots['source-replay'],
targetReplayRoot: roots['target-replay'],
sourceStoreRoot: roots['source-store'],
targetStoreRoot: roots['target-store'],
maximumDurableBytesPerStore: 8589934592,
maximumForwardStorageBytesAggregate: 17179869184,
monotonicMillis: () => Date.now(),
callbackTimeoutMillis: 15000,
faultInjector: null
})
const capabilities = mintForwardHttpsAggregateQuotaCapabilitiesV3(quotaAuthority)
const id = fixed(0x61)
const sink = beginForwardHttpsAggregateQuotaRecoveryV3(capabilities.targetStoreQuotaCapability)
const options = {
root: roots['target-store'],
replayJournalAuthority: Object.freeze({}),
targetStoreQuotaCapability: capabilities.targetStoreQuotaCapability,
targetQuotaRecoverySink: sink,
wireV3AbiHash: fixed(0x44),
privateIpcV4Hash: fixed(0x45),
signedLaunchTopologyHash: fixed(0x46),
storeId: fixed(0x62),
mapGeneration: 1n,
ownerFenceTokenHash: fixed(0x63),
durabilityContinuityHash: fixed(0x64),
targetSignerPublicKey: fixed(0x47),
targetSignerDescriptorSequence: 1n,
targetSignerDescriptorHash: fixed(0x48),
signResult: async () => b4a.alloc(64),
createResponderState: () => ({}),
advanceResponderIngress: () => {},
advanceResponderOutcome: () => {},
atRestKey: fixed(0x49),
epochSeconds: () => 1000000,
monotonicMillis: () => Date.now()
}
const store = await openForwardHttpsTargetStoreV3(options)
const targetFinal = await finishForwardHttpsAggregateQuotaRecoveryV3(sink)
if (mode === 'setup') {
// The gate requires an OPEN quota authority: finish the other role's
// recovery and initialize before the first operation.
const sourceSink = beginForwardHttpsAggregateQuotaRecoveryV3(capabilities.sourceStoreQuotaCapability)
const sourceFinal = await finishForwardHttpsAggregateQuotaRecoveryV3(sourceSink)
await initializeForwardHttpsAggregateQuotaV3(quotaAuthority, {
sourceRecoveryFinalState: sourceFinal,
targetRecoveryFinalState: targetFinal
})
const opened = await acceptForwardedHttpsTargetTurnV3(store, { stableSessionId: id, body: b4a.alloc(4, 0x65) })
console.log(JSON.stringify({ walSequence: opened.walSequence.toString() }))
await closeForwardHttpsTargetStoreV3(store)
await closeForwardHttpsAggregateQuotaV3(quotaAuthority)
return
}
if (mode === 'crash') {
// Durable FPR9 tombstone through the child WAL, then die before in-memory apply.
const slot = store.slots.get(b4a.toString(id, 'hex'))
const payload = encodeForwardHttpsRetentionPrunedV3({
role: 'TARGET_STORE',
stableSessionId: id,
priorSessionRevision: slot.priorRevision,
pruneEpochSeconds: 60,
trustedEpochHighWatermark: 60,
expiresAtEpoch: 0,
recoveryGraceUntilEpoch: 0,
removedOrdinaryLogicalBytes: slot.registry.removedLogicalBytes(),
chargeEntryCount: slot.registry.count,
beforeAuthorityBitmap: 0,
allocationDisposition: 1,
terminalSlotState: 1,
chargeEntryBuffers: slot.registry.entriesAscending(),
authorityCommitments: Array.from({ length: 10 }, () => b4a.alloc(32))
})
await store.store.appendAndApply({
type: 118,
transactionId: b4a.alloc(32, 0x66),
virtualBucket: 0,
payload
}, () => {})
process.exit(2)
}
if (mode === 'verify') {
const status = forwardHttpsTargetStoreV3Status(store)
const slot = store.slots.get(b4a.toString(id, 'hex'))
console.log(JSON.stringify({
state: status.state,
walHeadSequence: status.walHeadSequence.toString(),
unconsumedSlots: status.unconsumedSlots,
slotCapacity: status.slotCapacity,
slotState: slot ? slot.state : null,
prunedReleased: slot ? slot.prunedReleased : null
}))
await closeForwardHttpsTargetStoreV3(store)
await closeForwardHttpsAggregateQuotaV3(quotaAuthority)
return
}
console.error(`unknown mode ${mode}`)
process.exit(64)
}
main().catch(error => {
console.error(error && error.stack ? error.stack : String(error))
process.exit(1)
})