@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,503 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SECRET_CREATED,
|
|
3
|
+
SECRET_DELETED,
|
|
4
|
+
SECRET_UPDATED,
|
|
5
|
+
type SecretsEventMap,
|
|
6
|
+
VARIABLE_CREATED,
|
|
7
|
+
VARIABLE_DELETED,
|
|
8
|
+
VARIABLE_UPDATED,
|
|
9
|
+
} from '@shipfox/api-secrets-dto';
|
|
10
|
+
import {writeOutboxEvent} from '@shipfox/node-outbox';
|
|
11
|
+
import {
|
|
12
|
+
db,
|
|
13
|
+
deleteSecretManagementRows,
|
|
14
|
+
deleteVariableManagementRows,
|
|
15
|
+
getSecretManagementRow,
|
|
16
|
+
getVariableManagementRow,
|
|
17
|
+
listExistingSecretManagementKeys,
|
|
18
|
+
listExistingVariableManagementKeys,
|
|
19
|
+
listSecretManagementRows,
|
|
20
|
+
listVariableManagementRows,
|
|
21
|
+
lockWorkspaceEntries,
|
|
22
|
+
type SecretManagementRow,
|
|
23
|
+
type StoreScope,
|
|
24
|
+
secretsOutbox,
|
|
25
|
+
type Tx,
|
|
26
|
+
upsertSecretValueRows,
|
|
27
|
+
upsertSecretVariableRows,
|
|
28
|
+
} from '#db/index.js';
|
|
29
|
+
import type {SecretVariable} from '#db/schema/variables.js';
|
|
30
|
+
import {normalizedProjectId} from '#db/scope.js';
|
|
31
|
+
import {
|
|
32
|
+
classifySecretsOperationError,
|
|
33
|
+
operationScope,
|
|
34
|
+
recordSecretsEntriesMutated,
|
|
35
|
+
recordSecretsOperation,
|
|
36
|
+
type SecretsMetricResource,
|
|
37
|
+
} from '#metrics/instance.js';
|
|
38
|
+
import type {DekManager} from './dek-manager.js';
|
|
39
|
+
import {
|
|
40
|
+
SecretBatchDuplicateKeyError,
|
|
41
|
+
SecretNotFoundError,
|
|
42
|
+
VariableNotFoundError,
|
|
43
|
+
} from './errors.js';
|
|
44
|
+
import {fingerprintSecretValue} from './fingerprint.js';
|
|
45
|
+
import {assertWorkspaceCap, validateSecretKeys, validateValueBytes} from './store-validation.js';
|
|
46
|
+
import {encryptSecretValue} from './value-cipher.js';
|
|
47
|
+
|
|
48
|
+
export interface ManagementEntry {
|
|
49
|
+
key: string;
|
|
50
|
+
value: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ManagementWriteParams extends StoreScope {
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
entries: ManagementEntry[];
|
|
56
|
+
actorId: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ManagementKeyParams extends StoreScope {
|
|
60
|
+
workspaceId: string;
|
|
61
|
+
key: string;
|
|
62
|
+
actorId: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ManagementListParams extends StoreScope {
|
|
66
|
+
workspaceId: string;
|
|
67
|
+
limit: number;
|
|
68
|
+
cursor?: string | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createSecretsManagementApi(params: {dekManager: DekManager}) {
|
|
72
|
+
return {
|
|
73
|
+
async listSecrets(input: ManagementListParams) {
|
|
74
|
+
const startedAt = Date.now();
|
|
75
|
+
const scope = operationScope(input);
|
|
76
|
+
try {
|
|
77
|
+
const result = await listSecretManagementRows(input);
|
|
78
|
+
recordSecretsOperation({
|
|
79
|
+
resource: 'secret',
|
|
80
|
+
operation: 'list',
|
|
81
|
+
surface: 'management',
|
|
82
|
+
scope,
|
|
83
|
+
outcome: 'success',
|
|
84
|
+
durationMs: Date.now() - startedAt,
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
recordSecretsOperation({
|
|
89
|
+
resource: 'secret',
|
|
90
|
+
operation: 'list',
|
|
91
|
+
surface: 'management',
|
|
92
|
+
scope,
|
|
93
|
+
outcome: classifySecretsOperationError(error),
|
|
94
|
+
durationMs: Date.now() - startedAt,
|
|
95
|
+
});
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async listVariables(input: ManagementListParams) {
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
const scope = operationScope(input);
|
|
102
|
+
try {
|
|
103
|
+
const result = await listVariableManagementRows(input);
|
|
104
|
+
recordSecretsOperation({
|
|
105
|
+
resource: 'variable',
|
|
106
|
+
operation: 'list',
|
|
107
|
+
surface: 'management',
|
|
108
|
+
scope,
|
|
109
|
+
outcome: 'success',
|
|
110
|
+
durationMs: Date.now() - startedAt,
|
|
111
|
+
});
|
|
112
|
+
return result;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
recordSecretsOperation({
|
|
115
|
+
resource: 'variable',
|
|
116
|
+
operation: 'list',
|
|
117
|
+
surface: 'management',
|
|
118
|
+
scope,
|
|
119
|
+
outcome: classifySecretsOperationError(error),
|
|
120
|
+
durationMs: Date.now() - startedAt,
|
|
121
|
+
});
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
async getVariable(
|
|
126
|
+
input: StoreScope & {workspaceId: string; key: string},
|
|
127
|
+
): Promise<SecretVariable> {
|
|
128
|
+
const startedAt = Date.now();
|
|
129
|
+
const scope = operationScope(input);
|
|
130
|
+
try {
|
|
131
|
+
validateSecretKeys([input.key]);
|
|
132
|
+
|
|
133
|
+
const variable = await getVariableManagementRow(input);
|
|
134
|
+
if (!variable) throw new VariableNotFoundError(input.key);
|
|
135
|
+
recordSecretsOperation({
|
|
136
|
+
resource: 'variable',
|
|
137
|
+
operation: 'get',
|
|
138
|
+
surface: 'management',
|
|
139
|
+
scope,
|
|
140
|
+
outcome: 'success',
|
|
141
|
+
durationMs: Date.now() - startedAt,
|
|
142
|
+
});
|
|
143
|
+
return variable;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
recordSecretsOperation({
|
|
146
|
+
resource: 'variable',
|
|
147
|
+
operation: 'get',
|
|
148
|
+
surface: 'management',
|
|
149
|
+
scope,
|
|
150
|
+
outcome: classifySecretsOperationError(error),
|
|
151
|
+
durationMs: Date.now() - startedAt,
|
|
152
|
+
});
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
async setSecrets(input: ManagementWriteParams): Promise<SecretManagementRow[]> {
|
|
157
|
+
const startedAt = Date.now();
|
|
158
|
+
const scope = operationScope(input);
|
|
159
|
+
try {
|
|
160
|
+
const entries = normalizeEntries(input.entries);
|
|
161
|
+
validateSecretKeys(entries.map((entry) => entry.key));
|
|
162
|
+
validateValueBytes(entries.map((entry) => entry.value));
|
|
163
|
+
if (entries.length === 0) {
|
|
164
|
+
recordSecretsOperation({
|
|
165
|
+
resource: 'secret',
|
|
166
|
+
operation: 'set',
|
|
167
|
+
surface: 'management',
|
|
168
|
+
scope,
|
|
169
|
+
outcome: 'success',
|
|
170
|
+
durationMs: Date.now() - startedAt,
|
|
171
|
+
});
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const projectId = normalizedProjectId(input);
|
|
176
|
+
const dek = await params.dekManager.getPlaintextDek(input.workspaceId);
|
|
177
|
+
const rows = await writeManagementEntries({
|
|
178
|
+
resource: 'secret',
|
|
179
|
+
input,
|
|
180
|
+
keys: entries.map((entry) => entry.key),
|
|
181
|
+
listExistingKeys: listExistingSecretManagementKeys,
|
|
182
|
+
writeRows: async (tx) => {
|
|
183
|
+
await upsertSecretValueRows(
|
|
184
|
+
entries.map((entry) => ({
|
|
185
|
+
workspaceId: input.workspaceId,
|
|
186
|
+
projectId,
|
|
187
|
+
namespace: '',
|
|
188
|
+
key: entry.key,
|
|
189
|
+
ciphertext: encryptSecretValue({
|
|
190
|
+
dek,
|
|
191
|
+
workspaceId: input.workspaceId,
|
|
192
|
+
scope: {projectId},
|
|
193
|
+
namespace: '',
|
|
194
|
+
key: entry.key,
|
|
195
|
+
value: entry.value,
|
|
196
|
+
}),
|
|
197
|
+
fingerprint: fingerprintSecretValue(entry.value, dek),
|
|
198
|
+
lastEditedBy: input.actorId,
|
|
199
|
+
})),
|
|
200
|
+
tx,
|
|
201
|
+
);
|
|
202
|
+
const rows = await Promise.all(
|
|
203
|
+
entries.map((entry) =>
|
|
204
|
+
getSecretManagementRow(
|
|
205
|
+
{workspaceId: input.workspaceId, projectId, key: entry.key},
|
|
206
|
+
tx,
|
|
207
|
+
),
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
return rows.map(assertFound);
|
|
211
|
+
},
|
|
212
|
+
eventTypes: {created: SECRET_CREATED, updated: SECRET_UPDATED},
|
|
213
|
+
});
|
|
214
|
+
recordSecretsOperation({
|
|
215
|
+
resource: 'secret',
|
|
216
|
+
operation: 'set',
|
|
217
|
+
surface: 'management',
|
|
218
|
+
scope,
|
|
219
|
+
outcome: 'success',
|
|
220
|
+
durationMs: Date.now() - startedAt,
|
|
221
|
+
});
|
|
222
|
+
return rows;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
recordSecretsOperation({
|
|
225
|
+
resource: 'secret',
|
|
226
|
+
operation: 'set',
|
|
227
|
+
surface: 'management',
|
|
228
|
+
scope,
|
|
229
|
+
outcome: classifySecretsOperationError(error),
|
|
230
|
+
durationMs: Date.now() - startedAt,
|
|
231
|
+
});
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
async setVariables(input: ManagementWriteParams): Promise<SecretVariable[]> {
|
|
236
|
+
const startedAt = Date.now();
|
|
237
|
+
const scope = operationScope(input);
|
|
238
|
+
try {
|
|
239
|
+
const entries = normalizeEntries(input.entries);
|
|
240
|
+
validateSecretKeys(entries.map((entry) => entry.key));
|
|
241
|
+
validateValueBytes(entries.map((entry) => entry.value));
|
|
242
|
+
if (entries.length === 0) {
|
|
243
|
+
recordSecretsOperation({
|
|
244
|
+
resource: 'variable',
|
|
245
|
+
operation: 'set',
|
|
246
|
+
surface: 'management',
|
|
247
|
+
scope,
|
|
248
|
+
outcome: 'success',
|
|
249
|
+
durationMs: Date.now() - startedAt,
|
|
250
|
+
});
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const projectId = normalizedProjectId(input);
|
|
255
|
+
const rows = await writeManagementEntries({
|
|
256
|
+
resource: 'variable',
|
|
257
|
+
input,
|
|
258
|
+
keys: entries.map((entry) => entry.key),
|
|
259
|
+
listExistingKeys: listExistingVariableManagementKeys,
|
|
260
|
+
writeRows: async (tx) => {
|
|
261
|
+
await upsertSecretVariableRows(
|
|
262
|
+
entries.map((entry) => ({
|
|
263
|
+
workspaceId: input.workspaceId,
|
|
264
|
+
projectId,
|
|
265
|
+
namespace: '',
|
|
266
|
+
key: entry.key,
|
|
267
|
+
value: entry.value,
|
|
268
|
+
lastEditedBy: input.actorId,
|
|
269
|
+
})),
|
|
270
|
+
tx,
|
|
271
|
+
);
|
|
272
|
+
const rows = await Promise.all(
|
|
273
|
+
entries.map((entry) =>
|
|
274
|
+
getVariableManagementRow(
|
|
275
|
+
{workspaceId: input.workspaceId, projectId, key: entry.key},
|
|
276
|
+
tx,
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
return rows.map(assertFound);
|
|
281
|
+
},
|
|
282
|
+
eventTypes: {created: VARIABLE_CREATED, updated: VARIABLE_UPDATED},
|
|
283
|
+
});
|
|
284
|
+
recordSecretsOperation({
|
|
285
|
+
resource: 'variable',
|
|
286
|
+
operation: 'set',
|
|
287
|
+
surface: 'management',
|
|
288
|
+
scope,
|
|
289
|
+
outcome: 'success',
|
|
290
|
+
durationMs: Date.now() - startedAt,
|
|
291
|
+
});
|
|
292
|
+
return rows;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
recordSecretsOperation({
|
|
295
|
+
resource: 'variable',
|
|
296
|
+
operation: 'set',
|
|
297
|
+
surface: 'management',
|
|
298
|
+
scope,
|
|
299
|
+
outcome: classifySecretsOperationError(error),
|
|
300
|
+
durationMs: Date.now() - startedAt,
|
|
301
|
+
});
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
async deleteSecret(input: ManagementKeyParams): Promise<void> {
|
|
306
|
+
const startedAt = Date.now();
|
|
307
|
+
const scope = operationScope(input);
|
|
308
|
+
try {
|
|
309
|
+
validateSecretKeys([input.key]);
|
|
310
|
+
|
|
311
|
+
const deleted = await deleteManagementEntries({
|
|
312
|
+
resource: 'secret',
|
|
313
|
+
input,
|
|
314
|
+
keys: [input.key],
|
|
315
|
+
deleteRows: deleteSecretManagementRows,
|
|
316
|
+
eventType: SECRET_DELETED,
|
|
317
|
+
});
|
|
318
|
+
if (deleted === 0) throw new SecretNotFoundError(input.key);
|
|
319
|
+
recordSecretsOperation({
|
|
320
|
+
resource: 'secret',
|
|
321
|
+
operation: 'delete',
|
|
322
|
+
surface: 'management',
|
|
323
|
+
scope,
|
|
324
|
+
outcome: 'success',
|
|
325
|
+
durationMs: Date.now() - startedAt,
|
|
326
|
+
});
|
|
327
|
+
} catch (error) {
|
|
328
|
+
recordSecretsOperation({
|
|
329
|
+
resource: 'secret',
|
|
330
|
+
operation: 'delete',
|
|
331
|
+
surface: 'management',
|
|
332
|
+
scope,
|
|
333
|
+
outcome: classifySecretsOperationError(error),
|
|
334
|
+
durationMs: Date.now() - startedAt,
|
|
335
|
+
});
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
async deleteVariable(input: ManagementKeyParams): Promise<void> {
|
|
340
|
+
const startedAt = Date.now();
|
|
341
|
+
const scope = operationScope(input);
|
|
342
|
+
try {
|
|
343
|
+
validateSecretKeys([input.key]);
|
|
344
|
+
|
|
345
|
+
const deleted = await deleteManagementEntries({
|
|
346
|
+
resource: 'variable',
|
|
347
|
+
input,
|
|
348
|
+
keys: [input.key],
|
|
349
|
+
deleteRows: deleteVariableManagementRows,
|
|
350
|
+
eventType: VARIABLE_DELETED,
|
|
351
|
+
});
|
|
352
|
+
if (deleted === 0) throw new VariableNotFoundError(input.key);
|
|
353
|
+
recordSecretsOperation({
|
|
354
|
+
resource: 'variable',
|
|
355
|
+
operation: 'delete',
|
|
356
|
+
surface: 'management',
|
|
357
|
+
scope,
|
|
358
|
+
outcome: 'success',
|
|
359
|
+
durationMs: Date.now() - startedAt,
|
|
360
|
+
});
|
|
361
|
+
} catch (error) {
|
|
362
|
+
recordSecretsOperation({
|
|
363
|
+
resource: 'variable',
|
|
364
|
+
operation: 'delete',
|
|
365
|
+
surface: 'management',
|
|
366
|
+
scope,
|
|
367
|
+
outcome: classifySecretsOperationError(error),
|
|
368
|
+
durationMs: Date.now() - startedAt,
|
|
369
|
+
});
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function writeManagementEntries<Row extends {key: string}>(params: {
|
|
377
|
+
resource: SecretsMetricResource;
|
|
378
|
+
input: ManagementWriteParams;
|
|
379
|
+
keys: string[];
|
|
380
|
+
listExistingKeys: (
|
|
381
|
+
params: StoreScope & {workspaceId: string; keys: string[]},
|
|
382
|
+
tx: Tx,
|
|
383
|
+
) => Promise<Set<string>>;
|
|
384
|
+
writeRows: (tx: Tx) => Promise<Row[]>;
|
|
385
|
+
eventTypes: {
|
|
386
|
+
created: typeof SECRET_CREATED | typeof VARIABLE_CREATED;
|
|
387
|
+
updated: typeof SECRET_UPDATED | typeof VARIABLE_UPDATED;
|
|
388
|
+
};
|
|
389
|
+
}): Promise<Row[]> {
|
|
390
|
+
let createdCount = 0;
|
|
391
|
+
let updatedCount = 0;
|
|
392
|
+
const rows = await db().transaction(async (tx) => {
|
|
393
|
+
await lockWorkspaceEntries(params.input.workspaceId, tx);
|
|
394
|
+
const existingKeys = await params.listExistingKeys(
|
|
395
|
+
{
|
|
396
|
+
workspaceId: params.input.workspaceId,
|
|
397
|
+
projectId: normalizedProjectId(params.input),
|
|
398
|
+
keys: params.keys,
|
|
399
|
+
},
|
|
400
|
+
tx,
|
|
401
|
+
);
|
|
402
|
+
createdCount = params.keys.length - existingKeys.size;
|
|
403
|
+
updatedCount = existingKeys.size;
|
|
404
|
+
await assertWorkspaceCap({
|
|
405
|
+
workspaceId: params.input.workspaceId,
|
|
406
|
+
namespace: '',
|
|
407
|
+
incomingEntries: createdCount,
|
|
408
|
+
tx,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const rows = await params.writeRows(tx);
|
|
412
|
+
await Promise.all(
|
|
413
|
+
params.keys.map((key) =>
|
|
414
|
+
writeOutboxEvent<SecretsEventMap>(tx, secretsOutbox, {
|
|
415
|
+
type: existingKeys.has(key) ? params.eventTypes.updated : params.eventTypes.created,
|
|
416
|
+
payload: managementEventPayload(params.input, key),
|
|
417
|
+
}),
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
return rows;
|
|
421
|
+
});
|
|
422
|
+
recordSecretsEntriesMutated({
|
|
423
|
+
resource: params.resource,
|
|
424
|
+
operation: 'set',
|
|
425
|
+
effect: 'created',
|
|
426
|
+
surface: 'management',
|
|
427
|
+
count: createdCount,
|
|
428
|
+
});
|
|
429
|
+
recordSecretsEntriesMutated({
|
|
430
|
+
resource: params.resource,
|
|
431
|
+
operation: 'set',
|
|
432
|
+
effect: 'updated',
|
|
433
|
+
surface: 'management',
|
|
434
|
+
count: updatedCount,
|
|
435
|
+
});
|
|
436
|
+
return rows;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function deleteManagementEntries<Row extends {key: string}>(params: {
|
|
440
|
+
resource: SecretsMetricResource;
|
|
441
|
+
input: ManagementKeyParams;
|
|
442
|
+
keys: string[];
|
|
443
|
+
deleteRows: (
|
|
444
|
+
params: StoreScope & {workspaceId: string; keys: string[]},
|
|
445
|
+
tx: Tx,
|
|
446
|
+
) => Promise<Row[]>;
|
|
447
|
+
eventType: typeof SECRET_DELETED | typeof VARIABLE_DELETED;
|
|
448
|
+
}): Promise<number> {
|
|
449
|
+
const deleted = await db().transaction(async (tx) => {
|
|
450
|
+
await lockWorkspaceEntries(params.input.workspaceId, tx);
|
|
451
|
+
const rows = await params.deleteRows(
|
|
452
|
+
{
|
|
453
|
+
workspaceId: params.input.workspaceId,
|
|
454
|
+
projectId: normalizedProjectId(params.input),
|
|
455
|
+
keys: params.keys,
|
|
456
|
+
},
|
|
457
|
+
tx,
|
|
458
|
+
);
|
|
459
|
+
await Promise.all(
|
|
460
|
+
rows.map((row) =>
|
|
461
|
+
writeOutboxEvent<SecretsEventMap>(tx, secretsOutbox, {
|
|
462
|
+
type: params.eventType,
|
|
463
|
+
payload: managementEventPayload(params.input, row.key),
|
|
464
|
+
}),
|
|
465
|
+
),
|
|
466
|
+
);
|
|
467
|
+
return rows.length;
|
|
468
|
+
});
|
|
469
|
+
recordSecretsEntriesMutated({
|
|
470
|
+
resource: params.resource,
|
|
471
|
+
operation: 'delete',
|
|
472
|
+
effect: 'deleted',
|
|
473
|
+
surface: 'management',
|
|
474
|
+
count: deleted,
|
|
475
|
+
});
|
|
476
|
+
return deleted;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function normalizeEntries(entries: ManagementEntry[]): ManagementEntry[] {
|
|
480
|
+
const valuesByKey = new Map<string, string>();
|
|
481
|
+
for (const entry of entries) {
|
|
482
|
+
if (valuesByKey.has(entry.key)) throw new SecretBatchDuplicateKeyError(entry.key);
|
|
483
|
+
valuesByKey.set(entry.key, entry.value);
|
|
484
|
+
}
|
|
485
|
+
return [...valuesByKey].map(([key, value]) => ({key, value}));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function managementEventPayload(
|
|
489
|
+
input: Pick<ManagementWriteParams, 'workspaceId' | 'projectId' | 'actorId'>,
|
|
490
|
+
key: string,
|
|
491
|
+
) {
|
|
492
|
+
return {
|
|
493
|
+
actorId: input.actorId,
|
|
494
|
+
workspaceId: input.workspaceId,
|
|
495
|
+
projectId: normalizedProjectId(input),
|
|
496
|
+
key,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function assertFound<Row>(row: Row | undefined): Row {
|
|
501
|
+
if (!row) throw new Error('Management write returned no row');
|
|
502
|
+
return row;
|
|
503
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import {beforeEach, describe, expect, it} from '@shipfox/vitest/vi';
|
|
3
|
+
import {eq} from 'drizzle-orm';
|
|
4
|
+
import {
|
|
5
|
+
db,
|
|
6
|
+
listDataKeysPage,
|
|
7
|
+
secretDataKeys,
|
|
8
|
+
secretValues,
|
|
9
|
+
updateDataKeyWrapCas,
|
|
10
|
+
} from '#db/index.js';
|
|
11
|
+
import {DekManager} from './dek-manager.js';
|
|
12
|
+
import {KekVersionStrandedError} from './errors.js';
|
|
13
|
+
import {createLocalKeyProvider} from './key-provider.js';
|
|
14
|
+
import {createLocalSecretStore} from './local-secret-store.js';
|
|
15
|
+
import {rotateWorkspaceDataKeysWithProvider} from './rotate-kek.js';
|
|
16
|
+
import {encryptSecretValue} from './value-cipher.js';
|
|
17
|
+
|
|
18
|
+
describe('rotateWorkspaceDataKeysWithProvider', () => {
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await db().delete(secretDataKeys);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rewraps previous-version data keys under the current KEK', async () => {
|
|
24
|
+
const workspaceId = crypto.randomUUID();
|
|
25
|
+
const currentKek = crypto.randomBytes(32);
|
|
26
|
+
const previousKek = crypto.randomBytes(32);
|
|
27
|
+
const currentProvider = createLocalKeyProvider({currentKek, previousKek});
|
|
28
|
+
const previousProvider = createLocalKeyProvider({currentKek: previousKek});
|
|
29
|
+
const dek = crypto.randomBytes(32);
|
|
30
|
+
const previousWrapped = previousProvider.wrapDek(workspaceId, dek);
|
|
31
|
+
await db().insert(secretDataKeys).values({
|
|
32
|
+
workspaceId,
|
|
33
|
+
wrappedDek: previousWrapped.wrappedDek,
|
|
34
|
+
kekVersion: previousWrapped.kekVersion,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = await rotateWorkspaceDataKeysWithProvider(currentProvider, {
|
|
38
|
+
workspaceIds: [workspaceId],
|
|
39
|
+
});
|
|
40
|
+
const rows = await db()
|
|
41
|
+
.select()
|
|
42
|
+
.from(secretDataKeys)
|
|
43
|
+
.where(eq(secretDataKeys.workspaceId, workspaceId));
|
|
44
|
+
const row = rows[0];
|
|
45
|
+
if (!row) throw new Error('Expected rotated row');
|
|
46
|
+
const unwrapped = currentProvider.unwrapDek(row.workspaceId, row.wrappedDek, row.kekVersion);
|
|
47
|
+
|
|
48
|
+
expect(result.rotated).toBe(1);
|
|
49
|
+
expect(row.kekVersion).toBe(currentProvider.currentKeyVersion);
|
|
50
|
+
expect(row.wrappedDek).not.toBe(previousWrapped.wrappedDek);
|
|
51
|
+
expect(row.rotatedAt).toBeInstanceOf(Date);
|
|
52
|
+
expect(unwrapped.equals(dek)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('preserves value decryption after rewrapping the data key', async () => {
|
|
56
|
+
const workspaceId = crypto.randomUUID();
|
|
57
|
+
const currentKek = crypto.randomBytes(32);
|
|
58
|
+
const previousKek = crypto.randomBytes(32);
|
|
59
|
+
const currentProvider = createLocalKeyProvider({currentKek, previousKek});
|
|
60
|
+
const previousProvider = createLocalKeyProvider({currentKek: previousKek});
|
|
61
|
+
const dek = crypto.randomBytes(32);
|
|
62
|
+
const previousWrapped = previousProvider.wrapDek(workspaceId, dek);
|
|
63
|
+
await db().insert(secretDataKeys).values({
|
|
64
|
+
workspaceId,
|
|
65
|
+
wrappedDek: previousWrapped.wrappedDek,
|
|
66
|
+
kekVersion: previousWrapped.kekVersion,
|
|
67
|
+
});
|
|
68
|
+
await db()
|
|
69
|
+
.insert(secretValues)
|
|
70
|
+
.values({
|
|
71
|
+
workspaceId,
|
|
72
|
+
projectId: null,
|
|
73
|
+
namespace: '',
|
|
74
|
+
key: 'TOKEN',
|
|
75
|
+
ciphertext: encryptSecretValue({
|
|
76
|
+
dek,
|
|
77
|
+
workspaceId,
|
|
78
|
+
scope: {projectId: null},
|
|
79
|
+
namespace: '',
|
|
80
|
+
key: 'TOKEN',
|
|
81
|
+
value: 'still-readable',
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await rotateWorkspaceDataKeysWithProvider(currentProvider, {workspaceIds: [workspaceId]});
|
|
86
|
+
const localStore = createLocalSecretStore({
|
|
87
|
+
dekManager: new DekManager(currentProvider, {maxEntries: 10, ttlMs: 60_000}),
|
|
88
|
+
});
|
|
89
|
+
const value = await localStore.getSecret({workspaceId, namespace: '', key: 'TOKEN'});
|
|
90
|
+
|
|
91
|
+
expect(value).toBe('still-readable');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('is idempotent after rotating and is a no-op over an empty key set', async () => {
|
|
95
|
+
const workspaceId = crypto.randomUUID();
|
|
96
|
+
const emptyResult = await rotateWorkspaceDataKeysWithProvider(
|
|
97
|
+
createLocalKeyProvider({currentKek: crypto.randomBytes(32)}),
|
|
98
|
+
{workspaceIds: [workspaceId]},
|
|
99
|
+
);
|
|
100
|
+
const currentKek = crypto.randomBytes(32);
|
|
101
|
+
const previousKek = crypto.randomBytes(32);
|
|
102
|
+
const currentProvider = createLocalKeyProvider({currentKek, previousKek});
|
|
103
|
+
const previousProvider = createLocalKeyProvider({currentKek: previousKek});
|
|
104
|
+
const previousWrapped = previousProvider.wrapDek(workspaceId, crypto.randomBytes(32));
|
|
105
|
+
await db().insert(secretDataKeys).values({
|
|
106
|
+
workspaceId,
|
|
107
|
+
wrappedDek: previousWrapped.wrappedDek,
|
|
108
|
+
kekVersion: previousWrapped.kekVersion,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const first = await rotateWorkspaceDataKeysWithProvider(currentProvider, {
|
|
112
|
+
workspaceIds: [workspaceId],
|
|
113
|
+
});
|
|
114
|
+
const second = await rotateWorkspaceDataKeysWithProvider(currentProvider, {
|
|
115
|
+
workspaceIds: [workspaceId],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(emptyResult).toEqual({rotated: 0, skipped: 0});
|
|
119
|
+
expect(first.rotated).toBe(1);
|
|
120
|
+
expect(second).toEqual({rotated: 0, skipped: 1});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not clobber a data key after a concurrent rotation wins', async () => {
|
|
124
|
+
const workspaceId = crypto.randomUUID();
|
|
125
|
+
const currentProvider = createLocalKeyProvider({currentKek: crypto.randomBytes(32)});
|
|
126
|
+
const oldProvider = createLocalKeyProvider({currentKek: crypto.randomBytes(32)});
|
|
127
|
+
const oldWrapped = oldProvider.wrapDek(workspaceId, crypto.randomBytes(32));
|
|
128
|
+
const freshWrapped = currentProvider.wrapDek(workspaceId, crypto.randomBytes(32));
|
|
129
|
+
await db().insert(secretDataKeys).values({
|
|
130
|
+
workspaceId,
|
|
131
|
+
wrappedDek: freshWrapped.wrappedDek,
|
|
132
|
+
kekVersion: freshWrapped.kekVersion,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const updated = await updateDataKeyWrapCas({
|
|
136
|
+
workspaceId,
|
|
137
|
+
oldKekVersion: oldWrapped.kekVersion,
|
|
138
|
+
wrappedDek: oldWrapped.wrappedDek,
|
|
139
|
+
kekVersion: oldWrapped.kekVersion,
|
|
140
|
+
});
|
|
141
|
+
const rows = await db()
|
|
142
|
+
.select()
|
|
143
|
+
.from(secretDataKeys)
|
|
144
|
+
.where(eq(secretDataKeys.workspaceId, workspaceId));
|
|
145
|
+
|
|
146
|
+
expect(updated).toBe(false);
|
|
147
|
+
expect(rows[0]?.wrappedDek).toBe(freshWrapped.wrappedDek);
|
|
148
|
+
expect(rows[0]?.kekVersion).toBe(freshWrapped.kekVersion);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('treats an empty version filter as matching no data keys', async () => {
|
|
152
|
+
const workspaceId = crypto.randomUUID();
|
|
153
|
+
const wrapped = createLocalKeyProvider({currentKek: crypto.randomBytes(32)}).wrapDek(
|
|
154
|
+
workspaceId,
|
|
155
|
+
crypto.randomBytes(32),
|
|
156
|
+
);
|
|
157
|
+
await db().insert(secretDataKeys).values({
|
|
158
|
+
workspaceId,
|
|
159
|
+
wrappedDek: wrapped.wrappedDek,
|
|
160
|
+
kekVersion: wrapped.kekVersion,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const rows = await listDataKeysPage({limit: 10, versions: []});
|
|
164
|
+
|
|
165
|
+
expect(rows).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('fails loud when any data key version is stranded', async () => {
|
|
169
|
+
const workspaceId = crypto.randomUUID();
|
|
170
|
+
await db().insert(secretDataKeys).values({
|
|
171
|
+
workspaceId,
|
|
172
|
+
wrappedDek: 'v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
173
|
+
kekVersion: 'local:stranded',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await expect(
|
|
177
|
+
rotateWorkspaceDataKeysWithProvider(
|
|
178
|
+
createLocalKeyProvider({currentKek: crypto.randomBytes(32)}),
|
|
179
|
+
{workspaceIds: [workspaceId]},
|
|
180
|
+
),
|
|
181
|
+
).rejects.toThrow(KekVersionStrandedError);
|
|
182
|
+
});
|
|
183
|
+
});
|