@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 +4 -4
- package/src/backends/file.test.ts +15 -0
- package/src/backends/file.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/secrets-manager",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
16
|
-
"@crewhaus/errors": "0.1.
|
|
17
|
-
"@crewhaus/logging": "0.1.
|
|
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()", () => {
|
package/src/backends/file.ts
CHANGED
|
@@ -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 {
|
|
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);
|