@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.
- package/lib/advisory-locks.d.ts +5 -0
- package/lib/advisory-locks.js +12 -0
- package/lib/api-aliases.d.ts +3 -0
- package/lib/api-aliases.js +58 -0
- package/lib/api-entries.d.ts +3 -0
- package/lib/api-entries.js +157 -0
- package/lib/api-keys.d.ts +3 -0
- package/lib/api-keys.js +249 -0
- package/lib/api-provisioning.d.ts +3 -0
- package/lib/api-provisioning.js +227 -0
- package/lib/api-records.d.ts +3 -0
- package/lib/api-records.js +74 -0
- package/lib/api-types.d.ts +412 -0
- package/lib/api-types.js +14 -0
- package/lib/api.d.ts +5 -448
- package/lib/api.js +24 -977
- package/lib/definition.js +3 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +1 -0
- package/lib/migrations.js +62 -36
- package/lib/records.d.ts +206 -0
- package/lib/records.js +905 -0
- package/lib/store-context.d.ts +37 -0
- package/lib/store-context.js +139 -0
- package/lib/tables.d.ts +88 -64
- package/package.json +11 -8
|
@@ -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'>;
|
package/lib/api-keys.js
ADDED
|
@@ -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'>;
|