@kubun/store-p2p 0.10.1 → 0.11.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 +114 -120
- package/lib/api.js +593 -1
- package/lib/definition.js +7 -1
- package/lib/index.d.ts +1 -2
- package/lib/index.js +6 -1
- package/lib/migrations.js +105 -1
- package/lib/tables.d.ts +27 -38
- package/lib/tables.js +2 -1
- package/package.json +11 -10
- package/lib/revocation-backend.d.ts +0 -3
- package/lib/revocation-backend.js +0 -1
package/lib/api.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Adapter } from '@kubun/db-adapter';
|
|
2
|
-
import { type Logger } from '@kubun/logger';
|
|
3
2
|
import type { Kysely } from 'kysely';
|
|
4
|
-
import type { Circle, CircleMember,
|
|
3
|
+
import type { Circle, CircleMember, ControlLedgerEntry, Group, GroupMember, GroupMLSState, Hub, InsertCircle, InsertCircleMember, InsertControlLedgerEntry, InsertGroup, InsertGroupDelegation, InsertGroupHub, InsertGroupMember, InsertGroupMLSState, InsertHub, InsertSyncPeer, P2PStoreTables, PaginatedResult, PaginationParams, SyncPeer } from './tables.js';
|
|
5
4
|
export type P2PStoreAPI = {
|
|
6
5
|
createGroup(group: InsertGroup): Promise<void>;
|
|
7
6
|
getGroup(id: string): Promise<Group | undefined>;
|
|
@@ -10,6 +9,13 @@ export type P2PStoreAPI = {
|
|
|
10
9
|
listGroups(): Promise<Array<Group>>;
|
|
11
10
|
listGroupsOffsetPaged(params: PaginationParams): Promise<PaginatedResult<Group>>;
|
|
12
11
|
addGroupMember(member: InsertGroupMember): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Add (or revive) several members in a single multi-row insert. Same
|
|
14
|
+
* re-add-clears-tombstone semantics as {@link addGroupMember}, applied to every
|
|
15
|
+
* row. A single statement keeps the whole batch atomic and avoids issuing one
|
|
16
|
+
* round-trip per member on a single-connection adapter. A no-op for an empty list.
|
|
17
|
+
*/
|
|
18
|
+
addGroupMembers(members: Array<InsertGroupMember>): Promise<void>;
|
|
13
19
|
/**
|
|
14
20
|
* Soft-delete a member by tombstoning the row with `hlc`. Delegates to
|
|
15
21
|
* `markMemberRemoved`: earliest-wins LWW, ignore-if-absent. The row stays in
|
|
@@ -36,6 +42,34 @@ export type P2PStoreAPI = {
|
|
|
36
42
|
} | undefined>;
|
|
37
43
|
getGroupMember(groupID: string, memberDID: string): Promise<GroupMember | undefined>;
|
|
38
44
|
isGroupMember(groupID: string, memberDID: string): Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* Whether `did` was a member of `groupID` at the given HLC, ignoring role.
|
|
47
|
+
* Mirrors {@link isGroupAdmin}'s removal-boundary semantics without the
|
|
48
|
+
* `role = 'admin'` filter: with no `atHLC`, tombstoned rows are excluded
|
|
49
|
+
* (`removed_at_hlc IS NULL`); with `atHLC`, a member removed at HLC `R` still
|
|
50
|
+
* counts iff `atHLC < R`. Conjoined with ledger admin authority so an
|
|
51
|
+
* MLS-removed member loses admin regardless of the ledger.
|
|
52
|
+
*/
|
|
53
|
+
isGroupMemberAtHLC(groupID: string, did: string, atHLC?: string): Promise<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* Whether `did` holds an active `admin` role in `groupID`. With no `atHLC`,
|
|
56
|
+
* tombstoned rows are ignored (`removed_at_hlc IS NULL`) — the apply-time
|
|
57
|
+
* admin check. With `atHLC`, an admin removed at HLC `R` still counts iff the
|
|
58
|
+
* supplied HLC strictly precedes `R` (`atHLC < R`): a write claiming a moment
|
|
59
|
+
* before the removal is admitted, one at or after it is denied. This mirrors
|
|
60
|
+
* the membership boundary in `checkMembership` (which denies at
|
|
61
|
+
* `removed_at_hlc <= mutationHLC`), so the two checks agree on the
|
|
62
|
+
* remove-boundary instant.
|
|
63
|
+
*/
|
|
64
|
+
isGroupAdmin(groupID: string, did: string, atHLC?: string): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Overlay the role column for an existing member. Update-only: a grant for a
|
|
67
|
+
* DID with no membership row is a no-op, since membership is MLS-native and a
|
|
68
|
+
* non-member can hold no role. Never touches `hlc` or `removed_at_hlc`, so it
|
|
69
|
+
* neither churns membership LWW nor resurrects a tombstone — it is purely the
|
|
70
|
+
* projection target for the admin-roster fold.
|
|
71
|
+
*/
|
|
72
|
+
setGroupMemberRole(groupID: string, memberDID: string, role: string): Promise<void>;
|
|
39
73
|
isMemberOfAnyGroup(viewerDID: string, groupIDs: Array<string>): Promise<boolean>;
|
|
40
74
|
listGroupMembers(groupID: string): Promise<Array<GroupMember>>;
|
|
41
75
|
getGroupsForMember(memberDID: string): Promise<Array<Group>>;
|
|
@@ -104,123 +138,58 @@ export type P2PStoreAPI = {
|
|
|
104
138
|
getCirclesForMember(memberDID: string): Promise<Array<Circle>>;
|
|
105
139
|
isMemberOfAnyCircle(memberDID: string, circleIDs: Array<string>): Promise<boolean>;
|
|
106
140
|
/**
|
|
107
|
-
*
|
|
108
|
-
* `
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* and
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}): Promise<
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* `
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
*
|
|
155
|
-
* `
|
|
156
|
-
*
|
|
157
|
-
*/
|
|
158
|
-
|
|
159
|
-
getRevocation(jti: string): Promise<RevokedCapability | null>;
|
|
160
|
-
/**
|
|
161
|
-
* Hot-path check used at mutation-apply time. Returns `true` only when a
|
|
162
|
-
* row exists for `jti` AND it has been verified against the cap's issuer
|
|
163
|
-
* (`verified_at IS NOT NULL`). Pending (unverified) revocations do not
|
|
164
|
-
* gate writes; the gate fails open for light clients that do not
|
|
165
|
-
* replicate the revocations table.
|
|
166
|
-
*/
|
|
167
|
-
isRevoked(jti: string): Promise<boolean>;
|
|
168
|
-
/**
|
|
169
|
-
* Return verified revocations that target capabilities held by `audience`.
|
|
170
|
-
* INNER JOIN against `kubun_p2p_delegation_tokens` on `jti` — verified
|
|
171
|
-
* revocations whose cap row is missing locally are excluded, and
|
|
172
|
-
* `audience`/`grantor` are sourced from the cap row. Pending revocations
|
|
173
|
-
* (`verified_at IS NULL`) are not surfaced. No `atTime` filter: verified
|
|
174
|
-
* revocations remain visible to the audience until lifecycle GC removes
|
|
175
|
-
* them, even after the underlying cap has expired.
|
|
176
|
-
*/
|
|
177
|
-
getHeldRevocations(params: {
|
|
178
|
-
audience: string;
|
|
179
|
-
}): Promise<Array<HeldRevocation>>;
|
|
180
|
-
/**
|
|
181
|
-
* Flip a pending revocation to verified. Idempotent: returns `true` only
|
|
182
|
-
* when a previously-pending row was updated; returns `false` when the row
|
|
183
|
-
* is missing or already verified.
|
|
184
|
-
*/
|
|
185
|
-
markRevocationVerified(jti: string, params: {
|
|
186
|
-
cap_exp: number;
|
|
187
|
-
verified_at?: number;
|
|
188
|
-
}): Promise<boolean>;
|
|
189
|
-
getPendingRevocationByJti(jti: string): Promise<RevokedCapability | null>;
|
|
190
|
-
/**
|
|
191
|
-
* Delete a pending revocation row (`verified_at IS NULL`) for the given
|
|
192
|
-
* `jti`. Verified rows are preserved — a verified revocation is binding and
|
|
193
|
-
* must not be removed by this method (use `purgeExpiredRevocations` for
|
|
194
|
-
* lifecycle cleanup). Returns `true` when a pending row was deleted, `false`
|
|
195
|
-
* otherwise (no row, or row already verified).
|
|
196
|
-
*
|
|
197
|
-
* Used by the cap-arrival cross-check: when a held cap arrives whose issuer
|
|
198
|
-
* does not match a pre-existing pending revocation's `revoker_did`, the
|
|
199
|
-
* pending row is treated as a forged broadcast and dropped.
|
|
200
|
-
*/
|
|
201
|
-
deletePendingRevocation(jti: string): Promise<boolean>;
|
|
202
|
-
/**
|
|
203
|
-
* Delete verified revocation rows whose underlying capability expired more
|
|
204
|
-
* than `graceSeconds` ago (`cap_exp + graceSeconds < now`). Rows with a
|
|
205
|
-
* NULL `cap_exp` and pending (unverified) rows are preserved — they have
|
|
206
|
-
* no expiry to compare against. Returns the number of rows deleted.
|
|
207
|
-
*/
|
|
208
|
-
purgeExpiredRevocations(params: {
|
|
209
|
-
graceSeconds: number;
|
|
210
|
-
}): Promise<number>;
|
|
211
|
-
/**
|
|
212
|
-
* Delete pending revocation rows (`verified_at IS NULL`) whose `created_at`
|
|
213
|
-
* is older than `maxAgeSeconds`. Verified rows are preserved regardless of
|
|
214
|
-
* their `created_at` age. Returns the number of rows deleted.
|
|
215
|
-
*
|
|
216
|
-
* A negative `maxAgeSeconds` pushes the threshold into the future and will
|
|
217
|
-
* delete every pending row regardless of age — intended only for test
|
|
218
|
-
* scaffolding (avoids needing raw-SQL `UPDATE` to backdate `created_at`).
|
|
219
|
-
* Do not pass negative values from production code paths.
|
|
220
|
-
*/
|
|
221
|
-
purgeDeadPendingRevocations(params: {
|
|
222
|
-
maxAgeSeconds: number;
|
|
223
|
-
}): Promise<number>;
|
|
141
|
+
* Fold-authoritative circle projection write: unconditionally set the row for
|
|
142
|
+
* `id` to the supplied derived state. Distinct from the LWW writers —
|
|
143
|
+
* `createCircle` (insert-only), `updateCircle` (update-only, clears the
|
|
144
|
+
* tombstone) and `markCircleRemoved` (earliest-wins LWW). A reproject derives
|
|
145
|
+
* `hlc` from the winning active entry and `removedAtHLC` from the winning
|
|
146
|
+
* tombstone (or null when active), then overwrites every projected column so
|
|
147
|
+
* the row equals exactly what the ledger fold decides regardless of prior
|
|
148
|
+
* contents. A removed circle keeps a tombstone row (`removed_at_hlc` set), not
|
|
149
|
+
* a hard delete, so `includeRemoved` reads and the read-ACL joins keep working.
|
|
150
|
+
*/
|
|
151
|
+
reconcileCircleProjection(params: {
|
|
152
|
+
id: string;
|
|
153
|
+
groupID: string;
|
|
154
|
+
name: string;
|
|
155
|
+
description: string;
|
|
156
|
+
catalogIDs: Array<string>;
|
|
157
|
+
hlc: string;
|
|
158
|
+
removedAtHLC: string | null;
|
|
159
|
+
}): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Fold-authoritative circle-member projection write, the member-row analogue
|
|
162
|
+
* of {@link reconcileCircleProjection}: unconditionally upsert the
|
|
163
|
+
* `(circleID, memberDID)` row to the derived `(role, hlc, removedAtHLC)` state.
|
|
164
|
+
* A removed member keeps a tombstone row so the read-ACL joins still gate on
|
|
165
|
+
* the marker.
|
|
166
|
+
*/
|
|
167
|
+
reconcileCircleMemberProjection(params: {
|
|
168
|
+
circleID: string;
|
|
169
|
+
memberDID: string;
|
|
170
|
+
role: string;
|
|
171
|
+
hlc: string;
|
|
172
|
+
removedAtHLC: string | null;
|
|
173
|
+
}): Promise<void>;
|
|
174
|
+
/**
|
|
175
|
+
* Record the group a delegation `jti` was granted in. Idempotent on `jti`
|
|
176
|
+
* (the primary key): a re-record for a `jti` already present is a no-op. The
|
|
177
|
+
* delegation token itself lives in the core delegation store; this edge only
|
|
178
|
+
* carries the group association the p2p layer needs.
|
|
179
|
+
*/
|
|
180
|
+
addGroupDelegation(edge: InsertGroupDelegation): Promise<void>;
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the group a delegation `jti` was granted in, or `null` when no edge
|
|
183
|
+
* exists. Used to target revoke broadcasts and to enrich revocation listings
|
|
184
|
+
* with their originating group.
|
|
185
|
+
*/
|
|
186
|
+
getGroupIDByJTI(jti: string): Promise<string | null>;
|
|
187
|
+
/**
|
|
188
|
+
* Batch variant of {@link getGroupIDByJTI}: resolve the group each `jti` was
|
|
189
|
+
* granted in, returning a `Map` keyed by `jti`. A `jti` with no edge is absent
|
|
190
|
+
* from the map. Used to enrich revocation listings without an N+1 lookup.
|
|
191
|
+
*/
|
|
192
|
+
getGroupIDsByJTIs(jtis: Array<string>): Promise<Map<string, string>>;
|
|
224
193
|
saveMLSState(state: InsertGroupMLSState): Promise<void>;
|
|
225
194
|
getMLSState(groupID: string, deviceID: string): Promise<GroupMLSState | undefined>;
|
|
226
195
|
deleteMLSState(groupID: string, deviceID: string): Promise<void>;
|
|
@@ -241,6 +210,31 @@ export type P2PStoreAPI = {
|
|
|
241
210
|
version: string;
|
|
242
211
|
mlsGroupID: string;
|
|
243
212
|
}): Promise<boolean>;
|
|
213
|
+
/**
|
|
214
|
+
* Append a verified ledger entry. Idempotent on `entry_id` (the token
|
|
215
|
+
* digest): a re-delivered entry is a no-op. `issuer_did` is normalized on
|
|
216
|
+
* write. Returns `true` when a new row was inserted, `false` on a duplicate.
|
|
217
|
+
*/
|
|
218
|
+
appendLedgerEntry(entry: InsertControlLedgerEntry): Promise<boolean>;
|
|
219
|
+
/** All ledger entries for a group, ordered by `(hlc, entry_id)`. */
|
|
220
|
+
getLedgerEntries(groupID: string): Promise<Array<ControlLedgerEntry>>;
|
|
221
|
+
/** Ledger entries for a group filtered by `type`, ordered by `(hlc, entry_id)`. */
|
|
222
|
+
getLedgerEntriesByType(groupID: string, type: string): Promise<Array<ControlLedgerEntry>>;
|
|
223
|
+
/**
|
|
224
|
+
* The HLC through which `projection` has been folded for `groupID`, or `null`
|
|
225
|
+
* when no projection has run yet. A caller treats an entry whose HLC is `>`
|
|
226
|
+
* this value as in-order (apply incrementally); `<=` or `null` means the
|
|
227
|
+
* cached projection is no longer a correct fold-through-this-entry and must be
|
|
228
|
+
* rebuilt from the whole ledger.
|
|
229
|
+
*/
|
|
230
|
+
getProjectionWatermark(groupID: string, projection: string): Promise<string | null>;
|
|
231
|
+
/**
|
|
232
|
+
* Set the `(groupID, projection)` watermark to `hlc`. Overwrite-on-conflict:
|
|
233
|
+
* the row is unconditionally replaced. Callers must only advance the watermark
|
|
234
|
+
* — the incremental path passes a value strictly newer than the current one,
|
|
235
|
+
* and the reproject path passes the true max over the folded ledger.
|
|
236
|
+
*/
|
|
237
|
+
setProjectionWatermark(groupID: string, projection: string, hlc: string): Promise<void>;
|
|
244
238
|
insertHub(hub: InsertHub): Promise<void>;
|
|
245
239
|
getHub(id: string): Promise<Hub | undefined>;
|
|
246
240
|
getHubByURL(url: string): Promise<Hub | undefined>;
|
|
@@ -257,4 +251,4 @@ export type P2PStoreAPI = {
|
|
|
257
251
|
listGroupsByHubID(hubID: string): Promise<Array<Group>>;
|
|
258
252
|
deleteGroupHubsByHubID(hubID: string): Promise<void>;
|
|
259
253
|
};
|
|
260
|
-
export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter
|
|
254
|
+
export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter): P2PStoreAPI;
|
package/lib/api.js
CHANGED
|
@@ -1 +1,593 @@
|
|
|
1
|
-
import{getKubunLogger as e}from"@kubun/logger";export function createP2PStore(r,t,u=e("store-p2p")){let i={async createGroup(e){await r.insertInto("kubun_p2p_groups").values({id:e.id,name:e.name,description:e.description,created_by:e.created_by,hlc:e.hlc}).execute()},getGroup:async e=>await r.selectFrom("kubun_p2p_groups").selectAll().where("id","=",e).executeTakeFirst(),async updateGroup(e,u){let i={};null!=u.name&&(i.name=u.name),null!=u.description&&(i.description=u.description),null!=u.hlc&&(i.hlc=u.hlc),i.updated_at=t.encodeTimestamp(new Date),await r.updateTable("kubun_p2p_groups").set(i).where("id","=",e).execute()},async deleteGroup(e){await r.deleteFrom("kubun_p2p_groups").where("id","=",e).execute()},listGroups:async()=>await r.selectFrom("kubun_p2p_groups").selectAll().execute(),async listGroupsOffsetPaged(e){let{limit:t,offset:u}=e,i=r.selectFrom("kubun_p2p_groups").selectAll().orderBy("name");if(null!=u&&(i=i.offset(u)),null!=t){i=i.limit(t+1);let e=await i.execute(),r=e.length>t;return{entries:r?e.slice(0,t):e,hasMore:r}}return{entries:await i.execute(),hasMore:!1}},async addGroupMember(e){await r.insertInto("kubun_p2p_group_members").values({group_id:e.group_id,member_did:e.member_did,role:e.role,hlc:e.hlc}).onConflict(e=>e.columns(["group_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc"),removed_at_hlc:null,updated_at:t.encodeTimestamp(new Date)}))).execute()},async markMemberRemoved(e,u,i){await r.updateTable("kubun_p2p_group_members").set({removed_at_hlc:i,updated_at:t.encodeTimestamp(new Date)}).where("group_id","=",e).where("member_did","=",u).where(e=>e.or([e("removed_at_hlc","is",null),e("removed_at_hlc",">",i)])).execute()},async getMemberRemoval(e,t){let u=await r.selectFrom("kubun_p2p_group_members").select("removed_at_hlc").where("group_id","=",e).where("member_did","=",t).executeTakeFirst();return null==u?void 0:{removed_at_hlc:u.removed_at_hlc}},async removeGroupMember(e,r,t){await i.markMemberRemoved(e,r,t)},getGroupMember:async(e,t)=>await r.selectFrom("kubun_p2p_group_members").selectAll().where("group_id","=",e).where("member_did","=",t).where("removed_at_hlc","is",null).executeTakeFirst(),isGroupMember:async(e,t)=>null!=await r.selectFrom("kubun_p2p_group_members").select("member_did").where("group_id","=",e).where("member_did","=",t).where("removed_at_hlc","is",null).executeTakeFirst(),isMemberOfAnyGroup:async(e,t)=>0!==t.length&&null!=await r.selectFrom("kubun_p2p_group_members").select("group_id").where("member_did","=",e).where("group_id","in",t).where("removed_at_hlc","is",null).limit(1).executeTakeFirst(),listGroupMembers:async e=>await r.selectFrom("kubun_p2p_group_members").selectAll().where("group_id","=",e).where("removed_at_hlc","is",null).execute(),getGroupsForMember:async e=>await r.selectFrom("kubun_p2p_group_members").innerJoin("kubun_p2p_groups","kubun_p2p_groups.id","kubun_p2p_group_members.group_id").selectAll("kubun_p2p_groups").where("kubun_p2p_group_members.member_did","=",e).where("kubun_p2p_group_members.removed_at_hlc","is",null).execute(),async getGroupsForMembers(e){let t=new Map;if(0===e.length)return t;for(let u of(await r.selectFrom("kubun_p2p_group_members").innerJoin("kubun_p2p_groups","kubun_p2p_groups.id","kubun_p2p_group_members.group_id").select(["kubun_p2p_group_members.member_did as member_did","kubun_p2p_groups.id as id","kubun_p2p_groups.name as name","kubun_p2p_groups.description as description","kubun_p2p_groups.created_by as created_by","kubun_p2p_groups.hlc as hlc","kubun_p2p_groups.created_at as created_at","kubun_p2p_groups.updated_at as updated_at"]).where("kubun_p2p_group_members.member_did","in",e).where("kubun_p2p_group_members.removed_at_hlc","is",null).execute())){let{member_did:e,...r}=u,i=t.get(e);null!=i?i.push(r):t.set(e,[r])}return t},async createCircle(e){await r.insertInto("kubun_p2p_circles").values({id:e.id,group_id:e.group_id,name:e.name,description:e.description,catalog_ids:t.encodeJSON(e.catalog_ids),hlc:e.hlc}).execute()},async getCircle(e,t){let u=r.selectFrom("kubun_p2p_circles").selectAll().where("id","=",e);return t?.includeRemoved||(u=u.where("removed_at_hlc","is",null)),await u.executeTakeFirst()},async getCircles(e){let t=new Map;if(0===e.length)return t;for(let u of(await r.selectFrom("kubun_p2p_circles").selectAll().where("id","in",e).execute()))t.set(u.id,u);return t},async updateCircle(e,u){let i={};null!=u.name&&(i.name=u.name),null!=u.description&&(i.description=u.description),null!=u.catalog_ids&&(i.catalog_ids=t.encodeJSON(u.catalog_ids)),null!=u.hlc&&(i.hlc=u.hlc,i.removed_at_hlc=null),i.updated_at=t.encodeTimestamp(new Date),await r.updateTable("kubun_p2p_circles").set(i).where("id","=",e).execute()},async deleteCircle(e){await r.deleteFrom("kubun_p2p_circles").where("id","=",e).execute()},async markCircleRemoved(e,u){await r.updateTable("kubun_p2p_circles").set({removed_at_hlc:u,updated_at:t.encodeTimestamp(new Date)}).where("id","=",e).where(e=>e.or([e("removed_at_hlc","is",null),e("removed_at_hlc",">",u)])).execute()},async listCirclesByGroup(e,t){let u=r.selectFrom("kubun_p2p_circles").selectAll().where("group_id","=",e);return t?.includeRemoved||(u=u.where("removed_at_hlc","is",null)),await u.execute()},async addCircleMember(e){await r.insertInto("kubun_p2p_circle_members").values({circle_id:e.circle_id,member_did:e.member_did,role:e.role,hlc:e.hlc}).onConflict(e=>e.columns(["circle_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc"),removed_at_hlc:null,updated_at:t.encodeTimestamp(new Date)}))).execute()},async removeCircleMember(e,u,i){await r.updateTable("kubun_p2p_circle_members").set({removed_at_hlc:i,updated_at:t.encodeTimestamp(new Date)}).where("circle_id","=",e).where("member_did","=",u).where(e=>e.or([e("removed_at_hlc","is",null),e("removed_at_hlc",">",i)])).execute()},getCircleMember:async(e,t)=>await r.selectFrom("kubun_p2p_circle_members").selectAll().where("circle_id","=",e).where("member_did","=",t).executeTakeFirst(),async listCircleMembers(e,t){let u=r.selectFrom("kubun_p2p_circle_members").selectAll().where("circle_id","=",e);return t?.includeRemoved||(u=u.where("removed_at_hlc","is",null)),await u.execute()},getCirclesForMember:async e=>await r.selectFrom("kubun_p2p_circle_members").innerJoin("kubun_p2p_circles","kubun_p2p_circles.id","kubun_p2p_circle_members.circle_id").selectAll("kubun_p2p_circles").where("kubun_p2p_circle_members.member_did","=",e).where("kubun_p2p_circle_members.removed_at_hlc","is",null).where("kubun_p2p_circles.removed_at_hlc","is",null).execute(),isMemberOfAnyCircle:async(e,t)=>0!==t.length&&null!=await r.selectFrom("kubun_p2p_circle_members").innerJoin("kubun_p2p_circles","kubun_p2p_circles.id","kubun_p2p_circle_members.circle_id").select("kubun_p2p_circle_members.circle_id").where("kubun_p2p_circle_members.member_did","=",e).where("kubun_p2p_circle_members.circle_id","in",t).where("kubun_p2p_circle_members.removed_at_hlc","is",null).where("kubun_p2p_circles.removed_at_hlc","is",null).limit(1).executeTakeFirst(),async addDelegationToken(e){let a,_=await r.selectFrom("kubun_p2p_delegation_tokens").select(["jti","token","act","exp","group_id","hlc"]).where("grantor","=",e.grantor).where("audience","=",e.audience).where("resource","=",e.resource).executeTakeFirst();if(await r.insertInto("kubun_p2p_delegation_tokens").values({jti:e.jti,grantor:e.grantor,audience:e.audience,token:e.token,resource:e.resource,act:e.act,exp:e.exp,group_id:e.group_id,hlc:e.hlc}).onConflict(e=>e.columns(["grantor","audience","resource"]).doUpdateSet(e=>({jti:e.ref("excluded.jti"),token:e.ref("excluded.token"),act:e.ref("excluded.act"),exp:e.ref("excluded.exp"),group_id:e.ref("excluded.group_id"),hlc:e.ref("excluded.hlc"),updated_at:t.encodeTimestamp(new Date)})).where(e=>e("excluded.hlc",">",e.ref("kubun_p2p_delegation_tokens.hlc")))).execute(),a=null==_||!(e.hlc<=_.hlc)&&(_.jti!==e.jti||_.token!==e.token||_.act!==e.act||_.exp!==e.exp||_.group_id!==e.group_id),.01>Math.random())try{await i.purgeExpiredRevocations({graceSeconds:2592e3}),await i.purgeDeadPendingRevocations({maxAgeSeconds:604800})}catch(e){u.warn("revocation GC sweep failed during addDelegationToken: {error}",{error:e})}return a},getDelegationTokens:async e=>await r.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("grantor","=",e.grantor).where("audience","=",e.audience).execute(),getHeldTokens:async e=>await r.selectFrom("kubun_p2p_delegation_tokens").leftJoin("kubun_p2p_revoked_capabilities","kubun_p2p_revoked_capabilities.jti","kubun_p2p_delegation_tokens.jti").selectAll("kubun_p2p_delegation_tokens").where("audience","=",e.audience).where("exp",">",e.atTime).where(e=>e.or([e("kubun_p2p_revoked_capabilities.jti","is",null),e("kubun_p2p_revoked_capabilities.verified_at","is",null)])).execute(),listIssuedTokens:async e=>await r.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("grantor","=",e).execute(),getDelegationTokenByJti:async e=>await r.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("jti","=",e).executeTakeFirst()??null,async addRevocation(e){let u=await r.selectFrom("kubun_p2p_revoked_capabilities").select(["revoker_did","revoked_iat","revocation_token","verified_at","cap_exp","group_id","hlc"]).where("jti","=",e.jti).executeTakeFirst();if(await r.insertInto("kubun_p2p_revoked_capabilities").values({jti:e.jti,revoker_did:e.revoker_did,revoked_iat:e.revoked_iat,revocation_token:e.revocation_token,verified_at:e.verified_at??null,cap_exp:e.cap_exp??null,group_id:e.group_id,hlc:e.hlc}).onConflict(e=>e.column("jti").doUpdateSet(e=>({revoker_did:e.ref("excluded.revoker_did"),revoked_iat:e.ref("excluded.revoked_iat"),revocation_token:e.ref("excluded.revocation_token"),verified_at:e.ref("excluded.verified_at"),cap_exp:e.ref("excluded.cap_exp"),group_id:e.ref("excluded.group_id"),hlc:e.ref("excluded.hlc"),updated_at:t.encodeTimestamp(new Date)})).where(e=>e("excluded.hlc",">",e.ref("kubun_p2p_revoked_capabilities.hlc")))).execute(),null==u)return!0;if(e.hlc<=u.hlc)return!1;let i=e.verified_at??null,a=e.cap_exp??null;return u.revoker_did!==e.revoker_did||u.revoked_iat!==e.revoked_iat||u.revocation_token!==e.revocation_token||u.verified_at!==i||u.cap_exp!==a||u.group_id!==e.group_id},getRevocation:async e=>await r.selectFrom("kubun_p2p_revoked_capabilities").selectAll().where("jti","=",e).executeTakeFirst()??null,isRevoked:async e=>null!=await r.selectFrom("kubun_p2p_revoked_capabilities").select("jti").where("jti","=",e).where("verified_at","is not",null).limit(1).executeTakeFirst(),getHeldRevocations:async e=>(await r.selectFrom("kubun_p2p_revoked_capabilities").innerJoin("kubun_p2p_delegation_tokens","kubun_p2p_delegation_tokens.jti","kubun_p2p_revoked_capabilities.jti").select(["kubun_p2p_revoked_capabilities.jti as jti","kubun_p2p_delegation_tokens.grantor as grantor","kubun_p2p_delegation_tokens.audience as audience","kubun_p2p_revoked_capabilities.revoker_did as revoker_did","kubun_p2p_revoked_capabilities.revoked_iat as revoked_iat","kubun_p2p_revoked_capabilities.verified_at as verified_at","kubun_p2p_revoked_capabilities.cap_exp as cap_exp","kubun_p2p_revoked_capabilities.group_id as group_id"]).where("kubun_p2p_revoked_capabilities.verified_at","is not",null).where("kubun_p2p_delegation_tokens.audience","=",e.audience).execute()).map(e=>({jti:e.jti,grantor:e.grantor,audience:e.audience,revoker_did:e.revoker_did,revoked_iat:e.revoked_iat,verified_at:e.verified_at,cap_exp:e.cap_exp,group_id:e.group_id})),async markRevocationVerified(e,u){let i=u.verified_at??Math.floor(Date.now()/1e3);return((await r.updateTable("kubun_p2p_revoked_capabilities").set({verified_at:i,cap_exp:u.cap_exp,updated_at:t.encodeTimestamp(new Date)}).where("jti","=",e).where("verified_at","is",null).executeTakeFirst()).numUpdatedRows??0n)>0n},getPendingRevocationByJti:async e=>await r.selectFrom("kubun_p2p_revoked_capabilities").selectAll().where("jti","=",e).where("verified_at","is",null).executeTakeFirst()??null,deletePendingRevocation:async e=>((await r.deleteFrom("kubun_p2p_revoked_capabilities").where("jti","=",e).where("verified_at","is",null).executeTakeFirst()).numDeletedRows??0n)>0n,async purgeExpiredRevocations(e){let t=Math.floor(Date.now()/1e3)-e.graceSeconds;return Number((await r.deleteFrom("kubun_p2p_revoked_capabilities").where("verified_at","is not",null).where("cap_exp","is not",null).where("cap_exp","<",t).executeTakeFirst()).numDeletedRows??0n)},async purgeDeadPendingRevocations(e){let u=t.encodeTimestamp(new Date(Date.now()-1e3*e.maxAgeSeconds));return Number((await r.deleteFrom("kubun_p2p_revoked_capabilities").where("verified_at","is",null).where("created_at","<",u).executeTakeFirst()).numDeletedRows??0n)},async saveMLSState(e){await r.insertInto("kubun_p2p_group_mls_state").values({group_id:e.group_id,device_id:e.device_id,mls_state:t.encodeBinary(e.mls_state),credential:e.credential,epoch:e.epoch,root_capability:e.root_capability}).onConflict(e=>e.columns(["group_id","device_id"]).doUpdateSet(e=>({mls_state:e.ref("excluded.mls_state"),credential:e.ref("excluded.credential"),epoch:e.ref("excluded.epoch"),root_capability:e.ref("excluded.root_capability"),updated_at:t.encodeTimestamp(new Date)}))).execute()},getMLSState:async(e,t)=>await r.selectFrom("kubun_p2p_group_mls_state").selectAll().where("group_id","=",e).where("device_id","=",t).executeTakeFirst(),async deleteMLSState(e,t){await r.deleteFrom("kubun_p2p_group_mls_state").where("group_id","=",e).where("device_id","=",t).execute()},listMLSStates:async e=>await r.selectFrom("kubun_p2p_group_mls_state").selectAll().where("group_id","=",e).execute(),async deleteGroupMLSStates(e){await r.deleteFrom("kubun_p2p_group_mls_state").where("group_id","=",e).execute()},async addSyncPeer(e){let u=t.encodeJSON(e.allowed_users),i=t.encodeJSON(e.config);await r.insertInto("kubun_p2p_sync_peers").values({...e,allowed_users:u,config:i}).onConflict(r=>r.column("peer_did").doUpdateSet({endpoint:e.endpoint,mode:e.mode,allowed_users:u,priority:e.priority,trust_level:e.trust_level,config:i,updated_at:e.updated_at})).execute()},getSyncPeer:async e=>await r.selectFrom("kubun_p2p_sync_peers").selectAll().where("peer_did","=",e).executeTakeFirst(),listSyncPeers:async()=>await r.selectFrom("kubun_p2p_sync_peers").selectAll().execute(),async updateSyncPeer(e,u){let i={};null!=u.endpoint&&(i.endpoint=u.endpoint),null!=u.mode&&(i.mode=u.mode),null!=u.allowed_users&&(i.allowed_users=t.encodeJSON(u.allowed_users)),null!=u.priority&&(i.priority=u.priority),null!=u.trust_level&&(i.trust_level=u.trust_level),null!=u.config&&(i.config=t.encodeJSON(u.config)),i.updated_at=Date.now(),await r.updateTable("kubun_p2p_sync_peers").set(i).where("peer_did","=",e).execute()},async removeSyncPeer(e){await r.deleteFrom("kubun_p2p_sync_peers").where("peer_did","=",e).execute()},recordBroadcast:async e=>((await r.insertInto("kubun_p2p_broadcast_log").values({doc_id:e.docID,version:e.version,mls_group_id:e.mlsGroupID}).onConflict(e=>e.columns(["doc_id","version","mls_group_id"]).doNothing()).executeTakeFirst()).numInsertedOrUpdatedRows??0n)>0n,hasBroadcast:async e=>null!=await r.selectFrom("kubun_p2p_broadcast_log").select("doc_id").where("doc_id","=",e.docID).where("version","=",e.version).where("mls_group_id","=",e.mlsGroupID).limit(1).executeTakeFirst(),async insertHub(e){await r.insertInto("kubun_p2p_hubs").values({id:e.id,label:e.label,url:e.url,server_did:e.server_did}).execute()},getHub:async e=>await r.selectFrom("kubun_p2p_hubs").selectAll().where("id","=",e).executeTakeFirst(),getHubByURL:async e=>await r.selectFrom("kubun_p2p_hubs").selectAll().where("url","=",e).executeTakeFirst(),async updateHub(e,u){let i={};void 0!==u.label&&(i.label=u.label),void 0!==u.url&&(i.url=u.url),void 0!==u.server_did&&(i.server_did=u.server_did),i.updated_at=t.encodeTimestamp(new Date),await r.updateTable("kubun_p2p_hubs").set(i).where("id","=",e).execute()},async deleteHub(e){await r.deleteFrom("kubun_p2p_hubs").where("id","=",e).execute()},listHubs:async()=>await r.selectFrom("kubun_p2p_hubs").selectAll().execute(),insertGroupHub:async e=>((await r.insertInto("kubun_p2p_group_hubs").values({group_id:e.group_id,hub_id:e.hub_id}).onConflict(e=>e.columns(["group_id","hub_id"]).doNothing()).executeTakeFirst()).numInsertedOrUpdatedRows??0n)>0n,deleteGroupHub:async e=>((await r.deleteFrom("kubun_p2p_group_hubs").where("group_id","=",e.groupID).where("hub_id","=",e.hubID).executeTakeFirst()).numDeletedRows??0n)>0n,listGroupIDsByHubID:async e=>(await r.selectFrom("kubun_p2p_group_hubs").select("group_id").where("hub_id","=",e).execute()).map(e=>e.group_id),listHubsByGroupID:async e=>await r.selectFrom("kubun_p2p_group_hubs").innerJoin("kubun_p2p_hubs","kubun_p2p_hubs.id","kubun_p2p_group_hubs.hub_id").selectAll("kubun_p2p_hubs").where("kubun_p2p_group_hubs.group_id","=",e).execute(),listGroupsByHubID:async e=>await r.selectFrom("kubun_p2p_group_hubs").innerJoin("kubun_p2p_groups","kubun_p2p_groups.id","kubun_p2p_group_hubs.group_id").select(["kubun_p2p_groups.id as id","kubun_p2p_groups.name as name","kubun_p2p_groups.description as description","kubun_p2p_groups.created_by as created_by","kubun_p2p_groups.hlc as hlc","kubun_p2p_groups.created_at as created_at","kubun_p2p_groups.updated_at as updated_at"]).where("kubun_p2p_group_hubs.hub_id","=",e).execute(),async deleteGroupHubsByHubID(e){await r.deleteFrom("kubun_p2p_group_hubs").where("hub_id","=",e).execute()}};return i}
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
// --- Factory function ---
|
|
3
|
+
export function createP2PStore(db, adapter) {
|
|
4
|
+
const api = {
|
|
5
|
+
// --- Group operations ---
|
|
6
|
+
async createGroup (group) {
|
|
7
|
+
await db.insertInto('kubun_p2p_groups').values({
|
|
8
|
+
id: group.id,
|
|
9
|
+
name: group.name,
|
|
10
|
+
description: group.description,
|
|
11
|
+
created_by: group.created_by,
|
|
12
|
+
hlc: group.hlc
|
|
13
|
+
}).execute();
|
|
14
|
+
},
|
|
15
|
+
async getGroup (id) {
|
|
16
|
+
return await db.selectFrom('kubun_p2p_groups').selectAll().where('id', '=', id).executeTakeFirst();
|
|
17
|
+
},
|
|
18
|
+
async updateGroup (id, update) {
|
|
19
|
+
const values = {};
|
|
20
|
+
if (update.name != null) values.name = update.name;
|
|
21
|
+
if (update.description != null) values.description = update.description;
|
|
22
|
+
if (update.hlc != null) values.hlc = update.hlc;
|
|
23
|
+
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
24
|
+
await db.updateTable('kubun_p2p_groups').set(values).where('id', '=', id).execute();
|
|
25
|
+
},
|
|
26
|
+
async deleteGroup (id) {
|
|
27
|
+
await db.deleteFrom('kubun_p2p_groups').where('id', '=', id).execute();
|
|
28
|
+
},
|
|
29
|
+
async listGroups () {
|
|
30
|
+
return await db.selectFrom('kubun_p2p_groups').selectAll().execute();
|
|
31
|
+
},
|
|
32
|
+
async listGroupsOffsetPaged (params) {
|
|
33
|
+
const { limit, offset } = params;
|
|
34
|
+
let query = db.selectFrom('kubun_p2p_groups').selectAll().orderBy('name');
|
|
35
|
+
if (offset != null) {
|
|
36
|
+
query = query.offset(offset);
|
|
37
|
+
}
|
|
38
|
+
if (limit != null) {
|
|
39
|
+
query = query.limit(limit + 1);
|
|
40
|
+
const rows = await query.execute();
|
|
41
|
+
const hasMore = rows.length > limit;
|
|
42
|
+
return {
|
|
43
|
+
entries: hasMore ? rows.slice(0, limit) : rows,
|
|
44
|
+
hasMore
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const rows = await query.execute();
|
|
48
|
+
return {
|
|
49
|
+
entries: rows,
|
|
50
|
+
hasMore: false
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
// --- Group member operations ---
|
|
54
|
+
async addGroupMember (member) {
|
|
55
|
+
// Re-adding a previously-removed member clears the tombstone and refreshes
|
|
56
|
+
// the row's HLC, so a member that left and rejoined is active again.
|
|
57
|
+
await db.insertInto('kubun_p2p_group_members').values({
|
|
58
|
+
group_id: member.group_id,
|
|
59
|
+
member_did: normalizeDID(member.member_did),
|
|
60
|
+
role: member.role,
|
|
61
|
+
hlc: member.hlc
|
|
62
|
+
}).onConflict((oc)=>oc.columns([
|
|
63
|
+
'group_id',
|
|
64
|
+
'member_did'
|
|
65
|
+
]).doUpdateSet((eb)=>({
|
|
66
|
+
role: eb.ref('excluded.role'),
|
|
67
|
+
hlc: eb.ref('excluded.hlc'),
|
|
68
|
+
removed_at_hlc: null,
|
|
69
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
70
|
+
}))).execute();
|
|
71
|
+
},
|
|
72
|
+
async addGroupMembers (members) {
|
|
73
|
+
if (members.length === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await db.insertInto('kubun_p2p_group_members').values(members.map((member)=>({
|
|
77
|
+
group_id: member.group_id,
|
|
78
|
+
member_did: normalizeDID(member.member_did),
|
|
79
|
+
role: member.role,
|
|
80
|
+
hlc: member.hlc
|
|
81
|
+
}))).onConflict((oc)=>oc.columns([
|
|
82
|
+
'group_id',
|
|
83
|
+
'member_did'
|
|
84
|
+
]).doUpdateSet((eb)=>({
|
|
85
|
+
role: eb.ref('excluded.role'),
|
|
86
|
+
hlc: eb.ref('excluded.hlc'),
|
|
87
|
+
removed_at_hlc: null,
|
|
88
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
89
|
+
}))).execute();
|
|
90
|
+
},
|
|
91
|
+
// Earliest-wins LWW on `removed_at_hlc`: the update lands only when the row
|
|
92
|
+
// exists AND it is either not yet tombstoned or the incoming `hlc` precedes
|
|
93
|
+
// the stored removal HLC. A removal for a `(groupID, memberDID)` with no
|
|
94
|
+
// local row is ignored — there is no member to tombstone, and creating a
|
|
95
|
+
// row would leak a phantom member. The string `<` compare mirrors the SQL
|
|
96
|
+
// LWW used by the delegation-token paths: the serialized HLC form is
|
|
97
|
+
// lexicographically ordered, so SQL ordering matches HLC ordering.
|
|
98
|
+
async markMemberRemoved (groupID, memberDID, hlc) {
|
|
99
|
+
await db.updateTable('kubun_p2p_group_members').set({
|
|
100
|
+
removed_at_hlc: hlc,
|
|
101
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
102
|
+
}).where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where((eb)=>eb.or([
|
|
103
|
+
eb('removed_at_hlc', 'is', null),
|
|
104
|
+
eb('removed_at_hlc', '>', hlc)
|
|
105
|
+
])).execute();
|
|
106
|
+
},
|
|
107
|
+
async getMemberRemoval (groupID, memberDID) {
|
|
108
|
+
const row = await db.selectFrom('kubun_p2p_group_members').select('removed_at_hlc').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
|
|
109
|
+
return row == null ? undefined : {
|
|
110
|
+
removed_at_hlc: row.removed_at_hlc
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
async removeGroupMember (groupID, memberDID, hlc) {
|
|
114
|
+
await api.markMemberRemoved(groupID, memberDID, hlc);
|
|
115
|
+
},
|
|
116
|
+
async getGroupMember (groupID, memberDID) {
|
|
117
|
+
return await db.selectFrom('kubun_p2p_group_members').selectAll().where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).executeTakeFirst();
|
|
118
|
+
},
|
|
119
|
+
async isGroupMember (groupID, memberDID) {
|
|
120
|
+
const row = await db.selectFrom('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).executeTakeFirst();
|
|
121
|
+
return row != null;
|
|
122
|
+
},
|
|
123
|
+
async isGroupMemberAtHLC (groupID, did, atHLC) {
|
|
124
|
+
let query = db.selectFrom('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(did));
|
|
125
|
+
if (atHLC == null) {
|
|
126
|
+
query = query.where('removed_at_hlc', 'is', null);
|
|
127
|
+
} else {
|
|
128
|
+
query = query.where((eb)=>eb.or([
|
|
129
|
+
eb('removed_at_hlc', 'is', null),
|
|
130
|
+
eb('removed_at_hlc', '>', atHLC)
|
|
131
|
+
]));
|
|
132
|
+
}
|
|
133
|
+
const row = await query.limit(1).executeTakeFirst();
|
|
134
|
+
return row != null;
|
|
135
|
+
},
|
|
136
|
+
async isGroupAdmin (groupID, did, atHLC) {
|
|
137
|
+
let query = db.selectFrom('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(did)).where('role', '=', 'admin');
|
|
138
|
+
if (atHLC == null) {
|
|
139
|
+
query = query.where('removed_at_hlc', 'is', null);
|
|
140
|
+
} else {
|
|
141
|
+
// Admit a write whose HLC strictly precedes the removal HLC; deny
|
|
142
|
+
// at-or-after. Complements `checkMembership`'s `removed_at_hlc <= hlc`
|
|
143
|
+
// denial so both agree on the remove-boundary instant.
|
|
144
|
+
query = query.where((eb)=>eb.or([
|
|
145
|
+
eb('removed_at_hlc', 'is', null),
|
|
146
|
+
eb('removed_at_hlc', '>', atHLC)
|
|
147
|
+
]));
|
|
148
|
+
}
|
|
149
|
+
const row = await query.limit(1).executeTakeFirst();
|
|
150
|
+
return row != null;
|
|
151
|
+
},
|
|
152
|
+
async setGroupMemberRole (groupID, memberDID, role) {
|
|
153
|
+
// Update-only by design: a missing row leaves the table untouched, so a
|
|
154
|
+
// role grant can never conjure a phantom member. hlc/removed_at_hlc are
|
|
155
|
+
// left alone to keep membership LWW and tombstones intact.
|
|
156
|
+
await db.updateTable('kubun_p2p_group_members').set({
|
|
157
|
+
role,
|
|
158
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
159
|
+
}).where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).execute();
|
|
160
|
+
},
|
|
161
|
+
async isMemberOfAnyGroup (viewerDID, groupIDs) {
|
|
162
|
+
if (groupIDs.length === 0) return false;
|
|
163
|
+
const row = await db.selectFrom('kubun_p2p_group_members').select('group_id').where('member_did', '=', normalizeDID(viewerDID)).where('group_id', 'in', groupIDs).where('removed_at_hlc', 'is', null).limit(1).executeTakeFirst();
|
|
164
|
+
return row != null;
|
|
165
|
+
},
|
|
166
|
+
async listGroupMembers (groupID) {
|
|
167
|
+
return await db.selectFrom('kubun_p2p_group_members').selectAll().where('group_id', '=', groupID).where('removed_at_hlc', 'is', null).execute();
|
|
168
|
+
},
|
|
169
|
+
async getGroupsForMember (memberDID) {
|
|
170
|
+
return await db.selectFrom('kubun_p2p_group_members').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_members.group_id').selectAll('kubun_p2p_groups').where('kubun_p2p_group_members.member_did', '=', normalizeDID(memberDID)).where('kubun_p2p_group_members.removed_at_hlc', 'is', null).execute();
|
|
171
|
+
},
|
|
172
|
+
// Result Map is keyed by the normalized stored `member_did`; callers must
|
|
173
|
+
// normalize before looking up by key (existing callers iterate `.values()`).
|
|
174
|
+
async getGroupsForMembers (memberDIDs) {
|
|
175
|
+
const result = new Map();
|
|
176
|
+
if (memberDIDs.length === 0) return result;
|
|
177
|
+
const normalizedDIDs = memberDIDs.map(normalizeDID);
|
|
178
|
+
const rows = await db.selectFrom('kubun_p2p_group_members').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_members.group_id').select([
|
|
179
|
+
'kubun_p2p_group_members.member_did as member_did',
|
|
180
|
+
'kubun_p2p_groups.id as id',
|
|
181
|
+
'kubun_p2p_groups.name as name',
|
|
182
|
+
'kubun_p2p_groups.description as description',
|
|
183
|
+
'kubun_p2p_groups.created_by as created_by',
|
|
184
|
+
'kubun_p2p_groups.hlc as hlc',
|
|
185
|
+
'kubun_p2p_groups.created_at as created_at',
|
|
186
|
+
'kubun_p2p_groups.updated_at as updated_at'
|
|
187
|
+
]).where('kubun_p2p_group_members.member_did', 'in', normalizedDIDs).where('kubun_p2p_group_members.removed_at_hlc', 'is', null).execute();
|
|
188
|
+
for (const row of rows){
|
|
189
|
+
const { member_did, ...group } = row;
|
|
190
|
+
const list = result.get(member_did);
|
|
191
|
+
if (list != null) {
|
|
192
|
+
list.push(group);
|
|
193
|
+
} else {
|
|
194
|
+
result.set(member_did, [
|
|
195
|
+
group
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
},
|
|
201
|
+
// --- Circle operations ---
|
|
202
|
+
async createCircle (circle) {
|
|
203
|
+
await db.insertInto('kubun_p2p_circles').values({
|
|
204
|
+
id: circle.id,
|
|
205
|
+
group_id: circle.group_id,
|
|
206
|
+
name: circle.name,
|
|
207
|
+
description: circle.description,
|
|
208
|
+
catalog_ids: adapter.encodeJSON(circle.catalog_ids),
|
|
209
|
+
hlc: circle.hlc
|
|
210
|
+
}).execute();
|
|
211
|
+
},
|
|
212
|
+
async getCircle (id, options) {
|
|
213
|
+
let query = db.selectFrom('kubun_p2p_circles').selectAll().where('id', '=', id);
|
|
214
|
+
if (!options?.includeRemoved) {
|
|
215
|
+
query = query.where('removed_at_hlc', 'is', null);
|
|
216
|
+
}
|
|
217
|
+
return await query.executeTakeFirst();
|
|
218
|
+
},
|
|
219
|
+
async getCircles (ids) {
|
|
220
|
+
const result = new Map();
|
|
221
|
+
if (ids.length === 0) return result;
|
|
222
|
+
const rows = await db.selectFrom('kubun_p2p_circles').selectAll().where('id', 'in', ids).execute();
|
|
223
|
+
for (const row of rows){
|
|
224
|
+
result.set(row.id, row);
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
},
|
|
228
|
+
async updateCircle (id, update) {
|
|
229
|
+
const values = {};
|
|
230
|
+
if (update.name != null) values.name = update.name;
|
|
231
|
+
if (update.description != null) values.description = update.description;
|
|
232
|
+
if (update.catalog_ids != null) {
|
|
233
|
+
values.catalog_ids = adapter.encodeJSON(update.catalog_ids);
|
|
234
|
+
}
|
|
235
|
+
if (update.hlc != null) {
|
|
236
|
+
values.hlc = update.hlc;
|
|
237
|
+
// A create/update that wins the apply-layer HLC check resurrects a
|
|
238
|
+
// tombstoned circle: clear the removal alongside the HLC refresh.
|
|
239
|
+
values.removed_at_hlc = null;
|
|
240
|
+
}
|
|
241
|
+
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
242
|
+
await db.updateTable('kubun_p2p_circles').set(values).where('id', '=', id).execute();
|
|
243
|
+
},
|
|
244
|
+
async deleteCircle (id) {
|
|
245
|
+
await db.deleteFrom('kubun_p2p_circles').where('id', '=', id).execute();
|
|
246
|
+
},
|
|
247
|
+
// Earliest-wins LWW on `removed_at_hlc`: the update lands only when the row
|
|
248
|
+
// exists AND it is either not yet tombstoned or the incoming `hlc` precedes
|
|
249
|
+
// the stored removal HLC. A removal for a `circleID` with no local row is
|
|
250
|
+
// ignored — there is no circle to tombstone, and creating a row would leak
|
|
251
|
+
// a phantom circle.
|
|
252
|
+
async markCircleRemoved (circleID, hlc) {
|
|
253
|
+
await db.updateTable('kubun_p2p_circles').set({
|
|
254
|
+
removed_at_hlc: hlc,
|
|
255
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
256
|
+
}).where('id', '=', circleID).where((eb)=>eb.or([
|
|
257
|
+
eb('removed_at_hlc', 'is', null),
|
|
258
|
+
eb('removed_at_hlc', '>', hlc)
|
|
259
|
+
])).execute();
|
|
260
|
+
},
|
|
261
|
+
async listCirclesByGroup (groupID, options) {
|
|
262
|
+
let query = db.selectFrom('kubun_p2p_circles').selectAll().where('group_id', '=', groupID);
|
|
263
|
+
if (!options?.includeRemoved) {
|
|
264
|
+
query = query.where('removed_at_hlc', 'is', null);
|
|
265
|
+
}
|
|
266
|
+
return await query.execute();
|
|
267
|
+
},
|
|
268
|
+
async addCircleMember (member) {
|
|
269
|
+
// Re-adding a previously-removed member clears the tombstone and refreshes
|
|
270
|
+
// the row's HLC, so a member that left and rejoined is active again.
|
|
271
|
+
await db.insertInto('kubun_p2p_circle_members').values({
|
|
272
|
+
circle_id: member.circle_id,
|
|
273
|
+
member_did: normalizeDID(member.member_did),
|
|
274
|
+
role: member.role,
|
|
275
|
+
hlc: member.hlc
|
|
276
|
+
}).onConflict((oc)=>oc.columns([
|
|
277
|
+
'circle_id',
|
|
278
|
+
'member_did'
|
|
279
|
+
]).doUpdateSet((eb)=>({
|
|
280
|
+
role: eb.ref('excluded.role'),
|
|
281
|
+
hlc: eb.ref('excluded.hlc'),
|
|
282
|
+
removed_at_hlc: null,
|
|
283
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
284
|
+
}))).execute();
|
|
285
|
+
},
|
|
286
|
+
// Earliest-wins LWW on `removed_at_hlc`, mirroring `markMemberRemoved`: the
|
|
287
|
+
// update lands only when the row exists AND it is either not yet tombstoned
|
|
288
|
+
// or the incoming `hlc` precedes the stored removal HLC. A removal for a
|
|
289
|
+
// `(circleID, memberDID)` with no local row is ignored — no phantom row is
|
|
290
|
+
// created.
|
|
291
|
+
async removeCircleMember (circleID, memberDID, hlc) {
|
|
292
|
+
await db.updateTable('kubun_p2p_circle_members').set({
|
|
293
|
+
removed_at_hlc: hlc,
|
|
294
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
295
|
+
}).where('circle_id', '=', circleID).where('member_did', '=', normalizeDID(memberDID)).where((eb)=>eb.or([
|
|
296
|
+
eb('removed_at_hlc', 'is', null),
|
|
297
|
+
eb('removed_at_hlc', '>', hlc)
|
|
298
|
+
])).execute();
|
|
299
|
+
},
|
|
300
|
+
async getCircleMember (circleID, memberDID) {
|
|
301
|
+
return await db.selectFrom('kubun_p2p_circle_members').selectAll().where('circle_id', '=', circleID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
|
|
302
|
+
},
|
|
303
|
+
async listCircleMembers (circleID, options) {
|
|
304
|
+
let query = db.selectFrom('kubun_p2p_circle_members').selectAll().where('circle_id', '=', circleID);
|
|
305
|
+
if (!options?.includeRemoved) {
|
|
306
|
+
query = query.where('removed_at_hlc', 'is', null);
|
|
307
|
+
}
|
|
308
|
+
return await query.execute();
|
|
309
|
+
},
|
|
310
|
+
// Read-ACL / sync-scope source: a tombstoned circle or a tombstoned
|
|
311
|
+
// membership grants no read access, so gate both sides on `removed_at_hlc`.
|
|
312
|
+
async getCirclesForMember (memberDID) {
|
|
313
|
+
return await db.selectFrom('kubun_p2p_circle_members').innerJoin('kubun_p2p_circles', 'kubun_p2p_circles.id', 'kubun_p2p_circle_members.circle_id').selectAll('kubun_p2p_circles').where('kubun_p2p_circle_members.member_did', '=', normalizeDID(memberDID)).where('kubun_p2p_circle_members.removed_at_hlc', 'is', null).where('kubun_p2p_circles.removed_at_hlc', 'is', null).execute();
|
|
314
|
+
},
|
|
315
|
+
// Read-ACL / sync-scope source: a tombstoned circle or a tombstoned
|
|
316
|
+
// membership grants no read access, so gate both sides on `removed_at_hlc`.
|
|
317
|
+
async isMemberOfAnyCircle (memberDID, circleIDs) {
|
|
318
|
+
if (circleIDs.length === 0) return false;
|
|
319
|
+
const row = await db.selectFrom('kubun_p2p_circle_members').innerJoin('kubun_p2p_circles', 'kubun_p2p_circles.id', 'kubun_p2p_circle_members.circle_id').select('kubun_p2p_circle_members.circle_id').where('kubun_p2p_circle_members.member_did', '=', normalizeDID(memberDID)).where('kubun_p2p_circle_members.circle_id', 'in', circleIDs).where('kubun_p2p_circle_members.removed_at_hlc', 'is', null).where('kubun_p2p_circles.removed_at_hlc', 'is', null).limit(1).executeTakeFirst();
|
|
320
|
+
return row != null;
|
|
321
|
+
},
|
|
322
|
+
// Unconditional fold-authoritative upsert: every projected column is set
|
|
323
|
+
// from the supplied derived state, including `removed_at_hlc` (a value
|
|
324
|
+
// tombstones, null marks active). No HLC guard — the reproject already
|
|
325
|
+
// resolved the winner via the ledger fold, so the store write must not
|
|
326
|
+
// re-arbitrate.
|
|
327
|
+
async reconcileCircleProjection (params) {
|
|
328
|
+
await db.insertInto('kubun_p2p_circles').values({
|
|
329
|
+
id: params.id,
|
|
330
|
+
group_id: params.groupID,
|
|
331
|
+
name: params.name,
|
|
332
|
+
description: params.description,
|
|
333
|
+
catalog_ids: adapter.encodeJSON(params.catalogIDs),
|
|
334
|
+
hlc: params.hlc,
|
|
335
|
+
removed_at_hlc: params.removedAtHLC
|
|
336
|
+
}).onConflict((oc)=>oc.column('id').doUpdateSet((eb)=>({
|
|
337
|
+
group_id: eb.ref('excluded.group_id'),
|
|
338
|
+
name: eb.ref('excluded.name'),
|
|
339
|
+
description: eb.ref('excluded.description'),
|
|
340
|
+
catalog_ids: eb.ref('excluded.catalog_ids'),
|
|
341
|
+
hlc: eb.ref('excluded.hlc'),
|
|
342
|
+
removed_at_hlc: eb.ref('excluded.removed_at_hlc'),
|
|
343
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
344
|
+
}))).execute();
|
|
345
|
+
},
|
|
346
|
+
async reconcileCircleMemberProjection (params) {
|
|
347
|
+
await db.insertInto('kubun_p2p_circle_members').values({
|
|
348
|
+
circle_id: params.circleID,
|
|
349
|
+
member_did: normalizeDID(params.memberDID),
|
|
350
|
+
role: params.role,
|
|
351
|
+
hlc: params.hlc,
|
|
352
|
+
removed_at_hlc: params.removedAtHLC
|
|
353
|
+
}).onConflict((oc)=>oc.columns([
|
|
354
|
+
'circle_id',
|
|
355
|
+
'member_did'
|
|
356
|
+
]).doUpdateSet((eb)=>({
|
|
357
|
+
role: eb.ref('excluded.role'),
|
|
358
|
+
hlc: eb.ref('excluded.hlc'),
|
|
359
|
+
removed_at_hlc: eb.ref('excluded.removed_at_hlc'),
|
|
360
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
361
|
+
}))).execute();
|
|
362
|
+
},
|
|
363
|
+
// --- Group-delegation edge operations ---
|
|
364
|
+
async addGroupDelegation (edge) {
|
|
365
|
+
await db.insertInto('kubun_p2p_group_delegation').values({
|
|
366
|
+
group_id: edge.group_id,
|
|
367
|
+
jti: edge.jti
|
|
368
|
+
}).onConflict((oc)=>oc.column('jti').doNothing()).execute();
|
|
369
|
+
},
|
|
370
|
+
async getGroupIDByJTI (jti) {
|
|
371
|
+
const row = await db.selectFrom('kubun_p2p_group_delegation').select('group_id').where('jti', '=', jti).executeTakeFirst();
|
|
372
|
+
return row?.group_id ?? null;
|
|
373
|
+
},
|
|
374
|
+
async getGroupIDsByJTIs (jtis) {
|
|
375
|
+
if (jtis.length === 0) {
|
|
376
|
+
return new Map();
|
|
377
|
+
}
|
|
378
|
+
const rows = await db.selectFrom('kubun_p2p_group_delegation').select([
|
|
379
|
+
'jti',
|
|
380
|
+
'group_id'
|
|
381
|
+
]).where('jti', 'in', jtis).execute();
|
|
382
|
+
return new Map(rows.map((row)=>[
|
|
383
|
+
row.jti,
|
|
384
|
+
row.group_id
|
|
385
|
+
]));
|
|
386
|
+
},
|
|
387
|
+
// --- MLS state operations ---
|
|
388
|
+
async saveMLSState (state) {
|
|
389
|
+
await db.insertInto('kubun_p2p_group_mls_state').values({
|
|
390
|
+
group_id: state.group_id,
|
|
391
|
+
device_id: state.device_id,
|
|
392
|
+
mls_state: adapter.encodeBinary(state.mls_state),
|
|
393
|
+
credential: state.credential,
|
|
394
|
+
epoch: state.epoch,
|
|
395
|
+
root_capability: state.root_capability
|
|
396
|
+
}).onConflict((oc)=>oc.columns([
|
|
397
|
+
'group_id',
|
|
398
|
+
'device_id'
|
|
399
|
+
]).doUpdateSet((eb)=>({
|
|
400
|
+
mls_state: eb.ref('excluded.mls_state'),
|
|
401
|
+
credential: eb.ref('excluded.credential'),
|
|
402
|
+
epoch: eb.ref('excluded.epoch'),
|
|
403
|
+
root_capability: eb.ref('excluded.root_capability'),
|
|
404
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
405
|
+
}))// Monotonic-epoch guard: only overwrite when the incoming epoch is
|
|
406
|
+
// at least the stored one. The MLS epoch never moves backwards, so a
|
|
407
|
+
// write carrying a lower epoch is a stale handle re-persisting an
|
|
408
|
+
// already-superseded ratchet — apply it and we roll the ratchet back.
|
|
409
|
+
// When this predicate is false the existing row is left untouched
|
|
410
|
+
// with no error (a silent no-op); a fresh insert has no conflict and
|
|
411
|
+
// is unaffected. The bound is `<=`, not `<`: decrypting an
|
|
412
|
+
// application message advances the secret-tree generation WITHIN an
|
|
413
|
+
// epoch while `epoch` stays constant, so a legitimate same-epoch
|
|
414
|
+
// re-persist carries a changed `mls_state` blob at the same epoch and
|
|
415
|
+
// must still win — `<` would reject it and wedge decryption.
|
|
416
|
+
.where((eb)=>eb('kubun_p2p_group_mls_state.epoch', '<=', eb.ref('excluded.epoch')))).execute();
|
|
417
|
+
},
|
|
418
|
+
async getMLSState (groupID, deviceID) {
|
|
419
|
+
return await db.selectFrom('kubun_p2p_group_mls_state').selectAll().where('group_id', '=', groupID).where('device_id', '=', deviceID).executeTakeFirst();
|
|
420
|
+
},
|
|
421
|
+
async deleteMLSState (groupID, deviceID) {
|
|
422
|
+
await db.deleteFrom('kubun_p2p_group_mls_state').where('group_id', '=', groupID).where('device_id', '=', deviceID).execute();
|
|
423
|
+
},
|
|
424
|
+
async listMLSStates (groupID) {
|
|
425
|
+
return await db.selectFrom('kubun_p2p_group_mls_state').selectAll().where('group_id', '=', groupID).execute();
|
|
426
|
+
},
|
|
427
|
+
async deleteGroupMLSStates (groupID) {
|
|
428
|
+
await db.deleteFrom('kubun_p2p_group_mls_state').where('group_id', '=', groupID).execute();
|
|
429
|
+
},
|
|
430
|
+
// --- Sync peer operations ---
|
|
431
|
+
async addSyncPeer (peer) {
|
|
432
|
+
const allowedUsers = adapter.encodeJSON(peer.allowed_users);
|
|
433
|
+
const config = adapter.encodeJSON(peer.config);
|
|
434
|
+
await db.insertInto('kubun_p2p_sync_peers').values({
|
|
435
|
+
...peer,
|
|
436
|
+
allowed_users: allowedUsers,
|
|
437
|
+
config
|
|
438
|
+
}).onConflict((oc)=>oc.column('peer_did').doUpdateSet({
|
|
439
|
+
endpoint: peer.endpoint,
|
|
440
|
+
mode: peer.mode,
|
|
441
|
+
allowed_users: allowedUsers,
|
|
442
|
+
priority: peer.priority,
|
|
443
|
+
trust_level: peer.trust_level,
|
|
444
|
+
config,
|
|
445
|
+
updated_at: peer.updated_at
|
|
446
|
+
})).execute();
|
|
447
|
+
},
|
|
448
|
+
async getSyncPeer (peerDID) {
|
|
449
|
+
return await db.selectFrom('kubun_p2p_sync_peers').selectAll().where('peer_did', '=', peerDID).executeTakeFirst();
|
|
450
|
+
},
|
|
451
|
+
async listSyncPeers () {
|
|
452
|
+
return await db.selectFrom('kubun_p2p_sync_peers').selectAll().execute();
|
|
453
|
+
},
|
|
454
|
+
async updateSyncPeer (peerDID, update) {
|
|
455
|
+
const values = {};
|
|
456
|
+
if (update.endpoint != null) values.endpoint = update.endpoint;
|
|
457
|
+
if (update.mode != null) values.mode = update.mode;
|
|
458
|
+
if (update.allowed_users != null) values.allowed_users = adapter.encodeJSON(update.allowed_users);
|
|
459
|
+
if (update.priority != null) values.priority = update.priority;
|
|
460
|
+
if (update.trust_level != null) values.trust_level = update.trust_level;
|
|
461
|
+
if (update.config != null) values.config = adapter.encodeJSON(update.config);
|
|
462
|
+
values.updated_at = Date.now();
|
|
463
|
+
await db.updateTable('kubun_p2p_sync_peers').set(values).where('peer_did', '=', peerDID).execute();
|
|
464
|
+
},
|
|
465
|
+
async removeSyncPeer (peerDID) {
|
|
466
|
+
await db.deleteFrom('kubun_p2p_sync_peers').where('peer_did', '=', peerDID).execute();
|
|
467
|
+
},
|
|
468
|
+
// --- Broadcast log operations ---
|
|
469
|
+
async recordBroadcast (entry) {
|
|
470
|
+
const result = await db.insertInto('kubun_p2p_broadcast_log').values({
|
|
471
|
+
doc_id: entry.docID,
|
|
472
|
+
version: entry.version,
|
|
473
|
+
mls_group_id: entry.mlsGroupID
|
|
474
|
+
}).onConflict((oc)=>oc.columns([
|
|
475
|
+
'doc_id',
|
|
476
|
+
'version',
|
|
477
|
+
'mls_group_id'
|
|
478
|
+
]).doNothing()).executeTakeFirst();
|
|
479
|
+
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
480
|
+
},
|
|
481
|
+
async hasBroadcast (entry) {
|
|
482
|
+
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();
|
|
483
|
+
return row != null;
|
|
484
|
+
},
|
|
485
|
+
// --- Control ledger operations ---
|
|
486
|
+
async appendLedgerEntry (entry) {
|
|
487
|
+
const result = await db.insertInto('kubun_p2p_control_ledger').values({
|
|
488
|
+
group_id: entry.group_id,
|
|
489
|
+
entry_id: entry.entry_id,
|
|
490
|
+
type: entry.type,
|
|
491
|
+
subject: entry.subject,
|
|
492
|
+
value: adapter.encodeJSON(entry.value),
|
|
493
|
+
hlc: entry.hlc,
|
|
494
|
+
issuer_did: normalizeDID(entry.issuer_did),
|
|
495
|
+
signed_token: entry.signed_token
|
|
496
|
+
}).onConflict((oc)=>oc.column('entry_id').doNothing()).executeTakeFirst();
|
|
497
|
+
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
498
|
+
},
|
|
499
|
+
async getLedgerEntries (groupID) {
|
|
500
|
+
return await db.selectFrom('kubun_p2p_control_ledger').selectAll().where('group_id', '=', groupID).orderBy('hlc').orderBy('entry_id').execute();
|
|
501
|
+
},
|
|
502
|
+
async getLedgerEntriesByType (groupID, type) {
|
|
503
|
+
return await db.selectFrom('kubun_p2p_control_ledger').selectAll().where('group_id', '=', groupID).where('type', '=', type).orderBy('hlc').orderBy('entry_id').execute();
|
|
504
|
+
},
|
|
505
|
+
// --- Projection state operations ---
|
|
506
|
+
async getProjectionWatermark (groupID, projection) {
|
|
507
|
+
const row = await db.selectFrom('kubun_p2p_projection_state').select('projected_through_hlc').where('group_id', '=', groupID).where('projection', '=', projection).executeTakeFirst();
|
|
508
|
+
return row?.projected_through_hlc ?? null;
|
|
509
|
+
},
|
|
510
|
+
// Overwrite-on-conflict: callers only ever advance the watermark, so there
|
|
511
|
+
// is no need for a SQL max/guard here.
|
|
512
|
+
async setProjectionWatermark (groupID, projection, hlc) {
|
|
513
|
+
await db.insertInto('kubun_p2p_projection_state').values({
|
|
514
|
+
group_id: groupID,
|
|
515
|
+
projection,
|
|
516
|
+
projected_through_hlc: hlc
|
|
517
|
+
}).onConflict((oc)=>oc.columns([
|
|
518
|
+
'group_id',
|
|
519
|
+
'projection'
|
|
520
|
+
]).doUpdateSet((eb)=>({
|
|
521
|
+
projected_through_hlc: eb.ref('excluded.projected_through_hlc'),
|
|
522
|
+
updated_at: adapter.encodeTimestamp(new Date())
|
|
523
|
+
}))).execute();
|
|
524
|
+
},
|
|
525
|
+
// --- Hub operations ---
|
|
526
|
+
async insertHub (hub) {
|
|
527
|
+
await db.insertInto('kubun_p2p_hubs').values({
|
|
528
|
+
id: hub.id,
|
|
529
|
+
label: hub.label,
|
|
530
|
+
url: hub.url,
|
|
531
|
+
server_did: hub.server_did
|
|
532
|
+
}).execute();
|
|
533
|
+
},
|
|
534
|
+
async getHub (id) {
|
|
535
|
+
return await db.selectFrom('kubun_p2p_hubs').selectAll().where('id', '=', id).executeTakeFirst();
|
|
536
|
+
},
|
|
537
|
+
async getHubByURL (url) {
|
|
538
|
+
return await db.selectFrom('kubun_p2p_hubs').selectAll().where('url', '=', url).executeTakeFirst();
|
|
539
|
+
},
|
|
540
|
+
async updateHub (id, update) {
|
|
541
|
+
const values = {};
|
|
542
|
+
if (update.label !== undefined) values.label = update.label;
|
|
543
|
+
if (update.url !== undefined) values.url = update.url;
|
|
544
|
+
if (update.server_did !== undefined) values.server_did = update.server_did;
|
|
545
|
+
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
546
|
+
await db.updateTable('kubun_p2p_hubs').set(values).where('id', '=', id).execute();
|
|
547
|
+
},
|
|
548
|
+
async deleteHub (id) {
|
|
549
|
+
await db.deleteFrom('kubun_p2p_hubs').where('id', '=', id).execute();
|
|
550
|
+
},
|
|
551
|
+
async listHubs () {
|
|
552
|
+
return await db.selectFrom('kubun_p2p_hubs').selectAll().execute();
|
|
553
|
+
},
|
|
554
|
+
// --- Group-hub binding operations ---
|
|
555
|
+
async insertGroupHub (binding) {
|
|
556
|
+
const result = await db.insertInto('kubun_p2p_group_hubs').values({
|
|
557
|
+
group_id: binding.group_id,
|
|
558
|
+
hub_id: binding.hub_id
|
|
559
|
+
}).onConflict((oc)=>oc.columns([
|
|
560
|
+
'group_id',
|
|
561
|
+
'hub_id'
|
|
562
|
+
]).doNothing()).executeTakeFirst();
|
|
563
|
+
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
564
|
+
},
|
|
565
|
+
async deleteGroupHub (params) {
|
|
566
|
+
const result = await db.deleteFrom('kubun_p2p_group_hubs').where('group_id', '=', params.groupID).where('hub_id', '=', params.hubID).executeTakeFirst();
|
|
567
|
+
return (result.numDeletedRows ?? 0n) > 0n;
|
|
568
|
+
},
|
|
569
|
+
async listGroupIDsByHubID (hubID) {
|
|
570
|
+
const rows = await db.selectFrom('kubun_p2p_group_hubs').select('group_id').where('hub_id', '=', hubID).execute();
|
|
571
|
+
return rows.map((r)=>r.group_id);
|
|
572
|
+
},
|
|
573
|
+
async listHubsByGroupID (groupID) {
|
|
574
|
+
return await db.selectFrom('kubun_p2p_group_hubs').innerJoin('kubun_p2p_hubs', 'kubun_p2p_hubs.id', 'kubun_p2p_group_hubs.hub_id').selectAll('kubun_p2p_hubs').where('kubun_p2p_group_hubs.group_id', '=', groupID).execute();
|
|
575
|
+
},
|
|
576
|
+
async listGroupsByHubID (hubID) {
|
|
577
|
+
const rows = await db.selectFrom('kubun_p2p_group_hubs').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_hubs.group_id').select([
|
|
578
|
+
'kubun_p2p_groups.id as id',
|
|
579
|
+
'kubun_p2p_groups.name as name',
|
|
580
|
+
'kubun_p2p_groups.description as description',
|
|
581
|
+
'kubun_p2p_groups.created_by as created_by',
|
|
582
|
+
'kubun_p2p_groups.hlc as hlc',
|
|
583
|
+
'kubun_p2p_groups.created_at as created_at',
|
|
584
|
+
'kubun_p2p_groups.updated_at as updated_at'
|
|
585
|
+
]).where('kubun_p2p_group_hubs.hub_id', '=', hubID).execute();
|
|
586
|
+
return rows;
|
|
587
|
+
},
|
|
588
|
+
async deleteGroupHubsByHubID (hubID) {
|
|
589
|
+
await db.deleteFrom('kubun_p2p_group_hubs').where('hub_id', '=', hubID).execute();
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
return api;
|
|
593
|
+
}
|
package/lib/definition.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
import{createP2PStore
|
|
1
|
+
import { createP2PStore } from './api.js';
|
|
2
|
+
import { getP2PMigrations } from './migrations.js';
|
|
3
|
+
export const p2pStoreDefinition = {
|
|
4
|
+
name: 'p2p',
|
|
5
|
+
migrations: getP2PMigrations,
|
|
6
|
+
createAPI: createP2PStore
|
|
7
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { P2PStoreAPI } from './api.js';
|
|
|
3
3
|
export type { P2PStoreAPI } from './api.js';
|
|
4
4
|
export { createP2PStore } from './api.js';
|
|
5
5
|
export { p2pStoreDefinition } from './definition.js';
|
|
6
|
-
export { createP2PRevocationBackend } from './revocation-backend.js';
|
|
7
6
|
export declare const P2P_STORE: "p2p";
|
|
8
7
|
export declare function getP2PStore(provider: StoreProvider): Promise<P2PStoreAPI>;
|
|
9
|
-
export type { Circle, CircleMember, CircleMemberTable, CircleTable,
|
|
8
|
+
export type { Circle, CircleMember, CircleMemberTable, CircleTable, ControlLedgerEntry, ControlLedgerTable, Group, GroupDelegation, GroupDelegationTable, GroupHub, GroupHubTable, GroupMember, GroupMemberTable, GroupMLSState, GroupMLSStateTable, GroupTable, Hub, HubTable, InsertCircle, InsertCircleMember, InsertControlLedgerEntry, InsertGroup, InsertGroupDelegation, InsertGroupHub, InsertGroupMember, InsertGroupMLSState, InsertHub, InsertProjectionState, InsertSyncCheckpoint, InsertSyncPeer, P2PStoreTables, PaginatedResult, PaginationParams, ProjectionState, ProjectionStateTable, SyncCheckpoint, SyncCheckpointTable, SyncPeer, SyncPeerTable, UpdateGroupMLSState, } from './tables.js';
|
package/lib/index.js
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export{createP2PStore}from
|
|
1
|
+
export { createP2PStore } from './api.js';
|
|
2
|
+
export { p2pStoreDefinition } from './definition.js';
|
|
3
|
+
export const P2P_STORE = 'p2p';
|
|
4
|
+
export function getP2PStore(provider) {
|
|
5
|
+
return provider.getStore(P2P_STORE);
|
|
6
|
+
}
|
package/lib/migrations.js
CHANGED
|
@@ -1 +1,105 @@
|
|
|
1
|
-
export function getP2PMigrations(
|
|
1
|
+
export function getP2PMigrations(ctx) {
|
|
2
|
+
const t = ctx.types;
|
|
3
|
+
const now = ctx.functions.now;
|
|
4
|
+
const init = {
|
|
5
|
+
async up (db) {
|
|
6
|
+
// Groups
|
|
7
|
+
await db.schema.createTable('kubun_p2p_groups').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('description', t.text, (col)=>col.notNull().defaultTo('')).addColumn('created_by', t.text, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).execute();
|
|
8
|
+
// Group members
|
|
9
|
+
await db.schema.createTable('kubun_p2p_group_members').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('member_did', t.text, (col)=>col.notNull()).addColumn('role', t.text, (col)=>col.notNull().defaultTo('member'))// LWW version key for the row, set on insert and refreshed on re-add.
|
|
10
|
+
.addColumn('hlc', t.text, (col)=>col.notNull())// HLC at which this peer learned the member was removed from the MLS
|
|
11
|
+
// group. NULL means the member is active; a non-null value tombstones
|
|
12
|
+
// the row so read-ACL / sync-scope queries exclude it while the
|
|
13
|
+
// apply-time check can still compare against the removal HLC.
|
|
14
|
+
.addColumn('removed_at_hlc', t.text).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).addPrimaryKeyConstraint('pk_p2p_group_members', [
|
|
15
|
+
'group_id',
|
|
16
|
+
'member_did'
|
|
17
|
+
]).execute();
|
|
18
|
+
await db.schema.createIndex('idx_p2p_group_members_did').ifNotExists().on('kubun_p2p_group_members').column('member_did').execute();
|
|
19
|
+
// Circles
|
|
20
|
+
await db.schema.createTable('kubun_p2p_circles').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('name', t.text, (col)=>col.notNull()).addColumn('description', t.text, (col)=>col.notNull().defaultTo('')).addColumn('catalog_ids', t.json, (col)=>col.notNull().defaultTo('[]')).addColumn('hlc', t.text, (col)=>col.notNull())// NULL means the circle is active; a non-null value tombstones the row
|
|
21
|
+
// so reads exclude it while the removal HLC stays comparable.
|
|
22
|
+
.addColumn('removed_at_hlc', t.text).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).execute();
|
|
23
|
+
await db.schema.createIndex('idx_p2p_circles_group').ifNotExists().on('kubun_p2p_circles').column('group_id').execute();
|
|
24
|
+
// Circle members
|
|
25
|
+
await db.schema.createTable('kubun_p2p_circle_members').ifNotExists().addColumn('circle_id', t.text, (col)=>col.notNull()).addColumn('member_did', t.text, (col)=>col.notNull()).addColumn('role', t.text, (col)=>col.notNull().defaultTo('member')).addColumn('hlc', t.text, (col)=>col.notNull())// NULL means the circle member is active; a non-null value tombstones
|
|
26
|
+
// the row so reads exclude it while the removal HLC stays comparable.
|
|
27
|
+
.addColumn('removed_at_hlc', t.text).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).addPrimaryKeyConstraint('pk_p2p_circle_members', [
|
|
28
|
+
'circle_id',
|
|
29
|
+
'member_did'
|
|
30
|
+
]).execute();
|
|
31
|
+
await db.schema.createIndex('idx_p2p_circle_members_did').ifNotExists().on('kubun_p2p_circle_members').column('member_did').execute();
|
|
32
|
+
// Group-delegation edge. The delegation token and revocation rows live in
|
|
33
|
+
// the core delegation store (group-agnostic); this thin edge records the
|
|
34
|
+
// one group a delegation `jti` was granted in, so the p2p layer can still
|
|
35
|
+
// resolve a cap's group for revoke-broadcast targeting and listing
|
|
36
|
+
// enrichment. One group per `jti`, so `jti` is the primary key.
|
|
37
|
+
await db.schema.createTable('kubun_p2p_group_delegation').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('jti', t.text, (col)=>col.notNull().primaryKey()).execute();
|
|
38
|
+
// Group MLS state
|
|
39
|
+
await db.schema.createTable('kubun_p2p_group_mls_state').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('device_id', t.text, (col)=>col.notNull()).addColumn('mls_state', t.binary, (col)=>col.notNull()).addColumn('credential', t.text, (col)=>col.notNull()).addColumn('epoch', 'integer', (col)=>col.notNull()).addColumn('root_capability', t.text, (col)=>col.notNull().defaultTo('')).addColumn('updated_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addPrimaryKeyConstraint('pk_p2p_group_mls_state', [
|
|
40
|
+
'group_id',
|
|
41
|
+
'device_id'
|
|
42
|
+
]).execute();
|
|
43
|
+
// Sync peers
|
|
44
|
+
await db.schema.createTable('kubun_p2p_sync_peers').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('peer_did', t.text, (col)=>col.notNull().unique()).addColumn('endpoint', t.text, (col)=>col.notNull()).addColumn('mode', t.text, (col)=>col.notNull()).addColumn('allowed_users', t.json, (col)=>col.notNull()).addColumn('priority', t.text, (col)=>col.notNull().defaultTo('0')).addColumn('trust_level', t.text, (col)=>col.notNull()).addColumn('config', t.json, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
|
|
45
|
+
// Sync checkpoints
|
|
46
|
+
await db.schema.createTable('kubun_p2p_sync_checkpoints').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('peer_did', t.text, (col)=>col.notNull()).addColumn('scope', t.text, (col)=>col.notNull()).addColumn('checkpoint_hlc', t.text, (col)=>col.notNull()).addColumn('updated_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
|
|
47
|
+
// Broadcast log
|
|
48
|
+
await db.schema.createTable('kubun_p2p_broadcast_log').ifNotExists().addColumn('doc_id', t.text, (col)=>col.notNull()).addColumn('version', t.text, (col)=>col.notNull()).addColumn('mls_group_id', t.text, (col)=>col.notNull()).addColumn('broadcast_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addPrimaryKeyConstraint('pk_p2p_broadcast_log', [
|
|
49
|
+
'doc_id',
|
|
50
|
+
'version',
|
|
51
|
+
'mls_group_id'
|
|
52
|
+
]).execute();
|
|
53
|
+
await db.schema.createIndex('idx_p2p_broadcast_log_doc').ifNotExists().on('kubun_p2p_broadcast_log').column('doc_id').execute();
|
|
54
|
+
// Hubs (device-local, never synced — no hlc column)
|
|
55
|
+
await db.schema.createTable('kubun_p2p_hubs').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('label', t.text).addColumn('url', t.text, (col)=>col.notNull().unique()).addColumn('server_did', t.text).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).execute();
|
|
56
|
+
// Group hubs (join table, device-local — no hlc column)
|
|
57
|
+
await db.schema.createTable('kubun_p2p_group_hubs').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('hub_id', t.text, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addPrimaryKeyConstraint('pk_p2p_group_hubs', [
|
|
58
|
+
'group_id',
|
|
59
|
+
'hub_id'
|
|
60
|
+
]).execute();
|
|
61
|
+
await db.schema.createIndex('idx_p2p_group_hubs_hub').ifNotExists().on('kubun_p2p_group_hubs').column('hub_id').execute();
|
|
62
|
+
// Control ledger: append-only signed-claim log. `entry_id` (content
|
|
63
|
+
// digest of `signed_token`) is the primary key, so a re-delivered entry
|
|
64
|
+
// is a no-op insert. Rows are read ordered by `(hlc, entry_id)` for a
|
|
65
|
+
// deterministic reducer fold; index that pair scoped to the group.
|
|
66
|
+
await db.schema.createTable('kubun_p2p_control_ledger').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('entry_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('type', t.text, (col)=>col.notNull()).addColumn('subject', t.text, (col)=>col.notNull()).addColumn('value', t.json, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addColumn('issuer_did', t.text, (col)=>col.notNull()).addColumn('signed_token', t.text, (col)=>col.notNull()).addColumn('received_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
|
|
67
|
+
await db.schema.createIndex('idx_p2p_control_ledger_group_order').ifNotExists().on('kubun_p2p_control_ledger').columns([
|
|
68
|
+
'group_id',
|
|
69
|
+
'hlc',
|
|
70
|
+
'entry_id'
|
|
71
|
+
]).execute();
|
|
72
|
+
await db.schema.createIndex('idx_p2p_control_ledger_group_type').ifNotExists().on('kubun_p2p_control_ledger').columns([
|
|
73
|
+
'group_id',
|
|
74
|
+
'type'
|
|
75
|
+
]).execute();
|
|
76
|
+
// Projection watermark: how far a given `(group, projection)` fold has
|
|
77
|
+
// been materialized. An in-order entry (hlc > watermark) applies
|
|
78
|
+
// incrementally and advances this; an out-of-order or absent watermark
|
|
79
|
+
// triggers a full reproject. The projection is disposable — this row only
|
|
80
|
+
// optimizes away re-folding the whole ledger per receive.
|
|
81
|
+
await db.schema.createTable('kubun_p2p_projection_state').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('projection', t.text, (col)=>col.notNull()).addColumn('projected_through_hlc', t.text, (col)=>col.notNull()).addColumn('updated_at', t.timestamp).addPrimaryKeyConstraint('pk_p2p_projection_state', [
|
|
82
|
+
'group_id',
|
|
83
|
+
'projection'
|
|
84
|
+
]).execute();
|
|
85
|
+
},
|
|
86
|
+
async down (db) {
|
|
87
|
+
await db.schema.dropTable('kubun_p2p_projection_state').ifExists().execute();
|
|
88
|
+
await db.schema.dropTable('kubun_p2p_control_ledger').ifExists().execute();
|
|
89
|
+
await db.schema.dropTable('kubun_p2p_group_hubs').ifExists().execute();
|
|
90
|
+
await db.schema.dropTable('kubun_p2p_hubs').ifExists().execute();
|
|
91
|
+
await db.schema.dropTable('kubun_p2p_broadcast_log').ifExists().execute();
|
|
92
|
+
await db.schema.dropTable('kubun_p2p_sync_checkpoints').ifExists().execute();
|
|
93
|
+
await db.schema.dropTable('kubun_p2p_sync_peers').ifExists().execute();
|
|
94
|
+
await db.schema.dropTable('kubun_p2p_group_mls_state').ifExists().execute();
|
|
95
|
+
await db.schema.dropTable('kubun_p2p_group_delegation').ifExists().execute();
|
|
96
|
+
await db.schema.dropTable('kubun_p2p_circle_members').ifExists().execute();
|
|
97
|
+
await db.schema.dropTable('kubun_p2p_circles').ifExists().execute();
|
|
98
|
+
await db.schema.dropTable('kubun_p2p_group_members').ifExists().execute();
|
|
99
|
+
await db.schema.dropTable('kubun_p2p_groups').ifExists().execute();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
'0-init': init
|
|
104
|
+
};
|
|
105
|
+
}
|
package/lib/tables.d.ts
CHANGED
|
@@ -50,45 +50,12 @@ export type CircleMemberTable = {
|
|
|
50
50
|
};
|
|
51
51
|
export type CircleMember = Selectable<CircleMemberTable>;
|
|
52
52
|
export type InsertCircleMember = Insertable<CircleMemberTable>;
|
|
53
|
-
export type
|
|
54
|
-
jti: string;
|
|
55
|
-
grantor: string;
|
|
56
|
-
audience: string;
|
|
57
|
-
token: string;
|
|
58
|
-
resource: string;
|
|
59
|
-
act: string;
|
|
60
|
-
exp: number;
|
|
53
|
+
export type GroupDelegationTable = {
|
|
61
54
|
group_id: string;
|
|
62
|
-
hlc: string;
|
|
63
|
-
created_at: CreatedAtColumn;
|
|
64
|
-
updated_at: UpdatedAtColumn;
|
|
65
|
-
};
|
|
66
|
-
export type DelegationToken = Selectable<DelegationTokenTable>;
|
|
67
|
-
export type InsertDelegationToken = Insertable<DelegationTokenTable>;
|
|
68
|
-
export type RevokedCapabilityTable = {
|
|
69
|
-
jti: string;
|
|
70
|
-
revoker_did: string;
|
|
71
|
-
revoked_iat: number;
|
|
72
|
-
revocation_token: string;
|
|
73
|
-
verified_at: number | null;
|
|
74
|
-
cap_exp: number | null;
|
|
75
|
-
group_id: string;
|
|
76
|
-
hlc: string;
|
|
77
|
-
created_at: CreatedAtColumn;
|
|
78
|
-
updated_at: UpdatedAtColumn;
|
|
79
|
-
};
|
|
80
|
-
export type RevokedCapability = Selectable<RevokedCapabilityTable>;
|
|
81
|
-
export type InsertRevokedCapability = Insertable<RevokedCapabilityTable>;
|
|
82
|
-
export type HeldRevocation = {
|
|
83
55
|
jti: string;
|
|
84
|
-
grantor: string;
|
|
85
|
-
audience: string;
|
|
86
|
-
revoker_did: string;
|
|
87
|
-
revoked_iat: number;
|
|
88
|
-
verified_at: number;
|
|
89
|
-
cap_exp: number | null;
|
|
90
|
-
group_id: string;
|
|
91
56
|
};
|
|
57
|
+
export type GroupDelegation = Selectable<GroupDelegationTable>;
|
|
58
|
+
export type InsertGroupDelegation = Insertable<GroupDelegationTable>;
|
|
92
59
|
export type GroupMLSStateTable = {
|
|
93
60
|
group_id: string;
|
|
94
61
|
device_id: string;
|
|
@@ -134,6 +101,27 @@ export type BroadcastLogTable = {
|
|
|
134
101
|
};
|
|
135
102
|
export type BroadcastLogEntry = Selectable<BroadcastLogTable>;
|
|
136
103
|
export type InsertBroadcastLogEntry = Insertable<BroadcastLogTable>;
|
|
104
|
+
export type ControlLedgerTable = {
|
|
105
|
+
group_id: string;
|
|
106
|
+
entry_id: string;
|
|
107
|
+
type: string;
|
|
108
|
+
subject: string;
|
|
109
|
+
value: JSONValueColumn<unknown>;
|
|
110
|
+
hlc: string;
|
|
111
|
+
issuer_did: string;
|
|
112
|
+
signed_token: string;
|
|
113
|
+
received_at: CreatedAtColumn;
|
|
114
|
+
};
|
|
115
|
+
export type ControlLedgerEntry = Selectable<ControlLedgerTable>;
|
|
116
|
+
export type InsertControlLedgerEntry = Insertable<ControlLedgerTable>;
|
|
117
|
+
export type ProjectionStateTable = {
|
|
118
|
+
group_id: string;
|
|
119
|
+
projection: string;
|
|
120
|
+
projected_through_hlc: string;
|
|
121
|
+
updated_at: UpdatedAtColumn;
|
|
122
|
+
};
|
|
123
|
+
export type ProjectionState = Selectable<ProjectionStateTable>;
|
|
124
|
+
export type InsertProjectionState = Insertable<ProjectionStateTable>;
|
|
137
125
|
export type HubTable = {
|
|
138
126
|
id: string;
|
|
139
127
|
label: string | null;
|
|
@@ -164,12 +152,13 @@ export type P2PStoreTables = {
|
|
|
164
152
|
kubun_p2p_group_members: GroupMemberTable;
|
|
165
153
|
kubun_p2p_circles: CircleTable;
|
|
166
154
|
kubun_p2p_circle_members: CircleMemberTable;
|
|
167
|
-
|
|
168
|
-
kubun_p2p_revoked_capabilities: RevokedCapabilityTable;
|
|
155
|
+
kubun_p2p_group_delegation: GroupDelegationTable;
|
|
169
156
|
kubun_p2p_group_mls_state: GroupMLSStateTable;
|
|
170
157
|
kubun_p2p_sync_peers: SyncPeerTable;
|
|
171
158
|
kubun_p2p_sync_checkpoints: SyncCheckpointTable;
|
|
172
159
|
kubun_p2p_broadcast_log: BroadcastLogTable;
|
|
160
|
+
kubun_p2p_control_ledger: ControlLedgerTable;
|
|
161
|
+
kubun_p2p_projection_state: ProjectionStateTable;
|
|
173
162
|
kubun_p2p_hubs: HubTable;
|
|
174
163
|
kubun_p2p_group_hubs: GroupHubTable;
|
|
175
164
|
};
|
package/lib/tables.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
// --- Aggregate P2PStoreTables type ---
|
|
2
|
+
export { };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/store-p2p",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"type": "module",
|
|
@@ -15,21 +15,22 @@
|
|
|
15
15
|
],
|
|
16
16
|
"sideEffects": false,
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@
|
|
18
|
+
"@kokuin/capability": "^0.1.0",
|
|
19
|
+
"@kokuin/token": "^0.1.1",
|
|
19
20
|
"kysely": "^0.29.2",
|
|
20
|
-
"@kubun/db
|
|
21
|
-
"@kubun/
|
|
22
|
-
"@kubun/
|
|
21
|
+
"@kubun/db": "^0.11.0",
|
|
22
|
+
"@kubun/logger": "^0.11.0",
|
|
23
|
+
"@kubun/db-adapter": "^0.11.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
|
-
"@testcontainers/postgresql": "^12.0.
|
|
26
|
-
"@kubun/db-
|
|
27
|
-
"@kubun/
|
|
28
|
-
"@kubun/
|
|
26
|
+
"@testcontainers/postgresql": "^12.0.4",
|
|
27
|
+
"@kubun/db-better-sqlite": "^0.11.0",
|
|
28
|
+
"@kubun/db-postgres": "^0.11.0",
|
|
29
|
+
"@kubun/hlc": "^0.11.0"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"build:clean": "del lib",
|
|
32
|
-
"build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
|
|
33
|
+
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
33
34
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
34
35
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
35
36
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function createP2PRevocationBackend(e){return{async add(e){},isRevoked:async a=>await e.isRevoked(a)}}
|