@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,100 @@
|
|
|
1
|
+
export class SecretDecryptionError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super('Secret value could not be decrypted.');
|
|
4
|
+
this.name = 'SecretDecryptionError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class DekWrapError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super('Data encryption key could not be wrapped.');
|
|
11
|
+
this.name = 'DekWrapError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DekUnwrapError extends Error {
|
|
16
|
+
constructor() {
|
|
17
|
+
super('Data encryption key could not be unwrapped.');
|
|
18
|
+
this.name = 'DekUnwrapError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class KekConfigurationError extends Error {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'KekConfigurationError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class SecretKeyValidationError extends Error {
|
|
30
|
+
constructor(public readonly key: string) {
|
|
31
|
+
super(`Invalid secret key: ${key}`);
|
|
32
|
+
this.name = 'SecretKeyValidationError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class NamespaceValidationError extends Error {
|
|
37
|
+
constructor(public readonly namespace: string) {
|
|
38
|
+
super(`Invalid secret namespace: ${namespace}`);
|
|
39
|
+
this.name = 'NamespaceValidationError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class WorkspaceSecretCapExceededError extends Error {
|
|
44
|
+
constructor(
|
|
45
|
+
public readonly workspaceId: string,
|
|
46
|
+
public readonly cap: number,
|
|
47
|
+
) {
|
|
48
|
+
super(`Workspace ${workspaceId} exceeds the secret and variable cap of ${cap}.`);
|
|
49
|
+
this.name = 'WorkspaceSecretCapExceededError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class SecretValueTooLargeError extends Error {
|
|
54
|
+
constructor(public readonly maxBytes: number) {
|
|
55
|
+
super(`Secret and variable values must not exceed ${maxBytes} bytes.`);
|
|
56
|
+
this.name = 'SecretValueTooLargeError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class SecretBatchScopeMismatchError extends Error {
|
|
61
|
+
constructor() {
|
|
62
|
+
super('Secret and variable batches must target a single project scope.');
|
|
63
|
+
this.name = 'SecretBatchScopeMismatchError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class SecretBatchDuplicateKeyError extends Error {
|
|
68
|
+
constructor(public readonly key: string) {
|
|
69
|
+
super(`Duplicate secret or variable key in batch: ${key}`);
|
|
70
|
+
this.name = 'SecretBatchDuplicateKeyError';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class SecretNotFoundError extends Error {
|
|
75
|
+
constructor(public readonly key: string) {
|
|
76
|
+
super(`Secret not found: ${key}`);
|
|
77
|
+
this.name = 'SecretNotFoundError';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class VariableNotFoundError extends Error {
|
|
82
|
+
constructor(public readonly key: string) {
|
|
83
|
+
super(`Variable not found: ${key}`);
|
|
84
|
+
this.name = 'VariableNotFoundError';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class UnknownSecretStoreError extends Error {
|
|
89
|
+
constructor(public readonly store: string) {
|
|
90
|
+
super(`Unknown secret store: ${store.slice(0, 64)}`);
|
|
91
|
+
this.name = 'UnknownSecretStoreError';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class KekVersionStrandedError extends Error {
|
|
96
|
+
constructor(public readonly kekVersion: string) {
|
|
97
|
+
super(`Data key is stranded on unknown KEK version: ${kekVersion}`);
|
|
98
|
+
this.name = 'KekVersionStrandedError';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {describe, expect, it} from '@shipfox/vitest/vi';
|
|
2
|
+
import {fingerprintSecretValue} from './fingerprint.js';
|
|
3
|
+
|
|
4
|
+
const HMAC_FINGERPRINT_PATTERN = /^hmac-sha256:[A-Za-z0-9_-]{43}$/;
|
|
5
|
+
|
|
6
|
+
describe('fingerprintSecretValue', () => {
|
|
7
|
+
it('returns a keyed non-reversible fingerprint', () => {
|
|
8
|
+
const key = Buffer.alloc(32, 1);
|
|
9
|
+
|
|
10
|
+
const result = fingerprintSecretValue('abcdefghijklmnopqrstuvwxyz', key);
|
|
11
|
+
|
|
12
|
+
expect(result).toMatch(HMAC_FINGERPRINT_PATTERN);
|
|
13
|
+
expect(result).not.toContain('wxyz');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('is stable for the same key and value and changes with the key', () => {
|
|
17
|
+
const firstKey = Buffer.alloc(32, 1);
|
|
18
|
+
const secondKey = Buffer.alloc(32, 2);
|
|
19
|
+
|
|
20
|
+
const first = fingerprintSecretValue('secret-value', firstKey);
|
|
21
|
+
const same = fingerprintSecretValue('secret-value', firstKey);
|
|
22
|
+
const differentKey = fingerprintSecretValue('secret-value', secondKey);
|
|
23
|
+
|
|
24
|
+
expect(same).toBe(first);
|
|
25
|
+
expect(differentKey).not.toBe(first);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const FINGERPRINT_PREFIX = 'hmac-sha256:';
|
|
4
|
+
const FINGERPRINT_DOMAIN = 'shipfox:secrets:value-fingerprint:v1';
|
|
5
|
+
|
|
6
|
+
export function fingerprintSecretValue(value: string, key: Buffer): string {
|
|
7
|
+
const digest = crypto
|
|
8
|
+
.createHmac('sha256', key)
|
|
9
|
+
.update(FINGERPRINT_DOMAIN)
|
|
10
|
+
.update('\0')
|
|
11
|
+
.update(value)
|
|
12
|
+
.digest('base64url');
|
|
13
|
+
return `${FINGERPRINT_PREFIX}${digest}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {config} from '#config.js';
|
|
2
|
+
import {decodeBase64Key} from './crypto.js';
|
|
3
|
+
import {DekManager} from './dek-manager.js';
|
|
4
|
+
import {createLocalKeyProvider, type KeyProvider} from './key-provider.js';
|
|
5
|
+
import {createLocalSecretStore} from './local-secret-store.js';
|
|
6
|
+
import {createSecretsManagementApi} from './management.js';
|
|
7
|
+
import {rotateWorkspaceDataKeysWithProvider} from './rotate-kek.js';
|
|
8
|
+
import {createSecretStoreApi} from './secret-store.js';
|
|
9
|
+
import {
|
|
10
|
+
BUILTIN_LOCAL_STORE,
|
|
11
|
+
createSecretStoreResolver,
|
|
12
|
+
type SecretStoreProvider,
|
|
13
|
+
} from './store-resolver.js';
|
|
14
|
+
|
|
15
|
+
export * from './errors.js';
|
|
16
|
+
export type {
|
|
17
|
+
ManagementEntry,
|
|
18
|
+
ManagementKeyParams,
|
|
19
|
+
ManagementListParams,
|
|
20
|
+
ManagementWriteParams,
|
|
21
|
+
} from './management.js';
|
|
22
|
+
export type {DeleteSecretsParams, SetSecretsParams} from './secret-store.js';
|
|
23
|
+
export type {DeleteVariablesParams, SetVariablesParams} from './variable-store.js';
|
|
24
|
+
export {
|
|
25
|
+
deleteVariables,
|
|
26
|
+
getVariable,
|
|
27
|
+
getVariablesByNamespace,
|
|
28
|
+
setVariables,
|
|
29
|
+
} from './variable-store.js';
|
|
30
|
+
export {BUILTIN_LOCAL_STORE, type SecretStoreProvider};
|
|
31
|
+
|
|
32
|
+
let memoizedKeyProvider: KeyProvider | undefined;
|
|
33
|
+
let memoizedDekManager: DekManager | undefined;
|
|
34
|
+
let memoizedLocalStore: SecretStoreProvider | undefined;
|
|
35
|
+
let memoizedSecretApi: ReturnType<typeof createSecretStoreApi> | undefined;
|
|
36
|
+
let memoizedManagementApi: ReturnType<typeof createSecretsManagementApi> | undefined;
|
|
37
|
+
|
|
38
|
+
export function keyProvider(): KeyProvider {
|
|
39
|
+
if (memoizedKeyProvider) return memoizedKeyProvider;
|
|
40
|
+
memoizedKeyProvider = createLocalKeyProvider({
|
|
41
|
+
currentKek: decodeBase64Key(config.SECRETS_ENCRYPTION_KEK, 'SECRETS_ENCRYPTION_KEK'),
|
|
42
|
+
previousKek: config.SECRETS_ENCRYPTION_KEK_PREVIOUS
|
|
43
|
+
? decodeBase64Key(config.SECRETS_ENCRYPTION_KEK_PREVIOUS, 'SECRETS_ENCRYPTION_KEK_PREVIOUS')
|
|
44
|
+
: undefined,
|
|
45
|
+
});
|
|
46
|
+
return memoizedKeyProvider;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function dekManager(): DekManager {
|
|
50
|
+
if (memoizedDekManager) return memoizedDekManager;
|
|
51
|
+
memoizedDekManager = new DekManager(keyProvider(), {
|
|
52
|
+
maxEntries: 1000,
|
|
53
|
+
ttlMs: 5 * 60 * 1000,
|
|
54
|
+
});
|
|
55
|
+
return memoizedDekManager;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function localSecretStore(): SecretStoreProvider {
|
|
59
|
+
if (memoizedLocalStore) return memoizedLocalStore;
|
|
60
|
+
memoizedLocalStore = createLocalSecretStore({dekManager: dekManager()});
|
|
61
|
+
return memoizedLocalStore;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveSecretStore(name?: string | undefined): SecretStoreProvider {
|
|
65
|
+
return createSecretStoreResolver(localSecretStore())(name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function secretApi(): ReturnType<typeof createSecretStoreApi> {
|
|
69
|
+
if (memoizedSecretApi) return memoizedSecretApi;
|
|
70
|
+
memoizedSecretApi = createSecretStoreApi({
|
|
71
|
+
dekManager: dekManager(),
|
|
72
|
+
resolveSecretStore,
|
|
73
|
+
});
|
|
74
|
+
return memoizedSecretApi;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function managementApi(): ReturnType<typeof createSecretsManagementApi> {
|
|
78
|
+
if (memoizedManagementApi) return memoizedManagementApi;
|
|
79
|
+
memoizedManagementApi = createSecretsManagementApi({dekManager: dekManager()});
|
|
80
|
+
return memoizedManagementApi;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getSecret(
|
|
84
|
+
...args: Parameters<ReturnType<typeof createSecretStoreApi>['getSecret']>
|
|
85
|
+
) {
|
|
86
|
+
return secretApi().getSecret(...args);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getSecretsByNamespace(
|
|
90
|
+
...args: Parameters<ReturnType<typeof createSecretStoreApi>['getSecretsByNamespace']>
|
|
91
|
+
) {
|
|
92
|
+
return secretApi().getSecretsByNamespace(...args);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function setSecrets(
|
|
96
|
+
...args: Parameters<ReturnType<typeof createSecretStoreApi>['setSecrets']>
|
|
97
|
+
) {
|
|
98
|
+
return secretApi().setSecrets(...args);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function deleteSecrets(
|
|
102
|
+
...args: Parameters<ReturnType<typeof createSecretStoreApi>['deleteSecrets']>
|
|
103
|
+
) {
|
|
104
|
+
return secretApi().deleteSecrets(...args);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function listManagedSecrets(
|
|
108
|
+
...args: Parameters<ReturnType<typeof createSecretsManagementApi>['listSecrets']>
|
|
109
|
+
) {
|
|
110
|
+
return managementApi().listSecrets(...args);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function listManagedVariables(
|
|
114
|
+
...args: Parameters<ReturnType<typeof createSecretsManagementApi>['listVariables']>
|
|
115
|
+
) {
|
|
116
|
+
return managementApi().listVariables(...args);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getManagedVariable(
|
|
120
|
+
...args: Parameters<ReturnType<typeof createSecretsManagementApi>['getVariable']>
|
|
121
|
+
) {
|
|
122
|
+
return managementApi().getVariable(...args);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function setManagedSecrets(
|
|
126
|
+
...args: Parameters<ReturnType<typeof createSecretsManagementApi>['setSecrets']>
|
|
127
|
+
) {
|
|
128
|
+
return managementApi().setSecrets(...args);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function setManagedVariables(
|
|
132
|
+
...args: Parameters<ReturnType<typeof createSecretsManagementApi>['setVariables']>
|
|
133
|
+
) {
|
|
134
|
+
return managementApi().setVariables(...args);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function deleteManagedSecret(
|
|
138
|
+
...args: Parameters<ReturnType<typeof createSecretsManagementApi>['deleteSecret']>
|
|
139
|
+
) {
|
|
140
|
+
return managementApi().deleteSecret(...args);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function deleteManagedVariable(
|
|
144
|
+
...args: Parameters<ReturnType<typeof createSecretsManagementApi>['deleteVariable']>
|
|
145
|
+
) {
|
|
146
|
+
return managementApi().deleteVariable(...args);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function rotateWorkspaceDataKeys() {
|
|
150
|
+
return rotateWorkspaceDataKeysWithProvider(keyProvider());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export {rotateWorkspaceDataKeysWithProvider};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import {describe, expect, it} from '@shipfox/vitest/vi';
|
|
3
|
+
import {DekUnwrapError} from './errors.js';
|
|
4
|
+
import {createLocalKeyProvider} from './key-provider.js';
|
|
5
|
+
|
|
6
|
+
describe('local key provider', () => {
|
|
7
|
+
it('wraps with the current KEK and unwraps with current or previous KEK', () => {
|
|
8
|
+
const currentKek = crypto.randomBytes(32);
|
|
9
|
+
const previousKek = crypto.randomBytes(32);
|
|
10
|
+
const provider = createLocalKeyProvider({currentKek, previousKek});
|
|
11
|
+
const previousProvider = createLocalKeyProvider({currentKek: previousKek});
|
|
12
|
+
const workspaceId = crypto.randomUUID();
|
|
13
|
+
const dek = crypto.randomBytes(32);
|
|
14
|
+
|
|
15
|
+
const previousWrapped = previousProvider.wrapDek(workspaceId, dek);
|
|
16
|
+
const unwrappedPrevious = provider.unwrapDek(
|
|
17
|
+
workspaceId,
|
|
18
|
+
previousWrapped.wrappedDek,
|
|
19
|
+
previousWrapped.kekVersion,
|
|
20
|
+
);
|
|
21
|
+
const currentWrapped = provider.wrapDek(workspaceId, unwrappedPrevious);
|
|
22
|
+
const unwrappedCurrent = provider.unwrapDek(
|
|
23
|
+
workspaceId,
|
|
24
|
+
currentWrapped.wrappedDek,
|
|
25
|
+
currentWrapped.kekVersion,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(currentWrapped.kekVersion).toBe(provider.currentKeyVersion);
|
|
29
|
+
expect(previousWrapped.kekVersion).toBe(provider.previousKeyVersion);
|
|
30
|
+
expect(unwrappedCurrent.equals(dek)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('rejects unknown versions and wrong workspace AAD', () => {
|
|
34
|
+
const provider = createLocalKeyProvider({currentKek: crypto.randomBytes(32)});
|
|
35
|
+
const wrapped = provider.wrapDek(crypto.randomUUID(), crypto.randomBytes(32));
|
|
36
|
+
|
|
37
|
+
expect(() =>
|
|
38
|
+
provider.unwrapDek(crypto.randomUUID(), wrapped.wrappedDek, wrapped.kekVersion),
|
|
39
|
+
).toThrow(DekUnwrapError);
|
|
40
|
+
expect(() =>
|
|
41
|
+
provider.unwrapDek(crypto.randomUUID(), wrapped.wrappedDek, 'local:unknown'),
|
|
42
|
+
).toThrow(DekUnwrapError);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import {aadForDek, aesGcmOpen, aesGcmSeal} from './crypto.js';
|
|
3
|
+
import {DekUnwrapError, DekWrapError} from './errors.js';
|
|
4
|
+
|
|
5
|
+
const KEK_VERSION_DOMAIN = 'shipfox-secrets-kek-version';
|
|
6
|
+
|
|
7
|
+
export interface WrappedDek {
|
|
8
|
+
wrappedDek: string;
|
|
9
|
+
kekVersion: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface KeyProvider {
|
|
13
|
+
readonly currentKeyVersion: string;
|
|
14
|
+
readonly previousKeyVersion: string | null;
|
|
15
|
+
wrapDek(workspaceId: string, plaintextDek: Buffer): WrappedDek;
|
|
16
|
+
unwrapDek(workspaceId: string, wrappedDek: string, kekVersion: string): Buffer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LocalKeyProviderParams {
|
|
20
|
+
currentKek: Buffer;
|
|
21
|
+
previousKek?: Buffer | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createLocalKeyProvider(params: LocalKeyProviderParams): KeyProvider {
|
|
25
|
+
const currentKeyVersion = deriveLocalKekVersion(params.currentKek);
|
|
26
|
+
const previousKeyVersion = params.previousKek ? deriveLocalKekVersion(params.previousKek) : null;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
currentKeyVersion,
|
|
30
|
+
previousKeyVersion,
|
|
31
|
+
wrapDek(workspaceId, plaintextDek) {
|
|
32
|
+
try {
|
|
33
|
+
return {
|
|
34
|
+
wrappedDek: aesGcmSeal({
|
|
35
|
+
key: params.currentKek,
|
|
36
|
+
plaintext: plaintextDek,
|
|
37
|
+
aad: aadForDek(workspaceId, currentKeyVersion),
|
|
38
|
+
}),
|
|
39
|
+
kekVersion: currentKeyVersion,
|
|
40
|
+
};
|
|
41
|
+
} catch {
|
|
42
|
+
throw new DekWrapError();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
unwrapDek(workspaceId, wrappedDek, kekVersion) {
|
|
46
|
+
const key =
|
|
47
|
+
kekVersion === currentKeyVersion
|
|
48
|
+
? params.currentKek
|
|
49
|
+
: kekVersion === previousKeyVersion
|
|
50
|
+
? params.previousKek
|
|
51
|
+
: undefined;
|
|
52
|
+
if (!key) throw new DekUnwrapError();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
return aesGcmOpen({
|
|
56
|
+
key,
|
|
57
|
+
encoded: wrappedDek,
|
|
58
|
+
aad: aadForDek(workspaceId, kekVersion),
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
throw new DekUnwrapError();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function deriveLocalKekVersion(kek: Buffer): string {
|
|
68
|
+
const hash = crypto.createHash('sha256').update(KEK_VERSION_DOMAIN).update(kek).digest('hex');
|
|
69
|
+
return `local:${hash.slice(0, 16)}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSecretValueRowWithPrecedence,
|
|
3
|
+
listSecretValueRowsByNamespace,
|
|
4
|
+
type StoreScope,
|
|
5
|
+
} from '#db/index.js';
|
|
6
|
+
import type {DekManager} from './dek-manager.js';
|
|
7
|
+
import {decryptSecretValue} from './value-cipher.js';
|
|
8
|
+
|
|
9
|
+
export interface LocalSecretStoreParams {
|
|
10
|
+
dekManager: DekManager;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createLocalSecretStore(params: LocalSecretStoreParams) {
|
|
14
|
+
return {
|
|
15
|
+
async getSecret(input: StoreScope & {workspaceId: string; namespace: string; key: string}) {
|
|
16
|
+
const row = await getSecretValueRowWithPrecedence(input);
|
|
17
|
+
if (!row) return null;
|
|
18
|
+
|
|
19
|
+
const dek = await params.dekManager.getPlaintextDek(input.workspaceId);
|
|
20
|
+
return decryptSecretValue({
|
|
21
|
+
dek,
|
|
22
|
+
workspaceId: row.workspaceId,
|
|
23
|
+
scope: {projectId: row.projectId},
|
|
24
|
+
namespace: row.namespace,
|
|
25
|
+
key: row.key,
|
|
26
|
+
ciphertext: row.ciphertext,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
async getSecretsByNamespace(input: StoreScope & {workspaceId: string; namespace: string}) {
|
|
30
|
+
const rows = await listSecretValueRowsByNamespace(input);
|
|
31
|
+
const dek =
|
|
32
|
+
rows.length > 0 ? await params.dekManager.getPlaintextDek(input.workspaceId) : null;
|
|
33
|
+
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
rows.map((row) => [
|
|
36
|
+
row.key,
|
|
37
|
+
decryptSecretValue({
|
|
38
|
+
dek: dek as Buffer,
|
|
39
|
+
workspaceId: row.workspaceId,
|
|
40
|
+
scope: {projectId: row.projectId},
|
|
41
|
+
namespace: row.namespace,
|
|
42
|
+
key: row.key,
|
|
43
|
+
ciphertext: row.ciphertext,
|
|
44
|
+
}),
|
|
45
|
+
]),
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {eq} from 'drizzle-orm';
|
|
2
|
+
import {db, secretDataKeys, secretVariables} from '#db/index.js';
|
|
3
|
+
import {setManagedSecrets, setManagedVariables} from './index.js';
|
|
4
|
+
|
|
5
|
+
describe('secrets management core', () => {
|
|
6
|
+
let workspaceId: string;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
workspaceId = crypto.randomUUID();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('treats empty managed writes as no-ops', async () => {
|
|
13
|
+
const secretResult = await setManagedSecrets({
|
|
14
|
+
workspaceId,
|
|
15
|
+
actorId: crypto.randomUUID(),
|
|
16
|
+
entries: [],
|
|
17
|
+
});
|
|
18
|
+
const variableResult = await setManagedVariables({
|
|
19
|
+
workspaceId,
|
|
20
|
+
actorId: crypto.randomUUID(),
|
|
21
|
+
entries: [],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const dataKeys = await db()
|
|
25
|
+
.select()
|
|
26
|
+
.from(secretDataKeys)
|
|
27
|
+
.where(eq(secretDataKeys.workspaceId, workspaceId));
|
|
28
|
+
const variables = await db()
|
|
29
|
+
.select()
|
|
30
|
+
.from(secretVariables)
|
|
31
|
+
.where(eq(secretVariables.workspaceId, workspaceId));
|
|
32
|
+
expect(secretResult).toEqual([]);
|
|
33
|
+
expect(variableResult).toEqual([]);
|
|
34
|
+
expect(dataKeys).toHaveLength(0);
|
|
35
|
+
expect(variables).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
});
|