@luckystack/secret-manager 0.1.9 → 0.2.1

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.
@@ -1,114 +1,114 @@
1
- # Architecture — secret-manager client + external server
2
-
3
- > The client (`@luckystack/secret-manager`) is implemented and described here. The external **secret-manager server** it talks to lives in a separate repository (`luckystack-secret-manager`); only its wire contract is captured below so the client can be developed against a stable API.
4
-
5
- `@luckystack/secret-manager` is a **wiring client**, not a vault. It resolves committed `.env` pointers to real secrets at boot and writes them into `process.env`. Storage, versioning, auth, and the admin UI all live in the external server.
6
-
7
- ## TL;DR
8
-
9
- - **This package does:** scan `process.env` for pointer-shaped values (`<BASE>_V<n>`), `POST /resolve` them in one request, overwrite each `process.env` entry with the real secret, and (optionally, in dev) re-resolve on `.env` change / on an interval.
10
- - **This package does NOT:** store secrets, version values, render an admin UI, or know what a "secret" is. All of that lives in the external server.
11
- - **The workflow it enables:** developers commit `.env` containing only pointers (`OPENAI_KEY=OPENAI_AUTHORIZATION_KEY_V5`), not real secrets. The server resolves `OPENAI_AUTHORIZATION_KEY_V5` to its actual value at boot.
12
- - **The strict invariant (enforced by the server):** values are **append-only by version**. `..._V5` is immutable; rotating means publishing `..._V6`. Old branches keep working because their committed `.env` still points at `..._V5`.
13
-
14
- ## Why this design
15
-
16
- 1. **`.env` is dangerous to commit, painful to share.** Pointers are safe to commit (they are opaque names, not values), so the team shares one source of truth and nobody leaks a secret into git history.
17
- 2. **Rotation breaks old branches.** With versioned values, a six-month-old branch still references `..._V5` (resolvable until you retire it), while a fresh PR adopts `..._V6`. No "works on main, broken on old branch" surprises.
18
- 3. **One flat keystore, one token.** A single HTTP boundary with one shared bearer token — no per-app vault SDK wiring.
19
-
20
- ## The pointer model
21
-
22
- - A pointer is any `.env` value matching `^(.+)_V(\d+)$` (configurable via `pointerPattern`).
23
- - The env **name** is decoupled from the secret **base name**: `OPENAI_KEY` (name) -> `OPENAI_AUTHORIZATION_KEY_V5` (value) -> base `OPENAI_AUTHORIZATION_KEY`, version `5`.
24
- - The client sends the **full pointer string** to the server and overwrites the env name with the resolved value. The base/version split is the server's job.
25
- - A non-pointer value (`NODE_ENV=production`, or a real secret pasted locally) is a literal: never sent, never overwritten. Local overrides win for free.
26
-
27
- ## Boot flow
28
-
29
- ```
30
- process start
31
- -> dotenv loads .env / .env.local (consumer's server.ts)
32
- -> initSecretManager(...) <-- THIS CLIENT, first line of server.ts
33
- 1. capture { envName -> pointer } from process.env (once)
34
- 2. POST /resolve { keys: [unique pointers] }
35
- 3. overwrite process.env[envName] = values[pointer]
36
- -> framework boot (reads the resolved process.env)
37
- -> server.listen()
38
- ```
39
-
40
- `initSecretManager` runs **before** anything else that reads `process.env` at module-init time.
41
-
42
- ## Modes
43
-
44
- | `source` | Behavior | Writes to `process.env`? |
45
- | --- | --- | --- |
46
- | `'remote'` (default) | Resolve from the server. A missing pointer or fetch error throws — production hard-stop. | After a successful resolve. |
47
- | `'local'` | No network. Pointers untouched. Tests / offline dev. | Never. |
48
- | `'hybrid'` | Try the server; on failure warn and keep local env. | Only on a successful resolve; missing pointers are warned and left as-is. |
49
-
50
- ## Dev hot reload (opt-in, dev-only)
51
-
52
- Set `config.dev` to live-reload while a long-running dev process is up (no-op when `NODE_ENV === 'production'`):
53
-
54
- - `dev.watch` (default `true`) — a debounced `fs.watch` on `dev.envFiles` (default `.env` + `.env.local`). On change the files are **re-parsed and applied**: plain values (`ENVIRONMENT=production`, `PORT=123`) are injected straight into `process.env` (live config reload), and pointer-shaped values are re-resolved against the server. A pointer added or bumped after boot is picked up here — no restart.
55
- - `dev.pollIntervalMs` (default `0`/off) — re-resolve the current pointers every N ms, catching server-side rotations. The interval lives in `config.ts`, changeable in one place.
56
- - `dev.envFiles` — override which files are watched + re-parsed (default `['.env', '.env.local']`).
57
-
58
- The file parse is done by a tiny in-package `.env` parser (standard `KEY=VALUE`, comments, quotes), so the package stays dependency-free. Both channels swallow + warn on a transient error rather than crashing the dev process.
59
-
60
- ## Auth model
61
-
62
- A single shared bearer token accompanies every request:
63
-
64
- ```
65
- Authorization: Bearer <token>
66
- ```
67
-
68
- The token is the only real secret on the developer machine. Keep it in a gitignored single-line file referenced via `token: { fromFile: '.secret-manager-token' }` (read at resolve time, so file rotation is picked up by the next poll). CI runners inject the file from their secret store.
69
-
70
- ## What this package does NOT do
71
-
72
- - **No secret storage.** No on-disk persistence beyond reading the token file. The in-memory cache is plain JS, discarded on exit.
73
- - **No versioning logic.** It sends the full pointer string and writes back whatever the server returns. Version naming + resolution is the server's job.
74
- - **No admin / write operations.** Read-only `POST /resolve`. Publishing new versions happens through the server's own UI.
75
- - **No app-key validation.** Whether `DATABASE_URL` is a valid URL is the app's concern (validate after `initSecretManager` returns).
76
-
77
- ## External server — wire contract (separate repo)
78
-
79
- The server lives in its own repo (`luckystack-secret-manager`). The client only depends on this one endpoint:
80
-
81
- ### `POST /resolve`
82
-
83
- Request (the app sends only the pointers it references):
84
-
85
- ```json
86
- { "keys": ["OPENAI_AUTHORIZATION_KEY_V5", "STRIPE_SECRET_KEY_V2"] }
87
- ```
88
-
89
- Response:
90
-
91
- ```json
92
- { "values": { "OPENAI_AUTHORIZATION_KEY_V5": "sk-...", "STRIPE_SECRET_KEY_V2": "rk-..." } }
93
- ```
94
-
95
- - Auth: `Authorization: Bearer <token>` (the shared token). 401 on mismatch.
96
- - Pointers the server can't resolve are omitted from `values`. The client treats a missing pointer as fatal in `'remote'` mode, and a warning in `'hybrid'`.
97
-
98
- The server additionally exposes admin endpoints (`GET /keys` listing masked values, `POST /keys` appending a new version) used only by its own admin webpage — the client never calls them. The full server + admin-UI implementation lives in the separate, running `luckystack-secret-manager` repo.
99
-
100
- ## Implementation status
101
-
102
- | Piece | Status | Where |
103
- | --- | --- | --- |
104
- | Resolver client (this package) | Implemented | `packages/secret-manager/src/index.ts` |
105
- | local / remote / hybrid modes | Implemented | this doc |
106
- | Opt-in dev hot reload (watch + poll) | Implemented | this doc |
107
- | External secret-manager server (storage, versioning, admin UI, auth) | **Separate repo** | `luckystack-secret-manager` |
108
-
109
- ## Related
110
-
111
- - Function index: `../CLAUDE.md`.
112
- - Consumer quickstart: `../README.md`.
113
- - Framework-wide packaging map: `/docs/PACKAGE_OVERVIEW.md`.
114
- - Architecture deep-dive: `/docs/ARCHITECTURE_SECRET_MANAGER.md`.
1
+ # Architecture — secret-manager client + external server
2
+
3
+ > The client (`@luckystack/secret-manager`) is implemented and described here. The external **secret-manager server** it talks to lives in a separate repository (`luckystack-secret-manager`); only its wire contract is captured below so the client can be developed against a stable API.
4
+
5
+ `@luckystack/secret-manager` is a **wiring client**, not a vault. It resolves committed `.env` pointers to real secrets at boot and writes them into `process.env`. Storage, versioning, auth, and the admin UI all live in the external server.
6
+
7
+ ## TL;DR
8
+
9
+ - **This package does:** scan `process.env` for pointer-shaped values (`<BASE>_V<n>`), `POST /resolve` them in one request, overwrite each `process.env` entry with the real secret, and (optionally, in dev) re-resolve on `.env` change / on an interval.
10
+ - **This package does NOT:** store secrets, version values, render an admin UI, or know what a "secret" is. All of that lives in the external server.
11
+ - **The workflow it enables:** developers commit `.env` containing only pointers (`OPENAI_KEY=OPENAI_AUTHORIZATION_KEY_V5`), not real secrets. The server resolves `OPENAI_AUTHORIZATION_KEY_V5` to its actual value at boot.
12
+ - **The strict invariant (enforced by the server):** values are **append-only by version**. `..._V5` is immutable; rotating means publishing `..._V6`. Old branches keep working because their committed `.env` still points at `..._V5`.
13
+
14
+ ## Why this design
15
+
16
+ 1. **`.env` is dangerous to commit, painful to share.** Pointers are safe to commit (they are opaque names, not values), so the team shares one source of truth and nobody leaks a secret into git history.
17
+ 2. **Rotation breaks old branches.** With versioned values, a six-month-old branch still references `..._V5` (resolvable until you retire it), while a fresh PR adopts `..._V6`. No "works on main, broken on old branch" surprises.
18
+ 3. **One flat keystore, one token.** A single HTTP boundary with one shared bearer token — no per-app vault SDK wiring.
19
+
20
+ ## The pointer model
21
+
22
+ - A pointer is any `.env` value matching `^(.+)_V(\d+)$` (configurable via `pointerPattern`).
23
+ - The env **name** is decoupled from the secret **base name**: `OPENAI_KEY` (name) -> `OPENAI_AUTHORIZATION_KEY_V5` (value) -> base `OPENAI_AUTHORIZATION_KEY`, version `5`.
24
+ - The client sends the **full pointer string** to the server and overwrites the env name with the resolved value. The base/version split is the server's job.
25
+ - A non-pointer value (`NODE_ENV=production`, or a real secret pasted locally) is a literal: never sent, never overwritten. Local overrides win for free.
26
+
27
+ ## Boot flow
28
+
29
+ ```
30
+ process start
31
+ -> dotenv loads .env / .env.local (consumer's server.ts)
32
+ -> initSecretManager(...) <-- THIS CLIENT, first line of server.ts
33
+ 1. capture { envName -> pointer } from process.env (once)
34
+ 2. POST /resolve { keys: [unique pointers] }
35
+ 3. overwrite process.env[envName] = values[pointer]
36
+ -> framework boot (reads the resolved process.env)
37
+ -> server.listen()
38
+ ```
39
+
40
+ `initSecretManager` runs **before** anything else that reads `process.env` at module-init time.
41
+
42
+ ## Modes
43
+
44
+ | `source` | Behavior | Writes to `process.env`? |
45
+ | --- | --- | --- |
46
+ | `'remote'` (default) | Resolve from the server. A missing pointer or fetch error throws — production hard-stop. | After a successful resolve. |
47
+ | `'local'` | No network. Pointers untouched. Tests / offline dev. | Never. |
48
+ | `'hybrid'` | Try the server; on failure warn and keep local env. | Only on a successful resolve; missing pointers are warned and left as-is. |
49
+
50
+ ## Dev hot reload (opt-in, dev-only)
51
+
52
+ Set `config.dev` to live-reload while a long-running dev process is up (no-op when `NODE_ENV === 'production'`):
53
+
54
+ - `dev.watch` (default `true`) — a debounced `fs.watch` on `dev.envFiles` (default `.env` + `.env.local`). On change the files are **re-parsed and applied**: plain values (`ENVIRONMENT=production`, `PORT=123`) are injected straight into `process.env` (live config reload), and pointer-shaped values are re-resolved against the server. A pointer added or bumped after boot is picked up here — no restart.
55
+ - `dev.pollIntervalMs` (default `0`/off) — re-resolve the current pointers every N ms, catching server-side rotations. The interval lives in `config.ts`, changeable in one place.
56
+ - `dev.envFiles` — override which files are watched + re-parsed (default `['.env', '.env.local']`).
57
+
58
+ The file parse is done by a tiny in-package `.env` parser (standard `KEY=VALUE`, comments, quotes), so the package stays dependency-free. Both channels swallow + warn on a transient error rather than crashing the dev process.
59
+
60
+ ## Auth model
61
+
62
+ A single shared bearer token accompanies every request:
63
+
64
+ ```
65
+ Authorization: Bearer <token>
66
+ ```
67
+
68
+ The token is the only real secret on the developer machine. Keep it in a gitignored single-line file referenced via `token: { fromFile: '.secret-manager-token' }` (read at resolve time, so file rotation is picked up by the next poll). CI runners inject the file from their secret store.
69
+
70
+ ## What this package does NOT do
71
+
72
+ - **No secret storage.** No on-disk persistence beyond reading the token file. The in-memory cache is plain JS, discarded on exit.
73
+ - **No versioning logic.** It sends the full pointer string and writes back whatever the server returns. Version naming + resolution is the server's job.
74
+ - **No admin / write operations.** Read-only `POST /resolve`. Publishing new versions happens through the server's own UI.
75
+ - **No app-key validation.** Whether `DATABASE_URL` is a valid URL is the app's concern (validate after `initSecretManager` returns).
76
+
77
+ ## External server — wire contract (separate repo)
78
+
79
+ The server lives in its own repo (`luckystack-secret-manager`). The client only depends on this one endpoint:
80
+
81
+ ### `POST /resolve`
82
+
83
+ Request (the app sends only the pointers it references):
84
+
85
+ ```json
86
+ { "keys": ["OPENAI_AUTHORIZATION_KEY_V5", "STRIPE_SECRET_KEY_V2"] }
87
+ ```
88
+
89
+ Response:
90
+
91
+ ```json
92
+ { "values": { "OPENAI_AUTHORIZATION_KEY_V5": "sk-...", "STRIPE_SECRET_KEY_V2": "rk-..." } }
93
+ ```
94
+
95
+ - Auth: `Authorization: Bearer <token>` (the shared token). 401 on mismatch.
96
+ - Pointers the server can't resolve are omitted from `values`. The client treats a missing pointer as fatal in `'remote'` mode, and a warning in `'hybrid'`.
97
+
98
+ The server additionally exposes admin endpoints (`GET /keys` listing masked values, `POST /keys` appending a new version) used only by its own admin webpage — the client never calls them. The full server + admin-UI implementation lives in the separate, running `luckystack-secret-manager` repo.
99
+
100
+ ## Implementation status
101
+
102
+ | Piece | Status | Where |
103
+ | --- | --- | --- |
104
+ | Resolver client (this package) | Implemented | `packages/secret-manager/src/index.ts` |
105
+ | local / remote / hybrid modes | Implemented | this doc |
106
+ | Opt-in dev hot reload (watch + poll) | Implemented | this doc |
107
+ | External secret-manager server (storage, versioning, admin UI, auth) | **Separate repo** | `luckystack-secret-manager` |
108
+
109
+ ## Related
110
+
111
+ - Function index: `../CLAUDE.md`.
112
+ - Consumer quickstart: `../README.md`.
113
+ - Framework-wide packaging map: `/docs/PACKAGE_OVERVIEW.md`.
114
+ - Architecture deep-dive: `/docs/ARCHITECTURE_SECRET_MANAGER.md`.
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@luckystack/secret-manager",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
- "access": "public"
6
+ "access": "public",
7
+ "provenance": true
7
8
  },
8
9
  "type": "module",
9
10
  "description": "Rotation-aware secret resolver client for LuckyStack. Commit `.env` pointers (e.g. OPENAI_KEY=OPENAI_AUTHORIZATION_KEY_V5) instead of real secrets; at boot this client resolves them against an external append-only secret-manager server and writes the real values into process.env. Supports local / remote / hybrid modes + opt-in dev hot reload.",
@@ -50,5 +51,8 @@
50
51
  "build": "tsup",
51
52
  "build:watch": "tsup --watch",
52
53
  "test": "vitest run"
54
+ },
55
+ "dependencies": {
56
+ "@luckystack/core": "^0.2.1"
53
57
  }
54
58
  }