@coffre/vault 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +26 -2
- package/dist/cloudflare.d.ts +55 -0
- package/dist/cloudflare.js +73 -0
- package/dist/index-qXbB_vlp.d.ts +40 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/node.d.ts +48 -0
- package/dist/node.js +142 -0
- package/dist/vault-DvTsSAOX.js +2225 -0
- package/package.json +51 -3
- package/src/accounting.ts +110 -0
- package/src/checkpoint.ts +25 -0
- package/src/cloudflare-workers.d.ts +14 -0
- package/src/cloudflare.ts +89 -0
- package/src/config.ts +146 -0
- package/src/index.ts +6 -0
- package/src/local.ts +48 -0
- package/src/log.ts +140 -0
- package/src/node.ts +141 -0
- package/src/replay.ts +150 -0
- package/src/rows.ts +67 -0
- package/src/store.ts +420 -0
- package/src/vault.ts +1564 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import type { Author, StoredEntry } from '@coffre/core/audit';
|
|
2
|
+
import { ACCESS_ACTIONS, type SecretRef, type WrappedKey } from '@coffre/core/vault';
|
|
3
|
+
import { tablesOf, type Queryable, type Transaction } from '@coffre/db';
|
|
4
|
+
import { clockMillis, engineOf, forUpdate } from '@coffre/db/dialect';
|
|
5
|
+
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, sql } from 'drizzle-orm';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every query the vault makes, and the only code in it that holds them.
|
|
9
|
+
* Each reads or writes one thing; what they add up to, a decision, is in
|
|
10
|
+
* `vault.ts`. The tables are the shared database's: `vault_members` and
|
|
11
|
+
* `vault_grants`, which only the vault writes, the places it decides on,
|
|
12
|
+
* and the audit log, where its entries are the ones `author = 'vault'`.
|
|
13
|
+
*
|
|
14
|
+
* Times are milliseconds since the epoch, from the database's clock.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A member's row; schema.ts in @coffre/db says what each column means. */
|
|
18
|
+
export type Member = {
|
|
19
|
+
principal: string;
|
|
20
|
+
status: 'active' | 'removed';
|
|
21
|
+
owner: boolean;
|
|
22
|
+
generation: number;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
createdBy: string;
|
|
25
|
+
statusChangedAt: number;
|
|
26
|
+
statusChangedBy: string;
|
|
27
|
+
/** The vault's last access entry about the member. */
|
|
28
|
+
accessSeq: bigint;
|
|
29
|
+
/** The vault's MAC over this row and the member's grants; rows.ts. */
|
|
30
|
+
mac: Buffer;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Every entry that changes who is a member or what they hold. */
|
|
34
|
+
export { ACCESS_ACTIONS };
|
|
35
|
+
|
|
36
|
+
/** Where a grant applies: a project (`environmentId` null), or one of its environments. */
|
|
37
|
+
export type Place = { projectId: string; environmentId: string | null };
|
|
38
|
+
|
|
39
|
+
export type GrantRow = Place & {
|
|
40
|
+
principal: string;
|
|
41
|
+
role: string;
|
|
42
|
+
expiresAt: number | null;
|
|
43
|
+
grantedAt: number;
|
|
44
|
+
grantedBy: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* On Postgres, fail a lock wait in this transaction after `ms`, rather than
|
|
49
|
+
* wait for ever behind a transaction that never ends.
|
|
50
|
+
*/
|
|
51
|
+
export async function boundLockWaits(tx: Transaction, ms: number): Promise<void> {
|
|
52
|
+
if (engineOf(tx) === 'postgres') await tx.execute(sql.raw(`SET LOCAL lock_timeout = ${Math.trunc(ms)}`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The database's clock, now. */
|
|
56
|
+
export async function now(db: Queryable): Promise<number> {
|
|
57
|
+
const { auditChainHead } = tablesOf(db);
|
|
58
|
+
const [{ at }] = await db.select({ at: clockMillis(db) }).from(auditChainHead);
|
|
59
|
+
return at;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- members --------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function memberColumns(db: Queryable) {
|
|
65
|
+
const { vaultMembers } = tablesOf(db);
|
|
66
|
+
return {
|
|
67
|
+
principal: vaultMembers.principal,
|
|
68
|
+
status: vaultMembers.status,
|
|
69
|
+
owner: vaultMembers.owner,
|
|
70
|
+
generation: vaultMembers.generation,
|
|
71
|
+
createdAt: vaultMembers.createdAt,
|
|
72
|
+
createdBy: vaultMembers.createdBy,
|
|
73
|
+
statusChangedAt: vaultMembers.statusChangedAt,
|
|
74
|
+
statusChangedBy: vaultMembers.statusChangedBy,
|
|
75
|
+
accessSeq: vaultMembers.accessSeq,
|
|
76
|
+
mac: vaultMembers.mac,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function member(db: Queryable, principal: string): Promise<Member | undefined> {
|
|
81
|
+
const { vaultMembers } = tablesOf(db);
|
|
82
|
+
const [row] = await db.select(memberColumns(db)).from(vaultMembers).where(eq(vaultMembers.principal, principal));
|
|
83
|
+
return row as Member | undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function allMembers(db: Queryable): Promise<Member[]> {
|
|
87
|
+
const { vaultMembers } = tablesOf(db);
|
|
88
|
+
return (await db.select(memberColumns(db)).from(vaultMembers).orderBy(asc(vaultMembers.principal))) as Member[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Lock these members' rows for the rest of the transaction, in one order
|
|
93
|
+
* everywhere, and read them: those that have none are not in the map. Every
|
|
94
|
+
* decision about a member takes its row first, so two of them, from any
|
|
95
|
+
* process, never interleave.
|
|
96
|
+
*/
|
|
97
|
+
export async function lockMembers(tx: Transaction, principals: readonly string[]): Promise<Map<string, Member>> {
|
|
98
|
+
const { vaultMembers } = tablesOf(tx);
|
|
99
|
+
const rows = await forUpdate(
|
|
100
|
+
tx,
|
|
101
|
+
tx
|
|
102
|
+
.select(memberColumns(tx))
|
|
103
|
+
.from(vaultMembers)
|
|
104
|
+
.where(inArray(vaultMembers.principal, [...new Set(principals)]))
|
|
105
|
+
.orderBy(asc(vaultMembers.principal)),
|
|
106
|
+
'no key update',
|
|
107
|
+
);
|
|
108
|
+
return new Map((rows as Member[]).map((row) => [row.principal, row]));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Add `row`. A principal has one row: when two decisions admit the same
|
|
113
|
+
* one at once, the second fails here, and rolls back with its entries.
|
|
114
|
+
*/
|
|
115
|
+
export async function insertMember(tx: Transaction, row: Member): Promise<void> {
|
|
116
|
+
const { vaultMembers } = tablesOf(tx);
|
|
117
|
+
await tx.insert(vaultMembers).values(row);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function updateMember(
|
|
121
|
+
tx: Transaction,
|
|
122
|
+
principal: string,
|
|
123
|
+
change: Partial<Omit<Member, 'principal'>>,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const { vaultMembers } = tablesOf(tx);
|
|
126
|
+
await tx.update(vaultMembers).set(change).where(eq(vaultMembers.principal, principal));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- grants ---------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/** Grants, lapsed ones too, each with its project, which an environment's grant finds through `environments`. */
|
|
132
|
+
export async function grants(db: Queryable, principal?: string): Promise<GrantRow[]> {
|
|
133
|
+
const { vaultGrants, environments } = tablesOf(db);
|
|
134
|
+
return db
|
|
135
|
+
.select({
|
|
136
|
+
principal: vaultGrants.principal,
|
|
137
|
+
projectId: sql<string>`coalesce(${vaultGrants.projectId}, ${environments.projectId})`,
|
|
138
|
+
environmentId: vaultGrants.environmentId,
|
|
139
|
+
role: vaultGrants.role,
|
|
140
|
+
expiresAt: vaultGrants.expiresAt,
|
|
141
|
+
grantedAt: vaultGrants.grantedAt,
|
|
142
|
+
grantedBy: vaultGrants.grantedBy,
|
|
143
|
+
})
|
|
144
|
+
.from(vaultGrants)
|
|
145
|
+
.leftJoin(environments, eq(environments.id, vaultGrants.environmentId))
|
|
146
|
+
.where(principal === undefined ? undefined : eq(vaultGrants.principal, principal));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A grant names its environment, or its project when it has none: exactly one. */
|
|
150
|
+
function at(db: Queryable, place: Place) {
|
|
151
|
+
const { vaultGrants } = tablesOf(db);
|
|
152
|
+
return place.environmentId === null
|
|
153
|
+
? and(eq(vaultGrants.projectId, place.projectId), isNull(vaultGrants.environmentId))
|
|
154
|
+
: eq(vaultGrants.environmentId, place.environmentId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function insertGrant(tx: Transaction, grant: GrantRow): Promise<void> {
|
|
158
|
+
const { vaultGrants } = tablesOf(tx);
|
|
159
|
+
await tx.insert(vaultGrants).values({
|
|
160
|
+
...grant,
|
|
161
|
+
projectId: grant.environmentId === null ? grant.projectId : null,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function deleteGrant(tx: Transaction, principal: string, place: Place): Promise<void> {
|
|
166
|
+
const { vaultGrants } = tablesOf(tx);
|
|
167
|
+
await tx.delete(vaultGrants).where(and(eq(vaultGrants.principal, principal), at(tx, place)));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function deleteGrants(tx: Transaction, principal: string): Promise<void> {
|
|
171
|
+
const { vaultGrants } = tablesOf(tx);
|
|
172
|
+
await tx.delete(vaultGrants).where(eq(vaultGrants.principal, principal));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Up to `limit` of the newest stored data keys wrapped under one KEK, each
|
|
177
|
+
* with the secret it opens for: what proves a KEK is the one the data was
|
|
178
|
+
* wrapped with, before the vault records a check value for it.
|
|
179
|
+
*/
|
|
180
|
+
export async function wrappedUnder(db: Queryable, provider: string, keyId: string, limit: number) {
|
|
181
|
+
const { secretVersions, secrets } = tablesOf(db);
|
|
182
|
+
const rows = await db
|
|
183
|
+
.select({
|
|
184
|
+
projectId: secrets.projectId,
|
|
185
|
+
environmentId: secrets.environmentId,
|
|
186
|
+
secretId: secretVersions.secretId,
|
|
187
|
+
version: secretVersions.version,
|
|
188
|
+
kekProvider: secretVersions.kekProvider,
|
|
189
|
+
kekId: secretVersions.kekId,
|
|
190
|
+
kekVersion: secretVersions.kekVersion,
|
|
191
|
+
bytes: secretVersions.wrappedDek,
|
|
192
|
+
})
|
|
193
|
+
.from(secretVersions)
|
|
194
|
+
.innerJoin(secrets, eq(secrets.id, secretVersions.secretId))
|
|
195
|
+
.where(and(eq(secretVersions.kekProvider, provider), eq(secretVersions.kekId, keyId)))
|
|
196
|
+
.orderBy(desc(secretVersions.createdAt))
|
|
197
|
+
.limit(limit);
|
|
198
|
+
return rows.map((row) => ({ ...row, bytes: Buffer.from(row.bytes) }));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Of these projects and environments, the ones that exist, each environment with its project. */
|
|
202
|
+
export async function places(
|
|
203
|
+
db: Queryable,
|
|
204
|
+
projectIds: readonly string[],
|
|
205
|
+
environmentIds: readonly string[],
|
|
206
|
+
): Promise<{ projects: Set<string>; environments: Map<string, string> }> {
|
|
207
|
+
const { projects, environments } = tablesOf(db);
|
|
208
|
+
const [foundProjects, foundEnvironments] = await Promise.all([
|
|
209
|
+
projectIds.length === 0 ? [] : db.select({ id: projects.id }).from(projects).where(inArray(projects.id, [...projectIds])),
|
|
210
|
+
environmentIds.length === 0
|
|
211
|
+
? []
|
|
212
|
+
: db
|
|
213
|
+
.select({ id: environments.id, projectId: environments.projectId })
|
|
214
|
+
.from(environments)
|
|
215
|
+
.where(inArray(environments.id, [...environmentIds])),
|
|
216
|
+
]);
|
|
217
|
+
return {
|
|
218
|
+
projects: new Set(foundProjects.map((row) => row.id)),
|
|
219
|
+
environments: new Map(foundEnvironments.map((row) => [row.id, row.projectId])),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- the log --------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/** The action of an entry that releases a key, which the bulk limit counts. */
|
|
226
|
+
export const RELEASE = 'secret.read';
|
|
227
|
+
|
|
228
|
+
/** How many keys the vault has released to `principal` since `after`. */
|
|
229
|
+
export async function releasesSince(db: Queryable, principal: string, after: number): Promise<number> {
|
|
230
|
+
const { auditLog } = tablesOf(db);
|
|
231
|
+
const [{ n }] = await db
|
|
232
|
+
.select({ n: count() })
|
|
233
|
+
.from(auditLog)
|
|
234
|
+
.where(
|
|
235
|
+
and(
|
|
236
|
+
eq(auditLog.author, 'vault'),
|
|
237
|
+
eq(auditLog.actor, principal),
|
|
238
|
+
eq(auditLog.action, RELEASE),
|
|
239
|
+
eq(auditLog.decision, 'allow'),
|
|
240
|
+
gt(auditLog.occurredAt, after),
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
return Number(n);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function entryColumns(db: Queryable) {
|
|
247
|
+
const { auditLog } = tablesOf(db);
|
|
248
|
+
return {
|
|
249
|
+
seq: auditLog.seq,
|
|
250
|
+
author: auditLog.author,
|
|
251
|
+
keyId: auditLog.keyId,
|
|
252
|
+
occurredAt: auditLog.occurredAt,
|
|
253
|
+
actor: auditLog.actor,
|
|
254
|
+
action: auditLog.action,
|
|
255
|
+
decision: auditLog.decision,
|
|
256
|
+
code: auditLog.code,
|
|
257
|
+
subjectPrincipal: auditLog.subjectPrincipal,
|
|
258
|
+
projectId: auditLog.projectId,
|
|
259
|
+
environmentId: auditLog.environmentId,
|
|
260
|
+
secretId: auditLog.secretId,
|
|
261
|
+
secretVersionId: auditLog.secretVersionId,
|
|
262
|
+
operationId: auditLog.operationId,
|
|
263
|
+
requestId: auditLog.requestId,
|
|
264
|
+
sourceIp: auditLog.sourceIp,
|
|
265
|
+
relatedSeq: auditLog.relatedSeq,
|
|
266
|
+
metadata: auditLog.metadata,
|
|
267
|
+
prevHash: auditLog.prevHash,
|
|
268
|
+
mac: auditLog.mac,
|
|
269
|
+
hash: auditLog.hash,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Rows of the log as the chain's codec reads them: `author` and `decision` are checked by the table. */
|
|
274
|
+
function stored(rows: object[]): StoredEntry[] {
|
|
275
|
+
return rows as StoredEntry[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Up to `limit` entries of either author from `fromSeq`, oldest first: the chain as it runs. */
|
|
279
|
+
export async function entriesFrom(db: Queryable, fromSeq: bigint, limit: number): Promise<StoredEntry[]> {
|
|
280
|
+
const { auditLog } = tablesOf(db);
|
|
281
|
+
const rows = await db
|
|
282
|
+
.select(entryColumns(db))
|
|
283
|
+
.from(auditLog)
|
|
284
|
+
.where(gte(auditLog.seq, fromSeq))
|
|
285
|
+
.orderBy(asc(auditLog.seq))
|
|
286
|
+
.limit(limit);
|
|
287
|
+
return stored(rows);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The entry at `seq`'s hash, or undefined when there is none. */
|
|
291
|
+
export async function hashAt(db: Queryable, seq: bigint): Promise<Buffer | undefined> {
|
|
292
|
+
const { auditLog } = tablesOf(db);
|
|
293
|
+
const [row] = await db.select({ hash: auditLog.hash }).from(auditLog).where(eq(auditLog.seq, seq));
|
|
294
|
+
return row?.hash;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Up to `limit` of the vault's entries before `before` (the newest when undefined), newest first. */
|
|
298
|
+
export async function vaultPage(db: Queryable, before: bigint | undefined, limit: number): Promise<StoredEntry[]> {
|
|
299
|
+
const { auditLog } = tablesOf(db);
|
|
300
|
+
const rows = await db
|
|
301
|
+
.select(entryColumns(db))
|
|
302
|
+
.from(auditLog)
|
|
303
|
+
.where(and(eq(auditLog.author, 'vault' satisfies Author), before === undefined ? undefined : lt(auditLog.seq, before)))
|
|
304
|
+
.orderBy(desc(auditLog.seq))
|
|
305
|
+
.limit(limit);
|
|
306
|
+
return stored(rows);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The vault's newest entry of any of these actions, allowed, or undefined. */
|
|
310
|
+
export async function latestVaultEntry(db: Queryable, actions: readonly string[]): Promise<StoredEntry | undefined> {
|
|
311
|
+
const { auditLog } = tablesOf(db);
|
|
312
|
+
const [row] = await db
|
|
313
|
+
.select(entryColumns(db))
|
|
314
|
+
.from(auditLog)
|
|
315
|
+
.where(and(eq(auditLog.author, 'vault'), inArray(auditLog.action, [...actions]), eq(auditLog.decision, 'allow')))
|
|
316
|
+
.orderBy(desc(auditLog.seq))
|
|
317
|
+
.limit(1);
|
|
318
|
+
return row === undefined ? undefined : stored([row])[0];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Up to `limit` of the vault's allowed access entries about `principal`, newest first. */
|
|
322
|
+
export async function accessEntriesAbout(db: Queryable, principal: string, limit: number): Promise<StoredEntry[]> {
|
|
323
|
+
const { auditLog } = tablesOf(db);
|
|
324
|
+
const rows = await db
|
|
325
|
+
.select(entryColumns(db))
|
|
326
|
+
.from(auditLog)
|
|
327
|
+
.where(
|
|
328
|
+
and(
|
|
329
|
+
eq(auditLog.author, 'vault'),
|
|
330
|
+
eq(auditLog.subjectPrincipal, principal),
|
|
331
|
+
inArray(auditLog.action, [...ACCESS_ACTIONS]),
|
|
332
|
+
eq(auditLog.decision, 'allow'),
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
.orderBy(desc(auditLog.seq))
|
|
336
|
+
.limit(limit);
|
|
337
|
+
return stored(rows);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The vault's newest allowed access entry about each member who has one. */
|
|
341
|
+
export async function newestAccessEntries(db: Queryable): Promise<Map<string, StoredEntry>> {
|
|
342
|
+
const { auditLog } = tablesOf(db);
|
|
343
|
+
const newest = db
|
|
344
|
+
.select({ seq: sql`max(${auditLog.seq})` })
|
|
345
|
+
.from(auditLog)
|
|
346
|
+
.where(
|
|
347
|
+
and(eq(auditLog.author, 'vault'), inArray(auditLog.action, [...ACCESS_ACTIONS]), eq(auditLog.decision, 'allow')),
|
|
348
|
+
)
|
|
349
|
+
.groupBy(auditLog.subjectPrincipal);
|
|
350
|
+
const rows = stored(await db.select(entryColumns(db)).from(auditLog).where(inArray(auditLog.seq, newest)));
|
|
351
|
+
return new Map(rows.flatMap((row) => (row.subjectPrincipal === null ? [] : [[row.subjectPrincipal, row]])));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Up to `limit` of the vault's allowed entries of these actions after `afterSeq`, oldest first. */
|
|
355
|
+
export async function vaultEntriesOf(
|
|
356
|
+
db: Queryable,
|
|
357
|
+
actions: readonly string[],
|
|
358
|
+
afterSeq: bigint,
|
|
359
|
+
limit: number,
|
|
360
|
+
): Promise<StoredEntry[]> {
|
|
361
|
+
const { auditLog } = tablesOf(db);
|
|
362
|
+
const rows = await db
|
|
363
|
+
.select(entryColumns(db))
|
|
364
|
+
.from(auditLog)
|
|
365
|
+
.where(
|
|
366
|
+
and(
|
|
367
|
+
eq(auditLog.author, 'vault'),
|
|
368
|
+
inArray(auditLog.action, [...actions]),
|
|
369
|
+
eq(auditLog.decision, 'allow'),
|
|
370
|
+
gt(auditLog.seq, afterSeq),
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
.orderBy(asc(auditLog.seq))
|
|
374
|
+
.limit(limit);
|
|
375
|
+
return stored(rows);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** A stored version and the binding and key the vault reads for itself. */
|
|
379
|
+
export type SecretVersion = { id: string; secret: SecretRef; wrapped: WrappedKey };
|
|
380
|
+
|
|
381
|
+
export async function versions(db: Queryable, ids: readonly string[]): Promise<SecretVersion[]> {
|
|
382
|
+
if (ids.length === 0) return [];
|
|
383
|
+
const { secretVersions, secrets, environments, projects } = tablesOf(db);
|
|
384
|
+
const rows = await db
|
|
385
|
+
.select({
|
|
386
|
+
id: secretVersions.id,
|
|
387
|
+
version: secretVersions.version,
|
|
388
|
+
secretId: secrets.id,
|
|
389
|
+
projectId: secrets.projectId,
|
|
390
|
+
environmentId: secrets.environmentId,
|
|
391
|
+
project: projects.slug,
|
|
392
|
+
environment: environments.slug,
|
|
393
|
+
key: secrets.key,
|
|
394
|
+
kekProvider: secretVersions.kekProvider,
|
|
395
|
+
kekId: secretVersions.kekId,
|
|
396
|
+
kekVersion: secretVersions.kekVersion,
|
|
397
|
+
wrappedDek: secretVersions.wrappedDek,
|
|
398
|
+
})
|
|
399
|
+
.from(secretVersions)
|
|
400
|
+
.innerJoin(secrets, eq(secrets.id, secretVersions.secretId))
|
|
401
|
+
.innerJoin(environments, eq(environments.id, secrets.environmentId))
|
|
402
|
+
.innerJoin(projects, eq(projects.id, secrets.projectId))
|
|
403
|
+
.where(inArray(secretVersions.id, [...new Set(ids)]));
|
|
404
|
+
return rows.map((row) => ({
|
|
405
|
+
id: row.id,
|
|
406
|
+
secret: {
|
|
407
|
+
projectId: row.projectId,
|
|
408
|
+
environmentId: row.environmentId,
|
|
409
|
+
secretId: row.secretId,
|
|
410
|
+
version: row.version,
|
|
411
|
+
path: `${row.project}/${row.environment}/${row.key}`,
|
|
412
|
+
},
|
|
413
|
+
wrapped: {
|
|
414
|
+
kekProvider: row.kekProvider,
|
|
415
|
+
kekId: row.kekId,
|
|
416
|
+
kekVersion: row.kekVersion,
|
|
417
|
+
bytes: row.wrappedDek.toString('base64'),
|
|
418
|
+
},
|
|
419
|
+
}));
|
|
420
|
+
}
|