deadbyte-mcp 0.15.47 → 0.15.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/MANIFEST.SHA256 +111 -48
  2. package/PROVENANCE.json +1 -1
  3. package/README.txt +18 -18
  4. package/RELEASE.json +3 -3
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +2 -2
  7. package/bench/r121-epoch-readhead.mjs +54 -0
  8. package/bench/r124-epoch-predecessor.mjs +53 -0
  9. package/bin/deadbyte-epoch-lock-r85.node +0 -0
  10. package/controller/deadbyte-process-policy.json +4 -4
  11. package/docs/HARNESS2-ADAPTATION-20261020.md +24 -0
  12. package/docs/R128-RELEASE-CANDIDATE.md +26 -0
  13. package/docs/R75-RECOVERY-HANDOFF.md +16 -0
  14. package/docs/R85-EPOCH-LOCK-BINARY.json +13 -0
  15. package/native/deadbyte-epoch-lock-r85.cpp +241 -0
  16. package/package.json +1 -1
  17. package/scripts/build-epoch-lock-r85.ps1 +34 -0
  18. package/scripts/deploy-r48.mjs +3 -1
  19. package/scripts/npm-release-r48.mjs +18 -4
  20. package/scripts/r75-npm-bin-integrity-gate.mjs +33 -0
  21. package/scripts/release-manifest.mjs +1 -1
  22. package/scripts/verify-authority-epoch-history.mjs +38 -0
  23. package/scripts/verify-r70-full-regression.ps1 +12 -2
  24. package/src/activation-authority-proof-r75.mjs +67 -0
  25. package/src/activation-continuity-engine-r65.mjs +17 -10
  26. package/src/activation-preflight-r65.mjs +13 -0
  27. package/src/agent-eval-r42.mjs +30 -0
  28. package/src/authority-continuity-r65.mjs +442 -29
  29. package/src/autonomous-local-supervisor.mjs +77 -47
  30. package/src/deadbyte-cli.mjs +73 -46
  31. package/src/epoch-lock-r85.mjs +48 -0
  32. package/src/foreground-marker-cas-win32.mjs +284 -0
  33. package/src/foreground-process-identity.mjs +3 -3
  34. package/src/privileged-install-trust-r89.mjs +196 -0
  35. package/src/remote-live-log.mjs +15 -8
  36. package/src/remote-log-decode-r60.mjs +65 -0
  37. package/src/remote-session-guardian.mjs +13 -12
  38. package/src/runtime-update.mjs +5 -22
  39. package/src/version.mjs +1 -1
  40. package/src/windows-powershell-environment.mjs +24 -0
  41. package/test/autonomous-local-supervisor.test.mjs +1 -1
  42. package/test/cli-entrypoint.test.mjs +1 -1
  43. package/test/harness2-eval-context-isolation.test.mjs +90 -0
  44. package/test/helpers/r75-authority-proof.mjs +9 -0
  45. package/test/r100-posix-owned-lock-race.test.mjs +72 -0
  46. package/test/r101-acl-probe-environment.test.mjs +51 -0
  47. package/test/r106-trusted-powershell-spawn.test.mjs +53 -0
  48. package/test/r107-updater-powershell-path.test.mjs +77 -0
  49. package/test/r108-remote-powershell-path.test.mjs +59 -0
  50. package/test/r110-supervisor-powershell-path.test.mjs +28 -0
  51. package/test/r111-supervisor-marker-cas.test.mjs +116 -0
  52. package/test/r112-supervisor-claim-cas.test.mjs +63 -0
  53. package/test/r113-supervisor-cleanup-terminality.test.mjs +100 -0
  54. package/test/r114-supervisor-absence-proof.test.mjs +45 -0
  55. package/test/r115-posix-continuity-entrypoint.test.mjs +17 -0
  56. package/test/r116-authority-crash-matrix.test.mjs +83 -0
  57. package/test/r117-concurrent-orphan-recovery.test.mjs +54 -0
  58. package/test/r118-cross-process-orphan-recovery.test.mjs +99 -0
  59. package/test/r119-recovery-crash-after-commit.test.mjs +91 -0
  60. package/test/r120-authority-history-audit.test.mjs +103 -0
  61. package/test/r121-signed-transition-invariant.test.mjs +132 -0
  62. package/test/r122-epoch-bounded-read.test.mjs +58 -0
  63. package/test/r123-recovery-bounded-read.test.mjs +64 -0
  64. package/test/r124-live-log-integrity.test.mjs +66 -0
  65. package/test/r124-live-log-tail-boundaries.test.mjs +57 -0
  66. package/test/r124-signed-predecessor-shape.test.mjs +66 -0
  67. package/test/r125-restore-binding-types.test.mjs +63 -0
  68. package/test/r126-restore-policy-envelope.test.mjs +89 -0
  69. package/test/r127-restore-append-policy-boundary.test.mjs +25 -0
  70. package/test/r33-closeout-regression.test.mjs +2 -1
  71. package/test/r54-offline-predecessor-self-exclusion.test.mjs +2 -2
  72. package/test/r55-offline-predecessor-guardian-family.test.mjs +2 -2
  73. package/test/r56-rollback-launcher-restore.test.mjs +2 -2
  74. package/test/r57-core-startup-race.test.mjs +2 -2
  75. package/test/r58-concurrent-recovery.test.mjs +10 -7
  76. package/test/r58-stale-owner.test.mjs +5 -6
  77. package/test/r59-tool-surface-compat.test.mjs +1 -1
  78. package/test/r65-activation-orchestration.test.mjs +2 -1
  79. package/test/r65-activation-preflight.test.mjs +7 -6
  80. package/test/r65-authority-epoch-recovery.test.mjs +3 -1
  81. package/test/r65-authority-epoch-store.test.mjs +9 -6
  82. package/test/r65-authority-restored-safe.test.mjs +2 -1
  83. package/test/r65-deploy-preflight.test.mjs +13 -14
  84. package/test/r65-epoch-native-activation-integration.test.mjs +5 -4
  85. package/test/r65-legacy-authority-migration.test.mjs +2 -1
  86. package/test/r67-npm-cross-volume.test.mjs +9 -2
  87. package/test/r71-full-verifier-artifact.test.mjs +21 -15
  88. package/test/r71-isolated-slot-rollback-restore.test.mjs +3 -1
  89. package/test/r71-release-identity.test.mjs +3 -1
  90. package/test/r74-kernel-lock.test.mjs +12 -6
  91. package/test/r74-prepublish-contract.test.mjs +3 -1
  92. package/test/r75-activation-authority-proof.test.mjs +46 -0
  93. package/test/r75-authority-evidence.test.mjs +77 -0
  94. package/test/r76-pinned-marker-cas.test.mjs +78 -0
  95. package/test/r77-epoch-grant-cas.test.mjs +51 -0
  96. package/test/r78-parent-junction-cas.test.mjs +62 -0
  97. package/test/r79-orphan-revocation-reconcile.test.mjs +88 -0
  98. package/test/r79-windows-fsync-regression.test.mjs +45 -0
  99. package/test/r80-durable-epoch-commit.test.mjs +76 -0
  100. package/test/r82-epoch-lock-owned-cas.test.mjs +93 -0
  101. package/test/r83-epoch-lock-fileid-cas.test.mjs +92 -0
  102. package/test/r84-epoch-lock-created-fileid.test.mjs +112 -0
  103. package/test/r85-epoch-lock-release-lifetime.test.mjs +25 -0
  104. package/test/r85-epoch-repeat-append.test.mjs +30 -0
  105. package/test/r86-epoch-cleanup-and-loader.test.mjs +126 -0
  106. package/test/r87-crashed-orphan-revoke-reconcile.test.mjs +110 -0
  107. package/test/r88-native-disposition-failure.test.mjs +35 -0
  108. package/test/r89-privileged-install-boundary.test.mjs +123 -0
  109. package/test/r92-acl-ace-applicability.test.mjs +45 -0
  110. package/test/r94-win32-propagation-enum.test.mjs +51 -0
  111. package/test/r99-privileged-import-closure.test.mjs +45 -0
  112. package/test/remote-session-cli.test.mjs +69 -9
@@ -1,5 +1,5 @@
1
1
  import {createHash,createPrivateKey,createPublicKey,randomBytes,sign as cryptoSign,verify as cryptoVerify} from 'node:crypto';
2
- import {mkdir,open,readFile,readdir,rename,rm,writeFile} from 'node:fs/promises';
2
+ import {lstat,mkdir,open,readFile,readdir,rename,rm,writeFile} from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import {canonicalJson} from './canonical-json.mjs';
5
5
  import {decodeUtf8Strict} from './text-policy.mjs';
@@ -214,10 +214,63 @@ const durablePublicKeys=new Map();
214
214
  const RESTORE_BINDING_FIELDS=Object.freeze(['prior_grant_sha256','prior_grant_instance_nonce','predecessor_policy_envelope_sha256','successor_policy_envelope_sha256','candidate_manifest_sha256','continuity_subject_sha256']);
215
215
  function normalizeRestoreBinding(value,{required=false}={}){
216
216
  if(value===undefined||value===null){if(required)throw new Error('authority epoch transaction restore binding required');return null;}
217
- if(!plain(value)||Object.keys(value).length!==RESTORE_BINDING_FIELDS.length||RESTORE_BINDING_FIELDS.some(key=>!SHAPAT.test(String(value[key]))))
217
+ if(!plain(value)||Object.keys(value).length!==RESTORE_BINDING_FIELDS.length||
218
+ RESTORE_BINDING_FIELDS.some(key=>typeof value[key]!=='string'||!SHAPAT.test(value[key])))
218
219
  throw new Error('authority epoch transaction restore binding invalid');
219
220
  for(const key of Object.keys(value))if(!RESTORE_BINDING_FIELDS.includes(key))throw new Error('authority epoch transaction restore binding invalid');
220
- return Object.freeze(Object.fromEntries(RESTORE_BINDING_FIELDS.map(key=>[key,String(value[key])])));
221
+ return Object.freeze(Object.fromEntries(RESTORE_BINDING_FIELDS.map(key=>[key,value[key]])));
222
+ }
223
+
224
+ function assertEpochActionIdentity(current){
225
+ const action=current?.action;
226
+ if(!DURABLE_EPOCH_ACTIONS.has(action)||
227
+ (action.startsWith('transaction_')?
228
+ typeof current.update_id!=='string'||!/^[A-Za-z0-9._-]{1,96}$/.test(current.update_id):
229
+ current.update_id!==null))
230
+ throw new Error('authority epoch transition invalid');
231
+ if(action==='transaction_restore')normalizeRestoreBinding(current.restore_binding,{required:true});
232
+ else if(current.restore_binding!==null)throw new Error('authority epoch transition invalid');
233
+ }
234
+
235
+ function assertEpochPredecessorIdentity(event){
236
+ if(event.schema!==DURABLE_EPOCH_SCHEMA||
237
+ typeof event.grant_sha256!=='string'||!SHAPAT.test(event.grant_sha256)||
238
+ typeof event.grant_instance_nonce!=='string'||!SHAPAT.test(event.grant_instance_nonce)||
239
+ (event.seq===1?event.previous_event_sha256!==null:
240
+ typeof event.previous_event_sha256!=='string'||!SHAPAT.test(event.previous_event_sha256))||
241
+ ((event.policy_sha256!==undefined||event.policy_envelope_sha256!==undefined)&&
242
+ (typeof event.policy_sha256!=='string'||!SHAPAT.test(event.policy_sha256)||
243
+ typeof event.policy_envelope_sha256!=='string'||!SHAPAT.test(event.policy_envelope_sha256))))
244
+ throw new Error('authority epoch transition invalid');
245
+ assertEpochActionIdentity(event);
246
+ }
247
+
248
+ function assertEpochTransition(current,previous){
249
+ assertEpochActionIdentity(current);
250
+ const action=current.action;
251
+ if(!previous){
252
+ if(action.startsWith('transaction_'))throw new Error('authority epoch transition invalid');
253
+ return;
254
+ }
255
+ if(action==='transaction_revoke'&&
256
+ (!['grant','transaction_restore'].includes(previous.action)||
257
+ current.grant_sha256!==previous.grant_sha256||
258
+ current.grant_instance_nonce!==previous.grant_instance_nonce))
259
+ throw new Error('authority epoch transition invalid');
260
+ if(action==='transaction_restore'){
261
+ const binding=normalizeRestoreBinding(current.restore_binding,{required:true});
262
+ if(previous.action!=='transaction_revoke'||current.update_id!==previous.update_id||
263
+ binding.prior_grant_sha256!==previous.grant_sha256||
264
+ binding.prior_grant_instance_nonce!==previous.grant_instance_nonce||
265
+ current.grant_instance_nonce===previous.grant_instance_nonce||
266
+ (previous.policy_envelope_sha256!==undefined&&
267
+ binding.predecessor_policy_envelope_sha256!==previous.policy_envelope_sha256)||
268
+ (current.policy_envelope_sha256!==undefined&&
269
+ binding.successor_policy_envelope_sha256!==current.policy_envelope_sha256))
270
+ throw new Error('authority epoch transition invalid');
271
+ }
272
+ if(action==='grant'&&current.grant_instance_nonce===previous.grant_instance_nonce)
273
+ throw new Error('authority epoch transition invalid');
221
274
  }
222
275
 
223
276
  async function durableEpochKeys(runtimeDir,{privateRequired=false}={}){
@@ -271,38 +324,159 @@ function processAlive(pid){
271
324
  if(pid===process.pid)return true;
272
325
  try{process.kill(pid,0);return true;}catch(error){return error?.code==='EPERM';}
273
326
  }
274
- async function atomicJsonWrite(file,value){
327
+ let cachedEpochOwnerIdentity=null;
328
+ async function windowsEpochOwnerIdentity(pid){
329
+ const {queryForegroundIdentity,validForegroundIdentity}=await import('./foreground-process-identity.mjs');
330
+ const identity=queryForegroundIdentity(pid);
331
+ if(identity!==null&&!validForegroundIdentity(identity))throw new Error('authority epoch owner process identity incomplete');
332
+ return identity;
333
+ }
334
+ async function currentEpochOwnerIdentity(){
335
+ if(process.platform!=='win32')return null;
336
+ if(!cachedEpochOwnerIdentity)cachedEpochOwnerIdentity=await windowsEpochOwnerIdentity(process.pid);
337
+ if(!cachedEpochOwnerIdentity||cachedEpochOwnerIdentity.pid!==process.pid)
338
+ throw new Error('authority epoch owner process identity unavailable');
339
+ return cachedEpochOwnerIdentity;
340
+ }
341
+ // R80: A successful file write is not a durability barrier. Flush the exact
342
+ // open file handle BEFORE publishing an event or replacing the signed head.
343
+ // Node on Windows does not expose a verified directory-metadata flush: keep
344
+ // that limitation explicit instead of treating rename() as a power-loss proof.
345
+ export function authorityEpochDurabilityAssessment(){
346
+ return Object.freeze({
347
+ event_file:'FLUSH_REQUIRED',head_temp_file:'FLUSH_REQUIRED',
348
+ directory_metadata:process.platform==='win32'?'UNVERIFIED_ON_WINDOWS':'FSYNC_REQUIRED',
349
+ power_loss:'NOT_VERIFIED_BY_TESTS'
350
+ });
351
+ }
352
+ async function syncDirectoryMetadata(dir){
353
+ if(process.platform==='win32')return false; // No verified Node directory FlushFileBuffers contract.
354
+ const handle=await open(dir,'r');
355
+ try{await handle.sync();}
356
+ finally{await handle.close();}
357
+ return true;
358
+ }
359
+ async function writeDurableEvent(file,bytes,{stage=async()=>{}}={}){
360
+ const handle=await open(file,'wx'); // immutable identity, exclusive creation
361
+ try{
362
+ await stage('event_created');
363
+ await handle.writeFile(bytes);
364
+ await stage('event_written');
365
+ await handle.sync(); // event bytes must be flushed BEFORE head visibility
366
+ await stage('event_file_synced');
367
+ }finally{await handle.close();}
368
+ await syncDirectoryMetadata(path.dirname(file));
369
+ await stage('event_directory_barrier');
370
+ }
371
+ async function atomicJsonWrite(file,value,{stage=async()=>{}}={}){
275
372
  const dir=path.dirname(file);
276
373
  await mkdir(dir,{recursive:true});
277
374
  const tmp=`${file}.tmp-${process.pid}-${randomBytes(8).toString('hex')}`;
278
375
  try{
279
- await writeFile(tmp,canonicalJson(value),{encoding:'utf8',flag:'wx'});
376
+ const handle=await open(tmp,'wx');
377
+ try{
378
+ await stage('head_temp_created');
379
+ await handle.writeFile(Buffer.from(canonicalJson(value),'utf8'));
380
+ await stage('head_temp_written');
381
+ await handle.sync(); // temp head content flushed BEFORE rename
382
+ await stage('head_temp_file_synced');
383
+ }finally{await handle.close();}
280
384
  await rename(tmp,file);
385
+ await stage('head_renamed'); // rename succeeded: caller outcome may now be ambiguous
386
+ await syncDirectoryMetadata(dir);
387
+ await stage('head_directory_barrier');
281
388
  }finally{
282
389
  await rm(tmp,{force:true}).catch(()=>{});
283
390
  }
284
391
  }
285
- async function acquireEpochLock(dir){
392
+ // R82: On Windows, lock reclamation/release MUST be conditional on the
393
+ // exact observed lock bytes and must dispose through one pinned native handle.
394
+ // The R78 implementation also pins every non-reparse ancestor directory.
395
+ // R83: Windows lock snapshots now bind owner bytes + native FileIdInfo to
396
+ // the same pinned handle. The later pinned deletion compares BOTH identity
397
+ // and content before disposition, defeating byte-identical file replacement.
398
+ // POSIX remains legacy/unsupported against adversarial lock-path replacement.
399
+ async function epochLockSnapshot(lockPath){
400
+ if(process.platform==='win32'){
401
+ const {readPinnedEpochLockSnapshot}=await import('./foreground-marker-cas-win32.mjs');
402
+ return readPinnedEpochLockSnapshot(lockPath);
403
+ }
404
+ try{return {bytes:await readFile(lockPath),fileId:null};}
405
+ catch(error){if(error?.code==='ENOENT')return null;throw error;}
406
+ }
407
+ async function disposeObservedEpochLock(lockPath,snapshot){
408
+ if(process.platform==='win32'){
409
+ if(!snapshot?.fileId)throw new Error('epoch lock native identity snapshot is required');
410
+ const {deletePinnedRemoteMarker}=await import('./foreground-marker-cas-win32.mjs');
411
+ return deletePinnedRemoteMarker(lockPath,hashBytes(snapshot.bytes),{expectedFileId:snapshot.fileId});
412
+ }
413
+ throw new Error('authority epoch POSIX lock disposition unsupported');
414
+ }
415
+
416
+ async function acquireEpochLock(dir,{allowStaleRecovery=true,onLockDispositionStage=null}={}){
417
+ // POSIX open('wx') protects creation, but a later read/check followed by
418
+ // unlink(path) cannot atomically prove the pathname still denotes our lock.
419
+ // Until a kernel-held, identity-bound POSIX protocol exists, no authority
420
+ // epoch mutation may start there. Read-only head verification remains usable.
421
+ if(process.platform!=='win32')
422
+ throw new Error('authority epoch POSIX mutation unsupported: lock pathname cannot be disposed atomically');
286
423
  await mkdir(dir,{recursive:true});
287
424
  const lockPath=path.join(dir,'.lock');
425
+ const stage=async label=>{if(onLockDispositionStage)await onLockDispositionStage(label,lockPath);};
288
426
  for(let attempt=0;attempt<2;attempt++){
289
427
  const token=randomBytes(16).toString('hex');
290
428
  try{
291
- const handle=await open(lockPath,'wx');
292
- try{await handle.writeFile(canonicalJson({schema:'deadbyte.authority-epoch-lock.r65.v1',pid:process.pid,token,created_at_utc:new Date().toISOString()}),'utf8');}
293
- finally{await handle.close();}
294
- return async()=>{
295
- try{
296
- const current=JSON.parse(await readFile(lockPath,'utf8'));
297
- if(current?.token===token)await rm(lockPath,{force:true});
298
- }catch(error){if(error?.code!=='ENOENT')throw error;}
429
+ const ownerIdentity=await currentEpochOwnerIdentity();
430
+ const lockBytes=Buffer.from(canonicalJson({schema:'deadbyte.authority-epoch-lock.r65.v1',pid:process.pid,token,
431
+ created_at_utc:new Date().toISOString(),...(ownerIdentity?{owner_identity:ownerIdentity}:{})}),'utf8');
432
+ let nativeLease=null;
433
+ if(process.platform==='win32'){
434
+ const {acquireHeldEpochLock}=await import('./epoch-lock-r85.mjs');
435
+ nativeLease=await acquireHeldEpochLock(lockPath,lockBytes);
436
+ if(!nativeLease)throw Object.assign(new Error('authority epoch lock exists'),{code:'EEXIST'});
437
+ }else throw new Error('authority epoch POSIX mutation unsupported');
438
+ const verifyOwned=async()=>{
439
+ // On Windows the in-process native handle holds the leaf and every
440
+ // non-reparse ancestor through the mutation. No pathname recheck can
441
+ // improve that atomic ownership guarantee.
442
+ if(!nativeLease)throw new Error('authority epoch lock ownership unverified');
443
+ return null;
299
444
  };
445
+ const release=async()=>{
446
+ if(nativeLease){
447
+ let stageError=null;
448
+ try{await stage('release_before_cas');}catch(error){stageError=error;}
449
+ let releaseError=null;
450
+ try{await nativeLease.release();}catch(error){releaseError=error;}
451
+ if(stageError&&releaseError)throw new AggregateError([stageError,releaseError],
452
+ 'authority epoch release observer and native cleanup both failed');
453
+ if(stageError)throw stageError;
454
+ if(releaseError)throw releaseError;
455
+ return;
456
+ }
457
+ throw new Error('authority epoch lock release without native ownership denied');
458
+ };
459
+ release.verifyOwned=verifyOwned;
460
+ return release;
300
461
  }catch(error){
301
462
  if(error?.code!=='EEXIST')throw error;
463
+ const snapshot=await epochLockSnapshot(lockPath);
302
464
  let owner=null;
303
- try{owner=JSON.parse(await readFile(lockPath,'utf8'));}catch{}
304
- if(attempt===0&&owner&&Number.isInteger(owner.pid)&&!processAlive(owner.pid)){
305
- await rm(lockPath,{force:true});
465
+ try{if(snapshot)owner=JSON.parse(snapshot.bytes.toString('utf8'));}catch{}
466
+ if(attempt===0&&allowStaleRecovery&&snapshot&&owner&&Number.isInteger(owner.pid)&&owner.pid>0){
467
+ if(process.platform!=='win32')throw new Error('authority epoch mutation locked (POSIX automatic recovery disabled)');
468
+ const {validForegroundIdentity,foregroundIdentityMatches}=await import('./foreground-process-identity.mjs');
469
+ if(!validForegroundIdentity(owner.owner_identity)||owner.owner_identity.pid!==owner.pid)
470
+ throw new Error('authority epoch mutation locked (owner identity unverified)');
471
+ const observed=await windowsEpochOwnerIdentity(owner.pid);
472
+ if(observed){
473
+ if(!foregroundIdentityMatches(owner.owner_identity,observed))
474
+ throw new Error('authority epoch mutation locked (owner process birth changed)');
475
+ throw new Error('authority epoch mutation locked (owner still active)');
476
+ }
477
+ await stage('stale_before_cas');
478
+ const disposition=await disposeObservedEpochLock(lockPath,snapshot);
479
+ if(disposition!=='REMOVED')throw new Error(`authority epoch mutation locked (stale owner changed: ${disposition})`);
306
480
  continue;
307
481
  }
308
482
  throw new Error('authority epoch mutation locked');
@@ -310,12 +484,40 @@ async function acquireEpochLock(dir){
310
484
  }
311
485
  throw new Error('authority epoch mutation locked');
312
486
  }
313
- async function readDurableHead(runtimeDir,plane){
487
+ const MAX_DURABLE_EPOCH_RECORD_BYTES=1048576;
488
+
489
+ async function readBoundedEpochHandle(handle,label){
490
+ const stat=await handle.stat();
491
+ if(!stat.isFile()||!Number.isSafeInteger(stat.size)||stat.size>MAX_DURABLE_EPOCH_RECORD_BYTES)
492
+ throw new Error(`authority epoch ${label} oversized`);
493
+ // Read at most the observed size plus one byte. A concurrent extension
494
+ // cannot turn this into an unbounded read, and is rejected rather than
495
+ // silently truncating the record that will be hashed or parsed.
496
+ const bytes=Buffer.alloc(stat.size+1);
497
+ let used=0;
498
+ while(used<bytes.length){
499
+ const {bytesRead}=await handle.read(bytes,used,bytes.length-used,used);
500
+ if(bytesRead===0)break;
501
+ used+=bytesRead;
502
+ }
503
+ if(used!==stat.size)throw new Error(`authority epoch ${label} changed during read`);
504
+ return bytes.subarray(0,used);
505
+ }
506
+
507
+ async function readBoundedEpochRecord(file,label){
508
+ const handle=await open(file,'r');
509
+ try{
510
+ return await readBoundedEpochHandle(handle,label);
511
+ }finally{await handle.close();}
512
+ }
513
+
514
+ async function readDurableHead(runtimeDir,plane,{recoveryOrphanFile=null}={}){
314
515
  const dir=durableEpochDir(runtimeDir,plane),headPath=path.join(dir,'head.json');
315
516
  let head;
316
- try{head=JSON.parse(await readFile(headPath,'utf8'));}
517
+ try{head=JSON.parse(decodeUtf8Strict(await readBoundedEpochRecord(headPath,'head')));}
317
518
  catch(error){
318
519
  if(error?.code==='ENOENT')return null;
520
+ if(String(error?.message??'').includes('authority epoch head oversized'))throw error;
319
521
  throw new Error('authority epoch head invalid');
320
522
  }
321
523
  if(!plain(head)||head.schema!==DURABLE_HEAD_SCHEMA||head.plane!==plane||
@@ -334,7 +536,8 @@ async function readDurableHead(runtimeDir,plane){
334
536
  // the head. The head remains freshly read on every authority decision.
335
537
  const [eventNames,eventRead,keys]=await Promise.all([
336
538
  readdir(path.join(dir,'events')),
337
- readFile(path.join(dir,'events',head.event_file)).then(bytes=>({bytes}),()=>({bytes:null})),
539
+ readBoundedEpochRecord(path.join(dir,'events',head.event_file),'event')
540
+ .then(bytes=>({bytes}),error=>({error})),
338
541
  durableEpochKeys(runtimeDir)
339
542
  ]);
340
543
  if(eventNames.length>10000)throw new Error('authority epoch event budget exceeded');
@@ -342,11 +545,14 @@ async function readDurableHead(runtimeDir,plane){
342
545
  const match=/^(\d{16})-([0-9a-f]{64})\.json$/.exec(name);
343
546
  if(!match)throw new Error('authority epoch unexpected event file');
344
547
  const seq=Number(match[1]);
345
- if(!Number.isSafeInteger(seq)||seq>head.seq||(seq===head.seq&&name!==head.event_file))
548
+ if(!Number.isSafeInteger(seq)||(seq>head.seq&&name!==recoveryOrphanFile)||(seq===head.seq&&name!==head.event_file))
346
549
  throw new Error('authority epoch head replay or incomplete write');
347
550
  }
551
+ if(eventRead.error){
552
+ if(eventRead.error.code==='ENOENT')throw new Error('authority epoch event missing');
553
+ throw eventRead.error;
554
+ }
348
555
  const eventBytes=eventRead.bytes;
349
- if(!eventBytes)throw new Error('authority epoch event missing');
350
556
  if(hashBytes(eventBytes)!==head.event_sha256)throw new Error('authority epoch event hash mismatch');
351
557
  let event;
352
558
  try{event=JSON.parse(decodeUtf8Strict(eventBytes));}catch{throw new Error('authority epoch event invalid');}
@@ -363,30 +569,223 @@ async function readDurableHead(runtimeDir,plane){
363
569
  (!SHAPAT.test(String(head.policy_sha256))||!SHAPAT.test(String(head.policy_envelope_sha256))||
364
570
  head.policy_sha256!==event.policy_sha256||head.policy_envelope_sha256!==event.policy_envelope_sha256))
365
571
  throw new Error('authority epoch policy binding invalid');
366
- if(head.seq===1&&head.previous_event_sha256!==null)throw new Error('authority epoch genesis chain invalid');
572
+ if(head.seq===1){
573
+ if(head.previous_event_sha256!==null)throw new Error('authority epoch genesis chain invalid');
574
+ assertEpochTransition(head,null);
575
+ }
367
576
  if(head.seq>1){
368
577
  if(!SHAPAT.test(String(head.previous_event_sha256)))throw new Error('authority epoch predecessor missing');
369
578
  const priorName=durableEventName(head.seq-1,head.previous_event_sha256);
370
579
  let priorBytes;
371
- try{priorBytes=await readFile(path.join(dir,'events',priorName));}
372
- catch{throw new Error('authority epoch predecessor event missing');}
580
+ try{priorBytes=await readBoundedEpochRecord(path.join(dir,'events',priorName),'predecessor event');}
581
+ catch(error){
582
+ if(error?.code==='ENOENT')throw new Error('authority epoch predecessor event missing');
583
+ throw error;
584
+ }
373
585
  if(hashBytes(priorBytes)!==head.previous_event_sha256)throw new Error('authority epoch predecessor hash mismatch');
374
586
  let prior;
375
587
  try{prior=JSON.parse(decodeUtf8Strict(priorBytes));}catch{throw new Error('authority epoch predecessor invalid');}
376
588
  if(!verifyDurableEvent(prior,publicKey,keyId)||prior.seq!==head.seq-1||prior.plane!==plane)
377
589
  throw new Error('authority epoch predecessor signature invalid');
590
+ assertEpochPredecessorIdentity(prior);
591
+ if(prior.seq===1)assertEpochTransition(prior,null);
592
+ assertEpochTransition(head,prior);
378
593
  }
379
594
  return Object.freeze({...head});
380
595
  }
381
596
 
382
- export function createAuthorityEpochStore({runtimeDir,policyBytes=null}={}){
597
+ // Deliberately separate from the per-request authority decision. This is an
598
+ // independent, bounded audit of the complete signed chain, including events
599
+ // older than the immediate predecessor checked by readDurableHead().
600
+ async function auditDurableHistory(runtimeDir,plane){
601
+ const dir=durableEpochDir(runtimeDir,plane),events=path.join(dir,'events');
602
+ const head=await readDurableHead(runtimeDir,plane);
603
+ if(!head){
604
+ let names;
605
+ try{names=await readdir(events);}catch(error){if(error?.code==='ENOENT')return Object.freeze({status:'EMPTY',plane,verified_events:0});throw error;}
606
+ if(names.length)throw new Error('authority epoch history events exist without head');
607
+ return Object.freeze({status:'EMPTY',plane,verified_events:0});
608
+ }
609
+ if(head.seq>10000)throw new Error('authority epoch history event budget exceeded');
610
+ const headPath=path.join(dir,'head.json'),headBytes=await readBoundedEpochRecord(headPath,'head');
611
+ let pinnedHead;
612
+ try{pinnedHead=JSON.parse(decodeUtf8Strict(headBytes));}catch{throw new Error('authority epoch history head changed');}
613
+ if(canonicalJson(pinnedHead)!==canonicalJson(head))throw new Error('authority epoch history head changed');
614
+ const names=await readdir(events);
615
+ if(names.length!==head.seq)throw new Error('authority epoch history event count mismatch');
616
+ const namesSet=new Set(names);
617
+ const {publicKey,keyId}=await durableEpochKeys(runtimeDir);
618
+ let expectedSha=head.event_sha256,totalBytes=0,newer=null;
619
+ for(let seq=head.seq;seq>=1;seq--){
620
+ const name=durableEventName(seq,expectedSha);
621
+ if(!namesSet.has(name))throw new Error('authority epoch history event missing');
622
+ const file=path.join(events,name),st=await lstat(file);
623
+ if(!st.isFile()||st.isSymbolicLink()||st.size>1048576)
624
+ throw new Error('authority epoch history event identity invalid');
625
+ totalBytes+=st.size;
626
+ if(totalBytes>67108864)throw new Error('authority epoch history byte budget exceeded');
627
+ const bytes=await readBoundedEpochRecord(file,'event');
628
+ if(hashBytes(bytes)!==expectedSha)throw new Error('authority epoch history hash mismatch');
629
+ let event;
630
+ try{event=JSON.parse(decodeUtf8Strict(bytes));}catch{throw new Error('authority epoch history event invalid');}
631
+ if(!verifyDurableEvent(event,publicKey,keyId)||event.schema!==DURABLE_EPOCH_SCHEMA||
632
+ event.plane!==plane||event.seq!==seq||!DURABLE_EPOCH_ACTIONS.has(event.action)||
633
+ !SHAPAT.test(String(event.grant_sha256))||!SHAPAT.test(String(event.grant_instance_nonce)))
634
+ throw new Error('authority epoch history event signature or identity invalid');
635
+ normalizeRestoreBinding(event.restore_binding,{required:event.action==='transaction_restore'});
636
+ if((event.action.startsWith('transaction_')?!/^[A-Za-z0-9._-]{1,96}$/.test(String(event.update_id??'')):event.update_id!==null))
637
+ throw new Error('authority epoch history update identity invalid');
638
+ if(event.policy_sha256!==undefined||event.policy_envelope_sha256!==undefined){
639
+ if(!SHAPAT.test(String(event.policy_sha256))||!SHAPAT.test(String(event.policy_envelope_sha256)))
640
+ throw new Error('authority epoch history policy binding invalid');
641
+ }
642
+ if(seq===1){
643
+ if(event.previous_event_sha256!==null)throw new Error('authority epoch history genesis chain invalid');
644
+ }else if(!SHAPAT.test(String(event.previous_event_sha256)))
645
+ throw new Error('authority epoch history predecessor missing');
646
+ if(newer)assertEpochTransition(newer,event);
647
+ else if(seq===1)assertEpochTransition(event,null);
648
+ newer=event;
649
+ expectedSha=event.previous_event_sha256;
650
+ }
651
+ if(newer&&head.seq>1)assertEpochTransition(newer,null);
652
+ if(!(await readBoundedEpochRecord(headPath,'head')).equals(headBytes))throw new Error('authority epoch history head changed during audit');
653
+ if((await durableEpochKeys(runtimeDir)).keyId!==keyId)throw new Error('authority epoch history trust changed during audit');
654
+ return Object.freeze({status:'PASS',plane,seq:head.seq,event_sha256:head.event_sha256,
655
+ verified_events:head.seq,verified_bytes:totalBytes});
656
+ }
657
+
658
+ // R79: An orphan event is not a committed head. Ordinary reads, verification
659
+ // and appends MUST keep rejecting it. Only explicit revocation-only recovery
660
+ // can use the bounded exception to inspect an older, still-signed head.
661
+ async function inspectOrphanRevocation(runtimeDir,plane,{expectedHeadSha256,expectedOrphanSha256}){
662
+ if(!SCHEMAS[plane]||!SHAPAT.test(String(expectedHeadSha256))||!SHAPAT.test(String(expectedOrphanSha256)))
663
+ throw new Error('authority epoch recovery requires exact signed event identities');
664
+ const dir=durableEpochDir(runtimeDir,plane),events=path.join(dir,'events');
665
+ // Reject known static reparse/junction boundaries. Handle-pinned directory
666
+ // protection for concurrent substitutions is a separate Windows requirement.
667
+ for(const parent of [path.resolve(runtimeDir),path.join(path.resolve(runtimeDir),'authority-epochs'),dir,events]){
668
+ const st=await lstat(parent);
669
+ if(!st.isDirectory()||st.isSymbolicLink())throw new Error('authority epoch recovery directory identity invalid');
670
+ }
671
+ const headPath=path.join(dir,'head.json');
672
+ const headStat=await lstat(headPath);
673
+ if(!headStat.isFile()||headStat.isSymbolicLink())throw new Error('authority epoch recovery head identity invalid');
674
+ const rawHead=await readBoundedEpochRecord(headPath,'head');
675
+ let untrusted;
676
+ try{untrusted=JSON.parse(decodeUtf8Strict(rawHead));}catch{throw new Error('authority epoch recovery head invalid');}
677
+ if(!Number.isSafeInteger(untrusted?.seq)||untrusted.seq<1)
678
+ throw new Error('authority epoch recovery head invalid');
679
+ const names=await readdir(events);
680
+ // Do not accept more than one event beyond the old head, even if signed.
681
+ const orphanNames=names.filter(name=>{
682
+ const m=/^(\d{16})-([0-9a-f]{64})\.json$/.exec(name);
683
+ return m&&Number(m[1])>untrusted.seq;
684
+ });
685
+ if(orphanNames.length!==1)throw new Error('authority epoch recovery requires exactly one orphan event');
686
+ const orphanFile=orphanNames[0];
687
+ if(orphanFile!==durableEventName(untrusted.seq+1,expectedOrphanSha256))
688
+ throw new Error('authority epoch recovery orphan identity mismatch');
689
+ const previous=await readDurableHead(runtimeDir,plane,{recoveryOrphanFile:orphanFile});
690
+ if(!previous||previous.event_sha256!==expectedHeadSha256||previous.seq!==untrusted.seq)
691
+ throw new Error('authority epoch recovery previous head mismatch');
692
+ if(!['grant','transaction_restore','transaction_revoke'].includes(previous.action))
693
+ throw new Error('authority epoch recovery previous action invalid');
694
+ const orphanPath=path.join(events,orphanFile);
695
+ const orphanStat=await lstat(orphanPath);
696
+ if(!orphanStat.isFile()||orphanStat.isSymbolicLink())throw new Error('authority epoch recovery orphan file identity invalid');
697
+ const orphanBytes=await readBoundedEpochRecord(orphanPath,'event');
698
+ if(hashBytes(orphanBytes)!==expectedOrphanSha256)
699
+ throw new Error('authority epoch recovery orphan bytes mismatch');
700
+ let event;
701
+ try{event=JSON.parse(decodeUtf8Strict(orphanBytes));}catch{throw new Error('authority epoch recovery orphan invalid');}
702
+ const {publicKey,keyId}=await durableEpochKeys(runtimeDir);
703
+ if(!verifyDurableEvent(event,publicKey,keyId))
704
+ throw new Error('authority epoch recovery orphan signature invalid');
705
+ if(event.schema!==DURABLE_EPOCH_SCHEMA||event.plane!==plane||event.seq!==previous.seq+1||
706
+ !['transaction_revoke','user_revoke'].includes(event.action)||
707
+ (event.action==='transaction_revoke'&&!/^[A-Za-z0-9._-]{1,96}$/.test(String(event.update_id??'')))||
708
+ (event.action==='user_revoke'&&event.update_id!==null)||
709
+ event.previous_event_sha256!==previous.event_sha256||
710
+ event.grant_sha256!==previous.grant_sha256||event.grant_instance_nonce!==previous.grant_instance_nonce||
711
+ event.policy_sha256!==previous.policy_sha256||event.policy_envelope_sha256!==previous.policy_envelope_sha256||
712
+ event.restore_binding!==null||
713
+ (event.action==='transaction_revoke'&&!['grant','transaction_restore'].includes(previous.action)))
714
+ throw new Error('authority epoch recovery orphan revocation binding invalid');
715
+ // Head cannot be promoted unless it is still exactly the same committed
716
+ // predecessor. No authority-creating action is ever eligible.
717
+ const replay=await readBoundedEpochRecord(headPath,'head');
718
+ if(!replay.equals(rawHead))throw new Error('authority epoch recovery head changed');
719
+ const newHead={schema:DURABLE_HEAD_SCHEMA,plane,seq:event.seq,action:event.action,
720
+ update_id:event.update_id,grant_sha256:event.grant_sha256,
721
+ grant_instance_nonce:event.grant_instance_nonce,restore_binding:null,
722
+ ...(event.policy_sha256===undefined?{}:{policy_sha256:event.policy_sha256,policy_envelope_sha256:event.policy_envelope_sha256}),
723
+ previous_event_sha256:event.previous_event_sha256,event_sha256:expectedOrphanSha256,event_file:orphanFile};
724
+ return Object.freeze({previous,newHead,headBytes:rawHead,orphanBytes});
725
+ }
726
+
727
+ export function createAuthorityEpochStore({runtimeDir,policyBytes=null,onDurabilityStage=null,onLockDispositionStage=null}={}){
383
728
  const root=path.resolve(String(runtimeDir??''));
384
729
  if(!runtimeDir)throw new Error('authority epoch runtime directory invalid');
385
730
  if(policyBytes!==null&&!Buffer.isBuffer(policyBytes))throw new Error('authority epoch policy bytes invalid');
731
+ if(onDurabilityStage!==null&&(typeof onDurabilityStage!=='function'||process.env.NODE_TEST_CONTEXT!=='child-v8'))
732
+ throw new Error('authority epoch stage observer is restricted to node test workers');
733
+ if(onLockDispositionStage!==null&&(typeof onLockDispositionStage!=='function'||process.env.NODE_TEST_CONTEXT!=='child-v8'))
734
+ throw new Error('authority epoch lock observer is restricted to node test workers');
735
+ // Node-test-worker-only fault observer. It can abort, never bypass, flush/order checks.
736
+ const stage=async name=>{if(onDurabilityStage)await onDurabilityStage(name);};
386
737
  return Object.freeze({
387
738
  async readHead({plane}){
388
739
  return readDurableHead(root,plane);
389
740
  },
741
+ async auditHistory({plane}){
742
+ return auditDurableHistory(root,plane);
743
+ },
744
+ async reconcileOrphanedRevocation({plane,expectedHeadSha256,expectedOrphanSha256,operatorConfirmed=false}){
745
+ // NOT called from grant verification, append, startup or automatic
746
+ // recovery. The operator must supply two observed event identities.
747
+ if(operatorConfirmed!==true)throw new Error('authority epoch recovery requires explicit operator confirmation');
748
+ const dir=durableEpochDir(root,plane);
749
+ // Reject wrong or stale operator evidence before even reclaiming a dead
750
+ // owner's lock. This is a read-only preflight, not an ownership proof;
751
+ // the same exact evidence is independently rechecked under the lock.
752
+ await inspectOrphanRevocation(root,plane,{expectedHeadSha256,expectedOrphanSha256});
753
+ // Explicit, identity-bound revocation reconciliation may reclaim a
754
+ // verified dead owner's lock through the same native stale-owner CAS as
755
+ // append. A live or unverified owner still fails closed. The orphan
756
+ // itself is inspected only after exclusive ownership is established.
757
+ const release=await acquireEpochLock(dir,{allowStaleRecovery:true,onLockDispositionStage});
758
+ try{
759
+ const {newHead,headBytes,orphanBytes}=await inspectOrphanRevocation(root,plane,{expectedHeadSha256,expectedOrphanSha256});
760
+ // Windows FlushFileBuffers requires a writable file handle. The old
761
+ // read-only ('r') handle returned EPERM on Windows even for a valid,
762
+ // signed revocation. 'r+' grants the handle sync rights; we do not
763
+ // write event bytes. Revalidate exact bytes through this same handle
764
+ // before syncing, refusing a replaced or modified orphan event.
765
+ // Directory/power-loss durability is NOT proven here.
766
+ const fh=await open(path.join(dir,'events',newHead.event_file),'r+');
767
+ try{
768
+ const st=await fh.stat();
769
+ if(!st.isFile()||st.size!==orphanBytes.length)
770
+ throw new Error('authority epoch recovery pinned orphan identity invalid');
771
+ const pinnedBytes=await readBoundedEpochHandle(fh,'event');
772
+ if(!pinnedBytes.equals(orphanBytes)||hashBytes(pinnedBytes)!==expectedOrphanSha256)
773
+ throw new Error('authority epoch recovery orphan changed before sync');
774
+ await fh.sync();
775
+ }finally{await fh.close();}
776
+ if(!(await readBoundedEpochRecord(path.join(dir,'head.json'),'head')).equals(headBytes))
777
+ throw new Error('authority epoch recovery head changed before commit');
778
+ await release.verifyOwned();
779
+ await atomicJsonWrite(path.join(dir,'head.json'),newHead,{stage});
780
+ const committed=await readDurableHead(root,plane);
781
+ if(committed.event_sha256!==expectedOrphanSha256||
782
+ !['transaction_revoke','user_revoke'].includes(committed.action))
783
+ throw new Error('authority epoch recovered head re-verification failed');
784
+ return Object.freeze({status:'REVOKED_ONLY_RECONCILED',plane,seq:committed.seq,
785
+ action:committed.action,event_sha256:committed.event_sha256,
786
+ standing_grant_authorized:false});
787
+ }finally{await release();}
788
+ },
390
789
  async append({plane,action,grantBytes,grantInstanceNonce,updateId=null,expectedHeadSha256=undefined,restoreBinding=null}){
391
790
  const transactional=action?.startsWith('transaction_')===true;
392
791
  if(!SCHEMAS[plane]||!DURABLE_EPOCH_ACTIONS.has(action)||!Buffer.isBuffer(grantBytes)||
@@ -409,8 +808,13 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null}={}){
409
808
  if(parsedGrant?.policy_sha256!==policySha256)throw new Error('authority epoch grant policy digest mismatch');
410
809
  policyBinding={policy_sha256:policySha256,policy_envelope_sha256:deriveSecurityEnvelope(plane,parsedPolicy).sha256};
411
810
  }
811
+ // A mismatched successor envelope is known before acquiring a native
812
+ // lock or writing any signed event. Refuse it without side effects.
813
+ if(action==='transaction_restore'&&policyBinding?.policy_envelope_sha256!==undefined&&
814
+ normalizedRestoreBinding.successor_policy_envelope_sha256!==policyBinding.policy_envelope_sha256)
815
+ throw new Error('authority epoch transaction restore successor policy envelope mismatch');
412
816
  const dir=durableEpochDir(root,plane),events=path.join(dir,'events');
413
- const release=await acquireEpochLock(dir);
817
+ const release=await acquireEpochLock(dir,{onLockDispositionStage});
414
818
  try{
415
819
  await mkdir(events,{recursive:true});
416
820
  const previous=await readDurableHead(root,plane);
@@ -433,6 +837,12 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null}={}){
433
837
  throw new Error('authority epoch transaction restore predecessor grant binding mismatch');
434
838
  if(previous.grant_instance_nonce===grantInstanceNonce)
435
839
  throw new Error('authority epoch transaction restore requires fresh nonce; revoked nonce cannot be resurrected');
840
+ if(previous.policy_envelope_sha256!==undefined&&
841
+ normalizedRestoreBinding.predecessor_policy_envelope_sha256!==previous.policy_envelope_sha256)
842
+ throw new Error('authority epoch transaction restore predecessor policy envelope mismatch');
843
+ if(policyBinding?.policy_envelope_sha256!==undefined&&
844
+ normalizedRestoreBinding.successor_policy_envelope_sha256!==policyBinding.policy_envelope_sha256)
845
+ throw new Error('authority epoch transaction restore successor policy envelope mismatch');
436
846
  }
437
847
  if(action==='grant'&&previous?.grant_instance_nonce===grantInstanceNonce)
438
848
  throw new Error('authority epoch grant requires fresh nonce; prior nonce cannot be resurrected');
@@ -445,12 +855,15 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null}={}){
445
855
  const event={...body,attestation:{algorithm:'ed25519',domain:'DEADBYTE-AUTHORITY-DURABLE-EPOCH-R65-V1',key_id:keyId,signature_base64:signature.toString('base64')}};
446
856
  const eventBytes=Buffer.from(canonicalJson(event),'utf8'),eventSha256=hashBytes(eventBytes);
447
857
  const eventFile=durableEventName(seq,eventSha256);
448
- await writeFile(path.join(events,eventFile),eventBytes,{flag:'wx'});
858
+ await writeDurableEvent(path.join(events,eventFile),eventBytes,{stage});
449
859
  const head={schema:DURABLE_HEAD_SCHEMA,plane,seq,action,update_id:updateId,
450
860
  grant_sha256:grantSha256,grant_instance_nonce:grantInstanceNonce,restore_binding:normalizedRestoreBinding,
451
861
  ...(policyBinding??{}),
452
862
  previous_event_sha256:event.previous_event_sha256,event_sha256:eventSha256,event_file:eventFile};
453
- await atomicJsonWrite(path.join(dir,'head.json'),head);
863
+ // A replaced lock observed before publication must not authorize a
864
+ // head commit. This narrows the window but is not a held-handle lock.
865
+ await release.verifyOwned();
866
+ await atomicJsonWrite(path.join(dir,'head.json'),head,{stage});
454
867
  return Object.freeze({...head});
455
868
  }finally{await release();}
456
869
  },