@kubun/store-p2p 0.19.0 → 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/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('kubun_p2p_group_mls_state').values({
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('kubun_p2p_group_mls_state').set((eb)=>({
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('kubun_p2p_group_mls_state').values({
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('kubun_p2p_group_mls_state.epoch', '=', eb.ref('excluded.epoch')).then(eb.ref('kubun_p2p_group_mls_state.sender_generation_hwm')).else(0).end(),
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('kubun_p2p_group_mls_state.epoch', '<', eb.ref('excluded.epoch')),
72
+ eb('p2p_group_mls_state.epoch', '<', eb.ref('excluded.epoch')),
65
73
  eb.and([
66
- eb('kubun_p2p_group_mls_state.epoch', '=', eb.ref('excluded.epoch')),
67
- eb('kubun_p2p_group_mls_state.state_version', '<', eb.ref('excluded.state_version'))
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('kubun_p2p_group_mls_state').select([
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('kubun_p2p_group_mls_state').set({
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('kubun_p2p_group_mls_state').set({
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('kubun_p2p_group_mls_state').selectAll().where('group_id', '=', groupID).where('device_id', '=', deviceID);
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
- return await query.executeTakeFirst();
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('kubun_p2p_group_mls_state').set((eb)=>({
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
- return await db.selectFrom('kubun_p2p_group_mls_state').selectAll().where('group_id', '=', groupID).where('is_deleted', '=', 0).execute();
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('kubun_p2p_group_mls_state').select('group_id').where('device_id', '=', deviceID).where('is_deleted', '=', 0).execute();
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('kubun_p2p_group_mls_state').set((eb)=>({
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),
@@ -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 ownAgentsAdvisoryLockIDs(ids: ReadonlyArray<bigint>): Array<bigint>;
68
- export declare function createOwnAgentsOps(db: Kysely<P2PStoreTables>, adapter: Adapter): OwnAgentsOps;
68
+ export declare function createOwnAgentsOps(db: Kysely<P2PStoreTables>, adapter: Adapter, context: StoreAPIContext): OwnAgentsOps;
@@ -1,7 +1,8 @@
1
+ import { recordClaimDigest, recordDecisionFromVerifiedCopies, recordKeyText, recordPayloadMatches, recordSignerKey, verifySignedRecord } from '@kokuin/records';
1
2
  import { normalizeDID } from '@kokuin/token';
2
- import { AbstractPostgresAdapter, chunked } from '@kubun/db-adapter';
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 row of rows){
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(copy, methods);
51
- if (op !== null && recordPayloadMatches(copy, op.claim)) valid.push({
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 ownAgentsAdvisoryLockIDs(ids) {
59
- return [
60
- ...new Set(ids)
61
- ].sort((left, right)=>left < right ? -1 : left > right ? 1 : 0);
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('kubun_p2p_own_agents_records').selectAll().where('type', '=', key.type).where('owner', '=', key.owner).where('id', '=', key.id).execute();
78
- const deleteCopy = (tx, key, hash)=>tx.deleteFrom('kubun_p2p_own_agents_records').where('type', '=', key.type).where('owner', '=', key.owner).where('id', '=', key.id).where('op_hash', '=', hash).execute();
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('kubun_p2p_access_default_revocations').values(deviceDIDs.map((did)=>({
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('kubun_p2p_access_default_revocations').select('device_did').where('controller_did', '=', controllerDID).where('device_did', '=', normalizeDID(deviceDID)).executeTakeFirst() != null;
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('kubun_p2p_binding_evidence').values([
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('kubun_p2p_binding_evidence').select('controller_did').where('device_did', '=', normalizeDID(deviceDID)).orderBy('controller_did').execute();
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('kubun_p2p_groups').set({
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('kubun_p2p_groups').select('kind').where('id', '=', groupID).executeTakeFirst();
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('kubun_p2p_groups').select('kind').where('id', '=', groupID).executeTakeFirst())?.kind;
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(key, await verifiedRows(await rowsFor(db, key), methods), 'hlc');
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('kubun_p2p_own_agents_records').selectAll().where('type', '=', type).where('id', 'in', chunk).execute();
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(key, await verifiedRows(rows, methods), 'hlc');
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('kubun_p2p_own_agents_records').selectAll().where('type', 'in', [
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(key, await verifiedRows(rows, methods), 'hlc');
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(record, methods);
201
- if (op === null || !recordPayloadMatches(record, op.claim)) throw new OwnAgentsRecordError('Invalid signed own agents copy');
202
- const signerKey = recordSignerKey(op.issuer, record.opJWT);
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(op.claim);
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(rows, methods);
216
- const winner = recordDecisionFromVerifiedCopies(key, valid, 'hlc');
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(entry.op.issuer, entry.copy.opJWT);
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(entry.op.issuer, entry.copy.opJWT);
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('kubun_p2p_own_agents_records').values({
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
- if (db.isTransaction) return await write(db);
256
- return await db.transaction().execute(write);
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 lockKeys(tx, [
263
- ownAgentsRecordLockKey(key)
264
- ]);
265
- const valid = await verifiedRows(await rowsFor(tx, key), methods);
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
- if (db.isTransaction) await write(db);
272
- else await db.transaction().execute(write);
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 lockKeys(db, keys);
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 db.insertInto('kubun_p2p_own_agents_grants').values(row).onConflict((oc)=>oc.columns([
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('kubun_p2p_own_agents_grants').selectAll();
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 kubun_p2p_own_agents_join_capabilities.token = excluded.token
320
- then ${sql.ref(`kubun_p2p_own_agents_join_capabilities.${column}`)}
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('kubun_p2p_own_agents_join_capabilities').values(row).onConflict((oc)=>{
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('kubun_p2p_own_agents_join_capabilities').where('group_id', '=', groupID).execute();
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('kubun_p2p_own_agents_join_capabilities').selectAll().where('group_id', '=', groupID).executeTakeFirst();
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 || row.signer_prefix == 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
- // The JSON results plugin also parses serialized events inside arrays.
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 { Adapter } from '@kubun/db-adapter';
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('kubun_p2p_sync_peers').values({
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
- return await db.selectFrom('kubun_p2p_sync_peers').selectAll().where('peer_did', '=', normalizeDID(peerDID)).executeTakeFirst();
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
- return await db.selectFrom('kubun_p2p_sync_peers').selectAll().execute();
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('kubun_p2p_sync_peers').set(values).where('peer_did', '=', normalizeDID(peerDID)).execute();
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('kubun_p2p_sync_peers').where('peer_did', '=', normalizeDID(peerDID)).execute();
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('kubun_p2p_broadcast_log').values({
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('kubun_p2p_broadcast_log').select('doc_id').where('doc_id', '=', entry.docID).where('version', '=', entry.version).where('mls_group_id', '=', entry.mlsGroupID).limit(1).executeTakeFirst();
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('kubun_p2p_broadcast_log').where((eb)=>eb(eb.refTuple('doc_id', 'version', 'mls_group_id'), 'in', eb.selectFrom('kubun_p2p_broadcast_log').select([
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>;