@coffre/vault 0.4.6 → 0.5.1
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 -2
- package/dist/cloudflare.js +7 -1
- package/dist/node.js +3 -1
- package/dist/{vault-CeKwkzSV.js → vault-CP8yU8xq.js} +486 -129
- package/package.json +3 -3
- package/src/cloudflare.ts +3 -0
- package/src/local.ts +2 -0
- package/src/replay.ts +20 -7
- package/src/rows.ts +17 -3
- package/src/store.ts +65 -48
- package/src/vault.ts +373 -94
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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/
|
|
40
|
-
"@coffre/
|
|
39
|
+
"@coffre/db": "0.5.1",
|
|
40
|
+
"@coffre/core": "0.5.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "26.1.1",
|
package/src/cloudflare.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
EndReferencesInput,
|
|
23
23
|
RewrapInput,
|
|
24
24
|
SetAccessInput,
|
|
25
|
+
SetSettingsInput,
|
|
25
26
|
UnwrapInput,
|
|
26
27
|
Vault,
|
|
27
28
|
VerifyLogInput,
|
|
@@ -91,6 +92,8 @@ export class VaultEntrypoint extends WorkerEntrypoint implements Vault {
|
|
|
91
92
|
async setAccess(input: SetAccessInput) { return this.#call((vault) => vault.setAccess(input)); }
|
|
92
93
|
async admit(input: AdmitInput) { return this.#call((vault) => vault.admit(input)); }
|
|
93
94
|
async remove(input: RemoveInput) { return this.#call((vault) => vault.remove(input)); }
|
|
95
|
+
async settings() { return this.#call((vault) => vault.settings()); }
|
|
96
|
+
async setSettings(input: SetSettingsInput) { return this.#call((vault) => vault.setSettings(input)); }
|
|
94
97
|
async checkpoint() { return this.#call((vault) => vault.checkpoint()); }
|
|
95
98
|
async about() { return this.#call((vault) => vault.about()); }
|
|
96
99
|
async keyChecks() { return this.#call((vault) => vault.keyChecks()); }
|
package/src/local.ts
CHANGED
package/src/replay.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EVERYWHERE, isInstanceRole, isScope, normalScope, type InstanceRole, type Scope } from '@coffre/core/access';
|
|
2
2
|
import type { StoredEntry } from '@coffre/core/audit';
|
|
3
3
|
import type { AccessFault, FaultGrant } from '@coffre/core/vault';
|
|
4
4
|
|
|
@@ -28,7 +28,8 @@ export function replayFault(state: Replayed, rows: readonly Member[], storedGran
|
|
|
28
28
|
const [inStore, inLog] = [stored.get(principal), members.get(principal)];
|
|
29
29
|
if (inLog === undefined) return { kind: 'unlogged-member', principal };
|
|
30
30
|
if (inStore === undefined) return { kind: 'missing-member', principal };
|
|
31
|
-
const
|
|
31
|
+
const same = (field: keyof LoggedMember) => (field === 'scope' ? JSON.stringify(inStore.scope) === JSON.stringify(inLog.scope) : inStore[field] === inLog[field]);
|
|
32
|
+
const fields = MEMBER_FIELDS.filter(([field]) => !same(field)).map(([, column]) => column);
|
|
32
33
|
if (fields.length > 0) return { kind: 'member-differs', principal, fields };
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -63,7 +64,7 @@ export function apply(state: Replayed, row: StoredEntry): AccessFault | null {
|
|
|
63
64
|
state.members.set(principal, {
|
|
64
65
|
principal,
|
|
65
66
|
status: 'active',
|
|
66
|
-
|
|
67
|
+
...standingIn(detail),
|
|
67
68
|
generation: before?.generation ?? 0,
|
|
68
69
|
createdAt: before?.createdAt ?? row.occurredAt,
|
|
69
70
|
createdBy: before?.createdBy ?? row.actor,
|
|
@@ -72,15 +73,16 @@ export function apply(state: Replayed, row: StoredEntry): AccessFault | null {
|
|
|
72
73
|
});
|
|
73
74
|
return null;
|
|
74
75
|
case 'member.owner':
|
|
76
|
+
case 'member.role':
|
|
75
77
|
if (before === undefined) return { kind: 'unadmitted-change', seq: Number(row.seq), principal };
|
|
76
|
-
state.members.set(principal, { ...before,
|
|
78
|
+
state.members.set(principal, { ...before, ...standingIn(detail), accessSeq: row.seq });
|
|
77
79
|
return null;
|
|
78
80
|
case 'member.remove': {
|
|
79
81
|
if (before === undefined) return { kind: 'unadmitted-change', seq: Number(row.seq), principal };
|
|
80
82
|
// A removal names the generation it moved to; one before this format
|
|
81
83
|
// was always the next.
|
|
82
84
|
const generation = typeof detail.generation === 'number' ? detail.generation : before.generation + 1;
|
|
83
|
-
state.members.set(principal, { ...before, status: 'removed',
|
|
85
|
+
state.members.set(principal, { ...before, status: 'removed', role: 'member', scope: EVERYWHERE, generation, accessSeq: row.seq, ...changed });
|
|
84
86
|
grantsOf.clear();
|
|
85
87
|
return null;
|
|
86
88
|
}
|
|
@@ -103,10 +105,21 @@ export function apply(state: Replayed, row: StoredEntry): AccessFault | null {
|
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
/**
|
|
109
|
+
* The instance role an entry gives: its `role` and `scope`, or, in one
|
|
110
|
+
* 0.4 wrote, whether it made them an owner, an Admin everywhere.
|
|
111
|
+
*/
|
|
112
|
+
function standingIn(detail: Record<string, unknown>): { role: InstanceRole; scope: Scope } {
|
|
113
|
+
if (typeof detail.role !== 'string') return { role: detail.owner === true ? 'admin' : 'member', scope: EVERYWHERE };
|
|
114
|
+
const role = isInstanceRole(detail.role) ? detail.role : 'member';
|
|
115
|
+
return { role, scope: role !== 'member' && isScope(detail.scope) ? normalScope(detail.scope) : EVERYWHERE };
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
/** A member's fields, and the columns a fault names them by. */
|
|
107
119
|
const MEMBER_FIELDS = [
|
|
108
120
|
['status', 'status'],
|
|
109
|
-
['
|
|
121
|
+
['role', 'role'],
|
|
122
|
+
['scope', 'scope'],
|
|
110
123
|
['generation', 'generation'],
|
|
111
124
|
['createdAt', 'created_at'],
|
|
112
125
|
['createdBy', 'created_by'],
|
|
@@ -129,7 +142,7 @@ function placeOf(row: StoredEntry, detail: Record<string, unknown>): Place {
|
|
|
129
142
|
|
|
130
143
|
/** A grant's place: its environment, or its project when it has none, or every project's path. */
|
|
131
144
|
function placeKey(place: Place): string {
|
|
132
|
-
return place.environmentId ?? place.projectId ??
|
|
145
|
+
return place.environmentId ?? place.projectId ?? (place.environmentSlug === null ? '*' : `*/${place.environmentSlug}`);
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
function grantKey(grant: GrantRow): string {
|
package/src/rows.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHmac, hkdfSync, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import { normalScope, unscoped } from '@coffre/core/access';
|
|
4
|
+
|
|
3
5
|
import type { GrantRow, Member } from './store.ts';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -24,8 +26,8 @@ export function rowKey(signingKey: Uint8Array): Buffer {
|
|
|
24
26
|
/**
|
|
25
27
|
* A member's grants as the MAC covers them: each one's place, role, end and
|
|
26
28
|
* 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
|
+
* environment, its project, or neither, which is a grant on every project
|
|
30
|
+
* of 0.4, with the slug it covers, or null, until the vault replaces it. A member who holds none of those has the
|
|
29
31
|
* set, and so the MAC, they had before there were any.
|
|
30
32
|
*/
|
|
31
33
|
export function grantSet(grants: readonly GrantRow[]): string[] {
|
|
@@ -48,13 +50,25 @@ export function sameGrants(a: readonly GrantRow[], b: readonly GrantRow[]): bool
|
|
|
48
50
|
return x.length === y.length && x.every((tuple, i) => tuple === y[i]);
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A member's instance role and its scope, as the MAC covers them: as 0.4
|
|
55
|
+
* did, whether they are an owner, for a Member or an Admin everywhere, so
|
|
56
|
+
* that every row 0.4 sealed holds as it is; otherwise the role and the
|
|
57
|
+
* scope, which no row of 0.4 can match.
|
|
58
|
+
*/
|
|
59
|
+
export function standing(member: Pick<Member, 'role' | 'scope'>): boolean | [string, unknown] {
|
|
60
|
+
const scope = normalScope(member.scope);
|
|
61
|
+
if (unscoped(scope) && (member.role === 'member' || member.role === 'admin')) return member.role === 'admin';
|
|
62
|
+
return [member.role, scope];
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
export function memberMac(key: Buffer, member: Omit<Member, 'mac'>, grants: readonly GrantRow[]): Buffer {
|
|
52
66
|
const held = grantSet(grants);
|
|
53
67
|
const tuple = [
|
|
54
68
|
'coffre.vault.member.v1',
|
|
55
69
|
member.principal,
|
|
56
70
|
member.status,
|
|
57
|
-
member
|
|
71
|
+
standing(member),
|
|
58
72
|
member.generation,
|
|
59
73
|
member.accessSeq.toString(),
|
|
60
74
|
member.createdAt,
|
package/src/store.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { EVERYWHERE, type InstanceRole, type Scope } from '@coffre/core/access';
|
|
2
2
|
import type { Author, StoredEntry } from '@coffre/core/audit';
|
|
3
3
|
import { ACCESS_ACTIONS, type SecretRef, type WrappedKey } from '@coffre/core/vault';
|
|
4
4
|
import { tablesOf, type Queryable, type Transaction } from '@coffre/db';
|
|
5
5
|
import { clockMillis, engineOf, forUpdate, tombstone, truth } from '@coffre/db/dialect';
|
|
6
|
-
import { readGrants, type GrantRow } from '@coffre/db/grants';
|
|
6
|
+
import { readGrants, roleColumns, storedRole, type GrantRow } from '@coffre/db/grants';
|
|
7
|
+
import { entryColumns } from '@coffre/db/log';
|
|
7
8
|
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, lte, or, sql, type SQL, type SQLWrapper } from 'drizzle-orm';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -20,7 +21,9 @@ import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, lte, or, sql,
|
|
|
20
21
|
export type Member = {
|
|
21
22
|
principal: string;
|
|
22
23
|
status: 'active' | 'removed';
|
|
23
|
-
|
|
24
|
+
/** A person's instance role and its scope; `member` everywhere for anyone else (`memberOf`). */
|
|
25
|
+
role: InstanceRole;
|
|
26
|
+
scope: Scope;
|
|
24
27
|
generation: number;
|
|
25
28
|
createdAt: number;
|
|
26
29
|
createdBy: string;
|
|
@@ -36,15 +39,16 @@ export type Member = {
|
|
|
36
39
|
export { ACCESS_ACTIONS };
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
|
-
* Where a grant applies: a project, one of its environments (with its
|
|
40
|
-
* project, which its row finds through `environments`),
|
|
41
|
-
*
|
|
42
|
+
* Where a grant applies: a project, or one of its environments (with its
|
|
43
|
+
* project, which its row finds through `environments`); or, for a grant on
|
|
44
|
+
* every project of 0.4, until the vault replaces it, neither, with the one
|
|
45
|
+
* environment slug it covers in each, if any.
|
|
42
46
|
*/
|
|
43
|
-
export type Place =
|
|
47
|
+
export type Place = Pick<GrantRow, 'projectId' | 'environmentId' | 'environmentSlug'>;
|
|
44
48
|
|
|
45
49
|
/** A grant as stored, which `@coffre/db/grants` reads and writes. */
|
|
46
50
|
export type { GrantRow };
|
|
47
|
-
export { insertGrant } from '@coffre/db/grants';
|
|
51
|
+
export { accessEntriesAbout, insertGrant } from '@coffre/db/grants';
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
54
|
* On Postgres, fail a lock wait in this transaction after `ms`, rather than
|
|
@@ -69,6 +73,8 @@ function memberColumns(db: Queryable) {
|
|
|
69
73
|
principal: vaultMembers.principal,
|
|
70
74
|
status: vaultMembers.status,
|
|
71
75
|
owner: vaultMembers.owner,
|
|
76
|
+
role: vaultMembers.role,
|
|
77
|
+
scope: vaultMembers.scope,
|
|
72
78
|
generation: vaultMembers.generation,
|
|
73
79
|
createdAt: vaultMembers.createdAt,
|
|
74
80
|
createdBy: vaultMembers.createdBy,
|
|
@@ -79,15 +85,29 @@ function memberColumns(db: Queryable) {
|
|
|
79
85
|
};
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
/** A row as stored: `role` and `scope` as their columns hold them. */
|
|
89
|
+
type StoredMember = Omit<Member, 'role' | 'scope'> & { owner: boolean; role: string | null; scope: string | null };
|
|
90
|
+
|
|
91
|
+
/** A member as their row says (`storedRole`). */
|
|
92
|
+
function memberOf({ owner, role, scope, ...row }: StoredMember): Member {
|
|
93
|
+
return { ...row, ...storedRole({ owner, role, scope }) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function storedChange(change: Partial<Omit<Member, 'principal'>>): Record<string, unknown> {
|
|
97
|
+
const { role, scope, ...rest } = change;
|
|
98
|
+
if (role === undefined) return rest;
|
|
99
|
+
return { ...rest, ...roleColumns(role, scope ?? EVERYWHERE) };
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
export async function member(db: Queryable, principal: string): Promise<Member | undefined> {
|
|
83
103
|
const { vaultMembers } = tablesOf(db);
|
|
84
104
|
const [row] = await db.select(memberColumns(db)).from(vaultMembers).where(eq(vaultMembers.principal, principal));
|
|
85
|
-
return row
|
|
105
|
+
return row === undefined ? undefined : memberOf(row as StoredMember);
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
export async function allMembers(db: Queryable): Promise<Member[]> {
|
|
89
109
|
const { vaultMembers } = tablesOf(db);
|
|
90
|
-
return (await db.select(memberColumns(db)).from(vaultMembers).orderBy(asc(vaultMembers.principal))) as
|
|
110
|
+
return ((await db.select(memberColumns(db)).from(vaultMembers).orderBy(asc(vaultMembers.principal))) as StoredMember[]).map(memberOf);
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
/**
|
|
@@ -107,7 +127,7 @@ export async function lockMembers(tx: Transaction, principals: readonly string[]
|
|
|
107
127
|
.orderBy(asc(vaultMembers.principal)),
|
|
108
128
|
'no key update',
|
|
109
129
|
);
|
|
110
|
-
return new Map((rows as
|
|
130
|
+
return new Map((rows as StoredMember[]).map((row) => [row.principal, memberOf(row)]));
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
/**
|
|
@@ -116,7 +136,7 @@ export async function lockMembers(tx: Transaction, principals: readonly string[]
|
|
|
116
136
|
*/
|
|
117
137
|
export async function insertMember(tx: Transaction, row: Member): Promise<void> {
|
|
118
138
|
const { vaultMembers } = tablesOf(tx);
|
|
119
|
-
await tx.insert(vaultMembers).values(row);
|
|
139
|
+
await tx.insert(vaultMembers).values(storedChange(row) as typeof vaultMembers.$inferInsert);
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
export async function updateMember(
|
|
@@ -125,7 +145,7 @@ export async function updateMember(
|
|
|
125
145
|
change: Partial<Omit<Member, 'principal'>>,
|
|
126
146
|
): Promise<void> {
|
|
127
147
|
const { vaultMembers } = tablesOf(tx);
|
|
128
|
-
await tx.update(vaultMembers).set(change).where(eq(vaultMembers.principal, principal));
|
|
148
|
+
await tx.update(vaultMembers).set(storedChange(change)).where(eq(vaultMembers.principal, principal));
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
// --- grants ---------------------------------------------------------------------
|
|
@@ -211,12 +231,12 @@ export async function wrappedUnder(db: Queryable, provider: string, keyId: strin
|
|
|
211
231
|
return rows.map((row) => ({ ...row, bytes: Buffer.from(row.bytes) }));
|
|
212
232
|
}
|
|
213
233
|
|
|
214
|
-
/** Of these projects and environments, the ones that exist, each environment with its project. */
|
|
234
|
+
/** Of these projects and environments, the ones that exist, each environment with its project and slug. */
|
|
215
235
|
export async function places(
|
|
216
236
|
db: Queryable,
|
|
217
237
|
projectIds: readonly string[],
|
|
218
238
|
environmentIds: readonly string[],
|
|
219
|
-
): Promise<{ projects: Set<string>; environments: Map<string, string>; deleted: Set<string> }> {
|
|
239
|
+
): Promise<{ projects: Set<string>; environments: Map<string, { projectId: string; slug: string }>; deleted: Set<string> }> {
|
|
220
240
|
const { projects, environments } = tablesOf(db);
|
|
221
241
|
const [foundProjects, foundEnvironments] = await Promise.all([
|
|
222
242
|
projectIds.length === 0
|
|
@@ -225,19 +245,43 @@ export async function places(
|
|
|
225
245
|
environmentIds.length === 0
|
|
226
246
|
? []
|
|
227
247
|
: db
|
|
228
|
-
.select({ id: environments.id, projectId: environments.projectId, deleted: truth(or(tombstone(environments.slug), tombstone(projects.slug))!) })
|
|
248
|
+
.select({ id: environments.id, projectId: environments.projectId, slug: environments.slug, deleted: truth(or(tombstone(environments.slug), tombstone(projects.slug))!) })
|
|
229
249
|
.from(environments)
|
|
230
250
|
.innerJoin(projects, eq(projects.id, environments.projectId))
|
|
231
251
|
.where(inArray(environments.id, [...environmentIds])),
|
|
232
252
|
]);
|
|
233
253
|
return {
|
|
234
254
|
projects: new Set(foundProjects.map((row) => row.id)),
|
|
235
|
-
environments: new Map(foundEnvironments.map((row) => [row.id, row.projectId])),
|
|
255
|
+
environments: new Map(foundEnvironments.map((row) => [row.id, { projectId: row.projectId, slug: row.slug }])),
|
|
236
256
|
// The projects and environments among them that were deleted, alone or with their project.
|
|
237
257
|
deleted: new Set([...foundProjects, ...foundEnvironments].filter((row) => row.deleted).map((row) => row.id)),
|
|
238
258
|
};
|
|
239
259
|
}
|
|
240
260
|
|
|
261
|
+
/** Every project not deleted, with its environments not deleted: what a grant on every project of 0.4 reached. */
|
|
262
|
+
export async function liveProjects(db: Queryable): Promise<{ id: string; environments: { id: string; slug: string }[] }[]> {
|
|
263
|
+
const { projects, environments } = tablesOf(db);
|
|
264
|
+
const [found, inside] = await Promise.all([
|
|
265
|
+
db.select({ id: projects.id, deleted: truth(tombstone(projects.slug)) }).from(projects).orderBy(asc(projects.id)),
|
|
266
|
+
db.select({ id: environments.id, projectId: environments.projectId, slug: environments.slug, deleted: truth(tombstone(environments.slug)) }).from(environments).orderBy(asc(environments.id)),
|
|
267
|
+
]);
|
|
268
|
+
return found.filter((project) => !project.deleted).map((project) => ({
|
|
269
|
+
id: project.id,
|
|
270
|
+
environments: inside.filter((environment) => environment.projectId === project.id && !environment.deleted).map(({ id, slug }) => ({ id, slug })),
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Who holds a grant on every project of 0.4, lapsed or not, which the vault replaces (`#convert`). */
|
|
275
|
+
export async function everyProjectHolders(db: Queryable): Promise<string[]> {
|
|
276
|
+
const { vaultGrants } = tablesOf(db);
|
|
277
|
+
const rows = await db
|
|
278
|
+
.selectDistinct({ principal: vaultGrants.principal })
|
|
279
|
+
.from(vaultGrants)
|
|
280
|
+
.where(and(isNull(vaultGrants.projectId), isNull(vaultGrants.environmentId)))
|
|
281
|
+
.orderBy(asc(vaultGrants.principal));
|
|
282
|
+
return rows.map((row) => row.principal);
|
|
283
|
+
}
|
|
284
|
+
|
|
241
285
|
// --- the log --------------------------------------------------------------------
|
|
242
286
|
|
|
243
287
|
/** The action of an entry that releases a key, which the bulk limit counts. */
|
|
@@ -261,33 +305,6 @@ export async function releasesSince(db: Queryable, principal: string, after: num
|
|
|
261
305
|
return Number(n);
|
|
262
306
|
}
|
|
263
307
|
|
|
264
|
-
function entryColumns(db: Queryable) {
|
|
265
|
-
const { auditLog } = tablesOf(db);
|
|
266
|
-
return {
|
|
267
|
-
seq: auditLog.seq,
|
|
268
|
-
author: auditLog.author,
|
|
269
|
-
keyId: auditLog.keyId,
|
|
270
|
-
occurredAt: auditLog.occurredAt,
|
|
271
|
-
actor: auditLog.actor,
|
|
272
|
-
action: auditLog.action,
|
|
273
|
-
decision: auditLog.decision,
|
|
274
|
-
code: auditLog.code,
|
|
275
|
-
subjectPrincipal: auditLog.subjectPrincipal,
|
|
276
|
-
projectId: auditLog.projectId,
|
|
277
|
-
environmentId: auditLog.environmentId,
|
|
278
|
-
secretId: auditLog.secretId,
|
|
279
|
-
secretVersionId: auditLog.secretVersionId,
|
|
280
|
-
operationId: auditLog.operationId,
|
|
281
|
-
requestId: auditLog.requestId,
|
|
282
|
-
sourceIp: auditLog.sourceIp,
|
|
283
|
-
relatedSeq: auditLog.relatedSeq,
|
|
284
|
-
metadata: auditLog.metadata,
|
|
285
|
-
prevHash: auditLog.prevHash,
|
|
286
|
-
mac: auditLog.mac,
|
|
287
|
-
hash: auditLog.hash,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
308
|
/** Rows of the log as the chain's codec reads them: `author` and `decision` are checked by the table. */
|
|
292
309
|
function stored(rows: object[]): StoredEntry[] {
|
|
293
310
|
return rows as StoredEntry[];
|
|
@@ -447,8 +464,8 @@ export async function latestVaultEntry(db: Queryable, actions: readonly string[]
|
|
|
447
464
|
return row === undefined ? undefined : stored([row])[0];
|
|
448
465
|
}
|
|
449
466
|
|
|
450
|
-
/** Up to `limit` of the vault's allowed
|
|
451
|
-
export async function
|
|
467
|
+
/** Up to `limit` of the vault's allowed entries of `action` before `before` (the newest when undefined), newest first. */
|
|
468
|
+
export async function newestVaultEntries(db: Queryable, action: string, limit: number, before?: bigint): Promise<StoredEntry[]> {
|
|
452
469
|
const { auditLog } = tablesOf(db);
|
|
453
470
|
const rows = await db
|
|
454
471
|
.select(entryColumns(db))
|
|
@@ -456,9 +473,9 @@ export async function accessEntriesAbout(db: Queryable, principal: string, limit
|
|
|
456
473
|
.where(
|
|
457
474
|
and(
|
|
458
475
|
eq(auditLog.author, 'vault'),
|
|
459
|
-
eq(auditLog.
|
|
460
|
-
inArray(auditLog.action, [...ACCESS_ACTIONS]),
|
|
476
|
+
eq(auditLog.action, action),
|
|
461
477
|
eq(auditLog.decision, 'allow'),
|
|
478
|
+
before === undefined ? undefined : lt(auditLog.seq, before),
|
|
462
479
|
),
|
|
463
480
|
)
|
|
464
481
|
.orderBy(desc(auditLog.seq))
|