@cortexkit/common-auth 0.9.1 → 0.9.2

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.
@@ -271,18 +271,22 @@ async function writeStateAtomic(path, state) {
271
271
  }
272
272
  const temporary = join(parent, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
273
273
  let descriptor;
274
+ let created = false;
274
275
  try {
275
276
  descriptor = await open(temporary, 'wx', 0o600);
277
+ created = true;
276
278
  await descriptor.writeFile(bytes, 'utf8');
279
+ await descriptor.chmod(0o600);
277
280
  await descriptor.sync();
278
281
  await descriptor.close();
279
282
  descriptor = undefined;
280
- await chmod(temporary, 0o600);
281
283
  await rename(temporary, path);
284
+ created = false;
282
285
  }
283
286
  finally {
284
287
  await descriptor?.close().catch(() => { });
285
- await unlink(temporary).catch(() => { });
288
+ if (created)
289
+ await unlink(temporary).catch(() => { });
286
290
  }
287
291
  }
288
292
  export async function readClaustrumEnrollmentStatus(paths, proposedName) {
@@ -59,6 +59,8 @@ export interface DumpOptions {
59
59
  * files that were written stay.
60
60
  */
61
61
  cleanupFailedDumps?: boolean;
62
+ /** Test seam for forcing response staging-name collisions. */
63
+ stageName?: () => string;
62
64
  }
63
65
  export interface DumpInput {
64
66
  /** The session the request belongs to; missing means `session-unknown`. */
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { chmod, lstat, mkdir, readdir, readFile, rename, unlink, writeFile, } from 'node:fs/promises';
2
+ import { chmod, lstat, mkdir, open, readdir, readFile, rename, unlink, writeFile, } from 'node:fs/promises';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { createRedactor } from '../logger/redact.js';
5
5
  const PREVIOUS_BODY_LIMIT = 100;
@@ -132,21 +132,23 @@ export async function sweepDumpDirectory(options) {
132
132
  * reader never sees half a file and a symlink planted at a predictable
133
133
  * staging name is never followed.
134
134
  */
135
- async function replaceFile(path, text) {
135
+ async function replaceFile(path, text, stageName) {
136
136
  for (let attempt = 0;; attempt += 1) {
137
- const staging = `${path}.${randomBytes(12).toString('hex')}.partial`;
137
+ const staging = `${path}.${stageName ? stageName() : randomBytes(12).toString('hex')}.partial`;
138
138
  let created = false;
139
+ let handle;
139
140
  try {
140
- await writeFile(staging, text, {
141
- encoding: 'utf8',
142
- mode: 0o600,
143
- flag: 'wx',
144
- });
141
+ handle = await open(staging, 'wx', 0o600);
145
142
  created = true;
143
+ await handle.writeFile(text, 'utf8');
144
+ await handle.chmod(0o600);
145
+ await handle.close();
146
+ handle = undefined;
146
147
  await rename(staging, path);
147
148
  return;
148
149
  }
149
150
  catch (error) {
151
+ await handle?.close().catch(() => { });
150
152
  if (created)
151
153
  await unlink(staging).catch(() => { });
152
154
  const code = error.code;
@@ -500,7 +502,7 @@ export function createDumper(options) {
500
502
  complete: input.complete ?? true,
501
503
  });
502
504
  try {
503
- await replaceFile(result.responseFile, `${JSON.stringify(artifact, null, 2)}\n`);
505
+ await replaceFile(result.responseFile, `${JSON.stringify(artifact, null, 2)}\n`, options.stageName);
504
506
  return result.responseFile;
505
507
  }
506
508
  catch (error) {
@@ -1,5 +1,7 @@
1
1
  export interface AtomicWriteOptions {
2
2
  serialize?: (value: unknown) => string;
3
3
  beforeRename?: () => Promise<void>;
4
+ /** Test seam for forcing staging-name collisions; defaults to randomUUID. */
5
+ stageName?: () => string;
4
6
  }
5
7
  export declare function writeJsonAtomic(path: string, value: unknown, options?: AtomicWriteOptions): Promise<void>;
@@ -1,22 +1,28 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { mkdir, open, rename, rm } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
4
  export async function writeJsonAtomic(path, value, options = {}) {
5
5
  await mkdir(dirname(path), { recursive: true });
6
- const tempPath = `${path}.${randomUUID()}.tmp`;
6
+ const tempPath = `${path}.${(options.stageName ?? randomUUID)()}.tmp`;
7
+ let handle;
8
+ let created = false;
7
9
  try {
8
- await writeFile(tempPath, options.serialize
10
+ handle = await open(tempPath, 'wx', 0o600);
11
+ created = true;
12
+ await handle.writeFile(options.serialize
9
13
  ? options.serialize(value)
10
- : `${JSON.stringify(value, null, 2)}\n`, {
11
- encoding: 'utf8',
12
- mode: 0o600,
13
- });
14
+ : `${JSON.stringify(value, null, 2)}\n`, 'utf8');
15
+ await handle.chmod(0o600);
16
+ await handle.close();
17
+ handle = undefined;
14
18
  await options.beforeRename?.();
15
19
  await rename(tempPath, path);
20
+ created = false;
16
21
  }
17
- catch (error) {
18
- // A failed write can leave partial staging bytes just like a failed rename.
19
- await rm(tempPath, { force: true }).catch(() => { });
20
- throw error;
22
+ finally {
23
+ await handle?.close().catch(() => { });
24
+ // A failed write can leave partial bytes, but a collision is not ours to remove.
25
+ if (created)
26
+ await rm(tempPath, { force: true }).catch(() => { });
21
27
  }
22
28
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { mkdir, open, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { LockOwnershipError, lockPathFor } from './with-lock.js';
5
5
  const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
@@ -67,12 +67,22 @@ export async function acquireRefreshFileLock(options) {
67
67
  async function writeOwner() {
68
68
  // Readers must see a complete lease, even while renewal is writing.
69
69
  const tempPath = `${lockPath}.${randomUUID()}.tmp`;
70
+ let handle;
71
+ let created = false;
70
72
  try {
71
- await writeFile(tempPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
73
+ handle = await open(tempPath, 'wx', 0o600);
74
+ created = true;
75
+ await handle.writeFile(`${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, 'utf8');
76
+ await handle.chmod(0o600);
77
+ await handle.close();
78
+ handle = undefined;
72
79
  await rename(tempPath, lockPath);
80
+ created = false;
73
81
  }
74
82
  finally {
75
- await rm(tempPath, { force: true }).catch(() => { });
83
+ await handle?.close().catch(() => { });
84
+ if (created)
85
+ await rm(tempPath, { force: true }).catch(() => { });
76
86
  }
77
87
  }
78
88
  async function tryAcquire() {
@@ -15,6 +15,8 @@ export declare function writePortFile(dir: string, entry: {
15
15
  }, options?: {
16
16
  secureDir?: boolean;
17
17
  beforeWrite?: () => void | Promise<void>;
18
+ /** Test seam for forcing staging-name collisions; defaults to randomUUID. */
19
+ stageName?: () => string;
18
20
  }): Promise<string>;
19
21
  export declare function sweepRpcState(root: string, activeDir: string, isManagedDir: (name: string) => boolean, log?: RpcLogChannel): Promise<void>;
20
22
  export interface DiscoverPortFileOptions {
@@ -1,5 +1,5 @@
1
- import { createHash } from 'node:crypto';
2
- import { chmod, mkdir, readdir, readFile, rename, rmdir, unlink, writeFile, } from 'node:fs/promises';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, open, readdir, readFile, rename, rmdir, unlink, } from 'node:fs/promises';
3
3
  import { join, resolve } from 'node:path';
4
4
  /**
5
5
  * A PID a liveness probe may address. `process.kill(0 | negative, 0)` signals
@@ -63,7 +63,7 @@ async function removeCorruptPortFile(portFile, log) {
63
63
  }
64
64
  export async function writePortFile(dir, entry, options = {}) {
65
65
  // The directory can be removed by another project's sweep between our
66
- // mkdir and the first writeFile/rename, so the whole create-then-rename
66
+ // mkdir and the first open/rename, so the whole create-then-rename
67
67
  // unit is retried once on ENOENT. The retry recreates the directory; a
68
68
  // persistent ENOENT (e.g. permission, read-only parent) will surface on
69
69
  // the second attempt — failing fast beats an unbounded loop.
@@ -77,18 +77,24 @@ export async function writePortFile(dir, entry, options = {}) {
77
77
  await options.beforeWrite?.();
78
78
  const full = { ...entry, startedAt: Date.now() };
79
79
  const target = join(dir, `port-${entry.pid}.json`);
80
- const tmp = `${target}.${process.pid}.tmp`;
80
+ const tmp = `${target}.${(options.stageName ?? randomUUID)()}.tmp`;
81
+ let handle;
82
+ let created = false;
81
83
  try {
82
- await writeFile(tmp, JSON.stringify(full), {
83
- encoding: 'utf8',
84
- mode: 0o600,
85
- });
84
+ handle = await open(tmp, 'wx', 0o600);
85
+ created = true;
86
+ await handle.writeFile(JSON.stringify(full), 'utf8');
87
+ await handle.chmod(0o600);
88
+ await handle.close();
89
+ handle = undefined;
86
90
  await rename(tmp, target);
91
+ created = false;
87
92
  }
88
- catch (error) {
89
- // The staged file carries the server token; never leave it behind.
90
- await unlink(tmp).catch(() => { });
91
- throw error;
93
+ finally {
94
+ await handle?.close().catch(() => { });
95
+ // Remove token-bearing staging bytes only if this call created them.
96
+ if (created)
97
+ await unlink(tmp).catch(() => { });
92
98
  }
93
99
  return target;
94
100
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/common-auth",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Shared code for the CortexKit auth plugins: account pool, quota and routing, commands and auth menu, OpenCode 2 hooks, Claustrum custody, and plumbing (loopback RPC, file locks, logger, sidebar state, TUI preferences and build).",
5
5
  "license": "MIT",
6
6
  "repository": {