@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,436 @@
|
|
|
1
|
+
import { compareRecordDecisionHLC } from '@kokuin/records';
|
|
2
|
+
import { decodeVaultRecord, factorsFromDescriptors, getSuite, NoAvailableFactor, vaultRevocationRecordID } from '@kokuin/vault';
|
|
3
|
+
import { VaultKeyLocked, VaultNotFound, VaultRepairPending } from './coordinator-context.js';
|
|
4
|
+
import { settleAll } from './settle.js';
|
|
5
|
+
export function createRotateFunctions(ctx, open, records) {
|
|
6
|
+
const { params } = ctx;
|
|
7
|
+
function excluded(row, recipients) {
|
|
8
|
+
const denied = new Set(recipients.map((did)=>vaultRevocationRecordID(row.keyID, did)));
|
|
9
|
+
return [
|
|
10
|
+
row.recipientDID,
|
|
11
|
+
...row.descriptors.filter((d)=>d.kind === 'did').map((d)=>d.params.recipient)
|
|
12
|
+
].some((did)=>typeof did === 'string' && denied.has(vaultRevocationRecordID(row.keyID, did)));
|
|
13
|
+
}
|
|
14
|
+
async function currentEntries(key, store) {
|
|
15
|
+
const rows = await store.listEntries(key);
|
|
16
|
+
const infos = await records.entryInfos(rows, store);
|
|
17
|
+
return rows.map((row, index)=>{
|
|
18
|
+
const info = infos[index];
|
|
19
|
+
if (info === undefined) throw new VaultNotFound();
|
|
20
|
+
return {
|
|
21
|
+
entryID: row.entryID,
|
|
22
|
+
row,
|
|
23
|
+
...info,
|
|
24
|
+
opHash: '',
|
|
25
|
+
keyVersion: row.keyVersion,
|
|
26
|
+
branchID: row.provenance
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function candidate(key, record) {
|
|
31
|
+
const op = await ctx.verified(record);
|
|
32
|
+
const meta = op.claim.meta;
|
|
33
|
+
if (op.claim.owner !== key.ownerDID || meta.keyID !== key.keyID || typeof meta.contentHLC !== 'string' || typeof meta.keyVersion !== 'number') throw new VaultRepairPending();
|
|
34
|
+
let row;
|
|
35
|
+
if (!op.claim.deleted) {
|
|
36
|
+
const decoded = decodeVaultRecord(record, meta);
|
|
37
|
+
if (decoded.type !== 'vault.entry' || decoded.meta.kind === undefined || decoded.meta.label === undefined || decoded.meta.attributes === undefined || decoded.meta.branchID === undefined) throw new VaultRepairPending();
|
|
38
|
+
row = {
|
|
39
|
+
ownerDID: key.ownerDID,
|
|
40
|
+
keyID: key.keyID,
|
|
41
|
+
entryID: record.id,
|
|
42
|
+
keyVersion: meta.keyVersion,
|
|
43
|
+
hlc: record.hlc,
|
|
44
|
+
provenance: decoded.meta.branchID,
|
|
45
|
+
kind: decoded.meta.kind,
|
|
46
|
+
label: decoded.meta.label,
|
|
47
|
+
attributes: decoded.meta.attributes,
|
|
48
|
+
...decoded.payload
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
entryID: record.id,
|
|
53
|
+
row,
|
|
54
|
+
contentHLC: meta.contentHLC,
|
|
55
|
+
consumer: meta.consumer,
|
|
56
|
+
opHash: record.opHash,
|
|
57
|
+
keyVersion: meta.keyVersion,
|
|
58
|
+
branchID: typeof meta.branchID === 'string' ? meta.branchID : ''
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function candidates(key, store) {
|
|
62
|
+
const api = ctx.recordsFor(store);
|
|
63
|
+
const [current, prior] = await settleAll([
|
|
64
|
+
api.listEntryCandidates({
|
|
65
|
+
...key,
|
|
66
|
+
keyVersion: key.version
|
|
67
|
+
}),
|
|
68
|
+
api.listPriorEntryTombstones({
|
|
69
|
+
...key,
|
|
70
|
+
beforeVersion: key.version
|
|
71
|
+
})
|
|
72
|
+
]);
|
|
73
|
+
// Checks run together; the first failure in record order still wins.
|
|
74
|
+
const values = await settleAll([
|
|
75
|
+
...current,
|
|
76
|
+
...prior
|
|
77
|
+
].map((record)=>candidate(key, record)));
|
|
78
|
+
const grouped = new Map();
|
|
79
|
+
for (const value of values){
|
|
80
|
+
const list = grouped.get(value.entryID) ?? [];
|
|
81
|
+
list.push(value);
|
|
82
|
+
grouped.set(value.entryID, list);
|
|
83
|
+
}
|
|
84
|
+
return grouped;
|
|
85
|
+
}
|
|
86
|
+
const compare = (a, b)=>{
|
|
87
|
+
return compareRecordDecisionHLC(a.contentHLC, b.contentHLC) || a.keyVersion - b.keyVersion || (a.opHash < b.opHash ? -1 : a.opHash > b.opHash ? 1 : 0);
|
|
88
|
+
};
|
|
89
|
+
async function rotateLocked(request, store, repair) {
|
|
90
|
+
if (!Array.isArray(request.excludedRecipients) || !Array.isArray(request.reissue)) throw new TypeError('Explicit exclusions and entry reissue instructions are required');
|
|
91
|
+
const key = await ctx.key(request, store);
|
|
92
|
+
const branches = await ctx.branches(key, key.version, store);
|
|
93
|
+
if (repair && branches.length < 2) return null;
|
|
94
|
+
const wrappingsRead = async ()=>{
|
|
95
|
+
const existing = await store.listWrappings(key);
|
|
96
|
+
const current = existing.filter((w)=>w.keyVersion === key.version);
|
|
97
|
+
return {
|
|
98
|
+
existing,
|
|
99
|
+
current,
|
|
100
|
+
evidence: await ctx.wrappingEvidences(current, store)
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
// Ordered settling keeps the original precedence: branch, then wrapping, then entry failures.
|
|
104
|
+
const [winner, wrappingState, currentSelections] = await settleAll([
|
|
105
|
+
ctx.branch(key, key.version, store),
|
|
106
|
+
wrappingsRead(),
|
|
107
|
+
currentEntries(key, store)
|
|
108
|
+
]);
|
|
109
|
+
const expectedBranches = branches.map((b)=>b.branchID);
|
|
110
|
+
const existingWrappings = wrappingState.existing;
|
|
111
|
+
const evidence = new Map(wrappingState.evidence.map((item)=>[
|
|
112
|
+
item.record.id,
|
|
113
|
+
item
|
|
114
|
+
]));
|
|
115
|
+
let retained = wrappingState.current.filter((w)=>!excluded(w, request.excludedRecipients));
|
|
116
|
+
let selections = currentSelections;
|
|
117
|
+
const keys = new Map();
|
|
118
|
+
const pending = [];
|
|
119
|
+
const plaintexts = [];
|
|
120
|
+
let pinGate = Promise.resolve();
|
|
121
|
+
let generated;
|
|
122
|
+
try {
|
|
123
|
+
const openSource = (row)=>{
|
|
124
|
+
const run = async ()=>{
|
|
125
|
+
const opened = await open.open({
|
|
126
|
+
...request,
|
|
127
|
+
atVersion: row.keyVersion
|
|
128
|
+
}, store, params.mode === 'replicated' ? row.provenance : undefined);
|
|
129
|
+
return {
|
|
130
|
+
contentKey: opened.contentKey,
|
|
131
|
+
suite: opened.suite
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
if (request.inputs?.pin === undefined) return run();
|
|
135
|
+
// PIN opens stay one at a time: a rejected PIN stops later attempts, spending one try.
|
|
136
|
+
const gated = pinGate.then(run);
|
|
137
|
+
pinGate = gated;
|
|
138
|
+
return gated;
|
|
139
|
+
};
|
|
140
|
+
const decrypt = (selection)=>{
|
|
141
|
+
const task = (async ()=>{
|
|
142
|
+
if (selection.row === undefined) return null;
|
|
143
|
+
const row = selection.row;
|
|
144
|
+
const identity = JSON.stringify([
|
|
145
|
+
row.keyVersion,
|
|
146
|
+
row.provenance
|
|
147
|
+
]);
|
|
148
|
+
let source = keys.get(identity);
|
|
149
|
+
// Cache the promise, not the key: concurrent decrypts must share one open.
|
|
150
|
+
if (source === undefined) {
|
|
151
|
+
source = openSource(row);
|
|
152
|
+
keys.set(identity, source);
|
|
153
|
+
}
|
|
154
|
+
const { contentKey, suite } = await source;
|
|
155
|
+
const plaintext = getSuite(suite).decryptEntry({
|
|
156
|
+
...row,
|
|
157
|
+
contentKey
|
|
158
|
+
});
|
|
159
|
+
plaintexts.push(plaintext);
|
|
160
|
+
return plaintext;
|
|
161
|
+
})();
|
|
162
|
+
pending.push(task);
|
|
163
|
+
return task;
|
|
164
|
+
};
|
|
165
|
+
if (repair) {
|
|
166
|
+
const grouped = await candidates(key, store);
|
|
167
|
+
const selected = [];
|
|
168
|
+
let addsEntry = false;
|
|
169
|
+
const comparisons = [];
|
|
170
|
+
for (const list of grouped.values()){
|
|
171
|
+
list.sort(compare);
|
|
172
|
+
const chosen = list.at(-1);
|
|
173
|
+
if (chosen === undefined) continue;
|
|
174
|
+
selected.push(chosen);
|
|
175
|
+
if (chosen.branchID === winner.branchID) continue;
|
|
176
|
+
const winning = list.filter((s)=>s.branchID === winner.branchID).sort(compare).at(-1);
|
|
177
|
+
if (winning === undefined || chosen.contentHLC !== winning.contentHLC) {
|
|
178
|
+
addsEntry = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
comparisons.push([
|
|
182
|
+
chosen,
|
|
183
|
+
winning
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
// Plaintext comparisons are independent. Decrypts run together; the first failure in
|
|
187
|
+
// group order wins.
|
|
188
|
+
const compared = await settleAll(comparisons.map(([chosen, winning])=>settleAll([
|
|
189
|
+
decrypt(chosen),
|
|
190
|
+
decrypt(winning)
|
|
191
|
+
])));
|
|
192
|
+
for (const [a, b] of compared)if (a === null !== (b === null) || a !== null && b !== null && (a.length !== b.length || !a.every((v, i)=>v === b[i]))) addsEntry = true;
|
|
193
|
+
const signature = (w)=>{
|
|
194
|
+
return JSON.stringify({
|
|
195
|
+
recipient: w.recipientDID,
|
|
196
|
+
factors: factorsFromDescriptors(w.descriptors, request.inputs)
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
const winnerRecipients = new Set(retained.filter((w)=>w.provenance === winner.branchID).map(signature));
|
|
200
|
+
const addsRecipient = retained.some((w)=>!winnerRecipients.has(signature(w)));
|
|
201
|
+
let unreadableProjection = false;
|
|
202
|
+
const versionBranches = new Map();
|
|
203
|
+
for (const selection of selections){
|
|
204
|
+
if (selection.row === undefined) continue;
|
|
205
|
+
// Selections share few key versions. Read each version's branch once.
|
|
206
|
+
let branch = versionBranches.get(selection.keyVersion);
|
|
207
|
+
if (branch === undefined) {
|
|
208
|
+
branch = ctx.branch(key, selection.keyVersion, store);
|
|
209
|
+
versionBranches.set(selection.keyVersion, branch);
|
|
210
|
+
}
|
|
211
|
+
if (selection.branchID !== (await branch).branchID) {
|
|
212
|
+
unreadableProjection = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!addsEntry && !addsRecipient && !unreadableProjection && request.reissue.length === 0 && request.excludedRecipients.length === 0) return null;
|
|
217
|
+
const unique = new Map();
|
|
218
|
+
for (const wrapping of [
|
|
219
|
+
...retained.filter((w)=>w.provenance === winner.branchID),
|
|
220
|
+
...retained
|
|
221
|
+
]){
|
|
222
|
+
const id = signature(wrapping);
|
|
223
|
+
if (!unique.has(id)) unique.set(id, wrapping);
|
|
224
|
+
}
|
|
225
|
+
retained = [
|
|
226
|
+
...unique.values()
|
|
227
|
+
];
|
|
228
|
+
selections = selected;
|
|
229
|
+
}
|
|
230
|
+
const instructions = new Map(request.reissue.map((instruction)=>[
|
|
231
|
+
instruction.entryID,
|
|
232
|
+
instruction
|
|
233
|
+
]));
|
|
234
|
+
if (instructions.size !== request.reissue.length || [
|
|
235
|
+
...instructions.keys()
|
|
236
|
+
].some((id)=>!selections.some((s)=>s.entryID === id))) throw new TypeError('Reissue instructions must name distinct existing entries');
|
|
237
|
+
const suite = getSuite(request.suite ?? key.suite);
|
|
238
|
+
const next = {
|
|
239
|
+
...key,
|
|
240
|
+
version: key.version + 1,
|
|
241
|
+
suite: suite.id
|
|
242
|
+
};
|
|
243
|
+
const contentKey = suite.generateContentKey(params.runtime);
|
|
244
|
+
generated = contentKey;
|
|
245
|
+
// Key records and wraps are independent. Wrapping IDs are drawn in retained order.
|
|
246
|
+
const [chain, wrapped] = await settleAll([
|
|
247
|
+
params.mode === 'replicated' ? ctx.keyRecords(next, store) : Promise.resolve({
|
|
248
|
+
records: [],
|
|
249
|
+
branchID: '',
|
|
250
|
+
branchRecordID: ''
|
|
251
|
+
}),
|
|
252
|
+
settleAll(retained.map(async (previous)=>{
|
|
253
|
+
const wrappingID = params.runtime.getRandomID();
|
|
254
|
+
const result = await suite.wrap({
|
|
255
|
+
runtime: params.runtime,
|
|
256
|
+
contentKey,
|
|
257
|
+
factors: factorsFromDescriptors(previous.descriptors, request.inputs),
|
|
258
|
+
wrappingID,
|
|
259
|
+
keyID: key.keyID,
|
|
260
|
+
keyVersion: next.version,
|
|
261
|
+
methods: params.methods
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
previous,
|
|
265
|
+
wrappingID,
|
|
266
|
+
result
|
|
267
|
+
};
|
|
268
|
+
}))
|
|
269
|
+
]);
|
|
270
|
+
// Signing stays sequential, so a failed signature stops the rest and a prompting signer is
|
|
271
|
+
// asked nothing after it. HLC allocation and output follow the order: wrappings, entries,
|
|
272
|
+
// then revocations.
|
|
273
|
+
const output = [
|
|
274
|
+
...chain.records
|
|
275
|
+
];
|
|
276
|
+
const sign = async (task)=>{
|
|
277
|
+
output.push(await task);
|
|
278
|
+
};
|
|
279
|
+
const wrappings = [];
|
|
280
|
+
for (const { previous, wrappingID, result } of wrapped){
|
|
281
|
+
const row = {
|
|
282
|
+
...next,
|
|
283
|
+
wrappingID,
|
|
284
|
+
keyVersion: next.version,
|
|
285
|
+
recipientDID: previous.recipientDID,
|
|
286
|
+
provenance: chain.branchID,
|
|
287
|
+
...result
|
|
288
|
+
};
|
|
289
|
+
wrappings.push(row);
|
|
290
|
+
if (params.mode === 'replicated') {
|
|
291
|
+
const prior = evidence.get(previous.wrappingID);
|
|
292
|
+
if (prior === undefined) throw new VaultNotFound();
|
|
293
|
+
await sign(records.wrapping(row, chain, prior.op.claim.meta.consumer, await params.nextHLC()));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const entries = [];
|
|
297
|
+
const deletions = [];
|
|
298
|
+
const reissued = selections.map((selected)=>{
|
|
299
|
+
const instruction = instructions.get(selected.entryID);
|
|
300
|
+
const deleting = instruction?.action === 'delete' || selected.row === undefined && instruction?.action !== 'replace';
|
|
301
|
+
const decrypting = instruction?.action === 'skip' || deleting || instruction?.action === 'replace' ? undefined : decrypt(selected);
|
|
302
|
+
// Awaited in selection order below. An earlier failure must not surface as unhandled.
|
|
303
|
+
decrypting?.catch(()=>{});
|
|
304
|
+
return {
|
|
305
|
+
selected,
|
|
306
|
+
instruction,
|
|
307
|
+
deleting,
|
|
308
|
+
decrypting
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
for (const { selected, instruction, deleting, decrypting } of reissued){
|
|
312
|
+
if (instruction?.action === 'skip') continue;
|
|
313
|
+
const contentHLC = instruction?.contentHLC ?? selected.contentHLC;
|
|
314
|
+
const consumer = instruction !== undefined && Object.hasOwn(instruction, 'consumer') ? instruction.consumer : selected.consumer;
|
|
315
|
+
const hlc = await params.nextHLC();
|
|
316
|
+
if (deleting) {
|
|
317
|
+
deletions.push(selected.entryID);
|
|
318
|
+
if (params.mode === 'replicated') await sign(ctx.signed('vault.entry', {
|
|
319
|
+
ownerDID: key.ownerDID,
|
|
320
|
+
id: selected.entryID
|
|
321
|
+
}, null, {
|
|
322
|
+
keyID: key.keyID,
|
|
323
|
+
keyVersion: next.version,
|
|
324
|
+
branchID: chain.branchID,
|
|
325
|
+
branchRecordID: chain.branchRecordID,
|
|
326
|
+
contentHLC,
|
|
327
|
+
consumer
|
|
328
|
+
}, hlc));
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const plaintext = instruction?.action === 'replace' ? instruction.plaintext : await decrypting;
|
|
332
|
+
if (plaintext === null || selected.row === undefined) throw new VaultRepairPending();
|
|
333
|
+
const encrypted = suite.encryptEntry({
|
|
334
|
+
runtime: params.runtime,
|
|
335
|
+
contentKey,
|
|
336
|
+
plaintext,
|
|
337
|
+
keyID: key.keyID,
|
|
338
|
+
keyVersion: next.version,
|
|
339
|
+
entryID: selected.entryID
|
|
340
|
+
});
|
|
341
|
+
const row = {
|
|
342
|
+
...selected.row,
|
|
343
|
+
keyVersion: next.version,
|
|
344
|
+
...encrypted,
|
|
345
|
+
hlc,
|
|
346
|
+
provenance: chain.branchID
|
|
347
|
+
};
|
|
348
|
+
entries.push(row);
|
|
349
|
+
if (params.mode === 'replicated') await sign(records.entry(row, chain, contentHLC, consumer));
|
|
350
|
+
}
|
|
351
|
+
if (params.mode === 'replicated') {
|
|
352
|
+
for (const did of new Set(request.excludedRecipients)){
|
|
353
|
+
const id = vaultRevocationRecordID(key.keyID, did);
|
|
354
|
+
const revokedDID = id.slice(id.indexOf('/') + 1);
|
|
355
|
+
await sign(ctx.signed('vault.revocation', {
|
|
356
|
+
ownerDID: key.ownerDID,
|
|
357
|
+
id
|
|
358
|
+
}, {}, {
|
|
359
|
+
keyID: key.keyID,
|
|
360
|
+
revokedDID,
|
|
361
|
+
consumer: null
|
|
362
|
+
}, await params.nextHLC()));
|
|
363
|
+
}
|
|
364
|
+
await ctx.write(store, output, {
|
|
365
|
+
key: {
|
|
366
|
+
...key,
|
|
367
|
+
expectedVersion: key.version,
|
|
368
|
+
expectedBranches
|
|
369
|
+
}
|
|
370
|
+
}, request);
|
|
371
|
+
} else {
|
|
372
|
+
await store.putKey(next);
|
|
373
|
+
// Each set names distinct rows, so the batched writes run together. The first failure in
|
|
374
|
+
// the old order wins.
|
|
375
|
+
await settleAll([
|
|
376
|
+
store.putWrappings(wrappings),
|
|
377
|
+
store.putEntries(entries),
|
|
378
|
+
store.deleteEntries(deletions.map((entryID)=>({
|
|
379
|
+
ownerDID: key.ownerDID,
|
|
380
|
+
entryID
|
|
381
|
+
}))),
|
|
382
|
+
store.deleteWrappings(existingWrappings.filter((w)=>excluded(w, request.excludedRecipients)))
|
|
383
|
+
]);
|
|
384
|
+
}
|
|
385
|
+
return next.version;
|
|
386
|
+
} finally{
|
|
387
|
+
// Let every started decrypt settle first, so no key or plaintext arrives after zeroing.
|
|
388
|
+
await Promise.allSettled(pending);
|
|
389
|
+
for (const source of (await Promise.allSettled(keys.values())))if (source.status === 'fulfilled') source.value.contentKey.fill(0);
|
|
390
|
+
for (const plaintext of plaintexts)plaintext.fill(0);
|
|
391
|
+
generated?.fill(0);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function rotate(request) {
|
|
395
|
+
await ctx.authorize('rotate', 'administer', request);
|
|
396
|
+
const version = await ctx.locked([
|
|
397
|
+
request
|
|
398
|
+
], (tx)=>rotateLocked(request, tx, false));
|
|
399
|
+
if (version === null) throw new VaultNotFound();
|
|
400
|
+
ctx.evict(request);
|
|
401
|
+
return version;
|
|
402
|
+
}
|
|
403
|
+
async function repairFork(request) {
|
|
404
|
+
await ctx.authorize('repairFork', 'administer', request);
|
|
405
|
+
if (params.mode === 'local') return {
|
|
406
|
+
status: 'not-needed'
|
|
407
|
+
};
|
|
408
|
+
try {
|
|
409
|
+
const version = await ctx.locked([
|
|
410
|
+
request
|
|
411
|
+
], async (tx)=>{
|
|
412
|
+
// A projection naming the wrong record refuses every branch read as pending. Rebuild it
|
|
413
|
+
// from the signed records first, so repair sees the key's real branches.
|
|
414
|
+
await ctx.recordsFor(tx).rebuildKeyBranches(request);
|
|
415
|
+
return rotateLocked(request, tx, true);
|
|
416
|
+
});
|
|
417
|
+
if (version === null) return {
|
|
418
|
+
status: 'not-needed'
|
|
419
|
+
};
|
|
420
|
+
ctx.evict(request);
|
|
421
|
+
return {
|
|
422
|
+
status: 'rotated',
|
|
423
|
+
version
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (error instanceof VaultRepairPending || error instanceof VaultKeyLocked || error instanceof NoAvailableFactor) return {
|
|
427
|
+
status: 'pending'
|
|
428
|
+
};
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
rotate,
|
|
434
|
+
repairFork
|
|
435
|
+
};
|
|
436
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { SignedRecord } from '@kokuin/records';
|
|
2
|
+
import type { SigningIdentity } from '@kokuin/token';
|
|
3
|
+
import type { FactorInput, FactorInputs, KeyCacheParams, PinThrottleParams, CredentialAuthorityRequest as RecordAuthorityRequest, SecretFactorKind, UnwrapParams, WrapParams } from '@kokuin/vault';
|
|
4
|
+
import type { VaultRecords, VaultRecordsParams } from './records.js';
|
|
5
|
+
import type { EntryAddress, EntryFilter, EntryRow, KeyAddress, KeyRow, UpdateEntryResult, VaultStoreAPI } from './types.js';
|
|
6
|
+
/** Unsigned operation requests never fabricate record signatures, including replicated reads. */
|
|
7
|
+
export type CredentialAuthorityRequest = RecordAuthorityRequest | {
|
|
8
|
+
source: 'local';
|
|
9
|
+
mode: 'local' | 'replicated';
|
|
10
|
+
operation: string;
|
|
11
|
+
owner: string;
|
|
12
|
+
issuer: string;
|
|
13
|
+
verb: 'read' | 'administer';
|
|
14
|
+
keyID?: string;
|
|
15
|
+
entryID?: string;
|
|
16
|
+
transportContext: unknown;
|
|
17
|
+
request: unknown;
|
|
18
|
+
store: Pick<VaultStoreAPI, 'getKey' | 'getEntry' | 'listKeys' | 'listWrappings' | 'listEntries'>;
|
|
19
|
+
};
|
|
20
|
+
export type CredentialAuthority = {
|
|
21
|
+
authorize(request: CredentialAuthorityRequest): Promise<boolean>;
|
|
22
|
+
};
|
|
23
|
+
export type CoordinatorCall = {
|
|
24
|
+
transportContext?: unknown;
|
|
25
|
+
};
|
|
26
|
+
export type OpenKeyParams = KeyAddress & CoordinatorCall & {
|
|
27
|
+
inputs?: FactorInputs;
|
|
28
|
+
atVersion?: number;
|
|
29
|
+
};
|
|
30
|
+
export type OpenedKey = {
|
|
31
|
+
contentKey: Uint8Array;
|
|
32
|
+
version: number;
|
|
33
|
+
branchID: string;
|
|
34
|
+
suite: number;
|
|
35
|
+
};
|
|
36
|
+
export type CreateKeyParams = CoordinatorCall & {
|
|
37
|
+
ownerDID: string;
|
|
38
|
+
wrappings?: Array<Array<FactorInput>>;
|
|
39
|
+
suite?: number;
|
|
40
|
+
consumer?: unknown;
|
|
41
|
+
};
|
|
42
|
+
export type EntryWriteParams = KeyAddress & EntryAddress & CoordinatorCall & Pick<EntryRow, 'kind' | 'label' | 'attributes'> & {
|
|
43
|
+
plaintext: Uint8Array;
|
|
44
|
+
inputs?: FactorInputs;
|
|
45
|
+
consumer?: unknown;
|
|
46
|
+
};
|
|
47
|
+
export type EntryUpdateParams = EntryAddress & CoordinatorCall & {
|
|
48
|
+
plaintext: Uint8Array;
|
|
49
|
+
inputs?: FactorInputs;
|
|
50
|
+
};
|
|
51
|
+
export type EntryReissue = {
|
|
52
|
+
entryID: string;
|
|
53
|
+
contentHLC?: string;
|
|
54
|
+
consumer?: unknown;
|
|
55
|
+
} & ({
|
|
56
|
+
action: 'preserve' | 'skip' | 'delete';
|
|
57
|
+
} | {
|
|
58
|
+
action: 'replace';
|
|
59
|
+
plaintext: Uint8Array;
|
|
60
|
+
});
|
|
61
|
+
export type RotateKeyParams = OpenKeyParams & {
|
|
62
|
+
excludedRecipients: ReadonlyArray<string>;
|
|
63
|
+
reissue: ReadonlyArray<EntryReissue>;
|
|
64
|
+
suite?: number;
|
|
65
|
+
};
|
|
66
|
+
export type ForkRepairResult = {
|
|
67
|
+
status: 'rotated';
|
|
68
|
+
version: number;
|
|
69
|
+
} | {
|
|
70
|
+
status: 'not-needed' | 'pending';
|
|
71
|
+
};
|
|
72
|
+
export type KeyStatus = {
|
|
73
|
+
state: 'unlocked' | 'available' | 'unopenable';
|
|
74
|
+
} | {
|
|
75
|
+
state: 'locked';
|
|
76
|
+
unlockOptions: Array<Array<SecretFactorKind>>;
|
|
77
|
+
};
|
|
78
|
+
type CommonParams = {
|
|
79
|
+
store: VaultStoreAPI;
|
|
80
|
+
principal: string;
|
|
81
|
+
identity: UnwrapParams['identity'];
|
|
82
|
+
runtime: WrapParams['runtime'];
|
|
83
|
+
authority: CredentialAuthority;
|
|
84
|
+
nextHLC(): Promise<string>;
|
|
85
|
+
methods?: WrapParams['methods'];
|
|
86
|
+
cache?: KeyCacheParams;
|
|
87
|
+
pinThrottle?: PinThrottleParams;
|
|
88
|
+
};
|
|
89
|
+
export type VaultCoordinatorParams = CommonParams & ({
|
|
90
|
+
mode: 'local';
|
|
91
|
+
} | {
|
|
92
|
+
mode: 'replicated';
|
|
93
|
+
signer: SigningIdentity;
|
|
94
|
+
records: Omit<VaultRecordsParams, 'store' | 'authority'>;
|
|
95
|
+
});
|
|
96
|
+
export type VaultCoordinator = {
|
|
97
|
+
/** Replicated mode only. Receipt remains subject to the records authority callback. */
|
|
98
|
+
get records(): VaultRecords;
|
|
99
|
+
createKey(params: CreateKeyParams): Promise<KeyAddress>;
|
|
100
|
+
openKey(params: OpenKeyParams): Promise<OpenedKey>;
|
|
101
|
+
unlock(params: OpenKeyParams): Promise<void>;
|
|
102
|
+
lock(address: KeyAddress & CoordinatorCall): Promise<void>;
|
|
103
|
+
lockAll(ownerDID: string, call?: CoordinatorCall): Promise<void>;
|
|
104
|
+
status(params: OpenKeyParams): Promise<KeyStatus>;
|
|
105
|
+
listKeys(ownerDID: string, call?: CoordinatorCall): Promise<Array<KeyRow>>;
|
|
106
|
+
setKeyState(params: KeyAddress & CoordinatorCall & Pick<KeyRow, 'state'>): Promise<void>;
|
|
107
|
+
deleteKey(params: KeyAddress & CoordinatorCall): Promise<void>;
|
|
108
|
+
grant(params: OpenKeyParams & {
|
|
109
|
+
recipientDID: string;
|
|
110
|
+
}): Promise<void>;
|
|
111
|
+
addFactor(params: OpenKeyParams & {
|
|
112
|
+
factor: FactorInput;
|
|
113
|
+
}): Promise<void>;
|
|
114
|
+
removeFactor(params: OpenKeyParams & {
|
|
115
|
+
kind: string;
|
|
116
|
+
}): Promise<void>;
|
|
117
|
+
rotate(params: RotateKeyParams): Promise<number>;
|
|
118
|
+
repairFork(params: RotateKeyParams): Promise<ForkRepairResult>;
|
|
119
|
+
putEntry(params: EntryWriteParams): Promise<EntryAddress>;
|
|
120
|
+
updateEntry(params: EntryUpdateParams): Promise<void>;
|
|
121
|
+
updateEntryIfPresent(params: EntryUpdateParams): Promise<'updated' | 'absent'>;
|
|
122
|
+
updateEntryIfUnchanged(params: EntryUpdateParams & {
|
|
123
|
+
expectedHLC: string;
|
|
124
|
+
}): Promise<UpdateEntryResult>;
|
|
125
|
+
readEntry(params: EntryAddress & CoordinatorCall & {
|
|
126
|
+
inputs?: FactorInputs;
|
|
127
|
+
}): Promise<Uint8Array>;
|
|
128
|
+
getEntry(params: EntryAddress & CoordinatorCall): Promise<EntryRow | undefined>;
|
|
129
|
+
listEntries(params: EntryFilter & CoordinatorCall): Promise<Array<EntryRow>>;
|
|
130
|
+
deleteEntry(params: EntryAddress & CoordinatorCall): Promise<void>;
|
|
131
|
+
reKeyEntry(params: EntryAddress & CoordinatorCall & {
|
|
132
|
+
toKeyID: string;
|
|
133
|
+
inputs?: FactorInputs;
|
|
134
|
+
}): Promise<void>;
|
|
135
|
+
reendorse(params: KeyAddress & CoordinatorCall & {
|
|
136
|
+
records: ReadonlyArray<SignedRecord>;
|
|
137
|
+
}): Promise<void>;
|
|
138
|
+
};
|
|
139
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|