@kubun/store-p2p 0.10.1 → 0.12.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 CHANGED
@@ -1,260 +1,28 @@
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, DelegationToken, Group, GroupMember, GroupMLSState, HeldRevocation, Hub, InsertCircle, InsertCircleMember, InsertDelegationToken, InsertGroup, InsertGroupHub, InsertGroupMember, InsertGroupMLSState, InsertHub, InsertRevokedCapability, InsertSyncPeer, P2PStoreTables, PaginatedResult, PaginationParams, RevokedCapability, SyncPeer } from './tables.js';
5
- export type P2PStoreAPI = {
6
- createGroup(group: InsertGroup): Promise<void>;
7
- getGroup(id: string): Promise<Group | undefined>;
8
- updateGroup(id: string, update: Partial<Pick<InsertGroup, 'name' | 'description' | 'hlc'>>): Promise<void>;
9
- deleteGroup(id: string): Promise<void>;
10
- listGroups(): Promise<Array<Group>>;
11
- listGroupsOffsetPaged(params: PaginationParams): Promise<PaginatedResult<Group>>;
12
- addGroupMember(member: InsertGroupMember): Promise<void>;
13
- /**
14
- * Soft-delete a member by tombstoning the row with `hlc`. Delegates to
15
- * `markMemberRemoved`: earliest-wins LWW, ignore-if-absent. The row stays in
16
- * the table (read queries exclude it) so the apply-time check can compare a
17
- * mutation's HLC against the removal HLC.
18
- */
19
- removeGroupMember(groupID: string, memberDID: string, hlc: string): Promise<void>;
20
- /**
21
- * Tombstone an existing membership row at `hlc` using earliest-wins LWW: the
22
- * removal sticks only when no removal is recorded yet, or when `hlc` precedes
23
- * the stored removal HLC. A removal for a `(groupID, memberDID)` with no local
24
- * row is a no-op — absence means there is no member to tombstone, and no row
25
- * is created.
26
- */
27
- markMemberRemoved(groupID: string, memberDID: string, hlc: string): Promise<void>;
28
- /**
29
- * Read a membership row's removal state, including tombstoned rows. This is
30
- * the only query that sees soft-deleted members. Returns `undefined` when no
31
- * row exists; `{ removed_at_hlc: null }` for an active member; and
32
- * `{ removed_at_hlc: <hlc> }` for a removed one.
33
- */
34
- getMemberRemoval(groupID: string, memberDID: string): Promise<{
35
- removed_at_hlc: string | null;
36
- } | undefined>;
37
- getGroupMember(groupID: string, memberDID: string): Promise<GroupMember | undefined>;
38
- isGroupMember(groupID: string, memberDID: string): Promise<boolean>;
39
- isMemberOfAnyGroup(viewerDID: string, groupIDs: Array<string>): Promise<boolean>;
40
- listGroupMembers(groupID: string): Promise<Array<GroupMember>>;
41
- getGroupsForMember(memberDID: string): Promise<Array<Group>>;
42
- /**
43
- * Batch fetch groups for multiple members. Returns a Map keyed by
44
- * `memberDID`; values are arrays of groups for that member. Members with
45
- * no groups are absent from the map. Single SQL query joining
46
- * `group_members` to `groups`. Empty input short-circuits with an empty
47
- * Map (no SQL).
48
- *
49
- * Caller is responsible for deduplicating DIDs.
50
- */
51
- getGroupsForMembers(memberDIDs: Array<string>): Promise<Map<string, Array<Group>>>;
52
- createCircle(circle: InsertCircle): Promise<void>;
53
- /**
54
- * Active-only by default (`removed_at_hlc IS NULL`). Pass
55
- * `{ includeRemoved: true }` to also see tombstoned circles — used by the
56
- * apply path so a `circle:create`/`circle:update` can HLC-compare against a
57
- * tombstoned row and resurrect it, and by the catch-up responder.
58
- */
59
- getCircle(id: string, options?: {
60
- includeRemoved?: boolean;
61
- }): Promise<Circle | undefined>;
62
- /**
63
- * Batch fetch circles by ID. Returns a Map keyed by ID; missing IDs are
64
- * absent from the map. Single SQL query with `WHERE id IN (...)`. Empty
65
- * input short-circuits with an empty Map (no SQL).
66
- *
67
- * Caller is responsible for deduplicating IDs.
68
- */
69
- getCircles(ids: Array<string>): Promise<Map<string, Circle>>;
70
- updateCircle(id: string, update: Partial<Pick<InsertCircle, 'name' | 'description' | 'catalog_ids' | 'hlc'>>): Promise<void>;
71
- deleteCircle(id: string): Promise<void>;
72
- /**
73
- * Tombstone an existing circle row at `hlc` using earliest-wins LWW: the
74
- * removal sticks only when no removal is recorded yet, or when `hlc` precedes
75
- * the stored removal HLC. A removal for a `circleID` with no local row is a
76
- * no-op — absence means there is no circle to tombstone, and no row is
77
- * created.
78
- */
79
- markCircleRemoved(circleID: string, hlc: string): Promise<void>;
80
- /**
81
- * Active-only by default (`removed_at_hlc IS NULL`). Pass
82
- * `{ includeRemoved: true }` to include tombstoned circles for the catch-up
83
- * responder.
84
- */
85
- listCirclesByGroup(groupID: string, options?: {
86
- includeRemoved?: boolean;
87
- }): Promise<Array<Circle>>;
88
- addCircleMember(member: InsertCircleMember): Promise<void>;
89
- /**
90
- * Soft-delete a circle member by tombstoning the row with `hlc`: earliest-wins
91
- * LWW, ignore-if-absent. The row stays in the table so the removal HLC stays
92
- * comparable.
93
- */
94
- removeCircleMember(circleID: string, memberDID: string, hlc: string): Promise<void>;
95
- getCircleMember(circleID: string, memberDID: string): Promise<CircleMember | undefined>;
96
- /**
97
- * Active-only by default (`removed_at_hlc IS NULL`). Pass
98
- * `{ includeRemoved: true }` to include tombstoned members for the catch-up
99
- * responder.
100
- */
101
- listCircleMembers(circleID: string, options?: {
102
- includeRemoved?: boolean;
103
- }): Promise<Array<CircleMember>>;
104
- getCirclesForMember(memberDID: string): Promise<Array<Circle>>;
105
- isMemberOfAnyCircle(memberDID: string, circleIDs: Array<string>): Promise<boolean>;
106
- /**
107
- * Store a delegation token. A re-minted grant covering the same
108
- * `(grantor, audience, resource)` replaces the existing row when its `hlc`
109
- * is causally newer; an older or equal `hlc` is ignored. Held rows
110
- * (`audience` is self) and issued rows (`grantor` is self) are distinct keys
111
- * and never collide.
112
- *
113
- * Returns `true` when the stored row is new or materially different from
114
- * the prior row (insert, or LWW upsert that changed any of `jti`, `token`,
115
- * `act`, `exp`, `group_id`). Returns `false` when the call was a no-op:
116
- * either the incoming `hlc` lost LWW against an existing row, or the LWW
117
- * upsert won but every content field matched the prior row (identical
118
- * re-broadcast). Used by emission gates to suppress duplicate
119
- * `delegationTokenAdded` events on idempotent re-stores.
120
- */
121
- addDelegationToken(token: InsertDelegationToken): Promise<boolean>;
122
- getDelegationTokens(params: {
123
- grantor: string;
124
- audience: string;
125
- }): Promise<Array<DelegationToken>>;
126
- /**
127
- * Return the tokens held by `audience` that are still valid at `atTime`
128
- * (`exp > atTime`). `atTime` is a Unix epoch in seconds, matching the
129
- * `exp` column. Expired tokens and tokens addressed to other audiences are
130
- * excluded.
131
- */
132
- getHeldTokens(params: {
133
- audience: string;
134
- atTime: number;
135
- }): Promise<Array<DelegationToken>>;
136
- listIssuedTokens(grantor: string): Promise<Array<DelegationToken>>;
137
- /**
138
- * Look up a delegation token by its `jti`. `jti` is not part of the schema's
139
- * composite PK `(grantor, audience, resource)`, so the column is non-unique
140
- * at the SQL level; in practice every `grantWriteCapability` call mints a
141
- * fresh random `jti`, so collisions across distinct `(grantor, audience,
142
- * resource)` tuples do not occur. Returns the first matching row, or `null`
143
- * when no row exists for the given `jti`.
144
- */
145
- getDelegationTokenByJti(jti: string): Promise<DelegationToken | null>;
146
- /**
147
- * Store a revocation record keyed by the revoked capability's `jti`. LWW on
148
- * `hlc` — a re-broadcast with an older or equal `hlc` is ignored. Returns
149
- * `true` when the row is new, or when LWW won and any content field
150
- * (`revoker_did`, `revoked_iat`, `revocation_token`, `verified_at`,
151
- * `cap_exp`, `group_id`) materially changed. Returns `false` on a no-op
152
- * (lost LWW, or won LWW with byte-identical content). Used by emission
153
- * gates to suppress duplicate downstream events on idempotent re-stores.
154
- *
155
- * `verified_at` is `null` while the revoker-vs-cap-issuer cross-check is
156
- * pending; verification flips it via `markRevocationVerified`.
157
- */
158
- addRevocation(input: InsertRevokedCapability): Promise<boolean>;
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>;
224
- saveMLSState(state: InsertGroupMLSState): Promise<void>;
225
- getMLSState(groupID: string, deviceID: string): Promise<GroupMLSState | undefined>;
226
- deleteMLSState(groupID: string, deviceID: string): Promise<void>;
227
- listMLSStates(groupID: string): Promise<Array<GroupMLSState>>;
228
- deleteGroupMLSStates(groupID: string): Promise<void>;
229
- addSyncPeer(peer: InsertSyncPeer): Promise<void>;
230
- getSyncPeer(peerDID: string): Promise<SyncPeer | undefined>;
231
- listSyncPeers(): Promise<Array<SyncPeer>>;
232
- updateSyncPeer(peerDID: string, update: Partial<InsertSyncPeer>): Promise<void>;
233
- removeSyncPeer(peerDID: string): Promise<void>;
234
- recordBroadcast(entry: {
235
- docID: string;
236
- version: string;
237
- mlsGroupID: string;
238
- }): Promise<boolean>;
239
- hasBroadcast(entry: {
240
- docID: string;
241
- version: string;
242
- mlsGroupID: string;
243
- }): Promise<boolean>;
244
- insertHub(hub: InsertHub): Promise<void>;
245
- getHub(id: string): Promise<Hub | undefined>;
246
- getHubByURL(url: string): Promise<Hub | undefined>;
247
- updateHub(id: string, update: Partial<Pick<InsertHub, 'label' | 'url' | 'server_did'>>): Promise<void>;
248
- deleteHub(id: string): Promise<void>;
249
- listHubs(): Promise<Array<Hub>>;
250
- insertGroupHub(binding: InsertGroupHub): Promise<boolean>;
251
- deleteGroupHub(params: {
252
- groupID: string;
253
- hubID: string;
254
- }): Promise<boolean>;
255
- listGroupIDsByHubID(hubID: string): Promise<Array<string>>;
256
- listHubsByGroupID(groupID: string): Promise<Array<Hub>>;
257
- listGroupsByHubID(hubID: string): Promise<Array<Group>>;
258
- deleteGroupHubsByHubID(hubID: string): Promise<void>;
259
- };
260
- export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter, logger?: Logger): P2PStoreAPI;
3
+ import { type AppLaneOps } from './ops/app-lane.js';
4
+ import { type CircleOps } from './ops/circles.js';
5
+ import { type ControlOps } from './ops/control.js';
6
+ import { type DelegationOps } from './ops/delegations.js';
7
+ import { type DiscoveryOps } from './ops/discovery.js';
8
+ import { type GroupOps } from './ops/groups.js';
9
+ import { type HubOps } from './ops/hubs.js';
10
+ import { type MLSOps } from './ops/mls.js';
11
+ import { type SyncOps } from './ops/sync.js';
12
+ import type { P2PStoreTables } from './tables.js';
13
+ export type { AppLaneOps } from './ops/app-lane.js';
14
+ export type { CircleOps } from './ops/circles.js';
15
+ export type { CommitJournalEntryKind, ControlOps, ControlRequestKind, ControlRequestOutcome, } from './ops/control.js';
16
+ export type { DelegationOps } from './ops/delegations.js';
17
+ export type { DiscoveryOps, PutPendingJoinRequestParams, SetLocalPeerProfileParams, UpsertPeerAnnouncementParams, } from './ops/discovery.js';
18
+ export type { GroupOps } from './ops/groups.js';
19
+ export type { HubOps } from './ops/hubs.js';
20
+ export type { MLSOps } from './ops/mls.js';
21
+ export type { SyncOps } from './ops/sync.js';
22
+ /**
23
+ * The store's whole surface, one slice per domain. Each slice module owns its
24
+ * tables and its own helpers; this intersection is the only coupling between
25
+ * them, so a caller still sees one flat API.
26
+ */
27
+ export type P2PStoreAPI = GroupOps & CircleOps & DelegationOps & MLSOps & SyncOps & ControlOps & AppLaneOps & HubOps & DiscoveryOps;
28
+ export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter): P2PStoreAPI;
package/lib/api.js CHANGED
@@ -1 +1,22 @@
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 { createAppLaneOps } from './ops/app-lane.js';
2
+ import { createCircleOps } from './ops/circles.js';
3
+ import { createControlOps } from './ops/control.js';
4
+ import { createDelegationOps } from './ops/delegations.js';
5
+ import { createDiscoveryOps } from './ops/discovery.js';
6
+ import { createGroupOps } from './ops/groups.js';
7
+ import { createHubOps } from './ops/hubs.js';
8
+ import { createMLSOps } from './ops/mls.js';
9
+ import { createSyncOps } from './ops/sync.js';
10
+ export function createP2PStore(db, adapter) {
11
+ return {
12
+ ...createGroupOps(db, adapter),
13
+ ...createCircleOps(db, adapter),
14
+ ...createDelegationOps(db),
15
+ ...createMLSOps(db, adapter),
16
+ ...createSyncOps(db, adapter),
17
+ ...createControlOps(db, adapter),
18
+ ...createAppLaneOps(db, adapter),
19
+ ...createHubOps(db, adapter),
20
+ ...createDiscoveryOps(db, adapter)
21
+ };
22
+ }
package/lib/definition.js CHANGED
@@ -1 +1,7 @@
1
- import{createP2PStore as i}from"./api.js";import{getP2PMigrations as o}from"./migrations.js";export const p2pStoreDefinition={name:"p2p",migrations:o,createAPI:i};
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
@@ -1,9 +1,8 @@
1
1
  import type { StoreProvider } from '@kubun/db';
2
2
  import type { P2PStoreAPI } from './api.js';
3
- export type { P2PStoreAPI } from './api.js';
3
+ export type { ControlRequestKind, ControlRequestOutcome, P2PStoreAPI, PutPendingJoinRequestParams, SetLocalPeerProfileParams, UpsertPeerAnnouncementParams, } 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, DelegationToken, DelegationTokenTable, Group, GroupHub, GroupHubTable, GroupMember, GroupMemberTable, GroupMLSState, GroupMLSStateTable, GroupTable, HeldRevocation, Hub, HubTable, InsertCircle, InsertCircleMember, InsertDelegationToken, InsertGroup, InsertGroupHub, InsertGroupMember, InsertGroupMLSState, InsertHub, InsertSyncCheckpoint, InsertSyncPeer, P2PStoreTables, PaginatedResult, PaginationParams, SyncCheckpoint, SyncCheckpointTable, SyncPeer, SyncPeerTable, UpdateGroupMLSState, } from './tables.js';
8
+ export type { AppCursor, AppCursorTable, Circle, CircleMember, CircleMemberTable, CircleTable, ControlRequest, ControlRequestTable, Group, GroupAnchor, GroupAnchorTable, GroupDelegation, GroupDelegationTable, GroupHub, GroupHubTable, GroupMember, GroupMemberTable, GroupMLSState, GroupMLSStateTable, GroupStatus, GroupTable, Hub, HubTable, InsertCircle, InsertCircleMember, InsertGroup, InsertGroupDelegation, InsertGroupHub, InsertGroupMember, InsertGroupMLSState, InsertHub, InsertSyncCheckpoint, InsertSyncPeer, LocalPeerProfile, LocalPeerProfileTable, P2PStoreTables, PaginatedResult, PaginationParams, PeerAnnouncement, PeerAnnouncementTable, PeerCapabilityRecord, PendingJoinRequestRow, PendingJoinRequestTable, SyncCheckpoint, SyncCheckpointTable, SyncPeer, SyncPeerTable, UpdateGroupMLSState, } from './tables.js';
package/lib/index.js CHANGED
@@ -1 +1,6 @@
1
- export{createP2PStore}from"./api.js";export{p2pStoreDefinition}from"./definition.js";export{createP2PRevocationBackend}from"./revocation-backend.js";export const P2P_STORE="p2p";export function getP2PStore(e){return e.getStore("p2p")}
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
+ }
@@ -1,3 +1,24 @@
1
1
  import type { MigrationContext } from '@kubun/db';
2
2
  import type { Migration } from 'kysely/migration';
3
+ /**
4
+ * Partial unique index keeping one commit-hub designation per group. Named here
5
+ * because the store recognises a lost first-bind race by this name appearing in
6
+ * the driver's constraint-violation error.
7
+ */
8
+ export declare const COMMIT_HUB_UNIQUE_INDEX = "uq_p2p_group_hubs_commit";
9
+ /**
10
+ * A shipped migration is immutable: once a database has run it, editing it
11
+ * changes nothing on that database and silently diverges the schema from the
12
+ * code. Every schema change after a release goes in a NEW migration.
13
+ *
14
+ * That rule is suspended for this release, deliberately. The eight migrations
15
+ * this store had accumulated are folded into `0-init`, which is now the whole
16
+ * schema in one readable place. It is a BREAKING change — a database created
17
+ * before it will not pick the new shape up and must be recreated — and it costs
18
+ * nothing here only because the release already requires recreating local
19
+ * databases for unrelated reasons.
20
+ *
21
+ * Reinstate the rule the moment this ships: the next schema change is a new
22
+ * migration, and `0-init` is immutable again.
23
+ */
3
24
  export declare function getP2PMigrations(ctx: MigrationContext): Record<string, Migration>;