@kokuin/store-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/README.md +126 -2
- package/lib/api.d.ts +5 -0
- package/lib/api.js +504 -0
- package/lib/batch.d.ts +89 -0
- package/lib/batch.js +159 -0
- package/lib/coordinator-context.d.ts +71 -0
- package/lib/coordinator-context.js +410 -0
- package/lib/coordinator-entries.d.ts +20 -0
- package/lib/coordinator-entries.js +258 -0
- package/lib/coordinator-open.d.ts +9 -0
- package/lib/coordinator-open.js +141 -0
- package/lib/coordinator-records.d.ts +27 -0
- package/lib/coordinator-records.js +132 -0
- package/lib/coordinator-rotate.d.ts +9 -0
- package/lib/coordinator-rotate.js +436 -0
- package/lib/coordinator-types.d.ts +139 -0
- package/lib/coordinator-types.js +1 -0
- package/lib/coordinator.d.ts +2 -0
- package/lib/coordinator.js +459 -0
- package/lib/definition.d.ts +6 -0
- package/lib/definition.js +12 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +7 -0
- package/lib/migrations.d.ts +2 -0
- package/lib/migrations.js +52 -0
- package/lib/record-context.d.ts +24 -0
- package/lib/record-context.js +34 -0
- package/lib/record-definition.d.ts +7 -0
- package/lib/record-definition.js +93 -0
- package/lib/record-tables.d.ts +47 -0
- package/lib/record-tables.js +1 -0
- package/lib/records-adapter.d.ts +3 -0
- package/lib/records-adapter.js +197 -0
- package/lib/records-ports.d.ts +4 -0
- package/lib/records-ports.js +72 -0
- package/lib/records-projection.d.ts +3 -0
- package/lib/records-projection.js +105 -0
- package/lib/records-rows.d.ts +54 -0
- package/lib/records-rows.js +155 -0
- package/lib/records.d.ts +59 -0
- package/lib/records.js +233 -0
- package/lib/settle.d.ts +12 -0
- package/lib/settle.js +18 -0
- package/lib/tables.d.ts +42 -0
- package/lib/tables.js +1 -0
- package/lib/types.d.ts +74 -0
- package/lib/types.js +1 -0
- package/package.json +45 -3
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { createVaultKeyReference, factorsFromDescriptors, getSuite, isSupportedSuite, recipientDIDForFactors, satisfiableFactors, vaultRevocationRecordID } from '@kokuin/vault';
|
|
2
|
+
import { createCoordinatorContext, VaultNotFound } from './coordinator-context.js';
|
|
3
|
+
import { createEntryFunctions } from './coordinator-entries.js';
|
|
4
|
+
import { createOpenFunctions } from './coordinator-open.js';
|
|
5
|
+
import { createRecordFunctions } from './coordinator-records.js';
|
|
6
|
+
import { createRotateFunctions } from './coordinator-rotate.js';
|
|
7
|
+
import { VaultWriteConflict } from './record-context.js';
|
|
8
|
+
import { settleAll, settledValue } from './settle.js';
|
|
9
|
+
export function createVaultCoordinator(params) {
|
|
10
|
+
const ctx = createCoordinatorContext(params);
|
|
11
|
+
const open = createOpenFunctions(ctx);
|
|
12
|
+
const records = createRecordFunctions(ctx);
|
|
13
|
+
const rotate = createRotateFunctions(ctx, open, records);
|
|
14
|
+
const entries = createEntryFunctions(ctx, open, records);
|
|
15
|
+
async function createKey(request) {
|
|
16
|
+
await ctx.authorize('createKey', 'administer', request);
|
|
17
|
+
const unique = params.runtime.getRandomValues(new Uint8Array(16));
|
|
18
|
+
const { keyID, keyUnique } = createVaultKeyReference(request.ownerDID, unique);
|
|
19
|
+
const key = {
|
|
20
|
+
ownerDID: request.ownerDID,
|
|
21
|
+
keyID,
|
|
22
|
+
version: 1,
|
|
23
|
+
suite: request.suite ?? 1,
|
|
24
|
+
state: 'active'
|
|
25
|
+
};
|
|
26
|
+
const suite = getSuite(key.suite);
|
|
27
|
+
let wrappings = request.wrappings;
|
|
28
|
+
if (wrappings === undefined) {
|
|
29
|
+
if (params.identity === null) throw new TypeError('An explicit wrapping or agreement identity is required');
|
|
30
|
+
const recipientDID = 'longForm' in params.identity && typeof params.identity.longForm === 'string' ? params.identity.longForm : params.identity.id;
|
|
31
|
+
wrappings = [
|
|
32
|
+
[
|
|
33
|
+
{
|
|
34
|
+
kind: 'did',
|
|
35
|
+
recipientDID
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
if (wrappings.length === 0) throw new TypeError('At least one wrapping is required');
|
|
41
|
+
const factorSets = wrappings;
|
|
42
|
+
const contentKey = suite.generateContentKey(params.runtime);
|
|
43
|
+
try {
|
|
44
|
+
await ctx.locked([
|
|
45
|
+
key
|
|
46
|
+
], async (store)=>{
|
|
47
|
+
if (await store.getKey(key) !== undefined) throw new VaultWriteConflict();
|
|
48
|
+
// Key records and wraps are independent. Wrapping IDs are drawn in factor-set order.
|
|
49
|
+
const [chain, wrapped] = await settleAll([
|
|
50
|
+
params.mode === 'replicated' ? ctx.keyRecords(key, store, keyUnique, request.consumer) : Promise.resolve({
|
|
51
|
+
records: [],
|
|
52
|
+
branchID: '',
|
|
53
|
+
branchRecordID: ''
|
|
54
|
+
}),
|
|
55
|
+
settleAll(factorSets.map(async (factors)=>{
|
|
56
|
+
const wrappingID = params.runtime.getRandomID();
|
|
57
|
+
const result = await suite.wrap({
|
|
58
|
+
runtime: params.runtime,
|
|
59
|
+
contentKey,
|
|
60
|
+
factors,
|
|
61
|
+
wrappingID,
|
|
62
|
+
keyID: key.keyID,
|
|
63
|
+
keyVersion: 1,
|
|
64
|
+
methods: params.methods
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
factors,
|
|
68
|
+
wrappingID,
|
|
69
|
+
result
|
|
70
|
+
};
|
|
71
|
+
}))
|
|
72
|
+
]);
|
|
73
|
+
const rows = wrapped.map(({ factors, wrappingID, result })=>{
|
|
74
|
+
return {
|
|
75
|
+
...key,
|
|
76
|
+
wrappingID,
|
|
77
|
+
keyVersion: 1,
|
|
78
|
+
recipientDID: recipientDIDForFactors(factors),
|
|
79
|
+
provenance: chain.branchID,
|
|
80
|
+
...result
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
if (params.mode === 'replicated') {
|
|
84
|
+
const output = [
|
|
85
|
+
...chain.records
|
|
86
|
+
];
|
|
87
|
+
// Signing stays sequential: a failure stops the rest. HLC allocation and output order
|
|
88
|
+
// follow the factor sets.
|
|
89
|
+
for (const row of rows)output.push(await records.wrapping(row, chain, request.consumer ?? null));
|
|
90
|
+
await ctx.write(store, output, {
|
|
91
|
+
key: {
|
|
92
|
+
...key,
|
|
93
|
+
expectedVersion: null,
|
|
94
|
+
expectedBranches: []
|
|
95
|
+
}
|
|
96
|
+
}, request);
|
|
97
|
+
} else {
|
|
98
|
+
await store.putKey(key);
|
|
99
|
+
await store.putWrappings(rows);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
ownerDID: key.ownerDID,
|
|
104
|
+
keyID: key.keyID
|
|
105
|
+
};
|
|
106
|
+
} finally{
|
|
107
|
+
contentKey.fill(0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function rewrap(request, change) {
|
|
111
|
+
await ctx.locked([
|
|
112
|
+
request
|
|
113
|
+
], async (store)=>{
|
|
114
|
+
const key = await ctx.key(request, store);
|
|
115
|
+
const [branch, branches, wrappings] = await settleAll([
|
|
116
|
+
ctx.branch(request, key.version, store),
|
|
117
|
+
ctx.branches(key, key.version, store),
|
|
118
|
+
store.listWrappings({
|
|
119
|
+
...key,
|
|
120
|
+
keyVersion: key.version
|
|
121
|
+
})
|
|
122
|
+
]);
|
|
123
|
+
const rows = wrappings.filter((w)=>{
|
|
124
|
+
return (params.mode === 'local' || w.provenance === branch.branchID) && w.descriptors.some((d)=>d.kind === 'did') && satisfiableFactors(w.descriptors, params.identity) !== null;
|
|
125
|
+
});
|
|
126
|
+
if (rows.length !== 1) throw new VaultNotFound();
|
|
127
|
+
const existing = rows[0];
|
|
128
|
+
if (existing === undefined) throw new VaultNotFound();
|
|
129
|
+
if (request.atVersion !== undefined && request.atVersion !== key.version) throw new VaultWriteConflict();
|
|
130
|
+
const evidence = await ctx.wrappingEvidence(existing, store);
|
|
131
|
+
const opened = await open.open({
|
|
132
|
+
...request,
|
|
133
|
+
atVersion: key.version
|
|
134
|
+
}, store);
|
|
135
|
+
try {
|
|
136
|
+
const wrappingID = params.runtime.getRandomID();
|
|
137
|
+
const factors = change(factorsFromDescriptors(existing.descriptors, request.inputs));
|
|
138
|
+
const wrapped = await getSuite(opened.suite).wrap({
|
|
139
|
+
runtime: params.runtime,
|
|
140
|
+
contentKey: opened.contentKey,
|
|
141
|
+
factors,
|
|
142
|
+
wrappingID,
|
|
143
|
+
keyID: key.keyID,
|
|
144
|
+
keyVersion: key.version,
|
|
145
|
+
methods: params.methods
|
|
146
|
+
});
|
|
147
|
+
const row = {
|
|
148
|
+
...existing,
|
|
149
|
+
wrappingID,
|
|
150
|
+
...wrapped,
|
|
151
|
+
recipientDID: recipientDIDForFactors(factors)
|
|
152
|
+
};
|
|
153
|
+
if (params.mode === 'local') {
|
|
154
|
+
// Different wrapping IDs: the write and the delete are independent.
|
|
155
|
+
await settleAll([
|
|
156
|
+
store.putWrapping(row),
|
|
157
|
+
store.deleteWrapping(existing)
|
|
158
|
+
]);
|
|
159
|
+
} else {
|
|
160
|
+
// The verified evidence already holds the prior wrapping's authenticated metadata.
|
|
161
|
+
if (evidence === undefined) throw new VaultNotFound();
|
|
162
|
+
const consumer = evidence.op.claim.meta.consumer;
|
|
163
|
+
const replacement = await records.wrapping(row, branch, consumer);
|
|
164
|
+
const deletion = await ctx.signed('vault.wrapping', {
|
|
165
|
+
ownerDID: key.ownerDID,
|
|
166
|
+
id: existing.wrappingID
|
|
167
|
+
}, null, {
|
|
168
|
+
keyID: key.keyID,
|
|
169
|
+
keyVersion: key.version,
|
|
170
|
+
recipient: existing.recipientDID,
|
|
171
|
+
consumer
|
|
172
|
+
});
|
|
173
|
+
await ctx.write(store, [
|
|
174
|
+
replacement,
|
|
175
|
+
deletion
|
|
176
|
+
], {
|
|
177
|
+
key: {
|
|
178
|
+
...key,
|
|
179
|
+
expectedVersion: key.version,
|
|
180
|
+
expectedBranches: branches.map((b)=>b.branchID)
|
|
181
|
+
}
|
|
182
|
+
}, request);
|
|
183
|
+
}
|
|
184
|
+
} finally{
|
|
185
|
+
opened.contentKey.fill(0);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
ctx.evict(request);
|
|
189
|
+
}
|
|
190
|
+
const coordinator = {
|
|
191
|
+
get records () {
|
|
192
|
+
if (ctx.records === undefined) throw new TypeError('Local coordinator has no records path');
|
|
193
|
+
return ctx.records;
|
|
194
|
+
},
|
|
195
|
+
createKey,
|
|
196
|
+
async openKey (request) {
|
|
197
|
+
await ctx.authorize('openKey', 'read', request);
|
|
198
|
+
return open.open(request);
|
|
199
|
+
},
|
|
200
|
+
async unlock (request) {
|
|
201
|
+
await ctx.authorize('unlock', 'read', request);
|
|
202
|
+
const opened = await open.open(request);
|
|
203
|
+
opened.contentKey.fill(0);
|
|
204
|
+
},
|
|
205
|
+
async lock (address) {
|
|
206
|
+
await ctx.authorize('lock', 'read', address);
|
|
207
|
+
ctx.evict(address);
|
|
208
|
+
},
|
|
209
|
+
async lockAll (ownerDID, call = {}) {
|
|
210
|
+
await ctx.authorize('lockAll', 'read', {
|
|
211
|
+
ownerDID,
|
|
212
|
+
...call
|
|
213
|
+
});
|
|
214
|
+
ctx.evictAll(ownerDID);
|
|
215
|
+
},
|
|
216
|
+
async status (request) {
|
|
217
|
+
await ctx.authorize('status', 'read', request);
|
|
218
|
+
// Both reads run together. Their results are checked in the old order: key, suite, revocation.
|
|
219
|
+
const [keyRead, revokedRead] = await Promise.allSettled([
|
|
220
|
+
ctx.key(request),
|
|
221
|
+
open.revoked(request, params.store)
|
|
222
|
+
]);
|
|
223
|
+
const key = settledValue(keyRead);
|
|
224
|
+
if (!isSupportedSuite(key.suite)) return {
|
|
225
|
+
state: 'unopenable'
|
|
226
|
+
};
|
|
227
|
+
const version = request.atVersion ?? key.version;
|
|
228
|
+
if (settledValue(revokedRead)) {
|
|
229
|
+
ctx.evict(request);
|
|
230
|
+
return {
|
|
231
|
+
state: 'unopenable'
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const branch = await ctx.branch(request, version);
|
|
235
|
+
const hit = ctx.cache.get(ctx.cacheID(request), version);
|
|
236
|
+
if (hit !== null) {
|
|
237
|
+
const matches = hit.branchID === branch.branchID;
|
|
238
|
+
hit.contentKey.fill(0);
|
|
239
|
+
if (matches) return {
|
|
240
|
+
state: 'unlocked'
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const options = (await params.store.listWrappings({
|
|
244
|
+
...request,
|
|
245
|
+
keyVersion: version
|
|
246
|
+
})).filter((w)=>params.mode === 'local' || w.provenance === branch.branchID).map((w)=>satisfiableFactors(w.descriptors, params.identity)).filter((value)=>value !== null);
|
|
247
|
+
if (options.some((o)=>o.length === 0)) return {
|
|
248
|
+
state: 'available'
|
|
249
|
+
};
|
|
250
|
+
return options.length > 0 ? {
|
|
251
|
+
state: 'locked',
|
|
252
|
+
unlockOptions: options
|
|
253
|
+
} : {
|
|
254
|
+
state: 'unopenable'
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
async listKeys (ownerDID, call = {}) {
|
|
258
|
+
await ctx.authorize('listKeys', 'read', {
|
|
259
|
+
ownerDID,
|
|
260
|
+
...call
|
|
261
|
+
});
|
|
262
|
+
return params.store.listKeys(ownerDID);
|
|
263
|
+
},
|
|
264
|
+
async setKeyState (request) {
|
|
265
|
+
await ctx.authorize('setKeyState', 'administer', request);
|
|
266
|
+
await ctx.locked([
|
|
267
|
+
request
|
|
268
|
+
], async (store)=>{
|
|
269
|
+
if (params.mode === 'local') {
|
|
270
|
+
await ctx.key(request, store);
|
|
271
|
+
await store.setKeyState(request);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const [key, previous] = await settleAll([
|
|
275
|
+
ctx.key(request, store),
|
|
276
|
+
ctx.stored({
|
|
277
|
+
type: 'vault.key',
|
|
278
|
+
owner: request.ownerDID,
|
|
279
|
+
id: request.keyID
|
|
280
|
+
}, store)
|
|
281
|
+
]);
|
|
282
|
+
const record = await ctx.signed('vault.key', {
|
|
283
|
+
ownerDID: key.ownerDID,
|
|
284
|
+
id: key.keyID
|
|
285
|
+
}, {
|
|
286
|
+
suite: key.suite,
|
|
287
|
+
state: request.state
|
|
288
|
+
}, {
|
|
289
|
+
...previous.op.claim.meta,
|
|
290
|
+
keyID: key.keyID,
|
|
291
|
+
keyUnique: previous.op.claim.meta.keyUnique,
|
|
292
|
+
version: key.version,
|
|
293
|
+
state: request.state,
|
|
294
|
+
consumer: previous.op.claim.meta.consumer
|
|
295
|
+
});
|
|
296
|
+
await ctx.write(store, [
|
|
297
|
+
record
|
|
298
|
+
], {
|
|
299
|
+
key: {
|
|
300
|
+
...key,
|
|
301
|
+
expectedVersion: key.version
|
|
302
|
+
}
|
|
303
|
+
}, request);
|
|
304
|
+
});
|
|
305
|
+
ctx.evict(request);
|
|
306
|
+
},
|
|
307
|
+
async deleteKey (request) {
|
|
308
|
+
await ctx.authorize('deleteKey', 'administer', request);
|
|
309
|
+
await ctx.locked([
|
|
310
|
+
request
|
|
311
|
+
], async (store)=>{
|
|
312
|
+
if (params.mode === 'local') {
|
|
313
|
+
await ctx.key(request, store);
|
|
314
|
+
await store.deleteKey(request);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const [key, previous] = await settleAll([
|
|
318
|
+
ctx.key(request, store),
|
|
319
|
+
ctx.stored({
|
|
320
|
+
type: 'vault.key',
|
|
321
|
+
owner: request.ownerDID,
|
|
322
|
+
id: request.keyID
|
|
323
|
+
}, store)
|
|
324
|
+
]);
|
|
325
|
+
const keyUnique = previous.op.claim.meta.keyUnique;
|
|
326
|
+
if (typeof keyUnique !== 'string') throw new VaultNotFound();
|
|
327
|
+
const record = await ctx.signed('vault.key', {
|
|
328
|
+
ownerDID: key.ownerDID,
|
|
329
|
+
id: key.keyID
|
|
330
|
+
}, null, {
|
|
331
|
+
keyID: key.keyID,
|
|
332
|
+
keyUnique,
|
|
333
|
+
version: key.version,
|
|
334
|
+
state: 'deleted',
|
|
335
|
+
consumer: previous.op.claim.meta.consumer
|
|
336
|
+
});
|
|
337
|
+
await ctx.write(store, [
|
|
338
|
+
record
|
|
339
|
+
], {
|
|
340
|
+
key: {
|
|
341
|
+
...key,
|
|
342
|
+
expectedVersion: key.version
|
|
343
|
+
}
|
|
344
|
+
}, request);
|
|
345
|
+
});
|
|
346
|
+
ctx.evict(request);
|
|
347
|
+
},
|
|
348
|
+
async grant (request) {
|
|
349
|
+
await ctx.authorize('grant', 'administer', request);
|
|
350
|
+
await ctx.locked([
|
|
351
|
+
request
|
|
352
|
+
], async (store)=>{
|
|
353
|
+
const [key, revocations] = await settleAll([
|
|
354
|
+
ctx.key(request, store),
|
|
355
|
+
params.mode === 'replicated' ? ctx.recordsFor(store).listRevocations(request) : Promise.resolve([])
|
|
356
|
+
]);
|
|
357
|
+
if (revocations.some((did)=>{
|
|
358
|
+
return vaultRevocationRecordID(key.keyID, did) === vaultRevocationRecordID(key.keyID, request.recipientDID);
|
|
359
|
+
})) throw new VaultNotFound();
|
|
360
|
+
if (request.atVersion !== undefined && request.atVersion !== key.version) throw new VaultWriteConflict();
|
|
361
|
+
const opened = await open.open({
|
|
362
|
+
...request,
|
|
363
|
+
atVersion: key.version
|
|
364
|
+
}, store);
|
|
365
|
+
try {
|
|
366
|
+
const wrappingID = params.runtime.getRandomID();
|
|
367
|
+
const wrapped = await getSuite(opened.suite).wrap({
|
|
368
|
+
runtime: params.runtime,
|
|
369
|
+
contentKey: opened.contentKey,
|
|
370
|
+
factors: [
|
|
371
|
+
{
|
|
372
|
+
kind: 'did',
|
|
373
|
+
recipientDID: request.recipientDID
|
|
374
|
+
}
|
|
375
|
+
],
|
|
376
|
+
wrappingID,
|
|
377
|
+
keyID: key.keyID,
|
|
378
|
+
keyVersion: key.version,
|
|
379
|
+
methods: params.methods
|
|
380
|
+
});
|
|
381
|
+
const row = {
|
|
382
|
+
...key,
|
|
383
|
+
wrappingID,
|
|
384
|
+
keyVersion: key.version,
|
|
385
|
+
recipientDID: recipientDIDForFactors([
|
|
386
|
+
{
|
|
387
|
+
kind: 'did',
|
|
388
|
+
recipientDID: request.recipientDID
|
|
389
|
+
}
|
|
390
|
+
]),
|
|
391
|
+
provenance: opened.branchID,
|
|
392
|
+
...wrapped
|
|
393
|
+
};
|
|
394
|
+
if (params.mode === 'local') await store.putWrapping(row);
|
|
395
|
+
else {
|
|
396
|
+
const [branch, branches] = await settleAll([
|
|
397
|
+
ctx.branch(key, key.version, store),
|
|
398
|
+
ctx.branches(key, key.version, store)
|
|
399
|
+
]);
|
|
400
|
+
const record = await records.wrapping(row, branch);
|
|
401
|
+
await ctx.write(store, [
|
|
402
|
+
record
|
|
403
|
+
], {
|
|
404
|
+
key: {
|
|
405
|
+
...key,
|
|
406
|
+
expectedVersion: key.version,
|
|
407
|
+
expectedBranches: branches.map((b)=>b.branchID)
|
|
408
|
+
}
|
|
409
|
+
}, request);
|
|
410
|
+
}
|
|
411
|
+
} finally{
|
|
412
|
+
opened.contentKey.fill(0);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
async addFactor (request) {
|
|
417
|
+
await ctx.authorize('addFactor', 'administer', request);
|
|
418
|
+
await rewrap(request, (factors)=>[
|
|
419
|
+
...factors,
|
|
420
|
+
request.factor
|
|
421
|
+
]);
|
|
422
|
+
},
|
|
423
|
+
async removeFactor (request) {
|
|
424
|
+
await ctx.authorize('removeFactor', 'administer', request);
|
|
425
|
+
await rewrap(request, (factors)=>{
|
|
426
|
+
if (!factors.some((f)=>f.kind === request.kind)) throw new VaultNotFound();
|
|
427
|
+
return factors.filter((f)=>f.kind !== request.kind);
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
async readEntry (request) {
|
|
431
|
+
await ctx.authorize('readEntry', 'read', request);
|
|
432
|
+
return ctx.read(async (store)=>{
|
|
433
|
+
const row = await store.getEntry(request);
|
|
434
|
+
if (row === undefined) throw new VaultNotFound();
|
|
435
|
+
await ctx.authorize('readEntry', 'read', {
|
|
436
|
+
...request,
|
|
437
|
+
keyID: row.keyID
|
|
438
|
+
}, store);
|
|
439
|
+
await records.entryInfo(row, store);
|
|
440
|
+
return open.decrypt(row, {
|
|
441
|
+
...row,
|
|
442
|
+
inputs: request.inputs
|
|
443
|
+
}, store);
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
async getEntry (request) {
|
|
447
|
+
await ctx.authorize('getEntry', 'read', request);
|
|
448
|
+
return params.store.getEntry(request);
|
|
449
|
+
},
|
|
450
|
+
async listEntries (request) {
|
|
451
|
+
await ctx.authorize('listEntries', 'read', request);
|
|
452
|
+
return params.store.listEntries(request);
|
|
453
|
+
},
|
|
454
|
+
...entries,
|
|
455
|
+
...rotate,
|
|
456
|
+
reendorse: records.reendorse
|
|
457
|
+
};
|
|
458
|
+
return coordinator;
|
|
459
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StoreDefinition, StoreProvider } from '@hozon/db';
|
|
2
|
+
import type { VaultStoreTables } from './tables.js';
|
|
3
|
+
import type { VaultStoreAPI } from './types.js';
|
|
4
|
+
export declare const VAULT_STORE: "vault";
|
|
5
|
+
export declare const vaultStoreDefinition: StoreDefinition<VaultStoreTables, VaultStoreAPI>;
|
|
6
|
+
export declare function getVaultStore(provider: StoreProvider): Promise<VaultStoreAPI>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createVaultStore } from './api.js';
|
|
2
|
+
import { getVaultMigrations } from './migrations.js';
|
|
3
|
+
export const VAULT_STORE = 'vault';
|
|
4
|
+
export const vaultStoreDefinition = {
|
|
5
|
+
name: VAULT_STORE,
|
|
6
|
+
dependsOn: [],
|
|
7
|
+
migrations: getVaultMigrations,
|
|
8
|
+
createAPI: createVaultStore
|
|
9
|
+
};
|
|
10
|
+
export async function getVaultStore(provider) {
|
|
11
|
+
return provider.getStore(VAULT_STORE);
|
|
12
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createVaultStore } from './api.js';
|
|
2
|
+
export { createVaultCoordinator } from './coordinator.js';
|
|
3
|
+
export { VaultKeyLocked, VaultNotAuthorized, VaultNotFound, VaultRepairPending, VaultSupersededBranch, } from './coordinator-context.js';
|
|
4
|
+
export type * from './coordinator-types.js';
|
|
5
|
+
export { getVaultStore, VAULT_STORE, vaultStoreDefinition } from './definition.js';
|
|
6
|
+
export { VaultModeConflict, VaultWriteConflict } from './record-context.js';
|
|
7
|
+
export { getVaultRecordsStore, VAULT_RECORDS_STORE, vaultRecordsStoreDefinition, } from './record-definition.js';
|
|
8
|
+
export type * from './record-tables.js';
|
|
9
|
+
export type { VaultRecords, VaultRecordsParams, VaultWritePreconditions } from './records.js';
|
|
10
|
+
export { createVaultRecords, VaultRecordRowError, vaultRecordPortsFor } from './records.js';
|
|
11
|
+
export type * from './tables.js';
|
|
12
|
+
export type * from './types.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createVaultStore } from './api.js';
|
|
2
|
+
export { createVaultCoordinator } from './coordinator.js';
|
|
3
|
+
export { VaultKeyLocked, VaultNotAuthorized, VaultNotFound, VaultRepairPending, VaultSupersededBranch } from './coordinator-context.js';
|
|
4
|
+
export { getVaultStore, VAULT_STORE, vaultStoreDefinition } from './definition.js';
|
|
5
|
+
export { VaultModeConflict, VaultWriteConflict } from './record-context.js';
|
|
6
|
+
export { getVaultRecordsStore, VAULT_RECORDS_STORE, vaultRecordsStoreDefinition } from './record-definition.js';
|
|
7
|
+
export { createVaultRecords, VaultRecordRowError, vaultRecordPortsFor } from './records.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { sql } from '@hozon/db';
|
|
2
|
+
export function getVaultMigrations(ctx) {
|
|
3
|
+
const t = ctx.types;
|
|
4
|
+
const p = ctx.tablePrefix;
|
|
5
|
+
const hlcColumn = (column)=>{
|
|
6
|
+
const required = column.notNull();
|
|
7
|
+
return ctx.kind === 'postgres' ? required.modifyFront(sql`collate "C"`) : required;
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
'0-init': {
|
|
11
|
+
async up (db) {
|
|
12
|
+
await db.schema.createTable('vault_keys').addColumn('owner_did', t.text, (c)=>c.notNull()).addColumn('key_id', t.text, (c)=>c.notNull()).addColumn('suite', 'integer', (c)=>c.notNull()).addColumn('version', 'integer', (c)=>c.notNull()).addColumn('state', t.text, (c)=>c.notNull()).addColumn('mode', t.text, (c)=>c.notNull().defaultTo('local')).addPrimaryKeyConstraint(`${p}_vault_keys_pkey`, [
|
|
13
|
+
'owner_did',
|
|
14
|
+
'key_id'
|
|
15
|
+
]).execute();
|
|
16
|
+
await db.schema.createTable('vault_wrappings').addColumn('owner_did', t.text, (c)=>c.notNull()).addColumn('wrapping_id', t.text, (c)=>c.notNull()).addColumn('key_id', t.text, (c)=>c.notNull()).addColumn('key_version', 'integer', (c)=>c.notNull()).addColumn('descriptors', t.json, (c)=>c.notNull()).addColumn('descriptor_digest', t.text, (c)=>c.notNull()).addColumn('iv', t.binary, (c)=>c.notNull()).addColumn('wrapped_key', t.binary, (c)=>c.notNull()).addColumn('recipient_did', t.text).addColumn('provenance', t.text, (c)=>c.notNull()).addColumn('mode', t.text, (c)=>c.notNull().defaultTo('local')).addPrimaryKeyConstraint(`${p}_vault_wrappings_pkey`, [
|
|
17
|
+
'owner_did',
|
|
18
|
+
'wrapping_id'
|
|
19
|
+
]).execute();
|
|
20
|
+
await db.schema.createIndex(`${p}_vault_wrappings_key_idx`).on('vault_wrappings').columns([
|
|
21
|
+
'owner_did',
|
|
22
|
+
'key_id',
|
|
23
|
+
'key_version'
|
|
24
|
+
]).execute();
|
|
25
|
+
await db.schema.createTable('vault_entries').addColumn('owner_did', t.text, (c)=>c.notNull()).addColumn('entry_id', t.text, (c)=>c.notNull()).addColumn('key_id', t.text, (c)=>c.notNull()).addColumn('key_version', 'integer', (c)=>c.notNull()).addColumn('kind', t.text, (c)=>c.notNull()).addColumn('label', t.text, (c)=>c.notNull()).addColumn('attributes', t.json, (c)=>c.notNull()).addColumn('iv', t.binary, (c)=>c.notNull()).addColumn('ciphertext', t.binary, (c)=>c.notNull()).addColumn('hlc', t.text, hlcColumn).addColumn('provenance', t.text, (c)=>c.notNull()).addColumn('mode', t.text, (c)=>c.notNull().defaultTo('local')).addPrimaryKeyConstraint(`${p}_vault_entries_pkey`, [
|
|
26
|
+
'owner_did',
|
|
27
|
+
'entry_id'
|
|
28
|
+
]).execute();
|
|
29
|
+
for (const column of [
|
|
30
|
+
'kind',
|
|
31
|
+
'label'
|
|
32
|
+
]){
|
|
33
|
+
await db.schema.createIndex(`${p}_vault_entries_${column}_idx`).on('vault_entries').columns([
|
|
34
|
+
'owner_did',
|
|
35
|
+
'key_id',
|
|
36
|
+
column
|
|
37
|
+
]).execute();
|
|
38
|
+
}
|
|
39
|
+
await db.schema.createIndex(`${p}_vault_entries_id_idx`).on('vault_entries').column('entry_id').execute();
|
|
40
|
+
await db.schema.createIndex(`${p}_vault_entries_hlc_idx`).on('vault_entries').column('hlc').execute();
|
|
41
|
+
if (ctx.kind === 'postgres') {
|
|
42
|
+
await db.schema.createIndex(`${p}_vault_entries_attributes_idx`).on('vault_entries').using('gin').column('attributes').execute();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async down (db) {
|
|
46
|
+
await db.schema.dropTable('vault_entries').execute();
|
|
47
|
+
await db.schema.dropTable('vault_wrappings').execute();
|
|
48
|
+
await db.schema.dropTable('vault_keys').execute();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Adapter } from '@hozon/adapter';
|
|
2
|
+
import type { Kysely, StoreContext } from '@hozon/db';
|
|
3
|
+
import type { DecisionKey } from './batch.js';
|
|
4
|
+
import type { VaultRecordsTables } from './record-tables.js';
|
|
5
|
+
import type { VaultStoreAPI } from './types.js';
|
|
6
|
+
export type VaultDatabase = {
|
|
7
|
+
db: Kysely<VaultRecordsTables>;
|
|
8
|
+
adapter: Adapter;
|
|
9
|
+
context: StoreContext;
|
|
10
|
+
};
|
|
11
|
+
export declare const vaultDatabases: WeakMap<VaultStoreAPI, VaultDatabase>;
|
|
12
|
+
export declare class VaultModeConflict extends Error {
|
|
13
|
+
constructor();
|
|
14
|
+
}
|
|
15
|
+
export declare class VaultWriteConflict extends Error {
|
|
16
|
+
constructor();
|
|
17
|
+
}
|
|
18
|
+
export declare function claimMode(db: Kysely<VaultRecordsTables>, key: {
|
|
19
|
+
type: string;
|
|
20
|
+
owner: string;
|
|
21
|
+
id: string;
|
|
22
|
+
}, mode: 'local' | 'replicated'): Promise<void>;
|
|
23
|
+
/** Claims every key for `mode`, in one insert and one read per statement chunk. */
|
|
24
|
+
export declare function claimModes(db: Kysely<VaultRecordsTables>, keys: ReadonlyArray<DecisionKey>, mode: 'local' | 'replicated'): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { recordKeyText } from '@kokuin/records';
|
|
2
|
+
import { DECISION_COLUMNS, insertDecisionClaims, selectDecisionModes, statementChunks } from './batch.js';
|
|
3
|
+
import { settleAll } from './settle.js';
|
|
4
|
+
export const vaultDatabases = new WeakMap();
|
|
5
|
+
export class VaultModeConflict extends Error {
|
|
6
|
+
constructor(){
|
|
7
|
+
super('The record belongs to another vault storage mode');
|
|
8
|
+
this.name = 'VaultModeConflict';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class VaultWriteConflict extends Error {
|
|
12
|
+
constructor(){
|
|
13
|
+
super('Vault write preconditions no longer hold');
|
|
14
|
+
this.name = 'VaultWriteConflict';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function claimMode(db, key, mode) {
|
|
18
|
+
await claimModes(db, [
|
|
19
|
+
key
|
|
20
|
+
], mode);
|
|
21
|
+
}
|
|
22
|
+
/** Claims every key for `mode`, in one insert and one read per statement chunk. */ export async function claimModes(db, keys, mode) {
|
|
23
|
+
const unique = [
|
|
24
|
+
...new Map(keys.map((key)=>[
|
|
25
|
+
recordKeyText(key),
|
|
26
|
+
key
|
|
27
|
+
])).values()
|
|
28
|
+
];
|
|
29
|
+
if (unique.length === 0) return;
|
|
30
|
+
const parts = statementChunks(unique, DECISION_COLUMNS);
|
|
31
|
+
await settleAll(parts.map((part)=>insertDecisionClaims(db, part, mode).execute()));
|
|
32
|
+
const stored = (await settleAll(parts.map((part)=>selectDecisionModes(db, part).execute()))).flat();
|
|
33
|
+
if (stored.length !== unique.length || stored.some((row)=>row.mode !== mode)) throw new VaultModeConflict();
|
|
34
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StoreDefinition, StoreProvider } from '@hozon/db';
|
|
2
|
+
import type { VaultDatabase } from './record-context.js';
|
|
3
|
+
import type { VaultRecordsTables } from './record-tables.js';
|
|
4
|
+
export declare const VAULT_RECORDS_STORE = "vault-records";
|
|
5
|
+
/** Register separately to keep local-only databases free of replicated state. */
|
|
6
|
+
export declare const vaultRecordsStoreDefinition: StoreDefinition<VaultRecordsTables, VaultDatabase>;
|
|
7
|
+
export declare function getVaultRecordsStore(provider: StoreProvider, opKind?: string): Promise<VaultDatabase>;
|