@coffre/vault 0.2.0 → 0.4.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/dist/cloudflare.d.ts +7 -1
- package/dist/cloudflare.js +7 -1
- package/dist/node.js +3 -1
- package/dist/{vault-BexBMpZL.js → vault-CeKwkzSV.js} +551 -104
- package/package.json +4 -4
- package/src/cloudflare.ts +4 -0
- package/src/local.ts +2 -0
- package/src/replay.ts +19 -5
- package/src/rows.ts +9 -3
- package/src/store.ts +136 -43
- package/src/vault.ts +435 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "coffre's vault: keys, grants and members, in the database the app uses, as a Cloudflare Worker or on Node.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"drizzle-orm": "0.45.2",
|
|
39
|
-
"@coffre/core": "0.
|
|
40
|
-
"@coffre/db": "0.
|
|
39
|
+
"@coffre/core": "0.4.0",
|
|
40
|
+
"@coffre/db": "0.4.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "26.1.1",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"pg": "8.16.3",
|
|
46
46
|
"tsdown": "0.23.0",
|
|
47
47
|
"typescript": "5.9.3",
|
|
48
|
-
"wrangler": "4.
|
|
48
|
+
"wrangler": "4.142.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "tsdown",
|
package/src/cloudflare.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import type {
|
|
19
19
|
AdmitInput,
|
|
20
20
|
RemoveInput,
|
|
21
|
+
ReferenceInput,
|
|
22
|
+
EndReferencesInput,
|
|
21
23
|
RewrapInput,
|
|
22
24
|
SetAccessInput,
|
|
23
25
|
UnwrapInput,
|
|
@@ -83,6 +85,8 @@ export class VaultEntrypoint extends WorkerEntrypoint implements Vault {
|
|
|
83
85
|
async unwrap(input: UnwrapInput) { return this.#call((vault) => vault.unwrap(input)); }
|
|
84
86
|
async wrap(input: WrapInput) { return this.#call((vault) => vault.wrap(input)); }
|
|
85
87
|
async rewrap(input: RewrapInput) { return this.#call((vault) => vault.rewrap(input)); }
|
|
88
|
+
async reference(input: ReferenceInput) { return this.#call((vault) => vault.reference(input)); }
|
|
89
|
+
async endReferences(input: EndReferencesInput) { return this.#call((vault) => vault.endReferences(input)); }
|
|
86
90
|
async access(principal: string) { return this.#call((vault) => vault.access(principal)); }
|
|
87
91
|
async setAccess(input: SetAccessInput) { return this.#call((vault) => vault.setAccess(input)); }
|
|
88
92
|
async admit(input: AdmitInput) { return this.#call((vault) => vault.admit(input)); }
|
package/src/local.ts
CHANGED
package/src/replay.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { everyProjectPath } from '@coffre/core/access';
|
|
1
2
|
import type { StoredEntry } from '@coffre/core/audit';
|
|
2
3
|
import type { AccessFault, FaultGrant } from '@coffre/core/vault';
|
|
3
4
|
|
|
@@ -51,7 +52,7 @@ export type Replayed = { members: Map<string, LoggedMember>; held: Map<string, M
|
|
|
51
52
|
export function apply(state: Replayed, row: StoredEntry): AccessFault | null {
|
|
52
53
|
const principal = row.subjectPrincipal!;
|
|
53
54
|
const detail = JSON.parse(row.metadata) as Record<string, unknown>;
|
|
54
|
-
const place
|
|
55
|
+
const place = placeOf(row, detail);
|
|
55
56
|
const grantsOf = state.held.get(principal) ?? new Map<string, GrantRow>();
|
|
56
57
|
state.held.set(principal, grantsOf);
|
|
57
58
|
const before = state.members.get(principal);
|
|
@@ -114,9 +115,21 @@ const MEMBER_FIELDS = [
|
|
|
114
115
|
['accessSeq', 'access_seq'],
|
|
115
116
|
] as const satisfies readonly (readonly [keyof LoggedMember, string])[];
|
|
116
117
|
|
|
117
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* The place an access entry is about: its project and environment, or, with
|
|
120
|
+
* neither, every project, which it names as a path (`*`, or `*` and a slug).
|
|
121
|
+
*/
|
|
122
|
+
function placeOf(row: StoredEntry, detail: Record<string, unknown>): Place {
|
|
123
|
+
if (row.projectId !== null || row.environmentId !== null) {
|
|
124
|
+
return { projectId: row.projectId, environmentId: row.environmentId, environmentSlug: null };
|
|
125
|
+
}
|
|
126
|
+
const slug = typeof detail.place === 'string' ? detail.place.split('/')[1] : undefined;
|
|
127
|
+
return { projectId: null, environmentId: null, environmentSlug: slug ?? null };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A grant's place: its environment, or its project when it has none, or every project's path. */
|
|
118
131
|
function placeKey(place: Place): string {
|
|
119
|
-
return place.environmentId ?? place.projectId;
|
|
132
|
+
return place.environmentId ?? place.projectId ?? everyProjectPath(place.environmentSlug);
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
function grantKey(grant: GrantRow): string {
|
|
@@ -124,6 +137,7 @@ function grantKey(grant: GrantRow): string {
|
|
|
124
137
|
grant.principal,
|
|
125
138
|
grant.projectId,
|
|
126
139
|
grant.environmentId,
|
|
140
|
+
grant.environmentSlug,
|
|
127
141
|
grant.role,
|
|
128
142
|
grant.expiresAt,
|
|
129
143
|
grant.grantedAt,
|
|
@@ -132,6 +146,6 @@ function grantKey(grant: GrantRow): string {
|
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
function faultGrant(key: string): FaultGrant {
|
|
135
|
-
const [principal, projectId, environmentId, role] = JSON.parse(key) as [string, string, string | null, string];
|
|
136
|
-
return { principal, projectId, environmentId, role };
|
|
149
|
+
const [principal, projectId, environmentId, environmentSlug, role] = JSON.parse(key) as [string, string | null, string | null, string | null, string];
|
|
150
|
+
return { principal, projectId, environmentId, environmentSlug, role };
|
|
137
151
|
}
|
package/src/rows.ts
CHANGED
|
@@ -21,12 +21,18 @@ export function rowKey(signingKey: Uint8Array): Buffer {
|
|
|
21
21
|
return Buffer.from(hkdfSync('sha256', signingKey, new Uint8Array(0), 'coffre.vault.rows.v1', 32));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* A member's grants as the MAC covers them: each one's place, role, end and
|
|
26
|
+
* grant, as a sorted set. A grant is kept by what its row names first: its
|
|
27
|
+
* environment, its project, or neither, which is a grant on every project,
|
|
28
|
+
* with the slug it covers, or null. A member who holds none of those has the
|
|
29
|
+
* set, and so the MAC, they had before there were any.
|
|
30
|
+
*/
|
|
25
31
|
export function grantSet(grants: readonly GrantRow[]): string[] {
|
|
26
32
|
return grants
|
|
27
33
|
.map((grant) => [
|
|
28
|
-
grant.environmentId
|
|
29
|
-
grant.environmentId ?? grant.projectId,
|
|
34
|
+
grant.environmentId !== null ? 'environment' : grant.projectId !== null ? 'project' : 'every-project',
|
|
35
|
+
grant.environmentId ?? grant.projectId ?? grant.environmentSlug,
|
|
30
36
|
grant.role,
|
|
31
37
|
grant.expiresAt,
|
|
32
38
|
grant.grantedAt,
|
package/src/store.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { GrantPlace } from '@coffre/core/access';
|
|
1
2
|
import type { Author, StoredEntry } from '@coffre/core/audit';
|
|
2
3
|
import { ACCESS_ACTIONS, type SecretRef, type WrappedKey } from '@coffre/core/vault';
|
|
3
4
|
import { tablesOf, type Queryable, type Transaction } from '@coffre/db';
|
|
4
|
-
import { clockMillis, engineOf, forUpdate } from '@coffre/db/dialect';
|
|
5
|
-
import {
|
|
5
|
+
import { clockMillis, engineOf, forUpdate, tombstone, truth } from '@coffre/db/dialect';
|
|
6
|
+
import { readGrants, type GrantRow } from '@coffre/db/grants';
|
|
7
|
+
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, lte, or, sql, type SQL, type SQLWrapper } from 'drizzle-orm';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Every query the vault makes, and the only code in it that holds them.
|
|
@@ -33,16 +35,16 @@ export type Member = {
|
|
|
33
35
|
/** Every entry that changes who is a member or what they hold. */
|
|
34
36
|
export { ACCESS_ACTIONS };
|
|
35
37
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Where a grant applies: a project, one of its environments (with its
|
|
40
|
+
* project, which its row finds through `environments`), every project, or
|
|
41
|
+
* one environment slug in every project.
|
|
42
|
+
*/
|
|
43
|
+
export type Place = GrantPlace;
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
expiresAt: number | null;
|
|
43
|
-
grantedAt: number;
|
|
44
|
-
grantedBy: string;
|
|
45
|
-
};
|
|
45
|
+
/** A grant as stored, which `@coffre/db/grants` reads and writes. */
|
|
46
|
+
export type { GrantRow };
|
|
47
|
+
export { insertGrant } from '@coffre/db/grants';
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
50
|
* On Postgres, fail a lock wait in this transaction after `ms`, rather than
|
|
@@ -129,37 +131,48 @@ export async function updateMember(
|
|
|
129
131
|
// --- grants ---------------------------------------------------------------------
|
|
130
132
|
|
|
131
133
|
/** Grants, lapsed ones too, each with its project, which an environment's grant finds through `environments`. */
|
|
132
|
-
export
|
|
133
|
-
|
|
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));
|
|
134
|
+
export function grants(db: Queryable, principal?: string): Promise<GrantRow[]> {
|
|
135
|
+
return readGrants(db, principal === undefined ? {} : { principal });
|
|
147
136
|
}
|
|
148
137
|
|
|
149
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* A grant's row names its environment, or its project when it has none, or
|
|
140
|
+
* neither on every project, with the slug it covers there, if any.
|
|
141
|
+
*/
|
|
150
142
|
function at(db: Queryable, place: Place) {
|
|
151
143
|
const { vaultGrants } = tablesOf(db);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
if (place.environmentId !== null) return eq(vaultGrants.environmentId, place.environmentId);
|
|
145
|
+
if (place.projectId !== null) return and(eq(vaultGrants.projectId, place.projectId), isNull(vaultGrants.environmentId));
|
|
146
|
+
return and(
|
|
147
|
+
isNull(vaultGrants.projectId),
|
|
148
|
+
isNull(vaultGrants.environmentId),
|
|
149
|
+
place.environmentSlug === null ? isNull(vaultGrants.environmentSlug) : eq(vaultGrants.environmentSlug, place.environmentSlug),
|
|
150
|
+
);
|
|
155
151
|
}
|
|
156
152
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Each of these environments that exists, by id, with its project, its
|
|
155
|
+
* slug, what grants on one slug in every project match, and whether it was
|
|
156
|
+
* deleted, alone or with its project: then no key under it opens, or is
|
|
157
|
+
* wrapped, whatever grant would cover it.
|
|
158
|
+
*/
|
|
159
|
+
export async function environmentsById(
|
|
160
|
+
db: Queryable,
|
|
161
|
+
ids: readonly string[],
|
|
162
|
+
): Promise<Map<string, { projectId: string; slug: string; deleted: boolean }>> {
|
|
163
|
+
if (ids.length === 0) return new Map();
|
|
164
|
+
const { environments, projects } = tablesOf(db);
|
|
165
|
+
const rows = await db
|
|
166
|
+
.select({
|
|
167
|
+
id: environments.id,
|
|
168
|
+
projectId: environments.projectId,
|
|
169
|
+
slug: environments.slug,
|
|
170
|
+
deleted: truth(or(tombstone(environments.slug), tombstone(projects.slug))!),
|
|
171
|
+
})
|
|
172
|
+
.from(environments)
|
|
173
|
+
.innerJoin(projects, eq(projects.id, environments.projectId))
|
|
174
|
+
.where(inArray(environments.id, [...new Set(ids)]));
|
|
175
|
+
return new Map(rows.map(({ id, ...environment }) => [id, environment]));
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
export async function deleteGrant(tx: Transaction, principal: string, place: Place): Promise<void> {
|
|
@@ -192,7 +205,7 @@ export async function wrappedUnder(db: Queryable, provider: string, keyId: strin
|
|
|
192
205
|
})
|
|
193
206
|
.from(secretVersions)
|
|
194
207
|
.innerJoin(secrets, eq(secrets.id, secretVersions.secretId))
|
|
195
|
-
.where(and(eq(secretVersions.kekProvider, provider), eq(secretVersions.kekId, keyId)))
|
|
208
|
+
.where(and(eq(secretVersions.kekProvider, provider), eq(secretVersions.kekId, keyId), sealed(secretVersions.wrappedDek)))
|
|
196
209
|
.orderBy(desc(secretVersions.createdAt))
|
|
197
210
|
.limit(limit);
|
|
198
211
|
return rows.map((row) => ({ ...row, bytes: Buffer.from(row.bytes) }));
|
|
@@ -203,20 +216,25 @@ export async function places(
|
|
|
203
216
|
db: Queryable,
|
|
204
217
|
projectIds: readonly string[],
|
|
205
218
|
environmentIds: readonly string[],
|
|
206
|
-
): Promise<{ projects: Set<string>; environments: Map<string, string> }> {
|
|
219
|
+
): Promise<{ projects: Set<string>; environments: Map<string, string>; deleted: Set<string> }> {
|
|
207
220
|
const { projects, environments } = tablesOf(db);
|
|
208
221
|
const [foundProjects, foundEnvironments] = await Promise.all([
|
|
209
|
-
projectIds.length === 0
|
|
222
|
+
projectIds.length === 0
|
|
223
|
+
? []
|
|
224
|
+
: db.select({ id: projects.id, deleted: truth(tombstone(projects.slug)) }).from(projects).where(inArray(projects.id, [...projectIds])),
|
|
210
225
|
environmentIds.length === 0
|
|
211
226
|
? []
|
|
212
227
|
: db
|
|
213
|
-
.select({ id: environments.id, projectId: environments.projectId })
|
|
228
|
+
.select({ id: environments.id, projectId: environments.projectId, deleted: truth(or(tombstone(environments.slug), tombstone(projects.slug))!) })
|
|
214
229
|
.from(environments)
|
|
230
|
+
.innerJoin(projects, eq(projects.id, environments.projectId))
|
|
215
231
|
.where(inArray(environments.id, [...environmentIds])),
|
|
216
232
|
]);
|
|
217
233
|
return {
|
|
218
234
|
projects: new Set(foundProjects.map((row) => row.id)),
|
|
219
235
|
environments: new Map(foundEnvironments.map((row) => [row.id, row.projectId])),
|
|
236
|
+
// The projects and environments among them that were deleted, alone or with their project.
|
|
237
|
+
deleted: new Set([...foundProjects, ...foundEnvironments].filter((row) => row.deleted).map((row) => row.id)),
|
|
220
238
|
};
|
|
221
239
|
}
|
|
222
240
|
|
|
@@ -362,6 +380,61 @@ export async function firstVaultEntryUnder(db: Queryable, keyId: string): Promis
|
|
|
362
380
|
return row?.seq;
|
|
363
381
|
}
|
|
364
382
|
|
|
383
|
+
/** The entries at these seqs, any author, by seq: what a reference names as its seal. */
|
|
384
|
+
export async function entriesAt(db: Queryable, seqs: readonly bigint[]): Promise<Map<bigint, StoredEntry>> {
|
|
385
|
+
if (seqs.length === 0) return new Map();
|
|
386
|
+
const { auditLog } = tablesOf(db);
|
|
387
|
+
const rows = stored(await db.select(entryColumns(db)).from(auditLog).where(inArray(auditLog.seq, [...new Set(seqs)])));
|
|
388
|
+
return new Map(rows.map((row) => [row.seq, row]));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Which of these `reference.create` entries a `reference.end` of the
|
|
393
|
+
* vault's names, allowed: one read through `audit_log_reference_end_idx`.
|
|
394
|
+
* An end that fails its MAC still ends: putting a reference back takes
|
|
395
|
+
* more than a forged end can undo, and refusing is the safe side.
|
|
396
|
+
*/
|
|
397
|
+
export async function endedReferences(db: Queryable, seqs: readonly bigint[]): Promise<Set<bigint>> {
|
|
398
|
+
if (seqs.length === 0) return new Set();
|
|
399
|
+
const { auditLog } = tablesOf(db);
|
|
400
|
+
const rows = await db
|
|
401
|
+
.select({ relatedSeq: auditLog.relatedSeq })
|
|
402
|
+
.from(auditLog)
|
|
403
|
+
.where(and(eq(auditLog.author, 'vault'), eq(auditLog.action, 'reference.end'), eq(auditLog.decision, 'allow'), inArray(auditLog.relatedSeq, [...new Set(seqs)])));
|
|
404
|
+
return new Set(rows.map((row) => row.relatedSeq!));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The project each environment is in: a holder's ids, checked against each other. */
|
|
408
|
+
export async function projectsOfEnvironments(db: Queryable, ids: readonly string[]): Promise<Map<string, string>> {
|
|
409
|
+
if (ids.length === 0) return new Map();
|
|
410
|
+
const { environments } = tablesOf(db);
|
|
411
|
+
const rows = await db.select({ id: environments.id, projectId: environments.projectId }).from(environments).where(inArray(environments.id, [...new Set(ids)]));
|
|
412
|
+
return new Map(rows.map((row) => [row.id, row.projectId]));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** A secret as the vault reads it to make a reference to it: where it is, its path, and its current version. */
|
|
416
|
+
export type SecretPlace = { secretId: string; projectId: string; environmentId: string; path: string; currentVersionId: string | null };
|
|
417
|
+
|
|
418
|
+
export async function secretsByIds(db: Queryable, ids: readonly string[]): Promise<Map<string, SecretPlace>> {
|
|
419
|
+
if (ids.length === 0) return new Map();
|
|
420
|
+
const { secrets, environments, projects } = tablesOf(db);
|
|
421
|
+
const rows = await db
|
|
422
|
+
.select({
|
|
423
|
+
secretId: secrets.id,
|
|
424
|
+
projectId: secrets.projectId,
|
|
425
|
+
environmentId: secrets.environmentId,
|
|
426
|
+
project: projects.slug,
|
|
427
|
+
environment: environments.slug,
|
|
428
|
+
key: secrets.key,
|
|
429
|
+
currentVersionId: secrets.currentVersionId,
|
|
430
|
+
})
|
|
431
|
+
.from(secrets)
|
|
432
|
+
.innerJoin(environments, eq(environments.id, secrets.environmentId))
|
|
433
|
+
.innerJoin(projects, eq(projects.id, secrets.projectId))
|
|
434
|
+
.where(inArray(secrets.id, [...new Set(ids)]));
|
|
435
|
+
return new Map(rows.map(({ project, environment, key, ...row }) => [row.secretId, { ...row, path: `${project}/${environment}/${key}` }]));
|
|
436
|
+
}
|
|
437
|
+
|
|
365
438
|
/** The vault's newest entry of any of these actions, allowed, or undefined. */
|
|
366
439
|
export async function latestVaultEntry(db: Queryable, actions: readonly string[]): Promise<StoredEntry | undefined> {
|
|
367
440
|
const { auditLog } = tablesOf(db);
|
|
@@ -431,8 +504,22 @@ export async function vaultEntriesOf(
|
|
|
431
504
|
return stored(rows);
|
|
432
505
|
}
|
|
433
506
|
|
|
434
|
-
/**
|
|
435
|
-
|
|
507
|
+
/**
|
|
508
|
+
* A version that still holds its wrapped key. A deleted place's versions are
|
|
509
|
+
* erased, their keys emptied: there is nothing left to open, or to try a
|
|
510
|
+
* vault key on.
|
|
511
|
+
*/
|
|
512
|
+
function sealed(wrappedDek: SQLWrapper): SQL {
|
|
513
|
+
return sql`length(${wrappedDek}) > 0`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* A stored version and the binding and key the vault reads for itself;
|
|
518
|
+
* whether its place was deleted, and whether it is its secret's current
|
|
519
|
+
* one, as a read through a reference needs. An erased version outside a
|
|
520
|
+
* deleted place is no version.
|
|
521
|
+
*/
|
|
522
|
+
export type SecretVersion = { id: string; secret: SecretRef; wrapped: WrappedKey; deleted: boolean; current: boolean };
|
|
436
523
|
|
|
437
524
|
export async function versions(db: Queryable, ids: readonly string[]): Promise<SecretVersion[]> {
|
|
438
525
|
if (ids.length === 0) return [];
|
|
@@ -451,14 +538,19 @@ export async function versions(db: Queryable, ids: readonly string[]): Promise<S
|
|
|
451
538
|
kekId: secretVersions.kekId,
|
|
452
539
|
kekVersion: secretVersions.kekVersion,
|
|
453
540
|
wrappedDek: secretVersions.wrappedDek,
|
|
541
|
+
deleted: truth(or(tombstone(environments.slug), tombstone(projects.slug))!),
|
|
542
|
+
currentVersionId: secrets.currentVersionId,
|
|
543
|
+
// The newest by the versions' own order: the app may write `current_version_id`, never a version.
|
|
544
|
+
newest: sql<number | string>`(SELECT max(newest.version) FROM secret_versions newest WHERE newest.secret_id = ${secretVersions.secretId})`,
|
|
454
545
|
})
|
|
455
546
|
.from(secretVersions)
|
|
456
547
|
.innerJoin(secrets, eq(secrets.id, secretVersions.secretId))
|
|
457
548
|
.innerJoin(environments, eq(environments.id, secrets.environmentId))
|
|
458
549
|
.innerJoin(projects, eq(projects.id, secrets.projectId))
|
|
459
|
-
.where(inArray(secretVersions.id, [...new Set(ids)]));
|
|
550
|
+
.where(and(inArray(secretVersions.id, [...new Set(ids)]), or(sealed(secretVersions.wrappedDek), tombstone(environments.slug), tombstone(projects.slug))));
|
|
460
551
|
return rows.map((row) => ({
|
|
461
552
|
id: row.id,
|
|
553
|
+
deleted: row.deleted,
|
|
462
554
|
secret: {
|
|
463
555
|
projectId: row.projectId,
|
|
464
556
|
environmentId: row.environmentId,
|
|
@@ -472,5 +564,6 @@ export async function versions(db: Queryable, ids: readonly string[]): Promise<S
|
|
|
472
564
|
kekVersion: row.kekVersion,
|
|
473
565
|
bytes: row.wrappedDek.toString('base64'),
|
|
474
566
|
},
|
|
567
|
+
current: row.currentVersionId === row.id && Number(row.newest) === row.version,
|
|
475
568
|
}));
|
|
476
569
|
}
|