@wrongstack/persistence 0.306.0 → 0.306.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.
- package/dist/file-permissions.d.ts +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-only file hardening, on both POSIX and Windows.
|
|
3
|
+
*
|
|
4
|
+
* WS-045: this lived as a private function inside `secret-vault.ts` and was
|
|
5
|
+
* therefore reachable only by the config file. Every other secret WrongStack
|
|
6
|
+
* writes — the HQ `auth.json` (bearer tokens), `runtime.json` (local client
|
|
7
|
+
* token), the vault `.key` — got `chmod(0o600)` at best, which on Windows is
|
|
8
|
+
* a no-op for everything except the read-only bit: the file stayed readable by
|
|
9
|
+
* every other account on the machine. One exported module so a new secret file
|
|
10
|
+
* has an obvious thing to call.
|
|
11
|
+
*
|
|
12
|
+
* ## Why it lives in `@wrongstack/persistence`
|
|
13
|
+
*
|
|
14
|
+
* It started in `@wrongstack/core/security`, which put it out of reach of the
|
|
15
|
+
* one project daemon that needed it most: `@wrongstack/kanban` depends only on
|
|
16
|
+
* this package, and it cannot depend on core — core already depends on kanban,
|
|
17
|
+
* so that edge would close a cycle. Every project daemon writes a metadata
|
|
18
|
+
* file holding its per-process IPC auth token, and every one of them needs the
|
|
19
|
+
* same owner-only guarantee, so the helper belongs next to `atomicWrite`: a
|
|
20
|
+
* dependency-free filesystem primitive that all of core, kanban, sage and
|
|
21
|
+
* tools already import. `@wrongstack/core/security` re-exports it, so existing
|
|
22
|
+
* callers are unchanged.
|
|
23
|
+
*
|
|
24
|
+
* @module file-permissions
|
|
25
|
+
*/
|
|
26
|
+
/** Owner read/write, nothing for group or other. */
|
|
27
|
+
export declare const SECRET_FILE_MODE = 384;
|
|
28
|
+
/** Owner-only directory: enter + list + write. */
|
|
29
|
+
export declare const SECRET_DIR_MODE = 448;
|
|
30
|
+
export interface RestrictPermissionsOptions {
|
|
31
|
+
warn?: ((msg: string) => void) | undefined;
|
|
32
|
+
/** Label used in warnings, e.g. `secret-vault` or `hq-auth`. */
|
|
33
|
+
label?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Restrict a file to owner-only access.
|
|
37
|
+
*
|
|
38
|
+
* POSIX: `chmod 0600`. Windows: `chmod` cannot express this, so we use
|
|
39
|
+
* `icacls` to drop inherited ACEs and grant the current user alone.
|
|
40
|
+
*
|
|
41
|
+
* Failures are warned, never thrown — a hardening step must not be able to
|
|
42
|
+
* block the write it protects, and `icacls` is absent in some minimal
|
|
43
|
+
* environments (containers, WINE, restricted CI images).
|
|
44
|
+
*/
|
|
45
|
+
export declare function restrictFilePermissions(filePath: string, opts?: RestrictPermissionsOptions): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* `restrictFilePermissions` for a directory: `chmod 0700` on POSIX, the same
|
|
48
|
+
* icacls treatment (applied recursively to new children via inheritance) on
|
|
49
|
+
* Windows. Use on the directory that holds secret files so a file created
|
|
50
|
+
* there by a path we do not control still lands owner-only.
|
|
51
|
+
*/
|
|
52
|
+
export declare function restrictDirPermissions(dirPath: string, opts?: RestrictPermissionsOptions): Promise<void>;
|
|
53
|
+
//# sourceMappingURL=file-permissions.d.ts.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -239,6 +239,58 @@ var atomicWrite = defaultPrimitives.atomicWrite;
|
|
|
239
239
|
var ensureDir = defaultPrimitives.ensureDir;
|
|
240
240
|
var withFileLock = defaultPrimitives.withFileLock;
|
|
241
241
|
|
|
242
|
+
// src/file-permissions.ts
|
|
243
|
+
import { chmod as chmod2 } from "node:fs/promises";
|
|
244
|
+
var SECRET_FILE_MODE = 384;
|
|
245
|
+
var SECRET_DIR_MODE = 448;
|
|
246
|
+
async function restrictFilePermissions(filePath, opts) {
|
|
247
|
+
const label = opts?.label ?? "file-permissions";
|
|
248
|
+
const warn = opts?.warn ?? ((msg) => console.warn(msg));
|
|
249
|
+
if (process.platform === "win32") {
|
|
250
|
+
try {
|
|
251
|
+
const { execFile } = await import("node:child_process");
|
|
252
|
+
const { promisify } = await import("node:util");
|
|
253
|
+
const execFileAsync = promisify(execFile);
|
|
254
|
+
const user = windowsAccountName();
|
|
255
|
+
if (!user) {
|
|
256
|
+
warn(
|
|
257
|
+
`[${label}] Could not determine the current Windows user for ${filePath}; skipping icacls hardening.`
|
|
258
|
+
);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await execFileAsync("icacls", [filePath, "/inheritance:r", "/grant:r", `${user}:(F)`], {
|
|
262
|
+
windowsHide: true
|
|
263
|
+
});
|
|
264
|
+
} catch {
|
|
265
|
+
warn(
|
|
266
|
+
`[${label}] Could not restrict permissions on ${filePath} \u2014 it may be readable by other users on this system.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
try {
|
|
271
|
+
await chmod2(filePath, SECRET_FILE_MODE);
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function restrictDirPermissions(dirPath, opts) {
|
|
277
|
+
if (process.platform === "win32") {
|
|
278
|
+
await restrictFilePermissions(dirPath, opts);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
await chmod2(dirPath, SECRET_DIR_MODE);
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function windowsAccountName() {
|
|
287
|
+
const username = process.env.USERNAME || process.env.USER;
|
|
288
|
+
if (!username || username.includes("\0")) return void 0;
|
|
289
|
+
const domain = process.env.USERDOMAIN;
|
|
290
|
+
if (domain && !domain.includes("\0")) return `${domain}\\${username}`;
|
|
291
|
+
return username;
|
|
292
|
+
}
|
|
293
|
+
|
|
242
294
|
// src/project-endpoint.ts
|
|
243
295
|
import * as fsPromises from "node:fs/promises";
|
|
244
296
|
import * as net from "node:net";
|
|
@@ -367,6 +419,8 @@ async function bindProjectEndpoint(options) {
|
|
|
367
419
|
}
|
|
368
420
|
export {
|
|
369
421
|
PersistenceFsError,
|
|
422
|
+
SECRET_DIR_MODE,
|
|
423
|
+
SECRET_FILE_MODE,
|
|
370
424
|
assertUnixSocketPathWithinLimit,
|
|
371
425
|
atomicWrite,
|
|
372
426
|
bindProjectEndpoint,
|
|
@@ -374,6 +428,8 @@ export {
|
|
|
374
428
|
createPersistencePrimitives,
|
|
375
429
|
ensureDir,
|
|
376
430
|
isProjectEndpointLive,
|
|
431
|
+
restrictDirPermissions,
|
|
432
|
+
restrictFilePermissions,
|
|
377
433
|
unixSocketPathLimit,
|
|
378
434
|
withFileLock
|
|
379
435
|
};
|