File size: 1,571 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import { randomBytes } from 'node:crypto';
import {
  chmod,
  mkdir,
  open,
  readFile,
  rename,
  rm,
  stat,
} from 'node:fs/promises';
import { dirname } from 'node:path';

export class PrivateFileTooPermissiveError extends Error {
  readonly code = 'EPRIVATE_FILE_TOO_PERMISSIVE';

  constructor(
    readonly filePath: string,
    readonly mode: number,
  ) {
    super(
      `private file ${filePath} is too permissive (mode ${mode.toString(8).padStart(3, '0')}); expected 0600`,
    );
    this.name = 'PrivateFileTooPermissiveError';
  }
}

export async function writePrivateFile(
  filePath: string,
  data: string | Buffer,
): Promise<void> {
  const dir = dirname(filePath);
  await mkdir(dir, { recursive: true, mode: 0o700 });
  await chmod(dir, 0o700);

  const tmp = `${filePath}.tmp.${randomBytes(8).toString('hex')}`;

  let handle: Awaited<ReturnType<typeof open>> | undefined;
  try {
    handle = await open(tmp, 'w', 0o600);
    await handle.chmod(0o600);
    await handle.writeFile(data);
    await handle.sync();
    await handle.close();
    handle = undefined;
    await rename(tmp, filePath);
  } catch (err) {
    if (handle) {
      await handle.close().catch(() => {});
    }
    await rm(tmp, { force: true }).catch(() => {});
    throw err;
  }
}

export async function readPrivateFile(filePath: string): Promise<Buffer> {
  const info = await stat(filePath);
  if (process.platform !== 'win32' && (info.mode & 0o077) !== 0) {
    throw new PrivateFileTooPermissiveError(filePath, info.mode & 0o777);
  }
  return readFile(filePath);
}