@coffre/vault 0.14.4 → 0.15.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/README.md +6 -4
- package/dist/cloudflare.d.ts +343 -9
- package/dist/cloudflare.js +802 -6
- package/dist/{index-CIq4i_KT.d.ts → index-DQ-FKVF3.d.ts} +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/log-entry-BYWqj3O5.d.ts +364 -0
- package/dist/node.d.ts +182 -7
- package/dist/node.js +358 -16
- package/dist/{src-NM5NsYFs.js → src-DheG92EM.js} +9 -2
- package/dist/vault-kmtojYnW.js +14623 -0
- package/package.json +4 -5
- package/src/bulk.ts +89 -0
- package/src/changes.ts +478 -0
- package/src/checkpointer-node.ts +342 -0
- package/src/checkpointer-workers.ts +514 -0
- package/src/checkpointer.ts +1165 -0
- package/src/cloudflare.ts +207 -13
- package/src/config.ts +15 -3
- package/src/credentials.postgres.ts +78 -0
- package/src/credentials.sqlite.ts +240 -0
- package/src/credentials.ts +630 -0
- package/src/cutover.ts +275 -0
- package/src/decide.ts +437 -2
- package/src/inputs.ts +499 -0
- package/src/local.ts +20 -4
- package/src/log-checkpointer.ts +116 -0
- package/src/log-entry.ts +113 -0
- package/src/log-replay.ts +188 -0
- package/src/log-store.ts +695 -0
- package/src/node.ts +73 -9
- package/src/objects.ts +141 -0
- package/src/reads.ts +502 -0
- package/src/retention.ts +229 -0
- package/src/snapshot.postgres.ts +39 -9
- package/src/snapshot.sqlite.ts +28 -3
- package/src/snapshot.ts +72 -12
- package/src/store.ts +862 -37
- package/src/use.postgres.ts +111 -0
- package/src/use.sqlite.ts +542 -0
- package/src/use.ts +1060 -0
- package/src/vault.ts +1513 -240
- package/src/verifier.ts +136 -0
- package/src/witness-workers.ts +163 -0
- package/dist/vault-BBdXsny3.js +0 -5077
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { MAX_BINDINGS } from '@coffre/core/identity';
|
|
2
|
+
import { tablesOf, type Queryable } from '@coffre/db';
|
|
3
|
+
import { and, desc, eq, gt, inArray, isNull, sql } from 'drizzle-orm';
|
|
4
|
+
|
|
5
|
+
import type { Answer, BindingsCall, IssueCall, RevokeCall, SignInCall } from './credentials.ts';
|
|
6
|
+
import { objectKey } from './inputs.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `sign_in` on SQLite, step for step, in one transaction: SQLite runs one
|
|
10
|
+
* writer at a time, so its locks are implicit, and an entry's number is
|
|
11
|
+
* one past the log's highest.
|
|
12
|
+
*/
|
|
13
|
+
export async function signInOnSqlite(tx: Queryable, c: SignInCall): Promise<Answer> {
|
|
14
|
+
const { auditLog, consumedTokens, credentials, logChanges, serviceBindings, vaultMembers } = tablesOf(tx);
|
|
15
|
+
const target = objectKey({ kind: 'credential', id: c.credential.id });
|
|
16
|
+
const k = c.credential;
|
|
17
|
+
const [member] = await tx.select({ status: vaultMembers.status, generation: vaultMembers.generation, seal: vaultMembers.seal }).from(vaultMembers).where(eq(vaultMembers.principal, c.principal));
|
|
18
|
+
if (member?.status !== 'active' || member.generation !== c.binding.generation) return { state: 'unbound' };
|
|
19
|
+
// Its row as its newest change sealed it, or 0.14's with neither: a row put back is none.
|
|
20
|
+
const [sealedAs] = await tx.select({ seal: logChanges.seal }).from(logChanges).where(eq(logChanges.object, objectKey({ kind: 'principal', id: c.principal }))).orderBy(desc(logChanges.number)).limit(1);
|
|
21
|
+
if (!same(member.seal ?? null, sealedAs?.seal ?? null)) return { state: 'unbound' };
|
|
22
|
+
// The binding as the vault checked its seal, and that seal its object's newest change's (BindingSealed).
|
|
23
|
+
const [binding] = await tx.select().from(serviceBindings).where(eq(serviceBindings.id, c.binding.id));
|
|
24
|
+
const [bindingSealedAs] = await tx.select({ seal: logChanges.seal }).from(logChanges).where(eq(logChanges.object, objectKey({ kind: 'binding', id: c.binding.id }))).orderBy(desc(logChanges.number)).limit(1);
|
|
25
|
+
if (binding === undefined || binding.principal !== c.principal || binding.generation !== c.binding.generation || binding.revokedAt !== null || binding.profile !== c.binding.profile
|
|
26
|
+
|| binding.scope !== c.binding.scope || binding.policy !== c.binding.policy || binding.issuer !== c.binding.issuer || binding.jwksUri !== c.binding.jwksUri
|
|
27
|
+
|| binding.claims !== c.binding.claims || !same(binding.seal ?? null, c.bindingSeal) || !same(bindingSealedAs?.seal ?? null, c.bindingSeal)) {
|
|
28
|
+
return { state: 'unbound' };
|
|
29
|
+
}
|
|
30
|
+
const [moved] = await tx.select({ seq: auditLog.seq }).from(auditLog)
|
|
31
|
+
.where(and(eq(auditLog.author, 'app'), eq(auditLog.subjectPrincipal, c.appKey), inArray(auditLog.action, ['key.reseal', 'key.rotate']))).limit(1);
|
|
32
|
+
if (moved !== undefined) return { state: 'retired' };
|
|
33
|
+
const named = (key: string) => sql`json_extract(${auditLog.metadata}, ${`$.${key}`})`;
|
|
34
|
+
if (c.frozen) {
|
|
35
|
+
for (const target of ['instance', c.principal, `binding:${c.binding.id}`]) {
|
|
36
|
+
const [newest] = await tx.select({ action: auditLog.action }).from(auditLog)
|
|
37
|
+
.where(and(eq(auditLog.author, 'app'), inArray(auditLog.action, ['workload.freeze', 'workload.unfreeze']), eq(auditLog.decision, 'allow'), eq(named('target'), target)))
|
|
38
|
+
.orderBy(desc(auditLog.seq)).limit(1);
|
|
39
|
+
if (newest?.action === 'workload.freeze') return { state: 'frozen' };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const recent = await tx.select({ id: credentials.id }).from(credentials)
|
|
44
|
+
.where(and(eq(credentials.principal, c.principal), eq(credentials.createdBy, k.createdBy), gt(credentials.createdAt, new Date(now - c.window))))
|
|
45
|
+
.limit(c.rate);
|
|
46
|
+
if (recent.length >= c.rate) return { state: 'busy' };
|
|
47
|
+
if ((c.validUntil !== null && c.validUntil <= now) || k.expiresAt <= now || k.expiresAt > now + c.longest + 60_000 || (k.notAfter !== null && (k.notAfter <= now || k.expiresAt > k.notAfter))) return { state: 'expired' };
|
|
48
|
+
if (c.spent !== null) {
|
|
49
|
+
const spent = await tx.insert(consumedTokens).values({ hash: c.spent }).onConflictDoNothing().returning({ hash: consumedTokens.hash });
|
|
50
|
+
if (spent.length === 0) return { state: 'replayed' };
|
|
51
|
+
}
|
|
52
|
+
await tx.insert(credentials).values({
|
|
53
|
+
id: k.id, kind: k.kind, authMac: k.authMacBytes, tokenHash: k.tokenHashBytes, tokenHint: k.tokenHint, generation: c.binding.generation,
|
|
54
|
+
principal: c.principal, identityId: null, scope: k.scope, places: k.places, label: k.label, createdBy: k.createdBy,
|
|
55
|
+
expiresAt: new Date(k.expiresAt), revokedAt: null, jkt: k.jkt, notAfter: k.notAfter === null ? null : new Date(k.notAfter), seal: c.seal,
|
|
56
|
+
});
|
|
57
|
+
await tx.update(serviceBindings).set({ lastUsedAt: new Date(now) }).where(eq(serviceBindings.id, c.binding.id));
|
|
58
|
+
const number = await nextNumber(tx);
|
|
59
|
+
await tx.insert(logChanges).values({ number, at: c.at, requestId: c.requestId, object: target, actor: c.actor, payload: c.payload, seal: c.seal, mac: c.mac });
|
|
60
|
+
return { number: number.toString() };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `revoke_credential` on SQLite, step for step, in one transaction. */
|
|
64
|
+
export async function revokeOnSqlite(tx: Queryable, c: RevokeCall): Promise<Answer> {
|
|
65
|
+
const { auditLog, credentials, environments, logChanges, projects } = tablesOf(tx);
|
|
66
|
+
const { change, policy } = c;
|
|
67
|
+
const [row] = await tx.select({ seal: credentials.seal, authMac: credentials.authMac, revokedAt: credentials.revokedAt }).from(credentials).where(eq(credentials.id, c.ref.id));
|
|
68
|
+
if (row === undefined || row.revokedAt !== null || !same(row.authMac, c.macFrom) || !same(row.seal ?? null, change.sealFrom)) return { state: 'moved' };
|
|
69
|
+
const [newest] = await tx.select({ seal: logChanges.seal }).from(logChanges).where(eq(logChanges.object, change.object)).orderBy(desc(logChanges.number)).limit(1);
|
|
70
|
+
if (!same(newest?.seal ?? null, change.sealFrom)) return { state: 'stale' };
|
|
71
|
+
if (policy !== null) {
|
|
72
|
+
const [changed] = await tx.select({ seq: auditLog.seq }).from(auditLog)
|
|
73
|
+
.where(and(eq(auditLog.author, 'vault'), eq(auditLog.action, 'policy.change'), eq(auditLog.decision, 'allow'))).orderBy(desc(auditLog.seq)).limit(1);
|
|
74
|
+
if ((changed === undefined ? null : String(changed.seq)) !== policy.seq) return { state: 'policy' };
|
|
75
|
+
for (const { environmentId, slug } of policy.protected) {
|
|
76
|
+
const [place] = await tx.select({ slug: environments.slug, project: projects.slug }).from(environments)
|
|
77
|
+
.innerJoin(projects, eq(projects.id, environments.projectId)).where(eq(environments.id, environmentId));
|
|
78
|
+
const standing = place === undefined || place.slug.includes('~') || place.project.includes('~') ? null : place.slug;
|
|
79
|
+
if (standing !== slug) return { state: 'policy' };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await tx.update(credentials).set({ revokedAt: new Date(c.revokedAt), revokedBy: c.revokedBy, authMac: c.mac, seal: change.seal })
|
|
83
|
+
.where(and(eq(credentials.id, c.ref.id), isNull(credentials.revokedAt)));
|
|
84
|
+
const number = await nextNumber(tx);
|
|
85
|
+
await tx.insert(logChanges).values({ number, at: change.at, requestId: change.requestId, object: change.object, actor: change.actor, payload: change.payload, seal: change.seal, mac: change.mac });
|
|
86
|
+
return { number: number.toString() };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `issue_token` on SQLite, step for step, in one transaction: its one writer holds every row as read. */
|
|
90
|
+
export async function issueOnSqlite(tx: Queryable, c: IssueCall): Promise<Answer> {
|
|
91
|
+
const { auditLog, credentials, environments, logChanges, projects, vaultMembers } = tablesOf(tx);
|
|
92
|
+
const { change, input, read } = c;
|
|
93
|
+
const k = input.credential;
|
|
94
|
+
const standing = async (principal: string, generation: number, accessSeq: bigint) => {
|
|
95
|
+
const [row] = await tx.select({ status: vaultMembers.status, generation: vaultMembers.generation, accessSeq: vaultMembers.accessSeq }).from(vaultMembers).where(eq(vaultMembers.principal, principal));
|
|
96
|
+
return row?.status === 'active' && row.generation === generation && BigInt(row.accessSeq) === accessSeq;
|
|
97
|
+
};
|
|
98
|
+
if (!(await standing(input.actor, read.actor.generation, read.actor.accessSeq)) || !(await standing(input.principal, input.generation, read.principal.accessSeq))) return { state: 'moved' };
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
if (input.via !== null) {
|
|
101
|
+
const [via] = await tx.select({ principal: credentials.principal, revokedAt: credentials.revokedAt, seal: credentials.seal, expiresAt: credentials.expiresAt, notAfter: credentials.notAfter })
|
|
102
|
+
.from(credentials).where(eq(credentials.id, input.via));
|
|
103
|
+
const [newest] = await tx.select({ seal: logChanges.seal }).from(logChanges).where(eq(logChanges.object, objectKey({ kind: 'credential', id: input.via }))).orderBy(desc(logChanges.number)).limit(1);
|
|
104
|
+
// Its end as the vault authenticated it, still the columns' and still ahead (as `issue_token`).
|
|
105
|
+
const sealedAs = read.via?.seal ?? null;
|
|
106
|
+
const end = read.via?.columns.expiresAt ?? null;
|
|
107
|
+
const ends = via === undefined ? null : Math.min(via.expiresAt.getTime(), via.notAfter?.getTime() ?? Infinity);
|
|
108
|
+
if (via === undefined || via.principal !== input.actor || via.revokedAt !== null || sealedAs === null || end === null || ends !== end || end <= now
|
|
109
|
+
|| !same(via.seal ?? null, sealedAs) || !same(newest?.seal ?? null, sealedAs)) {
|
|
110
|
+
return { state: 'ended' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const [changed] = await tx.select({ seq: auditLog.seq }).from(auditLog)
|
|
114
|
+
.where(and(eq(auditLog.author, 'vault'), eq(auditLog.action, 'policy.change'), eq(auditLog.decision, 'allow'))).orderBy(desc(auditLog.seq)).limit(1);
|
|
115
|
+
if ((changed === undefined ? null : String(changed.seq)) !== input.policy.seq) return { state: 'policy' };
|
|
116
|
+
for (const { environmentId, slug } of read.slugs) {
|
|
117
|
+
const [place] = await tx.select({ slug: environments.slug, project: projects.slug }).from(environments).innerJoin(projects, eq(projects.id, environments.projectId)).where(eq(environments.id, environmentId));
|
|
118
|
+
if ((place === undefined || place.slug.includes('~') || place.project.includes('~') ? null : place.slug) !== slug) return { state: 'policy' };
|
|
119
|
+
}
|
|
120
|
+
const [moved] = await tx.select({ seq: auditLog.seq }).from(auditLog)
|
|
121
|
+
.where(and(eq(auditLog.author, 'app'), eq(auditLog.subjectPrincipal, input.appKey), inArray(auditLog.action, ['key.reseal', 'key.rotate']))).limit(1);
|
|
122
|
+
if (moved !== undefined) return { state: 'retired' };
|
|
123
|
+
if (k.expiresAt <= now) return { state: 'expired' };
|
|
124
|
+
await tx.insert(credentials).values({
|
|
125
|
+
id: k.id, kind: k.kind, authMac: c.authMac, tokenHash: c.tokenHash, tokenHint: k.tokenHint, generation: input.generation, principal: input.principal,
|
|
126
|
+
identityId: null, scope: k.scope, places: k.places, label: k.label, createdBy: k.createdBy, expiresAt: new Date(k.expiresAt), revokedAt: null, seal: change.seal,
|
|
127
|
+
});
|
|
128
|
+
const number = await nextNumber(tx);
|
|
129
|
+
await tx.insert(logChanges).values({ number, at: change.at, requestId: change.requestId, object: change.object, actor: change.actor, payload: change.payload, seal: change.seal, mac: change.mac });
|
|
130
|
+
return { number: number.toString() };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `change_bindings` on SQLite, step for step, in one transaction: SQLite runs one writer at a time. */
|
|
134
|
+
export async function bindingsOnSqlite(tx: Queryable, c: BindingsCall): Promise<Answer> {
|
|
135
|
+
const { auditLog, credentials, environments, logChanges, mcpConnections, projects, secrets, serviceBindings, vaultMembers } = tablesOf(tx);
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
const newest = async (ref: Parameters<typeof objectKey>[0]) =>
|
|
138
|
+
(await tx.select({ seal: logChanges.seal }).from(logChanges).where(eq(logChanges.object, objectKey(ref))).orderBy(desc(logChanges.number)).limit(1))[0]?.seal ?? null;
|
|
139
|
+
const m = c.make;
|
|
140
|
+
if (m !== null) {
|
|
141
|
+
const standing = async (principal: string, generation: number, accessSeq: bigint) => {
|
|
142
|
+
const [row] = await tx.select({ status: vaultMembers.status, generation: vaultMembers.generation, accessSeq: vaultMembers.accessSeq }).from(vaultMembers).where(eq(vaultMembers.principal, principal));
|
|
143
|
+
return row?.status === 'active' && row.generation === generation && BigInt(row.accessSeq) === accessSeq;
|
|
144
|
+
};
|
|
145
|
+
if (!(await standing(c.actor, m.read.actor.generation, m.read.actor.accessSeq)) || !(await standing(c.principal, m.row.generation, m.read.principal.accessSeq))) return { state: 'moved' };
|
|
146
|
+
if (m.via !== null) {
|
|
147
|
+
const [via] = await tx.select({ principal: credentials.principal, revokedAt: credentials.revokedAt, seal: credentials.seal, expiresAt: credentials.expiresAt, notAfter: credentials.notAfter })
|
|
148
|
+
.from(credentials).where(eq(credentials.id, m.via));
|
|
149
|
+
const sealedAs = m.read.via?.seal ?? null;
|
|
150
|
+
const end = m.read.via?.columns.expiresAt ?? null;
|
|
151
|
+
const ends = via === undefined ? null : Math.min(via.expiresAt.getTime(), via.notAfter?.getTime() ?? Infinity);
|
|
152
|
+
if (via === undefined || via.principal !== c.actor || via.revokedAt !== null || sealedAs === null || end === null || ends !== end || end <= now
|
|
153
|
+
|| !same(via.seal ?? null, sealedAs) || !same(await newest({ kind: 'credential', id: m.via }), sealedAs)) {
|
|
154
|
+
return { state: 'ended' };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// The MCP connection it came through, still connected; an id the log holds already, no binding to make (review r3).
|
|
158
|
+
if (m.connection !== null) {
|
|
159
|
+
const [connection] = await tx.select({ revokedAt: mcpConnections.revokedAt, generation: mcpConnections.generation }).from(mcpConnections).where(eq(mcpConnections.id, m.connection.id));
|
|
160
|
+
if (connection === undefined || connection.revokedAt !== null || connection.generation !== m.connection.generation) return { state: 'ended' };
|
|
161
|
+
}
|
|
162
|
+
const [made] = await tx.select({ number: logChanges.number }).from(logChanges).where(eq(logChanges.object, objectKey({ kind: 'binding', id: m.row.id }))).limit(1);
|
|
163
|
+
if (made !== undefined) return { state: 'stale' };
|
|
164
|
+
}
|
|
165
|
+
// The account's live bindings as the app read them, at most `MAX_BINDINGS` besides those it ends; the keys it reads by their names.
|
|
166
|
+
if (m !== null) {
|
|
167
|
+
const live = await tx.select({ id: serviceBindings.id }).from(serviceBindings)
|
|
168
|
+
.where(and(eq(serviceBindings.principal, c.principal), eq(serviceBindings.generation, m.row.generation), isNull(serviceBindings.revokedAt)));
|
|
169
|
+
if (live.map(({ id }) => id).sort().join() !== [...(c.live ?? [])].sort().join()) return { state: 'moved' };
|
|
170
|
+
if (live.filter(({ id }) => !c.ends.some((end) => end.id === id)).length >= MAX_BINDINGS) return { state: 'full' };
|
|
171
|
+
for (const read of c.reads) {
|
|
172
|
+
const [row] = await tx.select({ key: secrets.key }).from(secrets).where(eq(secrets.id, read.id));
|
|
173
|
+
if (row?.key !== read.key) return { state: 'moved' };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// The bindings it ends, and every live credential they issued, as read.
|
|
177
|
+
for (const end of c.ends) {
|
|
178
|
+
const [row] = await tx.select({ principal: serviceBindings.principal, revokedAt: serviceBindings.revokedAt, authMac: serviceBindings.authMac, seal: serviceBindings.seal })
|
|
179
|
+
.from(serviceBindings).where(eq(serviceBindings.id, end.id));
|
|
180
|
+
if (row === undefined || row.principal !== c.principal || row.revokedAt !== null || !same(row.authMac, end.macFrom) || !same(row.seal ?? null, end.sealFrom)) return { state: 'moved' };
|
|
181
|
+
const live = await tx.select({ id: credentials.id }).from(credentials)
|
|
182
|
+
.where(and(eq(credentials.principal, c.principal), eq(credentials.createdBy, `binding:${end.id}`), isNull(credentials.revokedAt), gt(credentials.expiresAt, new Date(now))));
|
|
183
|
+
const named = new Set(c.credentials.filter((x) => x.binding === end.id).map((x) => x.id));
|
|
184
|
+
if (live.some(({ id }) => !named.has(id))) return { state: 'moved' };
|
|
185
|
+
}
|
|
186
|
+
for (const x of c.credentials) {
|
|
187
|
+
const [row] = await tx.select({ createdBy: credentials.createdBy, revokedAt: credentials.revokedAt, authMac: credentials.authMac, seal: credentials.seal })
|
|
188
|
+
.from(credentials).where(eq(credentials.id, x.id));
|
|
189
|
+
if (row === undefined || row.createdBy !== `binding:${x.binding}` || !c.ends.some((end) => end.id === x.binding) || row.revokedAt !== null
|
|
190
|
+
|| !same(row.authMac, x.macFrom) || !same(row.seal ?? null, x.sealFrom)) {
|
|
191
|
+
return { state: 'moved' };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const [changed] = await tx.select({ seq: auditLog.seq }).from(auditLog)
|
|
195
|
+
.where(and(eq(auditLog.author, 'vault'), eq(auditLog.action, 'policy.change'), eq(auditLog.decision, 'allow'))).orderBy(desc(auditLog.seq)).limit(1);
|
|
196
|
+
if ((changed === undefined ? null : String(changed.seq)) !== c.policy.seq) return { state: 'policy' };
|
|
197
|
+
for (const { environmentId, slug } of c.policy.slugs) {
|
|
198
|
+
const [place] = await tx.select({ slug: environments.slug, project: projects.slug }).from(environments).innerJoin(projects, eq(projects.id, environments.projectId)).where(eq(environments.id, environmentId));
|
|
199
|
+
if ((place === undefined || place.slug.includes('~') || place.project.includes('~') ? null : place.slug) !== slug) return { state: 'policy' };
|
|
200
|
+
}
|
|
201
|
+
const [moved] = await tx.select({ seq: auditLog.seq }).from(auditLog)
|
|
202
|
+
.where(and(eq(auditLog.author, 'app'), eq(auditLog.subjectPrincipal, c.appKey), inArray(auditLog.action, ['key.reseal', 'key.rotate']))).limit(1);
|
|
203
|
+
if (moved !== undefined) return { state: 'retired' };
|
|
204
|
+
// ChangeChecksSeal: each row it ends its object's newest change's.
|
|
205
|
+
for (const end of c.ends) if (!same(await newest({ kind: 'binding', id: end.id }), end.sealFrom)) return { state: 'stale' };
|
|
206
|
+
for (const x of c.credentials) if (!same(await newest({ kind: 'credential', id: x.id }), x.sealFrom)) return { state: 'stale' };
|
|
207
|
+
if (m !== null) {
|
|
208
|
+
const b = m.row;
|
|
209
|
+
await tx.insert(serviceBindings).values({
|
|
210
|
+
id: b.id, authMac: m.authMac, principal: b.principal, generation: b.generation, profile: b.profile, issuer: b.issuer, jwksUri: b.jwksUri, jwks: b.jwks,
|
|
211
|
+
claims: b.claims, scope: b.scope, policy: b.policy, origin: b.origin, label: b.label, createdBy: b.createdBy, createdAt: new Date(b.createdAt), seal: m.seal,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const ended = new Date(c.revokedAt);
|
|
215
|
+
for (const end of c.ends) {
|
|
216
|
+
await tx.update(serviceBindings).set({ revokedAt: ended, revokedBy: c.revokedBy, authMac: end.authMac, seal: end.seal }).where(eq(serviceBindings.id, end.id));
|
|
217
|
+
}
|
|
218
|
+
for (const x of c.credentials) {
|
|
219
|
+
await tx.update(credentials).set({ revokedAt: ended, revokedBy: c.revokedBy, authMac: x.authMac, seal: x.seal }).where(eq(credentials.id, x.id));
|
|
220
|
+
}
|
|
221
|
+
let number = 0n;
|
|
222
|
+
for (const x of c.changes) {
|
|
223
|
+
number = await nextNumber(tx);
|
|
224
|
+
await tx.insert(logChanges).values({ number, at: c.at, requestId: x.requestId, object: x.object, actor: c.actor, payload: x.payload, seal: x.seal, mac: x.mac });
|
|
225
|
+
}
|
|
226
|
+
return { number: number.toString() };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** One past the log's highest number, both tables' (store.ts's `changeOnSqlite` says why). */
|
|
230
|
+
async function nextNumber(tx: Queryable): Promise<bigint> {
|
|
231
|
+
const { logChanges, logUses } = tablesOf(tx);
|
|
232
|
+
const [{ last }] = await tx.select({ last: sql<number | string | null>`max(n)` }).from(
|
|
233
|
+
sql`(SELECT max(${logChanges.number}) AS n FROM ${logChanges} UNION ALL SELECT max(${logUses.number}) FROM ${logUses})`,
|
|
234
|
+
);
|
|
235
|
+
return BigInt(last ?? 0) + 1n;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function same(a: Uint8Array | null, b: Uint8Array | null): boolean {
|
|
239
|
+
return a === null || b === null ? a === b : Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
|
|
240
|
+
}
|