@evomap/evolver-adapter-public 2.0.0-beta.2 → 2.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,1109 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- /** 凭证持久化(~/.evomap, 0600). token/keypair 私钥都经此, 文件权限收紧. */
1
+ import { randomBytes } from 'node:crypto';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { constants, closeSync, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
4
+ import { basename, dirname, resolve, win32 } from 'node:path';
5
+ import { POWERSHELL_STDIN_SCRIPT_COMMAND, windowsAclFailureDetail, } from './windowsPowerShell.js';
6
+ const DIRECTORY_MODE = 0o700;
7
+ const FILE_MODE = 0o600;
8
+ export class CredentialStoreError extends Error {
9
+ constructor(message, options) {
10
+ super(`Unsafe credential path: ${message}`, options);
11
+ this.name = 'CredentialStoreError';
12
+ }
13
+ }
14
+ /** Persists OAuth tokens and keypair private keys behind a local secret-file boundary. */
4
15
  export class CredentialStore {
5
16
  path;
6
- constructor(path) {
7
- this.path = path;
17
+ platform;
18
+ windowsAclOps;
19
+ darwinAclReader;
20
+ windowsParentStateReader;
21
+ linkFile;
22
+ renameFile;
23
+ // ctime detects in-place ACL drift while dev/ino detects entry replacement.
24
+ securedDirectoryState = null;
25
+ securedCredentialState = null;
26
+ securedAncestorStates = new Map();
27
+ trustedWindowsParentStates = new Map();
28
+ constructor(path, options = {}) {
29
+ this.path = resolve(path);
30
+ this.platform = options.platform ?? process.platform;
31
+ this.windowsAclOps = this.platform === 'win32'
32
+ ? (options.windowsAclOps ?? new PowerShellWindowsAclOps())
33
+ : undefined;
34
+ this.darwinAclReader = options.darwinAclReader ?? readDarwinAcl;
35
+ this.windowsParentStateReader = options.windowsParentStateReader ?? parentSecurityStates;
36
+ this.linkFile = options.linkFile ?? linkSync;
37
+ this.renameFile = options.renameFile ?? renameSync;
8
38
  }
9
39
  load() {
10
- if (!existsSync(this.path))
40
+ if (!this.prepareDirectory(false))
41
+ return null;
42
+ const directory = dirname(this.path);
43
+ const directoryIdentity = this.directoryIdentity(directory);
44
+ const fd = this.openCredentialFile();
45
+ if (fd === null)
11
46
  return null;
12
47
  try {
13
- return JSON.parse(readFileSync(this.path, 'utf8'));
48
+ this.assertDirectoryIdentity(directory, directoryIdentity);
49
+ this.secureCredentialFd(fd);
50
+ const raw = readFileSync(fd, 'utf8');
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
54
+ return null;
55
+ return parsed;
56
+ }
57
+ catch (error) {
58
+ if (error instanceof SyntaxError)
59
+ return null;
60
+ throw error;
61
+ }
14
62
  }
15
- catch {
63
+ finally {
64
+ closeSync(fd);
65
+ }
66
+ }
67
+ /** Validate an existing credential path without changing its mode, DACL, ACL, or contents. */
68
+ inspectTrustedExisting() {
69
+ if (!this.prepareDirectory(false, false))
70
+ return null;
71
+ const directory = dirname(this.path);
72
+ const directoryIdentity = this.directoryIdentity(directory);
73
+ const fd = this.openCredentialFile();
74
+ if (fd === null)
16
75
  return null;
76
+ try {
77
+ this.assertDirectoryIdentity(directory, directoryIdentity);
78
+ const stat = this.assertCredentialFdTrustedReadOnly(fd);
79
+ this.assertDirectoryIdentity(directory, directoryIdentity);
80
+ return {
81
+ dev: stat.dev,
82
+ ino: stat.ino,
83
+ birthtimeNs: stat.birthtimeNs,
84
+ ctimeNs: stat.ctimeNs,
85
+ mtimeNs: stat.mtimeNs,
86
+ size: stat.size,
87
+ mode: stat.mode,
88
+ uid: stat.uid,
89
+ };
90
+ }
91
+ finally {
92
+ closeSync(fd);
93
+ }
94
+ }
95
+ /** Validate the existing parent chain for a future create without creating or hardening it. */
96
+ inspectTrustedParentForCreate() {
97
+ const directory = dirname(this.path);
98
+ if (dirname(directory) === directory) {
99
+ throw new CredentialStoreError('filesystem root cannot be used as the credential directory');
100
+ }
101
+ const missing = this.missingDirectoryComponents(directory);
102
+ if (missing.length === 0) {
103
+ void this.prepareDirectory(false, false);
104
+ return;
105
+ }
106
+ if (!this.isPosix()) {
107
+ const highestMissing = missing.at(-1);
108
+ if (highestMissing === undefined) {
109
+ throw new CredentialStoreError('credential parent inspection failed');
110
+ }
111
+ this.assertTrustedWindowsParent(dirname(highestMissing), true);
17
112
  }
18
113
  }
19
114
  save(cred) {
20
- mkdirSync(dirname(this.path), { recursive: true });
21
- writeFileSync(this.path, JSON.stringify(cred), { mode: 0o600 });
115
+ void this.saveCredential(cred, true);
116
+ }
117
+ /** Persist a credential only when no filesystem entry already occupies its path. */
118
+ saveIfAbsent(cred) {
119
+ return this.saveCredential(cred, false);
120
+ }
121
+ saveCredential(cred, replaceExisting) {
122
+ this.prepareDirectory(true);
123
+ const existingFd = this.openCredentialFile();
124
+ if (existingFd !== null) {
125
+ try {
126
+ this.secureCredentialFd(existingFd);
127
+ }
128
+ finally {
129
+ closeSync(existingFd);
130
+ }
131
+ if (!replaceExisting)
132
+ return false;
133
+ }
134
+ const directory = dirname(this.path);
135
+ const directoryIdentity = this.directoryIdentity(directory);
136
+ const temporaryPath = resolve(directory, `.${basename(this.path)}.tmp-${randomBytes(16).toString('hex')}`);
137
+ let temporaryIdentity = null;
138
+ let fd = null;
139
+ try {
140
+ fd = openSync(temporaryPath, this.exclusiveWriteFlags(), FILE_MODE);
141
+ const temporaryStat = bigFstat(fd);
142
+ if (!temporaryStat.isFile())
143
+ throw new CredentialStoreError('temporary entry is not a regular file');
144
+ temporaryIdentity = identityOf(temporaryStat);
145
+ if (this.isPosix()) {
146
+ this.clearDarwinAcl(fd, temporaryIdentity, 'temporary');
147
+ fchmodSync(fd, FILE_MODE);
148
+ }
149
+ else
150
+ this.secureWindowsFile(temporaryPath, fd, temporaryIdentity);
151
+ writeFileSync(fd, JSON.stringify(cred), 'utf8');
152
+ fsyncSync(fd);
153
+ closeSync(fd);
154
+ fd = null;
155
+ this.assertDirectoryIdentity(directory, directoryIdentity);
156
+ if (replaceExisting) {
157
+ this.assertSafeDestination();
158
+ this.renameFile(temporaryPath, this.path);
159
+ }
160
+ else if (!this.publishIfAbsent(temporaryPath)) {
161
+ const incumbentFd = this.openCredentialFile();
162
+ if (incumbentFd === null) {
163
+ throw new CredentialStoreError('credential changed during no-clobber publication');
164
+ }
165
+ try {
166
+ this.secureCredentialFd(incumbentFd);
167
+ }
168
+ finally {
169
+ closeSync(incumbentFd);
170
+ }
171
+ return false;
172
+ }
173
+ this.verifySavedFile(temporaryIdentity);
174
+ if (!replaceExisting)
175
+ this.unlinkIfSameFile(temporaryPath, temporaryIdentity);
176
+ temporaryIdentity = null;
177
+ this.syncDirectory(directory);
178
+ return true;
179
+ }
180
+ finally {
181
+ if (fd !== null)
182
+ closeSync(fd);
183
+ if (temporaryIdentity !== null)
184
+ this.unlinkIfSameFile(temporaryPath, temporaryIdentity);
185
+ }
186
+ }
187
+ prepareDirectory(create, harden = true) {
188
+ const directory = dirname(this.path);
189
+ if (dirname(directory) === directory) {
190
+ throw new CredentialStoreError('filesystem root cannot be used as the credential directory');
191
+ }
192
+ const missing = this.missingDirectoryComponents(directory);
193
+ if (missing.length > 0) {
194
+ if (!create)
195
+ return false;
196
+ for (const component of missing.reverse())
197
+ this.createDirectoryComponent(component);
198
+ if (this.missingDirectoryComponents(directory).length > 0) {
199
+ throw new CredentialStoreError('parent directory disappeared during creation');
200
+ }
201
+ }
202
+ const stat = bigLstat(directory);
203
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
204
+ throw new CredentialStoreError('parent is a symlink or not a directory');
205
+ }
206
+ if (!this.isPosix()) {
207
+ if (harden) {
208
+ this.secureWindowsDirectory(directory, stat);
209
+ }
210
+ else {
211
+ this.assertTrustedWindowsParent(dirname(directory), true);
212
+ this.assertTrustedWindowsParent(directory, true);
213
+ }
214
+ return true;
215
+ }
216
+ const uid = BigInt(currentUid());
217
+ if (stat.uid !== uid)
218
+ throw new CredentialStoreError('parent directory is not owned by the current user');
219
+ const flags = constants.O_RDONLY | optionalConstant('O_DIRECTORY') | optionalConstant('O_NOFOLLOW');
220
+ const fd = openSync(directory, flags);
221
+ try {
222
+ const opened = bigFstat(fd);
223
+ if (!opened.isDirectory() || !sameIdentity(stat, opened)) {
224
+ throw new CredentialStoreError('parent directory changed during validation');
225
+ }
226
+ if (opened.uid !== uid)
227
+ throw new CredentialStoreError('parent directory is not owned by the current user');
228
+ if ((permissionMode(opened) & 0o022) !== 0) {
229
+ throw new CredentialStoreError('parent directory is writable by group or other');
230
+ }
231
+ this.assertSafeDarwinAncestor(directory, opened, false);
232
+ if (!harden)
233
+ return true;
234
+ this.clearDarwinAcl(fd, identityOf(opened), 'directory');
235
+ if (permissionMode(bigFstat(fd)) !== DIRECTORY_MODE)
236
+ fchmodSync(fd, DIRECTORY_MODE);
237
+ if (permissionMode(bigFstat(fd)) !== DIRECTORY_MODE) {
238
+ throw new CredentialStoreError('parent directory permissions could not be restricted to 0700');
239
+ }
240
+ }
241
+ finally {
242
+ closeSync(fd);
243
+ }
244
+ return true;
245
+ }
246
+ missingDirectoryComponents(directory) {
247
+ let current = directory;
248
+ let isCredentialDirectory = true;
249
+ const missing = [];
250
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
251
+ for (;;) {
252
+ let stat;
253
+ try {
254
+ stat = bigLstat(current);
255
+ }
256
+ catch (error) {
257
+ if (!isErrno(error, 'ENOENT'))
258
+ throw error;
259
+ missing.push(current);
260
+ isCredentialDirectory = false;
261
+ const parent = dirname(current);
262
+ if (parent === current)
263
+ return missing;
264
+ current = parent;
265
+ continue;
266
+ }
267
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
268
+ throw new CredentialStoreError(`parent component ${current} is a symlink or not a directory`);
269
+ }
270
+ if (this.isPosix()) {
271
+ if (uid === undefined)
272
+ throw new CredentialStoreError('current user ownership cannot be determined');
273
+ if (isCredentialDirectory && (stat.mode & 2n) !== 0n && (stat.mode & 512n) !== 0n) {
274
+ throw new CredentialStoreError(`credential directory ${current} is a shared sticky directory`);
275
+ }
276
+ if (stat.uid !== BigInt(uid) && stat.uid !== 0n) {
277
+ throw new CredentialStoreError(`parent component ${current} has an untrusted owner`);
278
+ }
279
+ if (!isCredentialDirectory && (stat.uid === BigInt(uid) || stat.uid === 0n)) {
280
+ this.assertSafeDarwinAncestor(current, stat, stat.uid === BigInt(uid) && stat.uid !== 0n);
281
+ }
282
+ if (!isCredentialDirectory && (stat.mode & 18n) !== 0n && (stat.mode & 512n) === 0n) {
283
+ throw new CredentialStoreError(`parent component ${current} is writable by untrusted users`);
284
+ }
285
+ }
286
+ const parent = dirname(current);
287
+ if (parent === current)
288
+ return missing;
289
+ // Root-owned directories are a stable trust anchor. Stopping here also
290
+ // avoids rejecting platform-managed aliases above that anchor (e.g. /var).
291
+ if (uid !== undefined && stat.uid === 0n)
292
+ return missing;
293
+ isCredentialDirectory = false;
294
+ current = parent;
295
+ }
296
+ }
297
+ createDirectoryComponent(component) {
298
+ if (!this.isPosix())
299
+ this.assertTrustedWindowsParent(dirname(component), true);
300
+ try {
301
+ mkdirSync(component, { mode: DIRECTORY_MODE });
302
+ }
303
+ catch (error) {
304
+ if (!isErrno(error, 'EEXIST'))
305
+ throw error;
306
+ }
307
+ const stat = bigLstat(component);
308
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
309
+ throw new CredentialStoreError(`parent component ${component} is a symlink or not a directory`);
310
+ }
311
+ if (!this.isPosix()) {
312
+ this.secureWindowsDirectory(component, stat);
313
+ return;
314
+ }
315
+ if (stat.uid !== BigInt(currentUid())) {
316
+ throw new CredentialStoreError(`created parent component ${component} is not owned by the current user`);
317
+ }
318
+ const fd = openSync(component, constants.O_RDONLY | optionalConstant('O_DIRECTORY') | optionalConstant('O_NOFOLLOW'));
319
+ try {
320
+ const opened = bigFstat(fd);
321
+ if (!opened.isDirectory() || !sameIdentity(stat, opened) || opened.uid !== BigInt(currentUid())) {
322
+ throw new CredentialStoreError(`parent component ${component} changed during creation`);
323
+ }
324
+ this.clearDarwinAcl(fd, identityOf(opened), 'directory');
325
+ if (permissionMode(bigFstat(fd)) !== DIRECTORY_MODE)
326
+ fchmodSync(fd, DIRECTORY_MODE);
327
+ }
328
+ finally {
329
+ closeSync(fd);
330
+ }
331
+ }
332
+ openCredentialFile() {
333
+ const before = safeLstat(this.path);
334
+ if (!before)
335
+ return null;
336
+ if (before.isSymbolicLink() || !before.isFile()) {
337
+ throw new CredentialStoreError('credential entry is a symlink or not a regular file');
338
+ }
339
+ let fd;
340
+ try {
341
+ fd = openSync(this.path, constants.O_RDONLY | optionalConstant('O_NOFOLLOW') | optionalConstant('O_NONBLOCK'));
342
+ }
343
+ catch (error) {
344
+ if (isErrno(error, 'ENOENT'))
345
+ return null;
346
+ if (isErrno(error, 'ELOOP'))
347
+ throw new CredentialStoreError('credential entry is a symlink');
348
+ throw error;
349
+ }
350
+ try {
351
+ const opened = bigFstat(fd);
352
+ const after = bigLstat(this.path);
353
+ if (!opened.isFile() || after.isSymbolicLink() || !after.isFile() ||
354
+ !sameIdentity(before, opened) || !sameIdentity(opened, after)) {
355
+ throw new CredentialStoreError('credential entry changed during validation');
356
+ }
357
+ return fd;
358
+ }
359
+ catch (error) {
360
+ closeSync(fd);
361
+ throw error;
362
+ }
363
+ }
364
+ secureCredentialFd(fd) {
365
+ const stat = bigFstat(fd);
366
+ if (!stat.isFile())
367
+ throw new CredentialStoreError('credential entry is not a regular file');
368
+ if (!this.isPosix()) {
369
+ this.secureWindowsFile(this.path, fd, identityOf(stat));
370
+ return;
371
+ }
372
+ if (stat.uid !== BigInt(currentUid()))
373
+ throw new CredentialStoreError('credential file is not owned by the current user');
374
+ // Reject group/other-writable modes before fchmod and before trusting content.
375
+ // Migrating 0644→0600 is still allowed (read exposure only); write exposure is fail-closed.
376
+ if ((permissionMode(stat) & 0o022) !== 0) {
377
+ throw new CredentialStoreError('credential file is writable by group or other');
378
+ }
379
+ this.clearDarwinAcl(fd, identityOf(stat), 'credential');
380
+ if (permissionMode(bigFstat(fd)) !== FILE_MODE)
381
+ fchmodSync(fd, FILE_MODE);
382
+ if (permissionMode(bigFstat(fd)) !== FILE_MODE) {
383
+ throw new CredentialStoreError('credential file permissions could not be restricted to 0600');
384
+ }
385
+ }
386
+ assertCredentialFdTrustedReadOnly(fd) {
387
+ const stat = bigFstat(fd);
388
+ if (!stat.isFile())
389
+ throw new CredentialStoreError('credential entry is not a regular file');
390
+ if (!this.isPosix()) {
391
+ if (!this.windowsAclOps)
392
+ throw new CredentialStoreError('Windows file ACL policy is unavailable');
393
+ const identity = identityOf(stat);
394
+ this.assertTrustedWindowsParent(dirname(this.path), false);
395
+ try {
396
+ this.windowsAclOps.assertTrustedFile(this.path);
397
+ }
398
+ catch (cause) {
399
+ throw windowsCredentialStoreError('Windows credential file ACL is not trusted', cause);
400
+ }
401
+ const pathStat = bigLstat(this.path);
402
+ const fdStat = bigFstat(fd);
403
+ if (pathStat.isSymbolicLink() || !pathStat.isFile() || !fdStat.isFile() ||
404
+ !sameIdentity(pathStat, identity) || !sameIdentity(fdStat, identity)) {
405
+ throw new CredentialStoreError('credential file changed during read-only ACL validation');
406
+ }
407
+ return fdStat;
408
+ }
409
+ if (stat.uid !== BigInt(currentUid())) {
410
+ throw new CredentialStoreError('credential file is not owned by the current user');
411
+ }
412
+ if ((permissionMode(stat) & 0o022) !== 0) {
413
+ throw new CredentialStoreError('credential file is writable by group or other');
414
+ }
415
+ this.assertTrustedDarwinFile(fd, identityOf(stat));
416
+ return bigFstat(fd);
417
+ }
418
+ assertSafeDestination() {
419
+ const fd = this.openCredentialFile();
420
+ if (fd === null)
421
+ return;
422
+ try {
423
+ this.secureCredentialFd(fd);
424
+ }
425
+ finally {
426
+ closeSync(fd);
427
+ }
428
+ }
429
+ verifySavedFile(expectedIdentity) {
430
+ const fd = this.openCredentialFile();
431
+ if (fd === null)
432
+ throw new CredentialStoreError('credential file disappeared after atomic replacement');
433
+ try {
434
+ if (!sameIdentity(bigFstat(fd), expectedIdentity)) {
435
+ throw new CredentialStoreError('credential file changed during atomic replacement');
436
+ }
437
+ this.securedCredentialState = securityStateOf(bigFstat(fd));
438
+ // A same-directory rename preserves the DACL already verified on the
439
+ // temporary inode. Avoid a post-commit ACL mutation that could fail after
440
+ // the old credential has already been replaced.
441
+ if (this.isPosix())
442
+ this.secureCredentialFd(fd);
443
+ }
444
+ finally {
445
+ closeSync(fd);
446
+ }
447
+ }
448
+ directoryIdentity(directory) {
449
+ const stat = bigLstat(directory);
450
+ if (stat.isSymbolicLink() || !stat.isDirectory())
451
+ throw new CredentialStoreError('parent is unsafe');
452
+ return identityOf(stat);
453
+ }
454
+ assertDirectoryIdentity(directory, identity) {
455
+ const stat = bigLstat(directory);
456
+ if (stat.isSymbolicLink() || !stat.isDirectory() || !sameIdentity(stat, identity)) {
457
+ throw new CredentialStoreError('parent directory changed during write');
458
+ }
459
+ }
460
+ exclusiveWriteFlags() {
461
+ return constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | optionalConstant('O_NOFOLLOW');
462
+ }
463
+ publishIfAbsent(temporaryPath) {
464
+ try {
465
+ // A same-directory hard link publishes the already-fsynced inode without
466
+ // replacing a destination that appears after the absence preflight.
467
+ this.linkFile(temporaryPath, this.path);
468
+ return true;
469
+ }
470
+ catch (error) {
471
+ if (isErrno(error, 'EEXIST'))
472
+ return false;
473
+ try {
474
+ if (safeLstat(this.path) !== null)
475
+ return false;
476
+ }
477
+ catch {
478
+ // Report a stable fail-closed error below.
479
+ }
480
+ throw new CredentialStoreError('atomic no-clobber publication is unavailable');
481
+ }
482
+ }
483
+ syncDirectory(directory) {
484
+ if (!this.isPosix())
485
+ return;
486
+ const fd = openSync(directory, constants.O_RDONLY | optionalConstant('O_DIRECTORY') | optionalConstant('O_NOFOLLOW'));
487
+ try {
488
+ fsyncSync(fd);
489
+ }
490
+ finally {
491
+ closeSync(fd);
492
+ }
493
+ }
494
+ unlinkIfSameFile(path, identity) {
495
+ const stat = safeLstat(path);
496
+ if (!stat || stat.isSymbolicLink() || !stat.isFile() || !sameIdentity(stat, identity))
497
+ return;
498
+ unlinkSync(path);
499
+ }
500
+ secureWindowsDirectory(path, stat) {
501
+ if (!this.windowsAclOps)
502
+ throw new CredentialStoreError('Windows directory ACL policy is unavailable');
503
+ const identity = identityOf(stat);
504
+ // An existing directory may itself grant DELETE to another SID. Requiring
505
+ // strict create rights on its direct parent prevents delete-and-recreate
506
+ // with a junction before the pathname-based ACL update completes.
507
+ this.assertTrustedWindowsParent(dirname(path), true);
508
+ // Reject unsafe grants on an existing directory instead of trying to
509
+ // migrate them in place. Tightening a DACL cannot revoke access already
510
+ // granted to a handle opened by an untrusted principal.
511
+ this.assertTrustedWindowsParent(path, true);
512
+ if (this.securedDirectoryState && sameSecurityState(this.securedDirectoryState, stat))
513
+ return;
514
+ try {
515
+ this.windowsAclOps.secureDirectory(path);
516
+ }
517
+ catch (cause) {
518
+ throw windowsCredentialStoreError('Windows directory ACL could not be secured', cause);
519
+ }
520
+ const after = bigLstat(path);
521
+ if (after.isSymbolicLink() || !after.isDirectory() || !sameIdentity(after, identity)) {
522
+ throw new CredentialStoreError('parent directory changed while securing its Windows ACL');
523
+ }
524
+ this.securedDirectoryState = securityStateOf(after);
525
+ }
526
+ secureWindowsFile(path, fd, identity) {
527
+ if (!this.windowsAclOps)
528
+ throw new CredentialStoreError('Windows file ACL policy is unavailable');
529
+ const isCredentialPath = path === this.path;
530
+ const before = bigFstat(fd);
531
+ this.assertTrustedWindowsParent(dirname(path), false);
532
+ // Reject unsafe grants on an existing file instead of migrating them in
533
+ // place and then consuming. Tightening a DACL cannot revoke access already
534
+ // granted to a handle opened by an untrusted principal.
535
+ try {
536
+ this.windowsAclOps.assertTrustedFile(path);
537
+ }
538
+ catch (cause) {
539
+ throw windowsCredentialStoreError('Windows credential file ACL is not trusted', cause);
540
+ }
541
+ if (isCredentialPath && this.securedCredentialState &&
542
+ sameSecurityState(this.securedCredentialState, before))
543
+ return;
544
+ try {
545
+ this.windowsAclOps.secureFile(path);
546
+ }
547
+ catch (cause) {
548
+ throw windowsCredentialStoreError('Windows file ACL could not be secured', cause);
549
+ }
550
+ const pathStat = bigLstat(path);
551
+ const fdStat = bigFstat(fd);
552
+ if (pathStat.isSymbolicLink() || !pathStat.isFile() || !fdStat.isFile() ||
553
+ !sameIdentity(pathStat, identity) || !sameIdentity(fdStat, identity)) {
554
+ throw new CredentialStoreError('credential file changed while securing its Windows ACL');
555
+ }
556
+ if (isCredentialPath)
557
+ this.securedCredentialState = securityStateOf(fdStat);
558
+ }
559
+ clearDarwinAcl(fd, identity, kind) {
560
+ if (this.platform !== 'darwin')
561
+ return;
562
+ const cached = kind === 'directory'
563
+ ? this.securedDirectoryState
564
+ : kind === 'credential' ? this.securedCredentialState : null;
565
+ if (cached && sameSecurityState(cached, bigFstat(fd)))
566
+ return;
567
+ try {
568
+ execFileSync('/bin/chmod', ['-N', '/dev/fd/3'], {
569
+ shell: false,
570
+ stdio: ['ignore', 'ignore', 'ignore', fd],
571
+ timeout: 10_000,
572
+ });
573
+ }
574
+ catch {
575
+ throw new CredentialStoreError(`${kind} extended ACL could not be removed`);
576
+ }
577
+ const after = bigFstat(fd);
578
+ if (!sameIdentity(after, identity)) {
579
+ throw new CredentialStoreError(`${kind} changed while removing its extended ACL`);
580
+ }
581
+ if (kind === 'directory')
582
+ this.securedDirectoryState = securityStateOf(after);
583
+ else if (kind === 'credential')
584
+ this.securedCredentialState = securityStateOf(after);
585
+ }
586
+ assertSafeDarwinAncestor(path, stat, rejectAnyAllow) {
587
+ if (this.platform !== 'darwin')
588
+ return;
589
+ const cached = this.securedAncestorStates.get(path);
590
+ if (cached && sameSecurityState(cached, stat))
591
+ return;
592
+ const identity = identityOf(stat);
593
+ const flags = constants.O_RDONLY | optionalConstant('O_DIRECTORY') | optionalConstant('O_NOFOLLOW');
594
+ const fd = openSync(path, flags);
595
+ try {
596
+ const opened = bigFstat(fd);
597
+ if (!opened.isDirectory() || !sameIdentity(opened, identity)) {
598
+ throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
599
+ }
600
+ for (let attempt = 0; attempt < 5; attempt += 1) {
601
+ const initialMetadata = bigFstat(fd);
602
+ const initialState = securityStateOf(initialMetadata);
603
+ let output;
604
+ try {
605
+ output = this.darwinAclReader(path);
606
+ }
607
+ catch {
608
+ throw new CredentialStoreError(`ancestor directory ${path} ACL could not be inspected`);
609
+ }
610
+ if (hasUnsafeDarwinAllowAcl(output, rejectAnyAllow)) {
611
+ throw new CredentialStoreError(`ancestor directory ${path} grants access through an extended ACL`);
612
+ }
613
+ const after = bigLstat(path);
614
+ const openedAfter = bigFstat(fd);
615
+ if (after.isSymbolicLink() || !after.isDirectory() ||
616
+ !sameIdentity(after, identity) || !sameIdentity(openedAfter, identity)) {
617
+ throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
618
+ }
619
+ if (sameSecurityState(initialState, after) && sameSecurityState(initialState, openedAfter)) {
620
+ this.securedAncestorStates.set(path, securityStateOf(openedAfter));
621
+ return;
622
+ }
623
+ if (sameDarwinAncestorMetadata(initialMetadata, after)
624
+ && sameDarwinAncestorMetadata(initialMetadata, openedAfter)) {
625
+ let confirmedOutput;
626
+ try {
627
+ confirmedOutput = this.darwinAclReader(path);
628
+ }
629
+ catch {
630
+ throw new CredentialStoreError(`ancestor directory ${path} ACL could not be inspected`);
631
+ }
632
+ if (hasUnsafeDarwinAllowAcl(confirmedOutput, rejectAnyAllow)) {
633
+ throw new CredentialStoreError(`ancestor directory ${path} grants access through an extended ACL`);
634
+ }
635
+ const confirmedPath = bigLstat(path);
636
+ const confirmedOpened = bigFstat(fd);
637
+ if (confirmedOutput === output
638
+ && !confirmedPath.isSymbolicLink()
639
+ && confirmedPath.isDirectory()
640
+ && sameDarwinAncestorMetadata(initialMetadata, confirmedPath)
641
+ && sameDarwinAncestorMetadata(initialMetadata, confirmedOpened)) {
642
+ this.securedAncestorStates.set(path, securityStateOf(confirmedOpened));
643
+ return;
644
+ }
645
+ }
646
+ }
647
+ throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
648
+ }
649
+ finally {
650
+ closeSync(fd);
651
+ }
652
+ }
653
+ assertTrustedDarwinFile(fd, identity) {
654
+ if (this.platform !== 'darwin')
655
+ return;
656
+ for (let attempt = 0; attempt < 5; attempt += 1) {
657
+ const initialState = securityStateOf(bigFstat(fd));
658
+ let output;
659
+ try {
660
+ output = this.darwinAclReader(this.path);
661
+ }
662
+ catch {
663
+ throw new CredentialStoreError('credential file ACL could not be inspected');
664
+ }
665
+ if (hasUnsafeDarwinAllowAcl(output, true)) {
666
+ throw new CredentialStoreError('credential file grants access through an extended ACL');
667
+ }
668
+ const after = bigLstat(this.path);
669
+ const openedAfter = bigFstat(fd);
670
+ if (after.isSymbolicLink() || !after.isFile() ||
671
+ !sameIdentity(after, identity) || !sameIdentity(openedAfter, identity)) {
672
+ throw new CredentialStoreError('credential file changed during ACL validation');
673
+ }
674
+ if (sameSecurityState(initialState, after) &&
675
+ sameSecurityState(initialState, openedAfter))
676
+ return;
677
+ }
678
+ throw new CredentialStoreError('credential file changed during ACL validation');
679
+ }
680
+ assertTrustedWindowsParent(path, strictCreate) {
681
+ if (!this.windowsAclOps)
682
+ throw new CredentialStoreError('Windows parent ACL policy is unavailable');
683
+ const cacheKey = `${strictCreate ? 'create' : 'existing'}:${path}`;
684
+ try {
685
+ const before = this.windowsParentStateReader(path);
686
+ const cached = this.trustedWindowsParentStates.get(cacheKey);
687
+ if (cached && samePathSecurityStates(cached, before))
688
+ return;
689
+ this.windowsAclOps.assertTrustedParent(path, strictCreate);
690
+ const after = this.windowsParentStateReader(path);
691
+ if (samePathSecurityStates(before, after)) {
692
+ this.trustedWindowsParentStates.set(cacheKey, after);
693
+ }
694
+ }
695
+ catch (cause) {
696
+ throw windowsCredentialStoreError('Windows parent directory chain is not trusted', cause);
697
+ }
698
+ }
699
+ isPosix() {
700
+ return this.platform !== 'win32';
701
+ }
702
+ }
703
+ function windowsCredentialStoreError(message, cause) {
704
+ const detail = cause instanceof Error ? windowsAclFailureDetail(cause) : '';
705
+ return new CredentialStoreError(detail ? `${message} (${detail})` : message, { cause });
706
+ }
707
+ const WINDOWS_ACL_SCRIPT = String.raw `
708
+ $ErrorActionPreference = 'Stop'
709
+ # Progress records can be serialized as CLIXML onto redirected stderr and
710
+ # obscure the actual failure. Suppress them so stderr carries only real errors.
711
+ $ProgressPreference = 'SilentlyContinue'
712
+
713
+ function ConvertTo-OneLineAclDiagnostic([object]$Value) {
714
+ if ($null -eq $Value) { return '' }
715
+ return ([string]$Value -replace '[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+', ' ').Trim()
716
+ }
717
+
718
+ function Throw-CredentialAclFailure(
719
+ [string]$Reason,
720
+ [string]$Path = '',
721
+ [string]$Sid = '',
722
+ [object]$Rights = $null,
723
+ [string]$Principal = ''
724
+ ) {
725
+ $parts = @($Reason)
726
+ $safePath = ConvertTo-OneLineAclDiagnostic $Path
727
+ $safeSid = ConvertTo-OneLineAclDiagnostic $Sid
728
+ $safeRights = ConvertTo-OneLineAclDiagnostic $Rights
729
+ $safePrincipal = ConvertTo-OneLineAclDiagnostic $Principal
730
+ if (-not [string]::IsNullOrWhiteSpace($safePath)) { $parts += ('path=' + $safePath) }
731
+ if (-not [string]::IsNullOrWhiteSpace($safeSid)) { $parts += ('sid=' + $safeSid) }
732
+ if (-not [string]::IsNullOrWhiteSpace($safePrincipal)) { $parts += ('principal=' + $safePrincipal) }
733
+ if (-not [string]::IsNullOrWhiteSpace($safeRights)) { $parts += ('rights=' + $safeRights) }
734
+ throw ($parts -join '; ')
735
+ }
736
+
737
+ # Windows PowerShell can serialize ordinary error-stream writes as CLIXML when
738
+ # stderr is redirected. Write the terminating exception directly to native stderr so
739
+ # Node receives the actionable message rather than only a serialized record.
740
+ trap {
741
+ $message = ConvertTo-OneLineAclDiagnostic $_.Exception.Message
742
+ if ([string]::IsNullOrWhiteSpace($message)) { $message = 'Credential ACL check failed' }
743
+ [Console]::Error.WriteLine($message)
744
+ exit 1
745
+ }
746
+
747
+ $Target = [Environment]::GetEnvironmentVariable('EVOMAP_CREDENTIAL_ACL_TARGET', 'Process')
748
+ $Kind = [Environment]::GetEnvironmentVariable('EVOMAP_CREDENTIAL_ACL_KIND', 'Process')
749
+ if ([string]::IsNullOrEmpty($Target) -or
750
+ ($Kind -ne 'assert-parent' -and $Kind -ne 'assert-create-parent' -and
751
+ $Kind -ne 'assert-file' -and $Kind -ne 'directory' -and $Kind -ne 'file')) {
752
+ throw 'Invalid credential ACL input'
753
+ }
754
+ $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
755
+ $trustedSids = @(
756
+ $sid.Value,
757
+ 'S-1-5-18',
758
+ 'S-1-5-32-544',
759
+ 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'
760
+ )
761
+ $dangerousRights = [System.Security.AccessControl.FileSystemRights](
762
+ [System.Security.AccessControl.FileSystemRights]::Delete -bor
763
+ [System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
764
+ [System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
765
+ [System.Security.AccessControl.FileSystemRights]::TakeOwnership
766
+ )
767
+ $dangerousFileRights = [System.Security.AccessControl.FileSystemRights](
768
+ [System.Security.AccessControl.FileSystemRights]::WriteData -bor
769
+ [System.Security.AccessControl.FileSystemRights]::AppendData -bor
770
+ [System.Security.AccessControl.FileSystemRights]::WriteAttributes -bor
771
+ [System.Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor
772
+ [System.Security.AccessControl.FileSystemRights]::Delete -bor
773
+ [System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
774
+ [System.Security.AccessControl.FileSystemRights]::TakeOwnership
775
+ )
776
+
777
+ function Assert-TrustedParent([string]$ParentPath, [bool]$StrictCreate) {
778
+ $full = [System.IO.Path]::GetFullPath($ParentPath)
779
+ $root = [System.IO.Path]::GetPathRoot($full)
780
+ if ([string]::IsNullOrEmpty($root) -or $root -notmatch '^[A-Za-z]:\\$') {
781
+ Throw-CredentialAclFailure -Reason 'Credential parent must be on a local drive' -Path $full
782
+ }
783
+ if ([System.IO.DriveInfo]::new($root).DriveType -ne [System.IO.DriveType]::Fixed) {
784
+ Throw-CredentialAclFailure -Reason 'Credential parent must be on a fixed local drive' -Path $full
785
+ }
786
+ $current = $root
787
+ $relative = $full.Substring($root.Length)
788
+ $segments = $relative.Split(
789
+ [char[]]@([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar),
790
+ [System.StringSplitOptions]::RemoveEmptyEntries
791
+ )
792
+ $paths = @($root)
793
+ foreach ($segment in $segments) {
794
+ $current = [System.IO.Path]::Combine($current, $segment)
795
+ $paths += $current
796
+ }
797
+ $trimSeparators = [char[]]@(
798
+ [System.IO.Path]::DirectorySeparatorChar,
799
+ [System.IO.Path]::AltDirectorySeparatorChar
800
+ )
801
+ $finalParent = $full.TrimEnd($trimSeparators)
802
+ foreach ($current in $paths) {
803
+ $isCreateParent = $StrictCreate -and [string]::Equals(
804
+ $current.TrimEnd($trimSeparators),
805
+ $finalParent,
806
+ [System.StringComparison]::OrdinalIgnoreCase
807
+ )
808
+ $item = Get-Item -LiteralPath $current -Force
809
+ if (-not $item.PSIsContainer -or
810
+ (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) {
811
+ Throw-CredentialAclFailure -Reason 'Credential parent contains a reparse point or non-directory' -Path $current
812
+ }
813
+ $parentAcl = Get-Acl -LiteralPath $current
814
+ $ownerSid = $parentAcl.GetOwner([System.Security.Principal.SecurityIdentifier])
815
+ if ($trustedSids -notcontains $ownerSid.Value) {
816
+ Throw-CredentialAclFailure -Reason 'Credential parent has an untrusted owner' -Path $current -Sid $ownerSid.Value
817
+ }
818
+ foreach ($parentRule in @($parentAcl.Access)) {
819
+ if ($parentRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) {
820
+ continue
821
+ }
822
+ $rights = $parentRule.FileSystemRights
823
+ $inheritOnly = (($parentRule.PropagationFlags -band
824
+ [System.Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0)
825
+ $containerInherit = (($parentRule.InheritanceFlags -band
826
+ [System.Security.AccessControl.InheritanceFlags]::ContainerInherit) -ne 0)
827
+ $objectInherit = (($parentRule.InheritanceFlags -band
828
+ [System.Security.AccessControl.InheritanceFlags]::ObjectInherit) -ne 0)
829
+ $hasGranularDanger = (($rights -band $dangerousRights) -ne 0)
830
+ $hasCreateDanger = (($rights -band (
831
+ [System.Security.AccessControl.FileSystemRights]::CreateDirectories -bor
832
+ [System.Security.AccessControl.FileSystemRights]::CreateFiles
833
+ )) -ne 0)
834
+ $hasCompositeDanger =
835
+ (($rights -band [System.Security.AccessControl.FileSystemRights]::Write) -eq
836
+ [System.Security.AccessControl.FileSystemRights]::Write) -or
837
+ (($rights -band [System.Security.AccessControl.FileSystemRights]::Modify) -eq
838
+ [System.Security.AccessControl.FileSystemRights]::Modify) -or
839
+ (($rights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq
840
+ [System.Security.AccessControl.FileSystemRights]::FullControl)
841
+ if ($inheritOnly) {
842
+ $dangerousGrant = $isCreateParent -and ($containerInherit -or $objectInherit) -and
843
+ ($hasGranularDanger -or $hasCreateDanger -or $hasCompositeDanger)
844
+ } else {
845
+ $dangerousGrant = $hasGranularDanger -or ($isCreateParent -and $hasCreateDanger)
846
+ }
847
+ if (-not $dangerousGrant) { continue }
848
+ try {
849
+ $parentRuleSid = $parentRule.IdentityReference.Translate(
850
+ [System.Security.Principal.SecurityIdentifier]
851
+ )
852
+ } catch {
853
+ Throw-CredentialAclFailure -Reason 'Credential parent contains an unresolvable write principal' -Path $current -Principal $parentRule.IdentityReference.Value -Rights $rights
854
+ }
855
+ if ($trustedSids -notcontains $parentRuleSid.Value) {
856
+ Throw-CredentialAclFailure -Reason 'Credential parent grants write access to an untrusted principal' -Path $current -Sid $parentRuleSid.Value -Rights $rights
857
+ }
858
+ }
859
+ }
860
+ }
861
+
862
+ function Assert-TrustedFile([string]$FilePath) {
863
+ $item = Get-Item -LiteralPath $FilePath -Force
864
+ if ($item.PSIsContainer -or
865
+ (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) {
866
+ Throw-CredentialAclFailure -Reason 'Credential file is a reparse point or not a regular file' -Path $FilePath
867
+ }
868
+ $fileAcl = Get-Acl -LiteralPath $FilePath
869
+ $ownerSid = $fileAcl.GetOwner([System.Security.Principal.SecurityIdentifier])
870
+ if ($trustedSids -notcontains $ownerSid.Value) {
871
+ Throw-CredentialAclFailure -Reason 'Credential file has an untrusted owner' -Path $FilePath -Sid $ownerSid.Value
872
+ }
873
+ foreach ($fileRule in @($fileAcl.Access)) {
874
+ if ($fileRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) {
875
+ continue
876
+ }
877
+ $rights = $fileRule.FileSystemRights
878
+ $hasGranularDanger = (($rights -band $dangerousFileRights) -ne 0)
879
+ $hasCompositeDanger =
880
+ (($rights -band [System.Security.AccessControl.FileSystemRights]::Write) -eq
881
+ [System.Security.AccessControl.FileSystemRights]::Write) -or
882
+ (($rights -band [System.Security.AccessControl.FileSystemRights]::Modify) -eq
883
+ [System.Security.AccessControl.FileSystemRights]::Modify) -or
884
+ (($rights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq
885
+ [System.Security.AccessControl.FileSystemRights]::FullControl)
886
+ if (-not ($hasGranularDanger -or $hasCompositeDanger)) { continue }
887
+ try {
888
+ $fileRuleSid = $fileRule.IdentityReference.Translate(
889
+ [System.Security.Principal.SecurityIdentifier]
890
+ )
891
+ } catch {
892
+ Throw-CredentialAclFailure -Reason 'Credential file contains an unresolvable write principal' -Path $FilePath -Principal $fileRule.IdentityReference.Value -Rights $rights
893
+ }
894
+ if ($trustedSids -notcontains $fileRuleSid.Value) {
895
+ Throw-CredentialAclFailure -Reason 'Credential file grants write access to an untrusted principal' -Path $FilePath -Sid $fileRuleSid.Value -Rights $rights
896
+ }
897
+ }
898
+ }
899
+
900
+ function Test-CanonicalCredentialAcl(
901
+ [System.Security.AccessControl.FileSystemSecurity]$CandidateAcl,
902
+ [System.Security.Principal.SecurityIdentifier]$ExpectedOwner,
903
+ [System.Security.AccessControl.InheritanceFlags]$ExpectedInheritance
904
+ ) {
905
+ try {
906
+ $candidateRules = @($CandidateAcl.Access)
907
+ if (-not $CandidateAcl.AreAccessRulesProtected -or $candidateRules.Count -ne 1) {
908
+ return $false
909
+ }
910
+ $candidateOwner = $CandidateAcl.GetOwner([System.Security.Principal.SecurityIdentifier])
911
+ $candidateRule = $candidateRules[0]
912
+ $candidateRuleSid = $candidateRule.IdentityReference.Translate(
913
+ [System.Security.Principal.SecurityIdentifier]
914
+ )
915
+ return (
916
+ $candidateOwner.Value -eq $ExpectedOwner.Value -and
917
+ $candidateRuleSid.Value -eq $ExpectedOwner.Value -and
918
+ $candidateRule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and
919
+ $candidateRule.FileSystemRights -eq [System.Security.AccessControl.FileSystemRights]::FullControl -and
920
+ $candidateRule.InheritanceFlags -eq $ExpectedInheritance -and
921
+ $candidateRule.PropagationFlags -eq [System.Security.AccessControl.PropagationFlags]::None -and
922
+ -not $candidateRule.IsInherited
923
+ )
924
+ } catch {
925
+ return $false
926
+ }
927
+ }
928
+
929
+ if ($Kind -eq 'assert-parent' -or $Kind -eq 'assert-create-parent') {
930
+ # Existing entries only require protection against replacement. Immediately
931
+ # before mkdir, also reject principals that could atomically squat the name.
932
+ Assert-TrustedParent $Target ($Kind -eq 'assert-create-parent')
933
+ exit 0
934
+ }
935
+
936
+ if ($Kind -eq 'assert-file') {
937
+ # Fail-closed: refuse untrusted write/modify/delete grants before Set-Acl
938
+ # and before trusting file content. Do not migrate unsafe grants in place.
939
+ Assert-TrustedFile $Target
940
+ exit 0
941
+ }
942
+
943
+ $expectedInheritance = if ($Kind -eq 'directory') {
944
+ [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
945
+ } else {
946
+ [System.Security.AccessControl.InheritanceFlags]::None
947
+ }
948
+ $acl = Get-Acl -LiteralPath $Target
949
+ if (Test-CanonicalCredentialAcl $acl $sid $expectedInheritance) {
950
+ exit 0
951
+ }
952
+ $acl.SetOwner($sid)
953
+ $acl.SetAccessRuleProtection($true, $false)
954
+ foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($rule) }
955
+ $access = [System.Security.AccessControl.FileSystemAccessRule]::new(
956
+ $sid,
957
+ [System.Security.AccessControl.FileSystemRights]::FullControl,
958
+ $expectedInheritance,
959
+ [System.Security.AccessControl.PropagationFlags]::None,
960
+ [System.Security.AccessControl.AccessControlType]::Allow
961
+ )
962
+ [void]$acl.AddAccessRule($access)
963
+ Set-Acl -LiteralPath $Target -AclObject $acl
964
+ $verified = Get-Acl -LiteralPath $Target
965
+ if (-not (Test-CanonicalCredentialAcl $verified $sid $expectedInheritance)) {
966
+ Throw-CredentialAclFailure -Reason 'Credential ACL verification failed' -Path $Target -Sid $sid.Value
967
+ }
968
+ `;
969
+ class PowerShellWindowsAclOps {
970
+ executable;
971
+ systemRoot;
972
+ constructor() {
973
+ const systemRoot = process.env['SystemRoot'];
974
+ if (!systemRoot || !win32.isAbsolute(systemRoot)) {
975
+ throw new CredentialStoreError('Windows SystemRoot is unavailable or invalid');
976
+ }
977
+ this.systemRoot = systemRoot;
978
+ this.executable = win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
979
+ }
980
+ secureDirectory(path) {
981
+ this.run(path, 'directory');
982
+ }
983
+ secureFile(path) {
984
+ this.run(path, 'file');
985
+ }
986
+ assertTrustedParent(path, strictCreate) {
987
+ this.run(path, strictCreate ? 'assert-create-parent' : 'assert-parent');
988
+ }
989
+ assertTrustedFile(path) {
990
+ this.run(path, 'assert-file');
991
+ }
992
+ run(path, kind) {
993
+ try {
994
+ execFileSync(this.executable, [
995
+ '-NoLogo',
996
+ '-NoProfile',
997
+ '-NonInteractive',
998
+ '-ExecutionPolicy', 'Bypass',
999
+ // The fixed wrapper parses stdin once. Bare -Command - can execute
1000
+ // PowerShell 5.1 input statement by statement and mask an earlier error.
1001
+ '-Command', POWERSHELL_STDIN_SCRIPT_COMMAND,
1002
+ ], {
1003
+ encoding: 'utf8',
1004
+ env: {
1005
+ SystemRoot: this.systemRoot,
1006
+ EVOMAP_CREDENTIAL_ACL_TARGET: path,
1007
+ EVOMAP_CREDENTIAL_ACL_KIND: kind,
1008
+ },
1009
+ shell: false,
1010
+ // Capture both streams rather than discarding them: the script's own
1011
+ // message names which path level and which SID failed, and without it
1012
+ // every rejection is indistinguishable from "PowerShell is missing".
1013
+ // The script prints nothing on success, so this stays quiet normally.
1014
+ input: WINDOWS_ACL_SCRIPT,
1015
+ stdio: ['pipe', 'pipe', 'pipe'],
1016
+ timeout: 15_000,
1017
+ windowsHide: true,
1018
+ });
1019
+ }
1020
+ catch (cause) {
1021
+ const detail = windowsAclFailureDetail(cause);
1022
+ throw new Error(detail ? `${kind} check failed: ${detail}` : `${kind} check failed`, { cause });
1023
+ }
1024
+ }
1025
+ }
1026
+ function readDarwinAcl(path) {
1027
+ return execFileSync('/bin/ls', ['-lde', path], {
1028
+ encoding: 'utf8',
1029
+ shell: false,
1030
+ stdio: ['ignore', 'pipe', 'ignore'],
1031
+ timeout: 10_000,
1032
+ });
1033
+ }
1034
+ function hasUnsafeDarwinAllowAcl(output, rejectAnyAllow) {
1035
+ const dangerousDirectoryRights = /\b(?:add_file|add_subdirectory|append|delete|delete_child|write|writeattr|writeextattr|writesecurity|chown)\b/;
1036
+ return output.split('\n').some((line) => {
1037
+ if (!/^ \d+:.* allow /.test(line))
1038
+ return false;
1039
+ return rejectAnyAllow || dangerousDirectoryRights.test(line);
1040
+ });
1041
+ }
1042
+ function currentUid() {
1043
+ if (typeof process.getuid !== 'function') {
1044
+ throw new CredentialStoreError('current user ownership cannot be determined');
1045
+ }
1046
+ return process.getuid();
1047
+ }
1048
+ function optionalConstant(name) {
1049
+ return constants[name] ?? 0;
1050
+ }
1051
+ function bigLstat(path) {
1052
+ return lstatSync(path, { bigint: true });
1053
+ }
1054
+ function bigFstat(fd) {
1055
+ return fstatSync(fd, { bigint: true });
1056
+ }
1057
+ function permissionMode(stat) {
1058
+ return Number(stat.mode & 511n);
1059
+ }
1060
+ function identityOf(stat) {
1061
+ return { dev: stat.dev, ino: stat.ino };
1062
+ }
1063
+ function securityStateOf(stat) {
1064
+ return { dev: stat.dev, ino: stat.ino, ctimeNs: stat.ctimeNs };
1065
+ }
1066
+ function parentSecurityStates(path) {
1067
+ const states = [];
1068
+ let current = path;
1069
+ for (;;) {
1070
+ states.push({ path: current, ...securityStateOf(bigLstat(current)) });
1071
+ const parent = dirname(current);
1072
+ if (parent === current)
1073
+ return states;
1074
+ current = parent;
1075
+ }
1076
+ }
1077
+ function sameIdentity(left, right) {
1078
+ return left.dev === right.dev && left.ino === right.ino;
1079
+ }
1080
+ function sameSecurityState(left, right) {
1081
+ return sameIdentity(left, right) && left.ctimeNs === right.ctimeNs;
1082
+ }
1083
+ function sameDarwinAncestorMetadata(left, right) {
1084
+ return sameIdentity(left, right)
1085
+ && right.isDirectory()
1086
+ && !right.isSymbolicLink()
1087
+ && left.uid === right.uid
1088
+ && left.mode === right.mode;
1089
+ }
1090
+ function samePathSecurityStates(left, right) {
1091
+ return left.length === right.length && left.every((state, index) => {
1092
+ const candidate = right[index];
1093
+ return candidate !== undefined && state.path === candidate.path &&
1094
+ sameIdentity(state, candidate) && state.ctimeNs === candidate.ctimeNs;
1095
+ });
1096
+ }
1097
+ function safeLstat(path) {
1098
+ try {
1099
+ return bigLstat(path);
1100
+ }
1101
+ catch (error) {
1102
+ if (isErrno(error, 'ENOENT'))
1103
+ return null;
1104
+ throw error;
22
1105
  }
1106
+ }
1107
+ function isErrno(error, code) {
1108
+ return error.code === code;
23
1109
  }