@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.
@@ -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
@@ -1,4 +1,5 @@
1
1
  export * from './atomic-write.js';
2
+ export * from './file-permissions.js';
2
3
  export * from './project-endpoint.js';
3
4
  export * from './socket-path.js';
4
5
  //# sourceMappingURL=index.d.ts.map
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/persistence",
3
- "version": "0.306.0",
3
+ "version": "0.306.2",
4
4
  "license": "MIT",
5
5
  "description": "Dependency-free filesystem persistence primitives shared across WrongStack packages.",
6
6
  "repository": {