agentic-workflow-manager 9.5.0 → 9.6.0
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/dist/src/commands/preflight/checks.js +2 -2
- package/dist/src/commands/sensors/bootstrap.js +171 -0
- package/dist/src/commands/sensors/compatibility/contract.js +18 -15
- package/dist/src/commands/sensors/compatibility/manifest.js +29 -1
- package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
- package/dist/src/commands/sensors/compatibility/safe-file.js +240 -5
- package/dist/src/commands/sensors/coverage/index.js +2 -2
- package/dist/src/commands/sensors/coverage/resolve.js +20 -10
- package/dist/src/commands/sensors/detection.js +31 -0
- package/dist/src/commands/sensors/exec.js +116 -11
- package/dist/src/commands/sensors/index.js +51 -10
- package/dist/src/commands/sensors/init.js +17 -170
- package/dist/src/commands/sensors/migrate.js +209 -0
- package/dist/src/commands/sensors/run.js +2 -2
- package/dist/src/core/init/steps.js +24 -11
- package/dist/src/core/secure-fs/native-bridge.js +209 -0
- package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +67 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +34 -1
- package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +10 -0
- package/dist/tests/commands/sensors/exec.test.js +86 -0
- package/dist/tests/commands/sensors/index.test.js +54 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
- package/dist/tests/commands/sensors/init.test.js +56 -490
- package/dist/tests/commands/sensors/migrate.test.js +513 -0
- package/dist/tests/core/init/orchestrator.test.js +3 -1
- package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +10 -2
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
- package/dist/tests/structural/native-release-artifacts.test.js +22 -0
- package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
- package/dist/tests/structural/release-registry-provenance.test.js +4 -0
- package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
- package/package.json +6 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/linux-arm64/secure_fs.node +0 -0
- package/prebuilds/linux-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const worker_threads_1 = require("worker_threads");
|
|
11
|
+
const native_bridge_1 = require("../../../src/core/secure-fs/native-bridge");
|
|
12
|
+
const artifact = (root, platform, arch) => path_1.default.join(root, 'prebuilds', `${platform}-${arch}`, 'secure_fs.node');
|
|
13
|
+
const releaseTargets = [
|
|
14
|
+
['linux', 'x64'], ['linux', 'arm64'],
|
|
15
|
+
['darwin', 'x64'], ['darwin', 'arm64'],
|
|
16
|
+
['win32', 'x64'], ['win32', 'arm64'],
|
|
17
|
+
];
|
|
18
|
+
function identityFixture(seed = 1) {
|
|
19
|
+
const identity = Buffer.alloc(24);
|
|
20
|
+
identity.write('SFSI', 0, 'ascii');
|
|
21
|
+
identity[4] = 1;
|
|
22
|
+
identity[5] = process.platform === 'win32' ? 2 : 1;
|
|
23
|
+
identity.writeBigUInt64LE(BigInt(seed), 8);
|
|
24
|
+
identity.writeBigUInt64LE(BigInt(seed + 1), 16);
|
|
25
|
+
return identity;
|
|
26
|
+
}
|
|
27
|
+
describe('native secure-fs Windows source contract', () => {
|
|
28
|
+
const windowsContractSource = (source) => {
|
|
29
|
+
const normalized = source.replace(/\r\n/g, '\n');
|
|
30
|
+
return normalized.slice(normalized.indexOf('#else\nstruct WindowsParent'), normalized.indexOf('#endif\n\nbool WriteOptionsArg'));
|
|
31
|
+
};
|
|
32
|
+
const windowsSource = () => windowsContractSource(fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../native/secure_fs.cc'), 'utf8'));
|
|
33
|
+
it('finds the Windows contract in a CRLF checkout', () => {
|
|
34
|
+
const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../native/secure_fs.cc'), 'utf8')
|
|
35
|
+
.replace(/\r\n/g, '\n')
|
|
36
|
+
.replace(/\n/g, '\r\n');
|
|
37
|
+
expect(windowsContractSource(source)).toContain('NtCreateFile');
|
|
38
|
+
});
|
|
39
|
+
it('uses a verified root handle for every descendant open, create, and cleanup', () => {
|
|
40
|
+
const source = windowsSource();
|
|
41
|
+
expect(source).toMatch(/GetProcAddress\([^\n]+"NtCreateFile"/);
|
|
42
|
+
expect(source).toMatch(/RootDirectory\s*=\s*root/);
|
|
43
|
+
expect(source).not.toMatch(/\bCreateDirectoryW\b|\bDeleteFileW\b|parent\.directory/);
|
|
44
|
+
expect(source.match(/\bCreateFileW\s*\(/g)).toHaveLength(1);
|
|
45
|
+
});
|
|
46
|
+
it('requests SYNCHRONIZE when opening a handle-relative directory', () => {
|
|
47
|
+
expect(windowsSource()).toMatch(/OpenRelativeDirectoryNoReparse\([\s\S]*?FILE_TRAVERSE\s*\|\s*FILE_READ_ATTRIBUTES\s*\|\s*SYNCHRONIZE/);
|
|
48
|
+
});
|
|
49
|
+
it('does not combine directory opens with the incompatible reparse-point option', () => {
|
|
50
|
+
const source = windowsSource();
|
|
51
|
+
const start = source.indexOf('bool OpenRelativeDirectoryNoReparse');
|
|
52
|
+
const end = source.indexOf('\nbool OpenWindowsParent', start);
|
|
53
|
+
const opener = source.slice(start, end);
|
|
54
|
+
expect(start).toBeGreaterThan(-1);
|
|
55
|
+
expect(opener).toContain('kFileDirectoryFile | kFileSynchronousIoNonalert | kFileOpenForBackupIntent');
|
|
56
|
+
expect(opener).not.toMatch(/kFileDirectoryFile\s*\|[^;]*kFileOpenReparsePoint/);
|
|
57
|
+
expect(opener).toContain('VerifyDirectDirectoryIdentity');
|
|
58
|
+
});
|
|
59
|
+
it('preserves the terminal Win32 staging error for transaction diagnostics', () => {
|
|
60
|
+
const source = windowsSource();
|
|
61
|
+
const start = source.indexOf('bool CreatePrivateStagingFile');
|
|
62
|
+
const end = source.indexOf('\nbool DiscardStagingFile', start);
|
|
63
|
+
const staging = source.slice(start, end);
|
|
64
|
+
expect(start).toBeGreaterThan(-1);
|
|
65
|
+
expect(staging).toContain('DWORD* last_error');
|
|
66
|
+
expect(staging).toContain('const DWORD error = GetLastError()');
|
|
67
|
+
expect(staging).toContain('*last_error = error');
|
|
68
|
+
expect(fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../native/secure_fs.cc'), 'utf8'))
|
|
69
|
+
.toMatch(/DWORD staging_error = ERROR_SUCCESS;[\s\S]*?CreatePrivateStagingFile\(parent, &staged, &staging_error\)/);
|
|
70
|
+
});
|
|
71
|
+
it('uses the portable default attributes when creating an exclusive staging file', () => {
|
|
72
|
+
const source = windowsSource();
|
|
73
|
+
const start = source.indexOf('bool CreatePrivateStagingFile');
|
|
74
|
+
const end = source.indexOf('\nbool DiscardStagingFile', start);
|
|
75
|
+
const staging = source.slice(start, end);
|
|
76
|
+
expect(start).toBeGreaterThan(-1);
|
|
77
|
+
expect(staging).toContain('FILE_ATTRIBUTE_NORMAL, &handle');
|
|
78
|
+
expect(staging).not.toContain('FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_TEMPORARY');
|
|
79
|
+
});
|
|
80
|
+
it('requests synchronous access for the synchronous staging handle', () => {
|
|
81
|
+
const source = windowsSource();
|
|
82
|
+
const start = source.indexOf('bool CreatePrivateStagingFile');
|
|
83
|
+
const end = source.indexOf('\nbool DiscardStagingFile', start);
|
|
84
|
+
const staging = source.slice(start, end);
|
|
85
|
+
expect(staging).toMatch(/GENERIC_WRITE \| DELETE \| FILE_READ_ATTRIBUTES \| SYNCHRONIZE/);
|
|
86
|
+
expect(staging).toContain('kFileSynchronousIoNonalert');
|
|
87
|
+
});
|
|
88
|
+
it('supplies a NUL-terminated buffer for each Win32 rename target', () => {
|
|
89
|
+
const source = windowsSource();
|
|
90
|
+
for (const functionName of ['PublishNoReplace', 'PublishReplace']) {
|
|
91
|
+
const start = source.indexOf(`PublishResult ${functionName}`);
|
|
92
|
+
const end = source.indexOf('\n}\n', start) + 2;
|
|
93
|
+
const rename = source.slice(start, end);
|
|
94
|
+
expect(start).toBeGreaterThan(-1);
|
|
95
|
+
expect(rename).toContain('basename.size() * sizeof(WCHAR) + sizeof(WCHAR)');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
it('uses the native rename-information class required for POSIX publication', () => {
|
|
99
|
+
const source = windowsSource();
|
|
100
|
+
expect(source).toContain('NtSetInformationFileFunction nt_set_information_file');
|
|
101
|
+
expect(source).toContain('constexpr ULONG kFileRenameInformationEx = 65');
|
|
102
|
+
for (const functionName of ['PublishNoReplace', 'PublishReplace']) {
|
|
103
|
+
const start = source.indexOf(`PublishResult ${functionName}`);
|
|
104
|
+
const end = source.indexOf('\n}\n', start) + 2;
|
|
105
|
+
const rename = source.slice(start, end);
|
|
106
|
+
expect(rename).toContain('api.nt_set_information_file');
|
|
107
|
+
expect(rename).toContain('kFileRenameInformationEx');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
it('preserves the terminal Win32 lease error for portable diagnostics', () => {
|
|
111
|
+
const source = windowsSource();
|
|
112
|
+
const start = source.indexOf('LeaseAcquireResult AcquirePlatformProjectLease');
|
|
113
|
+
const end = source.indexOf('\nvoid ReleasePlatformProjectLease', start);
|
|
114
|
+
const lease = source.slice(start, end);
|
|
115
|
+
expect(lease).toContain('DWORD* last_error');
|
|
116
|
+
expect(lease).toContain('*last_error = GetLastError()');
|
|
117
|
+
expect(fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../native/secure_fs.cc'), 'utf8'))
|
|
118
|
+
.toMatch(/DWORD lease_error = ERROR_SUCCESS;[\s\S]*?AcquirePlatformProjectLease\(project_root, lease, &lease_error\)/);
|
|
119
|
+
});
|
|
120
|
+
it('requests synchronous access for the synchronous Windows lease handle', () => {
|
|
121
|
+
const source = windowsSource();
|
|
122
|
+
const start = source.indexOf('LeaseAcquireResult AcquirePlatformProjectLease');
|
|
123
|
+
const end = source.indexOf('\nvoid ReleasePlatformProjectLease', start);
|
|
124
|
+
const lease = source.slice(start, end);
|
|
125
|
+
expect(lease).toMatch(/GENERIC_READ \| GENERIC_WRITE \| SYNCHRONIZE/);
|
|
126
|
+
expect(lease).toContain('kFileSynchronousIoNonalert');
|
|
127
|
+
});
|
|
128
|
+
it('declares eval mode for the inline junction-race worker', () => {
|
|
129
|
+
expect(fs_1.default.readFileSync(__filename, 'utf8'))
|
|
130
|
+
.toMatch(/new Worker\([\s\S]*?`\s*, \{ eval: true, workerData:/);
|
|
131
|
+
});
|
|
132
|
+
it('keeps a verified target handle that shares only deletion for atomic POSIX replacement', () => {
|
|
133
|
+
const source = windowsSource();
|
|
134
|
+
const fenceStart = source.indexOf('HANDLE OpenRegularFileForReplacementFence');
|
|
135
|
+
const fenceEnd = source.indexOf('\n}', fenceStart);
|
|
136
|
+
const replacementFence = source.slice(fenceStart, fenceEnd);
|
|
137
|
+
expect(fenceStart).toBeGreaterThan(-1);
|
|
138
|
+
expect(replacementFence).toContain('OpenRegularFileNoReparse(parent, FILE_READ_DATA, FILE_SHARE_DELETE)');
|
|
139
|
+
expect(replacementFence).not.toMatch(/FILE_SHARE_WRITE/);
|
|
140
|
+
expect(source).toMatch(/kFileRenameReplaceIfExists\s*=\s*0x00000001/);
|
|
141
|
+
expect(source).toMatch(/kFileRenamePosixSemantics\s*=\s*0x00000002/);
|
|
142
|
+
expect(source).toMatch(/rename->Flags\s*=\s*kFileRenameReplaceIfExists\s*\|\s*kFileRenamePosixSemantics/);
|
|
143
|
+
expect(replacementFence).not.toMatch(/\bLockFileEx\b|\bUnlockFileEx\b/);
|
|
144
|
+
expect(source).toMatch(/LockFileEx\([^;]+LOCKFILE_EXCLUSIVE_LOCK\s*\|\s*LOCKFILE_FAIL_IMMEDIATELY/);
|
|
145
|
+
expect(source).toMatch(/\bUnlockFileEx\b/);
|
|
146
|
+
});
|
|
147
|
+
it('preserves the native replacement status separately from an identity-fence mismatch', () => {
|
|
148
|
+
const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../native/secure_fs.cc'), 'utf8');
|
|
149
|
+
expect(source).toContain('DWORD replacement_error = ERROR_SUCCESS;');
|
|
150
|
+
expect(source).toContain('PublishReplace(staged, parent.handle, parent.basename, &replacement_error)');
|
|
151
|
+
expect(source).toContain('secure-fs Windows replacement failed (win32=');
|
|
152
|
+
});
|
|
153
|
+
it('opens a verified observed target with delete access before removing it', () => {
|
|
154
|
+
const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../native/secure_fs.cc'), 'utf8');
|
|
155
|
+
const removalStart = source.indexOf('napi_value RemoveObservedProjectFile');
|
|
156
|
+
const removal = source.slice(removalStart, source.indexOf('napi_value WriteProjectTransaction', removalStart));
|
|
157
|
+
expect(removalStart).toBeGreaterThan(-1);
|
|
158
|
+
expect(removal).toContain('OpenRegularFileNoReparse(parent, DELETE, 0)');
|
|
159
|
+
expect(removal).toContain('SetFileInformationByHandle(target, FileDispositionInfo');
|
|
160
|
+
expect(removal).not.toMatch(/\bDeleteFileW\b/);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('native secure-fs POSIX source contract', () => {
|
|
164
|
+
const source = () => fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../native/secure_fs.cc'), 'utf8');
|
|
165
|
+
it('uses an unpredictable nonce and a bounded retry loop for descriptor-relative staging', () => {
|
|
166
|
+
const nativeSource = source();
|
|
167
|
+
expect(nativeSource).toMatch(/constexpr unsigned int kMaxStagingAttempts = 128/);
|
|
168
|
+
expect(nativeSource).toMatch(/bool FillRandomBytes\(/);
|
|
169
|
+
expect(nativeSource).toMatch(/\bgetrandom\b/);
|
|
170
|
+
expect(nativeSource).toMatch(/\barc4random_buf\b/);
|
|
171
|
+
expect(nativeSource).toMatch(/attempt < kMaxStagingAttempts/);
|
|
172
|
+
expect(nativeSource).toMatch(/openat\(parent, temporary->c_str\(\),[^;]*O_EXCL[^;]*O_NOFOLLOW/);
|
|
173
|
+
expect(nativeSource).toMatch(/if \(errno != EEXIST\) return -1/);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe('native secure-fs bridge loader', () => {
|
|
177
|
+
let root;
|
|
178
|
+
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-secure-fs-')); });
|
|
179
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
180
|
+
it('locates prebuilds at package root from source and compiled layouts', () => {
|
|
181
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
182
|
+
expect((0, native_bridge_1.resolvePackageRoot)(path_1.default.join(root, 'src', 'core', 'secure-fs'))).toBe(root);
|
|
183
|
+
expect((0, native_bridge_1.resolvePackageRoot)(path_1.default.join(root, 'dist', 'src', 'core', 'secure-fs'))).toBe(root);
|
|
184
|
+
});
|
|
185
|
+
it('selects the artifact for the exact supported platform and architecture', () => {
|
|
186
|
+
const selected = artifact(root, 'linux', 'x64');
|
|
187
|
+
fs_1.default.mkdirSync(path_1.default.dirname(selected), { recursive: true });
|
|
188
|
+
fs_1.default.writeFileSync(selected, 'test artifact');
|
|
189
|
+
const binding = {
|
|
190
|
+
acquireProjectLease: jest.fn(() => ({})), releaseProjectLease: jest.fn(),
|
|
191
|
+
readRegularFile: jest.fn(), writeProjectTransaction: jest.fn(), removeObservedProjectFile: jest.fn(),
|
|
192
|
+
};
|
|
193
|
+
expect((0, native_bridge_1.loadNativeSecureFsBridge)({ root, platform: 'linux', arch: 'x64', load: candidate => {
|
|
194
|
+
expect(candidate).toBe(selected);
|
|
195
|
+
return binding;
|
|
196
|
+
} })).toBe(binding);
|
|
197
|
+
expect((0, native_bridge_1.nativeArtifactStatus)({ root, platform: 'linux', arch: 'x64' })).toEqual({
|
|
198
|
+
status: 'available', platform: 'linux', arch: 'x64', path: selected,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
it('fails before mutation when its selected artifact is absent', () => {
|
|
202
|
+
const mutate = jest.fn();
|
|
203
|
+
expect(() => (0, native_bridge_1.loadNativeSecureFsBridge)({ root, platform: 'win32', arch: 'arm64', load: mutate }))
|
|
204
|
+
.toThrow('secure-fs native artifact is unavailable for win32-arm64');
|
|
205
|
+
expect(mutate).not.toHaveBeenCalled();
|
|
206
|
+
expect((0, native_bridge_1.nativeArtifactStatus)({ root, platform: 'win32', arch: 'arm64' })).toEqual({
|
|
207
|
+
status: 'missing', platform: 'win32', arch: 'arm64', path: artifact(root, 'win32', 'arm64'),
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
it('rejects a legacy artifact that lacks the project lease primitives', () => {
|
|
211
|
+
const selected = artifact(root, 'linux', 'x64');
|
|
212
|
+
fs_1.default.mkdirSync(path_1.default.dirname(selected), { recursive: true });
|
|
213
|
+
fs_1.default.writeFileSync(selected, 'test artifact');
|
|
214
|
+
expect(() => (0, native_bridge_1.loadNativeSecureFsBridge)({
|
|
215
|
+
root,
|
|
216
|
+
platform: 'linux',
|
|
217
|
+
arch: 'x64',
|
|
218
|
+
load: () => ({ readRegularFile: jest.fn(), writeProjectTransaction: jest.fn() }),
|
|
219
|
+
})).toThrow('secure-fs native artifact is incompatible for linux-x64');
|
|
220
|
+
});
|
|
221
|
+
it('requires every declared release artifact before package verification succeeds', () => {
|
|
222
|
+
const prebuilds = path_1.default.join(root, 'prebuilds');
|
|
223
|
+
const script = path_1.default.resolve(__dirname, '../../../scripts/assert-native-package.js');
|
|
224
|
+
for (const [platform, arch] of releaseTargets.slice(0, -1)) {
|
|
225
|
+
const selected = artifact(root, platform, arch);
|
|
226
|
+
fs_1.default.mkdirSync(path_1.default.dirname(selected), { recursive: true });
|
|
227
|
+
fs_1.default.writeFileSync(selected, 'test artifact');
|
|
228
|
+
}
|
|
229
|
+
expect(() => (0, child_process_1.execFileSync)(process.execPath, [script], {
|
|
230
|
+
cwd: path_1.default.resolve(__dirname, '../../..'),
|
|
231
|
+
env: { ...process.env, AWM_NATIVE_PREBUILDS: prebuilds },
|
|
232
|
+
stdio: 'pipe',
|
|
233
|
+
})).toThrow(/win32-arm64/);
|
|
234
|
+
});
|
|
235
|
+
it('rejects an unsupported platform before loading any artifact', () => {
|
|
236
|
+
const load = jest.fn();
|
|
237
|
+
expect(() => (0, native_bridge_1.loadNativeSecureFsBridge)({ root, platform: 'freebsd', arch: 'x64', load }))
|
|
238
|
+
.toThrow('secure-fs native bridge does not support freebsd-x64');
|
|
239
|
+
expect(load).not.toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('secure-fs TypeScript boundary', () => {
|
|
243
|
+
const projectRoot = path_1.default.resolve(path_1.default.parse(process.cwd()).root, 'project');
|
|
244
|
+
const rawIdentity = identityFixture();
|
|
245
|
+
const binding = {
|
|
246
|
+
acquireProjectLease: jest.fn(() => ({})),
|
|
247
|
+
releaseProjectLease: jest.fn(),
|
|
248
|
+
readRegularFile: jest.fn(() => ({ bytes: Buffer.from('verified bytes'), identity: rawIdentity })),
|
|
249
|
+
writeProjectTransaction: jest.fn(),
|
|
250
|
+
removeObservedProjectFile: jest.fn(),
|
|
251
|
+
};
|
|
252
|
+
beforeEach(() => { jest.clearAllMocks(); });
|
|
253
|
+
it('holds one opaque native lease around the callback and releases it after success or exception', () => {
|
|
254
|
+
const rawLease = Object.freeze({ native: true });
|
|
255
|
+
const events = [];
|
|
256
|
+
const leaseBinding = {
|
|
257
|
+
...binding,
|
|
258
|
+
acquireProjectLease: jest.fn(() => { events.push('acquire'); return rawLease; }),
|
|
259
|
+
releaseProjectLease: jest.fn((token) => {
|
|
260
|
+
expect(token).toBe(rawLease);
|
|
261
|
+
events.push('release');
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => leaseBinding);
|
|
265
|
+
const operation = jest.fn(() => { events.push('callback'); return 42; });
|
|
266
|
+
expect(bridge.withProjectLease(projectRoot, operation)).toBe(42);
|
|
267
|
+
expect(operation).toHaveBeenCalledWith();
|
|
268
|
+
expect(events).toEqual(['acquire', 'callback', 'release']);
|
|
269
|
+
expect('releaseProjectLease' in bridge).toBe(false);
|
|
270
|
+
const callbackFailure = new Error('callback failed');
|
|
271
|
+
expect(() => bridge.withProjectLease(projectRoot, () => {
|
|
272
|
+
events.push('throwing callback');
|
|
273
|
+
throw callbackFailure;
|
|
274
|
+
})).toThrow(callbackFailure);
|
|
275
|
+
expect(events.slice(-3)).toEqual(['acquire', 'throwing callback', 'release']);
|
|
276
|
+
expect(leaseBinding.releaseProjectLease).toHaveBeenCalledTimes(2);
|
|
277
|
+
});
|
|
278
|
+
it('passes only an absolute project root, a safe relative destination, and opaque bytes to the native transaction', () => {
|
|
279
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => binding);
|
|
280
|
+
const payload = Buffer.from('{"schemaVersion":3}\n');
|
|
281
|
+
bridge.writeProjectTransaction(projectRoot, '.awm/sensors.json', payload, { mode: 'create', createParents: true });
|
|
282
|
+
expect(binding.writeProjectTransaction).toHaveBeenCalledWith(projectRoot, path_1.default.join('.awm', 'sensors.json'), payload, { mode: 'create', createParents: true });
|
|
283
|
+
expect(() => bridge.writeProjectTransaction('relative', '.awm/sensors.json', payload, { mode: 'create', createParents: true })).toThrow('absolute');
|
|
284
|
+
expect(() => bridge.writeProjectTransaction(projectRoot, '../sensors.json', payload, { mode: 'create', createParents: true })).toThrow('project-relative');
|
|
285
|
+
expect(() => bridge.writeProjectTransaction(projectRoot, '.awm/sensors.json', 'not bytes', { mode: 'create', createParents: true })).toThrow('Buffer');
|
|
286
|
+
});
|
|
287
|
+
it('permits compensation only for an opaque identity from the exact observed destination', () => {
|
|
288
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => binding);
|
|
289
|
+
const observed = bridge.readRegularFile(path_1.default.join(projectRoot, '.awm', 'created.json'), 1024);
|
|
290
|
+
bridge.removeObservedProjectFile(projectRoot, '.awm/created.json', observed.identity);
|
|
291
|
+
expect(binding.removeObservedProjectFile).toHaveBeenCalledWith(projectRoot, path_1.default.join('.awm', 'created.json'), rawIdentity);
|
|
292
|
+
expect(() => bridge.removeObservedProjectFile(projectRoot, '.awm/other.json', observed.identity))
|
|
293
|
+
.toThrow(/exact observed destination/i);
|
|
294
|
+
expect(() => bridge.removeObservedProjectFile(projectRoot, '.awm/created.json', rawIdentity))
|
|
295
|
+
.toThrow(/opaque identity/i);
|
|
296
|
+
});
|
|
297
|
+
it('rejects NUL bytes in absolute paths before invoking the native binding', () => {
|
|
298
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => binding);
|
|
299
|
+
const operation = jest.fn();
|
|
300
|
+
expect(() => bridge.withProjectLease(`${projectRoot}\0ignored`, operation)).toThrow(/NUL byte/i);
|
|
301
|
+
expect(() => bridge.readRegularFile(`${projectRoot}\0ignored`, 1024)).toThrow(/NUL byte/i);
|
|
302
|
+
expect(() => bridge.writeProjectTransaction(`${projectRoot}\0ignored`, '.awm/sensors.json', Buffer.from('manifest'), {
|
|
303
|
+
mode: 'create', createParents: true,
|
|
304
|
+
})).toThrow(/NUL byte/i);
|
|
305
|
+
expect(operation).not.toHaveBeenCalled();
|
|
306
|
+
expect(binding.acquireProjectLease).not.toHaveBeenCalled();
|
|
307
|
+
expect(binding.readRegularFile).not.toHaveBeenCalled();
|
|
308
|
+
expect(binding.writeProjectTransaction).not.toHaveBeenCalled();
|
|
309
|
+
});
|
|
310
|
+
it('maps only the structured native no-replace conflict to a stable destination error', () => {
|
|
311
|
+
const nativeConflict = Object.assign(new Error('native implementation detail'), {
|
|
312
|
+
code: 'AWM_SECURE_FS_DESTINATION_EXISTS',
|
|
313
|
+
});
|
|
314
|
+
const conflictBinding = { ...binding, writeProjectTransaction: jest.fn(() => { throw nativeConflict; }) };
|
|
315
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => conflictBinding);
|
|
316
|
+
expect(() => bridge.writeProjectTransaction(projectRoot, '.awm/sensors.json', Buffer.from('manifest'), {
|
|
317
|
+
mode: 'create', createParents: true,
|
|
318
|
+
})).toThrow('project destination already exists');
|
|
319
|
+
const ioFailure = new Error('secure-fs target exists or transaction failed');
|
|
320
|
+
const failedBinding = { ...binding, writeProjectTransaction: jest.fn(() => { throw ioFailure; }) };
|
|
321
|
+
const failedBridge = (0, native_bridge_1.createSecureFsBoundary)(() => failedBinding);
|
|
322
|
+
expect(() => failedBridge.writeProjectTransaction(projectRoot, '.awm/sensors.json', Buffer.from('manifest'), {
|
|
323
|
+
mode: 'create', createParents: true,
|
|
324
|
+
})).toThrow(ioFailure);
|
|
325
|
+
});
|
|
326
|
+
it('accepts canonical forward-slash destinations independently of the host separator', () => {
|
|
327
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => binding);
|
|
328
|
+
const separator = Object.getOwnPropertyDescriptor(path_1.default, 'sep');
|
|
329
|
+
Object.defineProperty(path_1.default, 'sep', { ...separator, value: '\\' });
|
|
330
|
+
try {
|
|
331
|
+
bridge.writeProjectTransaction(projectRoot, '.awm/assets/eslint.config.awm.mjs', Buffer.from('asset'), {
|
|
332
|
+
mode: 'create', createParents: true,
|
|
333
|
+
});
|
|
334
|
+
expect(binding.writeProjectTransaction).toHaveBeenLastCalledWith(projectRoot, '.awm\\assets\\eslint.config.awm.mjs', Buffer.from('asset'), { mode: 'create', createParents: true });
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
Object.defineProperty(path_1.default, 'sep', separator);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
it('rejects backslash-ambiguous destinations independently of the host separator', () => {
|
|
341
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => binding);
|
|
342
|
+
const separator = Object.getOwnPropertyDescriptor(path_1.default, 'sep');
|
|
343
|
+
Object.defineProperty(path_1.default, 'sep', { ...separator, value: '\\' });
|
|
344
|
+
try {
|
|
345
|
+
expect(() => bridge.writeProjectTransaction(projectRoot, '.awm\\sensors.json', Buffer.from('manifest'), {
|
|
346
|
+
mode: 'create', createParents: true,
|
|
347
|
+
})).toThrow('project-relative');
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
Object.defineProperty(path_1.default, 'sep', separator);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
it('requires the exact native read bytes and opaque identity to fence a replacement transaction', () => {
|
|
354
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => binding);
|
|
355
|
+
const replacement = Buffer.from('{"schemaVersion":3}\n');
|
|
356
|
+
const observed = bridge.readRegularFile(path_1.default.join(projectRoot, '.awm', 'sensors.json'), 1024);
|
|
357
|
+
bridge.writeProjectTransaction(projectRoot, '.awm/sensors.json', replacement, {
|
|
358
|
+
mode: 'replace', expected: observed.bytes, expectedIdentity: observed.identity, createParents: false,
|
|
359
|
+
});
|
|
360
|
+
expect(binding.writeProjectTransaction).toHaveBeenLastCalledWith(projectRoot, path_1.default.join('.awm', 'sensors.json'), replacement, {
|
|
361
|
+
mode: 'replace', expected: Buffer.from('verified bytes'), expectedIdentity: rawIdentity, createParents: false,
|
|
362
|
+
});
|
|
363
|
+
expect(() => bridge.writeProjectTransaction(projectRoot, '.awm/sensors.json', replacement, { mode: 'replace', createParents: false })).toThrow('expected');
|
|
364
|
+
expect(() => bridge.writeProjectTransaction(projectRoot, '.awm/sensors.json', replacement, {
|
|
365
|
+
mode: 'replace', expected: observed.bytes, createParents: false,
|
|
366
|
+
})).toThrow('identity');
|
|
367
|
+
expect(() => bridge.writeProjectTransaction(projectRoot, '.awm/sensors.json', replacement, {
|
|
368
|
+
mode: 'replace', expected: observed.bytes, expectedIdentity: rawIdentity, createParents: false,
|
|
369
|
+
})).toThrow('identity');
|
|
370
|
+
});
|
|
371
|
+
it('uses the native binding for a bounded regular-file read and returns an opaque identity token', () => {
|
|
372
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => binding);
|
|
373
|
+
const manifest = path_1.default.join(projectRoot, '.awm', 'sensors.json');
|
|
374
|
+
const observed = bridge.readRegularFile(manifest, 1024);
|
|
375
|
+
expect(observed.bytes).toEqual(Buffer.from('verified bytes'));
|
|
376
|
+
expect(observed.identity).toEqual(expect.any(Object));
|
|
377
|
+
expect(Buffer.isBuffer(observed.identity)).toBe(false);
|
|
378
|
+
expect(Object.isFrozen(observed.identity)).toBe(true);
|
|
379
|
+
expect(binding.readRegularFile).toHaveBeenCalledWith(manifest, 1024);
|
|
380
|
+
expect(() => bridge.readRegularFile(manifest, 0)).toThrow('positive safe integer');
|
|
381
|
+
});
|
|
382
|
+
it.each([
|
|
383
|
+
Buffer.from('legacy bytes-only result'),
|
|
384
|
+
{ bytes: Buffer.from('verified bytes') },
|
|
385
|
+
{ bytes: Buffer.from('verified bytes'), identity: Buffer.alloc(24) },
|
|
386
|
+
{ bytes: 'not bytes', identity: rawIdentity },
|
|
387
|
+
])('fails closed when the native read API returns an invalid result: %#', invalid => {
|
|
388
|
+
const bridge = (0, native_bridge_1.createSecureFsBoundary)(() => ({
|
|
389
|
+
acquireProjectLease: jest.fn(() => ({})),
|
|
390
|
+
releaseProjectLease: jest.fn(),
|
|
391
|
+
readRegularFile: jest.fn(() => invalid),
|
|
392
|
+
writeProjectTransaction: jest.fn(),
|
|
393
|
+
removeObservedProjectFile: jest.fn(),
|
|
394
|
+
}));
|
|
395
|
+
expect(() => bridge.readRegularFile(path_1.default.join(projectRoot, 'sensors.json'), 1024)).toThrow(/invalid.*read|identity/i);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
const nativeFixtureAvailable = ['linux', 'darwin', 'win32'].includes(process.platform)
|
|
399
|
+
&& ['x64', 'arm64'].includes(process.arch)
|
|
400
|
+
&& fs_1.default.existsSync(path_1.default.join(__dirname, '../../../prebuilds', `${process.platform}-${process.arch}`, 'secure_fs.node'));
|
|
401
|
+
const nativeOnly = nativeFixtureAvailable ? describe : describe.skip;
|
|
402
|
+
nativeOnly('native secure-fs identity fence fixtures', () => {
|
|
403
|
+
let root;
|
|
404
|
+
let binding;
|
|
405
|
+
beforeEach(() => {
|
|
406
|
+
root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-secure-fs-identity-'));
|
|
407
|
+
binding = require(path_1.default.join(__dirname, '../../../prebuilds', `${process.platform}-${process.arch}`, 'secure_fs.node'));
|
|
408
|
+
});
|
|
409
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
410
|
+
it('returns bytes with a canonical native identity from the same regular-file read', () => {
|
|
411
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
412
|
+
fs_1.default.writeFileSync(target, 'observed bytes');
|
|
413
|
+
const observed = binding.readRegularFile(target, 1024);
|
|
414
|
+
expect(observed.bytes).toEqual(Buffer.from('observed bytes'));
|
|
415
|
+
expect(observed.identity).toHaveLength(24);
|
|
416
|
+
expect(observed.identity.subarray(0, 4).toString('ascii')).toBe('SFSI');
|
|
417
|
+
expect(observed.identity[4]).toBe(1);
|
|
418
|
+
expect(observed.identity[5]).toBe(process.platform === 'win32' ? 2 : 1);
|
|
419
|
+
expect(observed.identity.subarray(6, 8)).toEqual(Buffer.alloc(2));
|
|
420
|
+
});
|
|
421
|
+
it('rejects embedded NUL path arguments before any truncated filesystem access', () => {
|
|
422
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
423
|
+
fs_1.default.writeFileSync(target, 'owner bytes');
|
|
424
|
+
expect(() => binding.acquireProjectLease(`${root}\0ignored`)).toThrow(/NUL byte/i);
|
|
425
|
+
expect(() => binding.readRegularFile(`${target}\0ignored`, 1024)).toThrow(/NUL byte/i);
|
|
426
|
+
expect(() => binding.writeProjectTransaction(`${root}\0ignored`, 'created.json', Buffer.from('new bytes'), {
|
|
427
|
+
mode: 'create', createParents: false,
|
|
428
|
+
})).toThrow(/NUL byte/i);
|
|
429
|
+
expect(() => binding.writeProjectTransaction(root, 'sensors.json\0ignored', Buffer.from('new bytes'), {
|
|
430
|
+
mode: 'create', createParents: false,
|
|
431
|
+
})).toThrow(/NUL byte/i);
|
|
432
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('owner bytes');
|
|
433
|
+
expect(fs_1.default.existsSync(path_1.default.join(root, 'created.json'))).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
it('marks an existing create target as a structured no-replace conflict', () => {
|
|
436
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
437
|
+
fs_1.default.writeFileSync(target, 'owner bytes');
|
|
438
|
+
let failure;
|
|
439
|
+
try {
|
|
440
|
+
binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
441
|
+
mode: 'create', createParents: false,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
failure = error;
|
|
446
|
+
}
|
|
447
|
+
expect(failure).toBeDefined();
|
|
448
|
+
expect(failure).toMatchObject({ code: 'AWM_SECURE_FS_DESTINATION_EXISTS' });
|
|
449
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('owner bytes');
|
|
450
|
+
});
|
|
451
|
+
it('rejects a valid identity observed from a different same-byte file', () => {
|
|
452
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
453
|
+
const other = path_1.default.join(root, 'other.json');
|
|
454
|
+
fs_1.default.writeFileSync(target, 'same bytes');
|
|
455
|
+
fs_1.default.writeFileSync(other, 'same bytes');
|
|
456
|
+
const targetRead = binding.readRegularFile(target, 1024);
|
|
457
|
+
const wrongIdentity = binding.readRegularFile(other, 1024).identity;
|
|
458
|
+
expect(() => binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
459
|
+
mode: 'replace', expected: targetRead.bytes, expectedIdentity: wrongIdentity, createParents: false,
|
|
460
|
+
})).toThrow(/original changed|transaction failed/i);
|
|
461
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('same bytes');
|
|
462
|
+
});
|
|
463
|
+
it('rejects a replacement call with a missing identity token', () => {
|
|
464
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
465
|
+
fs_1.default.writeFileSync(target, 'observed bytes');
|
|
466
|
+
const observed = binding.readRegularFile(target, 1024);
|
|
467
|
+
expect(() => binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
468
|
+
mode: 'replace', expected: observed.bytes, createParents: false,
|
|
469
|
+
})).toThrow(/invalid.*arguments|identity/i);
|
|
470
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('observed bytes');
|
|
471
|
+
});
|
|
472
|
+
it.each([
|
|
473
|
+
['truncated', (identity) => identity.subarray(0, identity.length - 1)],
|
|
474
|
+
['bad magic', (identity) => { const invalid = Buffer.from(identity); invalid[0] ^= 0xff; return invalid; }],
|
|
475
|
+
['unsupported version', (identity) => { const invalid = Buffer.from(identity); invalid[4] = 2; return invalid; }],
|
|
476
|
+
['wrong platform', (identity) => { const invalid = Buffer.from(identity); invalid[5] = process.platform === 'win32' ? 1 : 2; return invalid; }],
|
|
477
|
+
['nonzero reserved bytes', (identity) => { const invalid = Buffer.from(identity); invalid[6] = 1; return invalid; }],
|
|
478
|
+
])('rejects an identity token with %s before replacement', (_label, mutateIdentity) => {
|
|
479
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
480
|
+
fs_1.default.writeFileSync(target, 'observed bytes');
|
|
481
|
+
const observed = binding.readRegularFile(target, 1024);
|
|
482
|
+
expect(() => binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
483
|
+
mode: 'replace', expected: observed.bytes, expectedIdentity: mutateIdentity(observed.identity), createParents: false,
|
|
484
|
+
})).toThrow(/invalid.*arguments|identity/i);
|
|
485
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('observed bytes');
|
|
486
|
+
});
|
|
487
|
+
it('does not replace a same-byte target substituted after the exact native read', () => {
|
|
488
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
489
|
+
const original = path_1.default.join(root, 'original-sensors.json');
|
|
490
|
+
fs_1.default.writeFileSync(target, 'observed bytes');
|
|
491
|
+
const observed = binding.readRegularFile(target, 1024);
|
|
492
|
+
fs_1.default.renameSync(target, original);
|
|
493
|
+
fs_1.default.writeFileSync(target, 'observed bytes');
|
|
494
|
+
expect(() => binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
495
|
+
mode: 'replace', expected: observed.bytes, expectedIdentity: observed.identity, createParents: false,
|
|
496
|
+
})).toThrow(/original changed|transaction failed/i);
|
|
497
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('observed bytes');
|
|
498
|
+
expect(fs_1.default.readFileSync(original, 'utf8')).toBe('observed bytes');
|
|
499
|
+
});
|
|
500
|
+
it('removes the exact observed file but rejects a substituted same-byte destination', () => {
|
|
501
|
+
const target = path_1.default.join(root, 'created.json');
|
|
502
|
+
const original = path_1.default.join(root, 'original-created.json');
|
|
503
|
+
fs_1.default.writeFileSync(target, 'created bytes');
|
|
504
|
+
const observed = binding.readRegularFile(target, 1024);
|
|
505
|
+
binding.removeObservedProjectFile(root, 'created.json', observed.identity);
|
|
506
|
+
expect(fs_1.default.existsSync(target)).toBe(false);
|
|
507
|
+
fs_1.default.writeFileSync(target, 'created bytes');
|
|
508
|
+
const substituted = binding.readRegularFile(target, 1024);
|
|
509
|
+
fs_1.default.renameSync(target, original);
|
|
510
|
+
fs_1.default.writeFileSync(target, 'created bytes');
|
|
511
|
+
expect(() => binding.removeObservedProjectFile(root, 'created.json', substituted.identity))
|
|
512
|
+
.toThrow(/original changed|transaction failed|identity/i);
|
|
513
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('created bytes');
|
|
514
|
+
expect(fs_1.default.readFileSync(original, 'utf8')).toBe('created bytes');
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
const posixOnly = process.platform !== 'win32' && nativeFixtureAvailable ? describe : describe.skip;
|
|
518
|
+
posixOnly('native secure-fs POSIX staging fixtures', () => {
|
|
519
|
+
let root;
|
|
520
|
+
let binding;
|
|
521
|
+
beforeEach(() => {
|
|
522
|
+
root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-secure-fs-staging-'));
|
|
523
|
+
binding = require(path_1.default.join(__dirname, '../../../prebuilds', `${process.platform}-${process.arch}`, 'secure_fs.node'));
|
|
524
|
+
});
|
|
525
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
526
|
+
it('does not let a pre-created PID-derived staging name block publication', () => {
|
|
527
|
+
const legacyStaging = path_1.default.join(root, `.sensors.json.secure-fs.tmp.${process.pid}`);
|
|
528
|
+
fs_1.default.writeFileSync(legacyStaging, 'not ours');
|
|
529
|
+
binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
530
|
+
mode: 'create', createParents: false,
|
|
531
|
+
});
|
|
532
|
+
expect(fs_1.default.readFileSync(path_1.default.join(root, 'sensors.json'), 'utf8')).toBe('new bytes');
|
|
533
|
+
expect(fs_1.default.readFileSync(legacyStaging, 'utf8')).toBe('not ours');
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
nativeOnly('native secure-fs project lease fixtures', () => {
|
|
537
|
+
let root;
|
|
538
|
+
let binding;
|
|
539
|
+
const selectedArtifact = path_1.default.join(__dirname, '../../../prebuilds', `${process.platform}-${process.arch}`, 'secure_fs.node');
|
|
540
|
+
beforeEach(() => {
|
|
541
|
+
root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-secure-fs-lease-'));
|
|
542
|
+
binding = require(selectedArtifact);
|
|
543
|
+
});
|
|
544
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
545
|
+
it('rejects a second AWM process before mutation, then admits it after release', () => {
|
|
546
|
+
const target = path_1.default.join(root, 'owner.txt');
|
|
547
|
+
fs_1.default.writeFileSync(target, 'owner bytes');
|
|
548
|
+
const first = binding.acquireProjectLease(root);
|
|
549
|
+
const claimant = `
|
|
550
|
+
const fs = require('fs');
|
|
551
|
+
const [artifact, projectRoot, target, content] = process.argv.slice(1);
|
|
552
|
+
const binding = require(artifact);
|
|
553
|
+
try {
|
|
554
|
+
const lease = binding.acquireProjectLease(projectRoot);
|
|
555
|
+
fs.writeFileSync(target, content);
|
|
556
|
+
binding.releaseProjectLease(lease);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (/project lease is already held/i.test(String(error && error.message))) process.exit(73);
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
`;
|
|
562
|
+
const blocked = (0, child_process_1.spawnSync)(process.execPath, ['-e', claimant, selectedArtifact, root, target, 'claimant bytes'], { encoding: 'utf8' });
|
|
563
|
+
expect(blocked.status).toBe(73);
|
|
564
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('owner bytes');
|
|
565
|
+
binding.releaseProjectLease(first);
|
|
566
|
+
const admitted = (0, child_process_1.spawnSync)(process.execPath, ['-e', claimant, selectedArtifact, root, target, 'claimant bytes'], { encoding: 'utf8' });
|
|
567
|
+
expect(admitted).toMatchObject({ status: 0, signal: null });
|
|
568
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('claimant bytes');
|
|
569
|
+
expect(fs_1.default.lstatSync(path_1.default.join(root, '.awm', '.secure-fs-lease')).isFile()).toBe(true);
|
|
570
|
+
});
|
|
571
|
+
it('uses an unforgeable native token and makes release idempotent', () => {
|
|
572
|
+
const lease = binding.acquireProjectLease(root);
|
|
573
|
+
expect(lease).toEqual(expect.any(Object));
|
|
574
|
+
const releaseUnknown = binding.releaseProjectLease;
|
|
575
|
+
for (const invalid of [{}, null, 1, 'lease']) {
|
|
576
|
+
expect(() => releaseUnknown(invalid)).toThrow(/invalid.*lease token/i);
|
|
577
|
+
}
|
|
578
|
+
expect(() => binding.releaseProjectLease(lease)).not.toThrow();
|
|
579
|
+
expect(() => binding.releaseProjectLease(lease)).not.toThrow();
|
|
580
|
+
const next = binding.acquireProjectLease(root);
|
|
581
|
+
binding.releaseProjectLease(next);
|
|
582
|
+
});
|
|
583
|
+
it('rejects a symlink or reparse-point .awm ancestor without touching its target', () => {
|
|
584
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-secure-fs-lease-outside-'));
|
|
585
|
+
try {
|
|
586
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(root, '.awm'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
587
|
+
expect(() => binding.acquireProjectLease(root)).toThrow(/rejected path ancestor|project lease/i);
|
|
588
|
+
expect(fs_1.default.readdirSync(outside)).toEqual([]);
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
it('releases the OS-held lease automatically when its claimant process exits', () => {
|
|
595
|
+
const claimant = `
|
|
596
|
+
const binding = require(process.argv[1]);
|
|
597
|
+
globalThis.lease = binding.acquireProjectLease(process.argv[2]);
|
|
598
|
+
`;
|
|
599
|
+
const exited = (0, child_process_1.spawnSync)(process.execPath, ['-e', claimant, selectedArtifact, root], { encoding: 'utf8' });
|
|
600
|
+
expect(exited).toMatchObject({ status: 0, signal: null });
|
|
601
|
+
const next = binding.acquireProjectLease(root);
|
|
602
|
+
binding.releaseProjectLease(next);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
const windowsOnly = process.platform === 'win32' ? describe : describe.skip;
|
|
606
|
+
windowsOnly('native secure-fs Windows handle fixtures', () => {
|
|
607
|
+
let root;
|
|
608
|
+
let binding;
|
|
609
|
+
beforeEach(() => {
|
|
610
|
+
root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-secure-fs-win-'));
|
|
611
|
+
// The CI build packages the current platform's artifact before Jest runs.
|
|
612
|
+
binding = require(path_1.default.join(__dirname, '../../../prebuilds', `win32-${process.arch}`, 'secure_fs.node'));
|
|
613
|
+
});
|
|
614
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
615
|
+
it('rejects a junction ancestor without creating a file in its target', () => {
|
|
616
|
+
const outside = path_1.default.join(root, 'outside');
|
|
617
|
+
const junction = path_1.default.join(root, 'junction');
|
|
618
|
+
fs_1.default.mkdirSync(outside);
|
|
619
|
+
fs_1.default.symlinkSync(outside, junction, 'junction');
|
|
620
|
+
const target = path_1.default.join(junction, 'sensors.json');
|
|
621
|
+
expect(() => binding.writeProjectTransaction(root, 'junction/sensors.json', Buffer.from('new bytes'), { mode: 'create', createParents: false })).toThrow(/rejected path ancestor/i);
|
|
622
|
+
expect(fs_1.default.existsSync(path_1.default.join(outside, 'sensors.json'))).toBe(false);
|
|
623
|
+
});
|
|
624
|
+
it('rejects an existing target without mutating its bytes', () => {
|
|
625
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
626
|
+
fs_1.default.writeFileSync(target, 'existing bytes');
|
|
627
|
+
expect(() => binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), { mode: 'create', createParents: false })).toThrow(/target exists|destination already exists|transaction failed/i);
|
|
628
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('existing bytes');
|
|
629
|
+
});
|
|
630
|
+
it('rejects a stale replacement fence without changing a concurrent target', () => {
|
|
631
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
632
|
+
fs_1.default.writeFileSync(target, 'concurrent bytes');
|
|
633
|
+
const observed = binding.readRegularFile(target, 1024);
|
|
634
|
+
expect(() => binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
635
|
+
mode: 'replace', expected: Buffer.from('observed bytes'), expectedIdentity: observed.identity, createParents: false,
|
|
636
|
+
})).toThrow(/original changed|transaction failed/i);
|
|
637
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('concurrent bytes');
|
|
638
|
+
});
|
|
639
|
+
it('replaces the exact observed target while its mandatory share fence remains open', () => {
|
|
640
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
641
|
+
fs_1.default.writeFileSync(target, 'observed bytes');
|
|
642
|
+
const observed = binding.readRegularFile(target, 1024);
|
|
643
|
+
binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
644
|
+
mode: 'replace', expected: observed.bytes, expectedIdentity: observed.identity, createParents: false,
|
|
645
|
+
});
|
|
646
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('new bytes');
|
|
647
|
+
});
|
|
648
|
+
it('does not replace a same-byte target swapped after the exact native read', () => {
|
|
649
|
+
const target = path_1.default.join(root, 'sensors.json');
|
|
650
|
+
const original = path_1.default.join(root, 'original-sensors.json');
|
|
651
|
+
fs_1.default.writeFileSync(target, 'observed bytes');
|
|
652
|
+
const observed = binding.readRegularFile(target, 1024);
|
|
653
|
+
fs_1.default.renameSync(target, original);
|
|
654
|
+
fs_1.default.writeFileSync(target, 'observed bytes');
|
|
655
|
+
expect(() => binding.writeProjectTransaction(root, 'sensors.json', Buffer.from('new bytes'), {
|
|
656
|
+
mode: 'replace', expected: observed.bytes, expectedIdentity: observed.identity, createParents: false,
|
|
657
|
+
})).toThrow(/original changed|transaction failed/i);
|
|
658
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('observed bytes');
|
|
659
|
+
expect(fs_1.default.readFileSync(original, 'utf8')).toBe('observed bytes');
|
|
660
|
+
});
|
|
661
|
+
it('creates missing safe parent components before publishing', () => {
|
|
662
|
+
binding.writeProjectTransaction(root, '.awm/assets/eslint.config.awm.mjs', Buffer.from('new bytes'), { mode: 'create', createParents: true });
|
|
663
|
+
expect(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'assets', 'eslint.config.awm.mjs'), 'utf8')).toBe('new bytes');
|
|
664
|
+
});
|
|
665
|
+
it('never follows a junction swapped into a checked parent', async () => {
|
|
666
|
+
const trusted = path_1.default.join(root, 'trusted');
|
|
667
|
+
const parked = path_1.default.join(root, 'parked');
|
|
668
|
+
const outside = path_1.default.join(root, 'outside');
|
|
669
|
+
fs_1.default.mkdirSync(trusted);
|
|
670
|
+
fs_1.default.mkdirSync(outside);
|
|
671
|
+
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 2);
|
|
672
|
+
const state = new Int32Array(shared);
|
|
673
|
+
const worker = new worker_threads_1.Worker(`
|
|
674
|
+
const fs = require('fs');
|
|
675
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
676
|
+
const state = new Int32Array(workerData.shared);
|
|
677
|
+
const restore = () => {
|
|
678
|
+
try {
|
|
679
|
+
if (fs.lstatSync(workerData.trusted).isSymbolicLink()) fs.rmSync(workerData.trusted);
|
|
680
|
+
} catch {}
|
|
681
|
+
try {
|
|
682
|
+
if (!fs.existsSync(workerData.trusted) && fs.existsSync(workerData.parked)) {
|
|
683
|
+
fs.renameSync(workerData.parked, workerData.trusted);
|
|
684
|
+
}
|
|
685
|
+
} catch {}
|
|
686
|
+
};
|
|
687
|
+
while (Atomics.load(state, 0) === 0) {
|
|
688
|
+
try {
|
|
689
|
+
fs.renameSync(workerData.trusted, workerData.parked);
|
|
690
|
+
fs.symlinkSync(workerData.outside, workerData.trusted, 'junction');
|
|
691
|
+
Atomics.add(state, 1, 1);
|
|
692
|
+
Atomics.notify(state, 1);
|
|
693
|
+
Atomics.wait(state, 0, 0, 1);
|
|
694
|
+
fs.rmSync(workerData.trusted);
|
|
695
|
+
fs.renameSync(workerData.parked, workerData.trusted);
|
|
696
|
+
Atomics.wait(state, 0, 0, 1);
|
|
697
|
+
} catch {
|
|
698
|
+
restore();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
restore();
|
|
702
|
+
parentPort.postMessage('done');
|
|
703
|
+
`, { eval: true, workerData: { shared, trusted, parked, outside } });
|
|
704
|
+
const done = new Promise((resolve, reject) => {
|
|
705
|
+
worker.once('message', () => resolve());
|
|
706
|
+
worker.once('error', reject);
|
|
707
|
+
});
|
|
708
|
+
let published = 0;
|
|
709
|
+
try {
|
|
710
|
+
Atomics.wait(state, 1, 0, 5_000);
|
|
711
|
+
expect(Atomics.load(state, 1)).toBeGreaterThan(0);
|
|
712
|
+
for (let attempt = 0; attempt < 500; attempt += 1) {
|
|
713
|
+
try {
|
|
714
|
+
binding.writeProjectTransaction(root, `trusted/sensors-${attempt}.json`, Buffer.from('new bytes'), {
|
|
715
|
+
mode: 'create', createParents: false,
|
|
716
|
+
});
|
|
717
|
+
published += 1;
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
expect(String(error.message ?? error)).toMatch(/^secure-fs /);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
Atomics.store(state, 0, 1);
|
|
726
|
+
Atomics.notify(state, 0);
|
|
727
|
+
await done;
|
|
728
|
+
}
|
|
729
|
+
expect(Atomics.load(state, 1)).toBeGreaterThan(0);
|
|
730
|
+
expect(published).toBeGreaterThan(0);
|
|
731
|
+
expect(fs_1.default.readdirSync(outside)).toEqual([]);
|
|
732
|
+
}, 30_000);
|
|
733
|
+
});
|