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