@kubun/store-p2p 0.18.1 → 0.20.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/api.d.ts +5 -2
- package/lib/api.js +10 -6
- package/lib/definition.d.ts +6 -1
- package/lib/definition.js +37 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/migrations.js +90 -90
- package/lib/ops/app-lane.d.ts +1 -1
- package/lib/ops/app-lane.js +18 -12
- package/lib/ops/app-outbox.d.ts +4 -2
- package/lib/ops/app-outbox.js +33 -33
- package/lib/ops/catalog-offers.d.ts +1 -1
- package/lib/ops/catalog-offers.js +9 -10
- package/lib/ops/catalog-routes.js +9 -9
- package/lib/ops/catalog-sync.d.ts +2 -1
- package/lib/ops/catalog-sync.js +39 -42
- package/lib/ops/circles.js +33 -33
- package/lib/ops/control.d.ts +1 -1
- package/lib/ops/control.js +12 -10
- package/lib/ops/delegations.d.ts +2 -1
- package/lib/ops/delegations.js +17 -25
- package/lib/ops/discovery.d.ts +1 -1
- package/lib/ops/discovery.js +23 -10
- package/lib/ops/document-sweep.js +5 -5
- package/lib/ops/groups.d.ts +3 -2
- package/lib/ops/groups.js +60 -62
- package/lib/ops/hubs.js +38 -57
- package/lib/ops/log-handler-completion.js +3 -3
- package/lib/ops/mls.d.ts +1 -1
- package/lib/ops/mls.js +36 -26
- package/lib/ops/own-agents.d.ts +3 -3
- package/lib/ops/own-agents.js +128 -72
- package/lib/ops/sync.d.ts +1 -1
- package/lib/ops/sync.js +17 -8
- package/lib/scope.d.ts +8 -0
- package/lib/scope.js +8 -0
- package/lib/tables.d.ts +52 -42
- package/package.json +14 -13
package/lib/ops/mls.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
+
import { decodeJSON } from '@kubun/db-adapter';
|
|
2
|
+
function decodeMLSState(adapter, row) {
|
|
3
|
+
return {
|
|
4
|
+
...row,
|
|
5
|
+
credential: row.is_deleted === 1 ? null : decodeJSON(adapter.kind, row.credential),
|
|
6
|
+
ledger_entries: decodeJSON(adapter.kind, row.ledger_entries)
|
|
7
|
+
};
|
|
8
|
+
}
|
|
1
9
|
export function createMLSOps(db, adapter) {
|
|
2
10
|
return {
|
|
3
11
|
async saveMLSState (state, expected) {
|
|
4
12
|
if (expected === null) {
|
|
5
|
-
const inserted = await db.insertInto('
|
|
13
|
+
const inserted = await db.insertInto('p2p_group_mls_state').values({
|
|
6
14
|
group_id: state.group_id,
|
|
7
15
|
device_id: state.device_id,
|
|
8
16
|
mls_state: adapter.encodeBinary(state.mls_state),
|
|
9
|
-
credential: state.credential,
|
|
17
|
+
credential: adapter.encodeJSON(JSON.parse(state.credential)),
|
|
10
18
|
epoch: state.epoch,
|
|
11
19
|
state_version: state.state_version ?? 0,
|
|
12
20
|
is_deleted: 0,
|
|
13
21
|
sender_generation_hwm: state.sender_generation_hwm ?? 0,
|
|
14
|
-
ledger_entries: state.ledger_entries
|
|
22
|
+
ledger_entries: state.ledger_entries == null ? undefined : adapter.encodeJSON(JSON.parse(state.ledger_entries))
|
|
15
23
|
}).onConflict((oc)=>oc.columns([
|
|
16
24
|
'group_id',
|
|
17
25
|
'device_id'
|
|
@@ -23,28 +31,28 @@ export function createMLSOps(db, adapter) {
|
|
|
23
31
|
if ((state.state_version ?? 0) <= expected.stateVersion) {
|
|
24
32
|
throw new Error(`MLS state version must advance for group ${state.group_id}`);
|
|
25
33
|
}
|
|
26
|
-
const saved = await db.updateTable('
|
|
34
|
+
const saved = await db.updateTable('p2p_group_mls_state').set((eb)=>({
|
|
27
35
|
mls_state: adapter.encodeBinary(state.mls_state),
|
|
28
|
-
credential: state.credential,
|
|
36
|
+
credential: adapter.encodeJSON(JSON.parse(state.credential)),
|
|
29
37
|
epoch: state.epoch,
|
|
30
38
|
state_version: state.state_version ?? 0,
|
|
31
39
|
is_deleted: 0,
|
|
32
40
|
sender_generation_hwm: state.epoch === expected.epoch ? eb.ref('sender_generation_hwm') : 0,
|
|
33
|
-
ledger_entries: state.ledger_entries ?? '[]',
|
|
41
|
+
ledger_entries: adapter.encodeJSON(JSON.parse(state.ledger_entries ?? '[]')),
|
|
34
42
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
35
43
|
})).where('group_id', '=', state.group_id).where('device_id', '=', state.device_id).where('epoch', '=', expected.epoch).where('state_version', '=', expected.stateVersion).returning('state_version').executeTakeFirst();
|
|
36
44
|
return saved != null;
|
|
37
45
|
}
|
|
38
|
-
const saved = await db.insertInto('
|
|
46
|
+
const saved = await db.insertInto('p2p_group_mls_state').values({
|
|
39
47
|
group_id: state.group_id,
|
|
40
48
|
device_id: state.device_id,
|
|
41
49
|
mls_state: adapter.encodeBinary(state.mls_state),
|
|
42
|
-
credential: state.credential,
|
|
50
|
+
credential: adapter.encodeJSON(JSON.parse(state.credential)),
|
|
43
51
|
epoch: state.epoch,
|
|
44
52
|
state_version: state.state_version ?? 0,
|
|
45
53
|
is_deleted: 0,
|
|
46
54
|
sender_generation_hwm: state.sender_generation_hwm ?? 0,
|
|
47
|
-
ledger_entries: state.ledger_entries
|
|
55
|
+
ledger_entries: state.ledger_entries == null ? undefined : adapter.encodeJSON(JSON.parse(state.ledger_entries))
|
|
48
56
|
}).onConflict((oc)=>oc.columns([
|
|
49
57
|
'group_id',
|
|
50
58
|
'device_id'
|
|
@@ -54,17 +62,17 @@ export function createMLSOps(db, adapter) {
|
|
|
54
62
|
epoch: eb.ref('excluded.epoch'),
|
|
55
63
|
state_version: eb.ref('excluded.state_version'),
|
|
56
64
|
is_deleted: 0,
|
|
57
|
-
sender_generation_hwm: eb.case().when('
|
|
65
|
+
sender_generation_hwm: eb.case().when('p2p_group_mls_state.epoch', '=', eb.ref('excluded.epoch')).then(eb.ref('p2p_group_mls_state.sender_generation_hwm')).else(0).end(),
|
|
58
66
|
ledger_entries: eb.ref('excluded.ledger_entries'),
|
|
59
67
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
60
68
|
}))// Epochs order commits; the state version orders in-place secret-tree
|
|
61
69
|
// mutations within an epoch. A late persist of an older snapshot
|
|
62
70
|
// must never overwrite a newer generation at the same epoch.
|
|
63
71
|
.where((eb)=>eb.or([
|
|
64
|
-
eb('
|
|
72
|
+
eb('p2p_group_mls_state.epoch', '<', eb.ref('excluded.epoch')),
|
|
65
73
|
eb.and([
|
|
66
|
-
eb('
|
|
67
|
-
eb('
|
|
74
|
+
eb('p2p_group_mls_state.epoch', '=', eb.ref('excluded.epoch')),
|
|
75
|
+
eb('p2p_group_mls_state.state_version', '<', eb.ref('excluded.state_version'))
|
|
68
76
|
])
|
|
69
77
|
]))).returning('state_version').executeTakeFirst();
|
|
70
78
|
return saved != null;
|
|
@@ -74,7 +82,7 @@ export function createMLSOps(db, adapter) {
|
|
|
74
82
|
throw new Error('Invalid sender generation reservation');
|
|
75
83
|
}
|
|
76
84
|
for(;;){
|
|
77
|
-
const row = await db.selectFrom('
|
|
85
|
+
const row = await db.selectFrom('p2p_group_mls_state').select([
|
|
78
86
|
'epoch',
|
|
79
87
|
'sender_generation_hwm'
|
|
80
88
|
]).where('group_id', '=', groupID).where('device_id', '=', deviceID).where('is_deleted', '=', 0).executeTakeFirst();
|
|
@@ -86,7 +94,7 @@ export function createMLSOps(db, adapter) {
|
|
|
86
94
|
if (!Number.isSafeInteger(next)) throw new Error('Sender generation reservation overflow');
|
|
87
95
|
let reserved = next;
|
|
88
96
|
if (next !== row.sender_generation_hwm) {
|
|
89
|
-
const updated = await db.updateTable('
|
|
97
|
+
const updated = await db.updateTable('p2p_group_mls_state').set({
|
|
90
98
|
sender_generation_hwm: next
|
|
91
99
|
}).where('group_id', '=', groupID).where('device_id', '=', deviceID).where('epoch', '=', epoch).where('is_deleted', '=', 0).where('sender_generation_hwm', '=', row.sender_generation_hwm).returning('sender_generation_hwm').executeTakeFirst();
|
|
92
100
|
if (updated == null) continue;
|
|
@@ -94,7 +102,7 @@ export function createMLSOps(db, adapter) {
|
|
|
94
102
|
}
|
|
95
103
|
if (senderState != null) {
|
|
96
104
|
// A later reservation by another holder wins: its state is newer.
|
|
97
|
-
await db.updateTable('
|
|
105
|
+
await db.updateTable('p2p_group_mls_state').set({
|
|
98
106
|
mls_state: adapter.encodeBinary(senderState.mlsState)
|
|
99
107
|
}).where('group_id', '=', groupID).where('device_id', '=', deviceID).where('epoch', '=', epoch).where('is_deleted', '=', 0).where('state_version', '=', senderState.stateVersion).where('sender_generation_hwm', '=', reserved).execute();
|
|
100
108
|
}
|
|
@@ -102,15 +110,16 @@ export function createMLSOps(db, adapter) {
|
|
|
102
110
|
}
|
|
103
111
|
},
|
|
104
112
|
async getMLSState (groupID, deviceID, options) {
|
|
105
|
-
let query = db.selectFrom('
|
|
113
|
+
let query = db.selectFrom('p2p_group_mls_state').selectAll().where('group_id', '=', groupID).where('device_id', '=', deviceID);
|
|
106
114
|
if (options?.includeDeleted !== true) query = query.where('is_deleted', '=', 0);
|
|
107
|
-
|
|
115
|
+
const row = await query.executeTakeFirst();
|
|
116
|
+
return row == null ? undefined : decodeMLSState(adapter, row);
|
|
108
117
|
},
|
|
109
118
|
async deleteMLSState (groupID, deviceID, expected) {
|
|
110
|
-
let query = db.updateTable('
|
|
119
|
+
let query = db.updateTable('p2p_group_mls_state').set((eb)=>({
|
|
111
120
|
mls_state: adapter.encodeBinary(new Uint8Array()),
|
|
112
|
-
credential: '',
|
|
113
|
-
ledger_entries:
|
|
121
|
+
credential: adapter.encodeJSON(''),
|
|
122
|
+
ledger_entries: adapter.encodeJSON([]),
|
|
114
123
|
sender_generation_hwm: 0,
|
|
115
124
|
is_deleted: 1,
|
|
116
125
|
state_version: eb('state_version', '+', 1),
|
|
@@ -123,17 +132,18 @@ export function createMLSOps(db, adapter) {
|
|
|
123
132
|
return deleted != null;
|
|
124
133
|
},
|
|
125
134
|
async listMLSStates (groupID) {
|
|
126
|
-
|
|
135
|
+
const rows = await db.selectFrom('p2p_group_mls_state').selectAll().where('group_id', '=', groupID).where('is_deleted', '=', 0).execute();
|
|
136
|
+
return rows.map((row)=>decodeMLSState(adapter, row));
|
|
127
137
|
},
|
|
128
138
|
async listMLSStateGroupIDs (deviceID) {
|
|
129
|
-
const rows = await db.selectFrom('
|
|
139
|
+
const rows = await db.selectFrom('p2p_group_mls_state').select('group_id').where('device_id', '=', deviceID).where('is_deleted', '=', 0).execute();
|
|
130
140
|
return rows.map((row)=>row.group_id);
|
|
131
141
|
},
|
|
132
142
|
async deleteGroupMLSStates (groupID) {
|
|
133
|
-
await db.updateTable('
|
|
143
|
+
await db.updateTable('p2p_group_mls_state').set((eb)=>({
|
|
134
144
|
mls_state: adapter.encodeBinary(new Uint8Array()),
|
|
135
|
-
credential: '',
|
|
136
|
-
ledger_entries:
|
|
145
|
+
credential: adapter.encodeJSON(''),
|
|
146
|
+
ledger_entries: adapter.encodeJSON([]),
|
|
137
147
|
sender_generation_hwm: 0,
|
|
138
148
|
is_deleted: 1,
|
|
139
149
|
state_version: eb('state_version', '+', 1),
|
package/lib/ops/own-agents.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type RecordDecision, type RecordDecisionMap, type RecordKey, type SignedRecord } from '@kokuin/records';
|
|
1
2
|
import { type MethodRegistry } from '@kokuin/token';
|
|
3
|
+
import type { StoreAPIContext } from '@kubun/db';
|
|
2
4
|
import { type Adapter } from '@kubun/db-adapter';
|
|
3
|
-
import { type RecordDecision, type RecordDecisionMap, type RecordKey, type SignedRecord } from '@kubun/records';
|
|
4
5
|
import { type Kysely } from 'kysely';
|
|
5
6
|
import type { GroupKind, OwnAgentsGrantKind, P2PStoreTables } from '../tables.js';
|
|
6
7
|
export type OwnAgentsRecordKey = RecordKey;
|
|
@@ -64,5 +65,4 @@ export type OwnAgentsOps = {
|
|
|
64
65
|
deleteJoinCapability(groupID: string): Promise<void>;
|
|
65
66
|
};
|
|
66
67
|
export declare const ownAgentsRecordLockKey: (key: RecordKey) => string;
|
|
67
|
-
export declare function
|
|
68
|
-
export declare function createOwnAgentsOps(db: Kysely<P2PStoreTables>, adapter: Adapter): OwnAgentsOps;
|
|
68
|
+
export declare function createOwnAgentsOps(db: Kysely<P2PStoreTables>, adapter: Adapter, context: StoreAPIContext): OwnAgentsOps;
|
package/lib/ops/own-agents.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { recordClaimDigest, recordDecisionFromVerifiedCopies, recordKeyText, recordPayloadMatches, recordSignerKey, verifySignedRecord } from '@kokuin/records';
|
|
1
2
|
import { normalizeDID } from '@kokuin/token';
|
|
2
|
-
import {
|
|
3
|
-
import { recordClaimDigest, recordDecisionFromVerifiedCopies, recordKeyText, recordPayloadMatches, recordSignerKey, verifySignedRecord } from '@kubun/records';
|
|
3
|
+
import { chunked, decodeJSON } from '@kubun/db-adapter';
|
|
4
4
|
import { sql } from 'kysely';
|
|
5
|
+
import { withP2PKeyLocks } from '../scope.js';
|
|
5
6
|
export class OwnAgentsTransactionError extends Error {
|
|
6
7
|
constructor(){
|
|
7
8
|
super('Own agents operation requires a transaction');
|
|
@@ -35,9 +36,13 @@ export const ownAgentsRecordLockKey = (key)=>JSON.stringify([
|
|
|
35
36
|
function requireType(type) {
|
|
36
37
|
if (type !== 'ownAgents.group' && type !== 'ownAgents.capability') throw new OwnAgentsRecordError(`Unsupported own agents record type: ${type}`);
|
|
37
38
|
}
|
|
38
|
-
async function verifiedRows(rows, methods) {
|
|
39
|
+
async function verifiedRows({ rows, adapter, methods, crypto }) {
|
|
39
40
|
const valid = [];
|
|
40
|
-
for (const
|
|
41
|
+
for (const encoded of rows){
|
|
42
|
+
const row = {
|
|
43
|
+
...encoded,
|
|
44
|
+
meta: decodeJSON(adapter.kind, encoded.meta)
|
|
45
|
+
};
|
|
41
46
|
const copy = {
|
|
42
47
|
type: row.type,
|
|
43
48
|
owner: row.owner,
|
|
@@ -47,39 +52,35 @@ async function verifiedRows(rows, methods) {
|
|
|
47
52
|
opHash: row.op_hash,
|
|
48
53
|
payload: row.payload === null ? null : Uint8Array.from(row.payload)
|
|
49
54
|
};
|
|
50
|
-
const op = await verifySignedRecord(
|
|
51
|
-
|
|
55
|
+
const op = await verifySignedRecord({
|
|
56
|
+
record: copy,
|
|
57
|
+
opKind: 'kubun/record-op',
|
|
58
|
+
methods,
|
|
59
|
+
crypto
|
|
60
|
+
});
|
|
61
|
+
if (op !== null && await recordPayloadMatches({
|
|
62
|
+
record: copy,
|
|
63
|
+
claim: op.claim,
|
|
64
|
+
crypto
|
|
65
|
+
})) valid.push({
|
|
52
66
|
copy,
|
|
53
67
|
op
|
|
54
68
|
});
|
|
55
69
|
}
|
|
56
70
|
return valid;
|
|
57
71
|
}
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
export function createOwnAgentsOps(db, adapter) {
|
|
64
|
-
const lockKeys = async (tx, keys)=>{
|
|
65
|
-
if (!(adapter instanceof AbstractPostgresAdapter)) return;
|
|
66
|
-
const ids = new Set();
|
|
67
|
-
for (const key of new Set(keys)){
|
|
68
|
-
const result = await sql`select hashtextextended(${key}, 0)::text as id`.execute(tx);
|
|
69
|
-
for (const row of result.rows)ids.add(BigInt(row.id));
|
|
70
|
-
}
|
|
71
|
-
for (const id of ownAgentsAdvisoryLockIDs([
|
|
72
|
-
...ids
|
|
73
|
-
])){
|
|
74
|
-
await sql`select pg_advisory_xact_lock(${id.toString()}::bigint)`.execute(tx);
|
|
75
|
-
}
|
|
72
|
+
export function createOwnAgentsOps(db, adapter, context) {
|
|
73
|
+
const withRecordLocks = async (keys, run)=>{
|
|
74
|
+
return await withP2PKeyLocks(context, 'p2p_own_agents_records', keys.map((key)=>[
|
|
75
|
+
key
|
|
76
|
+
]), run);
|
|
76
77
|
};
|
|
77
|
-
const rowsFor = (tx, key)=>tx.selectFrom('
|
|
78
|
-
const deleteCopy = (tx, key, hash)=>tx.deleteFrom('
|
|
78
|
+
const rowsFor = (tx, key)=>tx.selectFrom('p2p_own_agents_records').selectAll().where('type', '=', key.type).where('owner', '=', key.owner).where('id', '=', key.id).execute();
|
|
79
|
+
const deleteCopy = (tx, key, hash)=>tx.deleteFrom('p2p_own_agents_records').where('type', '=', key.type).where('owner', '=', key.owner).where('id', '=', key.id).where('op_hash', '=', hash).execute();
|
|
79
80
|
return {
|
|
80
81
|
async retainAccessDefaultRevocations (controllerDID, deviceDIDs) {
|
|
81
82
|
if (deviceDIDs.length === 0) return;
|
|
82
|
-
await db.insertInto('
|
|
83
|
+
await db.insertInto('p2p_access_default_revocations').values(deviceDIDs.map((did)=>({
|
|
83
84
|
controller_did: controllerDID,
|
|
84
85
|
device_did: normalizeDID(did)
|
|
85
86
|
}))).onConflict((oc)=>oc.columns([
|
|
@@ -88,7 +89,7 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
88
89
|
]).doNothing()).execute();
|
|
89
90
|
},
|
|
90
91
|
async isAccessDefaultSignerRevoked (controllerDID, deviceDID) {
|
|
91
|
-
return await db.selectFrom('
|
|
92
|
+
return await db.selectFrom('p2p_access_default_revocations').select('device_did').where('controller_did', '=', controllerDID).where('device_did', '=', normalizeDID(deviceDID)).executeTakeFirst() != null;
|
|
92
93
|
},
|
|
93
94
|
async recordBindingEvidence (bindings) {
|
|
94
95
|
const rows = new Map();
|
|
@@ -103,7 +104,7 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
103
104
|
]), row);
|
|
104
105
|
}
|
|
105
106
|
if (rows.size === 0) return;
|
|
106
|
-
await db.insertInto('
|
|
107
|
+
await db.insertInto('p2p_binding_evidence').values([
|
|
107
108
|
...rows.values()
|
|
108
109
|
]).onConflict((oc)=>oc.columns([
|
|
109
110
|
'controller_did',
|
|
@@ -111,28 +112,37 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
111
112
|
]).doNothing()).execute();
|
|
112
113
|
},
|
|
113
114
|
async listBindingControllers (deviceDID) {
|
|
114
|
-
const rows = await db.selectFrom('
|
|
115
|
+
const rows = await db.selectFrom('p2p_binding_evidence').select('controller_did').where('device_did', '=', normalizeDID(deviceDID)).orderBy('controller_did').execute();
|
|
115
116
|
return rows.map((row)=>row.controller_did);
|
|
116
117
|
},
|
|
117
118
|
async setGroupKind (groupID, kind) {
|
|
118
119
|
if (!db.isTransaction) throw new OwnAgentsTransactionError();
|
|
119
|
-
let update = db.updateTable('
|
|
120
|
+
let update = db.updateTable('p2p_groups').set({
|
|
120
121
|
kind
|
|
121
122
|
}).where('id', '=', groupID);
|
|
122
123
|
if (kind === 'standard') update = update.where('kind', '=', 'standard');
|
|
123
124
|
const result = await update.executeTakeFirst();
|
|
124
125
|
if (result.numUpdatedRows === 0n) {
|
|
125
|
-
const row = await db.selectFrom('
|
|
126
|
+
const row = await db.selectFrom('p2p_groups').select('kind').where('id', '=', groupID).executeTakeFirst();
|
|
126
127
|
if (row === undefined) throw new OwnAgentsGroupNotFoundError(groupID);
|
|
127
128
|
throw new OwnAgentsKindDowngradeError(groupID);
|
|
128
129
|
}
|
|
129
130
|
},
|
|
130
131
|
async getGroupKind (groupID) {
|
|
131
|
-
return (await db.selectFrom('
|
|
132
|
+
return (await db.selectFrom('p2p_groups').select('kind').where('id', '=', groupID).executeTakeFirst())?.kind;
|
|
132
133
|
},
|
|
133
134
|
async getOwnAgentsRecordDecision (key, methods) {
|
|
134
135
|
requireType(key.type);
|
|
135
|
-
return recordDecisionFromVerifiedCopies(
|
|
136
|
+
return recordDecisionFromVerifiedCopies({
|
|
137
|
+
key,
|
|
138
|
+
entries: await verifiedRows({
|
|
139
|
+
rows: await rowsFor(db, key),
|
|
140
|
+
adapter,
|
|
141
|
+
methods,
|
|
142
|
+
crypto: context.crypto
|
|
143
|
+
}),
|
|
144
|
+
stampSource: 'hlc'
|
|
145
|
+
});
|
|
136
146
|
},
|
|
137
147
|
async getOwnAgentsRecordDecisions (keys, methods) {
|
|
138
148
|
const wanted = new Map();
|
|
@@ -150,7 +160,7 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
150
160
|
...idSet
|
|
151
161
|
];
|
|
152
162
|
for (const chunk of chunked(ids)){
|
|
153
|
-
const rows = await db.selectFrom('
|
|
163
|
+
const rows = await db.selectFrom('p2p_own_agents_records').selectAll().where('type', '=', type).where('id', 'in', chunk).execute();
|
|
154
164
|
for (const row of rows){
|
|
155
165
|
const text = recordKeyText(row);
|
|
156
166
|
if (!wanted.has(text)) continue;
|
|
@@ -163,7 +173,16 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
163
173
|
const decisions = new Map();
|
|
164
174
|
for (const [text, rows] of groups){
|
|
165
175
|
const key = wanted.get(text);
|
|
166
|
-
const decision = recordDecisionFromVerifiedCopies(
|
|
176
|
+
const decision = recordDecisionFromVerifiedCopies({
|
|
177
|
+
key,
|
|
178
|
+
entries: await verifiedRows({
|
|
179
|
+
rows,
|
|
180
|
+
adapter,
|
|
181
|
+
methods,
|
|
182
|
+
crypto: context.crypto
|
|
183
|
+
}),
|
|
184
|
+
stampSource: 'hlc'
|
|
185
|
+
});
|
|
167
186
|
if (decision !== null) decisions.set(text, decision);
|
|
168
187
|
}
|
|
169
188
|
return decisions;
|
|
@@ -171,7 +190,7 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
171
190
|
async listOwnAgentsRecordDecisions (types, methods) {
|
|
172
191
|
for (const type of types)requireType(type);
|
|
173
192
|
if (types.length === 0) return [];
|
|
174
|
-
const rows = await db.selectFrom('
|
|
193
|
+
const rows = await db.selectFrom('p2p_own_agents_records').selectAll().where('type', 'in', [
|
|
175
194
|
...types
|
|
176
195
|
]).orderBy('type').orderBy('owner').orderBy('id').execute();
|
|
177
196
|
const groups = new Map();
|
|
@@ -190,30 +209,60 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
190
209
|
owner: first.owner,
|
|
191
210
|
id: first.id
|
|
192
211
|
};
|
|
193
|
-
const decision = recordDecisionFromVerifiedCopies(
|
|
212
|
+
const decision = recordDecisionFromVerifiedCopies({
|
|
213
|
+
key,
|
|
214
|
+
entries: await verifiedRows({
|
|
215
|
+
rows,
|
|
216
|
+
adapter,
|
|
217
|
+
methods,
|
|
218
|
+
crypto: context.crypto
|
|
219
|
+
}),
|
|
220
|
+
stampSource: 'hlc'
|
|
221
|
+
});
|
|
194
222
|
if (decision !== null) decisions.push(decision);
|
|
195
223
|
}
|
|
196
224
|
return decisions;
|
|
197
225
|
},
|
|
198
226
|
async putOwnAgentsRecordCopy (record, methods) {
|
|
199
227
|
requireType(record.type);
|
|
200
|
-
const op = await verifySignedRecord(
|
|
201
|
-
|
|
202
|
-
|
|
228
|
+
const op = await verifySignedRecord({
|
|
229
|
+
record,
|
|
230
|
+
opKind: 'kubun/record-op',
|
|
231
|
+
methods,
|
|
232
|
+
crypto: context.crypto
|
|
233
|
+
});
|
|
234
|
+
if (op === null || !await recordPayloadMatches({
|
|
235
|
+
record,
|
|
236
|
+
claim: op.claim,
|
|
237
|
+
crypto: context.crypto
|
|
238
|
+
})) throw new OwnAgentsRecordError('Invalid signed own agents copy');
|
|
239
|
+
const signerKey = recordSignerKey({
|
|
240
|
+
issuer: op.issuer,
|
|
241
|
+
opJWT: record.opJWT
|
|
242
|
+
});
|
|
203
243
|
if (signerKey === null) throw new OwnAgentsRecordError('Invalid own agents signer key');
|
|
204
244
|
const key = {
|
|
205
245
|
type: record.type,
|
|
206
246
|
owner: record.owner,
|
|
207
247
|
id: record.id
|
|
208
248
|
};
|
|
209
|
-
const digest = recordClaimDigest(
|
|
249
|
+
const digest = await recordClaimDigest({
|
|
250
|
+
claim: op.claim,
|
|
251
|
+
crypto: context.crypto
|
|
252
|
+
});
|
|
210
253
|
const write = async (tx)=>{
|
|
211
|
-
await lockKeys(tx, [
|
|
212
|
-
ownAgentsRecordLockKey(key)
|
|
213
|
-
]);
|
|
214
254
|
const rows = await rowsFor(tx, key);
|
|
215
|
-
const valid = await verifiedRows(
|
|
216
|
-
|
|
255
|
+
const valid = await verifiedRows({
|
|
256
|
+
rows,
|
|
257
|
+
adapter,
|
|
258
|
+
methods,
|
|
259
|
+
crypto: context.crypto
|
|
260
|
+
});
|
|
261
|
+
const winner = recordDecisionFromVerifiedCopies({
|
|
262
|
+
key,
|
|
263
|
+
entries: valid,
|
|
264
|
+
stampSource: 'hlc'
|
|
265
|
+
});
|
|
217
266
|
const retained = winner?.decisionHLC === record.hlc && winner.contentDigest === digest ? valid : [];
|
|
218
267
|
const keepBySigner = new Map([
|
|
219
268
|
[
|
|
@@ -222,18 +271,24 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
222
271
|
]
|
|
223
272
|
]);
|
|
224
273
|
for (const entry of retained){
|
|
225
|
-
const key = recordSignerKey(
|
|
274
|
+
const key = recordSignerKey({
|
|
275
|
+
issuer: entry.op.issuer,
|
|
276
|
+
opJWT: entry.copy.opJWT
|
|
277
|
+
});
|
|
226
278
|
if (key === null) continue;
|
|
227
279
|
const previous = keepBySigner.get(key);
|
|
228
280
|
if (previous === undefined || compareStrings(entry.copy.opHash, previous) < 0) keepBySigner.set(key, entry.copy.opHash);
|
|
229
281
|
}
|
|
230
282
|
for (const row of rows){
|
|
231
283
|
const entry = retained.find((entry)=>entry.copy.opHash === row.op_hash);
|
|
232
|
-
const signer = entry === undefined ? null : recordSignerKey(
|
|
284
|
+
const signer = entry === undefined ? null : recordSignerKey({
|
|
285
|
+
issuer: entry.op.issuer,
|
|
286
|
+
opJWT: entry.copy.opJWT
|
|
287
|
+
});
|
|
233
288
|
if (signer === null || keepBySigner.get(signer) !== row.op_hash) await deleteCopy(tx, key, row.op_hash);
|
|
234
289
|
}
|
|
235
290
|
if (keepBySigner.get(signerKey) !== record.opHash) return false;
|
|
236
|
-
await tx.insertInto('
|
|
291
|
+
await tx.insertInto('p2p_own_agents_records').values({
|
|
237
292
|
type: record.type,
|
|
238
293
|
owner: record.owner,
|
|
239
294
|
id: record.id,
|
|
@@ -252,28 +307,32 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
252
307
|
]).doNothing()).execute();
|
|
253
308
|
return true;
|
|
254
309
|
};
|
|
255
|
-
|
|
256
|
-
|
|
310
|
+
return await withRecordLocks([
|
|
311
|
+
ownAgentsRecordLockKey(key)
|
|
312
|
+
], write);
|
|
257
313
|
},
|
|
258
314
|
async pruneOwnAgentsRecordCopy (key, signerDID, methods) {
|
|
259
315
|
requireType(key.type);
|
|
260
316
|
const signer = normalizeDID(signerDID);
|
|
261
317
|
const write = async (tx)=>{
|
|
262
|
-
await
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
318
|
+
const valid = await verifiedRows({
|
|
319
|
+
rows: await rowsFor(tx, key),
|
|
320
|
+
adapter,
|
|
321
|
+
methods,
|
|
322
|
+
crypto: context.crypto
|
|
323
|
+
});
|
|
266
324
|
if (!valid.some((entry)=>entry.op.issuer !== signer)) return;
|
|
267
325
|
for (const entry of valid){
|
|
268
326
|
if (entry.op.issuer === signer) await deleteCopy(tx, key, entry.copy.opHash);
|
|
269
327
|
}
|
|
270
328
|
};
|
|
271
|
-
|
|
272
|
-
|
|
329
|
+
await withRecordLocks([
|
|
330
|
+
ownAgentsRecordLockKey(key)
|
|
331
|
+
], write);
|
|
273
332
|
},
|
|
274
333
|
async lockOwnAgentsRecordKeys (keys) {
|
|
275
334
|
if (!db.isTransaction) throw new OwnAgentsTransactionError();
|
|
276
|
-
await
|
|
335
|
+
await withRecordLocks(keys, async ()=>{});
|
|
277
336
|
},
|
|
278
337
|
async putOwnGrant (controllerDID, kind, token, issuedAt, expiresAt) {
|
|
279
338
|
const row = {
|
|
@@ -283,13 +342,10 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
283
342
|
issued_at: issuedAt,
|
|
284
343
|
expires_at: expiresAt
|
|
285
344
|
};
|
|
286
|
-
await
|
|
287
|
-
'controller_did',
|
|
288
|
-
'kind'
|
|
289
|
-
]).doUpdateSet(row).whereRef('excluded.issued_at', '>', 'kubun_p2p_own_agents_grants.issued_at')).execute();
|
|
345
|
+
await context.compareWrite('p2p_own_agents_grants', row);
|
|
290
346
|
},
|
|
291
347
|
async getOwnGrants (controllerDID) {
|
|
292
|
-
let query = db.selectFrom('
|
|
348
|
+
let query = db.selectFrom('p2p_own_agents_grants').selectAll();
|
|
293
349
|
if (controllerDID != null) query = query.where('controller_did', '=', controllerDID);
|
|
294
350
|
const rows = await query.orderBy('kind').execute();
|
|
295
351
|
return rows.map((row)=>({
|
|
@@ -316,11 +372,11 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
316
372
|
};
|
|
317
373
|
const preserveBinding = (column)=>{
|
|
318
374
|
return sql`case
|
|
319
|
-
when
|
|
320
|
-
then ${sql.ref(`
|
|
375
|
+
when ${sql.ref('p2p_own_agents_join_capabilities.token')} = excluded.token
|
|
376
|
+
then ${sql.ref(`p2p_own_agents_join_capabilities.${column}`)}
|
|
321
377
|
else null end`;
|
|
322
378
|
};
|
|
323
|
-
await db.insertInto('
|
|
379
|
+
await db.insertInto('p2p_own_agents_join_capabilities').values(row).onConflict((oc)=>{
|
|
324
380
|
return oc.column('group_id').doUpdateSet({
|
|
325
381
|
...row,
|
|
326
382
|
// Re-accepting the same consent must not erase authenticated admission evidence.
|
|
@@ -334,23 +390,23 @@ export function createOwnAgentsOps(db, adapter) {
|
|
|
334
390
|
}).execute();
|
|
335
391
|
},
|
|
336
392
|
async deleteJoinCapability (groupID) {
|
|
337
|
-
await db.deleteFrom('
|
|
393
|
+
await db.deleteFrom('p2p_own_agents_join_capabilities').where('group_id', '=', groupID).execute();
|
|
338
394
|
},
|
|
339
395
|
async getJoinCapability (groupID) {
|
|
340
|
-
const row = await db.selectFrom('
|
|
396
|
+
const row = await db.selectFrom('p2p_own_agents_join_capabilities').selectAll().where('group_id', '=', groupID).executeTakeFirst();
|
|
397
|
+
const prefix = decodeJSON(adapter.kind, row?.signer_prefix);
|
|
341
398
|
return row == null ? undefined : {
|
|
342
399
|
groupID: row.group_id,
|
|
343
400
|
recipientDID: row.recipient_did,
|
|
344
401
|
capabilityID: row.capability_id,
|
|
345
402
|
token: row.token,
|
|
346
403
|
expiresAt: Number(row.expires_at),
|
|
347
|
-
...row.signer_did == null || row.signer_leaf_key == null || row.signer_capability == null ||
|
|
404
|
+
...row.signer_did == null || row.signer_leaf_key == null || row.signer_capability == null || prefix == null ? {} : {
|
|
348
405
|
signerBinding: {
|
|
349
406
|
did: row.signer_did,
|
|
350
407
|
leafKey: new Uint8Array(row.signer_leaf_key),
|
|
351
408
|
capability: row.signer_capability,
|
|
352
|
-
|
|
353
|
-
prefix: row.signer_prefix.events.map((event)=>typeof event === 'string' ? event : JSON.stringify(event))
|
|
409
|
+
prefix: prefix.events
|
|
354
410
|
}
|
|
355
411
|
}
|
|
356
412
|
};
|
package/lib/ops/sync.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Adapter } from '@kubun/db-adapter';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
3
|
import type { InsertSyncPeer, P2PStoreTables, SyncPeer } from '../tables.js';
|
|
4
4
|
export declare class BroadcastLogPruneParamsError extends Error {
|
package/lib/ops/sync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { decodeJSON } from '@kubun/db-adapter';
|
|
2
3
|
export class BroadcastLogPruneParamsError extends Error {
|
|
3
4
|
get code() {
|
|
4
5
|
return 'P2P_BROADCAST_LOG_PRUNE_PARAMS';
|
|
@@ -12,7 +13,7 @@ export function createSyncOps(db, adapter) {
|
|
|
12
13
|
return {
|
|
13
14
|
async addSyncPeer (peer) {
|
|
14
15
|
const config = adapter.encodeJSON(peer.config);
|
|
15
|
-
await db.insertInto('
|
|
16
|
+
await db.insertInto('p2p_sync_peers').values({
|
|
16
17
|
...peer,
|
|
17
18
|
peer_did: normalizeDID(peer.peer_did),
|
|
18
19
|
config
|
|
@@ -23,10 +24,18 @@ export function createSyncOps(db, adapter) {
|
|
|
23
24
|
})).execute();
|
|
24
25
|
},
|
|
25
26
|
async getSyncPeer (peerDID) {
|
|
26
|
-
|
|
27
|
+
const row = await db.selectFrom('p2p_sync_peers').selectAll().where('peer_did', '=', normalizeDID(peerDID)).executeTakeFirst();
|
|
28
|
+
return row == null ? undefined : {
|
|
29
|
+
...row,
|
|
30
|
+
config: decodeJSON(adapter.kind, row.config)
|
|
31
|
+
};
|
|
27
32
|
},
|
|
28
33
|
async listSyncPeers () {
|
|
29
|
-
|
|
34
|
+
const rows = await db.selectFrom('p2p_sync_peers').selectAll().execute();
|
|
35
|
+
return rows.map((row)=>({
|
|
36
|
+
...row,
|
|
37
|
+
config: decodeJSON(adapter.kind, row.config)
|
|
38
|
+
}));
|
|
30
39
|
},
|
|
31
40
|
async updateSyncPeer (peerDID, update) {
|
|
32
41
|
const values = {};
|
|
@@ -35,14 +44,14 @@ export function createSyncOps(db, adapter) {
|
|
|
35
44
|
// Through the adapter, like every sibling write: the column is a
|
|
36
45
|
// `timestamptz(3)` on Postgres, which a raw epoch number is not.
|
|
37
46
|
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
38
|
-
await db.updateTable('
|
|
47
|
+
await db.updateTable('p2p_sync_peers').set(values).where('peer_did', '=', normalizeDID(peerDID)).execute();
|
|
39
48
|
},
|
|
40
49
|
async removeSyncPeer (peerDID) {
|
|
41
|
-
await db.deleteFrom('
|
|
50
|
+
await db.deleteFrom('p2p_sync_peers').where('peer_did', '=', normalizeDID(peerDID)).execute();
|
|
42
51
|
},
|
|
43
52
|
// --- Broadcast log operations ---
|
|
44
53
|
async recordBroadcast (entry) {
|
|
45
|
-
const result = await db.insertInto('
|
|
54
|
+
const result = await db.insertInto('p2p_broadcast_log').values({
|
|
46
55
|
doc_id: entry.docID,
|
|
47
56
|
version: entry.version,
|
|
48
57
|
mls_group_id: entry.mlsGroupID
|
|
@@ -54,7 +63,7 @@ export function createSyncOps(db, adapter) {
|
|
|
54
63
|
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
55
64
|
},
|
|
56
65
|
async hasBroadcast (entry) {
|
|
57
|
-
const row = await db.selectFrom('
|
|
66
|
+
const row = await db.selectFrom('p2p_broadcast_log').select('doc_id').where('doc_id', '=', entry.docID).where('version', '=', entry.version).where('mls_group_id', '=', entry.mlsGroupID).limit(1).executeTakeFirst();
|
|
58
67
|
return row != null;
|
|
59
68
|
},
|
|
60
69
|
async pruneBroadcastLog (params) {
|
|
@@ -66,7 +75,7 @@ export function createSyncOps(db, adapter) {
|
|
|
66
75
|
throw new BroadcastLogPruneParamsError('pruneBroadcastLog: limit must be a positive integer');
|
|
67
76
|
}
|
|
68
77
|
const before = adapter.encodeTimestamp(new Date(params.olderThan));
|
|
69
|
-
const result = await db.deleteFrom('
|
|
78
|
+
const result = await db.deleteFrom('p2p_broadcast_log').where((eb)=>eb(eb.refTuple('doc_id', 'version', 'mls_group_id'), 'in', eb.selectFrom('p2p_broadcast_log').select([
|
|
70
79
|
'doc_id',
|
|
71
80
|
'version',
|
|
72
81
|
'mls_group_id'
|
package/lib/scope.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StoreAPIContext } from '@kubun/db';
|
|
2
|
+
import type { Kysely } from 'kysely';
|
|
3
|
+
import type { P2PStoreTables } from './tables.js';
|
|
4
|
+
export declare const runP2PScope: unique symbol;
|
|
5
|
+
export type P2PScope = {
|
|
6
|
+
[runP2PScope]<R>(run: (db: Kysely<P2PStoreTables>, context: StoreAPIContext) => Promise<R>): Promise<R>;
|
|
7
|
+
};
|
|
8
|
+
export declare function withP2PKeyLocks<R>(context: StoreAPIContext, table: string, keys: ReadonlyArray<ReadonlyArray<string | number>>, run: (db: Kysely<P2PStoreTables>, context: StoreAPIContext) => Promise<R>): Promise<R>;
|