deadbyte-mcp 0.15.47 → 0.15.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/MANIFEST.SHA256 +111 -48
  2. package/PROVENANCE.json +1 -1
  3. package/README.txt +18 -18
  4. package/RELEASE.json +3 -3
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +2 -2
  7. package/bench/r121-epoch-readhead.mjs +54 -0
  8. package/bench/r124-epoch-predecessor.mjs +53 -0
  9. package/bin/deadbyte-epoch-lock-r85.node +0 -0
  10. package/controller/deadbyte-process-policy.json +4 -4
  11. package/docs/HARNESS2-ADAPTATION-20261020.md +24 -0
  12. package/docs/R128-RELEASE-CANDIDATE.md +26 -0
  13. package/docs/R75-RECOVERY-HANDOFF.md +16 -0
  14. package/docs/R85-EPOCH-LOCK-BINARY.json +13 -0
  15. package/native/deadbyte-epoch-lock-r85.cpp +241 -0
  16. package/package.json +1 -1
  17. package/scripts/build-epoch-lock-r85.ps1 +34 -0
  18. package/scripts/deploy-r48.mjs +3 -1
  19. package/scripts/npm-release-r48.mjs +18 -4
  20. package/scripts/r75-npm-bin-integrity-gate.mjs +33 -0
  21. package/scripts/release-manifest.mjs +1 -1
  22. package/scripts/verify-authority-epoch-history.mjs +38 -0
  23. package/scripts/verify-r70-full-regression.ps1 +12 -2
  24. package/src/activation-authority-proof-r75.mjs +67 -0
  25. package/src/activation-continuity-engine-r65.mjs +17 -10
  26. package/src/activation-preflight-r65.mjs +13 -0
  27. package/src/agent-eval-r42.mjs +30 -0
  28. package/src/authority-continuity-r65.mjs +442 -29
  29. package/src/autonomous-local-supervisor.mjs +77 -47
  30. package/src/deadbyte-cli.mjs +73 -46
  31. package/src/epoch-lock-r85.mjs +48 -0
  32. package/src/foreground-marker-cas-win32.mjs +284 -0
  33. package/src/foreground-process-identity.mjs +3 -3
  34. package/src/privileged-install-trust-r89.mjs +196 -0
  35. package/src/remote-live-log.mjs +15 -8
  36. package/src/remote-log-decode-r60.mjs +65 -0
  37. package/src/remote-session-guardian.mjs +13 -12
  38. package/src/runtime-update.mjs +5 -22
  39. package/src/version.mjs +1 -1
  40. package/src/windows-powershell-environment.mjs +24 -0
  41. package/test/autonomous-local-supervisor.test.mjs +1 -1
  42. package/test/cli-entrypoint.test.mjs +1 -1
  43. package/test/harness2-eval-context-isolation.test.mjs +90 -0
  44. package/test/helpers/r75-authority-proof.mjs +9 -0
  45. package/test/r100-posix-owned-lock-race.test.mjs +72 -0
  46. package/test/r101-acl-probe-environment.test.mjs +51 -0
  47. package/test/r106-trusted-powershell-spawn.test.mjs +53 -0
  48. package/test/r107-updater-powershell-path.test.mjs +77 -0
  49. package/test/r108-remote-powershell-path.test.mjs +59 -0
  50. package/test/r110-supervisor-powershell-path.test.mjs +28 -0
  51. package/test/r111-supervisor-marker-cas.test.mjs +116 -0
  52. package/test/r112-supervisor-claim-cas.test.mjs +63 -0
  53. package/test/r113-supervisor-cleanup-terminality.test.mjs +100 -0
  54. package/test/r114-supervisor-absence-proof.test.mjs +45 -0
  55. package/test/r115-posix-continuity-entrypoint.test.mjs +17 -0
  56. package/test/r116-authority-crash-matrix.test.mjs +83 -0
  57. package/test/r117-concurrent-orphan-recovery.test.mjs +54 -0
  58. package/test/r118-cross-process-orphan-recovery.test.mjs +99 -0
  59. package/test/r119-recovery-crash-after-commit.test.mjs +91 -0
  60. package/test/r120-authority-history-audit.test.mjs +103 -0
  61. package/test/r121-signed-transition-invariant.test.mjs +132 -0
  62. package/test/r122-epoch-bounded-read.test.mjs +58 -0
  63. package/test/r123-recovery-bounded-read.test.mjs +64 -0
  64. package/test/r124-live-log-integrity.test.mjs +66 -0
  65. package/test/r124-live-log-tail-boundaries.test.mjs +57 -0
  66. package/test/r124-signed-predecessor-shape.test.mjs +66 -0
  67. package/test/r125-restore-binding-types.test.mjs +63 -0
  68. package/test/r126-restore-policy-envelope.test.mjs +89 -0
  69. package/test/r127-restore-append-policy-boundary.test.mjs +25 -0
  70. package/test/r33-closeout-regression.test.mjs +2 -1
  71. package/test/r54-offline-predecessor-self-exclusion.test.mjs +2 -2
  72. package/test/r55-offline-predecessor-guardian-family.test.mjs +2 -2
  73. package/test/r56-rollback-launcher-restore.test.mjs +2 -2
  74. package/test/r57-core-startup-race.test.mjs +2 -2
  75. package/test/r58-concurrent-recovery.test.mjs +10 -7
  76. package/test/r58-stale-owner.test.mjs +5 -6
  77. package/test/r59-tool-surface-compat.test.mjs +1 -1
  78. package/test/r65-activation-orchestration.test.mjs +2 -1
  79. package/test/r65-activation-preflight.test.mjs +7 -6
  80. package/test/r65-authority-epoch-recovery.test.mjs +3 -1
  81. package/test/r65-authority-epoch-store.test.mjs +9 -6
  82. package/test/r65-authority-restored-safe.test.mjs +2 -1
  83. package/test/r65-deploy-preflight.test.mjs +13 -14
  84. package/test/r65-epoch-native-activation-integration.test.mjs +5 -4
  85. package/test/r65-legacy-authority-migration.test.mjs +2 -1
  86. package/test/r67-npm-cross-volume.test.mjs +9 -2
  87. package/test/r71-full-verifier-artifact.test.mjs +21 -15
  88. package/test/r71-isolated-slot-rollback-restore.test.mjs +3 -1
  89. package/test/r71-release-identity.test.mjs +3 -1
  90. package/test/r74-kernel-lock.test.mjs +12 -6
  91. package/test/r74-prepublish-contract.test.mjs +3 -1
  92. package/test/r75-activation-authority-proof.test.mjs +46 -0
  93. package/test/r75-authority-evidence.test.mjs +77 -0
  94. package/test/r76-pinned-marker-cas.test.mjs +78 -0
  95. package/test/r77-epoch-grant-cas.test.mjs +51 -0
  96. package/test/r78-parent-junction-cas.test.mjs +62 -0
  97. package/test/r79-orphan-revocation-reconcile.test.mjs +88 -0
  98. package/test/r79-windows-fsync-regression.test.mjs +45 -0
  99. package/test/r80-durable-epoch-commit.test.mjs +76 -0
  100. package/test/r82-epoch-lock-owned-cas.test.mjs +93 -0
  101. package/test/r83-epoch-lock-fileid-cas.test.mjs +92 -0
  102. package/test/r84-epoch-lock-created-fileid.test.mjs +112 -0
  103. package/test/r85-epoch-lock-release-lifetime.test.mjs +25 -0
  104. package/test/r85-epoch-repeat-append.test.mjs +30 -0
  105. package/test/r86-epoch-cleanup-and-loader.test.mjs +126 -0
  106. package/test/r87-crashed-orphan-revoke-reconcile.test.mjs +110 -0
  107. package/test/r88-native-disposition-failure.test.mjs +35 -0
  108. package/test/r89-privileged-install-boundary.test.mjs +123 -0
  109. package/test/r92-acl-ace-applicability.test.mjs +45 -0
  110. package/test/r94-win32-propagation-enum.test.mjs +51 -0
  111. package/test/r99-privileged-import-closure.test.mjs +45 -0
  112. package/test/remote-session-cli.test.mjs +69 -9
@@ -0,0 +1,284 @@
1
+ // Windows-only, handle-bound conditional deletion. Never use pathname rm after a read.
2
+ import {createHash} from 'node:crypto';
3
+ import {lstat,readFile} from 'node:fs/promises';
4
+ import {spawnSync} from 'node:child_process';
5
+ import path from 'node:path';
6
+ import {windowsPowerShellEnvironment} from './windows-powershell-environment.mjs';
7
+
8
+ const HEX64=/^[0-9a-f]{64}$/;
9
+ const digest=bytes=>createHash('sha256').update(bytes).digest('hex');
10
+ const sameFile=(a,b)=>a&&b&&a.isFile()&&b.isFile()&&!a.isSymbolicLink()&&!b.isSymbolicLink()&&
11
+ a.dev===b.dev&&a.ino===b.ino&&a.size===b.size&&a.mtimeMs===b.mtimeMs&&a.ctimeMs===b.ctimeMs;
12
+
13
+ export async function readRemoteMarkerSnapshot(file){
14
+ let before;
15
+ try{before=await lstat(file);}catch(error){if(error?.code==='ENOENT')return null;throw error;}
16
+ if(!before.isFile()||before.isSymbolicLink()||before.size>16384||before.size<2)
17
+ throw new Error('remote marker not a bounded regular file');
18
+ const bytes=await readFile(file);
19
+ const after=await lstat(file);
20
+ if(!sameFile(before,after)||bytes.length!==before.size)
21
+ throw new Error('remote marker identity changed during snapshot');
22
+ let marker;
23
+ try{marker=JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/,''));}
24
+ catch{throw new Error('remote marker JSON invalid');}
25
+ return Object.freeze({marker,sha256:digest(bytes)});
26
+ }
27
+
28
+ // Native ancestor handles pin the non-reparse path chain, then a single leaf handle owns read/hash/deletion.
29
+ // Open share=READ only: concurrent replace, rename, delete and writes must fail.
30
+ // Do not replace this with a Node readFile()+rm(), rename(), or link()+unlink().
31
+ const nativeCommand=String.raw`
32
+ $ErrorActionPreference='Stop'
33
+ $source=@'
34
+ using System;
35
+ using System.IO;
36
+ using System.ComponentModel;
37
+ using System.Runtime.InteropServices;
38
+ using System.Security.Cryptography;
39
+ using Microsoft.Win32.SafeHandles;
40
+ using System.Collections.Generic;
41
+ public static class DeadbytePinnedDeleteV2 {
42
+ const uint FILE_READ_DATA = 0x0001;
43
+ const uint FILE_WRITE_DATA = 0x0002;
44
+ const uint FILE_READ_ATTRIBUTES = 0x0080;
45
+ const uint DELETE = 0x00010000;
46
+ const uint FILE_SHARE_READ = 0x00000001;
47
+ const uint OPEN_EXISTING = 3;
48
+ const uint CREATE_NEW = 1;
49
+ const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
50
+ const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
51
+ const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
52
+ const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
53
+ [StructLayout(LayoutKind.Sequential)] struct FILE_ATTRIBUTE_TAG_INFO {
54
+ public uint FileAttributes;
55
+ public uint ReparseTag;
56
+ }
57
+ // FileIdInfo = 18, FILE_ID_INFO = 8-byte volume serial + 16-byte file ID.
58
+ // No fallback to filename, hashes, inode heuristics, or legacy 64-bit file index.
59
+ [StructLayout(LayoutKind.Sequential)] struct FILE_ID_INFO {
60
+ public ulong VolumeSerialNumber;
61
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst=16)] public byte[] FileId;
62
+ }
63
+ [DllImport("kernel32.dll",EntryPoint="CreateFileW",CharSet=CharSet.Unicode,SetLastError=true)]
64
+ static extern SafeFileHandle CreateFile(string file,uint access,uint share,IntPtr security,uint creation,uint flags,IntPtr templateFile);
65
+ [DllImport("kernel32.dll",SetLastError=true)]
66
+ static extern bool GetFileInformationByHandleEx(SafeFileHandle file,int cls,out FILE_ATTRIBUTE_TAG_INFO info,uint size);
67
+ [DllImport("kernel32.dll",EntryPoint="GetFileInformationByHandleEx",SetLastError=true)]
68
+ static extern bool GetFileIdInformationByHandleEx(SafeFileHandle file,int cls,out FILE_ID_INFO info,uint size);
69
+ [DllImport("kernel32.dll",SetLastError=true)]
70
+ static extern bool SetFileInformationByHandle(SafeFileHandle file,int cls,IntPtr info,uint size);
71
+ // Pin the ENTIRE local-drive ancestor chain from the drive root to the leaf's parent.
72
+ // Share=READ only denies concurrent write/rename/delete of each pinned component.
73
+ // Checking names separately and closing each handle would reintroduce the race.
74
+ static void PinAncestor(string directory,List<SafeFileHandle> pins) {
75
+ var handle=CreateFile(directory,FILE_READ_ATTRIBUTES,FILE_SHARE_READ,
76
+ IntPtr.Zero,OPEN_EXISTING,FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT,IntPtr.Zero);
77
+ if(handle.IsInvalid){
78
+ int error=Marshal.GetLastWin32Error();handle.Dispose();
79
+ throw new Win32Exception(error,"cannot pin path ancestor (fail closed)");
80
+ }
81
+ try {
82
+ FILE_ATTRIBUTE_TAG_INFO attr;
83
+ if(!GetFileInformationByHandleEx(handle,9,out attr,(uint)Marshal.SizeOf(typeof(FILE_ATTRIBUTE_TAG_INFO))))
84
+ throw new Win32Exception(Marshal.GetLastWin32Error(),"ancestor attributes unavailable");
85
+ if((attr.FileAttributes&FILE_ATTRIBUTE_DIRECTORY)==0 ||
86
+ (attr.FileAttributes&FILE_ATTRIBUTE_REPARSE_POINT)!=0)
87
+ throw new InvalidOperationException("ancestor is a junction, symlink, reparse point or non-directory");
88
+ pins.Add(handle);
89
+ } catch {handle.Dispose();throw;}
90
+ }
91
+ static string FileIdentity(SafeFileHandle handle) {
92
+ FILE_ID_INFO info;
93
+ if(!GetFileIdInformationByHandleEx(handle,18,out info,(uint)Marshal.SizeOf(typeof(FILE_ID_INFO))))
94
+ throw new Win32Exception(Marshal.GetLastWin32Error(),"128-bit native File ID unavailable; fail closed");
95
+ if(info.FileId==null||info.FileId.Length!=16)
96
+ throw new InvalidOperationException("native file identity unavailable");
97
+ bool allZero=true;
98
+ foreach(byte b in info.FileId)if(b!=0){allZero=false;break;}
99
+ if(allZero)throw new InvalidOperationException("zero native File ID; fail closed");
100
+ return info.VolumeSerialNumber.ToString("x16")+":"+BitConverter.ToString(info.FileId).Replace("-","").ToLowerInvariant();
101
+ }
102
+ public static string DeleteIfHash(string file,string expectedSha,string expectedId) {
103
+ return WithPinnedAncestors(file,"delete",expectedSha,expectedId,null);
104
+ }
105
+ public static string Snapshot(string file) {
106
+ return WithPinnedAncestors(file,"snapshot",null,null,null);
107
+ }
108
+ public static string CreateLock(string file,string encodedBytes) {
109
+ return WithPinnedAncestors(file,"create",null,null,encodedBytes);
110
+ }
111
+ static string WithPinnedAncestors(string file,string operation,string expectedSha,string expectedId,string encodedBytes) {
112
+ string full=Path.GetFullPath(file);
113
+ // No UNC paths, device paths, ADS or extended-path aliases in this safety boundary.
114
+ if(full.Length<4 || !Char.IsLetter(full[0]) || full[1]!=':' || full[2]!='\\' ||
115
+ full.IndexOf(':',2)>=0 || String.IsNullOrEmpty(Path.GetFileName(full)))
116
+ throw new InvalidOperationException("pinned deletion requires an ordinary local drive file path");
117
+ string root=Path.GetPathRoot(full);
118
+ string parent=Path.GetDirectoryName(full);
119
+ var pins=new List<SafeFileHandle>();
120
+ try {
121
+ string current=root;
122
+ PinAncestor(current,pins);
123
+ string tail=parent.Substring(root.Length);
124
+ foreach(string component in tail.Split(new[]{'\\'},StringSplitOptions.RemoveEmptyEntries)){
125
+ current=Path.Combine(current,component);
126
+ PinAncestor(current,pins);
127
+ }
128
+ if(operation=="snapshot")return SnapshotPinnedLeaf(full);
129
+ if(operation=="create")return CreatePinnedLeaf(full,encodedBytes);
130
+ return DeletePinnedLeaf(full,expectedSha,expectedId);
131
+ } finally {
132
+ for(int i=pins.Count-1;i>=0;i--)pins[i].Dispose();
133
+ }
134
+ }
135
+ static byte[] ReadBoundedLeaf(SafeFileHandle handle){
136
+ using(var stream=new FileStream(handle,FileAccess.Read)){
137
+ if(stream.Length<2||stream.Length>16384)throw new InvalidOperationException("pinned file size invalid");
138
+ byte[] content=new byte[(int)stream.Length];int index=0,read;
139
+ while(index<content.Length&&(read=stream.Read(content,index,content.Length-index))>0)index+=read;
140
+ if(index!=content.Length)throw new IOException("pinned file truncated");
141
+ return content;
142
+ }
143
+ }
144
+ static void ValidateLeaf(SafeFileHandle handle){
145
+ FILE_ATTRIBUTE_TAG_INFO attr;
146
+ if(!GetFileInformationByHandleEx(handle,9,out attr,(uint)Marshal.SizeOf(typeof(FILE_ATTRIBUTE_TAG_INFO))))
147
+ throw new Win32Exception(Marshal.GetLastWin32Error(),"pinned leaf attributes unavailable");
148
+ if((attr.FileAttributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT))!=0)
149
+ throw new InvalidOperationException("pinned leaf is a reparse point or directory");
150
+ }
151
+ static string SnapshotPinnedLeaf(string file){
152
+ using(var handle=CreateFile(file,FILE_READ_DATA|FILE_READ_ATTRIBUTES,FILE_SHARE_READ,IntPtr.Zero,OPEN_EXISTING,FILE_FLAG_OPEN_REPARSE_POINT,IntPtr.Zero)){
153
+ if(handle.IsInvalid){
154
+ int error=Marshal.GetLastWin32Error();
155
+ if(error==2||error==3)return "MISSING";
156
+ throw new Win32Exception(error,"epoch lock could not be pinned for identity snapshot");
157
+ }
158
+ ValidateLeaf(handle);
159
+ string id=FileIdentity(handle);
160
+ byte[] bytes=ReadBoundedLeaf(handle);
161
+ string hash=BitConverter.ToString(SHA256.Create().ComputeHash(bytes)).Replace("-","").ToLowerInvariant();
162
+ return "SNAPSHOT|"+id+"|"+hash+"|"+Convert.ToBase64String(bytes);
163
+ }
164
+ }
165
+ static string CreatePinnedLeaf(string file,string encodedBytes){
166
+ byte[] bytes=Convert.FromBase64String(encodedBytes);
167
+ if(bytes.Length<2||bytes.Length>16384)throw new InvalidOperationException("epoch lock bytes invalid");
168
+ using(var handle=CreateFile(file,FILE_READ_DATA|FILE_WRITE_DATA|FILE_READ_ATTRIBUTES,
169
+ FILE_SHARE_READ,IntPtr.Zero,CREATE_NEW,FILE_FLAG_OPEN_REPARSE_POINT,IntPtr.Zero)){
170
+ if(handle.IsInvalid){
171
+ int error=Marshal.GetLastWin32Error();
172
+ if(error==80||error==183)return "EXISTS";
173
+ throw new Win32Exception(error,"epoch lock creation failed closed");
174
+ }
175
+ ValidateLeaf(handle);
176
+ string id=FileIdentity(handle);
177
+ using(var stream=new FileStream(handle,FileAccess.ReadWrite)){
178
+ stream.Write(bytes,0,bytes.Length);
179
+ stream.Flush(true);
180
+ }
181
+ string hash=BitConverter.ToString(SHA256.Create().ComputeHash(bytes)).Replace("-","").ToLowerInvariant();
182
+ return "CREATED|"+id+"|"+hash;
183
+ }
184
+ }
185
+ static string DeletePinnedLeaf(string file,string expectedSha,string expectedId) {
186
+ using(var handle=CreateFile(file,FILE_READ_DATA|FILE_READ_ATTRIBUTES|DELETE,FILE_SHARE_READ,IntPtr.Zero,OPEN_EXISTING,FILE_FLAG_OPEN_REPARSE_POINT,IntPtr.Zero)) {
187
+ if(handle.IsInvalid){
188
+ int error=Marshal.GetLastWin32Error();
189
+ if(error==2||error==3)return "MISSING";
190
+ throw new Win32Exception(error,"remote marker cannot be exclusively pinned");
191
+ }
192
+ ValidateLeaf(handle);
193
+ // PowerShell can marshal $null to an empty string for a C# string parameter.
194
+ // Only epoch-lock callers with an explicit File ID require this comparison.
195
+ string actualId=String.IsNullOrEmpty(expectedId)?null:FileIdentity(handle);
196
+ // Identity must be checked on the SAME handle used for hash and disposition.
197
+ if(!String.IsNullOrEmpty(expectedId)&&!String.Equals(actualId,expectedId,StringComparison.Ordinal))return "CHANGED";
198
+ using(var stream=new FileStream(handle,FileAccess.Read)){
199
+ if(stream.Length<2||stream.Length>16384)throw new InvalidOperationException("pinned file size invalid");
200
+ byte[] content=new byte[(int)stream.Length];int index=0,read;
201
+ while(index<content.Length&&(read=stream.Read(content,index,content.Length-index))>0)index+=read;
202
+ if(index!=content.Length)throw new IOException("pinned file truncated");
203
+ string actual=BitConverter.ToString(SHA256.Create().ComputeHash(content)).Replace("-","").ToLowerInvariant();
204
+ if(!String.Equals(actual,expectedSha,StringComparison.Ordinal))return "CHANGED";
205
+ IntPtr flag=Marshal.AllocHGlobal(4);
206
+ try {
207
+ Marshal.WriteInt32(flag,1);
208
+ if(!SetFileInformationByHandle(handle,4,flag,4))
209
+ throw new Win32Exception(Marshal.GetLastWin32Error(),"pinned file delete disposition denied");
210
+ } finally {Marshal.FreeHGlobal(flag);}
211
+ return "REMOVED";
212
+ }
213
+ }
214
+ }
215
+ }
216
+ '@
217
+ Add-Type -TypeDefinition $source -Language CSharp -ErrorAction Stop | Out-Null
218
+ $file=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:DEADBYTE_PINNED_MARKER_B64))
219
+ $sha=$env:DEADBYTE_PINNED_MARKER_SHA
220
+ $id=$env:DEADBYTE_PINNED_MARKER_FILE_ID
221
+ if($env:DEADBYTE_PINNED_MARKER_OP -eq 'snapshot') {
222
+ [Console]::Out.WriteLine([DeadbytePinnedDeleteV2]::Snapshot($file))
223
+ } elseif($env:DEADBYTE_PINNED_MARKER_OP -eq 'create') {
224
+ [Console]::Out.WriteLine([DeadbytePinnedDeleteV2]::CreateLock($file,$env:DEADBYTE_PINNED_MARKER_BYTES_B64))
225
+ } else {
226
+ if([String]::IsNullOrEmpty($id)){$id=$null}
227
+ [Console]::Out.WriteLine([DeadbytePinnedDeleteV2]::DeleteIfHash($file,$sha,$id))
228
+ }
229
+ `;
230
+
231
+ const FILE_ID_PATTERN=/^[0-9a-f]{16}:[0-9a-f]{32}$/;
232
+ function invokePinnedNative(file,{op,sha=null,fileId=null,bytes=null}={}){
233
+ if(process.platform!=='win32')throw new Error('native pinned file I/O is Windows-only');
234
+ if(typeof file!=='string'||!path.isAbsolute(file)||!['snapshot','delete','create'].includes(op)||
235
+ (op==='delete'&&!HEX64.test(sha??''))||
236
+ (op==='create'&&(!Buffer.isBuffer(bytes)||bytes.length<2||bytes.length>16384))||
237
+ (fileId!==null&&!FILE_ID_PATTERN.test(fileId)))
238
+ throw new Error('native pinned file arguments invalid');
239
+ const powershell='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
240
+ const result=spawnSync(powershell,['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',
241
+ Buffer.from(nativeCommand,'utf16le').toString('base64')],{
242
+ encoding:'utf8',windowsHide:true,shell:false,timeout:25000,maxBuffer:262144,
243
+ env:windowsPowerShellEnvironment({...process.env,SystemRoot:'C:\\Windows',WINDIR:'C:\\Windows',
244
+ DEADBYTE_PINNED_MARKER_B64:Buffer.from(file,'utf8').toString('base64'),
245
+ DEADBYTE_PINNED_MARKER_SHA:sha??'',
246
+ DEADBYTE_PINNED_MARKER_FILE_ID:fileId??'',
247
+ DEADBYTE_PINNED_MARKER_BYTES_B64:bytes?.toString('base64')??'',
248
+ DEADBYTE_PINNED_MARKER_OP:op})
249
+ });
250
+ if(result.error||result.status!==0)throw new Error('native pinned file I/O failed closed');
251
+ return result.stdout.trim();
252
+ }
253
+
254
+ // Snapshot locks using one native handle for owner bytes, SHA-256, and FileIdInfo.
255
+ // Node lstat dev/ino is insufficient as the sole cross-API identity authority.
256
+ export function readPinnedEpochLockSnapshot(file){
257
+ const result=invokePinnedNative(file,{op:'snapshot'});
258
+ if(result==='MISSING')return null;
259
+ const match=/^SNAPSHOT\|([0-9a-f]{16}:[0-9a-f]{32})\|([0-9a-f]{64})\|([A-Za-z0-9+/]+={0,2})$/.exec(result);
260
+ if(!match)throw new Error('native pinned epoch lock snapshot malformed');
261
+ const bytes=Buffer.from(match[3],'base64');
262
+ if(bytes.length<2||bytes.length>16384||bytes.toString('base64')!==match[3]||digest(bytes)!==match[2])
263
+ throw new Error('native pinned epoch lock snapshot content mismatch');
264
+ return Object.freeze({bytes,sha256:match[2],fileId:match[1]});
265
+ }
266
+
267
+ // Creation is pinned under the same non-reparse ancestor chain. The returned
268
+ // File ID belongs to the handle that wrote and flushed the original lock bytes.
269
+ export function createPinnedEpochLock(file,bytes){
270
+ const result=invokePinnedNative(file,{op:'create',bytes});
271
+ if(result==='EXISTS')return null;
272
+ const match=/^CREATED\|([0-9a-f]{16}:[0-9a-f]{32})\|([0-9a-f]{64})$/.exec(result);
273
+ if(!match||digest(bytes)!==match[2])throw new Error('native epoch lock creation identity invalid');
274
+ return Object.freeze({fileId:match[1],sha256:match[2]});
275
+ }
276
+
277
+ // Existing remote-marker API remains content-CAS for backward compatibility.
278
+ // Epoch locks MUST pass an expected FileIdInfo captured from a native snapshot.
279
+ export function deletePinnedRemoteMarker(file,expectedSha256,options={}){
280
+ const expectedFileId=options?.expectedFileId??null;
281
+ const answer=invokePinnedNative(file,{op:'delete',sha:expectedSha256,fileId:expectedFileId});
282
+ if(!['REMOVED','MISSING','CHANGED'].includes(answer))throw new Error('handle-bound deletion result invalid');
283
+ return answer;
284
+ }
@@ -1,7 +1,7 @@
1
1
  import {spawnSync} from 'node:child_process';
2
2
  import {createHash} from 'node:crypto';
3
3
  import path from 'node:path';
4
- import {windowsPowerShellEnvironment} from './runtime-update.mjs';
4
+ import {windowsPowerShellEnvironment} from './windows-powershell-environment.mjs';
5
5
 
6
6
  export function validForegroundIdentity(value){
7
7
  return value?.schema==='deadbyte.foreground-process.v1'&&Number.isSafeInteger(value.pid)&&value.pid>0&&Number.isSafeInteger(value.parent_pid)&&value.parent_pid>=0&&typeof value.creation_utc==='string'&&/^\d{4}-\d\d-\d\dT.*Z$/.test(value.creation_utc)&&Number.isFinite(Date.parse(value.creation_utc))&&typeof value.executable_path==='string'&&path.isAbsolute(value.executable_path)&&/^[0-9a-f]{64}$/.test(value.command_line_sha256??'');
@@ -14,9 +14,9 @@ export function foregroundIdentityMatches(expected,observed){
14
14
  }
15
15
  export function queryForegroundIdentity(pid){
16
16
  if(process.platform!=='win32'||!Number.isSafeInteger(pid)||pid<1)throw new Error('foreground identity requires a positive Windows PID');
17
- const root=process.env.SystemRoot||process.env.WINDIR;if(!root)throw new Error('Windows system root missing');
17
+ const powershell='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
18
18
  const command=`$p=Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}'; if ($null -eq $p) {Write-Output 'null';exit 0}; [pscustomobject]@{pid=$p.ProcessId;parent_pid=$p.ParentProcessId;creation_utc=$p.CreationDate.ToUniversalTime().ToString('o');executable_path=$p.ExecutablePath;command_line=$p.CommandLine} | ConvertTo-Json -Compress`;
19
- const result=spawnSync(path.join(root,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoLogo','-NoProfile','-Command',command],{encoding:'utf8',windowsHide:true,env:windowsPowerShellEnvironment(process.env),timeout:15000,maxBuffer:1024*1024});
19
+ const result=spawnSync(powershell,['-NoLogo','-NoProfile','-Command',command],{encoding:'utf8',windowsHide:true,env:windowsPowerShellEnvironment({...process.env,SystemRoot:'C:\\Windows',WINDIR:'C:\\Windows'}),timeout:15000,maxBuffer:1024*1024});
20
20
  if(result.error||result.status!==0)throw new Error('foreground process identity query failed');
21
21
  let raw;try{raw=JSON.parse(result.stdout);}catch{throw new Error('foreground process identity query malformed');}
22
22
  if(raw===null)return null;
@@ -0,0 +1,196 @@
1
+ // R89 development: fail closed before requiring a privileged Windows native addon.
2
+ // Trust is an OS-owned installation property, not a manifest hash or npm version.
3
+ // This checks an honest process against lower-trust filesystem principals; it is
4
+ // NOT an attestation against a malicious actor already executing with equivalent
5
+ // write/administrative rights, or against a compromised Node/PowerShell runtime.
6
+ import {execFile} from 'node:child_process';
7
+ import {promisify} from 'node:util';
8
+ import path from 'node:path';
9
+
10
+ const execFileAsync=promisify(execFile);
11
+ function windowsPowerShellEnvironment(base=process.env){
12
+ const env={...base};
13
+ // The ACL probe must not autoload commands from caller-controlled module
14
+ // directories. Its executable is pinned to C:\Windows below, so use only
15
+ // that installation's built-in modules and matching OS-root variables.
16
+ for(const key of Object.keys(env)){
17
+ if(['psmodulepath','systemroot','windir'].includes(key.toLowerCase()))delete env[key];
18
+ }
19
+ env.SystemRoot='C:\\Windows';
20
+ env.WINDIR='C:\\Windows';
21
+ env.PSModulePath='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules';
22
+ return env;
23
+ }
24
+ const DRIVE=/^[A-Za-z]:\\/;
25
+ const SYSTEM='S-1-5-18';
26
+ const ADMINISTRATORS='S-1-5-32-544';
27
+ const TRUSTED_INSTALLER='S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464';
28
+ const TRUSTED=new Set([SYSTEM,ADMINISTRATORS,TRUSTED_INSTALLER]);
29
+ // Block untrusted changes to an existing directory or file, including security
30
+ // descriptor, content, attributes, or deletion. Create-child alone is not enough
31
+ // to replace an existing child, but full Modify contains DELETE and is denied.
32
+ const COMMON_MUTATION=0x000d0150; // WRITE_EA, DELETE_CHILD, WRITE_ATTRIBUTES, DELETE, WRITE_DAC, WRITE_OWNER
33
+ const FILE_MUTATION=COMMON_MUTATION|0x00000002|0x00000004; // WRITE_DATA / APPEND_DATA
34
+ const DIR_MUTATION=COMMON_MUTATION;
35
+ // Win32 ACEs may retain generic/indeterminate masks rather than expanded
36
+ // FILE_GENERIC_WRITE/FILE_ALL_ACCESS. Do not treat those as harmless.
37
+ const GENERIC_WRITE_OR_ALL=0x40000000|0x10000000;
38
+ const INDETERMINATE_ACCESS=0x02000000; // MAXIMUM_ALLOWED is not a finite read-only grant
39
+ const ANY_MUTATION_OR_UNDECIDABLE=(GENERIC_WRITE_OR_ALL|INDETERMINATE_ACCESS);
40
+ // Windows ACE scope is not the same as ACE inheritance origin. INHERIT_ONLY
41
+ // means the ACE does NOT apply to the currently inspected file/directory;
42
+ // it may apply to children, which are checked separately in the path chain.
43
+ // Missing / malformed scope must never silently become an allow.
44
+ const INHERIT_ONLY=2; // .NET PropagationFlags.InheritOnly=2; NoPropagateInherit=1
45
+ const KNOWN_INHERITANCE_FLAGS=3; // ContainerInherit | ObjectInherit
46
+ const KNOWN_PROPAGATION_FLAGS=3; // InheritOnly | NoPropagateInherit
47
+
48
+ function normalizeWindows(file){
49
+ if(typeof file!=='string'||!DRIVE.test(file)||file.includes('\0')||file.startsWith('\\\\'))
50
+ throw new Error('R89 privileged path must be a Windows drive-absolute path');
51
+ const normalized=path.win32.normalize(file);
52
+ if(!DRIVE.test(normalized))throw new Error('R89 privileged path normalization invalid');
53
+ return normalized.toLowerCase();
54
+ }
55
+ export function privilegedEpochInstallFiles(root,nodeExecutable){
56
+ const join=(...segments)=>path.win32.join(root,...segments);
57
+ return [nodeExecutable,
58
+ join('bin','deadbyte-epoch-lock-r85.node'),
59
+ join('docs','R85-EPOCH-LOCK-BINARY.json'),
60
+ join('native','deadbyte-epoch-lock-r85.cpp'),
61
+ join('src','epoch-lock-r85.mjs'),
62
+ join('src','privileged-install-trust-r89.mjs'),
63
+ join('src','authority-continuity-r65.mjs'),
64
+ join('src','canonical-json.mjs'),
65
+ join('src','text-policy.mjs'),
66
+ join('src','foreground-marker-cas-win32.mjs'),
67
+ join('src','foreground-process-identity.mjs'),
68
+ join('src','windows-powershell-environment.mjs')];
69
+ }
70
+ export function requiredAclPathChain(files){
71
+ if(!Array.isArray(files)||files.length<1||files.length>32)
72
+ throw new Error('R89 privileged path inventory invalid');
73
+ const seen=new Set();
74
+ for(const file of files){
75
+ let current=normalizeWindows(file);
76
+ for(let depth=0;depth<128;depth++){
77
+ seen.add(current);
78
+ const parent=path.win32.dirname(current);
79
+ if(parent===current)break;
80
+ if(depth===127)throw new Error('R89 privileged ancestor depth exceeded');
81
+ current=parent;
82
+ }
83
+ }
84
+ return [...seen].sort();
85
+ }
86
+
87
+ // Pure conservative evaluator. Records must come from Windows Get-Acl, never
88
+ // from a writable package file, environment variable or release metadata.
89
+ export function evaluatePrivilegedAclSnapshot(records,files){
90
+ const required=requiredAclPathChain(files);
91
+ if(!Array.isArray(records)||records.length!==required.length)
92
+ return Object.freeze({trusted:false,reason:'INCOMPLETE_ACL_SNAPSHOT'});
93
+ const byPath=new Map();
94
+ for(const rec of records){
95
+ let key;
96
+ try{key=normalizeWindows(rec?.path);}catch{return Object.freeze({trusted:false,reason:'INVALID_ACL_PATH'});}
97
+ if(byPath.has(key))return Object.freeze({trusted:false,reason:'DUPLICATE_ACL_PATH',path:key});
98
+ byPath.set(key,rec);
99
+ }
100
+ const leaves=new Set(files.map(normalizeWindows));
101
+ for(const file of required){
102
+ const rec=byPath.get(file);
103
+ if(!rec)return Object.freeze({trusted:false,reason:'MISSING_ACL_NODE',path:file});
104
+ if(rec.reparse!==false || typeof rec.directory!=='boolean' ||
105
+ !TRUSTED.has(rec.owner) || !Array.isArray(rec.aces))
106
+ return Object.freeze({trusted:false,reason:'UNTRUSTED_OWNER_OR_REPARSE',path:file});
107
+ if(leaves.has(file)&&rec.directory===true)
108
+ return Object.freeze({trusted:false,reason:'EXPECTED_FILE_NOT_FILE',path:file});
109
+ if(!leaves.has(file)&&rec.directory!==true)
110
+ return Object.freeze({trusted:false,reason:'EXPECTED_PARENT_NOT_DIRECTORY',path:file});
111
+ for(const ace of rec.aces){
112
+ if(!ace||typeof ace.sid!=='string'||!['Allow','Deny'].includes(ace.type)||
113
+ !Number.isSafeInteger(ace.mask)||ace.mask<0||ace.mask>0xffffffff)
114
+ return Object.freeze({trusted:false,reason:'UNKNOWN_ACL_RULE',path:file});
115
+ // These values are obtained directly from OS AccessRule fields below.
116
+ // An inheritance-only CREATOR OWNER placeholder is not an effective
117
+ // mutation grant on THIS object. An effective CREATOR OWNER ACE (or an
118
+ // untrusted effective GENERIC_ALL) is still rejected, not whitelisted.
119
+ if(!Number.isInteger(ace.inheritance)||ace.inheritance<0||
120
+ (ace.inheritance&~KNOWN_INHERITANCE_FLAGS)!==0||
121
+ !Number.isInteger(ace.propagation)||ace.propagation<0||
122
+ (ace.propagation&~KNOWN_PROPAGATION_FLAGS)!==0||
123
+ (ace.propagation!==0&&ace.inheritance===0)||
124
+ ((ace.propagation&INHERIT_ONLY)!==0&&(!rec.directory||ace.inheritance===0)))
125
+ return Object.freeze({trusted:false,reason:'UNKNOWN_ACL_SCOPE',path:file});
126
+ if((ace.propagation&INHERIT_ONLY)!==0)continue;
127
+ if(ace.type==='Allow'&&!TRUSTED.has(ace.sid)&&
128
+ (ace.mask&((rec.directory?DIR_MUTATION:FILE_MUTATION)|ANY_MUTATION_OR_UNDECIDABLE))!==0)
129
+ return Object.freeze({trusted:false,reason:'UNTRUSTED_WRITE_ACE',path:file,sid:ace.sid});
130
+ }
131
+ }
132
+ return Object.freeze({trusted:true,reason:'PROTECTED_INSTALLATION_ACL_SNAPSHOT'});
133
+ }
134
+
135
+ // This command is compiled into the module; it does NOT run a repo-local .ps1.
136
+ // Errors, unknown ACEs/SIDs and inaccessible paths are fatal.
137
+ const aclProbe=String.raw`
138
+ $ErrorActionPreference='Stop'; Set-StrictMode -Version Latest
139
+ try {
140
+ $utf8=[Text.Encoding]::UTF8
141
+ $files=ConvertFrom-Json -InputObject ($utf8.GetString([Convert]::FromBase64String('__PATHS_BASE64__')))
142
+ $seen=[System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
143
+ $items=New-Object System.Collections.ArrayList
144
+ foreach($p in @($files)) {
145
+ $node=[IO.Path]::GetFullPath([string]$p)
146
+ while($true) {
147
+ if($seen.Add($node)) {
148
+ $item=Get-Item -LiteralPath $node -Force -ErrorAction Stop
149
+ $acl=Get-Acl -LiteralPath $node -ErrorAction Stop
150
+ $owner=$acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
151
+ $aces=New-Object System.Collections.ArrayList
152
+ foreach($entry in $acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])) {
153
+ [void]$aces.Add(@{sid=$entry.IdentityReference.Value;type=$entry.AccessControlType.ToString();mask=([int64]$entry.FileSystemRights -band [int64]4294967295);inheritance=[int]$entry.InheritanceFlags;propagation=[int]$entry.PropagationFlags})
154
+ }
155
+ [void]$items.Add(@{path=$node;owner=$owner;reparse=[bool]($item.Attributes -band [IO.FileAttributes]::ReparsePoint);directory=[bool]$item.PSIsContainer;aces=@($aces.ToArray())})
156
+ }
157
+ $parent=[IO.Directory]::GetParent($node)
158
+ if($null -eq $parent){break}
159
+ $node=$parent.FullName
160
+ }
161
+ }
162
+ ConvertTo-Json -InputObject @($items.ToArray()) -Depth 9 -Compress
163
+ } catch { [Console]::Error.WriteLine('R89_ACL_PROBE_FAILED: '+$_.Exception.GetType().Name); exit 12 }
164
+ `;
165
+
166
+ export async function inspectTrustedInstallation(files){
167
+ if(process.platform!=='win32'||process.arch!=='x64')
168
+ throw new Error('R89 privileged native loading requires Windows x64');
169
+ const required=requiredAclPathChain(files);
170
+ // Absolute OS executable: do not resolve a user-controlled PATH, run a
171
+ // repo-owned helper, or honor a caller-provided tool override.
172
+ const ps='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
173
+ const json=Buffer.from(JSON.stringify(files),'utf8').toString('base64');
174
+ const script=aclProbe.replace('__PATHS_BASE64__',json);
175
+ const encoded=Buffer.from(script,'utf16le').toString('base64');
176
+ let stdout;
177
+ try{
178
+ ({stdout}=await execFileAsync(ps,['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',encoded],
179
+ {windowsHide:true,timeout:12000,maxBuffer:512*1024,encoding:'utf8',env:windowsPowerShellEnvironment()}));
180
+ }catch(error){
181
+ throw Object.assign(new Error('R89 privileged installation ACL inspection failed closed',{cause:error}),
182
+ {code:'EPOCH_NATIVE_SLOT_UNTRUSTED'});
183
+ }
184
+ let records;
185
+ try{records=JSON.parse(stdout);}catch{
186
+ throw Object.assign(new Error('R89 privileged installation ACL response invalid'),
187
+ {code:'EPOCH_NATIVE_SLOT_UNTRUSTED'});
188
+ }
189
+ const decision=evaluatePrivilegedAclSnapshot(records,files);
190
+ if(!decision.trusted){
191
+ throw Object.assign(new Error(`R89 privileged installation not protected: ${decision.reason}`),
192
+ {code:'EPOCH_NATIVE_SLOT_UNTRUSTED',reason:decision.reason});
193
+ }
194
+ if(required.length!==records.length)throw new Error('R89 ACL result cardinality changed');
195
+ return decision;
196
+ }
@@ -254,13 +254,20 @@ function traceText(meta,displayMode='unicode'){
254
254
  }
255
255
  function outcomeClass(event){
256
256
  const outcome=event?.outcome;
257
- if(outcome?.side_effect_state==='unknown') return 'unknown';
258
- const status=outcome?.status;
259
- if(status==='timeout'||status==='timed_out') return 'unknown';
260
- if(outcome?.is_error===true||
261
- ['failed','denied','rejected','blocked','observed_failure','cancelled','canceled','unavailable','unsupported'].includes(status)) return 'error';
262
- if(['pending','running','input_required','paused','processing','accepted'].includes(status)) return 'pending';
263
- return 'ok';
257
+ // A terminal record is not evidence of success without a classified
258
+ // outcome. Never upgrade missing or new statuses to OK.
259
+ if(!outcome||typeof outcome!=='object'||Array.isArray(outcome))return 'unknown';
260
+ if(outcome.side_effect_state==='unknown')return 'unknown';
261
+ const status=outcome.status;
262
+ if(status==='timeout'||status==='timed_out'||status==='interrupted')return 'unknown';
263
+ if(outcome.is_error===true||
264
+ ['failed','threw','error','denied','rejected','blocked','observed_failure','cancelled','canceled','unavailable','unsupported'].includes(status))return 'error';
265
+ if(['pending','running','input_required','paused','processing','accepted'].includes(status))return 'pending';
266
+ if(['ok','success','succeeded','completed','committed','verified','passed','done'].includes(status))return 'ok';
267
+ // Legacy audit records with explicit is_error:false but no status remain
268
+ // supported; an unrecognized status is not evidence of success.
269
+ if((status===undefined||status===null)&&outcome.is_error===false)return 'ok';
270
+ return 'unknown';
264
271
  }
265
272
  export function classifyRemoteAuditOutcome(event){return outcomeClass(event);}
266
273
  function parseFilterExpression(expression){
@@ -391,7 +398,7 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
391
398
  const category=categoryForTool(displayTool);
392
399
  const receipt=Array.isArray(event.outcome?.receipt_sha256s)&&typeof event.outcome.receipt_sha256s[0]==='string'
393
400
  ?shortValue(event.outcome.receipt_sha256s[0],8):'';
394
- const marker=unknown?'FAILED_AFTER_UNKNOWN_EFFECT':
401
+ const marker=unknown?(event.outcome?.side_effect_state==='unknown'?'FAILED_AFTER_UNKNOWN_EFFECT':'OUTCOME_UNKNOWN'):
395
402
  error?(displayMode==='ascii'?'ERR':'✗'):
396
403
  pendingResult?'PENDING':
397
404
  ['write','edit','run'].includes(category)?(displayMode==='ascii'?'SUCCEEDED':'✅ SUCCEEDED'):
@@ -1,4 +1,6 @@
1
1
  import path from 'node:path';
2
+ import {createHash} from 'node:crypto';
3
+ import {open} from 'node:fs/promises';
2
4
 
3
5
  export function safeRemoteLogPrefixLength(bytes,encoding='utf8'){
4
6
  if(!Buffer.isBuffer(bytes))throw new Error('remote log prefix bytes invalid');
@@ -30,3 +32,66 @@ export function boundedRemoteDiagnostic(value,maxChars=2048){
30
32
  const line=String(value??'').replace(/\x1b\[[0-9;]*[A-Za-z]/g,'').replace(/[\x00-\x1f\x7f]+/g,' ').replace(/\s{2,}/g,' ').trim();
31
33
  return line.length<=maxChars?line:`${line.slice(0,maxChars)}...`;
32
34
  }
35
+
36
+ // Cursor pinned to the file handle's observed identity. We never use a path
37
+ // stat's size to allocate or seek on a separately opened file.
38
+ export async function readRemoteLogAppend(file,cursor=null,{startAtEnd=false,maxBytes=1048576}={}){
39
+ if(typeof file!=='string'||!file||!Number.isInteger(maxBytes)||maxBytes<4||maxBytes>1048576)
40
+ throw new Error('remote live log append arguments invalid');
41
+ if(cursor!==null&&(!Number.isSafeInteger(cursor.offset)||cursor.offset<0))
42
+ throw new Error('remote live log cursor invalid');
43
+ let handle;
44
+ try{handle=await open(file,'r');}
45
+ catch(error){
46
+ if(error?.code==='ENOENT')return {cursor:null,offset:0,text:'',reset:cursor!==null,resetReason:'file disappeared'};
47
+ throw error;
48
+ }
49
+ try{
50
+ const st=await handle.stat({bigint:true});
51
+ if(!st.isFile()||st.size<0n||st.size>BigInt(Number.MAX_SAFE_INTEGER))
52
+ throw new Error('remote live log regular file/size invalid');
53
+ const size=Number(st.size);
54
+ const identity=st.ino!==0n?st.dev.toString()+':'+st.ino.toString():null;
55
+ const mtimeNs=(st.mtimeNs??BigInt(Math.trunc(Number(st.mtimeMs)*1000000))).toString();
56
+ const gone=cursor!==null&&cursor.identity&&identity&&cursor.identity!==identity;
57
+ const shrunk=cursor!==null&&size<cursor.offset;
58
+ // NTFS may report the same timestamp for rapid same-size rewrites.
59
+ // Compare a bounded suffix fingerprint on the idle cursor as well.
60
+ // This is best-effort rewrite detection, NOT complete file integrity.
61
+ async function tailFingerprint(offset){
62
+ if(offset===0)return null;
63
+ const length=Math.min(offset,64),bytes=Buffer.allocUnsafe(length);
64
+ const {bytesRead}=await handle.read(bytes,0,length,offset-length);
65
+ if(bytesRead!==length)throw new Error('remote live log changed during fingerprint');
66
+ return createHash('sha256').update(bytes).digest('hex');
67
+ }
68
+ const priorTailChanged=cursor!==null&&!gone&&!shrunk&&size===cursor.offset&&
69
+ typeof cursor.tail_sha256==='string'&&
70
+ (await tailFingerprint(cursor.offset))!==cursor.tail_sha256;
71
+ const rewritten=cursor!==null&&!gone&&!shrunk&&size===cursor.offset&&
72
+ ((typeof cursor.mtimeNs==='string'&&cursor.mtimeNs!==mtimeNs)||priorTailChanged);
73
+ const reset=Boolean(gone||shrunk||rewritten);
74
+ let start=reset?0:(cursor?.offset??0);
75
+ const makeCursor=async offset=>Object.freeze({offset,identity,mtimeNs,tail_sha256:await tailFingerprint(offset)});
76
+ if(startAtEnd&&cursor===null)
77
+ return {cursor:await makeCursor(size),offset:size,text:'',reset:false,resetReason:null};
78
+ let encoding=null;
79
+ if(/\.log$/i.test(file)){
80
+ const bom=Buffer.allocUnsafe(2);
81
+ const header=await handle.read(bom,0,2,0);
82
+ if(header.bytesRead===2&&bom[0]===0xff&&bom[1]===0xfe){
83
+ encoding='utf16le';
84
+ if(start%2!==0)start--;
85
+ }
86
+ }
87
+ if(start>=size)
88
+ return {cursor:await makeCursor(start),offset:start,text:'',reset,resetReason:gone?'file identity changed':shrunk?'file shrank':rewritten?'same-size rewrite':null};
89
+ const buffer=Buffer.allocUnsafe(Math.min(maxBytes,size-start));
90
+ const {bytesRead}=await handle.read(buffer,0,buffer.length,start);
91
+ const complete=safeRemoteLogPrefixLength(buffer.subarray(0,bytesRead),encoding??'utf8');
92
+ const next=start+complete;
93
+ return {cursor:await makeCursor(next),offset:next,
94
+ text:decodeRemoteLogBytes(file,buffer.subarray(0,complete),{encoding}),
95
+ reset,resetReason:gone?'file identity changed':shrunk?'file shrank':rewritten?'same-size rewrite':null};
96
+ }finally{await handle.close();}
97
+ }