@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,93 @@
1
+ import { withStoreTransaction } from '@hozon/db';
2
+ export const VAULT_RECORDS_STORE = 'vault-records';
3
+ function migrations(ctx) {
4
+ const t = ctx.types;
5
+ const p = ctx.tablePrefix;
6
+ return {
7
+ '0-init': {
8
+ async up (db) {
9
+ await db.schema.createTable('vault_record_domain').addColumn('singleton', 'integer', (c)=>c.primaryKey()).addColumn('op_kind', t.text, (c)=>c.notNull()).execute();
10
+ await db.schema.createTable('vault_decisions').addColumn('type', t.text, (c)=>c.notNull()).addColumn('owner', t.text, (c)=>c.notNull()).addColumn('id', t.text, (c)=>c.notNull()).addColumn('mode', t.text, (c)=>c.notNull()).addColumn('content_digest', t.text).addColumn('decision_hlc', t.text).addPrimaryKeyConstraint(`${p}_vault_decisions_pkey`, [
11
+ 'type',
12
+ 'owner',
13
+ 'id'
14
+ ]).execute();
15
+ for (const table of [
16
+ 'vault_records',
17
+ 'vault_entry_candidates'
18
+ ]){
19
+ let builder = db.schema.createTable(table).addColumn('type', t.text, (c)=>c.notNull()).addColumn('owner', t.text, (c)=>c.notNull()).addColumn('id', t.text, (c)=>c.notNull()).addColumn('hlc', t.text, (c)=>c.notNull()).addColumn('op_jwt', t.text, (c)=>c.notNull()).addColumn('op_hash', t.text, (c)=>c.notNull()).addColumn('payload', t.binary).addColumn('payload_digest', t.text).addColumn('deleted', 'integer', (c)=>c.notNull()).addColumn('meta', t.json, (c)=>c.notNull());
20
+ if (table === 'vault_entry_candidates') {
21
+ builder = builder.addColumn('key_id', t.text, (c)=>c.notNull()).addColumn('key_version', 'integer', (c)=>c.notNull()).addColumn('branch_id', t.text, (c)=>c.notNull());
22
+ }
23
+ await builder.addPrimaryKeyConstraint(`${p}_${table}_pkey`, [
24
+ 'type',
25
+ 'owner',
26
+ 'id',
27
+ 'op_hash'
28
+ ]).execute();
29
+ }
30
+ await db.schema.createIndex(`${p}_vault_candidates_key_idx`).on('vault_entry_candidates').columns([
31
+ 'owner',
32
+ 'key_id',
33
+ 'key_version'
34
+ ]).execute();
35
+ await db.schema.createTable('vault_key_branches').addColumn('owner_did', t.text, (c)=>c.notNull()).addColumn('key_id', t.text, (c)=>c.notNull()).addColumn('key_version', 'integer', (c)=>c.notNull()).addColumn('record_id', t.text, (c)=>c.notNull()).addColumn('branch_id', t.text, (c)=>c.notNull()).addColumn('op_jwt', t.text, (c)=>c.notNull()).addPrimaryKeyConstraint(`${p}_vault_branches_pkey`, [
36
+ 'owner_did',
37
+ 'record_id'
38
+ ]).execute();
39
+ await db.schema.createIndex(`${p}_vault_branches_key_idx`).on('vault_key_branches').columns([
40
+ 'owner_did',
41
+ 'key_id',
42
+ 'key_version'
43
+ ]).execute();
44
+ await db.schema.createTable('vault_revocations').addColumn('owner_did', t.text, (c)=>c.notNull()).addColumn('key_id', t.text, (c)=>c.notNull()).addColumn('recipient_did', t.text, (c)=>c.notNull()).addPrimaryKeyConstraint(`${p}_vault_revocations_pkey`, [
45
+ 'owner_did',
46
+ 'key_id',
47
+ 'recipient_did'
48
+ ]).execute();
49
+ },
50
+ async down (db) {
51
+ for (const table of [
52
+ 'vault_revocations',
53
+ 'vault_key_branches',
54
+ 'vault_entry_candidates',
55
+ 'vault_records',
56
+ 'vault_decisions',
57
+ 'vault_record_domain'
58
+ ])await db.schema.dropTable(table).execute();
59
+ }
60
+ }
61
+ };
62
+ }
63
+ /** Register separately to keep local-only databases free of replicated state. */ export const vaultRecordsStoreDefinition = {
64
+ name: VAULT_RECORDS_STORE,
65
+ dependsOn: [
66
+ 'vault'
67
+ ],
68
+ migrations,
69
+ createAPI: (db, adapter, context)=>({
70
+ db,
71
+ adapter,
72
+ context
73
+ })
74
+ };
75
+ export async function getVaultRecordsStore(provider, opKind) {
76
+ if (opKind !== undefined) {
77
+ if (typeof opKind !== 'string' || opKind.length === 0) throw new TypeError('An explicit operation kind is required');
78
+ if (provider.isTransactionBound) throw new Error('Initialise the vault operation kind outside caller transactions');
79
+ }
80
+ const storage = await provider.getStore(VAULT_RECORDS_STORE);
81
+ if (opKind !== undefined) {
82
+ // Bind without vault key locks, before any caller transaction can apply records.
83
+ await withStoreTransaction(storage.db, async (db)=>{
84
+ await db.insertInto('vault_record_domain').values({
85
+ singleton: 1,
86
+ op_kind: opKind
87
+ }).onConflict((oc)=>oc.column('singleton').doNothing()).execute();
88
+ const row = await db.selectFrom('vault_record_domain').selectAll().executeTakeFirstOrThrow();
89
+ if (row.op_kind !== opKind) throw new TypeError('Vault records store belongs to another operation kind');
90
+ });
91
+ }
92
+ return storage;
93
+ }
@@ -0,0 +1,47 @@
1
+ import type { VaultStoreTables } from './tables.js';
2
+ export type VaultRecordTable = {
3
+ type: string;
4
+ owner: string;
5
+ id: string;
6
+ hlc: string;
7
+ op_jwt: string;
8
+ op_hash: string;
9
+ payload: Uint8Array | null;
10
+ payload_digest: string | null;
11
+ deleted: number;
12
+ meta: Record<string, unknown>;
13
+ };
14
+ export type VaultKeyBranchTable = {
15
+ owner_did: string;
16
+ key_id: string;
17
+ key_version: number;
18
+ record_id: string;
19
+ branch_id: string;
20
+ op_jwt: string;
21
+ };
22
+ export type VaultRecordsTables = VaultStoreTables & {
23
+ vault_records: VaultRecordTable;
24
+ vault_decisions: {
25
+ type: string;
26
+ owner: string;
27
+ id: string;
28
+ mode: 'local' | 'replicated';
29
+ content_digest: string | null;
30
+ decision_hlc: string | null;
31
+ };
32
+ vault_record_domain: {
33
+ singleton: number;
34
+ op_kind: string;
35
+ };
36
+ vault_entry_candidates: VaultRecordTable & {
37
+ key_id: string;
38
+ key_version: number;
39
+ branch_id: string;
40
+ };
41
+ vault_key_branches: VaultKeyBranchTable;
42
+ vault_revocations: {
43
+ owner_did: string;
44
+ key_id: string;
45
+ recipient_did: string;
46
+ };
47
+ };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,3 @@
1
+ import type { RecordStoreAdapter } from '@kokuin/records';
2
+ import type { Bound } from './records-rows.js';
3
+ export declare function makeAdapter(bound: Bound): RecordStoreAdapter;
@@ -0,0 +1,197 @@
1
+ import { chunk, withKeyLocks, withStoreTransaction } from '@hozon/db';
2
+ import { recordClaimDigest, recordKeyText, recordPayloadMatches, recordSignerKey, verifySignedRecord } from '@kokuin/records';
3
+ import { vaultRecordMeta } from '@kokuin/vault';
4
+ import { upsertReplicatedDecision } from './batch.js';
5
+ import { getVaultStore } from './definition.js';
6
+ import { VaultModeConflict } from './record-context.js';
7
+ import { getVaultRecordsStore } from './record-definition.js';
8
+ import { projection } from './records-projection.js';
9
+ import { assertDomain, boundAdapters, checked, decision, deleteCopies, GET_MANY_CHUNK, groupRows, supported, VaultRecordRowError, values, whereKey } from './records-rows.js';
10
+ import { settleAll } from './settle.js';
11
+ export function makeAdapter(bound) {
12
+ const { db } = bound;
13
+ const adapter = {
14
+ isTransaction: db.isTransaction,
15
+ async transaction (fn) {
16
+ return bound.context.provider.withTransaction(async (provider)=>{
17
+ const storage = await getVaultRecordsStore(provider);
18
+ const innerBound = {
19
+ ...bound,
20
+ ...storage,
21
+ store: await getVaultStore(provider),
22
+ locks: []
23
+ };
24
+ const inner = makeAdapter(innerBound);
25
+ return withStoreTransaction(storage.db, async ()=>{
26
+ await assertDomain(innerBound);
27
+ try {
28
+ return await fn(inner);
29
+ } finally{
30
+ for (const lock of [
31
+ ...innerBound.locks ?? []
32
+ ].reverse())lock.release();
33
+ await Promise.all((innerBound.locks ?? []).map((lock)=>lock.task));
34
+ }
35
+ });
36
+ });
37
+ },
38
+ async get (key) {
39
+ await assertDomain(bound);
40
+ if (!supported(key.type)) return null;
41
+ return withStoreTransaction(db, async (tx)=>{
42
+ return decision(await whereKey(tx, key).orderBy('op_hash').execute(), bound);
43
+ });
44
+ },
45
+ async getMany (keys) {
46
+ await assertDomain(bound);
47
+ const wanted = new Map(keys.filter((key)=>supported(key.type)).map((key)=>[
48
+ recordKeyText(key),
49
+ key
50
+ ]));
51
+ const loadChunk = async (addresses)=>{
52
+ return withStoreTransaction(db, async (tx)=>{
53
+ return tx.selectFrom('vault_records').selectAll().where((eb)=>{
54
+ return eb.or(addresses.map((key)=>{
55
+ return eb.and([
56
+ eb('type', '=', key.type),
57
+ eb('owner', '=', key.owner),
58
+ eb('id', '=', key.id)
59
+ ]);
60
+ }));
61
+ }).orderBy('op_hash').execute();
62
+ });
63
+ };
64
+ const chunks = await settleAll(chunk(wanted.values(), GET_MANY_CHUNK).map(loadChunk));
65
+ const groups = [
66
+ ...groupRows(chunks.flat())
67
+ ];
68
+ const decisions = await settleAll(groups.map(([, rows])=>decision(rows, bound)));
69
+ const result = new Map();
70
+ for (const [index, [key]] of groups.entries()){
71
+ const value = decisions[index];
72
+ if (value !== null && value !== undefined) result.set(key, value);
73
+ }
74
+ return result;
75
+ },
76
+ async list (types) {
77
+ await assertDomain(bound);
78
+ const selected = [
79
+ ...new Set(types.filter(supported))
80
+ ];
81
+ if (selected.length === 0) return [];
82
+ const rows = await withStoreTransaction(db, async (tx)=>{
83
+ return tx.selectFrom('vault_records').selectAll().where('type', 'in', selected).orderBy('type').orderBy('owner').orderBy('id').orderBy('op_hash').execute();
84
+ });
85
+ const groups = [
86
+ ...groupRows(rows).values()
87
+ ];
88
+ const decisions = await Promise.allSettled(groups.map((group)=>decision(group, bound)));
89
+ const result = [];
90
+ // Callbacks and non-row failures keep group order.
91
+ for (const [index, outcome] of decisions.entries()){
92
+ if (outcome.status === 'fulfilled') {
93
+ if (outcome.value !== null) result.push(outcome.value);
94
+ continue;
95
+ }
96
+ const error = outcome.reason;
97
+ if (!(error instanceof VaultRecordRowError)) throw error;
98
+ const row = groups[index]?.[0];
99
+ if (row !== undefined) bound.params.onCorruptRow?.({
100
+ type: row.type,
101
+ id: row.id,
102
+ error
103
+ });
104
+ }
105
+ return result;
106
+ },
107
+ async lock (keys) {
108
+ if (!bound.context.provider.isTransactionBound || bound.locks === undefined) throw new Error('Record locks require an adapter transaction');
109
+ if (keys.some((key)=>key.type !== 'vault.key')) throw new TypeError('Unknown vault lock namespace');
110
+ const entered = Promise.withResolvers();
111
+ const released = Promise.withResolvers();
112
+ // Keep hozon's queue held through authoritative reads and all apply effects.
113
+ const task = withKeyLocks(bound.context.provider, 'vault_keys', keys.map((key)=>[
114
+ key.key
115
+ ]), async (provider)=>{
116
+ bound.context = {
117
+ ...bound.context,
118
+ provider
119
+ };
120
+ bound.store = await getVaultStore(provider);
121
+ entered.resolve();
122
+ await released.promise;
123
+ });
124
+ bound.locks.push({
125
+ release: ()=>released.resolve(),
126
+ task
127
+ });
128
+ await Promise.race([
129
+ entered.promise,
130
+ task
131
+ ]);
132
+ },
133
+ async putCopy (record) {
134
+ if (!db.isTransaction) throw new Error('Copy persistence requires the apply transaction');
135
+ await assertDomain(bound);
136
+ const op = await verifySignedRecord(record, bound.opKind, bound.params.methods);
137
+ if (op === null || !supported(record.type) || !recordPayloadMatches(record, op.claim)) throw new Error('Invalid vault record copy');
138
+ const type = record.type;
139
+ if (vaultRecordMeta(type, op.claim) === null) throw new Error('Invalid vault record metadata');
140
+ const previousRows = await whereKey(db, record).orderBy('op_hash').execute();
141
+ const previous = await decision(previousRows, bound);
142
+ const contentDigest = recordClaimDigest(op.claim);
143
+ const decisionHLC = typeof op.claim.meta.version === 'string' ? op.claim.meta.version : op.claim.hlc;
144
+ const same = previous?.contentDigest === contentDigest && previous.decisionHLC === decisionHLC;
145
+ const incomingSigner = recordSignerKey(op.issuer, record.opJWT);
146
+ // One check per prior row, shared by both passes. The first failure in row order wins.
147
+ const priors = await settleAll(previousRows.map((row)=>checked(row, bound)));
148
+ let keepHash = record.opHash;
149
+ for (const [index, row] of previousRows.entries()){
150
+ const prior = priors[index] ?? null;
151
+ if (same && prior !== null && recordSignerKey(prior.op.issuer, prior.copy.opJWT) === incomingSigner && row.op_hash < keepHash) keepHash = row.op_hash;
152
+ }
153
+ if (!same) {
154
+ await db.deleteFrom('vault_records').where('type', '=', record.type).where('owner', '=', record.owner).where('id', '=', record.id).execute();
155
+ await projection(record, op, bound);
156
+ } else {
157
+ const dropped = previousRows.filter((row, index)=>{
158
+ const prior = priors[index] ?? null;
159
+ return prior === null || recordSignerKey(prior.op.issuer, prior.copy.opJWT) === incomingSigner && row.op_hash !== keepHash;
160
+ }).map((row)=>row.op_hash);
161
+ if (dropped.length > 0) await deleteCopies(db, record, dropped);
162
+ }
163
+ if (keepHash === record.opHash) await db.insertInto('vault_records').values(values(record, op, bound)).onConflict((oc)=>oc.columns([
164
+ 'type',
165
+ 'owner',
166
+ 'id',
167
+ 'op_hash'
168
+ ]).doNothing()).execute();
169
+ // Claims the key for replicated mode and records the decision in one statement. A key
170
+ // held in local mode is left alone and counts no row.
171
+ const claimed = await upsertReplicatedDecision(db, {
172
+ type: record.type,
173
+ owner: record.owner,
174
+ id: record.id,
175
+ content_digest: contentDigest,
176
+ decision_hlc: decisionHLC
177
+ }).executeTakeFirstOrThrow();
178
+ if (claimed.numInsertedOrUpdatedRows === 0n) throw new VaultModeConflict();
179
+ },
180
+ async pruneCopy (key, signerDID) {
181
+ if (!db.isTransaction) throw new Error('Copy pruning requires the apply transaction');
182
+ const current = await adapter.get(key);
183
+ if (current === null) return;
184
+ const verified = await Promise.all(current.copies.map(async (copy)=>{
185
+ return {
186
+ copy,
187
+ op: await verifySignedRecord(copy, bound.opKind, bound.params.methods)
188
+ };
189
+ }));
190
+ if (!verified.some(({ op })=>op !== null && op.issuer !== signerDID)) return;
191
+ const dropped = verified.filter(({ op })=>op?.issuer === signerDID).map(({ copy })=>copy.opHash);
192
+ if (dropped.length > 0) await deleteCopies(db, key, dropped);
193
+ }
194
+ };
195
+ boundAdapters.set(adapter, bound);
196
+ return adapter;
197
+ }
@@ -0,0 +1,4 @@
1
+ import type { RecordStoreAdapter } from '@kokuin/records';
2
+ import type { VaultRecordPorts } from '@kokuin/vault';
3
+ /** Resolve effects only through an adapter bound to the apply transaction. */
4
+ export declare function vaultRecordPortsFor(store: RecordStoreAdapter): VaultRecordPorts;
@@ -0,0 +1,72 @@
1
+ import { withStoreTransaction } from '@hozon/db';
2
+ import { recordPayloadMatches, verifySignedRecord } from '@kokuin/records';
3
+ import { vaultRecordMeta, vaultRevocationRecordID } from '@kokuin/vault';
4
+ import { selectWrappings, statementChunks } from './batch.js';
5
+ import { VaultModeConflict } from './record-context.js';
6
+ import { assertDomain, boundAdapters, values } from './records-rows.js';
7
+ import { settleAll } from './settle.js';
8
+ /** Resolve effects only through an adapter bound to the apply transaction. */ export function vaultRecordPortsFor(store) {
9
+ const bound = boundAdapters.get(store) ?? boundAdapters.get(store.adapterForType?.('vault.entry'));
10
+ if (bound === undefined) throw new Error('Vault adapter is unavailable for effect ports');
11
+ const { db } = bound;
12
+ return {
13
+ async retainEntryCandidate (record) {
14
+ if (!db.isTransaction) throw new Error('Candidate retention requires the apply transaction');
15
+ const op = await verifySignedRecord(record, bound.opKind, bound.params.methods);
16
+ if (op === null || !recordPayloadMatches(record, op.claim) || vaultRecordMeta('vault.entry', op.claim) === null) throw new Error('Invalid entry candidate');
17
+ await db.insertInto('vault_entry_candidates').values({
18
+ ...values(record, op, bound),
19
+ key_id: op.claim.meta.keyID,
20
+ key_version: op.claim.meta.keyVersion,
21
+ branch_id: op.claim.meta.branchID ?? ''
22
+ }).onConflict((oc)=>oc.columns([
23
+ 'type',
24
+ 'owner',
25
+ 'id',
26
+ 'op_hash'
27
+ ]).doNothing()).execute();
28
+ },
29
+ async listRevocations (address) {
30
+ await assertDomain(bound);
31
+ const rows = await withStoreTransaction(db, async (tx)=>{
32
+ return tx.selectFrom('vault_revocations').select('recipient_did').where('owner_did', '=', address.ownerDID).where('key_id', '=', address.keyID).orderBy('recipient_did').execute();
33
+ });
34
+ return rows.map((row)=>row.recipient_did);
35
+ },
36
+ async dropRevokedWrappings (address) {
37
+ if (!db.isTransaction) throw new Error('Wrapping suppression requires the apply transaction');
38
+ const [revocations, wrappings] = await settleAll([
39
+ this.listRevocations(address),
40
+ bound.store.listWrappings(address)
41
+ ]);
42
+ const denied = new Set(revocations.map((did)=>vaultRevocationRecordID(address.keyID, did)));
43
+ const revoked = wrappings.filter((row)=>{
44
+ const recipients = [
45
+ row.recipientDID,
46
+ ...row.descriptors.filter((d)=>d.kind === 'did').map((d)=>d.params.recipient)
47
+ ];
48
+ return recipients.some((recipient)=>{
49
+ return typeof recipient === 'string' && denied.has(vaultRevocationRecordID(address.keyID, recipient));
50
+ });
51
+ }).map((row)=>({
52
+ owner_did: row.ownerDID,
53
+ wrapping_id: row.wrappingID
54
+ }));
55
+ if (revoked.length === 0) return;
56
+ const parts = statementChunks(revoked, 2);
57
+ const held = await settleAll(parts.map((part)=>selectWrappings(db, part).execute()));
58
+ // Denial never deletes an unrelated local record.
59
+ if (held.flat().some((row)=>row.mode === 'local')) throw new VaultModeConflict();
60
+ await settleAll(parts.map((part)=>{
61
+ return db.deleteFrom('vault_wrappings').where((eb)=>{
62
+ return eb.or(part.map((row)=>{
63
+ return eb.and([
64
+ eb('owner_did', '=', row.owner_did),
65
+ eb('wrapping_id', '=', row.wrapping_id)
66
+ ]);
67
+ }));
68
+ }).execute();
69
+ }));
70
+ }
71
+ };
72
+ }
@@ -0,0 +1,3 @@
1
+ import type { SignedRecord, VerifiedRecordOp } from '@kokuin/records';
2
+ import type { Bound } from './records-rows.js';
3
+ export declare function projection(record: SignedRecord, op: VerifiedRecordOp, bound: Bound): Promise<void>;
@@ -0,0 +1,105 @@
1
+ import { decodeVaultRecord } from '@kokuin/vault';
2
+ import { VaultModeConflict } from './record-context.js';
3
+ export async function projection(record, op, bound) {
4
+ const { db, adapter } = bound;
5
+ const type = record.type;
6
+ const table = type === 'vault.key' ? 'vault_keys' : type === 'vault.entry' ? 'vault_entries' : type === 'vault.wrapping' ? 'vault_wrappings' : null;
7
+ if (table !== null) {
8
+ const column = type === 'vault.key' ? 'key_id' : type === 'vault.entry' ? 'entry_id' : 'wrapping_id';
9
+ const existing = await db.selectFrom(table).select('mode').where('owner_did', '=', record.owner).where(column, '=', record.id).executeTakeFirst();
10
+ if (existing?.mode === 'local') throw new VaultModeConflict();
11
+ // The caller, `putCopy`, claims the decision for replicated mode in the same transaction.
12
+ if (record.payload === null) {
13
+ await db.deleteFrom(table).where('owner_did', '=', record.owner).where(column, '=', record.id).execute();
14
+ return;
15
+ }
16
+ }
17
+ const decoded = decodeVaultRecord(record, op.claim.meta);
18
+ switch(decoded.type){
19
+ case 'vault.key':
20
+ {
21
+ const row = {
22
+ owner_did: decoded.owner,
23
+ key_id: decoded.id,
24
+ suite: decoded.payload.suite,
25
+ version: decoded.meta.version,
26
+ state: decoded.payload.state,
27
+ mode: 'replicated'
28
+ };
29
+ await db.insertInto('vault_keys').values(row).onConflict((oc)=>oc.columns([
30
+ 'owner_did',
31
+ 'key_id'
32
+ ]).doUpdateSet(row)).execute();
33
+ break;
34
+ }
35
+ case 'vault.wrapping':
36
+ {
37
+ const row = {
38
+ owner_did: decoded.owner,
39
+ wrapping_id: decoded.id,
40
+ key_id: decoded.meta.keyID,
41
+ key_version: decoded.meta.keyVersion,
42
+ descriptors: adapter.encodeJSON(decoded.payload.descriptors),
43
+ descriptor_digest: decoded.payload.descriptorDigest,
44
+ iv: adapter.encodeBinary(decoded.payload.iv),
45
+ wrapped_key: adapter.encodeBinary(decoded.payload.wrappedKey),
46
+ recipient_did: decoded.meta.recipient,
47
+ provenance: decoded.meta.branchID,
48
+ mode: 'replicated'
49
+ };
50
+ await db.insertInto('vault_wrappings').values(row).onConflict((oc)=>oc.columns([
51
+ 'owner_did',
52
+ 'wrapping_id'
53
+ ]).doUpdateSet(row)).execute();
54
+ break;
55
+ }
56
+ case 'vault.entry':
57
+ {
58
+ const row = {
59
+ owner_did: decoded.owner,
60
+ entry_id: decoded.id,
61
+ key_id: decoded.meta.keyID,
62
+ key_version: decoded.meta.keyVersion,
63
+ kind: decoded.meta.kind,
64
+ label: decoded.meta.label,
65
+ attributes: adapter.encodeJSON(decoded.meta.attributes),
66
+ iv: adapter.encodeBinary(decoded.payload.iv),
67
+ ciphertext: adapter.encodeBinary(decoded.payload.ciphertext),
68
+ hlc: decoded.hlc,
69
+ provenance: decoded.meta.branchID,
70
+ mode: 'replicated'
71
+ };
72
+ await db.insertInto('vault_entries').values(row).onConflict((oc)=>oc.columns([
73
+ 'owner_did',
74
+ 'entry_id'
75
+ ]).doUpdateSet(row)).execute();
76
+ break;
77
+ }
78
+ case 'vault.key-branch':
79
+ {
80
+ const row = {
81
+ owner_did: decoded.owner,
82
+ key_id: decoded.meta.keyID,
83
+ key_version: decoded.meta.keyVersion,
84
+ record_id: decoded.id,
85
+ branch_id: decoded.meta.branchID,
86
+ op_jwt: decoded.payload.opJWT
87
+ };
88
+ await db.insertInto('vault_key_branches').values(row).onConflict((oc)=>oc.columns([
89
+ 'owner_did',
90
+ 'record_id'
91
+ ]).doUpdateSet(row)).execute();
92
+ break;
93
+ }
94
+ case 'vault.revocation':
95
+ await db.insertInto('vault_revocations').values({
96
+ owner_did: decoded.owner,
97
+ key_id: decoded.meta.keyID,
98
+ recipient_did: decoded.meta.revokedDID
99
+ }).onConflict((oc)=>oc.columns([
100
+ 'owner_did',
101
+ 'key_id',
102
+ 'recipient_did'
103
+ ]).doNothing()).execute();
104
+ }
105
+ }
@@ -0,0 +1,54 @@
1
+ import type { Kysely } from '@hozon/db';
2
+ import type { RecordDecision, RecordKey, RecordStoreAdapter, SignedRecord, VerifiedRecordOp } from '@kokuin/records';
3
+ import type { VaultDatabase } from './record-context.js';
4
+ import type { VaultRecordsTables, VaultRecordTable } from './record-tables.js';
5
+ import type { VaultRecordsParams } from './records.js';
6
+ import type { VaultStoreAPI } from './types.js';
7
+ export declare class VaultRecordRowError extends Error {
8
+ #private;
9
+ constructor(key: RecordKey & {
10
+ cause?: unknown;
11
+ });
12
+ get type(): string;
13
+ get id(): string;
14
+ }
15
+ export type CheckedRecord = {
16
+ copy: SignedRecord;
17
+ op: VerifiedRecordOp;
18
+ };
19
+ export type HeldLock = {
20
+ release: () => void;
21
+ task: Promise<void>;
22
+ };
23
+ export type Bound = VaultDatabase & {
24
+ opKind: string;
25
+ params: VaultRecordsParams;
26
+ store: VaultStoreAPI;
27
+ locks?: Array<HeldLock>;
28
+ /** Set once the domain row matched this adapter's operation kind. */
29
+ domainChecked?: boolean;
30
+ };
31
+ export declare const GET_MANY_CHUNK = 160;
32
+ export declare const boundAdapters: WeakMap<RecordStoreAdapter, Bound>;
33
+ export declare const acceptedKinds: WeakMap<VaultStoreAPI, string>;
34
+ export declare function supported(type: string): boolean;
35
+ export declare function json(value: unknown): unknown;
36
+ export declare function groupRows(rows: Array<VaultRecordTable>): Map<string, Array<VaultRecordTable>>;
37
+ /** Deletes the named copies of one record, in statements of at most `PARAMETER_BUDGET` binds. */
38
+ export declare function deleteCopies(db: Kysely<VaultRecordsTables>, key: RecordKey, opHashes: Array<string>): Promise<void>;
39
+ export declare function whereKey(db: Kysely<VaultRecordsTables>, key: RecordKey): import("@hozon/db").SelectQueryBuilder<VaultRecordsTables, "vault_records", {
40
+ hlc: string;
41
+ type: string;
42
+ owner: string;
43
+ id: string;
44
+ op_jwt: string;
45
+ op_hash: string;
46
+ payload: Uint8Array<ArrayBufferLike> | null;
47
+ payload_digest: string | null;
48
+ deleted: number;
49
+ meta: Record<string, unknown>;
50
+ }>;
51
+ export declare function assertDomain(bound: Bound): Promise<void>;
52
+ export declare function checked(row: VaultRecordTable, bound: Bound): Promise<CheckedRecord | null>;
53
+ export declare function decision(rows: Array<VaultRecordTable>, bound: Bound): Promise<RecordDecision | null>;
54
+ export declare function values(record: SignedRecord, op: VerifiedRecordOp, bound: Bound): VaultRecordTable;