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.
- package/MANIFEST.SHA256 +111 -48
- package/PROVENANCE.json +1 -1
- package/README.txt +18 -18
- package/RELEASE.json +3 -3
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +2 -2
- package/bench/r121-epoch-readhead.mjs +54 -0
- package/bench/r124-epoch-predecessor.mjs +53 -0
- package/bin/deadbyte-epoch-lock-r85.node +0 -0
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/HARNESS2-ADAPTATION-20261020.md +24 -0
- package/docs/R128-RELEASE-CANDIDATE.md +26 -0
- package/docs/R75-RECOVERY-HANDOFF.md +16 -0
- package/docs/R85-EPOCH-LOCK-BINARY.json +13 -0
- package/native/deadbyte-epoch-lock-r85.cpp +241 -0
- package/package.json +1 -1
- package/scripts/build-epoch-lock-r85.ps1 +34 -0
- package/scripts/deploy-r48.mjs +3 -1
- package/scripts/npm-release-r48.mjs +18 -4
- package/scripts/r75-npm-bin-integrity-gate.mjs +33 -0
- package/scripts/release-manifest.mjs +1 -1
- package/scripts/verify-authority-epoch-history.mjs +38 -0
- package/scripts/verify-r70-full-regression.ps1 +12 -2
- package/src/activation-authority-proof-r75.mjs +67 -0
- package/src/activation-continuity-engine-r65.mjs +17 -10
- package/src/activation-preflight-r65.mjs +13 -0
- package/src/agent-eval-r42.mjs +30 -0
- package/src/authority-continuity-r65.mjs +442 -29
- package/src/autonomous-local-supervisor.mjs +77 -47
- package/src/deadbyte-cli.mjs +73 -46
- package/src/epoch-lock-r85.mjs +48 -0
- package/src/foreground-marker-cas-win32.mjs +284 -0
- package/src/foreground-process-identity.mjs +3 -3
- package/src/privileged-install-trust-r89.mjs +196 -0
- package/src/remote-live-log.mjs +15 -8
- package/src/remote-log-decode-r60.mjs +65 -0
- package/src/remote-session-guardian.mjs +13 -12
- package/src/runtime-update.mjs +5 -22
- package/src/version.mjs +1 -1
- package/src/windows-powershell-environment.mjs +24 -0
- package/test/autonomous-local-supervisor.test.mjs +1 -1
- package/test/cli-entrypoint.test.mjs +1 -1
- package/test/harness2-eval-context-isolation.test.mjs +90 -0
- package/test/helpers/r75-authority-proof.mjs +9 -0
- package/test/r100-posix-owned-lock-race.test.mjs +72 -0
- package/test/r101-acl-probe-environment.test.mjs +51 -0
- package/test/r106-trusted-powershell-spawn.test.mjs +53 -0
- package/test/r107-updater-powershell-path.test.mjs +77 -0
- package/test/r108-remote-powershell-path.test.mjs +59 -0
- package/test/r110-supervisor-powershell-path.test.mjs +28 -0
- package/test/r111-supervisor-marker-cas.test.mjs +116 -0
- package/test/r112-supervisor-claim-cas.test.mjs +63 -0
- package/test/r113-supervisor-cleanup-terminality.test.mjs +100 -0
- package/test/r114-supervisor-absence-proof.test.mjs +45 -0
- package/test/r115-posix-continuity-entrypoint.test.mjs +17 -0
- package/test/r116-authority-crash-matrix.test.mjs +83 -0
- package/test/r117-concurrent-orphan-recovery.test.mjs +54 -0
- package/test/r118-cross-process-orphan-recovery.test.mjs +99 -0
- package/test/r119-recovery-crash-after-commit.test.mjs +91 -0
- package/test/r120-authority-history-audit.test.mjs +103 -0
- package/test/r121-signed-transition-invariant.test.mjs +132 -0
- package/test/r122-epoch-bounded-read.test.mjs +58 -0
- package/test/r123-recovery-bounded-read.test.mjs +64 -0
- package/test/r124-live-log-integrity.test.mjs +66 -0
- package/test/r124-live-log-tail-boundaries.test.mjs +57 -0
- package/test/r124-signed-predecessor-shape.test.mjs +66 -0
- package/test/r125-restore-binding-types.test.mjs +63 -0
- package/test/r126-restore-policy-envelope.test.mjs +89 -0
- package/test/r127-restore-append-policy-boundary.test.mjs +25 -0
- package/test/r33-closeout-regression.test.mjs +2 -1
- package/test/r54-offline-predecessor-self-exclusion.test.mjs +2 -2
- package/test/r55-offline-predecessor-guardian-family.test.mjs +2 -2
- package/test/r56-rollback-launcher-restore.test.mjs +2 -2
- package/test/r57-core-startup-race.test.mjs +2 -2
- package/test/r58-concurrent-recovery.test.mjs +10 -7
- package/test/r58-stale-owner.test.mjs +5 -6
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r65-activation-orchestration.test.mjs +2 -1
- package/test/r65-activation-preflight.test.mjs +7 -6
- package/test/r65-authority-epoch-recovery.test.mjs +3 -1
- package/test/r65-authority-epoch-store.test.mjs +9 -6
- package/test/r65-authority-restored-safe.test.mjs +2 -1
- package/test/r65-deploy-preflight.test.mjs +13 -14
- package/test/r65-epoch-native-activation-integration.test.mjs +5 -4
- package/test/r65-legacy-authority-migration.test.mjs +2 -1
- package/test/r67-npm-cross-volume.test.mjs +9 -2
- package/test/r71-full-verifier-artifact.test.mjs +21 -15
- package/test/r71-isolated-slot-rollback-restore.test.mjs +3 -1
- package/test/r71-release-identity.test.mjs +3 -1
- package/test/r74-kernel-lock.test.mjs +12 -6
- package/test/r74-prepublish-contract.test.mjs +3 -1
- package/test/r75-activation-authority-proof.test.mjs +46 -0
- package/test/r75-authority-evidence.test.mjs +77 -0
- package/test/r76-pinned-marker-cas.test.mjs +78 -0
- package/test/r77-epoch-grant-cas.test.mjs +51 -0
- package/test/r78-parent-junction-cas.test.mjs +62 -0
- package/test/r79-orphan-revocation-reconcile.test.mjs +88 -0
- package/test/r79-windows-fsync-regression.test.mjs +45 -0
- package/test/r80-durable-epoch-commit.test.mjs +76 -0
- package/test/r82-epoch-lock-owned-cas.test.mjs +93 -0
- package/test/r83-epoch-lock-fileid-cas.test.mjs +92 -0
- package/test/r84-epoch-lock-created-fileid.test.mjs +112 -0
- package/test/r85-epoch-lock-release-lifetime.test.mjs +25 -0
- package/test/r85-epoch-repeat-append.test.mjs +30 -0
- package/test/r86-epoch-cleanup-and-loader.test.mjs +126 -0
- package/test/r87-crashed-orphan-revoke-reconcile.test.mjs +110 -0
- package/test/r88-native-disposition-failure.test.mjs +35 -0
- package/test/r89-privileged-install-boundary.test.mjs +123 -0
- package/test/r92-acl-ace-applicability.test.mjs +45 -0
- package/test/r94-win32-propagation-enum.test.mjs +51 -0
- package/test/r99-privileged-import-closure.test.mjs +45 -0
- package/test/remote-session-cli.test.mjs +69 -9
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {randomBytes} from 'node:crypto';
|
|
4
|
+
import childProcess from 'node:child_process';
|
|
5
|
+
import {syncBuiltinESMExports} from 'node:module';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import {existsSync, readFileSync, renameSync, writeFileSync} from 'node:fs';
|
|
8
|
+
import {mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const moduleUrl=new URL('../src/autonomous-local-supervisor.mjs',import.meta.url);
|
|
13
|
+
const freshModule=()=>import(`${moduleUrl.href}?race=${randomBytes(8).toString('hex')}`);
|
|
14
|
+
|
|
15
|
+
async function withIdenticalReplacement(markerPath,run){
|
|
16
|
+
const original=childProcess.execFile;
|
|
17
|
+
let replaced=0;
|
|
18
|
+
const replace=()=>{
|
|
19
|
+
if(replaced===0){
|
|
20
|
+
const before=readFileSync(markerPath);
|
|
21
|
+
const successor=`${markerPath}.${randomBytes(6).toString('hex')}.successor`;
|
|
22
|
+
writeFileSync(successor,before,{flag:'wx'});
|
|
23
|
+
renameSync(successor,markerPath);
|
|
24
|
+
assert.deepEqual(readFileSync(markerPath),before,'replacement must be byte-identical');
|
|
25
|
+
replaced++;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const wrapped=function(...args){replace();return original.apply(this,args);};
|
|
29
|
+
wrapped[promisify.custom]=function(...args){replace();return original[promisify.custom].apply(original,args);};
|
|
30
|
+
childProcess.execFile=wrapped;
|
|
31
|
+
syncBuiltinESMExports();
|
|
32
|
+
try {
|
|
33
|
+
const module=await freshModule();
|
|
34
|
+
await run(module);
|
|
35
|
+
assert.equal(replaced,1,'replacement must occur after the initial marker observation');
|
|
36
|
+
} finally {
|
|
37
|
+
childProcess.execFile=original;
|
|
38
|
+
syncBuiltinESMExports();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('owned supervisor release cannot remove a byte-identical successor marker', {skip:process.platform!=='win32'}, async()=>{
|
|
43
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r111-release-'));
|
|
44
|
+
const markerPath=path.join(root,'owner.json');
|
|
45
|
+
try {
|
|
46
|
+
const baseline=await freshModule();
|
|
47
|
+
const claim=await baseline.claimSupervisor({markerPath});
|
|
48
|
+
const original=await readFile(markerPath);
|
|
49
|
+
await withIdenticalReplacement(markerPath,async({releaseSupervisor})=>{
|
|
50
|
+
await assert.rejects(releaseSupervisor({markerPath,instanceNonce:claim.instance_nonce}),/changed before release: CHANGED/i);
|
|
51
|
+
});
|
|
52
|
+
assert.equal(existsSync(markerPath),true,'successor must survive release');
|
|
53
|
+
assert.deepEqual(await readFile(markerPath),original);
|
|
54
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('stale supervisor takeover cannot overwrite a byte-identical successor marker', {skip:process.platform!=='win32'}, async()=>{
|
|
58
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r111-takeover-'));
|
|
59
|
+
const markerPath=path.join(root,'owner.json');
|
|
60
|
+
try {
|
|
61
|
+
const baseline=await freshModule();
|
|
62
|
+
const claim=await baseline.claimSupervisor({markerPath});
|
|
63
|
+
const stale={...claim.marker,creation_time_utc:'2000-01-01T00:00:00.000Z'};
|
|
64
|
+
await writeFile(markerPath,`${JSON.stringify(stale)}\n`);
|
|
65
|
+
const original=await readFile(markerPath);
|
|
66
|
+
await withIdenticalReplacement(markerPath,async({takeoverSupervisor})=>{
|
|
67
|
+
await assert.rejects(takeoverSupervisor({markerPath}),/changed during takeover: CHANGED/i);
|
|
68
|
+
});
|
|
69
|
+
assert.equal(existsSync(markerPath),true,'successor must survive takeover');
|
|
70
|
+
assert.deepEqual(await readFile(markerPath),original);
|
|
71
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('unobservable process identity cannot authorize takeover or remove its marker', {skip:process.platform!=='win32'}, async()=>{
|
|
75
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r111-unobservable-'));
|
|
76
|
+
const markerPath=path.join(root,'owner.json');
|
|
77
|
+
const originalExec=childProcess.execFile;
|
|
78
|
+
try {
|
|
79
|
+
const baseline=await freshModule();
|
|
80
|
+
await baseline.claimSupervisor({markerPath});
|
|
81
|
+
const originalMarker=await readFile(markerPath);
|
|
82
|
+
const denied=function(...args){
|
|
83
|
+
const callback=args.at(-1);
|
|
84
|
+
if(typeof callback==='function') callback(new Error('process lookup denied'));
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
denied[promisify.custom]=()=>Promise.reject(new Error('process lookup denied'));
|
|
88
|
+
childProcess.execFile=denied;
|
|
89
|
+
syncBuiltinESMExports();
|
|
90
|
+
const module=await freshModule();
|
|
91
|
+
await assert.rejects(module.takeoverSupervisor({markerPath}),/unverified|unobservable|lookup|identity/i);
|
|
92
|
+
assert.deepEqual(await readFile(markerPath),originalMarker,'uncertain lookup must not delete a live owner marker');
|
|
93
|
+
} finally {
|
|
94
|
+
childProcess.execFile=originalExec;
|
|
95
|
+
syncBuiltinESMExports();
|
|
96
|
+
await rm(root,{recursive:true,force:true});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('same live process with mismatched image evidence cannot be taken over', {skip:process.platform!=='win32'}, async()=>{
|
|
101
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r111-image-drift-'));
|
|
102
|
+
const markerPath=path.join(root,'owner.json');
|
|
103
|
+
try {
|
|
104
|
+
const module=await freshModule();
|
|
105
|
+
const claim=await module.claimSupervisor({markerPath});
|
|
106
|
+
const mismatched={...claim.marker,image_sha256:'f'.repeat(64)};
|
|
107
|
+
await writeFile(markerPath,`${JSON.stringify(mismatched)}\n`);
|
|
108
|
+
const before=await readFile(markerPath);
|
|
109
|
+
const observation=await module.observeSupervisor({markerPath});
|
|
110
|
+
assert.equal(observation.reason,'identity_mismatch');
|
|
111
|
+
assert.equal(observation.observation.pid,process.pid);
|
|
112
|
+
assert.equal(observation.observation.creation_time_utc,claim.marker.creation_time_utc);
|
|
113
|
+
await assert.rejects(module.takeoverSupervisor({markerPath}),/unverified|live|identity/i);
|
|
114
|
+
assert.deepEqual(await readFile(markerPath),before,'live owner with image drift must retain marker');
|
|
115
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
116
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {access, mkdir, mkdtemp, readFile, rm, symlink, unlink, writeFile} from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {claimSupervisor, observeSupervisor} from '../src/autonomous-local-supervisor.mjs';
|
|
7
|
+
|
|
8
|
+
async function withJunctionFixture(nested,run){
|
|
9
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r112-claim-'));
|
|
10
|
+
const outside=path.join(root,'outside');
|
|
11
|
+
const parent=nested?path.join(root,'state','runtime'):path.join(root,'state');
|
|
12
|
+
const link=path.join(parent,'redirect');
|
|
13
|
+
await mkdir(parent,{recursive:true});
|
|
14
|
+
await mkdir(outside);
|
|
15
|
+
try {
|
|
16
|
+
await symlink(outside,link,'junction');
|
|
17
|
+
return await run({markerPath:path.join(link,'owner.json'),outside});
|
|
18
|
+
} finally {
|
|
19
|
+
await unlink(link).catch(()=>{});
|
|
20
|
+
await rm(root,{recursive:true,force:true});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for(const nested of [false,true]){
|
|
25
|
+
test(`supervisor claim rejects ${nested?'nested ':' '}parent junction before creating marker`,{skip:process.platform!=='win32'},async()=>{
|
|
26
|
+
await withJunctionFixture(nested,async({markerPath,outside})=>{
|
|
27
|
+
await assert.rejects(claimSupervisor({markerPath}),/reparse|pinned|failed closed|ancestor/i);
|
|
28
|
+
await assert.rejects(access(path.join(outside,'owner.json')),{code:'ENOENT'});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('supervisor claim does not create a missing directory through a parent junction',{skip:process.platform!=='win32'},async()=>{
|
|
34
|
+
await withJunctionFixture(false,async({markerPath,outside})=>{
|
|
35
|
+
const redirected=path.join(path.dirname(markerPath),'new-child','owner.json');
|
|
36
|
+
await assert.rejects(claimSupervisor({markerPath:redirected}),/reparse|pinned|failed closed|ancestor|missing/i);
|
|
37
|
+
await assert.rejects(access(path.join(outside,'new-child')),{code:'ENOENT'});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('supervisor claim requires an existing trusted marker parent',{skip:process.platform!=='win32'},async()=>{
|
|
42
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r112-missing-parent-'));
|
|
43
|
+
const parent=path.join(root,'new-runtime');
|
|
44
|
+
try {
|
|
45
|
+
await assert.rejects(claimSupervisor({markerPath:path.join(parent,'owner.json')}),/parent|pinned|failed closed|missing/i);
|
|
46
|
+
await assert.rejects(access(parent),{code:'ENOENT'});
|
|
47
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('live image mismatch is unverified, not a stale-owner assertion',{skip:process.platform!=='win32'},async()=>{
|
|
51
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r112-status-'));
|
|
52
|
+
const markerPath=path.join(root,'owner.json');
|
|
53
|
+
try {
|
|
54
|
+
const claim=await claimSupervisor({markerPath});
|
|
55
|
+
const modified={...claim.marker,image_sha256:'f'.repeat(64)};
|
|
56
|
+
await writeFile(markerPath,`${JSON.stringify(modified)}\n`);
|
|
57
|
+
const observed=await observeSupervisor({markerPath});
|
|
58
|
+
assert.equal(observed.reason,'identity_mismatch');
|
|
59
|
+
assert.equal(observed.observation.pid,process.pid);
|
|
60
|
+
assert.equal(observed.status,'unverified');
|
|
61
|
+
assert.deepEqual(JSON.parse(await readFile(markerPath,'utf8')),modified);
|
|
62
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
63
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {randomBytes} from 'node:crypto';
|
|
4
|
+
import childProcess from 'node:child_process';
|
|
5
|
+
import {syncBuiltinESMExports} from 'node:module';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import {readFileSync, renameSync, writeFileSync} from 'node:fs';
|
|
8
|
+
import {mkdtemp, readFile, rm} from 'node:fs/promises';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const moduleUrl=new URL('../src/autonomous-local-supervisor.mjs',import.meta.url);
|
|
13
|
+
|
|
14
|
+
async function withReleaseReplacement(markerPath,run){
|
|
15
|
+
const original=childProcess.execFile;
|
|
16
|
+
let armed=false;
|
|
17
|
+
let replaced=0;
|
|
18
|
+
const replace=()=>{
|
|
19
|
+
if(!armed||replaced) return;
|
|
20
|
+
const before=readFileSync(markerPath);
|
|
21
|
+
const successor=`${markerPath}.${randomBytes(6).toString('hex')}.successor`;
|
|
22
|
+
writeFileSync(successor,before,{flag:'wx'});
|
|
23
|
+
renameSync(successor,markerPath);
|
|
24
|
+
assert.deepEqual(readFileSync(markerPath),before);
|
|
25
|
+
replaced++;
|
|
26
|
+
};
|
|
27
|
+
const wrapped=function(...args){replace();return original.apply(this,args);};
|
|
28
|
+
wrapped[promisify.custom]=function(...args){replace();return original[promisify.custom].apply(original,args);};
|
|
29
|
+
childProcess.execFile=wrapped;
|
|
30
|
+
syncBuiltinESMExports();
|
|
31
|
+
try {
|
|
32
|
+
const module=await import(`${moduleUrl.href}?cleanup=${randomBytes(8).toString('hex')}`);
|
|
33
|
+
await run({module,arm:()=>{armed=true;},replacementCount:()=>replaced});
|
|
34
|
+
assert.equal(replaced,1,'test must replace the marker at cleanup time');
|
|
35
|
+
} finally {
|
|
36
|
+
childProcess.execFile=original;
|
|
37
|
+
syncBuiltinESMExports();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('startup failure retains both primary and cleanup failure and preserves successor marker', {skip:process.platform!=='win32'}, async()=>{
|
|
42
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r113-start-'));
|
|
43
|
+
const markerPath=path.join(root,'owner.json');
|
|
44
|
+
try {
|
|
45
|
+
await withReleaseReplacement(markerPath,async({module,arm})=>{
|
|
46
|
+
const policy={get planner(){arm();throw new Error('PRIMARY_START_FAILURE');}};
|
|
47
|
+
const runtime={policy};
|
|
48
|
+
await assert.rejects(module.startLocalSupervisorHost({runtime,policy,markerPath}),error=>{
|
|
49
|
+
assert.ok(error instanceof AggregateError,'cleanup failure must not be discarded');
|
|
50
|
+
assert.match(String(error.errors[0]),/PRIMARY_START_FAILURE/);
|
|
51
|
+
assert.match(String(error.errors[1]),/changed before release: CHANGED/i);
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
assert.ok((await readFile(markerPath)).length>0,'uncertain successor marker must survive');
|
|
55
|
+
});
|
|
56
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('failed stop never reports stopped on retry or deletes a changed marker', {skip:process.platform!=='win32'}, async()=>{
|
|
60
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r113-stop-'));
|
|
61
|
+
const markerPath=path.join(root,'owner.json');
|
|
62
|
+
try {
|
|
63
|
+
await withReleaseReplacement(markerPath,async({module,arm,replacementCount})=>{
|
|
64
|
+
const policy={evidence_dir:root,planner:{backend:'client_sampling'}};
|
|
65
|
+
const runtime={policy};
|
|
66
|
+
const host=await module.startLocalSupervisorHost({runtime,policy,markerPath,continuous:false});
|
|
67
|
+
arm();
|
|
68
|
+
await assert.rejects(host.stop(),/changed before release: CHANGED/i);
|
|
69
|
+
assert.equal(replacementCount(),1);
|
|
70
|
+
const survivor=await readFile(markerPath);
|
|
71
|
+
await assert.rejects(host.stop(),/changed before release: CHANGED|cleanup|uncertain/i);
|
|
72
|
+
assert.deepEqual(await readFile(markerPath),survivor,'retry must not remove the byte-identical successor');
|
|
73
|
+
});
|
|
74
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('ordinary startup rejection releases owned marker and retains the primary error', {skip:process.platform!=='win32'}, async()=>{
|
|
78
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r113-start-clean-'));
|
|
79
|
+
const markerPath=path.join(root,'owner.json');
|
|
80
|
+
try {
|
|
81
|
+
const module=await import(`${moduleUrl.href}?clean=${randomBytes(8).toString('hex')}`);
|
|
82
|
+
const policy={evidence_dir:root,planner:{backend:'client_sampling'}};
|
|
83
|
+
await assert.rejects(module.startLocalSupervisorHost({runtime:{policy},policy,markerPath,idleDelayMs:0}),
|
|
84
|
+
/idleDelayMs out of range/);
|
|
85
|
+
assert.equal((await module.observeSupervisor({markerPath})).reason,'no_owner');
|
|
86
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('concurrent stop calls share one verified cleanup outcome', {skip:process.platform!=='win32'}, async()=>{
|
|
90
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r113-stop-concurrent-'));
|
|
91
|
+
const markerPath=path.join(root,'owner.json');
|
|
92
|
+
try {
|
|
93
|
+
const module=await import(`${moduleUrl.href}?concurrent=${randomBytes(8).toString('hex')}`);
|
|
94
|
+
const policy={evidence_dir:root,planner:{backend:'client_sampling'}};
|
|
95
|
+
const host=await module.startLocalSupervisorHost({runtime:{policy},policy,markerPath,continuous:false});
|
|
96
|
+
const results=await Promise.all([host.stop(),host.stop()]);
|
|
97
|
+
assert.deepEqual(results,[{status:'stopped'},{status:'stopped'}]);
|
|
98
|
+
assert.equal((await module.observeSupervisor({markerPath})).reason,'no_owner');
|
|
99
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
100
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {randomBytes} from 'node:crypto';
|
|
4
|
+
import childProcess from 'node:child_process';
|
|
5
|
+
import {syncBuiltinESMExports} from 'node:module';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import {mkdtemp, readFile, rm} from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
const moduleUrl=new URL('../src/autonomous-local-supervisor.mjs',import.meta.url);
|
|
12
|
+
|
|
13
|
+
test('CIM false absence cannot authorize takeover of a live marker owner', {skip:process.platform!=='win32'}, async()=>{
|
|
14
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r114-cim-absence-'));
|
|
15
|
+
const markerPath=path.join(root,'owner.json');
|
|
16
|
+
const originalExec=childProcess.execFile;
|
|
17
|
+
try {
|
|
18
|
+
const baseline=await import(`${moduleUrl.href}?baseline=${randomBytes(8).toString('hex')}`);
|
|
19
|
+
await baseline.claimSupervisor({markerPath});
|
|
20
|
+
const before=await readFile(markerPath);
|
|
21
|
+
let deniedLookups=0, falseAbsences=0;
|
|
22
|
+
const mock=function(...args){return originalExec.apply(this,args);};
|
|
23
|
+
mock[promisify.custom]=function(...args){
|
|
24
|
+
const shellArgs=Array.isArray(args[1])?args[1]:[];
|
|
25
|
+
const script=String(shellArgs[shellArgs.indexOf('-Command')+1]??'');
|
|
26
|
+
if(script.includes('Get-Process -Id')) {deniedLookups++;return Promise.reject(new Error('process identity access denied'));}
|
|
27
|
+
if(script.includes('Get-CimInstance Win32_Process')) {falseAbsences++;return Promise.resolve({stdout:'ABSENT\n',stderr:''});}
|
|
28
|
+
return originalExec[promisify.custom].apply(originalExec,args);
|
|
29
|
+
};
|
|
30
|
+
childProcess.execFile=mock;
|
|
31
|
+
syncBuiltinESMExports();
|
|
32
|
+
const module=await import(`${moduleUrl.href}?attack=${randomBytes(8).toString('hex')}`);
|
|
33
|
+
const observed=await module.observeSupervisor({markerPath});
|
|
34
|
+
assert.equal(observed.status,'unverified','a native live handle must outweigh a false CIM null');
|
|
35
|
+
assert.equal(observed.reason,'process_unobservable');
|
|
36
|
+
await assert.rejects(module.takeoverSupervisor({markerPath}),/unverified|unobservable|ownership/i);
|
|
37
|
+
assert.deepEqual(await readFile(markerPath),before,'live owner marker must remain byte-identical');
|
|
38
|
+
assert.ok(deniedLookups>=2);
|
|
39
|
+
assert.ok(falseAbsences>=2);
|
|
40
|
+
} finally {
|
|
41
|
+
childProcess.execFile=originalExec;
|
|
42
|
+
syncBuiltinESMExports();
|
|
43
|
+
await rm(root,{recursive:true,force:true});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp, rm, access} from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {createEpochNativeContinuityEngine} from '../src/activation-continuity-engine-r65.mjs';
|
|
7
|
+
|
|
8
|
+
test('POSIX continuity engine refuses activation authority before any filesystem mutation',
|
|
9
|
+
{skip:process.platform==='win32'},async()=>{
|
|
10
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r115-posix-continuity-'));
|
|
11
|
+
try{
|
|
12
|
+
assert.throws(()=>createEpochNativeContinuityEngine({
|
|
13
|
+
stateRoot:root,predecessorPolicyPaths:{},successorPolicyPaths:async()=>({}),updateId:'r115-posix'
|
|
14
|
+
}),/POSIX.*unsupported|Windows-only/i);
|
|
15
|
+
await assert.rejects(access(path.join(root,'runtime')),{code:'ENOENT'});
|
|
16
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
17
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync, randomBytes, createHash} from 'node:crypto';
|
|
4
|
+
import {mkdtemp, mkdir, writeFile, rm} from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {createAuthorityEpochStore} from '../src/authority-continuity-r65.mjs';
|
|
8
|
+
|
|
9
|
+
const phases = ['event_created', 'event_written', 'event_file_synced',
|
|
10
|
+
'event_directory_barrier', 'head_temp_created', 'head_temp_written',
|
|
11
|
+
'head_temp_file_synced', 'head_renamed', 'head_directory_barrier'];
|
|
12
|
+
const sha256 = value => createHash('sha256').update(value).digest('hex');
|
|
13
|
+
const grant = nonce => Buffer.from(JSON.stringify({instance_nonce:nonce, fixture:'R116 isolated'}));
|
|
14
|
+
|
|
15
|
+
async function fixture(run) {
|
|
16
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r116-epoch-'));
|
|
17
|
+
try {
|
|
18
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
19
|
+
const trust = path.join(root, 'trust');
|
|
20
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
21
|
+
const keys = generateKeyPairSync('ed25519');
|
|
22
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'), keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
23
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'), keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
24
|
+
await run(runtimeDir);
|
|
25
|
+
} finally {
|
|
26
|
+
await rm(root, {recursive:true, force:true});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const phase of phases) {
|
|
31
|
+
test(`R116 interrupted user revoke at ${phase} never verifies the old grant`, () => fixture(async runtimeDir => {
|
|
32
|
+
const oldNonce = randomBytes(32).toString('hex');
|
|
33
|
+
const oldGrant = grant(oldNonce);
|
|
34
|
+
const store = createAuthorityEpochStore({runtimeDir});
|
|
35
|
+
await store.append({plane:'process', action:'grant', grantBytes:oldGrant, grantInstanceNonce:oldNonce});
|
|
36
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes:oldGrant, grantInstanceNonce:oldNonce})).trusted, true);
|
|
37
|
+
const interrupted = createAuthorityEpochStore({runtimeDir, onDurabilityStage:at => {
|
|
38
|
+
if (at === phase) throw new Error(`R116_INTERRUPTED:${at}`);
|
|
39
|
+
}});
|
|
40
|
+
await assert.rejects(interrupted.append({plane:'process', action:'user_revoke', grantBytes:oldGrant,
|
|
41
|
+
grantInstanceNonce:oldNonce}), /R116_INTERRUPTED/);
|
|
42
|
+
const fresh = createAuthorityEpochStore({runtimeDir});
|
|
43
|
+
assert.equal((await fresh.verifyGrant({plane:'process', grantBytes:oldGrant,
|
|
44
|
+
grantInstanceNonce:oldNonce})).trusted, false);
|
|
45
|
+
if (phase === 'head_renamed' || phase === 'head_directory_barrier') {
|
|
46
|
+
assert.equal((await fresh.readHead({plane:'process'})).action, 'user_revoke');
|
|
47
|
+
} else {
|
|
48
|
+
await assert.rejects(fresh.readHead({plane:'process'}), /replay or incomplete write/);
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
test(`R116 interrupted transaction restore at ${phase} is observed, never inferred`, () => fixture(async runtimeDir => {
|
|
53
|
+
const oldNonce = randomBytes(32).toString('hex');
|
|
54
|
+
const newNonce = randomBytes(32).toString('hex');
|
|
55
|
+
const oldGrant = grant(oldNonce);
|
|
56
|
+
const newGrant = grant(newNonce);
|
|
57
|
+
const store = createAuthorityEpochStore({runtimeDir});
|
|
58
|
+
const original = await store.append({plane:'process', action:'grant', grantBytes:oldGrant,
|
|
59
|
+
grantInstanceNonce:oldNonce});
|
|
60
|
+
const revoked = await store.append({plane:'process', action:'transaction_revoke', grantBytes:oldGrant,
|
|
61
|
+
grantInstanceNonce:oldNonce, updateId:'r116-isolated', expectedHeadSha256:original.event_sha256});
|
|
62
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes:oldGrant,
|
|
63
|
+
grantInstanceNonce:oldNonce})).trusted, false);
|
|
64
|
+
const interrupted = createAuthorityEpochStore({runtimeDir, onDurabilityStage:at => {
|
|
65
|
+
if (at === phase) throw new Error(`R116_INTERRUPTED:${at}`);
|
|
66
|
+
}});
|
|
67
|
+
const restoreBinding = {
|
|
68
|
+
prior_grant_sha256:sha256(oldGrant), prior_grant_instance_nonce:oldNonce,
|
|
69
|
+
predecessor_policy_envelope_sha256:'1'.repeat(64),
|
|
70
|
+
successor_policy_envelope_sha256:'2'.repeat(64),
|
|
71
|
+
candidate_manifest_sha256:'3'.repeat(64), continuity_subject_sha256:'4'.repeat(64)
|
|
72
|
+
};
|
|
73
|
+
await assert.rejects(interrupted.append({plane:'process', action:'transaction_restore', grantBytes:newGrant,
|
|
74
|
+
grantInstanceNonce:newNonce, updateId:'r116-isolated', expectedHeadSha256:revoked.event_sha256,
|
|
75
|
+
restoreBinding}), /R116_INTERRUPTED/);
|
|
76
|
+
const fresh = createAuthorityEpochStore({runtimeDir});
|
|
77
|
+
const observed = await fresh.verifyGrant({plane:'process', grantBytes:newGrant, grantInstanceNonce:newNonce});
|
|
78
|
+
const committed = phase === 'head_renamed' || phase === 'head_directory_barrier';
|
|
79
|
+
assert.equal(observed.trusted, committed);
|
|
80
|
+
if (committed) assert.equal((await fresh.readHead({plane:'process'})).action, 'transaction_restore');
|
|
81
|
+
else await assert.rejects(fresh.readHead({plane:'process'}), /replay or incomplete write/);
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync, randomBytes} from 'node:crypto';
|
|
4
|
+
import {mkdtemp, mkdir, readFile, writeFile, rm} from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {pathToFileURL} from 'node:url';
|
|
8
|
+
|
|
9
|
+
// A writable development checkout cannot load the privileged Windows addon.
|
|
10
|
+
// The optional test-only override must point to a protected, byte-identical
|
|
11
|
+
// source installation; callers verify that hash before using this mode.
|
|
12
|
+
const epochModule = process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE
|
|
13
|
+
? pathToFileURL(process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE).href
|
|
14
|
+
: new URL('../src/authority-continuity-r65.mjs', import.meta.url).href;
|
|
15
|
+
const {createAuthorityEpochStore} = await import(epochModule);
|
|
16
|
+
|
|
17
|
+
test('R117 two independent recoverers commit one orphaned revoke at most once', async () => {
|
|
18
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r117-recovery-'));
|
|
19
|
+
try {
|
|
20
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
21
|
+
const trust = path.join(root, 'trust');
|
|
22
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
23
|
+
const keys = generateKeyPairSync('ed25519');
|
|
24
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'), keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
25
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'), keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
26
|
+
const nonce = randomBytes(32).toString('hex');
|
|
27
|
+
const bytes = Buffer.from(JSON.stringify({instance_nonce:nonce, fixture:'R117 isolated'}));
|
|
28
|
+
const writer = createAuthorityEpochStore({runtimeDir});
|
|
29
|
+
const granted = await writer.append({plane:'process', action:'grant', grantBytes:bytes, grantInstanceNonce:nonce});
|
|
30
|
+
const headPath = path.join(runtimeDir, 'authority-epochs', 'process', 'head.json');
|
|
31
|
+
const predecessorBytes = await readFile(headPath);
|
|
32
|
+
const orphan = await writer.append({plane:'process', action:'user_revoke', grantBytes:bytes, grantInstanceNonce:nonce});
|
|
33
|
+
// Simulate interruption before the new head becomes visible. The signed
|
|
34
|
+
// revoke event remains, while the exact prior signed head is restored.
|
|
35
|
+
await writeFile(headPath, predecessorBytes);
|
|
36
|
+
const observed = createAuthorityEpochStore({runtimeDir});
|
|
37
|
+
assert.equal((await observed.verifyGrant({plane:'process', grantBytes:bytes, grantInstanceNonce:nonce})).trusted, false);
|
|
38
|
+
const recover = () => createAuthorityEpochStore({runtimeDir}).reconcileOrphanedRevocation({
|
|
39
|
+
plane:'process', expectedHeadSha256:granted.event_sha256,
|
|
40
|
+
expectedOrphanSha256:orphan.event_sha256, operatorConfirmed:true
|
|
41
|
+
});
|
|
42
|
+
const outcomes = await Promise.allSettled([recover(), recover()]);
|
|
43
|
+
assert.equal(outcomes.filter(item => item.status === 'fulfilled').length, 1,
|
|
44
|
+
'two independent attempts cannot both claim the same recovery');
|
|
45
|
+
assert.equal(outcomes.filter(item => item.status === 'rejected').length, 1);
|
|
46
|
+
assert.equal(outcomes.find(item => item.status === 'fulfilled').value.status, 'REVOKED_ONLY_RECONCILED');
|
|
47
|
+
const finalStore = createAuthorityEpochStore({runtimeDir});
|
|
48
|
+
assert.equal((await finalStore.readHead({plane:'process'})).event_sha256, orphan.event_sha256);
|
|
49
|
+
assert.equal((await finalStore.verifyGrant({plane:'process', grantBytes:bytes,
|
|
50
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
51
|
+
} finally {
|
|
52
|
+
await rm(root, {recursive:true, force:true});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync, randomBytes} from 'node:crypto';
|
|
4
|
+
import {spawn} from 'node:child_process';
|
|
5
|
+
import {mkdtemp, mkdir, readFile, writeFile, rm} from 'node:fs/promises';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {pathToFileURL} from 'node:url';
|
|
9
|
+
|
|
10
|
+
const moduleUrl = process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE
|
|
11
|
+
? pathToFileURL(process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE).href
|
|
12
|
+
: new URL('../src/authority-continuity-r65.mjs', import.meta.url).href;
|
|
13
|
+
const {createAuthorityEpochStore} = await import(moduleUrl);
|
|
14
|
+
|
|
15
|
+
function recoverer(runtimeDir, expectedHeadSha256, expectedOrphanSha256) {
|
|
16
|
+
// Each invocation is a separate OS process using the real native lock.
|
|
17
|
+
// IPC is only a starting barrier and result channel; it cannot bypass the
|
|
18
|
+
// authority store's signature, file identity, or ownership checks.
|
|
19
|
+
const code = `
|
|
20
|
+
import {createAuthorityEpochStore} from ${JSON.stringify(moduleUrl)};
|
|
21
|
+
process.once('message', async () => {
|
|
22
|
+
let result;
|
|
23
|
+
try {
|
|
24
|
+
const value = await createAuthorityEpochStore({runtimeDir:${JSON.stringify(runtimeDir)}})
|
|
25
|
+
.reconcileOrphanedRevocation({plane:'process',
|
|
26
|
+
expectedHeadSha256:${JSON.stringify(expectedHeadSha256)},
|
|
27
|
+
expectedOrphanSha256:${JSON.stringify(expectedOrphanSha256)},operatorConfirmed:true});
|
|
28
|
+
result = {kind:'RESULT',status:'FULFILLED',value:value.status};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
result = {kind:'RESULT',status:'REJECTED',reason:String(error?.message??error)};
|
|
31
|
+
}
|
|
32
|
+
process.send(result, () => process.disconnect());
|
|
33
|
+
});
|
|
34
|
+
process.send({kind:'READY'});
|
|
35
|
+
`;
|
|
36
|
+
const child = spawn(process.execPath, ['--input-type=module', '-e', code], {
|
|
37
|
+
env:process.env, stdio:['ignore', 'ignore', 'pipe', 'ipc'], windowsHide:true
|
|
38
|
+
});
|
|
39
|
+
let stderr = '';
|
|
40
|
+
child.stderr.on('data', chunk => { stderr = (stderr + chunk.toString()).slice(-2048); });
|
|
41
|
+
let readyResolve, readyReject, resultResolve, resultReject;
|
|
42
|
+
const ready = new Promise((resolve, reject) => {readyResolve=resolve; readyReject=reject;});
|
|
43
|
+
const result = new Promise((resolve, reject) => {resultResolve=resolve; resultReject=reject;});
|
|
44
|
+
let readySeen = false, resultSeen = false;
|
|
45
|
+
child.on('message', message => {
|
|
46
|
+
if (message?.kind === 'READY') {readySeen=true; readyResolve();}
|
|
47
|
+
if (message?.kind === 'RESULT') {resultSeen=true; resultResolve(message);}
|
|
48
|
+
});
|
|
49
|
+
child.once('error', error => {readyReject(error); resultReject(error);});
|
|
50
|
+
child.once('exit', (code, signal) => {
|
|
51
|
+
const error = new Error(`R118 child exited before result code=${code} signal=${signal} stderr=${stderr}`);
|
|
52
|
+
if (!readySeen) readyReject(error);
|
|
53
|
+
if (!resultSeen) resultReject(error);
|
|
54
|
+
});
|
|
55
|
+
return {child, ready, result};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('R118 two OS processes cannot both commit one signed orphaned user revoke',
|
|
59
|
+
{timeout:30000}, async () => {
|
|
60
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r118-cross-process-'));
|
|
61
|
+
const children = [];
|
|
62
|
+
try {
|
|
63
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
64
|
+
const trust = path.join(root, 'trust');
|
|
65
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
66
|
+
const keys = generateKeyPairSync('ed25519');
|
|
67
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'),
|
|
68
|
+
keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
69
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'),
|
|
70
|
+
keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
71
|
+
const nonce = randomBytes(32).toString('hex');
|
|
72
|
+
const grantBytes = Buffer.from(JSON.stringify({instance_nonce:nonce, fixture:'R118 isolated'}));
|
|
73
|
+
const store = createAuthorityEpochStore({runtimeDir});
|
|
74
|
+
const granted = await store.append({plane:'process', action:'grant',
|
|
75
|
+
grantBytes, grantInstanceNonce:nonce});
|
|
76
|
+
const headPath = path.join(runtimeDir, 'authority-epochs', 'process', 'head.json');
|
|
77
|
+
const predecessorBytes = await readFile(headPath);
|
|
78
|
+
const orphan = await store.append({plane:'process', action:'user_revoke',
|
|
79
|
+
grantBytes, grantInstanceNonce:nonce});
|
|
80
|
+
await writeFile(headPath, predecessorBytes);
|
|
81
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes,
|
|
82
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
83
|
+
children.push(recoverer(runtimeDir, granted.event_sha256, orphan.event_sha256));
|
|
84
|
+
children.push(recoverer(runtimeDir, granted.event_sha256, orphan.event_sha256));
|
|
85
|
+
await Promise.all(children.map(item => item.ready));
|
|
86
|
+
children.forEach(item => item.child.send({kind:'GO'}));
|
|
87
|
+
const results = await Promise.all(children.map(item => item.result));
|
|
88
|
+
assert.equal(results.filter(item => item.status === 'FULFILLED').length, 1, JSON.stringify(results));
|
|
89
|
+
assert.equal(results.filter(item => item.status === 'REJECTED').length, 1, JSON.stringify(results));
|
|
90
|
+
assert.equal(results.find(item => item.status === 'FULFILLED').value, 'REVOKED_ONLY_RECONCILED');
|
|
91
|
+
const fresh = createAuthorityEpochStore({runtimeDir});
|
|
92
|
+
assert.equal((await fresh.readHead({plane:'process'})).event_sha256, orphan.event_sha256);
|
|
93
|
+
assert.equal((await fresh.verifyGrant({plane:'process', grantBytes,
|
|
94
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
95
|
+
} finally {
|
|
96
|
+
for (const item of children) if (item.child.exitCode === null) item.child.kill();
|
|
97
|
+
await rm(root, {recursive:true, force:true, maxRetries:8, retryDelay:100});
|
|
98
|
+
}
|
|
99
|
+
});
|