deadbyte-mcp 0.15.13 → 0.15.15

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.
@@ -1,293 +1,294 @@
1
- import { createHash, randomBytes } from 'node:crypto';
2
- import { existsSync } from 'node:fs';
3
- import { lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
4
- import path from 'node:path';
5
-
6
- const HEX64=/^[0-9a-f]{64}$/;
7
- const SEMVER=/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
8
- const POINTER_SCHEMA='deadbyte.active-release.v1';
9
-
10
- export const sha256Bytes=bytes=>createHash('sha256').update(bytes).digest('hex');
11
-
12
- function safeRelative(rel){
13
- if(typeof rel!=='string'||rel.length<1) throw new Error('manifest path empty');
14
- const normalized=rel.replaceAll('\\','/');
15
- if(path.posix.isAbsolute(normalized)||normalized.split('/').includes('..')||normalized.includes('\0')) {
16
- throw new Error(`unsafe manifest path '${normalized}'`);
17
- }
18
- return normalized;
19
- }
20
-
21
- export function releaseId(version,manifestSha256){
22
- if(!SEMVER.test(String(version))) throw new Error('release version must be SemVer');
23
- if(!HEX64.test(String(manifestSha256))) throw new Error('release manifest must be lowercase SHA-256');
24
- return `v${version}-${manifestSha256.slice(0,16)}`;
25
- }
26
- export function parseManifest(text){
27
- const entries=new Map();
28
- for(const [index,line] of String(text).split(/\r?\n/).entries()){
29
- if(!line) continue;
30
- const match=/^([0-9a-f]{64}) (.+)$/.exec(line);
31
- if(!match) throw new Error(`manifest line ${index+1} malformed`);
32
- const rel=safeRelative(match[2]);
33
- if(entries.has(rel)) throw new Error(`manifest duplicate path '${rel}'`);
34
- entries.set(rel,match[1]);
35
- }
36
- if(entries.size<1) throw new Error('manifest has no entries');
37
- return entries;
38
- }
39
-
40
- async function plainManifestFile(root,rel,label){
41
- const rootPath=path.resolve(root);
42
- const rootStat=await lstat(rootPath).catch(error=>{throw new Error(`${label} root unavailable: ${error.message}`)});
43
- if(!rootStat.isDirectory()||rootStat.isSymbolicLink()) throw new Error(`${label} root must be a real directory`);
44
- let cursor=rootPath;
45
- const parts=safeRelative(rel).split('/');
46
- for(let index=0;index<parts.length;index++){
47
- cursor=path.join(cursor,parts[index]);
48
- const stat=await lstat(cursor).catch(error=>{throw new Error(`${label} file missing: ${rel}: ${error.message}`)});
49
- if(stat.isSymbolicLink()) throw new Error(`${label} symlink is forbidden: ${rel}`);
50
- if(index<parts.length-1&&!stat.isDirectory()) throw new Error(`${label} parent is not directory: ${rel}`);
51
- if(index===parts.length-1&&!stat.isFile()) throw new Error(`${label} file is not regular: ${rel}`);
52
- }
53
- return cursor;
54
- }
55
-
56
- export async function verifyManifestTree(root,manifest){
57
- for(const [rel,expected] of manifest){
58
- const file=await plainManifestFile(root,rel,'manifest');
59
- const actual=sha256Bytes(await readFile(file));
60
- if(actual!==expected) throw new Error(`hash mismatch: ${rel}`);
61
- }
62
- return {files:manifest.size};
63
- }
64
-
65
- async function listManifestTreeFiles(root){
66
- const base=path.resolve(root), files=[];
67
- const walk=async(dir)=>{
68
- for(const entry of await readdir(dir,{withFileTypes:true})){
69
- const absolute=path.join(dir,entry.name);
70
- const rel=path.relative(base,absolute).replaceAll('\\','/');
71
- if(entry.isSymbolicLink()) throw new Error(`manifest-tree symlink forbidden: ${rel}`);
72
- if(entry.isDirectory()) await walk(absolute);
73
- else if(entry.isFile()) { if(rel!=='MANIFEST.SHA256') files.push(rel); }
74
- else throw new Error(`manifest-tree unsupported entry: ${rel}`);
75
- }
76
- };
77
- await walk(base);
78
- return files.sort((a,b)=>a.localeCompare(b,'en'));
79
- }
80
-
81
- export async function verifyManifestTreeExact(root,manifest){
82
- await verifyManifestTree(root,manifest);
83
- const actual=await listManifestTreeFiles(root);
84
- const expected=[...manifest.keys()].sort((a,b)=>a.localeCompare(b,'en'));
85
- for(const rel of actual) if(!manifest.has(rel)) throw new Error(`unexpected manifest-tree file: ${rel}`);
86
- for(const rel of expected) if(!actual.includes(rel)) throw new Error(`missing manifest-tree file: ${rel}`);
87
- if(actual.length!==expected.length) throw new Error(`manifest-tree file count mismatch: expected=${expected.length} actual=${actual.length}`);
88
- return {files:manifest.size};
89
- }
90
- export async function copyManifestTree({source,destination,manifest}){
91
- await mkdir(destination,{recursive:false});
92
- try {
93
- for(const [rel] of manifest){
94
- const src=await plainManifestFile(source,rel,'candidate');
95
- const dst=path.resolve(destination,...safeRelative(rel).split('/'));
96
- await mkdir(path.dirname(dst),{recursive:true});
97
- await writeFile(dst,await readFile(src),{flag:'wx'});
98
- }
99
- await verifyManifestTreeExact(destination,manifest);
100
- return {files:manifest.size};
101
- } catch(error){
102
- await rm(destination,{recursive:true,force:true}).catch(()=>{});
103
- throw error;
104
- }
105
- }
106
-
107
- export async function materializeReleaseSlot({source,stateRoot,version,manifestSha256}){
108
- if(!SEMVER.test(String(version))) throw new Error('candidate version must be SemVer');
109
- if(!HEX64.test(String(manifestSha256))) throw new Error('candidate manifest must be lowercase SHA-256');
110
- const sourceRoot=path.resolve(source),state=path.resolve(stateRoot);
111
- const manifestBytes=await readFile(path.join(sourceRoot,'MANIFEST.SHA256'));
112
- if(sha256Bytes(manifestBytes)!==manifestSha256) throw new Error('candidate manifest digest mismatch');
113
- const manifest=parseManifest(manifestBytes.toString('utf8'));
114
- for(const required of [
115
- 'package.json','controller/deadbyte-controller.ps1','scripts/runtime-self-update.mjs',
116
- 'bin/deadbyte-exec.exe','bin/contained-transform-worker.exe',
117
- 'bin/contained-reverse-worker.exe','bin/WORKER-REGISTRY.txt'
118
- ]) {
119
- if(!manifest.has(required)) throw new Error(`candidate manifest missing required entry: ${required}`);
120
- }
121
- await verifyManifestTree(sourceRoot,manifest);
122
- const pkg=JSON.parse(await readFile(path.join(sourceRoot,'package.json'),'utf8'));
123
- if(pkg?.name!=='deadbyte-mcp'||pkg?.version!==version) throw new Error('candidate package identity mismatch');
124
- const id=releaseId(version,manifestSha256),releasesRoot=path.join(state,'releases'),releaseRoot=path.join(releasesRoot,id);
125
- const verifyFinal=async()=>{
126
- const finalManifest=await readFile(path.join(releaseRoot,'MANIFEST.SHA256'));
127
- if(sha256Bytes(finalManifest)!==manifestSha256) throw new Error('release slot manifest digest mismatch');
128
- const finalEntries=parseManifest(finalManifest.toString('utf8'));
129
- if(finalEntries.size!==manifest.size||[...manifest].some(([rel,hash])=>finalEntries.get(rel)!==hash)) throw new Error('release slot manifest entries mismatch');
130
- await verifyManifestTreeExact(releaseRoot,finalEntries);
131
- const finalPkg=JSON.parse(await readFile(path.join(releaseRoot,'package.json'),'utf8'));
132
- if(finalPkg?.name!=='deadbyte-mcp'||finalPkg?.version!==version) throw new Error('release slot package identity mismatch');
133
- return {release_id:id,release_root:releaseRoot,manifest_sha256:manifestSha256,files:finalEntries.size};
134
- };
135
- if(existsSync(releaseRoot)) return {...await verifyFinal(),adopted:true};
136
- await mkdir(releasesRoot,{recursive:true});
137
- const staging=path.join(releasesRoot,`.staging-${id}-${process.pid}-${randomBytes(4).toString('hex')}`);
138
- try {
139
- await copyManifestTree({source:sourceRoot,destination:staging,manifest});
140
- await writeFile(path.join(staging,'MANIFEST.SHA256'),manifestBytes,{flag:'wx'});
141
- await verifyManifestTreeExact(staging,manifest);
142
- await rename(staging,releaseRoot);
143
- } catch(error){
144
- await rm(staging,{recursive:true,force:true}).catch(()=>{});
145
- if(existsSync(releaseRoot)) return {...await verifyFinal(),adopted:true};
146
- throw error;
147
- }
148
- return {...await verifyFinal(),adopted:false};
149
- }
150
-
151
- async function atomicJson(file,value){
152
- await mkdir(path.dirname(file),{recursive:true});
153
- const temp=`${file}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
154
- const handle=await open(temp,'wx');
155
- try { await handle.writeFile(`${JSON.stringify(value,null,2)}\n`,'utf8'); await handle.sync(); }
156
- finally { await handle.close(); }
157
- try { await rename(temp,file); }
158
- catch(error){ await rm(temp,{force:true}).catch(()=>{}); throw error; }
159
- }
160
- export function parseActivePointer(text){
161
- let value;
162
- try { value=JSON.parse(String(text)); } catch(error){ throw new Error(`active pointer JSON invalid: ${error.message}`); }
163
- if(!value||typeof value!=='object'||Array.isArray(value)) throw new Error('active pointer must be object');
164
- if(value.schema!==POINTER_SCHEMA) throw new Error('active pointer schema mismatch');
165
- if(!SEMVER.test(String(value.version))) throw new Error('active pointer version invalid');
166
- if(!HEX64.test(String(value.manifest_sha256))) throw new Error('active pointer manifest invalid');
167
- if(value.release_id!==releaseId(value.version,value.manifest_sha256)) throw new Error('active pointer release id mismatch');
168
- if(!HEX64.test(String(value.generation_manifest_sha256))) throw new Error('active pointer generation manifest invalid');
169
- if(!HEX64.test(String(value.launcher_sha256))) throw new Error('active pointer launcher digest invalid');
170
- for(const key of ['release_root','generation_root','config_path']) {
171
- if(typeof value[key]!=='string'||value[key].length<3) throw new Error(`active pointer ${key} invalid`);
172
- }
173
- return {
174
- schema:POINTER_SCHEMA,
175
- release_id:value.release_id,
176
- version:value.version,
177
- manifest_sha256:value.manifest_sha256,
178
- release_root:value.release_root,
179
- generation_root:value.generation_root,
180
- generation_manifest_sha256:value.generation_manifest_sha256,
181
- launcher_sha256:value.launcher_sha256,
182
- config_path:value.config_path
183
- };
184
- }
185
-
186
- export async function atomicWriteActivePointer(file,pointer){
187
- const normalized=parseActivePointer(JSON.stringify(pointer));
188
- await atomicJson(file,normalized);
189
- const observed=parseActivePointer(await readFile(file,'utf8'));
190
- if(JSON.stringify(observed)!==JSON.stringify(normalized)) throw new Error('active pointer readback mismatch');
191
- return observed;
192
- }
193
- export function renderStableLauncherCmd(){
194
- return [
195
- '@echo off',
196
- 'setlocal EnableExtensions DisableDelayedExpansion',
197
- 'powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\\.deadbyte-mcp\\bin\\DEADBYTE-MCP.ps1" %*',
198
- 'exit /b %ERRORLEVEL%',
199
- ''
200
- ].join('\r\n');
201
- }
202
-
203
- export function renderStableLauncherPs1(){
204
- return [
205
- '#requires -Version 5.1',
206
- '[CmdletBinding()]',
207
- 'param([Parameter(ValueFromRemainingArguments=$true)][string[]]$ControllerArgs)',
208
- 'Set-StrictMode -Version 3.0',
209
- "$ErrorActionPreference = 'Stop'",
210
- "$StateRoot = Join-Path $env:USERPROFILE '.deadbyte-mcp'",
211
- "$PointerPath = Join-Path $StateRoot 'active-release.json'",
212
- "function Fail([string]$m) { throw ('DEADBYTE launcher: ' + $m) }",
213
- "if (-not (Test-Path -LiteralPath $PointerPath -PathType Leaf)) { Fail 'active release pointer missing' }",
214
- '$p = Get-Content -LiteralPath $PointerPath -Raw -Encoding UTF8 | ConvertFrom-Json',
215
- "if ($p.schema -ne 'deadbyte.active-release.v1') { Fail 'active release schema mismatch' }",
216
- "if ([string]$p.manifest_sha256 -notmatch '^[0-9a-f]{64}$') { Fail 'active manifest digest invalid' }",
217
- "if ([string]$p.generation_manifest_sha256 -notmatch '^[0-9a-f]{64}$') { Fail 'active generation manifest digest invalid' }",
218
- "if ([string]$p.launcher_sha256 -notmatch '^[0-9a-f]{64}$') { Fail 'active launcher digest invalid' }",
219
- '$launcherHash = (Get-FileHash -LiteralPath $MyInvocation.MyCommand.Path -Algorithm SHA256).Hash.ToLowerInvariant()',
220
- "if ($launcherHash -ne [string]$p.launcher_sha256) { Fail 'stable launcher hash mismatch' }",
221
- "$expectedId = 'v{0}-{1}' -f [string]$p.version,([string]$p.manifest_sha256).Substring(0,16)",
222
- "if ([string]$p.release_id -ne $expectedId) { Fail 'active release id mismatch' }",
223
- "$releases = [IO.Path]::GetFullPath((Join-Path $StateRoot 'releases')).TrimEnd('\\') + '\\'",
224
- "$generations = [IO.Path]::GetFullPath((Join-Path $StateRoot 'generations')).TrimEnd('\\') + '\\'",
225
- '$releaseRoot = [IO.Path]::GetFullPath([string]$p.release_root)',
226
- '$generationRoot = [IO.Path]::GetFullPath([string]$p.generation_root)',
227
- '$configPath = [IO.Path]::GetFullPath([string]$p.config_path)',
228
- "if (-not $releaseRoot.StartsWith($releases,[StringComparison]::OrdinalIgnoreCase)) { Fail 'release root escapes release store' }",
229
- "if (-not $generationRoot.StartsWith($generations,[StringComparison]::OrdinalIgnoreCase)) { Fail 'generation root escapes generation store' }",
230
- "$expectedConfig = Join-Path $generationRoot 'controller-config.psd1'",
231
- "if (-not [string]::Equals($configPath,$expectedConfig,[StringComparison]::OrdinalIgnoreCase)) { Fail 'config path is not generation-bound' }",
232
- "$manifest = Join-Path $releaseRoot 'MANIFEST.SHA256'",
233
- "$generationManifest = Join-Path $generationRoot 'GENERATION.SHA256'",
234
- "$package = Join-Path $releaseRoot 'package.json'",
235
- "$controller = Join-Path $releaseRoot 'controller\\deadbyte-controller.ps1'",
236
- "foreach ($file in @($manifest,$generationManifest,$package,$controller,$configPath)) { if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { Fail ('required file missing: ' + $file) } }",
237
- '$manifestHash = (Get-FileHash -LiteralPath $manifest -Algorithm SHA256).Hash.ToLowerInvariant()',
238
- "if ($manifestHash -ne [string]$p.manifest_sha256) { Fail 'active manifest hash mismatch' }",
239
- '$generationManifestHash = (Get-FileHash -LiteralPath $generationManifest -Algorithm SHA256).Hash.ToLowerInvariant()',
240
- "if ($generationManifestHash -ne [string]$p.generation_manifest_sha256) { Fail 'active generation manifest hash mismatch' }",
241
- '$releasePrefix = $releaseRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar',
242
- '$generationPrefix = $generationRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar',
243
- '$manifestLines = Get-Content -LiteralPath $manifest -Encoding UTF8',
244
- '$manifestPathSet = @{}',
245
- 'foreach ($line in $manifestLines) {',
246
- " if ([string]::IsNullOrWhiteSpace([string]$line)) { continue }",
247
- " if ([string]$line -notmatch '^([0-9a-f]{64}) (.+)$') { Fail 'release manifest line malformed' }",
248
- ' $expectedHash = [string]$Matches[1]',
249
- ' $rel = [string]$Matches[2]',
250
- " if ([IO.Path]::IsPathRooted($rel) -or (($rel -split '[\\/]') -contains '..')) { Fail ('unsafe release manifest path: ' + $rel) }",
251
- " $relKey = ($rel -replace '\\\\','/').ToLowerInvariant()",
252
- " if ($manifestPathSet.ContainsKey($relKey)) { Fail ('duplicate release manifest path: ' + $rel) }",
253
- ' $manifestPathSet[$relKey] = $true',
254
- ' $target = [IO.Path]::GetFullPath((Join-Path $releaseRoot $rel))',
255
- " if (-not $target.StartsWith($releasePrefix,[StringComparison]::OrdinalIgnoreCase)) { Fail ('release manifest path escapes slot: ' + $rel) }",
256
- " if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { Fail ('release file missing: ' + $rel) }",
257
- ' $targetItem = Get-Item -LiteralPath $target -Force',
258
- " if (($targetItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Fail ('release reparse point forbidden: ' + $rel) }",
259
- ' $actualHash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()',
260
- " if ($actualHash -ne $expectedHash) { Fail ('release file hash mismatch: ' + $rel) }",
261
- '}',
262
- '$releaseItems = Get-ChildItem -LiteralPath $releaseRoot -Force -Recurse',
263
- 'foreach ($item in $releaseItems) {',
264
- " if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Fail ('release reparse point forbidden: ' + $item.FullName) }",
265
- ' if ($item.PSIsContainer) { continue }',
266
- ' if ([string]::Equals($item.FullName,$manifest,[StringComparison]::OrdinalIgnoreCase)) { continue }',
267
- " if (-not $item.FullName.StartsWith($releasePrefix,[StringComparison]::OrdinalIgnoreCase)) { Fail ('release file escapes slot: ' + $item.FullName) }",
268
- " $actualRel = $item.FullName.Substring($releasePrefix.Length).Replace('\\','/')",
269
- " if (-not $manifestPathSet.ContainsKey($actualRel.ToLowerInvariant())) { Fail ('unexpected release file: ' + $actualRel) }",
270
- '}',
271
- '$generationLines = Get-Content -LiteralPath $generationManifest -Encoding UTF8',
272
- 'foreach ($line in $generationLines) {',
273
- " if ([string]::IsNullOrWhiteSpace([string]$line)) { continue }",
274
- " if ([string]$line -notmatch '^([0-9a-f]{64}) ([A-Za-z0-9._-]+)$') { Fail 'generation manifest line malformed' }",
275
- ' $expectedHash = [string]$Matches[1]',
276
- ' $rel = [string]$Matches[2]',
277
- ' $target = [IO.Path]::GetFullPath((Join-Path $generationRoot $rel))',
278
- " if (-not $target.StartsWith($generationPrefix,[StringComparison]::OrdinalIgnoreCase)) { Fail ('generation manifest path escapes generation: ' + $rel) }",
279
- " if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { Fail ('generation file missing: ' + $rel) }",
280
- ' $actualHash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()',
281
- " if ($actualHash -ne $expectedHash) { Fail ('generation file hash mismatch: ' + $rel) }",
282
- '}',
283
- '$pkg = Get-Content -LiteralPath $package -Raw -Encoding UTF8 | ConvertFrom-Json',
284
- "if ([string]$pkg.name -ne 'deadbyte-mcp' -or [string]$pkg.version -ne [string]$p.version) { Fail 'active package identity mismatch' }",
285
- '$env:DEADBYTE_STATE_ROOT = $StateRoot',
286
- '$env:DEADBYTE_CONTROLLER_CONFIG = $configPath',
287
- "$env:DEADBYTE_CONTROLLER_RUNTIME_ROOT = Join-Path $StateRoot 'runtime'",
288
- "$powershell = Join-Path $PSHOME 'powershell.exe'",
289
- '& $powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File $controller @ControllerArgs',
290
- 'exit $LASTEXITCODE',
291
- ''
292
- ].join('\r\n');
293
- }
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ const HEX64=/^[0-9a-f]{64}$/;
7
+ const SEMVER=/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
8
+ const POINTER_SCHEMA='deadbyte.active-release.v1';
9
+
10
+ export const sha256Bytes=bytes=>createHash('sha256').update(bytes).digest('hex');
11
+
12
+ function safeRelative(rel){
13
+ if(typeof rel!=='string'||rel.length<1) throw new Error('manifest path empty');
14
+ const normalized=rel.replaceAll('\\','/');
15
+ if(path.posix.isAbsolute(normalized)||normalized.split('/').includes('..')||normalized.includes('\0')) {
16
+ throw new Error(`unsafe manifest path '${normalized}'`);
17
+ }
18
+ return normalized;
19
+ }
20
+
21
+ export function releaseId(version,manifestSha256){
22
+ if(!SEMVER.test(String(version))) throw new Error('release version must be SemVer');
23
+ if(!HEX64.test(String(manifestSha256))) throw new Error('release manifest must be lowercase SHA-256');
24
+ return `v${version}-${manifestSha256.slice(0,16)}`;
25
+ }
26
+ export function parseManifest(text){
27
+ const entries=new Map();
28
+ for(const [index,line] of String(text).split(/\r?\n/).entries()){
29
+ if(!line) continue;
30
+ const match=/^([0-9a-f]{64}) (.+)$/.exec(line);
31
+ if(!match) throw new Error(`manifest line ${index+1} malformed`);
32
+ const rel=safeRelative(match[2]);
33
+ if(entries.has(rel)) throw new Error(`manifest duplicate path '${rel}'`);
34
+ entries.set(rel,match[1]);
35
+ }
36
+ if(entries.size<1) throw new Error('manifest has no entries');
37
+ return entries;
38
+ }
39
+
40
+ async function plainManifestFile(root,rel,label){
41
+ const rootPath=path.resolve(root);
42
+ const rootStat=await lstat(rootPath).catch(error=>{throw new Error(`${label} root unavailable: ${error.message}`)});
43
+ if(!rootStat.isDirectory()||rootStat.isSymbolicLink()) throw new Error(`${label} root must be a real directory`);
44
+ let cursor=rootPath;
45
+ const parts=safeRelative(rel).split('/');
46
+ for(let index=0;index<parts.length;index++){
47
+ cursor=path.join(cursor,parts[index]);
48
+ const stat=await lstat(cursor).catch(error=>{throw new Error(`${label} file missing: ${rel}: ${error.message}`)});
49
+ if(stat.isSymbolicLink()) throw new Error(`${label} symlink is forbidden: ${rel}`);
50
+ if(index<parts.length-1&&!stat.isDirectory()) throw new Error(`${label} parent is not directory: ${rel}`);
51
+ if(index===parts.length-1&&!stat.isFile()) throw new Error(`${label} file is not regular: ${rel}`);
52
+ }
53
+ return cursor;
54
+ }
55
+
56
+ export async function verifyManifestTree(root,manifest){
57
+ for(const [rel,expected] of manifest){
58
+ const file=await plainManifestFile(root,rel,'manifest');
59
+ const actual=sha256Bytes(await readFile(file));
60
+ if(actual!==expected) throw new Error(`hash mismatch: ${rel}`);
61
+ }
62
+ return {files:manifest.size};
63
+ }
64
+
65
+ async function listManifestTreeFiles(root){
66
+ const base=path.resolve(root), files=[];
67
+ const walk=async(dir)=>{
68
+ for(const entry of await readdir(dir,{withFileTypes:true})){
69
+ const absolute=path.join(dir,entry.name);
70
+ const rel=path.relative(base,absolute).replaceAll('\\','/');
71
+
72
+ if(entry.isSymbolicLink()) throw new Error(`manifest-tree symlink forbidden: ${rel}`);
73
+ if(entry.isDirectory()) await walk(absolute);
74
+ else if(entry.isFile()) { if(rel!=='MANIFEST.SHA256') files.push(rel); }
75
+ else throw new Error(`manifest-tree unsupported entry: ${rel}`);
76
+ }
77
+ };
78
+ await walk(base);
79
+ return files.sort((a,b)=>a.localeCompare(b,'en'));
80
+ }
81
+
82
+ export async function verifyManifestTreeExact(root,manifest){
83
+ await verifyManifestTree(root,manifest);
84
+ const actual=await listManifestTreeFiles(root);
85
+ const expected=[...manifest.keys()].sort((a,b)=>a.localeCompare(b,'en'));
86
+ for(const rel of actual) if(!manifest.has(rel)) throw new Error(`unexpected manifest-tree file: ${rel}`);
87
+ for(const rel of expected) if(!actual.includes(rel)) throw new Error(`missing manifest-tree file: ${rel}`);
88
+ if(actual.length!==expected.length) throw new Error(`manifest-tree file count mismatch: expected=${expected.length} actual=${actual.length}`);
89
+ return {files:manifest.size};
90
+ }
91
+ export async function copyManifestTree({source,destination,manifest}){
92
+ await mkdir(destination,{recursive:false});
93
+ try {
94
+ for(const [rel] of manifest){
95
+ const src=await plainManifestFile(source,rel,'candidate');
96
+ const dst=path.resolve(destination,...safeRelative(rel).split('/'));
97
+ await mkdir(path.dirname(dst),{recursive:true});
98
+ await writeFile(dst,await readFile(src),{flag:'wx'});
99
+ }
100
+ await verifyManifestTreeExact(destination,manifest);
101
+ return {files:manifest.size};
102
+ } catch(error){
103
+ await rm(destination,{recursive:true,force:true}).catch(()=>{});
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ export async function materializeReleaseSlot({source,stateRoot,version,manifestSha256}){
109
+ if(!SEMVER.test(String(version))) throw new Error('candidate version must be SemVer');
110
+ if(!HEX64.test(String(manifestSha256))) throw new Error('candidate manifest must be lowercase SHA-256');
111
+ const sourceRoot=path.resolve(source),state=path.resolve(stateRoot);
112
+ const manifestBytes=await readFile(path.join(sourceRoot,'MANIFEST.SHA256'));
113
+ if(sha256Bytes(manifestBytes)!==manifestSha256) throw new Error('candidate manifest digest mismatch');
114
+ const manifest=parseManifest(manifestBytes.toString('utf8'));
115
+ for(const required of [
116
+ 'package.json','controller/deadbyte-controller.ps1','scripts/runtime-self-update.mjs',
117
+ 'bin/deadbyte-exec.exe','bin/contained-transform-worker.exe',
118
+ 'bin/contained-reverse-worker.exe','bin/WORKER-REGISTRY.txt'
119
+ ]) {
120
+ if(!manifest.has(required)) throw new Error(`candidate manifest missing required entry: ${required}`);
121
+ }
122
+ await verifyManifestTree(sourceRoot,manifest);
123
+ const pkg=JSON.parse(await readFile(path.join(sourceRoot,'package.json'),'utf8'));
124
+ if(pkg?.name!=='deadbyte-mcp'||pkg?.version!==version) throw new Error('candidate package identity mismatch');
125
+ const id=releaseId(version,manifestSha256),releasesRoot=path.join(state,'releases'),releaseRoot=path.join(releasesRoot,id);
126
+ const verifyFinal=async()=>{
127
+ const finalManifest=await readFile(path.join(releaseRoot,'MANIFEST.SHA256'));
128
+ if(sha256Bytes(finalManifest)!==manifestSha256) throw new Error('release slot manifest digest mismatch');
129
+ const finalEntries=parseManifest(finalManifest.toString('utf8'));
130
+ if(finalEntries.size!==manifest.size||[...manifest].some(([rel,hash])=>finalEntries.get(rel)!==hash)) throw new Error('release slot manifest entries mismatch');
131
+ await verifyManifestTreeExact(releaseRoot,finalEntries);
132
+ const finalPkg=JSON.parse(await readFile(path.join(releaseRoot,'package.json'),'utf8'));
133
+ if(finalPkg?.name!=='deadbyte-mcp'||finalPkg?.version!==version) throw new Error('release slot package identity mismatch');
134
+ return {release_id:id,release_root:releaseRoot,manifest_sha256:manifestSha256,files:finalEntries.size};
135
+ };
136
+ if(existsSync(releaseRoot)) return {...await verifyFinal(),adopted:true};
137
+ await mkdir(releasesRoot,{recursive:true});
138
+ const staging=path.join(releasesRoot,`.staging-${id}-${process.pid}-${randomBytes(4).toString('hex')}`);
139
+ try {
140
+ await copyManifestTree({source:sourceRoot,destination:staging,manifest});
141
+ await writeFile(path.join(staging,'MANIFEST.SHA256'),manifestBytes,{flag:'wx'});
142
+ await verifyManifestTreeExact(staging,manifest);
143
+ await rename(staging,releaseRoot);
144
+ } catch(error){
145
+ await rm(staging,{recursive:true,force:true}).catch(()=>{});
146
+ if(existsSync(releaseRoot)) return {...await verifyFinal(),adopted:true};
147
+ throw error;
148
+ }
149
+ return {...await verifyFinal(),adopted:false};
150
+ }
151
+
152
+ async function atomicJson(file,value){
153
+ await mkdir(path.dirname(file),{recursive:true});
154
+ const temp=`${file}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
155
+ const handle=await open(temp,'wx');
156
+ try { await handle.writeFile(`${JSON.stringify(value,null,2)}\n`,'utf8'); await handle.sync(); }
157
+ finally { await handle.close(); }
158
+ try { await rename(temp,file); }
159
+ catch(error){ await rm(temp,{force:true}).catch(()=>{}); throw error; }
160
+ }
161
+ export function parseActivePointer(text){
162
+ let value;
163
+ try { value=JSON.parse(String(text)); } catch(error){ throw new Error(`active pointer JSON invalid: ${error.message}`); }
164
+ if(!value||typeof value!=='object'||Array.isArray(value)) throw new Error('active pointer must be object');
165
+ if(value.schema!==POINTER_SCHEMA) throw new Error('active pointer schema mismatch');
166
+ if(!SEMVER.test(String(value.version))) throw new Error('active pointer version invalid');
167
+ if(!HEX64.test(String(value.manifest_sha256))) throw new Error('active pointer manifest invalid');
168
+ if(value.release_id!==releaseId(value.version,value.manifest_sha256)) throw new Error('active pointer release id mismatch');
169
+ if(!HEX64.test(String(value.generation_manifest_sha256))) throw new Error('active pointer generation manifest invalid');
170
+ if(!HEX64.test(String(value.launcher_sha256))) throw new Error('active pointer launcher digest invalid');
171
+ for(const key of ['release_root','generation_root','config_path']) {
172
+ if(typeof value[key]!=='string'||value[key].length<3) throw new Error(`active pointer ${key} invalid`);
173
+ }
174
+ return {
175
+ schema:POINTER_SCHEMA,
176
+ release_id:value.release_id,
177
+ version:value.version,
178
+ manifest_sha256:value.manifest_sha256,
179
+ release_root:value.release_root,
180
+ generation_root:value.generation_root,
181
+ generation_manifest_sha256:value.generation_manifest_sha256,
182
+ launcher_sha256:value.launcher_sha256,
183
+ config_path:value.config_path
184
+ };
185
+ }
186
+
187
+ export async function atomicWriteActivePointer(file,pointer){
188
+ const normalized=parseActivePointer(JSON.stringify(pointer));
189
+ await atomicJson(file,normalized);
190
+ const observed=parseActivePointer(await readFile(file,'utf8'));
191
+ if(JSON.stringify(observed)!==JSON.stringify(normalized)) throw new Error('active pointer readback mismatch');
192
+ return observed;
193
+ }
194
+ export function renderStableLauncherCmd(){
195
+ return [
196
+ '@echo off',
197
+ 'setlocal EnableExtensions DisableDelayedExpansion',
198
+ 'powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\\.deadbyte-mcp\\bin\\DEADBYTE-MCP.ps1" %*',
199
+ 'exit /b %ERRORLEVEL%',
200
+ ''
201
+ ].join('\r\n');
202
+ }
203
+
204
+ export function renderStableLauncherPs1(){
205
+ return [
206
+ '#requires -Version 5.1',
207
+ '[CmdletBinding()]',
208
+ 'param([Parameter(ValueFromRemainingArguments=$true)][string[]]$ControllerArgs)',
209
+ 'Set-StrictMode -Version 3.0',
210
+ "$ErrorActionPreference = 'Stop'",
211
+ "$StateRoot = Join-Path $env:USERPROFILE '.deadbyte-mcp'",
212
+ "$PointerPath = Join-Path $StateRoot 'active-release.json'",
213
+ "function Fail([string]$m) { throw ('DEADBYTE launcher: ' + $m) }",
214
+ "if (-not (Test-Path -LiteralPath $PointerPath -PathType Leaf)) { Fail 'active release pointer missing' }",
215
+ '$p = Get-Content -LiteralPath $PointerPath -Raw -Encoding UTF8 | ConvertFrom-Json',
216
+ "if ($p.schema -ne 'deadbyte.active-release.v1') { Fail 'active release schema mismatch' }",
217
+ "if ([string]$p.manifest_sha256 -notmatch '^[0-9a-f]{64}$') { Fail 'active manifest digest invalid' }",
218
+ "if ([string]$p.generation_manifest_sha256 -notmatch '^[0-9a-f]{64}$') { Fail 'active generation manifest digest invalid' }",
219
+ "if ([string]$p.launcher_sha256 -notmatch '^[0-9a-f]{64}$') { Fail 'active launcher digest invalid' }",
220
+ '$launcherHash = (Get-FileHash -LiteralPath $MyInvocation.MyCommand.Path -Algorithm SHA256).Hash.ToLowerInvariant()',
221
+ "if ($launcherHash -ne [string]$p.launcher_sha256) { Fail 'stable launcher hash mismatch' }",
222
+ "$expectedId = 'v{0}-{1}' -f [string]$p.version,([string]$p.manifest_sha256).Substring(0,16)",
223
+ "if ([string]$p.release_id -ne $expectedId) { Fail 'active release id mismatch' }",
224
+ "$releases = [IO.Path]::GetFullPath((Join-Path $StateRoot 'releases')).TrimEnd('\\') + '\\'",
225
+ "$generations = [IO.Path]::GetFullPath((Join-Path $StateRoot 'generations')).TrimEnd('\\') + '\\'",
226
+ '$releaseRoot = [IO.Path]::GetFullPath([string]$p.release_root)',
227
+ '$generationRoot = [IO.Path]::GetFullPath([string]$p.generation_root)',
228
+ '$configPath = [IO.Path]::GetFullPath([string]$p.config_path)',
229
+ "if (-not $releaseRoot.StartsWith($releases,[StringComparison]::OrdinalIgnoreCase)) { Fail 'release root escapes release store' }",
230
+ "if (-not $generationRoot.StartsWith($generations,[StringComparison]::OrdinalIgnoreCase)) { Fail 'generation root escapes generation store' }",
231
+ "$expectedConfig = Join-Path $generationRoot 'controller-config.psd1'",
232
+ "if (-not [string]::Equals($configPath,$expectedConfig,[StringComparison]::OrdinalIgnoreCase)) { Fail 'config path is not generation-bound' }",
233
+ "$manifest = Join-Path $releaseRoot 'MANIFEST.SHA256'",
234
+ "$generationManifest = Join-Path $generationRoot 'GENERATION.SHA256'",
235
+ "$package = Join-Path $releaseRoot 'package.json'",
236
+ "$controller = Join-Path $releaseRoot 'controller\\deadbyte-controller.ps1'",
237
+ "foreach ($file in @($manifest,$generationManifest,$package,$controller,$configPath)) { if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { Fail ('required file missing: ' + $file) } }",
238
+ '$manifestHash = (Get-FileHash -LiteralPath $manifest -Algorithm SHA256).Hash.ToLowerInvariant()',
239
+ "if ($manifestHash -ne [string]$p.manifest_sha256) { Fail 'active manifest hash mismatch' }",
240
+ '$generationManifestHash = (Get-FileHash -LiteralPath $generationManifest -Algorithm SHA256).Hash.ToLowerInvariant()',
241
+ "if ($generationManifestHash -ne [string]$p.generation_manifest_sha256) { Fail 'active generation manifest hash mismatch' }",
242
+ '$releasePrefix = $releaseRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar',
243
+ '$generationPrefix = $generationRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar',
244
+ '$manifestLines = Get-Content -LiteralPath $manifest -Encoding UTF8',
245
+ '$manifestPathSet = @{}',
246
+ 'foreach ($line in $manifestLines) {',
247
+ " if ([string]::IsNullOrWhiteSpace([string]$line)) { continue }",
248
+ " if ([string]$line -notmatch '^([0-9a-f]{64}) (.+)$') { Fail 'release manifest line malformed' }",
249
+ ' $expectedHash = [string]$Matches[1]',
250
+ ' $rel = [string]$Matches[2]',
251
+ " if ([IO.Path]::IsPathRooted($rel) -or (($rel -split '[\\/]') -contains '..')) { Fail ('unsafe release manifest path: ' + $rel) }",
252
+ " $relKey = ($rel -replace '\\\\','/').ToLowerInvariant()",
253
+ " if ($manifestPathSet.ContainsKey($relKey)) { Fail ('duplicate release manifest path: ' + $rel) }",
254
+ ' $manifestPathSet[$relKey] = $true',
255
+ ' $target = [IO.Path]::GetFullPath((Join-Path $releaseRoot $rel))',
256
+ " if (-not $target.StartsWith($releasePrefix,[StringComparison]::OrdinalIgnoreCase)) { Fail ('release manifest path escapes slot: ' + $rel) }",
257
+ " if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { Fail ('release file missing: ' + $rel) }",
258
+ ' $targetItem = Get-Item -LiteralPath $target -Force',
259
+ " if (($targetItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Fail ('release reparse point forbidden: ' + $rel) }",
260
+ ' $actualHash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()',
261
+ " if ($actualHash -ne $expectedHash) { Fail ('release file hash mismatch: ' + $rel) }",
262
+ '}',
263
+ '$releaseItems = Get-ChildItem -LiteralPath $releaseRoot -Force -Recurse',
264
+ 'foreach ($item in $releaseItems) {',
265
+ " if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Fail ('release reparse point forbidden: ' + $item.FullName) }",
266
+ ' if ($item.PSIsContainer) { continue }',
267
+ ' if ([string]::Equals($item.FullName,$manifest,[StringComparison]::OrdinalIgnoreCase)) { continue }',
268
+ " if (-not $item.FullName.StartsWith($releasePrefix,[StringComparison]::OrdinalIgnoreCase)) { Fail ('release file escapes slot: ' + $item.FullName) }",
269
+ " $actualRel = $item.FullName.Substring($releasePrefix.Length).Replace('\\','/')",
270
+ " if (-not $manifestPathSet.ContainsKey($actualRel.ToLowerInvariant())) { Fail ('unexpected release file: ' + $actualRel) }",
271
+ '}',
272
+ '$generationLines = Get-Content -LiteralPath $generationManifest -Encoding UTF8',
273
+ 'foreach ($line in $generationLines) {',
274
+ " if ([string]::IsNullOrWhiteSpace([string]$line)) { continue }",
275
+ " if ([string]$line -notmatch '^([0-9a-f]{64}) ([A-Za-z0-9._-]+)$') { Fail 'generation manifest line malformed' }",
276
+ ' $expectedHash = [string]$Matches[1]',
277
+ ' $rel = [string]$Matches[2]',
278
+ ' $target = [IO.Path]::GetFullPath((Join-Path $generationRoot $rel))',
279
+ " if (-not $target.StartsWith($generationPrefix,[StringComparison]::OrdinalIgnoreCase)) { Fail ('generation manifest path escapes generation: ' + $rel) }",
280
+ " if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { Fail ('generation file missing: ' + $rel) }",
281
+ ' $actualHash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()',
282
+ " if ($actualHash -ne $expectedHash) { Fail ('generation file hash mismatch: ' + $rel) }",
283
+ '}',
284
+ '$pkg = Get-Content -LiteralPath $package -Raw -Encoding UTF8 | ConvertFrom-Json',
285
+ "if ([string]$pkg.name -ne 'deadbyte-mcp' -or [string]$pkg.version -ne [string]$p.version) { Fail 'active package identity mismatch' }",
286
+ '$env:DEADBYTE_STATE_ROOT = $StateRoot',
287
+ '$env:DEADBYTE_CONTROLLER_CONFIG = $configPath',
288
+ "$env:DEADBYTE_CONTROLLER_RUNTIME_ROOT = Join-Path $StateRoot 'runtime'",
289
+ "$powershell = Join-Path $PSHOME 'powershell.exe'",
290
+ '& $powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File $controller @ControllerArgs',
291
+ 'exit $LASTEXITCODE',
292
+ ''
293
+ ].join('\r\n');
294
+ }