@kubun/store-credential 0.17.4 → 0.18.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.
@@ -0,0 +1,5 @@
1
+ /** PostgreSQL advisory IDs are signed bigint values, not key-name order. */
2
+ export declare function orderAdvisoryLockIDs(keys: ReadonlyArray<{
3
+ keyID: string;
4
+ advisoryID: bigint;
5
+ }>): Array<bigint>;
@@ -0,0 +1,12 @@
1
+ /** PostgreSQL advisory IDs are signed bigint values, not key-name order. */ export function orderAdvisoryLockIDs(keys) {
2
+ const seenKeys = new Set();
3
+ const ids = new Set();
4
+ for (const { keyID, advisoryID } of keys){
5
+ if (seenKeys.has(keyID)) continue;
6
+ seenKeys.add(keyID);
7
+ ids.add(advisoryID);
8
+ }
9
+ return [
10
+ ...ids
11
+ ].sort((left, right)=>left < right ? -1 : left > right ? 1 : 0);
12
+ }
@@ -0,0 +1,3 @@
1
+ import type { CredentialStoreAPI } from './api-types.js';
2
+ import type { StoreContext } from './store-context.js';
3
+ export declare function createAliasesApi(ctx: StoreContext): Pick<CredentialStoreAPI, 'getDeviceAliasEntry' | 'getDeviceAliasEntryID' | 'setDeviceAliasEntryID' | 'deleteDeviceAliasEntryID' | 'deleteDeviceAliasIfEntry' | 'listDeviceAliasOwnersForProvider' | 'listDeviceAliasesForOwner'>;
@@ -0,0 +1,58 @@
1
+ export function createAliasesApi(ctx) {
2
+ const { db, gatedIfAmbient } = ctx;
3
+ return {
4
+ async getDeviceAliasEntry ({ providerName, ownerDID, deviceDID }) {
5
+ const row = await db.selectFrom('kubun_credential_device_aliases').select([
6
+ 'entry_id',
7
+ 'entry_owner_did'
8
+ ]).where('provider_name', '=', providerName).where('owner_did', '=', ownerDID).where('device_did', '=', deviceDID).executeTakeFirst();
9
+ return row == null ? null : {
10
+ entryID: row.entry_id,
11
+ entryOwnerDID: row.entry_owner_did
12
+ };
13
+ },
14
+ async getDeviceAliasEntryID (key) {
15
+ return (await ctx.api().getDeviceAliasEntry(key))?.entryID ?? null;
16
+ },
17
+ async setDeviceAliasEntryID ({ providerName, ownerDID, deviceDID, entryID, entryOwnerDID }) {
18
+ await gatedIfAmbient(()=>db.insertInto('kubun_credential_device_aliases').values({
19
+ provider_name: providerName,
20
+ owner_did: ownerDID,
21
+ device_did: deviceDID,
22
+ entry_id: entryID,
23
+ entry_owner_did: entryOwnerDID ?? ownerDID
24
+ }).onConflict((oc)=>oc.columns([
25
+ 'provider_name',
26
+ 'owner_did',
27
+ 'device_did'
28
+ ]).doUpdateSet({
29
+ entry_id: entryID,
30
+ entry_owner_did: entryOwnerDID ?? ownerDID
31
+ })).execute());
32
+ },
33
+ async deleteDeviceAliasEntryID ({ providerName, ownerDID, deviceDID }) {
34
+ await gatedIfAmbient(()=>db.deleteFrom('kubun_credential_device_aliases').where('provider_name', '=', providerName).where('owner_did', '=', ownerDID).where('device_did', '=', deviceDID).execute());
35
+ },
36
+ async deleteDeviceAliasIfEntry ({ providerName, ownerDID, deviceDID, entryID }) {
37
+ await gatedIfAmbient(()=>db.deleteFrom('kubun_credential_device_aliases').where('provider_name', '=', providerName).where('owner_did', '=', ownerDID).where('device_did', '=', deviceDID).where('entry_id', '=', entryID).execute());
38
+ },
39
+ async listDeviceAliasOwnersForProvider ({ providerName, deviceDID }) {
40
+ const rows = await db.selectFrom('kubun_credential_device_aliases').select('owner_did').where('provider_name', '=', providerName).where('device_did', '=', deviceDID).orderBy('owner_did', 'asc').execute();
41
+ return rows.map((row)=>row.owner_did);
42
+ },
43
+ async listDeviceAliasesForOwner (ownerDID) {
44
+ const rows = await db.selectFrom('kubun_credential_device_aliases').select([
45
+ 'provider_name',
46
+ 'owner_did',
47
+ 'device_did',
48
+ 'entry_id'
49
+ ]).where('owner_did', '=', ownerDID).orderBy('provider_name', 'asc').orderBy('device_did', 'asc').execute();
50
+ return rows.map((row)=>({
51
+ providerName: row.provider_name,
52
+ ownerDID: row.owner_did,
53
+ deviceDID: row.device_did,
54
+ entryID: row.entry_id
55
+ }));
56
+ }
57
+ };
58
+ }
@@ -0,0 +1,3 @@
1
+ import type { CredentialStoreAPI } from './api-types.js';
2
+ import type { StoreContext } from './store-context.js';
3
+ export declare function createEntriesApi(ctx: StoreContext): Pick<CredentialStoreAPI, 'putEntry' | 'moveEntry' | 'updateEntryIfPresent' | 'updateEntryIfUnchanged' | 'getEntry' | 'listEntryOwners' | 'getEntryWithProvenance' | 'listEntries' | 'deleteEntry' | 'getMaxEntryHLC' | 'listEntryCandidates' | 'listPriorEntryTombstones'>;
@@ -0,0 +1,157 @@
1
+ import { verifyRecordOp } from '@kubun/records';
2
+ export function createEntriesApi(ctx) {
3
+ const { db, adapter, oneOwner, ownerForKey, writeSubject, deleteSubject, writeAmbient, lockKeyRowInTransaction } = ctx;
4
+ async function writeEntry(params, allowMove) {
5
+ const owner = await ownerForKey(params.keyID, params.ownerDID);
6
+ const source = allowMove ? await ctx.api().getEntry(params.entryID, owner) : undefined;
7
+ const sourceKeyID = source?.key_id;
8
+ const values = {
9
+ owner_did: owner,
10
+ entry_id: params.entryID,
11
+ key_id: params.keyID,
12
+ key_version: params.keyVersion,
13
+ iv: adapter.encodeBinary(params.iv),
14
+ ciphertext: adapter.encodeBinary(params.ciphertext),
15
+ hlc: params.hlc
16
+ };
17
+ await writeSubject({
18
+ keyID: params.keyID,
19
+ keyIDs: sourceKeyID == null ? [
20
+ params.keyID
21
+ ] : [
22
+ sourceKeyID,
23
+ params.keyID
24
+ ],
25
+ accept: allowMove ? async (tx)=>source !== undefined && (await tx.selectFrom('kubun_credential_entries').select('key_id').where('owner_did', '=', source.owner_did).where('entry_id', '=', params.entryID).executeTakeFirst())?.key_id === sourceKeyID : undefined,
26
+ write: async (tx)=>{
27
+ if (!allowMove) {
28
+ const existing = await tx.selectFrom('kubun_credential_entries').select('key_id').where('owner_did', '=', owner).where('entry_id', '=', params.entryID).executeTakeFirst();
29
+ if (existing != null && existing.key_id !== params.keyID) {
30
+ throw new Error(`Credential entry ${params.entryID} already belongs to key ${existing.key_id}`);
31
+ }
32
+ }
33
+ if (source !== undefined && source.owner_did !== owner) {
34
+ await tx.deleteFrom('kubun_credential_entries').where('owner_did', '=', source.owner_did).where('entry_id', '=', params.entryID).execute();
35
+ }
36
+ const result = await tx.insertInto('kubun_credential_entries').values(values).onConflict((oc)=>{
37
+ const update = oc.columns([
38
+ 'owner_did',
39
+ 'entry_id'
40
+ ]).doUpdateSet({
41
+ key_id: values.key_id,
42
+ key_version: values.key_version,
43
+ iv: values.iv,
44
+ ciphertext: values.ciphertext,
45
+ hlc: values.hlc
46
+ });
47
+ return allowMove ? update : update.where('kubun_credential_entries.key_id', '=', values.key_id);
48
+ }).executeTakeFirst();
49
+ if (!allowMove && (result.numInsertedOrUpdatedRows ?? 0n) === 0n) {
50
+ throw new Error(`Credential entry ${params.entryID} belongs to another key`);
51
+ }
52
+ }
53
+ });
54
+ }
55
+ const updateEntryConditionally = async (params, expectedHLC)=>{
56
+ let outcome = 'absent';
57
+ const owner = await ownerForKey(params.keyID, params.ownerDID);
58
+ await writeAmbient(db, async (tx)=>{
59
+ await lockKeyRowInTransaction(tx, params.keyID);
60
+ const result = await tx.updateTable('kubun_credential_entries').set({
61
+ key_version: params.keyVersion,
62
+ iv: adapter.encodeBinary(params.iv),
63
+ ciphertext: adapter.encodeBinary(params.ciphertext),
64
+ hlc: params.hlc
65
+ }).where('entry_id', '=', params.entryID).where('owner_did', '=', owner).where('key_id', '=', params.keyID).$if(expectedHLC != null, (update)=>update.where('hlc', '=', expectedHLC)).executeTakeFirst();
66
+ if ((result.numUpdatedRows ?? 0n) !== 0n) {
67
+ outcome = 'updated';
68
+ } else if (expectedHLC != null) {
69
+ const row = await tx.selectFrom('kubun_credential_entries').select('hlc').where('entry_id', '=', params.entryID).where('owner_did', '=', owner).where('key_id', '=', params.keyID).executeTakeFirst();
70
+ if (row != null && row.hlc !== expectedHLC) outcome = 'superseded';
71
+ }
72
+ });
73
+ return outcome;
74
+ };
75
+ return {
76
+ async putEntry (params) {
77
+ await writeEntry(params, false);
78
+ },
79
+ async moveEntry (params) {
80
+ await writeEntry(params, true);
81
+ },
82
+ async updateEntryIfPresent (params) {
83
+ // Unconditional, so never superseded.
84
+ return await updateEntryConditionally(params, null);
85
+ },
86
+ async updateEntryIfUnchanged (params) {
87
+ return await updateEntryConditionally(params, params.expectedHLC);
88
+ },
89
+ async getEntry (entryID, ownerDID) {
90
+ let query = db.selectFrom('kubun_credential_entries').selectAll().where('entry_id', '=', entryID);
91
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
92
+ return oneOwner(await query.execute(), `entry ${entryID}`)[0];
93
+ },
94
+ async listEntryOwners (entryID) {
95
+ const rows = await db.selectFrom('kubun_credential_entries').select('owner_did').where('entry_id', '=', entryID).orderBy('owner_did', 'asc').execute();
96
+ return rows.map((row)=>row.owner_did);
97
+ },
98
+ async getEntryWithProvenance (entryID, ownerDID) {
99
+ const entry = await ctx.api().getEntry(entryID, ownerDID);
100
+ if (entry == null) return undefined;
101
+ const record = await db.selectFrom('kubun_credential_records').select('op_jwt').where('type', '=', 'credential.entry').where('owner', '=', entry.owner_did).where('id', '=', entryID).where('deleted', '=', 0).executeTakeFirst();
102
+ const op = record == null ? null : await verifyRecordOp(record.op_jwt);
103
+ return {
104
+ ...entry,
105
+ author_did: op?.issuer ?? null
106
+ };
107
+ },
108
+ async listEntries (keyID, ownerDID) {
109
+ let query = db.selectFrom('kubun_credential_entries').selectAll().where('key_id', '=', keyID);
110
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
111
+ return oneOwner(await query.orderBy('created_at', 'asc').orderBy('entry_id', 'asc').execute(), `entries ${keyID}`);
112
+ },
113
+ async deleteEntry (entryID, ownerDID) {
114
+ const entry = await ctx.api().getEntry(entryID, ownerDID);
115
+ if (entry === undefined) return;
116
+ const keyID = entry.key_id;
117
+ await deleteSubject({
118
+ keyID,
119
+ remove: async (tx)=>{
120
+ await tx.deleteFrom('kubun_credential_entries').where('owner_did', '=', entry.owner_did).where('entry_id', '=', entryID).where('key_id', '=', keyID).execute();
121
+ }
122
+ });
123
+ },
124
+ async getMaxEntryHLC () {
125
+ const row = await db.selectFrom('kubun_credential_entries').select((eb)=>eb.fn.max('hlc').as('max_hlc')).executeTakeFirst();
126
+ return row?.max_hlc ?? null;
127
+ },
128
+ async listEntryCandidates (keyID, keyVersion, ownerDID) {
129
+ let query = db.selectFrom('kubun_credential_entry_candidates').selectAll().where('key_id', '=', keyID).where('key_version', '=', keyVersion);
130
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
131
+ const rows = oneOwner(await query.orderBy('entry_id', 'asc').orderBy('op_hash', 'asc').execute(), `entry candidates ${keyID}`);
132
+ return rows.map((row)=>({
133
+ type: 'credential.entry',
134
+ id: row.entry_id,
135
+ owner: row.owner,
136
+ hlc: row.hlc,
137
+ opJWT: row.op_jwt,
138
+ opHash: row.op_hash,
139
+ payload: row.payload === null ? null : Uint8Array.from(row.payload)
140
+ }));
141
+ },
142
+ async listPriorEntryTombstones (keyID, beforeVersion, ownerDID) {
143
+ let query = db.selectFrom('kubun_credential_entry_candidates').selectAll().where('key_id', '=', keyID).where('key_version', '<', beforeVersion).where('payload', 'is', null);
144
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
145
+ const rows = oneOwner(await query.orderBy('entry_id', 'asc').orderBy('op_hash', 'asc').execute(), `entry tombstones ${keyID}`);
146
+ return rows.map((row)=>({
147
+ type: 'credential.entry',
148
+ id: row.entry_id,
149
+ owner: row.owner,
150
+ hlc: row.hlc,
151
+ opJWT: row.op_jwt,
152
+ opHash: row.op_hash,
153
+ payload: null
154
+ }));
155
+ }
156
+ };
157
+ }
@@ -0,0 +1,3 @@
1
+ import type { CredentialStoreAPI } from './api-types.js';
2
+ import type { StoreContext } from './store-context.js';
3
+ export declare function createKeysApi(ctx: StoreContext): Pick<CredentialStoreAPI, 'putKey' | 'getKey' | 'listKeys' | 'setKeyState' | 'setKeyVersion' | 'deleteKey' | 'listKeyBranches' | 'putWrapping' | 'getWrapping' | 'listWrappings' | 'listWrappingsWithBranch' | 'deleteWrapping' | 'deleteWrappingsBelowVersion' | 'putWrappingAuthority' | 'getWrappingAuthority' | 'refreshKeyOwnerDelegation' | 'replaceKeyOwnerDelegationIfUnchanged' | 'getKeyOwnerDelegation'>;
@@ -0,0 +1,249 @@
1
+ /**
2
+ * The strictly-greater-`refreshSeq` upsert shared by {@link CredentialStoreAPI.putWrappingAuthority}
3
+ * and `putWrapping`'s own-transaction variant — one predicate, so the two entry
4
+ * points cannot drift on what "newer" means.
5
+ */ async function upsertWrappingAuthority(db, params) {
6
+ const values = {
7
+ owner_did: params.ownerDID,
8
+ wrapping_id: params.wrappingID,
9
+ authority_token: params.envelope,
10
+ refresh_seq: params.refreshSeq
11
+ };
12
+ await db.insertInto('kubun_credential_wrapping_authority').values(values).onConflict((oc)=>oc.columns([
13
+ 'owner_did',
14
+ 'wrapping_id'
15
+ ]).doUpdateSet({
16
+ authority_token: values.authority_token,
17
+ refresh_seq: values.refresh_seq
18
+ }).where('kubun_credential_wrapping_authority.refresh_seq', '<', values.refresh_seq)).execute();
19
+ }
20
+ export function createKeysApi(ctx) {
21
+ const { db, adapter, oneOwner, ownerForKey, writeSubject, deleteSubject, writeAmbient, lockKeyRowInTransaction } = ctx;
22
+ return {
23
+ async putKey (params) {
24
+ await writeSubject({
25
+ keyID: params.keyID,
26
+ write: async (tx)=>{
27
+ await tx.insertInto('kubun_credential_keys').values({
28
+ key_id: params.keyID,
29
+ owner_did: params.ownerDID,
30
+ suite: params.suite,
31
+ version: params.version,
32
+ state: params.state
33
+ }).onConflict((oc)=>oc.columns([
34
+ 'owner_did',
35
+ 'key_id'
36
+ ]).doUpdateSet({
37
+ suite: params.suite,
38
+ version: params.version,
39
+ state: params.state
40
+ })).execute();
41
+ },
42
+ accept: async (tx)=>{
43
+ const existing = await tx.selectFrom('kubun_credential_keys').select('version').where('owner_did', '=', params.ownerDID).where('key_id', '=', params.keyID).executeTakeFirst();
44
+ return existing == null || params.version >= existing.version;
45
+ }
46
+ });
47
+ },
48
+ async getKey (keyID, ownerDID) {
49
+ let query = db.selectFrom('kubun_credential_keys').selectAll().where('key_id', '=', keyID);
50
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
51
+ return oneOwner(await query.execute(), `key ${keyID}`)[0];
52
+ },
53
+ async listKeys (ownerDID) {
54
+ return await db.selectFrom('kubun_credential_keys').selectAll().where('owner_did', '=', ownerDID).orderBy('created_at', 'asc').orderBy('key_id', 'asc').execute();
55
+ },
56
+ async setKeyState (keyID, state, ownerDID) {
57
+ const owner = await ownerForKey(keyID, ownerDID);
58
+ await writeSubject({
59
+ keyID,
60
+ write: async (tx)=>{
61
+ await tx.updateTable('kubun_credential_keys').set({
62
+ state
63
+ }).where('owner_did', '=', owner).where('key_id', '=', keyID).execute();
64
+ }
65
+ });
66
+ },
67
+ async setKeyVersion (keyID, version, ownerDID) {
68
+ const owner = await ownerForKey(keyID, ownerDID);
69
+ await writeSubject({
70
+ keyID,
71
+ write: async (tx)=>{
72
+ await tx.updateTable('kubun_credential_keys').set({
73
+ version
74
+ }).where('owner_did', '=', owner).where('key_id', '=', keyID).execute();
75
+ },
76
+ accept: async (tx)=>{
77
+ const existing = await tx.selectFrom('kubun_credential_keys').select('version').where('owner_did', '=', owner).where('key_id', '=', keyID).executeTakeFirst();
78
+ return existing == null || version >= existing.version;
79
+ }
80
+ });
81
+ },
82
+ async deleteKey (keyID, ownerDID) {
83
+ const owner = await ownerForKey(keyID, ownerDID);
84
+ await deleteSubject({
85
+ keyID,
86
+ remove: async (tx)=>{
87
+ await tx.deleteFrom('kubun_credential_keys').where('owner_did', '=', owner).where('key_id', '=', keyID).execute();
88
+ }
89
+ });
90
+ },
91
+ async listKeyBranches (keyID, keyVersion, ownerDID) {
92
+ let query = db.selectFrom('kubun_credential_key_branches').selectAll().where('key_id', '=', keyID).where('key_version', '=', keyVersion);
93
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
94
+ return oneOwner(await query.orderBy('branch_id', 'asc').execute(), `branches ${keyID}`);
95
+ },
96
+ async putWrapping (params) {
97
+ const { authority } = params;
98
+ const owner = await ownerForKey(params.keyID, params.ownerDID);
99
+ const values = {
100
+ owner_did: owner,
101
+ wrapping_id: params.wrappingID,
102
+ key_id: params.keyID,
103
+ key_version: params.keyVersion,
104
+ factors: adapter.encodeJSON(params.factors),
105
+ iv: adapter.encodeBinary(params.iv),
106
+ wrapped_key: adapter.encodeBinary(params.wrappedKey),
107
+ recipient_did: params.recipientDID ?? null
108
+ };
109
+ await writeSubject({
110
+ keyID: params.keyID,
111
+ write: async (tx)=>{
112
+ const existing = await tx.selectFrom('kubun_credential_wrappings').select('key_id').where('owner_did', '=', owner).where('wrapping_id', '=', params.wrappingID).executeTakeFirst();
113
+ if (existing != null && existing.key_id !== params.keyID) {
114
+ throw new Error(`Credential wrapping ${params.wrappingID} already belongs to key ${existing.key_id}`);
115
+ }
116
+ const result = await tx.insertInto('kubun_credential_wrappings').values(values).onConflict((oc)=>oc.columns([
117
+ 'owner_did',
118
+ 'wrapping_id'
119
+ ]).doUpdateSet({
120
+ key_id: values.key_id,
121
+ key_version: values.key_version,
122
+ factors: values.factors,
123
+ iv: values.iv,
124
+ wrapped_key: values.wrapped_key,
125
+ recipient_did: values.recipient_did
126
+ }).where('kubun_credential_wrappings.key_id', '=', values.key_id)).executeTakeFirst();
127
+ if ((result.numInsertedOrUpdatedRows ?? 0n) === 0n) {
128
+ throw new Error(`Credential wrapping ${params.wrappingID} belongs to another key`);
129
+ }
130
+ if (authority != null) {
131
+ await upsertWrappingAuthority(tx, {
132
+ wrappingID: params.wrappingID,
133
+ ownerDID: owner,
134
+ envelope: authority.envelope,
135
+ refreshSeq: authority.refreshSeq
136
+ });
137
+ }
138
+ }
139
+ });
140
+ },
141
+ async getWrapping (wrappingID, ownerDID) {
142
+ let query = db.selectFrom('kubun_credential_wrappings').selectAll().where('wrapping_id', '=', wrappingID);
143
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
144
+ return oneOwner(await query.execute(), `wrapping ${wrappingID}`)[0];
145
+ },
146
+ async listWrappings (keyID, keyVersion, ownerDID) {
147
+ let query = db.selectFrom('kubun_credential_wrappings').selectAll().where('key_id', '=', keyID);
148
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
149
+ if (keyVersion != null) {
150
+ query = query.where('key_version', '=', keyVersion);
151
+ }
152
+ return oneOwner(await query.orderBy('created_at', 'asc').orderBy('wrapping_id', 'asc').execute(), `wrappings ${keyID}`);
153
+ },
154
+ async listWrappingsWithBranch (keyID, keyVersion, ownerDID) {
155
+ // The row keeps its own label, so a wrapping a replicated record wrote,
156
+ // which has no op row, still reports its branch rather than the empty one.
157
+ let query = db.selectFrom('kubun_credential_wrappings').selectAll().where('key_id', '=', keyID);
158
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
159
+ if (keyVersion != null) {
160
+ query = query.where('key_version', '=', keyVersion);
161
+ }
162
+ return oneOwner(await query.orderBy('created_at', 'asc').orderBy('wrapping_id', 'asc').execute(), `wrappings ${keyID}`);
163
+ },
164
+ async deleteWrapping (wrappingID, ownerDID) {
165
+ const wrapping = await ctx.api().getWrapping(wrappingID, ownerDID);
166
+ if (wrapping === undefined) return;
167
+ const keyID = wrapping.key_id;
168
+ await deleteSubject({
169
+ keyID,
170
+ remove: async (tx)=>{
171
+ await tx.deleteFrom('kubun_credential_wrappings').where('owner_did', '=', wrapping.owner_did).where('wrapping_id', '=', wrappingID).where('key_id', '=', keyID).execute();
172
+ }
173
+ });
174
+ },
175
+ async deleteWrappingsBelowVersion (keyID, keyVersion, ownerDID) {
176
+ const owner = await ownerForKey(keyID, ownerDID);
177
+ await writeAmbient(db, async (tx)=>{
178
+ await lockKeyRowInTransaction(tx, keyID);
179
+ await tx.deleteFrom('kubun_credential_wrappings').where('owner_did', '=', owner).where('key_id', '=', keyID).where('key_version', '<', keyVersion).execute();
180
+ });
181
+ },
182
+ async putWrappingAuthority (params) {
183
+ const owner = params.ownerDID ?? (await ctx.api().getWrapping(params.wrappingID))?.owner_did ?? '';
184
+ await upsertWrappingAuthority(db, {
185
+ ...params,
186
+ ownerDID: owner
187
+ });
188
+ },
189
+ async getWrappingAuthority (wrappingID, ownerDID) {
190
+ let query = db.selectFrom('kubun_credential_wrapping_authority').select([
191
+ 'owner_did',
192
+ 'authority_token',
193
+ 'refresh_seq'
194
+ ]).where('wrapping_id', '=', wrappingID);
195
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
196
+ const row = oneOwner(await query.execute(), `wrapping authority ${wrappingID}`)[0];
197
+ if (row == null) return undefined;
198
+ return {
199
+ envelope: row.authority_token,
200
+ refreshSeq: row.refresh_seq
201
+ };
202
+ },
203
+ async refreshKeyOwnerDelegation (params) {
204
+ const owner = await ownerForKey(params.keyID, params.ownerDID);
205
+ const values = {
206
+ owner_did: owner,
207
+ key_id: params.keyID,
208
+ delegation_tokens: adapter.encodeJSON(params.delegationTokens),
209
+ refresh_seq: params.refreshSeq,
210
+ revision: 1
211
+ };
212
+ const result = await db.insertInto('kubun_credential_key_owner_delegation').values(values).onConflict((oc)=>oc.columns([
213
+ 'owner_did',
214
+ 'key_id'
215
+ ]).doUpdateSet({
216
+ delegation_tokens: values.delegation_tokens,
217
+ refresh_seq: values.refresh_seq,
218
+ // Bump so a concurrent replace CAS on the pre-refresh revision fails.
219
+ revision: (eb)=>eb('kubun_credential_key_owner_delegation.revision', '+', 1)
220
+ }).where('kubun_credential_key_owner_delegation.refresh_seq', '<', values.refresh_seq)).executeTakeFirst();
221
+ return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
222
+ },
223
+ async replaceKeyOwnerDelegationIfUnchanged (params) {
224
+ const owner = await ownerForKey(params.keyID, params.ownerDID);
225
+ const result = await db.updateTable('kubun_credential_key_owner_delegation').set({
226
+ delegation_tokens: adapter.encodeJSON(params.delegationTokens),
227
+ refresh_seq: params.refreshSeq,
228
+ revision: (eb)=>eb('kubun_credential_key_owner_delegation.revision', '+', 1)
229
+ }).where('key_id', '=', params.keyID).where('owner_did', '=', owner).where('revision', '=', params.expectedRevision).executeTakeFirst();
230
+ return (result.numUpdatedRows ?? 0n) > 0n;
231
+ },
232
+ async getKeyOwnerDelegation (keyID, ownerDID) {
233
+ let query = db.selectFrom('kubun_credential_key_owner_delegation').select([
234
+ 'owner_did',
235
+ 'delegation_tokens',
236
+ 'refresh_seq',
237
+ 'revision'
238
+ ]).where('key_id', '=', keyID);
239
+ if (ownerDID !== undefined) query = query.where('owner_did', '=', ownerDID);
240
+ const row = oneOwner(await query.execute(), `key delegation ${keyID}`)[0];
241
+ if (row == null) return undefined;
242
+ return {
243
+ delegationTokens: row.delegation_tokens,
244
+ refreshSeq: row.refresh_seq,
245
+ revision: row.revision
246
+ };
247
+ }
248
+ };
249
+ }
@@ -0,0 +1,3 @@
1
+ import type { CredentialStoreAPI } from './api-types.js';
2
+ import type { StoreContext } from './store-context.js';
3
+ export declare function createProvisioningApi(ctx: StoreContext): Pick<CredentialStoreAPI, 'nextGrantorSequence' | 'nextProvisioningEpoch' | 'putManifest' | 'lockManifestRow' | 'listManifests' | 'listDistributableKeys' | 'beginProvisioningAttempt' | 'recordProvisioningAttempt' | 'abandonProvisioningAttempt' | 'abandonProvisioningAttempts' | 'listProvisioningExpectations' | 'listProvisioningExpectationOwners'>;