deadbyte-mcp 0.15.74 → 0.15.76
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 +108 -42
- package/PROVENANCE.json +20 -22
- package/README.txt +23 -18
- package/RELEASE.json +69 -18
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +32 -2
- package/bin/deadbyte-desktop-uia-r199-fixed.exe +0 -0
- package/bin/deadbyte-desktop-uia-r199-fixed.obj +0 -0
- package/bin/deadbyte-desktop-uia-r199.exe +0 -0
- package/bin/deadbyte-desktop-uia-r199.obj +0 -0
- package/bin/deadbyte-fs-read-r197-fixed.exe +0 -0
- package/bin/deadbyte-fs-read-r198.exe +0 -0
- package/bin/deadbyte-fs-read-r198.obj +0 -0
- package/bin/deadbyte-root-relative-probe-r197.exe +0 -0
- package/bridge/package.json +1 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R184-package-lock.json +2 -8
- package/native/deadbyte-desktop-uia-r199.cpp +345 -0
- package/native/deadbyte-fs-read-r197.cpp +174 -0
- package/native/deadbyte-fs-read-r198.cpp +181 -0
- package/native/deadbyte-root-relative-probe-r197.cpp +97 -0
- package/package.json +5 -2
- package/scripts/build-native-desktop-uia-r199-fixed.ps1 +28 -0
- package/scripts/build-native-desktop-uia-r199.ps1 +28 -0
- package/scripts/build-native-fs-read-r197-fixed.ps1 +28 -0
- package/scripts/build-native-fs-read-r197.ps1 +28 -0
- package/scripts/build-native-fs-read-r198.ps1 +28 -0
- package/scripts/build-native-root-probe-r197.ps1 +28 -0
- package/scripts/gate-windows-r195.ps1 +114 -0
- package/scripts/gate-windows-r206.ps1 +116 -0
- package/scripts/gate-windows-r207.ps1 +104 -0
- package/scripts/init-desktop-policy-r199.ps1 +85 -0
- package/scripts/npm-release-r48.mjs +27 -0
- package/scripts/npm-release-readiness-r195.mjs +72 -0
- package/scripts/npm-release-readiness-r206.mjs +76 -0
- package/scripts/npm-release-readiness-r207.mjs +76 -0
- package/scripts/r193-owner-request-live.mjs +8 -7
- package/scripts/r193-owner-uac-broker.mjs +6 -5
- package/scripts/r205-candidate-selection.mjs +22 -0
- package/src/candidate-selection-evidence-r206.mjs +71 -0
- package/src/candidate-selection-r205.mjs +96 -0
- package/src/desktop-postcondition-r196.mjs +30 -0
- package/src/desktop-runtime-r40.mjs +44 -7
- package/src/desktop-uia-identity-r199.mjs +51 -0
- package/src/desktop-uia-r40.mjs +9 -1
- package/src/machine-fs-handle-read-r196.mjs +43 -0
- package/src/machine-fs-native-read-r197.mjs +73 -0
- package/src/machine-fs-native-read-r198.mjs +76 -0
- package/src/machine-fs-read-worker.mjs +5 -25
- package/src/machine-fs.mjs +31 -16
- package/src/r195-canonical-owner-observation.mjs +15 -0
- package/src/r207-owner-enrollment-io.mjs +30 -0
- package/src/remote-destination-journal-r202.mjs +120 -0
- package/src/remote-device-r44.mjs +89 -14
- package/src/remote-payload-boundary-r201.mjs +71 -0
- package/src/remote-terminal-evidence-r200.mjs +60 -0
- package/src/version.mjs +1 -1
- package/test/desktop-runtime-r40.test.mjs +111 -0
- package/test/desktop-uia-r40.test.mjs +15 -0
- package/test/fixtures/r197-junction-swap-worker.mjs +21 -0
- package/test/fixtures/r203-journal-writer.mjs +6 -0
- package/test/process-policy-refresh.test.mjs +4 -4
- package/test/r130-release-security-contract.test.mjs +8 -8
- package/test/r132-release-identity.test.mjs +7 -7
- package/test/r153-prepublish-identity.test.mjs +1 -1
- package/test/r160-release-contract.test.mjs +3 -3
- package/test/r165-live-cli-skew.test.mjs +1 -1
- package/test/r167-release-contract.test.mjs +11 -11
- package/test/r168-release-contract.test.mjs +4 -4
- package/test/r172-release-contract.test.mjs +6 -6
- package/test/r173-release-contract.test.mjs +16 -16
- package/test/r179-release-contract.test.mjs +12 -12
- package/test/r190-release-contract.test.mjs +10 -10
- package/test/r191-release-contract.test.mjs +12 -12
- package/test/r192-release-identity.test.mjs +9 -9
- package/test/r193-release-identity.test.mjs +24 -22
- package/test/r193-release-readiness.test.mjs +2 -2
- package/test/r193-windows-gate.test.mjs +10 -10
- package/test/r194-release-identity.test.mjs +19 -15
- package/test/r194-release-readiness.test.mjs +3 -3
- package/test/r194-windows-gate.test.mjs +9 -8
- package/test/r195-canonical-owner-observation.test.mjs +32 -0
- package/test/r195-release-readiness.test.mjs +22 -0
- package/test/r196-desktop-postcondition.test.mjs +53 -0
- package/test/r196-handle-bound-read.test.mjs +99 -0
- package/test/r196-remote-outcome.test.mjs +94 -0
- package/test/r197-namespace-race.test.mjs +41 -0
- package/test/r197-native-dispatch-race.test.mjs +49 -0
- package/test/r197-native-file-read.test.mjs +60 -0
- package/test/r197-native-root-open.test.mjs +61 -0
- package/test/r198-root-identity.test.mjs +88 -0
- package/test/r199-native-uia-worker.test.mjs +28 -0
- package/test/r199-uia-identity.test.mjs +43 -0
- package/test/r200-remote-reconcile.test.mjs +106 -0
- package/test/r201-payload-boundary.test.mjs +78 -0
- package/test/r201-payload-fuzz.test.mjs +37 -0
- package/test/r201-tool-boundary-fuzz.test.mjs +120 -0
- package/test/r202-destination-journal.test.mjs +92 -0
- package/test/r203-atomic-journal.test.mjs +86 -0
- package/test/r205-candidate-selection-cli.test.mjs +31 -0
- package/test/r205-candidate-selection.test.mjs +83 -0
- package/test/r206-candidate-evidence.test.mjs +64 -0
- package/test/r206-release-contract.test.mjs +42 -0
- package/test/r206-release-readiness.test.mjs +23 -0
- package/test/r207-owner-enrollment-io.test.mjs +37 -0
- package/test/r207-release-contract.test.mjs +51 -0
- package/test/r207-release-readiness.test.mjs +23 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r64-machine-fs-large-read.test.mjs +26 -12
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
5
|
+
const sha=value=>createHash('sha256').update(canonicalJson(value),'utf8').digest('hex');
|
|
6
|
+
function insist(ok,message){if(!ok)throw new Error(message);}
|
|
7
|
+
export function normalizeUiaRuntimeIdR199(value){
|
|
8
|
+
insist(Array.isArray(value)&&value.length>=1&&value.length<=64,'R199 UIA RuntimeId missing or unbounded');
|
|
9
|
+
insist(value.every(x=>Number.isInteger(x)&&x>=-2147483648&&x<=2147483647),
|
|
10
|
+
'R199 UIA RuntimeId component invalid');
|
|
11
|
+
return Object.freeze([...value]);
|
|
12
|
+
}
|
|
13
|
+
function eqArray(a,b){return a.length===b.length&&a.every((v,i)=>v===b[i]);}
|
|
14
|
+
function eqRect(a,b){return ['left','top','width','height'].every(k=>a?.[k]===b?.[k]);}
|
|
15
|
+
export function bindUiaElementR199({frame,grounded,candidate,query}={}){
|
|
16
|
+
insist(frame?.frame_id&&HEX64.test(frame.frame_id),'R199 UIA frame identity missing');
|
|
17
|
+
insist(typeof grounded?.query_sha256==='string'&&HEX64.test(grounded.query_sha256),'R199 UIA query hash invalid');
|
|
18
|
+
insist(grounded.truncated===false,'R199 UIA scan truncated');
|
|
19
|
+
const runtimeId=normalizeUiaRuntimeIdR199(candidate?.runtime_id);
|
|
20
|
+
insist(candidate.enabled===true&&!candidate.offscreen,'R199 UIA element not actionable');
|
|
21
|
+
const q=Object.freeze({
|
|
22
|
+
kind:query.kind,value:query.value,
|
|
23
|
+
max_results:query.max_results??8,ordinal:query.ordinal??0
|
|
24
|
+
});
|
|
25
|
+
const tokenSha256=sha({schema:'deadbyte.uia-element-binding-r199.v1',
|
|
26
|
+
frame_id:frame.frame_id,desktop_epoch:frame.desktop_epoch,
|
|
27
|
+
active_window:frame.active_window,query_sha256:grounded.query_sha256,
|
|
28
|
+
runtime_id:runtimeId,control_type:candidate.control_type,rect:candidate.rect});
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
schema:'deadbyte.uia-element-binding-r199.v1',
|
|
31
|
+
query:q,query_sha256:grounded.query_sha256,ordinal:grounded.ordinal,
|
|
32
|
+
runtime_id:runtimeId,control_type:candidate.control_type,
|
|
33
|
+
rect:Object.freeze({...candidate.rect}),token_sha256:tokenSha256
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function assertFreshUiaElementR199(binding,grounded){
|
|
37
|
+
insist(binding?.schema==='deadbyte.uia-element-binding-r199.v1','R199 UIA binding missing');
|
|
38
|
+
insist(grounded?.status==='ok'&&grounded.truncated===false,'R199 UIA reground scan invalid or incomplete');
|
|
39
|
+
insist(grounded.query_sha256===binding.query_sha256 &&
|
|
40
|
+
grounded.ordinal===binding.ordinal,'R199 UIA query identity changed');
|
|
41
|
+
insist(Array.isArray(grounded.matches)&&binding.ordinal<grounded.matches.length,
|
|
42
|
+
'R199 UIA stale target no longer found');
|
|
43
|
+
const candidate=grounded.matches[binding.ordinal];
|
|
44
|
+
const current=normalizeUiaRuntimeIdR199(candidate?.runtime_id);
|
|
45
|
+
insist(eqArray(binding.runtime_id,current),'R199 UIA element identity changed');
|
|
46
|
+
insist(candidate.control_type===binding.control_type&&candidate.enabled===true&&!candidate.offscreen,
|
|
47
|
+
'R199 UIA control state changed');
|
|
48
|
+
insist(eqRect(candidate.rect,binding.rect),'R199 UIA element rectangle changed');
|
|
49
|
+
return Object.freeze({token_sha256:binding.token_sha256,
|
|
50
|
+
runtime_id_sha256:sha(current),verified:true});
|
|
51
|
+
}
|
package/src/desktop-uia-r40.mjs
CHANGED
|
@@ -73,10 +73,18 @@ export function parseDesktopUiaResponse(bytes,{expectedWindow,query,expectedBoun
|
|
|
73
73
|
};
|
|
74
74
|
const inside=rect.left>=left&&rect.top>=top&&rect.left+rect.width<=left+width&&rect.top+rect.height<=top+height;
|
|
75
75
|
if(!inside) continue;
|
|
76
|
+
let runtimeId=null;
|
|
77
|
+
if(item.runtime_id!==undefined){
|
|
78
|
+
insist(Array.isArray(item.runtime_id)&&item.runtime_id.length<=64,'desktop UIA RuntimeId shape invalid');
|
|
79
|
+
if(item.runtime_id.length===0)insist(q.kind==='root','desktop UIA RuntimeId missing for non-root');
|
|
80
|
+
else runtimeId=Object.freeze(item.runtime_id.map(v=>
|
|
81
|
+
integer(v,'desktop UIA RuntimeId component',{min:-2147483648,max:2147483647})));
|
|
82
|
+
}
|
|
76
83
|
matches.push(Object.freeze({
|
|
77
84
|
rect:Object.freeze(rect),
|
|
78
85
|
control_type:integer(item.control_type,'desktop UIA control_type',{min:0,max:100000}),
|
|
79
|
-
enabled:item.enabled===true,offscreen:item.offscreen===true
|
|
86
|
+
enabled:item.enabled===true,offscreen:item.offscreen===true,
|
|
87
|
+
runtime_id:runtimeId
|
|
80
88
|
}));
|
|
81
89
|
}
|
|
82
90
|
return Object.freeze({
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
function sameMetadata(a,b){
|
|
4
|
+
return a.dev===b.dev&&a.ino===b.ino&&a.size===b.size&&
|
|
5
|
+
a.mtimeNs===b.mtimeNs&&a.ctimeNs===b.ctimeNs;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Hash bytes through the same open FileHandle used for the range read.
|
|
9
|
+
// This does not prove that the pathname was safe before open, or that
|
|
10
|
+
// concurrent in-place writes cannot alter data between individual reads.
|
|
11
|
+
export async function readRangeFromHandle(handle,{offset,length,hashFile=false,signal}){
|
|
12
|
+
if(!handle||typeof handle.read!=='function'||typeof handle.stat!=='function'||
|
|
13
|
+
typeof handle.createReadStream!=='function')throw new TypeError('R196 requires a live FileHandle');
|
|
14
|
+
if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(length)||length<0||length>4194304)
|
|
15
|
+
throw new RangeError('R196 invalid bounded file range');
|
|
16
|
+
const before=await handle.stat({bigint:true});
|
|
17
|
+
if(!before.isFile())throw new Error('R196 read requires a regular file');
|
|
18
|
+
if(before.size>BigInt(Number.MAX_SAFE_INTEGER))throw new Error('R196 file size exceeds safe integer');
|
|
19
|
+
const fileSize=Number(before.size);
|
|
20
|
+
if(offset>fileSize)throw new Error('R196 offset exceeds opened file size');
|
|
21
|
+
const buffer=Buffer.alloc(Math.min(length,fileSize-offset));
|
|
22
|
+
const read=buffer.length?await handle.read(buffer,0,buffer.length,offset):{bytesRead:0};
|
|
23
|
+
let digest=null;
|
|
24
|
+
if(hashFile){
|
|
25
|
+
const hash=createHash('sha256');
|
|
26
|
+
await new Promise((resolve,reject)=>{
|
|
27
|
+
const stream=handle.createReadStream({start:0,autoClose:false,signal});
|
|
28
|
+
stream.on('data',chunk=>hash.update(chunk));
|
|
29
|
+
stream.once('error',reject);
|
|
30
|
+
stream.once('end',resolve);
|
|
31
|
+
});
|
|
32
|
+
digest=hash.digest('hex');
|
|
33
|
+
}
|
|
34
|
+
const after=await handle.stat({bigint:true});
|
|
35
|
+
if(!sameMetadata(before,after))throw new Error('R196 opened file changed during read/hash');
|
|
36
|
+
return {
|
|
37
|
+
schema:'deadbyte.machine-fs-read-worker.v1',
|
|
38
|
+
bytes_read:read.bytesRead,
|
|
39
|
+
data_base64:buffer.subarray(0,read.bytesRead).toString('base64'),
|
|
40
|
+
file_sha256:digest,file_size:fileSize,
|
|
41
|
+
file_dev:String(before.dev),file_ino:String(before.ino)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {lstat,readFile} from 'node:fs/promises';
|
|
3
|
+
import {spawn} from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {validatePortableRelativePath} from './path-policy.mjs';
|
|
7
|
+
|
|
8
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
9
|
+
const DECIMAL=/^(?:0|[1-9][0-9]{0,19})$/;
|
|
10
|
+
const EXECUTABLE=fileURLToPath(new URL('../bin/deadbyte-fs-read-r197-fixed.exe',import.meta.url));
|
|
11
|
+
export const R197_NATIVE_READ_EXE_SHA256='7477ca3d94168c8b75711b734df5b9c20a43ecbd9338cfc234700d7e27982b63';
|
|
12
|
+
const hash=b=>createHash('sha256').update(b).digest('hex');
|
|
13
|
+
let pinnedInfo=null;
|
|
14
|
+
async function verifyExecutable(){
|
|
15
|
+
const info=await lstat(EXECUTABLE);
|
|
16
|
+
if(!info.isFile()||info.isSymbolicLink()||info.size<1024||info.size>4*1024*1024)
|
|
17
|
+
throw new Error('R197 native file reader binary is not a regular bounded executable');
|
|
18
|
+
// Only a trust-state fast path, NOT a general same-user tamper sandbox.
|
|
19
|
+
const current=hash(await readFile(EXECUTABLE));
|
|
20
|
+
if(current!==R197_NATIVE_READ_EXE_SHA256)throw new Error('R197 native reader executable SHA-256 mismatch');
|
|
21
|
+
pinnedInfo={sha256:current,bytes:info.size};
|
|
22
|
+
return pinnedInfo;
|
|
23
|
+
}
|
|
24
|
+
export async function boundedNativeReadR197({rootPath,relativePath,offset,length,hashFile=false,timeoutMs=30000}={}){
|
|
25
|
+
if(process.platform!=='win32')throw new Error('R197 native root-relative reader requires Windows');
|
|
26
|
+
if(typeof rootPath!=='string'||!path.win32.isAbsolute(rootPath)||!path.isAbsolute(rootPath))
|
|
27
|
+
throw new Error('R197 native root path invalid');
|
|
28
|
+
validatePortableRelativePath(relativePath);
|
|
29
|
+
if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(length)||length<0||length>4*1024*1024)
|
|
30
|
+
throw new Error('R197 native bounded range invalid');
|
|
31
|
+
if(!Number.isInteger(timeoutMs)||timeoutMs<100||timeoutMs>180000)
|
|
32
|
+
throw new Error('R197 native timeout invalid');
|
|
33
|
+
await verifyExecutable();
|
|
34
|
+
return await new Promise((resolve,reject)=>{
|
|
35
|
+
const args=[rootPath,relativePath,String(offset),String(length),hashFile?'1':'0'];
|
|
36
|
+
const child=spawn(EXECUTABLE,args,{windowsHide:true,shell:false,stdio:['ignore','pipe','pipe']});
|
|
37
|
+
const chunks=[];let byteCount=0,stderr='',settled=false,timer=null;
|
|
38
|
+
const finish=(error,value)=>{
|
|
39
|
+
if(settled)return;
|
|
40
|
+
settled=true;if(timer!==null)clearTimeout(timer);
|
|
41
|
+
error?reject(error):resolve(value);
|
|
42
|
+
};
|
|
43
|
+
child.stdout.on('data',chunk=>{
|
|
44
|
+
byteCount+=chunk.length;
|
|
45
|
+
if(byteCount>8*1024*1024){child.kill();finish(new Error('R197 native read stdout exceeds bound'));return;}
|
|
46
|
+
chunks.push(Buffer.from(chunk));
|
|
47
|
+
});
|
|
48
|
+
child.stderr.on('data',chunk=>{
|
|
49
|
+
stderr=(stderr+chunk.toString('utf8')).slice(-16384);
|
|
50
|
+
});
|
|
51
|
+
child.on('error',error=>finish(error));
|
|
52
|
+
child.on('close',(code,signal)=>{
|
|
53
|
+
if(settled)return;
|
|
54
|
+
if(code!==0)return finish(new Error('R197 native root-relative read refused exit='+code+' signal='+(signal??'none')+' '+stderr.slice(-512)));
|
|
55
|
+
let response;
|
|
56
|
+
try{response=JSON.parse(Buffer.concat(chunks).toString('utf8'));}
|
|
57
|
+
catch{return finish(new Error('R197 native read response invalid JSON'));}
|
|
58
|
+
const data=typeof response?.data_base64==='string'?Buffer.from(response.data_base64,'base64'):null;
|
|
59
|
+
if(response?.schema!=='deadbyte.machine-fs-read-worker.v1'||!data||
|
|
60
|
+
response.bytes_read!==data.length||data.length>length||
|
|
61
|
+
!Number.isSafeInteger(response.file_size)||!DECIMAL.test(response.file_dev??'')||
|
|
62
|
+
!DECIMAL.test(response.file_ino??'')||
|
|
63
|
+
(hashFile?!SHA.test(response.file_sha256??''):response.file_sha256!==null))
|
|
64
|
+
return finish(new Error('R197 native read response contract mismatch'));
|
|
65
|
+
return finish(null,response);
|
|
66
|
+
});
|
|
67
|
+
timer=setTimeout(()=>{
|
|
68
|
+
child.kill();
|
|
69
|
+
finish(new Error('R197 native read timed out after '+timeoutMs+'ms'));
|
|
70
|
+
},timeoutMs);
|
|
71
|
+
timer.unref?.();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {lstat,readFile} from 'node:fs/promises';
|
|
3
|
+
import {spawn} from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {validatePortableRelativePath} from './path-policy.mjs';
|
|
7
|
+
|
|
8
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
9
|
+
const DECIMAL=/^(?:0|[1-9][0-9]{0,19})$/;
|
|
10
|
+
const EXECUTABLE=fileURLToPath(new URL('../bin/deadbyte-fs-read-r198.exe',import.meta.url));
|
|
11
|
+
export const R198_NATIVE_READ_EXE_SHA256='4325ae4f033bba195b34d2c49b48493a1a468f3443e369a9bb887a7a129eac16';
|
|
12
|
+
const hash=b=>createHash('sha256').update(b).digest('hex');
|
|
13
|
+
let pinnedInfo=null;
|
|
14
|
+
async function verifyExecutable(){
|
|
15
|
+
const info=await lstat(EXECUTABLE);
|
|
16
|
+
if(!info.isFile()||info.isSymbolicLink()||info.size<1024||info.size>4*1024*1024)
|
|
17
|
+
throw new Error('R198 native file reader binary is not a regular bounded executable');
|
|
18
|
+
// Only a trust-state fast path, NOT a general same-user tamper sandbox.
|
|
19
|
+
const current=hash(await readFile(EXECUTABLE));
|
|
20
|
+
if(current!==R198_NATIVE_READ_EXE_SHA256)throw new Error('R198 native reader executable SHA-256 mismatch');
|
|
21
|
+
pinnedInfo={sha256:current,bytes:info.size};
|
|
22
|
+
return pinnedInfo;
|
|
23
|
+
}
|
|
24
|
+
export async function boundedNativeReadR198({rootPath,relativePath,offset,length,hashFile=false,timeoutMs=30000,expectedRootDev,expectedRootIno}={}){
|
|
25
|
+
if(process.platform!=='win32')throw new Error('R198 native root-relative reader requires Windows');
|
|
26
|
+
if(typeof rootPath!=='string'||!path.win32.isAbsolute(rootPath)||!path.isAbsolute(rootPath))
|
|
27
|
+
throw new Error('R198 native root path invalid');
|
|
28
|
+
validatePortableRelativePath(relativePath);
|
|
29
|
+
if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(length)||length<0||length>4*1024*1024)
|
|
30
|
+
throw new Error('R198 native bounded range invalid');
|
|
31
|
+
if(!Number.isInteger(timeoutMs)||timeoutMs<100||timeoutMs>180000)
|
|
32
|
+
throw new Error('R198 native timeout invalid');
|
|
33
|
+
if(typeof expectedRootDev!=='string'||!DECIMAL.test(expectedRootDev)||
|
|
34
|
+
typeof expectedRootIno!=='string'||!DECIMAL.test(expectedRootIno)||expectedRootIno==='0')
|
|
35
|
+
throw new Error('R198 expected root identity missing or invalid');
|
|
36
|
+
await verifyExecutable();
|
|
37
|
+
return await new Promise((resolve,reject)=>{
|
|
38
|
+
const args=[rootPath,relativePath,String(offset),String(length),hashFile?'1':'0',expectedRootDev,expectedRootIno];
|
|
39
|
+
const child=spawn(EXECUTABLE,args,{windowsHide:true,shell:false,stdio:['ignore','pipe','pipe']});
|
|
40
|
+
const chunks=[];let byteCount=0,stderr='',settled=false,timer=null;
|
|
41
|
+
const finish=(error,value)=>{
|
|
42
|
+
if(settled)return;
|
|
43
|
+
settled=true;if(timer!==null)clearTimeout(timer);
|
|
44
|
+
error?reject(error):resolve(value);
|
|
45
|
+
};
|
|
46
|
+
child.stdout.on('data',chunk=>{
|
|
47
|
+
byteCount+=chunk.length;
|
|
48
|
+
if(byteCount>8*1024*1024){child.kill();finish(new Error('R198 native read stdout exceeds bound'));return;}
|
|
49
|
+
chunks.push(Buffer.from(chunk));
|
|
50
|
+
});
|
|
51
|
+
child.stderr.on('data',chunk=>{
|
|
52
|
+
stderr=(stderr+chunk.toString('utf8')).slice(-16384);
|
|
53
|
+
});
|
|
54
|
+
child.on('error',error=>finish(error));
|
|
55
|
+
child.on('close',(code,signal)=>{
|
|
56
|
+
if(settled)return;
|
|
57
|
+
if(code!==0)return finish(new Error('R198 native root-relative read refused exit='+code+' signal='+(signal??'none')+' '+stderr.slice(-512)));
|
|
58
|
+
let response;
|
|
59
|
+
try{response=JSON.parse(Buffer.concat(chunks).toString('utf8'));}
|
|
60
|
+
catch{return finish(new Error('R198 native read response invalid JSON'));}
|
|
61
|
+
const data=typeof response?.data_base64==='string'?Buffer.from(response.data_base64,'base64'):null;
|
|
62
|
+
if(response?.schema!=='deadbyte.machine-fs-read-worker.v1'||!data||
|
|
63
|
+
response.bytes_read!==data.length||data.length>length||
|
|
64
|
+
!Number.isSafeInteger(response.file_size)||!DECIMAL.test(response.file_dev??'')||
|
|
65
|
+
!DECIMAL.test(response.file_ino??'')||
|
|
66
|
+
(hashFile?!SHA.test(response.file_sha256??''):response.file_sha256!==null))
|
|
67
|
+
return finish(new Error('R198 native read response contract mismatch'));
|
|
68
|
+
return finish(null,response);
|
|
69
|
+
});
|
|
70
|
+
timer=setTimeout(()=>{
|
|
71
|
+
child.kill();
|
|
72
|
+
finish(new Error('R198 native read timed out after '+timeoutMs+'ms'));
|
|
73
|
+
},timeoutMs);
|
|
74
|
+
timer.unref?.();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { readRangeFromHandle } from './machine-fs-handle-read-r196.mjs';
|
|
3
3
|
|
|
4
4
|
const [file, offsetText, lengthText, hashText] = process.argv.slice(2);
|
|
5
5
|
const offset = Number(offsetText);
|
|
@@ -13,33 +13,13 @@ if (typeof file !== 'string' || !Number.isSafeInteger(offset) || offset < 0 ||
|
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
15
|
const handle = await fs.open(file, 'r');
|
|
16
|
-
let
|
|
16
|
+
let result;
|
|
17
17
|
try {
|
|
18
|
-
|
|
19
|
-
const result = length ? await handle.read(buffer, 0, length, offset) : { bytesRead: 0 };
|
|
20
|
-
bytes = buffer.subarray(0, result.bytesRead);
|
|
18
|
+
result = await readRangeFromHandle(handle,{offset,length,hashFile:hashText==='1'});
|
|
21
19
|
} finally {
|
|
22
20
|
await handle.close();
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
let fileSha256 = null;
|
|
26
|
-
if (hashText === '1') {
|
|
27
|
-
const hash = createHash('sha256');
|
|
28
|
-
await new Promise((resolve, reject) => {
|
|
29
|
-
const stream = createReadStream(file);
|
|
30
|
-
stream.on('data', chunk => hash.update(chunk));
|
|
31
|
-
stream.once('error', reject);
|
|
32
|
-
stream.once('end', resolve);
|
|
33
|
-
});
|
|
34
|
-
fileSha256 = hash.digest('hex');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
process.stdout.write(JSON.stringify({
|
|
38
|
-
schema: 'deadbyte.machine-fs-read-worker.v1',
|
|
39
|
-
bytes_read: bytes.length,
|
|
40
|
-
data_base64: bytes.toString('base64'),
|
|
41
|
-
file_sha256: fileSha256
|
|
42
|
-
}));
|
|
22
|
+
process.stdout.write(JSON.stringify(result));
|
|
43
23
|
} catch (error) {
|
|
44
24
|
process.stderr.write(String(error?.stack ?? error));
|
|
45
25
|
process.exit(1);
|
package/src/machine-fs.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { isInside, validatePortableRelativePath } from './path-policy.mjs';
|
|
|
10
10
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
11
11
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
12
12
|
import { atomicMutation } from './atomic-mutation.mjs';
|
|
13
|
+
import { readRangeFromHandle } from './machine-fs-handle-read-r196.mjs';
|
|
14
|
+
import { boundedNativeReadR198 } from './machine-fs-native-read-r198.mjs';
|
|
13
15
|
|
|
14
16
|
export const MACHINE_FS_RECEIPT_SCHEMA = 'deadbyte.machine-fs-receipt.v1';
|
|
15
17
|
const RECEIPT_DOMAIN = Buffer.from('DEADBYTE-MACHINE-FS-RECEIPT-V1\0', 'utf8');
|
|
@@ -48,14 +50,7 @@ async function boundedReadInProcess({ file, offset, length, hashFile, timeoutMs
|
|
|
48
50
|
timer.unref?.();
|
|
49
51
|
});
|
|
50
52
|
try {
|
|
51
|
-
const operation = (
|
|
52
|
-
const buffer = Buffer.alloc(length);
|
|
53
|
-
const result = length ? await handle.read(buffer, 0, length, offset) : { bytesRead: 0 };
|
|
54
|
-
let fileSha256 = null;
|
|
55
|
-
if (hashFile) fileSha256 = await sha256File(file, abortController.signal);
|
|
56
|
-
return { schema: 'deadbyte.machine-fs-read-worker.v1', bytes_read: result.bytesRead,
|
|
57
|
-
data_base64: buffer.subarray(0, result.bytesRead).toString('base64'), file_sha256: fileSha256 };
|
|
58
|
-
})();
|
|
53
|
+
const operation = readRangeFromHandle(handle, { offset, length, hashFile, signal: abortController.signal });
|
|
59
54
|
operation.catch(() => {});
|
|
60
55
|
return await Promise.race([operation, abort]);
|
|
61
56
|
} finally {
|
|
@@ -414,22 +409,42 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath,
|
|
|
414
409
|
if (!Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > policy.max_read_bytes) throw new Error(`machine fs max_bytes must be in [1, ${policy.max_read_bytes}]`);
|
|
415
410
|
if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 180000) throw new Error('machine fs read timeout_ms must be in [100, 180000]');
|
|
416
411
|
const grant = await assertCodingReadAllowed(policy, rootId);
|
|
412
|
+
// Capture the root object before path validation. The native worker must
|
|
413
|
+
// compare its opened root HANDLE identity before resolving any child.
|
|
414
|
+
const authorizedRoot = getRoot(policy, rootId, 'read');
|
|
415
|
+
const rootBefore = await lstat(authorizedRoot.real_path, { bigint: true });
|
|
416
|
+
if (rootBefore.isSymbolicLink() || !rootBefore.isDirectory() || rootBefore.ino === 0n)
|
|
417
|
+
throw new Error('R198 root directory identity unavailable');
|
|
417
418
|
const found = await resolveExisting(policy, grant, rootId, relativePath, 'read', false);
|
|
418
|
-
|
|
419
|
+
// Node's numeric ino can round NTFS file IDs above 2^53.
|
|
420
|
+
// Keep the pre-open identity as BigInt and compare exact strings from the worker's fstat.
|
|
421
|
+
const info = await lstat(found.resolved, { bigint: true });
|
|
419
422
|
if (!info.isFile()) throw new Error('machine fs read target must be a regular file');
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
423
|
+
if (info.size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('machine fs read file size exceeds safe integer range');
|
|
424
|
+
if (info.ino === 0n) throw new Error('R196 file object identity unavailable');
|
|
425
|
+
const fileSize = Number(info.size);
|
|
426
|
+
const effectiveOffset = offset < 0 ? Math.max(0, fileSize + offset) : offset;
|
|
427
|
+
if (effectiveOffset > fileSize) throw new Error('machine fs read offset exceeds file size');
|
|
428
|
+
const length = Math.min(maxBytes, fileSize - effectiveOffset);
|
|
424
429
|
await recheckCodingReadAuthority(policy, grant, rootId);
|
|
425
|
-
const worker =
|
|
430
|
+
const worker = process.platform === 'win32' && readWorkerPath === machineFsReadWorkerPath
|
|
431
|
+
? await boundedNativeReadR198({
|
|
432
|
+
rootPath: found.root.real_path, relativePath: found.relative,
|
|
433
|
+
offset: effectiveOffset, length, hashFile, timeoutMs,
|
|
434
|
+
expectedRootDev: String(rootBefore.dev), expectedRootIno: String(rootBefore.ino)
|
|
435
|
+
})
|
|
436
|
+
: await boundedReadWorker({ file: found.resolved, offset: effectiveOffset, length, hashFile, timeoutMs, workerPath: readWorkerPath });
|
|
426
437
|
const buffer = Buffer.from(worker.data_base64, 'base64');
|
|
427
438
|
if (buffer.length !== worker.bytes_read || buffer.length > length) throw new Error('machine fs read worker byte count mismatch');
|
|
439
|
+
if (!Number.isSafeInteger(worker.file_size) || worker.file_size !== fileSize)
|
|
440
|
+
throw new Error('R196 opened file size drifted after precheck');
|
|
441
|
+
if (worker.file_dev !== String(info.dev) || worker.file_ino !== String(info.ino))
|
|
442
|
+
throw new Error('R196 opened file object identity differs from precheck');
|
|
428
443
|
await recheckCodingReadAuthority(policy, grant, rootId);
|
|
429
444
|
return {
|
|
430
445
|
status: 'ok', root_id: rootId, path: found.relative,
|
|
431
|
-
offset: effectiveOffset, requested_offset: offset, next_offset: effectiveOffset + buffer.length, file_size:
|
|
432
|
-
eof: effectiveOffset + buffer.length >=
|
|
446
|
+
offset: effectiveOffset, requested_offset: offset, next_offset: effectiveOffset + buffer.length, file_size: fileSize,
|
|
447
|
+
eof: effectiveOffset + buffer.length >= fileSize, data_base64: buffer.toString('base64'),
|
|
433
448
|
data_sha256: sha256Bytes(buffer), file_sha256: worker.file_sha256,
|
|
434
449
|
timeout_ms: timeoutMs
|
|
435
450
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export async function observeCanonicalController({stateRoot,powershell,env,execFileAsync}={}){
|
|
4
|
+
if(typeof stateRoot!=='string'||!path.win32.isAbsolute(stateRoot))
|
|
5
|
+
throw new Error('R195 canonical owner observation requires an absolute state root');
|
|
6
|
+
if(typeof powershell!=='string'||powershell.length<1||typeof execFileAsync!=='function')
|
|
7
|
+
throw new Error('R195 canonical owner observation dependency invalid');
|
|
8
|
+
const launcher=path.win32.join(path.win32.resolve(stateRoot),'bin','DEADBYTE-MCP.ps1');
|
|
9
|
+
const {stdout}=await execFileAsync(powershell,[
|
|
10
|
+
'-NoLogo','-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File',
|
|
11
|
+
launcher,'status','-Json'
|
|
12
|
+
],{env,windowsHide:true,shell:false,timeout:20000,maxBuffer:1024*1024,encoding:'utf8'});
|
|
13
|
+
try{return JSON.parse(stdout);}
|
|
14
|
+
catch{throw new Error('R195 canonical owner observation returned invalid JSON');}
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {lstat,mkdir,realpath} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function parseOwnerEnrollmentJson(bytes,label='owner enrollment JSON'){
|
|
5
|
+
const text=Buffer.isBuffer(bytes)?bytes.toString('utf8'):String(bytes);
|
|
6
|
+
let value;
|
|
7
|
+
try{value=JSON.parse(text.replace(/^\uFEFF/,''));}
|
|
8
|
+
catch(error){throw new Error(`${label} is invalid JSON: ${String(error?.message??error)}`);}
|
|
9
|
+
if(!value||typeof value!=='object'||Array.isArray(value))throw new Error(`${label} must be an object`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function ensureOwnerConsentDirectory(runtimeDir){
|
|
14
|
+
if(typeof runtimeDir!=='string'||!path.win32.isAbsolute(runtimeDir))
|
|
15
|
+
throw new Error('owner consent runtime root must be an absolute Windows path');
|
|
16
|
+
const runtime=path.win32.resolve(runtimeDir);
|
|
17
|
+
const runtimeInfo=await lstat(runtime);
|
|
18
|
+
if(!runtimeInfo.isDirectory()||runtimeInfo.isSymbolicLink())
|
|
19
|
+
throw new Error('owner consent runtime root must be a real directory');
|
|
20
|
+
const runtimeReal=path.win32.resolve(await realpath(runtime));
|
|
21
|
+
const consent=path.win32.join(runtime,'owner-consent');
|
|
22
|
+
await mkdir(consent,{recursive:true});
|
|
23
|
+
const consentInfo=await lstat(consent);
|
|
24
|
+
if(!consentInfo.isDirectory()||consentInfo.isSymbolicLink())
|
|
25
|
+
throw new Error('owner consent path must be a real directory');
|
|
26
|
+
const consentReal=path.win32.resolve(await realpath(consent));
|
|
27
|
+
if(path.win32.dirname(consentReal).toLowerCase()!==runtimeReal.toLowerCase())
|
|
28
|
+
throw new Error('owner consent path escapes the runtime root');
|
|
29
|
+
return consentReal;
|
|
30
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// R202 experimental destination-side storage for already-signed terminal evidence.
|
|
2
|
+
// This does not execute effects, sign new claims, authorize a capability, or retry a request.
|
|
3
|
+
import {createHash,randomBytes} from 'node:crypto';
|
|
4
|
+
import {link,lstat,mkdir,open,realpath,unlink} from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {verifySignedRemoteTerminalR200} from './remote-terminal-evidence-r200.mjs';
|
|
7
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
8
|
+
|
|
9
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
10
|
+
const MAX_RECEIPT_BYTES=16*1024;
|
|
11
|
+
const digest=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
12
|
+
function insist(ok,message){if(!ok)throw new Error('R202 destination journal '+message);}
|
|
13
|
+
|
|
14
|
+
export async function openRemoteDestinationJournalR202({directory,device}={}){
|
|
15
|
+
insist(typeof directory==='string'&&path.isAbsolute(directory),'directory invalid');
|
|
16
|
+
insist(device&&HEX64.test(device.device_id??'')&&Number.isSafeInteger(device.epoch)&&
|
|
17
|
+
device.epoch>=1&&typeof device.public_key_spki_base64==='string','device identity invalid');
|
|
18
|
+
await mkdir(directory,{recursive:true});
|
|
19
|
+
const inputRootInfo=await lstat(directory);
|
|
20
|
+
insist(inputRootInfo.isDirectory()&&!inputRootInfo.isSymbolicLink(),'input root is a reparse point');
|
|
21
|
+
const dir=await realpath(directory);
|
|
22
|
+
const rootInfo=await lstat(dir);
|
|
23
|
+
insist(rootInfo.isDirectory()&&!rootInfo.isSymbolicLink(),'root is not a real directory');
|
|
24
|
+
const filename=requestId=>{
|
|
25
|
+
insist(HEX64.test(requestId??''),'request ID invalid');
|
|
26
|
+
return path.join(dir,requestId+'.json');
|
|
27
|
+
};
|
|
28
|
+
// The bytes and identity come from the same opened file object, not a new path lookup.
|
|
29
|
+
async function checkedRead(file){
|
|
30
|
+
let before;
|
|
31
|
+
try{before=await lstat(file,{bigint:true});}
|
|
32
|
+
catch(error){if(error?.code==='ENOENT')return null;throw error;}
|
|
33
|
+
insist(before.isFile()&&!before.isSymbolicLink()&&before.size>=1n&&
|
|
34
|
+
before.size<=BigInt(MAX_RECEIPT_BYTES),'record is not a bounded regular file');
|
|
35
|
+
const h=await open(file,'r');
|
|
36
|
+
let bytes;
|
|
37
|
+
try{
|
|
38
|
+
const initial=await h.stat({bigint:true});
|
|
39
|
+
insist(initial.isFile()&&initial.dev===before.dev&&initial.ino===before.ino,
|
|
40
|
+
'record namespace swapped before handle open');
|
|
41
|
+
insist(initial.size>=1n&&initial.size<=BigInt(MAX_RECEIPT_BYTES),
|
|
42
|
+
'record size changed before read');
|
|
43
|
+
const buffer=Buffer.alloc(MAX_RECEIPT_BYTES+1);
|
|
44
|
+
const {bytesRead}=await h.read(buffer,0,buffer.length,0);
|
|
45
|
+
bytes=buffer.subarray(0,bytesRead);
|
|
46
|
+
const after=await h.stat({bigint:true});
|
|
47
|
+
insist(after.size===initial.size&&after.mtimeNs===initial.mtimeNs&&
|
|
48
|
+
after.ctimeNs===initial.ctimeNs&&after.dev===initial.dev&&after.ino===initial.ino&&
|
|
49
|
+
BigInt(bytesRead)===initial.size,'record mutated during handle read');
|
|
50
|
+
}finally{await h.close();}
|
|
51
|
+
const pathAfter=await lstat(file,{bigint:true});
|
|
52
|
+
insist(pathAfter.isFile()&&!pathAfter.isSymbolicLink()&&
|
|
53
|
+
pathAfter.ino===before.ino&&pathAfter.dev===before.dev,
|
|
54
|
+
'record namespace swapped after handle read');
|
|
55
|
+
let envelope;
|
|
56
|
+
try{envelope=JSON.parse(bytes.toString('utf8'));}
|
|
57
|
+
catch{throw new Error('R203 destination journal stored terminal is corrupt');}
|
|
58
|
+
return {envelope,bytes,sha256:digest(bytes)};
|
|
59
|
+
}
|
|
60
|
+
function verify(envelope,request){
|
|
61
|
+
insist(request&&request.device_id===device.device_id&&request.epoch===device.epoch&&
|
|
62
|
+
HEX64.test(request.request_id??'')&&HEX64.test(request.request_sha256??'')&&
|
|
63
|
+
typeof request.capability_id==='string','request identity invalid');
|
|
64
|
+
return verifySignedRemoteTerminalR200(envelope,{request,device});
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
directory:dir,
|
|
68
|
+
device_id:device.device_id,
|
|
69
|
+
async commitSignedTerminal({envelope,request}={}){
|
|
70
|
+
const proof=verify(envelope,request);
|
|
71
|
+
const body=Buffer.from(canonicalJson(envelope)+'\n','utf8');
|
|
72
|
+
insist(body.length<=MAX_RECEIPT_BYTES,'signed terminal exceeds bound');
|
|
73
|
+
const file=filename(request.request_id);
|
|
74
|
+
// fsync the complete temp before an atomic, create-only hardlink publication.
|
|
75
|
+
// No direct write to final name: a crash before link cannot poison it.
|
|
76
|
+
const temp=path.join(dir,'.'+request.request_id+'.'+randomBytes(12).toString('hex')+'.tmp');
|
|
77
|
+
let tempCreated=false, published=false;
|
|
78
|
+
try{
|
|
79
|
+
const h=await open(temp,'wx',0o600);
|
|
80
|
+
tempCreated=true;
|
|
81
|
+
try{await h.writeFile(body);await h.sync();}
|
|
82
|
+
finally{await h.close();}
|
|
83
|
+
try{
|
|
84
|
+
await link(temp,file);
|
|
85
|
+
published=true;
|
|
86
|
+
}catch(error){
|
|
87
|
+
if(error?.code!=='EEXIST')throw error;
|
|
88
|
+
const old=await checkedRead(file);
|
|
89
|
+
insist(old!==null,'record missing after conflict');
|
|
90
|
+
verify(old.envelope,request);
|
|
91
|
+
insist(old.sha256===digest(body),'conflicting signed terminal for one request');
|
|
92
|
+
return Object.freeze({status:'existing',replayed:true,request_id:request.request_id,
|
|
93
|
+
terminal_evidence_sha256:proof.terminal_evidence_sha256});
|
|
94
|
+
}
|
|
95
|
+
}finally{
|
|
96
|
+
if(tempCreated)await unlink(temp).catch(error=>{
|
|
97
|
+
if(error?.code!=='ENOENT' && !published)throw error;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const written=await checkedRead(file);
|
|
101
|
+
insist(written!==null&&written.sha256===digest(body),'postcommit bytes mismatch');
|
|
102
|
+
verify(written.envelope,request);
|
|
103
|
+
return Object.freeze({status:'stored',replayed:false,request_id:request.request_id,
|
|
104
|
+
terminal_evidence_sha256:proof.terminal_evidence_sha256});
|
|
105
|
+
},
|
|
106
|
+
async lookupTerminal({device_id,epoch,request_id,request_sha256}={}){
|
|
107
|
+
insist(device_id===device.device_id&&epoch===device.epoch&&
|
|
108
|
+
HEX64.test(request_id??'')&&HEX64.test(request_sha256??''),
|
|
109
|
+
'lookup request identity invalid');
|
|
110
|
+
const stored=await checkedRead(filename(request_id));
|
|
111
|
+
if(stored===null)return null;
|
|
112
|
+
const e=stored.envelope?.evidence;
|
|
113
|
+
insist(e&&e.request_sha256===request_sha256&&e.request_id===request_id,
|
|
114
|
+
'lookup request hash mismatch');
|
|
115
|
+
const request={device_id,epoch,request_id,request_sha256,capability_id:e.capability_id};
|
|
116
|
+
verify(stored.envelope,request);
|
|
117
|
+
return structuredClone(stored.envelope);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|