@shipfox/api-secrets 2.0.0
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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +116 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/core/crypto.d.ts +25 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +85 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/dek-manager.d.ts +16 -0
- package/dist/core/dek-manager.d.ts.map +1 -0
- package/dist/core/dek-manager.js +85 -0
- package/dist/core/dek-manager.js.map +1 -0
- package/dist/core/errors.d.ts +53 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +86 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/fingerprint.d.ts +2 -0
- package/dist/core/fingerprint.d.ts.map +1 -0
- package/dist/core/fingerprint.js +9 -0
- package/dist/core/fingerprint.js.map +1 -0
- package/dist/core/index.d.ts +30 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +97 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/key-provider.d.ts +17 -0
- package/dist/core/key-provider.d.ts.map +1 -0
- package/dist/core/key-provider.js +45 -0
- package/dist/core/key-provider.js.map +1 -0
- package/dist/core/local-secret-store.d.ts +19 -0
- package/dist/core/local-secret-store.d.ts.map +1 -0
- package/dist/core/local-secret-store.js +40 -0
- package/dist/core/local-secret-store.js.map +1 -0
- package/dist/core/management.d.ts +37 -0
- package/dist/core/management.d.ts.map +1 -0
- package/dist/core/management.js +403 -0
- package/dist/core/management.js.map +1 -0
- package/dist/core/rotate-kek.d.ts +10 -0
- package/dist/core/rotate-kek.d.ts.map +1 -0
- package/dist/core/rotate-kek.js +94 -0
- package/dist/core/rotate-kek.js.map +1 -0
- package/dist/core/secret-store.d.ts +33 -0
- package/dist/core/secret-store.d.ts.map +1 -0
- package/dist/core/secret-store.js +209 -0
- package/dist/core/secret-store.js.map +1 -0
- package/dist/core/store-resolver.d.ts +18 -0
- package/dist/core/store-resolver.d.ts.map +1 -0
- package/dist/core/store-resolver.js +10 -0
- package/dist/core/store-resolver.js.map +1 -0
- package/dist/core/store-validation.d.ts +11 -0
- package/dist/core/store-validation.d.ts.map +1 -0
- package/dist/core/store-validation.js +30 -0
- package/dist/core/store-validation.js.map +1 -0
- package/dist/core/value-cipher.d.ts +10 -0
- package/dist/core/value-cipher.d.ts.map +1 -0
- package/dist/core/value-cipher.js +22 -0
- package/dist/core/value-cipher.js.map +1 -0
- package/dist/core/variable-store.d.ts +24 -0
- package/dist/core/variable-store.d.ts.map +1 -0
- package/dist/core/variable-store.js +194 -0
- package/dist/core/variable-store.js.map +1 -0
- package/dist/db/cap.d.ts +8 -0
- package/dist/db/cap.d.ts.map +1 -0
- package/dist/db/cap.js +39 -0
- package/dist/db/cap.js.map +1 -0
- package/dist/db/data-keys.d.ts +24 -0
- package/dist/db/data-keys.d.ts.map +1 -0
- package/dist/db/data-keys.js +61 -0
- package/dist/db/data-keys.js.map +1 -0
- package/dist/db/db.d.ts +1254 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +24 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +13 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +15 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/management.d.ts +50 -0
- package/dist/db/management.d.ts.map +1 -0
- package/dist/db/management.js +127 -0
- package/dist/db/management.js.map +1 -0
- package/dist/db/schema/common.d.ts +3 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +8 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/data-keys.d.ts +103 -0
- package/dist/db/schema/data-keys.d.ts.map +1 -0
- package/dist/db/schema/data-keys.js +24 -0
- package/dist/db/schema/data-keys.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/schema/values.d.ts +193 -0
- package/dist/db/schema/values.d.ts.map +1 -0
- package/dist/db/schema/values.js +43 -0
- package/dist/db/schema/values.js.map +1 -0
- package/dist/db/schema/variables.d.ts +175 -0
- package/dist/db/schema/variables.d.ts.map +1 -0
- package/dist/db/schema/variables.js +41 -0
- package/dist/db/schema/variables.js.map +1 -0
- package/dist/db/scope.d.ts +20 -0
- package/dist/db/scope.d.ts.map +1 -0
- package/dist/db/scope.js +19 -0
- package/dist/db/scope.js.map +1 -0
- package/dist/db/values.d.ts +33 -0
- package/dist/db/values.d.ts.map +1 -0
- package/dist/db/values.js +119 -0
- package/dist/db/values.js.map +1 -0
- package/dist/db/variables.d.ts +32 -0
- package/dist/db/variables.d.ts.map +1 -0
- package/dist/db/variables.js +117 -0
- package/dist/db/variables.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +4 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +39 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +151 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/metrics/service.d.ts +2 -0
- package/dist/metrics/service.d.ts.map +1 -0
- package/dist/metrics/service.js +21 -0
- package/dist/metrics/service.js.map +1 -0
- package/dist/presentation/dto/index.d.ts +2 -0
- package/dist/presentation/dto/index.d.ts.map +1 -0
- package/dist/presentation/dto/index.js +3 -0
- package/dist/presentation/dto/index.js.map +1 -0
- package/dist/presentation/dto/management.d.ts +7 -0
- package/dist/presentation/dto/management.d.ts.map +1 -0
- package/dist/presentation/dto/management.js +32 -0
- package/dist/presentation/dto/management.js.map +1 -0
- package/dist/presentation/e2eRoutes/create-secret.d.ts +2 -0
- package/dist/presentation/e2eRoutes/create-secret.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/create-secret.js +34 -0
- package/dist/presentation/e2eRoutes/create-secret.js.map +1 -0
- package/dist/presentation/e2eRoutes/create-variable.d.ts +2 -0
- package/dist/presentation/e2eRoutes/create-variable.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/create-variable.js +34 -0
- package/dist/presentation/e2eRoutes/create-variable.js.map +1 -0
- package/dist/presentation/e2eRoutes/index.d.ts +3 -0
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/index.js +11 -0
- package/dist/presentation/e2eRoutes/index.js.map +1 -0
- package/dist/presentation/routes/auth.d.ts +16 -0
- package/dist/presentation/routes/auth.d.ts.map +1 -0
- package/dist/presentation/routes/auth.js +42 -0
- package/dist/presentation/routes/auth.js.map +1 -0
- package/dist/presentation/routes/batch-secrets.d.ts +2 -0
- package/dist/presentation/routes/batch-secrets.d.ts.map +1 -0
- package/dist/presentation/routes/batch-secrets.js +44 -0
- package/dist/presentation/routes/batch-secrets.js.map +1 -0
- package/dist/presentation/routes/batch-variables.d.ts +2 -0
- package/dist/presentation/routes/batch-variables.d.ts.map +1 -0
- package/dist/presentation/routes/batch-variables.js +44 -0
- package/dist/presentation/routes/batch-variables.js.map +1 -0
- package/dist/presentation/routes/cursor.d.ts +3 -0
- package/dist/presentation/routes/cursor.d.ts.map +1 -0
- package/dist/presentation/routes/cursor.js +19 -0
- package/dist/presentation/routes/cursor.js.map +1 -0
- package/dist/presentation/routes/delete-secret.d.ts +2 -0
- package/dist/presentation/routes/delete-secret.d.ts.map +1 -0
- package/dist/presentation/routes/delete-secret.js +40 -0
- package/dist/presentation/routes/delete-secret.js.map +1 -0
- package/dist/presentation/routes/delete-variable.d.ts +2 -0
- package/dist/presentation/routes/delete-variable.d.ts.map +1 -0
- package/dist/presentation/routes/delete-variable.js +40 -0
- package/dist/presentation/routes/delete-variable.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +73 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/get-variable.d.ts +2 -0
- package/dist/presentation/routes/get-variable.d.ts.map +1 -0
- package/dist/presentation/routes/get-variable.js +42 -0
- package/dist/presentation/routes/get-variable.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +3 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +29 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/list-secrets.d.ts +2 -0
- package/dist/presentation/routes/list-secrets.d.ts.map +1 -0
- package/dist/presentation/routes/list-secrets.js +50 -0
- package/dist/presentation/routes/list-secrets.js.map +1 -0
- package/dist/presentation/routes/list-variables.d.ts +2 -0
- package/dist/presentation/routes/list-variables.d.ts.map +1 -0
- package/dist/presentation/routes/list-variables.js +50 -0
- package/dist/presentation/routes/list-variables.js.map +1 -0
- package/dist/presentation/routes/put-secret.d.ts +2 -0
- package/dist/presentation/routes/put-secret.d.ts.map +1 -0
- package/dist/presentation/routes/put-secret.js +52 -0
- package/dist/presentation/routes/put-secret.js.map +1 -0
- package/dist/presentation/routes/put-variable.d.ts +2 -0
- package/dist/presentation/routes/put-variable.d.ts.map +1 -0
- package/dist/presentation/routes/put-variable.js +52 -0
- package/dist/presentation/routes/put-variable.js.map +1 -0
- package/dist/presentation/routes/warnings.d.ts +5 -0
- package/dist/presentation/routes/warnings.d.ts.map +1 -0
- package/dist/presentation/routes/warnings.js +16 -0
- package/dist/presentation/routes/warnings.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_lowly_ogun.sql +59 -0
- package/drizzle/meta/0000_snapshot.json +547 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +77 -0
- package/src/config.test.ts +41 -0
- package/src/config.ts +41 -0
- package/src/core/crypto.test.ts +115 -0
- package/src/core/crypto.ts +105 -0
- package/src/core/dek-manager.test.ts +120 -0
- package/src/core/dek-manager.ts +98 -0
- package/src/core/errors.ts +100 -0
- package/src/core/fingerprint.test.ts +27 -0
- package/src/core/fingerprint.ts +14 -0
- package/src/core/index.ts +153 -0
- package/src/core/key-provider.test.ts +44 -0
- package/src/core/key-provider.ts +70 -0
- package/src/core/local-secret-store.ts +49 -0
- package/src/core/management.test.ts +37 -0
- package/src/core/management.ts +503 -0
- package/src/core/rotate-kek.test.ts +183 -0
- package/src/core/rotate-kek.ts +100 -0
- package/src/core/secret-store.test.ts +301 -0
- package/src/core/secret-store.ts +251 -0
- package/src/core/store-resolver.test.ts +20 -0
- package/src/core/store-resolver.ts +28 -0
- package/src/core/store-validation.ts +43 -0
- package/src/core/value-cipher.ts +26 -0
- package/src/core/variable-store.test.ts +106 -0
- package/src/core/variable-store.ts +232 -0
- package/src/db/cap.ts +58 -0
- package/src/db/data-keys.ts +99 -0
- package/src/db/db.ts +26 -0
- package/src/db/index.ts +45 -0
- package/src/db/management.ts +252 -0
- package/src/db/schema/common.ts +8 -0
- package/src/db/schema/data-keys.ts +31 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/schema/values.ts +74 -0
- package/src/db/schema/variables.ts +71 -0
- package/src/db/scope.ts +42 -0
- package/src/db/values.ts +191 -0
- package/src/db/variables.ts +187 -0
- package/src/index.test.ts +11 -0
- package/src/index.ts +50 -0
- package/src/metrics/index.ts +19 -0
- package/src/metrics/instance.ts +219 -0
- package/src/metrics/service.ts +19 -0
- package/src/presentation/dto/index.ts +1 -0
- package/src/presentation/dto/management.ts +36 -0
- package/src/presentation/e2eRoutes/create-secret.ts +31 -0
- package/src/presentation/e2eRoutes/create-variable.ts +34 -0
- package/src/presentation/e2eRoutes/index.test.ts +94 -0
- package/src/presentation/e2eRoutes/index.ts +8 -0
- package/src/presentation/routes/auth.ts +50 -0
- package/src/presentation/routes/batch-secrets.ts +33 -0
- package/src/presentation/routes/batch-variables.ts +33 -0
- package/src/presentation/routes/cursor.ts +23 -0
- package/src/presentation/routes/delete-secret.ts +26 -0
- package/src/presentation/routes/delete-variable.ts +26 -0
- package/src/presentation/routes/errors.ts +63 -0
- package/src/presentation/routes/get-variable.ts +32 -0
- package/src/presentation/routes/index.ts +29 -0
- package/src/presentation/routes/list-secrets.ts +41 -0
- package/src/presentation/routes/list-variables.ts +41 -0
- package/src/presentation/routes/management.test.ts +452 -0
- package/src/presentation/routes/put-secret.ts +39 -0
- package/src/presentation/routes/put-variable.ts +39 -0
- package/src/presentation/routes/warnings.ts +23 -0
- package/test/env.ts +8 -0
- package/test/globalSetup.ts +18 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +7 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ shipfox-tsc-emit
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ shipfox-tsc-check
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @shipfox/api-secrets
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1b0d344: Publishes the complete API runtime closure with packed-consumer-safe internal imports and records its exact package set in application releases.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [0cd6dd4]
|
|
12
|
+
- Updated dependencies [1b0d344]
|
|
13
|
+
- Updated dependencies [521e006]
|
|
14
|
+
- @shipfox/node-module@0.2.0
|
|
15
|
+
- @shipfox/api-auth-context@2.0.0
|
|
16
|
+
- @shipfox/api-projects@2.0.0
|
|
17
|
+
- @shipfox/api-secrets-dto@2.0.0
|
|
18
|
+
- @shipfox/config@1.2.1
|
|
19
|
+
- @shipfox/node-drizzle@0.2.1
|
|
20
|
+
- @shipfox/node-fastify@0.2.1
|
|
21
|
+
- @shipfox/node-opentelemetry@0.5.0
|
|
22
|
+
- @shipfox/node-outbox@0.2.1
|
|
23
|
+
- @shipfox/node-postgres@0.4.1
|
|
24
|
+
|
|
25
|
+
## 0.1.2
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [705dd43]
|
|
30
|
+
- @shipfox/node-outbox@0.2.0
|
|
31
|
+
- @shipfox/api-projects@0.1.2
|
|
32
|
+
- @shipfox/node-module@0.1.2
|
|
33
|
+
|
|
34
|
+
## 0.1.1
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Updated dependencies [ec75cd5]
|
|
39
|
+
- Updated dependencies [6a1fb54]
|
|
40
|
+
- @shipfox/node-drizzle@0.2.0
|
|
41
|
+
- @shipfox/node-postgres@0.4.0
|
|
42
|
+
- @shipfox/api-projects@0.1.1
|
|
43
|
+
- @shipfox/node-module@0.1.1
|
|
44
|
+
- @shipfox/node-outbox@0.1.1
|
|
45
|
+
|
|
46
|
+
## 0.1.0
|
|
47
|
+
|
|
48
|
+
### Minor Changes
|
|
49
|
+
|
|
50
|
+
- 3b45d86: Adds the secrets and variables backend foundation with encrypted secret storage, plaintext variables, and shared DTO contracts.
|
|
51
|
+
- 3ddde91: Adds the secrets and variables management API with DTO contracts, exact-scope storage helpers, outbox events, and workspace-admin route guards.
|
|
52
|
+
|
|
53
|
+
### Patch Changes
|
|
54
|
+
|
|
55
|
+
- f66f606: Test suites can create managed secrets and variables through protected setup APIs.
|
|
56
|
+
- e51d464: Add the Secrets & Variables workspace settings UI (S1b).
|
|
57
|
+
- New `@shipfox/client-secrets` package: transport + React Query hooks (a shared
|
|
58
|
+
`createStoreApi` factory), a write-only secret form and a readable variable form
|
|
59
|
+
(TanStack Form + Zod, multiline `Textarea` values, live short-value / sensitive-name
|
|
60
|
+
advisories), and the workspace secrets/variables sections (single-call list, masked
|
|
61
|
+
secret values, copy-name, delete with blast-radius warning).
|
|
62
|
+
- `@shipfox/client-workspace-settings`: new Secrets and Variables settings pages and nav
|
|
63
|
+
entries.
|
|
64
|
+
- `@shipfox/api-secrets-dto`: export `SECRETS_MAX_LIST_LIMIT` and raise the list `limit`
|
|
65
|
+
cap so the settings UI can fetch the whole bounded set in one request; the variable
|
|
66
|
+
list item now carries `value_truncated`.
|
|
67
|
+
- `@shipfox/api-secrets`: the variable list returns a bounded single-line preview of each
|
|
68
|
+
value (the full value is read via `GET /variables/:key` when editing) so a single-call
|
|
69
|
+
list cannot materialize very large responses; startup fails if `SECRETS_MAX_PER_WORKSPACE`
|
|
70
|
+
exceeds the list limit.
|
|
71
|
+
- `@shipfox/client-router`: register the `/workspaces/$wid/settings/secrets` and
|
|
72
|
+
`/variables` routes.
|
|
73
|
+
|
|
74
|
+
- 82d22e4: Make the workspace-membership gate stateless. `requireWorkspaceAccess` now lives in `@shipfox/api-auth-context` and authorizes a request purely from the verified session-token claims, replacing the `requireMembership` gate in `@shipfox/api-workspaces` that read the workspace row from the database on every workspace-scoped request. Membership and role already travel in the token, so the check needs no database access.
|
|
75
|
+
|
|
76
|
+
This removes the per-request database read and severs the runtime dependency on `@shipfox/api-workspaces` from feature modules that only needed the membership gate (integration providers, secrets, projects, agent, runners). Workspace existence and `active`-status enforcement, which no code path currently exercises, moves off the hot path; enforce it at token issuance when workspace suspension is introduced.
|
|
77
|
+
|
|
78
|
+
- Updated dependencies [34ba284]
|
|
79
|
+
- Updated dependencies [3b45d86]
|
|
80
|
+
- Updated dependencies [5707d6d]
|
|
81
|
+
- Updated dependencies [b9c3f32]
|
|
82
|
+
- Updated dependencies [a81b68c]
|
|
83
|
+
- Updated dependencies [115655e]
|
|
84
|
+
- Updated dependencies [c0a883c]
|
|
85
|
+
- Updated dependencies [72ce351]
|
|
86
|
+
- Updated dependencies [e47f8da]
|
|
87
|
+
- Updated dependencies [7b175f5]
|
|
88
|
+
- Updated dependencies [ae7a63c]
|
|
89
|
+
- Updated dependencies [5729548]
|
|
90
|
+
- Updated dependencies [f92122b]
|
|
91
|
+
- Updated dependencies [857fd73]
|
|
92
|
+
- Updated dependencies [75520ff]
|
|
93
|
+
- Updated dependencies [f66f606]
|
|
94
|
+
- Updated dependencies [e51d464]
|
|
95
|
+
- Updated dependencies [d6d4862]
|
|
96
|
+
- Updated dependencies [c0a883c]
|
|
97
|
+
- Updated dependencies [6077301]
|
|
98
|
+
- Updated dependencies [3bea87f]
|
|
99
|
+
- Updated dependencies [82d22e4]
|
|
100
|
+
- Updated dependencies [8ecba0f]
|
|
101
|
+
- Updated dependencies [27770eb]
|
|
102
|
+
- Updated dependencies [2933c33]
|
|
103
|
+
- Updated dependencies [43fd0c1]
|
|
104
|
+
- Updated dependencies [6181819]
|
|
105
|
+
- Updated dependencies [3ddde91]
|
|
106
|
+
- Updated dependencies [9c149d1]
|
|
107
|
+
- @shipfox/node-fastify@0.2.0
|
|
108
|
+
- @shipfox/api-secrets-dto@0.1.0
|
|
109
|
+
- @shipfox/node-drizzle@0.1.0
|
|
110
|
+
- @shipfox/api-auth-context@0.1.0
|
|
111
|
+
- @shipfox/api-projects@0.1.0
|
|
112
|
+
- @shipfox/node-opentelemetry@0.4.2
|
|
113
|
+
- @shipfox/node-postgres@0.3.2
|
|
114
|
+
- @shipfox/node-module@0.1.0
|
|
115
|
+
- @shipfox/node-outbox@0.1.0
|
|
116
|
+
- @shipfox/config@1.2.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shipfox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @shipfox/api-secrets
|
|
2
|
+
|
|
3
|
+
`@shipfox/api-secrets` stores workspace secrets and variables for backend modules.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- **`secretsModule`**: Declares the module name and database migrations for the API module loader.
|
|
8
|
+
- **Secret store API**: `setSecrets`, `getSecret`, `getSecretsByNamespace`, and `deleteSecrets` manage encrypted values by scope and namespace.
|
|
9
|
+
- **Variable store API**: `setVariables`, `getVariable`, `getVariablesByNamespace`, and `deleteVariables` manage non-secret config values with the same scope rules.
|
|
10
|
+
- **`rotateWorkspaceDataKeys()`**: Re-wraps workspace data keys under the current KEK. It does not re-encrypt stored values.
|
|
11
|
+
- **Store resolver**: `resolveSecretStore` supports the built-in `local` store and rejects unknown store names with `UnknownSecretStoreError`.
|
|
12
|
+
- **Domain errors**: Typed errors let route layers map validation, cap, crypto, and config failures to client responses.
|
|
13
|
+
|
|
14
|
+
## Installation / Setup
|
|
15
|
+
|
|
16
|
+
Install the package from the registry:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm add @shipfox/api-secrets
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Register `secretsModule` before modules that read or write secrets so its migrations run first.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {getSecret, secretsModule, setSecrets, setVariables} from '@shipfox/api-secrets';
|
|
28
|
+
import {initializeModules} from '@shipfox/node-module';
|
|
29
|
+
|
|
30
|
+
await initializeModules({modules: [secretsModule]});
|
|
31
|
+
|
|
32
|
+
await setSecrets({
|
|
33
|
+
workspaceId: '018f1c9c-7c30-7f41-9c32-7668a7f3cc11',
|
|
34
|
+
projectId: '018f1c9d-1b3c-7d4a-a5a2-8c2fdf4d7a91',
|
|
35
|
+
namespace: 'system/agent/model-provider/openai',
|
|
36
|
+
values: {API_KEY: 'sk-live-value'},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await setVariables({
|
|
40
|
+
workspaceId: '018f1c9c-7c30-7f41-9c32-7668a7f3cc11',
|
|
41
|
+
namespace: 'agent/defaults',
|
|
42
|
+
values: {REGION: 'us-east-1'},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const apiKey = await getSecret({
|
|
46
|
+
workspaceId: '018f1c9c-7c30-7f41-9c32-7668a7f3cc11',
|
|
47
|
+
projectId: '018f1c9d-1b3c-7d4a-a5a2-8c2fdf4d7a91',
|
|
48
|
+
namespace: 'system/agent/model-provider/openai',
|
|
49
|
+
key: 'API_KEY',
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Namespaces
|
|
54
|
+
|
|
55
|
+
Namespaces group values that belong to one module-owned purpose. Use lowercase
|
|
56
|
+
slug paths in this shape:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
<module>/<entity>/<id>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use the module name first, then the entity type, then the entity identifier. For
|
|
63
|
+
module-internal records that should not appear in user-facing management lists,
|
|
64
|
+
prefix the namespace with `system/`:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
system/<module>/<entity>/<id>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For example, agent model-provider credentials for OpenAI live under:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
system/agent/model-provider/openai
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Keep the namespace specific to the entity that owns the keys. Do not put several
|
|
77
|
+
unrelated entity types under the same namespace; create separate namespaces such
|
|
78
|
+
as `system/agent/model-provider/openai` and `system/agent/<other-entity>/<id>`.
|
|
79
|
+
|
|
80
|
+
## Environment
|
|
81
|
+
|
|
82
|
+
| Variable | Default | Purpose |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `SECRETS_ENCRYPTION_KEK` | none | Required base64-encoded 32-byte key used to wrap workspace data keys. Generate one per environment. |
|
|
85
|
+
| `SECRETS_ENCRYPTION_KEK_PREVIOUS` | none | Previous KEK needed during rotation and for normal operations until all workspace data keys are re-wrapped. Set it to the old key until data-key rotation completes. |
|
|
86
|
+
| `SECRETS_MAX_PER_WORKSPACE` | `10000` | Maximum total count of secrets and variables in one workspace. The count includes all namespaces and project scopes. |
|
|
87
|
+
| `SECRETS_SHORT_VALUE_WARN_LENGTH` | `12` | Length below which management routes can show a short-secret warning. The core store does not emit the warning itself. |
|
|
88
|
+
|
|
89
|
+
Startup checks each KEK as a canonical base64 value for a 32-byte key. It also rejects non-positive numeric limits.
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
| Function | Result |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `setSecrets({workspaceId, projectId?, namespace?, values, editedBy?})` | Encrypts and upserts a batch of secret values. |
|
|
96
|
+
| `getSecret({workspaceId, projectId?, namespace?, key, store?})` | Returns one plaintext secret or `null`. Project scope wins over workspace scope. |
|
|
97
|
+
| `getSecretsByNamespace({workspaceId, projectId?, namespace?, store?})` | Returns plaintext secrets in a namespace. Project scope wins over workspace scope. |
|
|
98
|
+
| `deleteSecrets({workspaceId, projectId?, namespace?, keys?})` | Deletes exact-scope secrets. Omitted `keys` deletes the scope namespace. Empty `keys` deletes nothing. |
|
|
99
|
+
| `setVariables({workspaceId, projectId?, namespace?, values, editedBy?})` | Upserts plaintext variables with the same validation and cap rules as secrets. |
|
|
100
|
+
| `getVariable({workspaceId, projectId?, namespace?, key})` | Returns one variable or `null`. Project scope wins over workspace scope. |
|
|
101
|
+
| `getVariablesByNamespace({workspaceId, projectId?, namespace?})` | Returns variables in a namespace. Project scope wins over workspace scope. |
|
|
102
|
+
| `deleteVariables({workspaceId, projectId?, namespace?, keys?})` | Deletes exact-scope variables. Omitted `keys` deletes the scope namespace. Empty `keys` deletes nothing. |
|
|
103
|
+
| `rotateWorkspaceDataKeys()` | Re-wraps stored data keys from the previous KEK to the current KEK. |
|
|
104
|
+
|
|
105
|
+
Keys must match `^[A-Z_][A-Z0-9_]*$`. Keys and namespaces may be at most 128 characters. Namespaces may be empty or a lowercase slug path.
|
|
106
|
+
|
|
107
|
+
## Data Model
|
|
108
|
+
|
|
109
|
+
This module owns tables with the `secrets_` prefix.
|
|
110
|
+
|
|
111
|
+
| Table | Purpose |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `secrets_data_keys` | Stores one wrapped data-encryption key per workspace. The row includes the KEK version used to wrap it. |
|
|
114
|
+
| `secrets_values` | Stores encrypted secret values, a keyed HMAC fingerprint, scope columns, and edit metadata. |
|
|
115
|
+
| `secrets_variables` | Stores plaintext variable values, scope columns, and edit metadata. |
|
|
116
|
+
|
|
117
|
+
`secrets_values` and `secrets_variables` use partial unique indexes for scope:
|
|
118
|
+
|
|
119
|
+
- Workspace scope is unique by `(workspace_id, namespace, key)` where `project_id IS NULL`.
|
|
120
|
+
- Project scope is unique by `(workspace_id, project_id, namespace, key)` where `project_id IS NOT NULL`.
|
|
121
|
+
|
|
122
|
+
The database checks key and namespace patterns. The core layer also validates them before writing.
|
|
123
|
+
|
|
124
|
+
## Security Model
|
|
125
|
+
|
|
126
|
+
### Trust boundary
|
|
127
|
+
|
|
128
|
+
This package is an in-process storage module. It does not perform user authorization.
|
|
129
|
+
|
|
130
|
+
Callers must derive `workspaceId`, `projectId`, and namespace from trusted context. Do not pass attacker-controlled scope values into the store functions.
|
|
131
|
+
|
|
132
|
+
Secret management routes should keep secret values write-only at the HTTP boundary. Internal callers may read plaintext secrets only after their own tenancy checks pass.
|
|
133
|
+
|
|
134
|
+
### Encryption
|
|
135
|
+
|
|
136
|
+
Secret values use envelope encryption.
|
|
137
|
+
|
|
138
|
+
- Each workspace has one 32-byte data-encryption key (DEK).
|
|
139
|
+
- The DEK is generated with `crypto.randomBytes(32)` on first use.
|
|
140
|
+
- The DEK is wrapped with the configured key-encryption key (KEK) and stored in `secrets_data_keys`.
|
|
141
|
+
- Secret values are encrypted with AES-256-GCM under the plaintext DEK.
|
|
142
|
+
- Ciphertexts are encoded as `v1:<base64(iv || authTag || ciphertext)>`.
|
|
143
|
+
- AES-GCM uses a fresh 12-byte IV for each seal.
|
|
144
|
+
|
|
145
|
+
Additional authenticated data binds encrypted values to:
|
|
146
|
+
|
|
147
|
+
- `workspaceId`
|
|
148
|
+
- scope, either workspace or project ID
|
|
149
|
+
- namespace
|
|
150
|
+
- key
|
|
151
|
+
|
|
152
|
+
Moving ciphertext to another scope makes decryption fail closed with `SecretDecryptionError`.
|
|
153
|
+
|
|
154
|
+
### Key handling
|
|
155
|
+
|
|
156
|
+
`SECRETS_ENCRYPTION_KEK` is the current KEK. It must be a canonical base64-encoded 32-byte value. Startup rejects malformed, unpadded, over-padded, or whitespace-tainted keys.
|
|
157
|
+
|
|
158
|
+
`SECRETS_ENCRYPTION_KEK_PREVIOUS` supports rotation. During that window, the key provider can unwrap DEKs with the old KEK and wrap them with the new KEK.
|
|
159
|
+
|
|
160
|
+
KEK versions come from a domain-separated SHA-256 hash of the KEK. The version is stored with each wrapped DEK. Rotation refuses unknown KEK versions.
|
|
161
|
+
|
|
162
|
+
Plaintext DEKs are cached in memory for hot reads. The cache has a size limit and TTL. The manager stores cache-owned Buffer copies and returns defensive copies. Node cannot promise full memory wipes, so the design limits how long keys stay in memory.
|
|
163
|
+
|
|
164
|
+
### Rotation
|
|
165
|
+
|
|
166
|
+
KEK rotation re-wraps DEKs only. It does not re-encrypt `secrets_values`.
|
|
167
|
+
|
|
168
|
+
Rotation pages over `secrets_data_keys`. It unwraps each DEK with the previous KEK and wraps it with the current KEK. The update uses compare-and-swap on the old version. If two rotations race, one wins and the other skips the row.
|
|
169
|
+
|
|
170
|
+
After each unwrap in the rotation path, the temporary plaintext DEK Buffer is filled with zeroes.
|
|
171
|
+
|
|
172
|
+
### Fingerprints
|
|
173
|
+
|
|
174
|
+
`secrets_values.fingerprint` stores a keyed HMAC-SHA256 digest of the secret value using the workspace DEK. It is not a plaintext suffix.
|
|
175
|
+
|
|
176
|
+
The fingerprint supports equality and display workflows without storing part of the secret in clear text. A database leak does not reveal the end of a secret through this column.
|
|
177
|
+
|
|
178
|
+
### Failure behavior
|
|
179
|
+
|
|
180
|
+
Secret decryption failures collapse to `SecretDecryptionError`. The error does not include secret values, key material, or ciphertext.
|
|
181
|
+
|
|
182
|
+
Deleting a workspace data key makes existing secrets unreadable. Reads fail closed instead of returning unauthenticated plaintext.
|
|
183
|
+
|
|
184
|
+
## Behavior Notes
|
|
185
|
+
|
|
186
|
+
- **Scope precedence:** project-scoped rows override workspace-scoped rows on reads and namespace lists.
|
|
187
|
+
- **Exact deletes:** deletes target only the exact scope passed by the caller. Deleting a project secret does not delete the workspace fallback.
|
|
188
|
+
- **Batch writes:** each write batch must target one scope. Mixed workspace and project rows are rejected by the persistence layer.
|
|
189
|
+
- **Workspace cap:** cap checks count secrets and variables together. The check runs inside the write transaction. It uses a workspace advisory lock and counts only net-new keys.
|
|
190
|
+
- **Variables:** variables are plaintext by design. Use them for non-secret config only.
|
|
191
|
+
- **Stores:** `local` is the only built-in store. Unknown stores throw `UnknownSecretStoreError`.
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
The tests use PostgreSQL. Start local services before running package tests:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
docker compose up -d
|
|
199
|
+
turbo build --filter=@shipfox/api-secrets
|
|
200
|
+
turbo check --filter=@shipfox/api-secrets
|
|
201
|
+
turbo type --filter=@shipfox/api-secrets
|
|
202
|
+
turbo test --filter=@shipfox/api-secrets
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const MAX_VALUE_BYTES: number;
|
|
2
|
+
export declare const config: Readonly<{
|
|
3
|
+
SECRETS_ENCRYPTION_KEK: string;
|
|
4
|
+
SECRETS_ENCRYPTION_KEK_PREVIOUS: string | undefined;
|
|
5
|
+
SECRETS_MAX_PER_WORKSPACE: number;
|
|
6
|
+
SECRETS_SHORT_VALUE_WARN_LENGTH: number;
|
|
7
|
+
} & import("@shipfox/config").CleanedEnvAccessors>;
|
|
8
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,QAAY,CAAC;AAEzC,eAAO,MAAM,MAAM;;;;;kDAgBjB,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SECRETS_MAX_LIST_LIMIT } from '@shipfox/api-secrets-dto';
|
|
2
|
+
import { createConfig, num, str } from '@shipfox/config';
|
|
3
|
+
import { decodeBase64Key } from '#core/crypto.js';
|
|
4
|
+
export const MAX_VALUE_BYTES = 64 * 1024;
|
|
5
|
+
export const config = createConfig({
|
|
6
|
+
SECRETS_ENCRYPTION_KEK: str({
|
|
7
|
+
desc: 'Master key used to protect all stored secrets. Required. Generate a unique value per environment with openssl rand -base64 32 and provide it from a secret manager. The committed .env value is only for local development. Losing this key makes stored secrets unrecoverable. To rotate it, set SECRETS_ENCRYPTION_KEK_PREVIOUS to the old value during the rotation window.'
|
|
8
|
+
}),
|
|
9
|
+
SECRETS_ENCRYPTION_KEK_PREVIOUS: str({
|
|
10
|
+
desc: 'Previous master key used only while rotating stored secret data keys. Optional. Set it to the old SECRETS_ENCRYPTION_KEK value until rotation has completed.',
|
|
11
|
+
default: undefined
|
|
12
|
+
}),
|
|
13
|
+
SECRETS_MAX_PER_WORKSPACE: num({
|
|
14
|
+
desc: 'Maximum number of secrets and variables allowed per workspace across all project scopes and namespaces.',
|
|
15
|
+
default: 10000
|
|
16
|
+
}),
|
|
17
|
+
SECRETS_SHORT_VALUE_WARN_LENGTH: num({
|
|
18
|
+
desc: 'Secret values shorter than this many characters are classified for a write warning by management routes.',
|
|
19
|
+
default: 12
|
|
20
|
+
})
|
|
21
|
+
});
|
|
22
|
+
decodeBase64Key(config.SECRETS_ENCRYPTION_KEK, 'SECRETS_ENCRYPTION_KEK');
|
|
23
|
+
if (config.SECRETS_ENCRYPTION_KEK_PREVIOUS) {
|
|
24
|
+
decodeBase64Key(config.SECRETS_ENCRYPTION_KEK_PREVIOUS, 'SECRETS_ENCRYPTION_KEK_PREVIOUS');
|
|
25
|
+
}
|
|
26
|
+
if (config.SECRETS_MAX_PER_WORKSPACE < 1) {
|
|
27
|
+
throw new Error('SECRETS_MAX_PER_WORKSPACE must be greater than 0.');
|
|
28
|
+
}
|
|
29
|
+
if (config.SECRETS_MAX_PER_WORKSPACE > SECRETS_MAX_LIST_LIMIT) {
|
|
30
|
+
// The settings UI lists the whole bounded set in one call (limit = SECRETS_MAX_LIST_LIMIT).
|
|
31
|
+
// A cap above that would let the list silently truncate, so fail fast instead.
|
|
32
|
+
throw new Error(`SECRETS_MAX_PER_WORKSPACE (${config.SECRETS_MAX_PER_WORKSPACE}) cannot exceed SECRETS_MAX_LIST_LIMIT (${SECRETS_MAX_LIST_LIMIT}).`);
|
|
33
|
+
}
|
|
34
|
+
if (config.SECRETS_SHORT_VALUE_WARN_LENGTH < 1) {
|
|
35
|
+
throw new Error('SECRETS_SHORT_VALUE_WARN_LENGTH must be greater than 0.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {SECRETS_MAX_LIST_LIMIT} from '@shipfox/api-secrets-dto';\nimport {createConfig, num, str} from '@shipfox/config';\nimport {decodeBase64Key} from '#core/crypto.js';\n\nexport const MAX_VALUE_BYTES = 64 * 1024;\n\nexport const config = createConfig({\n SECRETS_ENCRYPTION_KEK: str({\n desc: 'Master key used to protect all stored secrets. Required. Generate a unique value per environment with openssl rand -base64 32 and provide it from a secret manager. The committed .env value is only for local development. Losing this key makes stored secrets unrecoverable. To rotate it, set SECRETS_ENCRYPTION_KEK_PREVIOUS to the old value during the rotation window.',\n }),\n SECRETS_ENCRYPTION_KEK_PREVIOUS: str({\n desc: 'Previous master key used only while rotating stored secret data keys. Optional. Set it to the old SECRETS_ENCRYPTION_KEK value until rotation has completed.',\n default: undefined,\n }),\n SECRETS_MAX_PER_WORKSPACE: num({\n desc: 'Maximum number of secrets and variables allowed per workspace across all project scopes and namespaces.',\n default: 10000,\n }),\n SECRETS_SHORT_VALUE_WARN_LENGTH: num({\n desc: 'Secret values shorter than this many characters are classified for a write warning by management routes.',\n default: 12,\n }),\n});\n\ndecodeBase64Key(config.SECRETS_ENCRYPTION_KEK, 'SECRETS_ENCRYPTION_KEK');\nif (config.SECRETS_ENCRYPTION_KEK_PREVIOUS) {\n decodeBase64Key(config.SECRETS_ENCRYPTION_KEK_PREVIOUS, 'SECRETS_ENCRYPTION_KEK_PREVIOUS');\n}\nif (config.SECRETS_MAX_PER_WORKSPACE < 1) {\n throw new Error('SECRETS_MAX_PER_WORKSPACE must be greater than 0.');\n}\nif (config.SECRETS_MAX_PER_WORKSPACE > SECRETS_MAX_LIST_LIMIT) {\n // The settings UI lists the whole bounded set in one call (limit = SECRETS_MAX_LIST_LIMIT).\n // A cap above that would let the list silently truncate, so fail fast instead.\n throw new Error(\n `SECRETS_MAX_PER_WORKSPACE (${config.SECRETS_MAX_PER_WORKSPACE}) cannot exceed SECRETS_MAX_LIST_LIMIT (${SECRETS_MAX_LIST_LIMIT}).`,\n );\n}\nif (config.SECRETS_SHORT_VALUE_WARN_LENGTH < 1) {\n throw new Error('SECRETS_SHORT_VALUE_WARN_LENGTH must be greater than 0.');\n}\n"],"names":["SECRETS_MAX_LIST_LIMIT","createConfig","num","str","decodeBase64Key","MAX_VALUE_BYTES","config","SECRETS_ENCRYPTION_KEK","desc","SECRETS_ENCRYPTION_KEK_PREVIOUS","default","undefined","SECRETS_MAX_PER_WORKSPACE","SECRETS_SHORT_VALUE_WARN_LENGTH","Error"],"mappings":"AAAA,SAAQA,sBAAsB,QAAO,2BAA2B;AAChE,SAAQC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AACvD,SAAQC,eAAe,QAAO,kBAAkB;AAEhD,OAAO,MAAMC,kBAAkB,KAAK,KAAK;AAEzC,OAAO,MAAMC,SAASL,aAAa;IACjCM,wBAAwBJ,IAAI;QAC1BK,MAAM;IACR;IACAC,iCAAiCN,IAAI;QACnCK,MAAM;QACNE,SAASC;IACX;IACAC,2BAA2BV,IAAI;QAC7BM,MAAM;QACNE,SAAS;IACX;IACAG,iCAAiCX,IAAI;QACnCM,MAAM;QACNE,SAAS;IACX;AACF,GAAG;AAEHN,gBAAgBE,OAAOC,sBAAsB,EAAE;AAC/C,IAAID,OAAOG,+BAA+B,EAAE;IAC1CL,gBAAgBE,OAAOG,+BAA+B,EAAE;AAC1D;AACA,IAAIH,OAAOM,yBAAyB,GAAG,GAAG;IACxC,MAAM,IAAIE,MAAM;AAClB;AACA,IAAIR,OAAOM,yBAAyB,GAAGZ,wBAAwB;IAC7D,4FAA4F;IAC5F,+EAA+E;IAC/E,MAAM,IAAIc,MACR,CAAC,2BAA2B,EAAER,OAAOM,yBAAyB,CAAC,wCAAwC,EAAEZ,uBAAuB,EAAE,CAAC;AAEvI;AACA,IAAIM,OAAOO,+BAA+B,GAAG,GAAG;IAC9C,MAAM,IAAIC,MAAM;AAClB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface AesGcmSealParams {
|
|
2
|
+
key: Buffer;
|
|
3
|
+
plaintext: Buffer;
|
|
4
|
+
aad: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AesGcmOpenParams {
|
|
7
|
+
key: Buffer;
|
|
8
|
+
encoded: string;
|
|
9
|
+
aad: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SecretScope {
|
|
12
|
+
projectId?: string | null | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface SecretValueAadParams {
|
|
15
|
+
workspaceId: string;
|
|
16
|
+
scope?: SecretScope | undefined;
|
|
17
|
+
namespace: string;
|
|
18
|
+
key: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function aesGcmSeal(params: AesGcmSealParams): string;
|
|
21
|
+
export declare function aesGcmOpen(params: AesGcmOpenParams): Buffer;
|
|
22
|
+
export declare function decodeBase64Key(encoded: string | undefined, label: string): Buffer;
|
|
23
|
+
export declare function aadForDek(workspaceId: string, kekVersion: string): string;
|
|
24
|
+
export declare function aadForValue(params: SecretValueAadParams): string;
|
|
25
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/core/crypto.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAS3D;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CA4B3D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAelF;AAED,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAIhE"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { KekConfigurationError, SecretDecryptionError } from './errors.js';
|
|
3
|
+
const CIPHER = 'aes-256-gcm';
|
|
4
|
+
const ENCODED_PREFIX = 'v1:';
|
|
5
|
+
const IV_BYTES = 12;
|
|
6
|
+
const AUTH_TAG_BYTES = 16;
|
|
7
|
+
const KEY_BYTES = 32;
|
|
8
|
+
const BASE64_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
9
|
+
const BASE64_PADDING_SUFFIX = /=+$/;
|
|
10
|
+
export function aesGcmSeal(params) {
|
|
11
|
+
const iv = crypto.randomBytes(IV_BYTES);
|
|
12
|
+
const cipher = crypto.createCipheriv(CIPHER, params.key, iv);
|
|
13
|
+
cipher.setAAD(Buffer.from(params.aad, 'utf8'));
|
|
14
|
+
const ciphertext = Buffer.concat([
|
|
15
|
+
cipher.update(params.plaintext),
|
|
16
|
+
cipher.final()
|
|
17
|
+
]);
|
|
18
|
+
const authTag = cipher.getAuthTag();
|
|
19
|
+
return `${ENCODED_PREFIX}${Buffer.concat([
|
|
20
|
+
iv,
|
|
21
|
+
authTag,
|
|
22
|
+
ciphertext
|
|
23
|
+
]).toString('base64')}`;
|
|
24
|
+
}
|
|
25
|
+
export function aesGcmOpen(params) {
|
|
26
|
+
if (!params.encoded.startsWith(ENCODED_PREFIX)) throw new SecretDecryptionError();
|
|
27
|
+
const encodedPayload = params.encoded.slice(ENCODED_PREFIX.length);
|
|
28
|
+
const payload = Buffer.from(encodedPayload, 'base64');
|
|
29
|
+
const canonical = payload.toString('base64');
|
|
30
|
+
if (!BASE64_KEY_PATTERN.test(encodedPayload) || canonical.replace(BASE64_PADDING_SUFFIX, '') !== encodedPayload.replace(BASE64_PADDING_SUFFIX, '')) {
|
|
31
|
+
throw new SecretDecryptionError();
|
|
32
|
+
}
|
|
33
|
+
if (payload.length < IV_BYTES + AUTH_TAG_BYTES) throw new SecretDecryptionError();
|
|
34
|
+
try {
|
|
35
|
+
const iv = payload.subarray(0, IV_BYTES);
|
|
36
|
+
const authTag = payload.subarray(IV_BYTES, IV_BYTES + AUTH_TAG_BYTES);
|
|
37
|
+
const ciphertext = payload.subarray(IV_BYTES + AUTH_TAG_BYTES);
|
|
38
|
+
const decipher = crypto.createDecipheriv(CIPHER, params.key, iv);
|
|
39
|
+
decipher.setAAD(Buffer.from(params.aad, 'utf8'));
|
|
40
|
+
decipher.setAuthTag(authTag);
|
|
41
|
+
return Buffer.concat([
|
|
42
|
+
decipher.update(ciphertext),
|
|
43
|
+
decipher.final()
|
|
44
|
+
]);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof SecretDecryptionError) throw error;
|
|
47
|
+
throw new SecretDecryptionError();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function decodeBase64Key(encoded, label) {
|
|
51
|
+
if (!encoded) {
|
|
52
|
+
throw new KekConfigurationError(`${label} is required and must be a base64-encoded 32-byte key. Generate one with openssl rand -base64 32.`);
|
|
53
|
+
}
|
|
54
|
+
const key = Buffer.from(encoded, 'base64');
|
|
55
|
+
if (key.length !== KEY_BYTES || !isCanonicalBase64Key(encoded, key)) {
|
|
56
|
+
throw new KekConfigurationError(`${label} must be a canonical base64-encoded 32-byte key. Strip whitespace and generate a new value with openssl rand -base64 32 if needed.`);
|
|
57
|
+
}
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
export function aadForDek(workspaceId, kekVersion) {
|
|
61
|
+
return JSON.stringify([
|
|
62
|
+
workspaceId,
|
|
63
|
+
kekVersion
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
export function aadForValue(params) {
|
|
67
|
+
const projectId = params.scope?.projectId ?? null;
|
|
68
|
+
const scopeTuple = projectId !== null ? [
|
|
69
|
+
'project',
|
|
70
|
+
projectId
|
|
71
|
+
] : [
|
|
72
|
+
'workspace'
|
|
73
|
+
];
|
|
74
|
+
return JSON.stringify([
|
|
75
|
+
params.workspaceId,
|
|
76
|
+
scopeTuple,
|
|
77
|
+
params.namespace,
|
|
78
|
+
params.key
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
function isCanonicalBase64Key(encoded, key) {
|
|
82
|
+
return BASE64_KEY_PATTERN.test(encoded) && key.toString('base64') === encoded;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/crypto.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport {KekConfigurationError, SecretDecryptionError} from './errors.js';\n\nconst CIPHER = 'aes-256-gcm';\nconst ENCODED_PREFIX = 'v1:';\nconst IV_BYTES = 12;\nconst AUTH_TAG_BYTES = 16;\nconst KEY_BYTES = 32;\nconst BASE64_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;\nconst BASE64_PADDING_SUFFIX = /=+$/;\n\nexport interface AesGcmSealParams {\n key: Buffer;\n plaintext: Buffer;\n aad: string;\n}\n\nexport interface AesGcmOpenParams {\n key: Buffer;\n encoded: string;\n aad: string;\n}\n\nexport interface SecretScope {\n projectId?: string | null | undefined;\n}\n\nexport interface SecretValueAadParams {\n workspaceId: string;\n scope?: SecretScope | undefined;\n namespace: string;\n key: string;\n}\n\nexport function aesGcmSeal(params: AesGcmSealParams): string {\n const iv = crypto.randomBytes(IV_BYTES);\n const cipher = crypto.createCipheriv(CIPHER, params.key, iv);\n cipher.setAAD(Buffer.from(params.aad, 'utf8'));\n\n const ciphertext = Buffer.concat([cipher.update(params.plaintext), cipher.final()]);\n const authTag = cipher.getAuthTag();\n\n return `${ENCODED_PREFIX}${Buffer.concat([iv, authTag, ciphertext]).toString('base64')}`;\n}\n\nexport function aesGcmOpen(params: AesGcmOpenParams): Buffer {\n if (!params.encoded.startsWith(ENCODED_PREFIX)) throw new SecretDecryptionError();\n\n const encodedPayload = params.encoded.slice(ENCODED_PREFIX.length);\n const payload = Buffer.from(encodedPayload, 'base64');\n const canonical = payload.toString('base64');\n if (\n !BASE64_KEY_PATTERN.test(encodedPayload) ||\n canonical.replace(BASE64_PADDING_SUFFIX, '') !==\n encodedPayload.replace(BASE64_PADDING_SUFFIX, '')\n ) {\n throw new SecretDecryptionError();\n }\n if (payload.length < IV_BYTES + AUTH_TAG_BYTES) throw new SecretDecryptionError();\n\n try {\n const iv = payload.subarray(0, IV_BYTES);\n const authTag = payload.subarray(IV_BYTES, IV_BYTES + AUTH_TAG_BYTES);\n const ciphertext = payload.subarray(IV_BYTES + AUTH_TAG_BYTES);\n const decipher = crypto.createDecipheriv(CIPHER, params.key, iv);\n decipher.setAAD(Buffer.from(params.aad, 'utf8'));\n decipher.setAuthTag(authTag);\n\n return Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n } catch (error) {\n if (error instanceof SecretDecryptionError) throw error;\n throw new SecretDecryptionError();\n }\n}\n\nexport function decodeBase64Key(encoded: string | undefined, label: string): Buffer {\n if (!encoded) {\n throw new KekConfigurationError(\n `${label} is required and must be a base64-encoded 32-byte key. Generate one with openssl rand -base64 32.`,\n );\n }\n\n const key = Buffer.from(encoded, 'base64');\n if (key.length !== KEY_BYTES || !isCanonicalBase64Key(encoded, key)) {\n throw new KekConfigurationError(\n `${label} must be a canonical base64-encoded 32-byte key. Strip whitespace and generate a new value with openssl rand -base64 32 if needed.`,\n );\n }\n\n return key;\n}\n\nexport function aadForDek(workspaceId: string, kekVersion: string): string {\n return JSON.stringify([workspaceId, kekVersion]);\n}\n\nexport function aadForValue(params: SecretValueAadParams): string {\n const projectId = params.scope?.projectId ?? null;\n const scopeTuple = projectId !== null ? ['project', projectId] : ['workspace'];\n return JSON.stringify([params.workspaceId, scopeTuple, params.namespace, params.key]);\n}\n\nfunction isCanonicalBase64Key(encoded: string, key: Buffer): boolean {\n return BASE64_KEY_PATTERN.test(encoded) && key.toString('base64') === encoded;\n}\n"],"names":["crypto","KekConfigurationError","SecretDecryptionError","CIPHER","ENCODED_PREFIX","IV_BYTES","AUTH_TAG_BYTES","KEY_BYTES","BASE64_KEY_PATTERN","BASE64_PADDING_SUFFIX","aesGcmSeal","params","iv","randomBytes","cipher","createCipheriv","key","setAAD","Buffer","from","aad","ciphertext","concat","update","plaintext","final","authTag","getAuthTag","toString","aesGcmOpen","encoded","startsWith","encodedPayload","slice","length","payload","canonical","test","replace","subarray","decipher","createDecipheriv","setAuthTag","error","decodeBase64Key","label","isCanonicalBase64Key","aadForDek","workspaceId","kekVersion","JSON","stringify","aadForValue","projectId","scope","scopeTuple","namespace"],"mappings":"AAAA,OAAOA,YAAY,cAAc;AACjC,SAAQC,qBAAqB,EAAEC,qBAAqB,QAAO,cAAc;AAEzE,MAAMC,SAAS;AACf,MAAMC,iBAAiB;AACvB,MAAMC,WAAW;AACjB,MAAMC,iBAAiB;AACvB,MAAMC,YAAY;AAClB,MAAMC,qBAAqB;AAC3B,MAAMC,wBAAwB;AAyB9B,OAAO,SAASC,WAAWC,MAAwB;IACjD,MAAMC,KAAKZ,OAAOa,WAAW,CAACR;IAC9B,MAAMS,SAASd,OAAOe,cAAc,CAACZ,QAAQQ,OAAOK,GAAG,EAAEJ;IACzDE,OAAOG,MAAM,CAACC,OAAOC,IAAI,CAACR,OAAOS,GAAG,EAAE;IAEtC,MAAMC,aAAaH,OAAOI,MAAM,CAAC;QAACR,OAAOS,MAAM,CAACZ,OAAOa,SAAS;QAAGV,OAAOW,KAAK;KAAG;IAClF,MAAMC,UAAUZ,OAAOa,UAAU;IAEjC,OAAO,GAAGvB,iBAAiBc,OAAOI,MAAM,CAAC;QAACV;QAAIc;QAASL;KAAW,EAAEO,QAAQ,CAAC,WAAW;AAC1F;AAEA,OAAO,SAASC,WAAWlB,MAAwB;IACjD,IAAI,CAACA,OAAOmB,OAAO,CAACC,UAAU,CAAC3B,iBAAiB,MAAM,IAAIF;IAE1D,MAAM8B,iBAAiBrB,OAAOmB,OAAO,CAACG,KAAK,CAAC7B,eAAe8B,MAAM;IACjE,MAAMC,UAAUjB,OAAOC,IAAI,CAACa,gBAAgB;IAC5C,MAAMI,YAAYD,QAAQP,QAAQ,CAAC;IACnC,IACE,CAACpB,mBAAmB6B,IAAI,CAACL,mBACzBI,UAAUE,OAAO,CAAC7B,uBAAuB,QACvCuB,eAAeM,OAAO,CAAC7B,uBAAuB,KAChD;QACA,MAAM,IAAIP;IACZ;IACA,IAAIiC,QAAQD,MAAM,GAAG7B,WAAWC,gBAAgB,MAAM,IAAIJ;IAE1D,IAAI;QACF,MAAMU,KAAKuB,QAAQI,QAAQ,CAAC,GAAGlC;QAC/B,MAAMqB,UAAUS,QAAQI,QAAQ,CAAClC,UAAUA,WAAWC;QACtD,MAAMe,aAAac,QAAQI,QAAQ,CAAClC,WAAWC;QAC/C,MAAMkC,WAAWxC,OAAOyC,gBAAgB,CAACtC,QAAQQ,OAAOK,GAAG,EAAEJ;QAC7D4B,SAASvB,MAAM,CAACC,OAAOC,IAAI,CAACR,OAAOS,GAAG,EAAE;QACxCoB,SAASE,UAAU,CAAChB;QAEpB,OAAOR,OAAOI,MAAM,CAAC;YAACkB,SAASjB,MAAM,CAACF;YAAamB,SAASf,KAAK;SAAG;IACtE,EAAE,OAAOkB,OAAO;QACd,IAAIA,iBAAiBzC,uBAAuB,MAAMyC;QAClD,MAAM,IAAIzC;IACZ;AACF;AAEA,OAAO,SAAS0C,gBAAgBd,OAA2B,EAAEe,KAAa;IACxE,IAAI,CAACf,SAAS;QACZ,MAAM,IAAI7B,sBACR,GAAG4C,MAAM,iGAAiG,CAAC;IAE/G;IAEA,MAAM7B,MAAME,OAAOC,IAAI,CAACW,SAAS;IACjC,IAAId,IAAIkB,MAAM,KAAK3B,aAAa,CAACuC,qBAAqBhB,SAASd,MAAM;QACnE,MAAM,IAAIf,sBACR,GAAG4C,MAAM,kIAAkI,CAAC;IAEhJ;IAEA,OAAO7B;AACT;AAEA,OAAO,SAAS+B,UAAUC,WAAmB,EAAEC,UAAkB;IAC/D,OAAOC,KAAKC,SAAS,CAAC;QAACH;QAAaC;KAAW;AACjD;AAEA,OAAO,SAASG,YAAYzC,MAA4B;IACtD,MAAM0C,YAAY1C,OAAO2C,KAAK,EAAED,aAAa;IAC7C,MAAME,aAAaF,cAAc,OAAO;QAAC;QAAWA;KAAU,GAAG;QAAC;KAAY;IAC9E,OAAOH,KAAKC,SAAS,CAAC;QAACxC,OAAOqC,WAAW;QAAEO;QAAY5C,OAAO6C,SAAS;QAAE7C,OAAOK,GAAG;KAAC;AACtF;AAEA,SAAS8B,qBAAqBhB,OAAe,EAAEd,GAAW;IACxD,OAAOR,mBAAmB6B,IAAI,CAACP,YAAYd,IAAIY,QAAQ,CAAC,cAAcE;AACxE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { KeyProvider } from './key-provider.js';
|
|
2
|
+
/**
|
|
3
|
+
* Plaintext DEKs live in memory by design so hot secret reads avoid unwrapping on
|
|
4
|
+
* every access. Node cannot reliably zeroize Buffers, so residency is bounded by
|
|
5
|
+
* LRU size and lazy TTL instead of pretending wipes are a complete mitigation.
|
|
6
|
+
*/
|
|
7
|
+
export declare class DekManager {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(keyProvider: KeyProvider, options: {
|
|
10
|
+
maxEntries: number;
|
|
11
|
+
ttlMs: number;
|
|
12
|
+
});
|
|
13
|
+
getPlaintextDek(workspaceId: string): Promise<Buffer>;
|
|
14
|
+
invalidate(workspaceId: string): void;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=dek-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dek-manager.d.ts","sourceRoot":"","sources":["../../src/core/dek-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAInD;;;;GAIG;AACH,qBAAa,UAAU;;gBAKT,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC;IAK5E,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA4D3D,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;CAetC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { getDataKey, insertDataKeyIfAbsent } from '#db/index.js';
|
|
3
|
+
import { classifyDekAccessError, recordSecretsDekAccess } from '#metrics/instance.js';
|
|
4
|
+
const DEK_BYTES = 32;
|
|
5
|
+
/**
|
|
6
|
+
* Plaintext DEKs live in memory by design so hot secret reads avoid unwrapping on
|
|
7
|
+
* every access. Node cannot reliably zeroize Buffers, so residency is bounded by
|
|
8
|
+
* LRU size and lazy TTL instead of pretending wipes are a complete mitigation.
|
|
9
|
+
*/ export class DekManager {
|
|
10
|
+
#cache;
|
|
11
|
+
#keyProvider;
|
|
12
|
+
#options;
|
|
13
|
+
constructor(keyProvider, options){
|
|
14
|
+
this.#cache = new Map();
|
|
15
|
+
this.#keyProvider = keyProvider;
|
|
16
|
+
this.#options = options;
|
|
17
|
+
}
|
|
18
|
+
async getPlaintextDek(workspaceId) {
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
try {
|
|
21
|
+
const cached = this.#cache.get(workspaceId);
|
|
22
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
23
|
+
this.#cache.delete(workspaceId);
|
|
24
|
+
this.#cache.set(workspaceId, cached);
|
|
25
|
+
recordSecretsDekAccess({
|
|
26
|
+
outcome: 'cache_hit',
|
|
27
|
+
durationMs: Date.now() - startedAt
|
|
28
|
+
});
|
|
29
|
+
return Buffer.from(cached.dek);
|
|
30
|
+
}
|
|
31
|
+
const hadExpiredCache = Boolean(cached);
|
|
32
|
+
if (cached) this.#cache.delete(workspaceId);
|
|
33
|
+
const existing = await getDataKey(workspaceId);
|
|
34
|
+
if (existing) {
|
|
35
|
+
const dek = this.#keyProvider.unwrapDek(workspaceId, existing.wrappedDek, existing.kekVersion);
|
|
36
|
+
this.#set(workspaceId, dek);
|
|
37
|
+
recordSecretsDekAccess({
|
|
38
|
+
outcome: hadExpiredCache ? 'cache_expired' : 'db_unwrapped',
|
|
39
|
+
durationMs: Date.now() - startedAt
|
|
40
|
+
});
|
|
41
|
+
return Buffer.from(dek);
|
|
42
|
+
}
|
|
43
|
+
const generatedDek = crypto.randomBytes(DEK_BYTES);
|
|
44
|
+
const wrapped = this.#keyProvider.wrapDek(workspaceId, generatedDek);
|
|
45
|
+
const inserted = await insertDataKeyIfAbsent({
|
|
46
|
+
workspaceId,
|
|
47
|
+
wrappedDek: wrapped.wrappedDek,
|
|
48
|
+
kekVersion: wrapped.kekVersion
|
|
49
|
+
});
|
|
50
|
+
// The DEK row commits before value writes. If concurrent first-use inserts race,
|
|
51
|
+
// the primary key decides the winner and every caller re-reads the persisted row.
|
|
52
|
+
const persisted = await getDataKey(workspaceId);
|
|
53
|
+
if (!persisted) throw new Error(`Data key was not persisted for workspace ${workspaceId}`);
|
|
54
|
+
const dek = this.#keyProvider.unwrapDek(workspaceId, persisted.wrappedDek, persisted.kekVersion);
|
|
55
|
+
this.#set(workspaceId, dek);
|
|
56
|
+
recordSecretsDekAccess({
|
|
57
|
+
outcome: inserted ? 'generated' : 'db_unwrapped',
|
|
58
|
+
durationMs: Date.now() - startedAt
|
|
59
|
+
});
|
|
60
|
+
return Buffer.from(dek);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
recordSecretsDekAccess({
|
|
63
|
+
outcome: classifyDekAccessError(error),
|
|
64
|
+
durationMs: Date.now() - startedAt
|
|
65
|
+
});
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
invalidate(workspaceId) {
|
|
70
|
+
this.#cache.delete(workspaceId);
|
|
71
|
+
}
|
|
72
|
+
#set(workspaceId, dek) {
|
|
73
|
+
this.#cache.set(workspaceId, {
|
|
74
|
+
dek: Buffer.from(dek),
|
|
75
|
+
expiresAt: Date.now() + this.#options.ttlMs
|
|
76
|
+
});
|
|
77
|
+
while(this.#cache.size > this.#options.maxEntries){
|
|
78
|
+
const oldest = this.#cache.keys().next().value;
|
|
79
|
+
if (!oldest) break;
|
|
80
|
+
this.#cache.delete(oldest);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=dek-manager.js.map
|