@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.
Files changed (48) hide show
  1. package/README.md +126 -2
  2. package/lib/api.d.ts +5 -0
  3. package/lib/api.js +504 -0
  4. package/lib/batch.d.ts +89 -0
  5. package/lib/batch.js +159 -0
  6. package/lib/coordinator-context.d.ts +71 -0
  7. package/lib/coordinator-context.js +410 -0
  8. package/lib/coordinator-entries.d.ts +20 -0
  9. package/lib/coordinator-entries.js +258 -0
  10. package/lib/coordinator-open.d.ts +9 -0
  11. package/lib/coordinator-open.js +141 -0
  12. package/lib/coordinator-records.d.ts +27 -0
  13. package/lib/coordinator-records.js +132 -0
  14. package/lib/coordinator-rotate.d.ts +9 -0
  15. package/lib/coordinator-rotate.js +436 -0
  16. package/lib/coordinator-types.d.ts +139 -0
  17. package/lib/coordinator-types.js +1 -0
  18. package/lib/coordinator.d.ts +2 -0
  19. package/lib/coordinator.js +459 -0
  20. package/lib/definition.d.ts +6 -0
  21. package/lib/definition.js +12 -0
  22. package/lib/index.d.ts +12 -0
  23. package/lib/index.js +7 -0
  24. package/lib/migrations.d.ts +2 -0
  25. package/lib/migrations.js +52 -0
  26. package/lib/record-context.d.ts +24 -0
  27. package/lib/record-context.js +34 -0
  28. package/lib/record-definition.d.ts +7 -0
  29. package/lib/record-definition.js +93 -0
  30. package/lib/record-tables.d.ts +47 -0
  31. package/lib/record-tables.js +1 -0
  32. package/lib/records-adapter.d.ts +3 -0
  33. package/lib/records-adapter.js +197 -0
  34. package/lib/records-ports.d.ts +4 -0
  35. package/lib/records-ports.js +72 -0
  36. package/lib/records-projection.d.ts +3 -0
  37. package/lib/records-projection.js +105 -0
  38. package/lib/records-rows.d.ts +54 -0
  39. package/lib/records-rows.js +155 -0
  40. package/lib/records.d.ts +59 -0
  41. package/lib/records.js +233 -0
  42. package/lib/settle.d.ts +12 -0
  43. package/lib/settle.js +18 -0
  44. package/lib/tables.d.ts +42 -0
  45. package/lib/tables.js +1 -0
  46. package/lib/types.d.ts +74 -0
  47. package/lib/types.js +1 -0
  48. 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,2 @@
1
+ import { type Migration, type MigrationContext } from '@hozon/db';
2
+ export declare function getVaultMigrations(ctx: MigrationContext): Record<string, Migration>;
@@ -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>;