@crewhaus/secrets-manager 0.1.3 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/secrets-manager",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Pluggable secrets backend with rotation + audit-log integration: env-var, file, vault",
6
6
  "main": "src/index.ts",
@@ -12,9 +12,9 @@
12
12
  "test": "bun test src"
13
13
  },
14
14
  "dependencies": {
15
- "@crewhaus/audit-log": "0.1.3",
16
- "@crewhaus/errors": "0.1.3",
17
- "@crewhaus/logging": "0.1.3"
15
+ "@crewhaus/audit-log": "0.1.4",
16
+ "@crewhaus/errors": "0.1.4",
17
+ "@crewhaus/logging": "0.1.4"
18
18
  },
19
19
  "license": "Apache-2.0",
20
20
  "author": {
@@ -54,6 +54,9 @@ mock.module("node:fs", () => ({
54
54
  }
55
55
  fsState.renames.push({ from, to });
56
56
  },
57
+ mkdirSync: (p: string, _opts?: { recursive?: boolean; mode?: number }) => {
58
+ fsState.dirs.add(p);
59
+ },
57
60
  readdirSync: (p: string) => {
58
61
  if (!fsState.dirs.has(p)) {
59
62
  const err = new Error(`ENOENT: no such dir ${p}`) as NodeJS.ErrnoException;
@@ -152,6 +155,18 @@ describe("file backend — rotate()", () => {
152
155
  expect(backend.rotate("path/with/slash")).rejects.toBeInstanceOf(SecretsError);
153
156
  expect(fsState.writes.length).toBe(0);
154
157
  });
158
+
159
+ test("creates the root dir on first rotate instead of failing with ENOENT", async () => {
160
+ const freshRoot = "/fake/brand-new/secrets";
161
+ expect(fsState.dirs.has(freshRoot)).toBe(false);
162
+ const backend = createFileBackend({ rootDir: freshRoot });
163
+
164
+ const v = await backend.rotate("TOKEN", { newValue: "fresh" });
165
+
166
+ expect(v).toBe("fresh");
167
+ expect(fsState.dirs.has(freshRoot)).toBe(true);
168
+ expect(fsState.files.get(`${freshRoot}/TOKEN`)?.content).toBe("fresh");
169
+ });
155
170
  });
156
171
 
157
172
  describe("file backend — list()", () => {
@@ -6,7 +6,14 @@
6
6
  * trailing newline-stripping ambiguity). Whitespace is preserved as-is
7
7
  * for tokens that may legitimately contain it.
8
8
  */
9
- import { existsSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ readdirSync,
14
+ renameSync,
15
+ writeFileSync,
16
+ } from "node:fs";
10
17
  import { join } from "node:path";
11
18
  import { type SecretValue, type SecretsBackend, SecretsError } from "../index";
12
19
 
@@ -37,6 +44,11 @@ export function createFileBackend(opts: FileBackendOptions): SecretsBackend {
37
44
  async rotate(name: string, rotateOpts): Promise<SecretValue> {
38
45
  const p = pathFor(name);
39
46
  const newValue = rotateOpts?.newValue ?? generateRandomSecret();
47
+ // Create the secrets root on first use so a fresh checkout can rotate a
48
+ // secret without a raw ENOENT — recursive mkdir is a no-op when it
49
+ // already exists. 0o700 keeps the directory owner-only, consistent with
50
+ // the 0o600 secret files written into it.
51
+ mkdirSync(rootDir, { recursive: true, mode: 0o700 });
40
52
  const tmp = `${p}.tmp`;
41
53
  writeFileSync(tmp, newValue, { encoding: "utf8", mode: 0o600 });
42
54
  renameSync(tmp, p);