@cortexkit/opencode-antigravity-auth 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +7 -7
  2. package/dist/cli.js +1722 -484
  3. package/dist/cli.js.map +4 -4
  4. package/dist/cli.meta.json +440 -120
  5. package/dist/index.js +2157 -803
  6. package/dist/index.js.map +4 -4
  7. package/dist/index.meta.json +476 -141
  8. package/dist/server.js +2248 -841
  9. package/dist/server.js.map +4 -4
  10. package/dist/server.meta.json +1 -1
  11. package/dist/src/common-auth-embedded/claustrum/enrollment.d.ts +3 -2
  12. package/dist/src/common-auth-embedded/claustrum/enrollment.js +84 -42
  13. package/dist/src/common-auth-embedded/claustrum/roster.js +2 -2
  14. package/dist/src/common-auth-embedded/fs/atomic-write.d.ts +5 -0
  15. package/dist/src/common-auth-embedded/fs/atomic-write.js +21 -0
  16. package/dist/src/common-auth-embedded/fs/index.d.ts +2 -0
  17. package/dist/src/common-auth-embedded/fs/index.js +1 -0
  18. package/dist/src/common-auth-embedded/fs/private-files.d.ts +66 -0
  19. package/dist/src/common-auth-embedded/fs/private-files.js +302 -0
  20. package/dist/src/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
  21. package/dist/src/common-auth-embedded/fs/refresh-file-lock.js +254 -37
  22. package/dist/src/common-auth-embedded/fs/with-lock.d.ts +3 -0
  23. package/dist/src/common-auth-embedded/fs/with-lock.js +4 -0
  24. package/dist/src/common-auth-embedded/logger/engine.d.ts +6 -8
  25. package/dist/src/common-auth-embedded/logger/engine.js +126 -11
  26. package/dist/src/common-auth-embedded/pi-slot/slot.d.ts +2 -0
  27. package/dist/src/common-auth-embedded/pi-slot/slot.js +34 -6
  28. package/dist/src/common-auth-embedded/rpc/port-file.js +30 -8
  29. package/dist/src/common-auth-embedded/rpc/rpc-client.js +15 -1
  30. package/dist/src/common-auth-embedded/rpc/rpc-server.js +8 -3
  31. package/dist/src/common-auth-embedded/source-output.json +92 -76
  32. package/dist/src/common-auth-embedded/store/mutate.js +2 -2
  33. package/dist/src/common-auth-embedded/store/pool.d.ts +1 -0
  34. package/dist/src/common-auth-embedded/store/pool.js +3 -0
  35. package/dist/src/common-auth-embedded/store/refresh-lock.d.ts +4 -0
  36. package/dist/src/common-auth-embedded/store/refresh-lock.js +5 -0
  37. package/dist/src/ga/rpc/protocol.d.ts +2 -3
  38. package/dist/src/ga/rpc/protocol.d.ts.map +1 -1
  39. package/dist/src/ga/rpc/protocol.js +60 -0
  40. package/dist/src/ga/rpc/protocol.js.map +1 -1
  41. package/dist/src/ga/server/index.d.ts +3 -2
  42. package/dist/src/ga/server/index.d.ts.map +1 -1
  43. package/dist/src/ga/server/index.js +23 -7
  44. package/dist/src/ga/server/index.js.map +1 -1
  45. package/dist/src/plugin/request.d.ts.map +1 -1
  46. package/dist/src/plugin/request.js +8 -14
  47. package/dist/src/plugin/request.js.map +1 -1
  48. package/dist/src/plugin/vault-custody.d.ts +6 -0
  49. package/dist/src/plugin/vault-custody.d.ts.map +1 -1
  50. package/dist/src/plugin/vault-custody.js +81 -2
  51. package/dist/src/plugin/vault-custody.js.map +1 -1
  52. package/dist/tui-build-map.json +29 -13
  53. package/dist/tui-ga-build-map.json +1 -1
  54. package/dist/types/src/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
  55. package/dist/types/src/common-auth-embedded/logger/engine.d.ts +6 -8
  56. package/dist/types/src/common-auth-embedded/store/pool.d.ts +1 -0
  57. package/dist/types/src/common-auth-embedded/store/refresh-lock.d.ts +4 -0
  58. package/dist/types/src/ga/rpc/protocol.d.ts +2 -3
  59. package/dist/types/src/ga/rpc/protocol.d.ts.map +1 -1
  60. package/dist/types/src/ga/server/index.d.ts +3 -2
  61. package/dist/types/src/ga/server/index.d.ts.map +1 -1
  62. package/dist/types/src/plugin/request.d.ts.map +1 -1
  63. package/dist/types/src/plugin/vault-custody.d.ts +6 -0
  64. package/dist/types/src/plugin/vault-custody.d.ts.map +1 -1
  65. package/package.json +5 -4
  66. package/src/common-auth-embedded/claustrum/enrollment.d.ts +3 -2
  67. package/src/common-auth-embedded/claustrum/enrollment.js +84 -42
  68. package/src/common-auth-embedded/claustrum/roster.js +2 -2
  69. package/src/common-auth-embedded/fs/atomic-write.d.ts +5 -0
  70. package/src/common-auth-embedded/fs/atomic-write.js +21 -0
  71. package/src/common-auth-embedded/fs/index.d.ts +2 -0
  72. package/src/common-auth-embedded/fs/index.js +1 -0
  73. package/src/common-auth-embedded/fs/private-files.d.ts +66 -0
  74. package/src/common-auth-embedded/fs/private-files.js +302 -0
  75. package/src/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
  76. package/src/common-auth-embedded/fs/refresh-file-lock.js +254 -37
  77. package/src/common-auth-embedded/fs/with-lock.d.ts +3 -0
  78. package/src/common-auth-embedded/fs/with-lock.js +4 -0
  79. package/src/common-auth-embedded/logger/engine.d.ts +6 -8
  80. package/src/common-auth-embedded/logger/engine.js +126 -11
  81. package/src/common-auth-embedded/pi-slot/slot.d.ts +2 -0
  82. package/src/common-auth-embedded/pi-slot/slot.js +34 -6
  83. package/src/common-auth-embedded/rpc/port-file.js +30 -8
  84. package/src/common-auth-embedded/rpc/rpc-client.js +15 -1
  85. package/src/common-auth-embedded/rpc/rpc-server.js +8 -3
  86. package/src/common-auth-embedded/source-output.json +92 -76
  87. package/src/common-auth-embedded/store/mutate.js +2 -2
  88. package/src/common-auth-embedded/store/pool.d.ts +1 -0
  89. package/src/common-auth-embedded/store/pool.js +3 -0
  90. package/src/common-auth-embedded/store/refresh-lock.d.ts +4 -0
  91. package/src/common-auth-embedded/store/refresh-lock.js +5 -0
  92. package/src/tui-compiled/common-auth-embedded/fs/private-files.js +302 -0
  93. package/src/tui-compiled/common-auth-embedded/logger/engine.js +126 -11
  94. package/src/tui-compiled/common-auth-embedded/rpc/port-file.js +30 -8
  95. package/src/tui-compiled/common-auth-embedded/rpc/rpc-client.js +15 -1
  96. package/src/tui-raw/common-auth-embedded/fs/private-files.js +302 -0
  97. package/src/tui-raw/common-auth-embedded/logger/engine.js +126 -11
  98. package/src/tui-raw/common-auth-embedded/rpc/port-file.js +30 -8
  99. package/src/tui-raw/common-auth-embedded/rpc/rpc-client.js +15 -1
@@ -97,8 +97,9 @@ export interface EnrollmentAncestorOptions {
97
97
  groupPath?: string;
98
98
  }
99
99
  /**
100
- * Protect setup writes from directory replacement. Group write access is safe
101
- * only when local account records prove that the effective user's group is private.
100
+ * Protect setup writes from directory replacement. On POSIX, group write access
101
+ * is safe only when local records prove the effective user's group is private;
102
+ * sticky ancestors are accepted. Windows uses the shared safe-parent policy.
102
103
  */
103
104
  export declare function refuseWritableAncestor(parent: string, options?: EnrollmentAncestorOptions): Promise<void>;
104
105
  export declare function readClaustrumEnrollmentStatus(paths: ClaustrumEnrollmentPaths, proposedName: string): Promise<ClaustrumEnrollmentStatus>;
@@ -4,6 +4,7 @@ import { chmod, mkdir, open, readFile, realpath, rename, stat, unlink, } from 'n
4
4
  import { basename, dirname, extname, isAbsolute, join, resolve, } from 'node:path';
5
5
  import { ClaustrumCredentialError, ClaustrumClient as ClaustrumWireClient, writeEnrollmentTokenFile, } from '@cortexkit/claustrum-client';
6
6
  import { acquireRefreshFileLock } from '../fs/index.js';
7
+ import { ensurePrivateDirectory as ensureWindowsDirectory, PrivateFileError, readPrivateText, verifySafeDirectory, windowsPrivateFiles, writePrivateAtomic, } from '../fs/private-files.js';
7
8
  import { ClaustrumConsumerError } from './errors.js';
8
9
  const ENROLLMENT_SCHEMA = 1;
9
10
  const ENROLLMENT_FILE_MAX_BYTES = 16 * 1024;
@@ -198,6 +199,21 @@ function parseSecretJson(text) {
198
199
  }
199
200
  }
200
201
  async function readBoundedJson(path) {
202
+ if (windowsPrivateFiles) {
203
+ try {
204
+ await ensureWindowsDirectory(dirname(path));
205
+ const text = await readPrivateText(path, true);
206
+ if (Buffer.byteLength(text) > ENROLLMENT_FILE_MAX_BYTES) {
207
+ throw new ClaustrumConsumerError('unsafe-file', 'Claustrum enrollment file is too large');
208
+ }
209
+ return parseSecretJson(text);
210
+ }
211
+ catch (error) {
212
+ if (error.code === 'ENOENT')
213
+ return undefined;
214
+ throw error;
215
+ }
216
+ }
201
217
  let descriptor;
202
218
  try {
203
219
  // O_NOFOLLOW: a symlink planted at the path must not redirect a secret read.
@@ -267,52 +283,76 @@ async function isPrivateGroup(uid, gid, options) {
267
283
  return false;
268
284
  }
269
285
  }
286
+ function ancestorPolicyOptions(options) {
287
+ if (!options.passwdPath && !options.groupPath)
288
+ return undefined;
289
+ return {
290
+ fs: {
291
+ readFile: (path) => readFile(path === '/etc/passwd'
292
+ ? (options.passwdPath ?? path)
293
+ : path === '/etc/group'
294
+ ? (options.groupPath ?? path)
295
+ : path),
296
+ },
297
+ };
298
+ }
270
299
  /**
271
- * Protect setup writes from directory replacement. Group write access is safe
272
- * only when local account records prove that the effective user's group is private.
300
+ * Protect setup writes from directory replacement. On POSIX, group write access
301
+ * is safe only when local records prove the effective user's group is private;
302
+ * sticky ancestors are accepted. Windows uses the shared safe-parent policy.
273
303
  */
274
304
  export async function refuseWritableAncestor(parent, options = {}) {
275
- const uid = process.geteuid?.();
276
- if (uid === undefined)
277
- return;
278
- const gid = process.getegid?.();
279
- let component;
280
- try {
281
- component = await realpath(parent);
282
- }
283
- catch {
284
- return;
285
- }
286
- // Load lazily, once per traversal: account changes take effect on the next write.
287
- let privateGroup;
288
- for (;;) {
289
- const metadata = await stat(component).catch(() => undefined);
290
- if (metadata &&
291
- (metadata.mode & 0o022) !== 0 &&
292
- (metadata.mode & 0o1000) === 0) {
293
- if (privateGroup === undefined) {
294
- privateGroup =
295
- gid !== undefined && (await isPrivateGroup(uid, gid, options));
296
- }
297
- if ((metadata.mode & 0o002) !== 0 ||
298
- metadata.uid !== uid ||
299
- metadata.gid !== gid ||
300
- !privateGroup) {
301
- throw new ClaustrumConsumerError('unsafe-file', `Claustrum enrollment path has a writable ancestor: ${component}. Another user could replace it. Run: chmod g-w,o-w ${component}`);
302
- }
305
+ if (!windowsPrivateFiles) {
306
+ const uid = process.geteuid?.();
307
+ if (uid === undefined)
308
+ return;
309
+ const gid = process.getegid?.();
310
+ let component;
311
+ try {
312
+ component = await realpath(parent);
303
313
  }
304
- const next = dirname(component);
305
- if (next === component)
314
+ catch {
306
315
  return;
307
- component = next;
316
+ }
317
+ // Load lazily, once per traversal: account changes take effect on the next write.
318
+ let privateGroup;
319
+ for (;;) {
320
+ const metadata = await stat(component).catch(() => undefined);
321
+ if (metadata &&
322
+ (metadata.mode & 0o022) !== 0 &&
323
+ (metadata.mode & 0o1000) === 0) {
324
+ if (privateGroup === undefined) {
325
+ privateGroup =
326
+ gid !== undefined && (await isPrivateGroup(uid, gid, options));
327
+ }
328
+ if ((metadata.mode & 0o002) !== 0 ||
329
+ metadata.uid !== uid ||
330
+ metadata.gid !== gid ||
331
+ !privateGroup) {
332
+ throw new ClaustrumConsumerError('unsafe-file', `Claustrum enrollment path has a writable ancestor: ${component}. Another user could replace it. Run: chmod g-w,o-w ${component}`);
333
+ }
334
+ }
335
+ const next = dirname(component);
336
+ if (next === component)
337
+ return;
338
+ component = next;
339
+ }
340
+ }
341
+ try {
342
+ await verifySafeDirectory(parent, ancestorPolicyOptions(options));
343
+ }
344
+ catch (error) {
345
+ if (!(error instanceof PrivateFileError))
346
+ throw error;
347
+ throw new ClaustrumConsumerError('unsafe-file', `Claustrum enrollment parent refused: ${error.path} (${error.reason ?? error.code}). Use a folder that other users cannot write or replace.`);
308
348
  }
309
349
  }
310
- /**
311
- * Create the enrollment directory owner-only (0700), and tighten it to 0700
312
- * when it already exists with group or other bits, so the token and the
313
- * request secret never sit in a directory another account can list.
314
- */
315
- async function ensurePrivateDirectory(directory) {
350
+ /** POSIX tightens the enrollment directory to 0700; Windows verifies safe ancestry without modifying existing parents. */
351
+ async function ensurePrivateDirectory(directory, options = {}) {
352
+ if (windowsPrivateFiles) {
353
+ await ensureWindowsDirectory(directory, false, ancestorPolicyOptions(options));
354
+ return;
355
+ }
316
356
  await mkdir(directory, { recursive: true, mode: 0o700 });
317
357
  const metadata = await stat(directory);
318
358
  const uid = process.geteuid?.();
@@ -324,12 +364,14 @@ async function ensurePrivateDirectory(directory) {
324
364
  }
325
365
  async function writeStateAtomic(path, state, ancestorOptions = {}) {
326
366
  const parent = dirname(path);
327
- await ensurePrivateDirectory(parent);
367
+ await ensurePrivateDirectory(parent, ancestorOptions);
328
368
  await refuseWritableAncestor(parent, ancestorOptions);
329
369
  const bytes = `${JSON.stringify(state)}\n`;
330
370
  if (Buffer.byteLength(bytes) > ENROLLMENT_FILE_MAX_BYTES) {
331
371
  throw new ClaustrumConsumerError('invalid-state', 'Claustrum enrollment state is too large');
332
372
  }
373
+ if (windowsPrivateFiles)
374
+ return writePrivateAtomic(path, bytes);
333
375
  const temporary = join(parent, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
334
376
  let descriptor;
335
377
  let created = false;
@@ -496,7 +538,7 @@ export class ClaustrumEnrollmentManager {
496
538
  return statusFromState(blocked);
497
539
  }
498
540
  async reconcile() {
499
- await ensurePrivateDirectory(dirname(this.#paths.statePath));
541
+ await ensurePrivateDirectory(dirname(this.#paths.statePath), this.#ancestorOptions);
500
542
  const lock = await acquireRefreshFileLock({
501
543
  name: 'ceremony',
502
544
  path: this.#paths.statePath,
@@ -616,7 +658,7 @@ export class ClaustrumEnrollmentManager {
616
658
  }
617
659
  // The vault returns the token exactly once: it reaches disk before the
618
660
  // pending metadata (and its secret) is replaced.
619
- await ensurePrivateDirectory(dirname(this.#paths.tokenPath));
661
+ await ensurePrivateDirectory(dirname(this.#paths.tokenPath), this.#ancestorOptions);
620
662
  await refuseWritableAncestor(dirname(this.#paths.tokenPath), this.#ancestorOptions);
621
663
  await this.#writeTokenFile(this.#paths.tokenPath, {
622
664
  token: outcome.token,
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
2
  import { acquireRefreshFileLock, withLock, writeJsonAtomic, } from '../fs/index.js';
3
+ import { readPrivateText, windowsPrivateFiles } from '../fs/private-files.js';
4
4
  import { isQuotaMap, mergeQuotaObservation, } from '../quota/index.js';
5
5
  import { ClaustrumConsumerError } from './errors.js';
6
6
  import { acceptAccount, declineAccount, isDeclined, } from './interlock.js';
@@ -289,7 +289,7 @@ function decodeRoster(value) {
289
289
  export async function readVaultRoster(path) {
290
290
  let text;
291
291
  try {
292
- text = await readFile(path, 'utf8');
292
+ text = await readPrivateText(path, windowsPrivateFiles);
293
293
  }
294
294
  catch (error) {
295
295
  if (error.code === 'ENOENT')
@@ -1,3 +1,4 @@
1
+ import { type PrivateWriteOutcome } from './private-files.js';
1
2
  export interface AtomicWriteOptions {
2
3
  serialize?: (value: unknown) => string;
3
4
  beforeRename?: () => Promise<void>;
@@ -5,6 +6,10 @@ export interface AtomicWriteOptions {
5
6
  stageName?: () => string;
6
7
  /** Sync the staged file and, after rename, its parent directory. Opt-in. */
7
8
  durable?: boolean;
9
+ /** Windows opt-in: flush file bytes, without promising directory metadata durability. */
10
+ durability?: 'file-only-on-windows';
11
+ /** Reports Windows rename success, file/directory durability, retry counts and error codes, never file bytes. */
12
+ onOutcome?: (outcome: PrivateWriteOutcome) => void;
8
13
  }
9
14
  /** Internal classification of fsync failures, including a failure after rename. */
10
15
  export declare class AtomicSyncError extends Error {
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, open, rename, rm } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
+ import { PrivateFileError, verifyPrivate, windowsPrivateFiles, writePrivateAtomic, } from './private-files.js';
4
5
  /** Internal classification of fsync failures, including a failure after rename. */
5
6
  export class AtomicSyncError extends Error {
6
7
  constructor(cause) {
@@ -16,6 +17,18 @@ export async function writeJsonAtomic(path, value, options = {}) {
16
17
  * and removed ids here, because a later directory sync cannot undo the rename.
17
18
  */
18
19
  export async function writeJsonAtomicTracked(path, value, options, onRenamed) {
20
+ if (windowsPrivateFiles) {
21
+ return writePrivateAtomic(path, options.serialize
22
+ ? options.serialize(value)
23
+ : `${JSON.stringify(value, null, 2)}\n`, {
24
+ beforeRename: options.beforeRename,
25
+ stagePath: `${path}.${(options.stageName ?? randomUUID)()}.tmp`,
26
+ onRenamed,
27
+ durable: options.durable,
28
+ durability: options.durability,
29
+ onOutcome: options.onOutcome,
30
+ });
31
+ }
19
32
  await mkdir(dirname(path), { recursive: true });
20
33
  const tempPath = `${path}.${(options.stageName ?? randomUUID)()}.tmp`;
21
34
  let handle;
@@ -63,6 +76,14 @@ export async function writeJsonAtomicTracked(path, value, options, onRenamed) {
63
76
  }
64
77
  /** Make an already renamed file and its directory durable without rewriting bytes. */
65
78
  export async function syncJsonFile(path) {
79
+ if (windowsPrivateFiles) {
80
+ await verifyPrivate(path, 'file');
81
+ const error = new PrivateFileError(path, 'directory-sync-unsupported', {
82
+ code: 'ENOTSUP',
83
+ });
84
+ error.published = true;
85
+ throw error;
86
+ }
66
87
  const file = await open(path, 'r');
67
88
  try {
68
89
  await file.sync();
@@ -1,6 +1,8 @@
1
1
  export type { AtomicWriteOptions } from './atomic-write.js';
2
2
  export { writeJsonAtomic } from './atomic-write.js';
3
3
  export { WRITER_LOCK_CONSTANTS } from './lock-constants.js';
4
+ export type { PrivateWriteOutcome } from './private-files.js';
5
+ export { PrivateFileError } from './private-files.js';
4
6
  export type { LockLoss, RefreshFileLock } from './refresh-file-lock.js';
5
7
  export { acquireRefreshFileLock, isLostMarkerRaceError, } from './refresh-file-lock.js';
6
8
  export type { LockOptions, LockOwnershipDetails } from './with-lock.js';
@@ -1,5 +1,6 @@
1
1
  export { writeJsonAtomic } from './atomic-write.js';
2
2
  export { WRITER_LOCK_CONSTANTS } from './lock-constants.js';
3
+ export { PrivateFileError } from './private-files.js';
3
4
  // The try-once lock underneath withLock, for callers that hold a lock across
4
5
  // several steps or poll instead of waiting (for example a refresh lease).
5
6
  export { acquireRefreshFileLock, isLostMarkerRaceError, } from './refresh-file-lock.js';
@@ -0,0 +1,66 @@
1
+ import { rename } from 'node:fs/promises';
2
+ import type { PosixOwnerOnlyPathAsyncOptions } from '@cortexkit/private-files';
3
+ export declare const windowsPrivateFiles: boolean;
4
+ type Backend = typeof import('@cortexkit/private-files');
5
+ type Kind = 'file' | 'directory';
6
+ /** Tests inject a backend here; production always imports the private-files package. */
7
+ export declare function setPrivateFilesBackendForTest(api?: Backend): void;
8
+ export declare class PrivateFileError extends Error {
9
+ readonly path: string;
10
+ readonly stage: string;
11
+ readonly code: string;
12
+ readonly win32Code?: number;
13
+ readonly reason?: string;
14
+ published: boolean;
15
+ durabilityConfirmed: boolean;
16
+ fileDurable: boolean;
17
+ attempts: number;
18
+ elapsedMs: number;
19
+ orphanPath?: string;
20
+ constructor(path: string, stage: string, cause: unknown);
21
+ }
22
+ export declare function privateOperation<T>(path: string, stage: string, operation: () => Promise<T>): Promise<T>;
23
+ /** Tests only: policy options used by safe-parent checks that pass none. */
24
+ export declare function setSafeParentOptionsForTest(options?: PosixOwnerOnlyPathAsyncOptions): void;
25
+ /** Existing containing directories use the shared entry-integrity policy, not secret-file permissions. */
26
+ export declare function verifySafeDirectory(path: string, options?: PosixOwnerOnlyPathAsyncOptions): Promise<void>;
27
+ /** Repair only the specific owned secret file supplied by a caller, never an existing directory tree. */
28
+ export declare function verifyPrivate(path: string, kind: Kind, migrate?: boolean): Promise<void>;
29
+ /** Create missing components privately; inspect existing ancestors without modifying them. */
30
+ export declare function ensurePrivateDirectory(path: string, _migrate?: boolean, options?: PosixOwnerOnlyPathAsyncOptions): Promise<void>;
31
+ export declare function readPrivateText(path: string, migrate?: boolean): Promise<string>;
32
+ export declare function readPrivateBuffer(path: string, migrate?: boolean): Promise<Buffer>;
33
+ export declare function createPrivateBytes(path: string, bytes: string): Promise<void>;
34
+ export declare function createPrivateDirectoryExclusive(path: string): Promise<void>;
35
+ export interface ReplaceOptions {
36
+ beforeRename?: () => Promise<void>;
37
+ /** Absolute monotonic deadline, for an expiring lease. */
38
+ deadline?: number;
39
+ now?: () => number;
40
+ sleep?: (ms: number) => Promise<void>;
41
+ rename?: typeof rename;
42
+ }
43
+ /** Retry Windows rename sharing/access errors within a budget; they never prove lease contention. */
44
+ export declare function replaceWindowsFile(from: string, to: string, options?: ReplaceOptions): Promise<{
45
+ attempts: number;
46
+ elapsedMs: number;
47
+ }>;
48
+ export interface PrivateWriteOutcome {
49
+ path: string;
50
+ stage: string;
51
+ published: boolean;
52
+ fileDurable: boolean;
53
+ durabilityConfirmed: boolean;
54
+ attempts: number;
55
+ elapsedMs: number;
56
+ code?: string;
57
+ win32Code?: number;
58
+ }
59
+ export declare function writePrivateAtomic(path: string, bytes: string, options?: ReplaceOptions & {
60
+ stagePath?: string;
61
+ onRenamed?: () => void;
62
+ durable?: boolean;
63
+ durability?: 'file-only-on-windows';
64
+ onOutcome?: (outcome: PrivateWriteOutcome) => void;
65
+ }): Promise<void>;
66
+ export {};
@@ -0,0 +1,302 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { lstat, readFile, rename, unlink } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ export const windowsPrivateFiles = process.platform === 'win32';
5
+ const migrationNotices = new Set();
6
+ function ioCode(detail) {
7
+ // Native errors retain their Win32 code. Only demonstrated absence/exclusive
8
+ // collisions are normalized; access/sharing failures never become lost races.
9
+ if (detail?.reason === 'io-error') {
10
+ if (detail.win32Code === 2 || detail.win32Code === 3)
11
+ return 'ENOENT';
12
+ if (detail.win32Code === 80 || detail.win32Code === 183)
13
+ return 'EEXIST';
14
+ }
15
+ return detail?.code ?? 'ERR_PRIVATE_FILE';
16
+ }
17
+ // Production loads this package only on Windows; POSIX keeps its own directory policy.
18
+ let backend;
19
+ /** Tests inject a backend here; production always imports the private-files package. */
20
+ export function setPrivateFilesBackendForTest(api) {
21
+ backend = api ? Promise.resolve(api) : undefined;
22
+ }
23
+ function loadBackend() {
24
+ backend ??= import('@cortexkit/private-files');
25
+ return backend;
26
+ }
27
+ export class PrivateFileError extends Error {
28
+ path;
29
+ stage;
30
+ code;
31
+ win32Code;
32
+ reason;
33
+ published = false;
34
+ durabilityConfirmed = false;
35
+ fileDurable = false;
36
+ attempts = 0;
37
+ elapsedMs = 0;
38
+ orphanPath;
39
+ constructor(path, stage, cause) {
40
+ const detail = cause;
41
+ super(`Private file refused at ${stage}: ${path} (${detail?.reason ?? detail?.code ?? 'io-error'}). Use a safe parent directory. Repair only a named secret file in safe ancestry whose owner the published policy trusts, without changing its owner or any existing directory. Previously exposed credentials may need rotation. Windows requires the private-files native prebuild and an ACL-persisting local filesystem such as NTFS.`, { cause });
42
+ this.path = path;
43
+ this.stage = stage;
44
+ this.name = 'PrivateFileError';
45
+ this.code = ioCode(detail);
46
+ this.win32Code = detail?.win32Code;
47
+ this.reason = detail?.reason;
48
+ }
49
+ }
50
+ export async function privateOperation(path, stage, operation) {
51
+ try {
52
+ return await operation();
53
+ }
54
+ catch (cause) {
55
+ if (cause instanceof PrivateFileError)
56
+ throw cause;
57
+ throw new PrivateFileError(path, stage, cause);
58
+ }
59
+ }
60
+ // Tests inject metadata into safe-parent checks, including checks deep inside
61
+ // callers that take no policy options. Production never sets this.
62
+ let safeParentOptionsForTest;
63
+ /** Tests only: policy options used by safe-parent checks that pass none. */
64
+ export function setSafeParentOptionsForTest(options) {
65
+ safeParentOptionsForTest = options;
66
+ }
67
+ /** Existing containing directories use the shared entry-integrity policy, not secret-file permissions. */
68
+ export async function verifySafeDirectory(path, options) {
69
+ path = resolve(path);
70
+ options ??= safeParentOptionsForTest;
71
+ await privateOperation(path, 'safe-parent', async () => {
72
+ const result = await (await loadBackend()).verifySafeParent(path, options);
73
+ if (result.ok === false) {
74
+ // Preserve an absent parent's ENOENT so callers can create it privately.
75
+ if (result.reason === 'io-error' || result.reason === 'wrong-kind')
76
+ await lstat(path);
77
+ throw new PrivateFileError(result.level ?? result.path, 'safe-parent', {
78
+ ...result,
79
+ code: 'ERR_PRIVATE_FILES_SECURITY',
80
+ });
81
+ }
82
+ });
83
+ }
84
+ /** Repair only the specific owned secret file supplied by a caller, never an existing directory tree. */
85
+ export async function verifyPrivate(path, kind, migrate = false) {
86
+ path = resolve(path);
87
+ if (kind === 'directory')
88
+ return verifySafeDirectory(path);
89
+ await privateOperation(path, 'verify', async () => {
90
+ const api = await loadBackend();
91
+ const result = await api.verifyOwnerOnly(path);
92
+ let repaired = false;
93
+ if (!result.ok) {
94
+ const { reason } = result.refusal;
95
+ // The result API intentionally omits native IO codes. Use the throwing
96
+ // API to preserve absence/collision details, never infer them from text.
97
+ if (reason === 'io-error')
98
+ await api.verifyPrivatePath(path, kind);
99
+ // DACL replacement repairs permissions only, never ownership or unsafe ancestry.
100
+ if (migrate &&
101
+ [
102
+ api.EXTRA_ACE,
103
+ api.NOT_PROTECTED,
104
+ api.NULL_DACL,
105
+ api.EMPTY_DACL,
106
+ ].includes(reason)) {
107
+ await verifySafeDirectory(dirname(path));
108
+ await api.securePrivatePath(path, kind);
109
+ repaired = true;
110
+ }
111
+ else {
112
+ throw Object.assign(new Error('Owner-only verification refused'), {
113
+ reason,
114
+ code: 'ERR_PRIVATE_FILES_SECURITY',
115
+ });
116
+ }
117
+ }
118
+ // Verify the expected object kind and its permissions by handle after repair.
119
+ await api.verifyPrivatePath(path, kind);
120
+ if (repaired && !migrationNotices.has(path)) {
121
+ migrationNotices.add(path);
122
+ process.emitWarning(`Private permissions tightened: ${path}. This object may have been readable before; rotate credentials if needed.`, 'PrivateFileMigration');
123
+ }
124
+ });
125
+ }
126
+ /** Create missing components privately; inspect existing ancestors without modifying them. */
127
+ export async function ensurePrivateDirectory(path, _migrate = false, options) {
128
+ path = resolve(path);
129
+ await privateOperation(path, 'directory', async () => {
130
+ const api = await loadBackend();
131
+ try {
132
+ await lstat(path);
133
+ return await verifySafeDirectory(path, options);
134
+ }
135
+ catch (error) {
136
+ if (ioCode(error) !== 'ENOENT')
137
+ throw error;
138
+ }
139
+ const parent = dirname(path);
140
+ if (parent === path)
141
+ throw Object.assign(new Error('Missing root'), { code: 'ENOENT' });
142
+ await ensurePrivateDirectory(parent, false, options);
143
+ try {
144
+ await api.createOwnerOnly(path, { kind: 'dir' });
145
+ }
146
+ catch (error) {
147
+ if (ioCode(error) !== 'EEXIST')
148
+ throw error;
149
+ }
150
+ await verifySafeDirectory(path, options);
151
+ });
152
+ }
153
+ export async function readPrivateText(path, migrate = false) {
154
+ if (!windowsPrivateFiles)
155
+ return readFile(path, 'utf8');
156
+ return (await readPrivateBuffer(path, migrate)).toString('utf8');
157
+ }
158
+ export async function readPrivateBuffer(path, migrate = false) {
159
+ if (!windowsPrivateFiles)
160
+ return readFile(path);
161
+ path = resolve(path);
162
+ if (migrate) {
163
+ await verifySafeDirectory(dirname(path));
164
+ await verifyPrivate(path, 'file', true);
165
+ }
166
+ return privateOperation(path, 'read', async () => (await loadBackend()).readOwnerOnly(path));
167
+ }
168
+ export async function createPrivateBytes(path, bytes) {
169
+ await verifySafeDirectory(dirname(resolve(path)));
170
+ await privateOperation(path, 'create', async () => {
171
+ // The backend verifies the creation handle while empty, before consuming bytes.
172
+ await (await loadBackend()).createPrivateFile(resolve(path), bytes);
173
+ });
174
+ }
175
+ export async function createPrivateDirectoryExclusive(path) {
176
+ await verifySafeDirectory(dirname(resolve(path)));
177
+ await privateOperation(path, 'directory-create', async () => {
178
+ await (await loadBackend()).createOwnerOnly(resolve(path), { kind: 'dir' });
179
+ });
180
+ }
181
+ /** Retry Windows rename sharing/access errors within a budget; they never prove lease contention. */
182
+ export async function replaceWindowsFile(from, to, options = {}) {
183
+ from = resolve(from);
184
+ to = resolve(to);
185
+ const now = options.now ?? (() => performance.now());
186
+ const start = now();
187
+ const deadline = Math.min(start + 500, options.deadline ?? Infinity);
188
+ const delays = [0, 10, 20, 40, 80, 100];
189
+ let original;
190
+ let attempts = 0;
191
+ for (const delay of delays) {
192
+ if (now() >= deadline)
193
+ break;
194
+ if (delay)
195
+ await (options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(Math.min(delay, Math.max(0, deadline - now())));
196
+ if (now() >= deadline)
197
+ break;
198
+ await options.beforeRename?.();
199
+ if (now() >= deadline)
200
+ break;
201
+ attempts++;
202
+ try {
203
+ await (options.rename ?? rename)(from, to);
204
+ return { attempts, elapsedMs: now() - start };
205
+ }
206
+ catch (error) {
207
+ original ??= error;
208
+ const detail = error;
209
+ const candidate = detail.win32Code !== undefined
210
+ ? detail.win32Code === 32 || detail.win32Code === 33
211
+ : ['EBUSY', 'EPERM', 'EACCES'].includes(detail.code ?? '') &&
212
+ !detail.reason;
213
+ if (!candidate)
214
+ break;
215
+ }
216
+ }
217
+ const error = new PrivateFileError(to, 'rename', original ??
218
+ Object.assign(new Error('Publication deadline expired'), {
219
+ code: 'ETIMEDOUT',
220
+ }));
221
+ error.attempts = attempts;
222
+ error.elapsedMs = now() - start;
223
+ throw error;
224
+ }
225
+ export async function writePrivateAtomic(path, bytes, options = {}) {
226
+ path = resolve(path);
227
+ await ensurePrivateDirectory(dirname(path));
228
+ try {
229
+ await verifyPrivate(path, 'file', true);
230
+ }
231
+ catch (error) {
232
+ if (error.code !== 'ENOENT')
233
+ throw error;
234
+ }
235
+ const stage = resolve(options.stagePath ?? `${path}.${randomUUID()}.tmp`);
236
+ if (dirname(stage) !== dirname(path))
237
+ throw new PrivateFileError(path, 'stage-directory', { code: 'EXDEV' });
238
+ let created = false;
239
+ let published = false;
240
+ let outcome = { attempts: 0, elapsedMs: 0 };
241
+ let stageName = 'stage-create';
242
+ const reportOutcome = (error) => {
243
+ try {
244
+ options.onOutcome?.({
245
+ path,
246
+ stage: error?.stage ?? 'published',
247
+ published,
248
+ fileDurable: published,
249
+ durabilityConfirmed: false,
250
+ attempts: error?.attempts ?? outcome.attempts,
251
+ elapsedMs: error?.elapsedMs ?? outcome.elapsedMs,
252
+ ...(error && { code: error.code, win32Code: error.win32Code }),
253
+ });
254
+ }
255
+ catch {
256
+ // Outcome observers cannot change whether publication succeeded.
257
+ }
258
+ };
259
+ try {
260
+ await createPrivateBytes(stage, bytes);
261
+ created = true;
262
+ stageName = 'before-rename';
263
+ outcome = await replaceWindowsFile(stage, path, options);
264
+ created = false;
265
+ published = true;
266
+ stageName = 'published-callback';
267
+ options.onRenamed?.();
268
+ stageName = 'published-verify';
269
+ await verifyPrivate(path, 'file');
270
+ // createPrivateFile flushes the file. Directory metadata durability has not
271
+ // been qualified on Windows, so never promise the POSIX directory fsync.
272
+ if (options.durable && options.durability !== 'file-only-on-windows')
273
+ throw new PrivateFileError(path, 'directory-sync-unsupported', {
274
+ code: 'ENOTSUP',
275
+ });
276
+ reportOutcome();
277
+ }
278
+ catch (cause) {
279
+ const error = cause instanceof PrivateFileError
280
+ ? cause
281
+ : new PrivateFileError(path, stageName, cause);
282
+ error.published = published;
283
+ error.fileDurable = published;
284
+ if (published)
285
+ Object.assign(error, outcome);
286
+ if (created) {
287
+ try {
288
+ await unlink(stage);
289
+ }
290
+ catch {
291
+ error.orphanPath = stage;
292
+ }
293
+ created = false;
294
+ }
295
+ reportOutcome(error);
296
+ throw error;
297
+ }
298
+ finally {
299
+ if (created)
300
+ await unlink(stage).catch(() => { });
301
+ }
302
+ }
@@ -29,5 +29,5 @@ export declare function acquireRefreshFileLock(options: {
29
29
  onContended?: () => void;
30
30
  /** Synchronous, never awaited; throws and returned promise/thenable rejections are isolated. */
31
31
  onRenewalTimer?: (event: 'scheduled' | 'cancelled') => void;
32
- onStep?: (step: 'stale-marker-stat' | 'stale-marker-claimed' | 'stale-lock-observed' | 'stale-lock-confirmed' | 'stale-lock-removed' | 'stale-lock-recreated' | 'eviction-marker-acquired' | 'renewal-owner-confirmed' | 'renewal-marker-unavailable' | 'renewal-write-fenced' | 'renewal-write-ready' | 'relinquish-read' | 'renewal-finished' | 'release-owner-confirmed') => void | Promise<void>;
32
+ onStep?: (step: 'stale-marker-stat' | 'stale-marker-claimed' | 'stale-lock-observed' | 'lock-read-ready' | 'lock-missing' | 'stale-lock-confirmed' | 'stale-lock-removed' | 'stale-lock-recreated' | 'eviction-marker-acquired' | 'renewal-owner-confirmed' | 'renewal-marker-unavailable' | 'renewal-write-fenced' | 'renewal-write-ready' | 'renewal-write-finished' | 'relinquish-read' | 'renewal-finished' | 'release-owner-confirmed') => void | Promise<void>;
33
33
  }): Promise<RefreshFileLock | null>;