@kubun/store-p2p 0.8.0 → 0.10.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,29 +1,191 @@
1
1
  import type { Adapter } from '@kubun/db-adapter';
2
+ import { type Logger } from '@kubun/logger';
2
3
  import type { Kysely } from 'kysely';
3
- import type { Circle, CircleMember, Group, GroupMember, GroupMLSState, InsertCircle, InsertCircleMember, InsertGroup, InsertGroupMember, InsertGroupMLSState, InsertSyncPeer, P2PStoreTables, PaginatedResult, PaginationParams, SyncPeer } from './tables.js';
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';
4
5
  export type P2PStoreAPI = {
5
6
  createGroup(group: InsertGroup): Promise<void>;
6
7
  getGroup(id: string): Promise<Group | undefined>;
7
- updateGroup(id: string, update: Partial<Pick<InsertGroup, 'name' | 'description' | 'hub_urls' | 'hlc'>>): Promise<void>;
8
+ updateGroup(id: string, update: Partial<Pick<InsertGroup, 'name' | 'description' | 'hlc'>>): Promise<void>;
8
9
  deleteGroup(id: string): Promise<void>;
9
10
  listGroups(): Promise<Array<Group>>;
10
11
  listGroupsOffsetPaged(params: PaginationParams): Promise<PaginatedResult<Group>>;
11
12
  addGroupMember(member: InsertGroupMember): Promise<void>;
12
- removeGroupMember(groupID: string, memberDID: string): 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>;
13
37
  getGroupMember(groupID: string, memberDID: string): Promise<GroupMember | undefined>;
14
38
  isGroupMember(groupID: string, memberDID: string): Promise<boolean>;
39
+ isMemberOfAnyGroup(viewerDID: string, groupIDs: Array<string>): Promise<boolean>;
15
40
  listGroupMembers(groupID: string): Promise<Array<GroupMember>>;
16
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>>>;
17
52
  createCircle(circle: InsertCircle): Promise<void>;
18
53
  getCircle(id: string): Promise<Circle | undefined>;
54
+ /**
55
+ * Batch fetch circles by ID. Returns a Map keyed by ID; missing IDs are
56
+ * absent from the map. Single SQL query with `WHERE id IN (...)`. Empty
57
+ * input short-circuits with an empty Map (no SQL).
58
+ *
59
+ * Caller is responsible for deduplicating IDs.
60
+ */
61
+ getCircles(ids: Array<string>): Promise<Map<string, Circle>>;
19
62
  updateCircle(id: string, update: Partial<Pick<InsertCircle, 'name' | 'description' | 'catalog_ids' | 'hlc'>>): Promise<void>;
20
63
  deleteCircle(id: string): Promise<void>;
21
64
  listCirclesByGroup(groupID: string): Promise<Array<Circle>>;
22
65
  addCircleMember(member: InsertCircleMember): Promise<void>;
23
66
  removeCircleMember(circleID: string, memberDID: string): Promise<void>;
67
+ getCircleMember(circleID: string, memberDID: string): Promise<CircleMember | undefined>;
24
68
  listCircleMembers(circleID: string): Promise<Array<CircleMember>>;
25
69
  getCirclesForMember(memberDID: string): Promise<Array<Circle>>;
26
70
  isMemberOfAnyCircle(memberDID: string, circleIDs: Array<string>): Promise<boolean>;
71
+ /**
72
+ * Store a delegation token. A re-minted grant covering the same
73
+ * `(grantor, audience, resource)` replaces the existing row when its `hlc`
74
+ * is causally newer; an older or equal `hlc` is ignored. Held rows
75
+ * (`audience` is self) and issued rows (`grantor` is self) are distinct keys
76
+ * and never collide.
77
+ *
78
+ * Returns `true` when the stored row is new or materially different from
79
+ * the prior row (insert, or LWW upsert that changed any of `jti`, `token`,
80
+ * `act`, `exp`, `group_id`). Returns `false` when the call was a no-op:
81
+ * either the incoming `hlc` lost LWW against an existing row, or the LWW
82
+ * upsert won but every content field matched the prior row (identical
83
+ * re-broadcast). Used by emission gates to suppress duplicate
84
+ * `delegationTokenAdded` events on idempotent re-stores.
85
+ */
86
+ addDelegationToken(token: InsertDelegationToken): Promise<boolean>;
87
+ getDelegationTokens(params: {
88
+ grantor: string;
89
+ audience: string;
90
+ }): Promise<Array<DelegationToken>>;
91
+ /**
92
+ * Return the tokens held by `audience` that are still valid at `atTime`
93
+ * (`exp > atTime`). `atTime` is a Unix epoch in seconds, matching the
94
+ * `exp` column. Expired tokens and tokens addressed to other audiences are
95
+ * excluded.
96
+ */
97
+ getHeldTokens(params: {
98
+ audience: string;
99
+ atTime: number;
100
+ }): Promise<Array<DelegationToken>>;
101
+ listIssuedTokens(grantor: string): Promise<Array<DelegationToken>>;
102
+ /**
103
+ * Look up a delegation token by its `jti`. `jti` is not part of the schema's
104
+ * composite PK `(grantor, audience, resource)`, so the column is non-unique
105
+ * at the SQL level; in practice every `grantWriteCapability` call mints a
106
+ * fresh random `jti`, so collisions across distinct `(grantor, audience,
107
+ * resource)` tuples do not occur. Returns the first matching row, or `null`
108
+ * when no row exists for the given `jti`.
109
+ */
110
+ getDelegationTokenByJti(jti: string): Promise<DelegationToken | null>;
111
+ /**
112
+ * Store a revocation record keyed by the revoked capability's `jti`. LWW on
113
+ * `hlc` — a re-broadcast with an older or equal `hlc` is ignored. Returns
114
+ * `true` when the row is new, or when LWW won and any content field
115
+ * (`revoker_did`, `revoked_iat`, `revocation_token`, `verified_at`,
116
+ * `cap_exp`, `group_id`) materially changed. Returns `false` on a no-op
117
+ * (lost LWW, or won LWW with byte-identical content). Used by emission
118
+ * gates to suppress duplicate downstream events on idempotent re-stores.
119
+ *
120
+ * `verified_at` is `null` while the revoker-vs-cap-issuer cross-check is
121
+ * pending; verification flips it via `markRevocationVerified`.
122
+ */
123
+ addRevocation(input: InsertRevokedCapability): Promise<boolean>;
124
+ getRevocation(jti: string): Promise<RevokedCapability | null>;
125
+ /**
126
+ * Hot-path check used at mutation-apply time. Returns `true` only when a
127
+ * row exists for `jti` AND it has been verified against the cap's issuer
128
+ * (`verified_at IS NOT NULL`). Pending (unverified) revocations do not
129
+ * gate writes; the gate fails open for light clients that do not
130
+ * replicate the revocations table.
131
+ */
132
+ isRevoked(jti: string): Promise<boolean>;
133
+ /**
134
+ * Return verified revocations that target capabilities held by `audience`.
135
+ * INNER JOIN against `kubun_p2p_delegation_tokens` on `jti` — verified
136
+ * revocations whose cap row is missing locally are excluded, and
137
+ * `audience`/`grantor` are sourced from the cap row. Pending revocations
138
+ * (`verified_at IS NULL`) are not surfaced. No `atTime` filter: verified
139
+ * revocations remain visible to the audience until lifecycle GC removes
140
+ * them, even after the underlying cap has expired.
141
+ */
142
+ getHeldRevocations(params: {
143
+ audience: string;
144
+ }): Promise<Array<HeldRevocation>>;
145
+ /**
146
+ * Flip a pending revocation to verified. Idempotent: returns `true` only
147
+ * when a previously-pending row was updated; returns `false` when the row
148
+ * is missing or already verified.
149
+ */
150
+ markRevocationVerified(jti: string, params: {
151
+ cap_exp: number;
152
+ verified_at?: number;
153
+ }): Promise<boolean>;
154
+ getPendingRevocationByJti(jti: string): Promise<RevokedCapability | null>;
155
+ /**
156
+ * Delete a pending revocation row (`verified_at IS NULL`) for the given
157
+ * `jti`. Verified rows are preserved — a verified revocation is binding and
158
+ * must not be removed by this method (use `purgeExpiredRevocations` for
159
+ * lifecycle cleanup). Returns `true` when a pending row was deleted, `false`
160
+ * otherwise (no row, or row already verified).
161
+ *
162
+ * Used by the cap-arrival cross-check: when a held cap arrives whose issuer
163
+ * does not match a pre-existing pending revocation's `revoker_did`, the
164
+ * pending row is treated as a forged broadcast and dropped.
165
+ */
166
+ deletePendingRevocation(jti: string): Promise<boolean>;
167
+ /**
168
+ * Delete verified revocation rows whose underlying capability expired more
169
+ * than `graceSeconds` ago (`cap_exp + graceSeconds < now`). Rows with a
170
+ * NULL `cap_exp` and pending (unverified) rows are preserved — they have
171
+ * no expiry to compare against. Returns the number of rows deleted.
172
+ */
173
+ purgeExpiredRevocations(params: {
174
+ graceSeconds: number;
175
+ }): Promise<number>;
176
+ /**
177
+ * Delete pending revocation rows (`verified_at IS NULL`) whose `created_at`
178
+ * is older than `maxAgeSeconds`. Verified rows are preserved regardless of
179
+ * their `created_at` age. Returns the number of rows deleted.
180
+ *
181
+ * A negative `maxAgeSeconds` pushes the threshold into the future and will
182
+ * delete every pending row regardless of age — intended only for test
183
+ * scaffolding (avoids needing raw-SQL `UPDATE` to backdate `created_at`).
184
+ * Do not pass negative values from production code paths.
185
+ */
186
+ purgeDeadPendingRevocations(params: {
187
+ maxAgeSeconds: number;
188
+ }): Promise<number>;
27
189
  saveMLSState(state: InsertGroupMLSState): Promise<void>;
28
190
  getMLSState(groupID: string, deviceID: string): Promise<GroupMLSState | undefined>;
29
191
  deleteMLSState(groupID: string, deviceID: string): Promise<void>;
@@ -34,5 +196,30 @@ export type P2PStoreAPI = {
34
196
  listSyncPeers(): Promise<Array<SyncPeer>>;
35
197
  updateSyncPeer(peerDID: string, update: Partial<InsertSyncPeer>): Promise<void>;
36
198
  removeSyncPeer(peerDID: string): Promise<void>;
199
+ recordBroadcast(entry: {
200
+ docID: string;
201
+ version: string;
202
+ mlsGroupID: string;
203
+ }): Promise<boolean>;
204
+ hasBroadcast(entry: {
205
+ docID: string;
206
+ version: string;
207
+ mlsGroupID: string;
208
+ }): Promise<boolean>;
209
+ insertHub(hub: InsertHub): Promise<void>;
210
+ getHub(id: string): Promise<Hub | undefined>;
211
+ getHubByURL(url: string): Promise<Hub | undefined>;
212
+ updateHub(id: string, update: Partial<Pick<InsertHub, 'label' | 'url' | 'server_did'>>): Promise<void>;
213
+ deleteHub(id: string): Promise<void>;
214
+ listHubs(): Promise<Array<Hub>>;
215
+ insertGroupHub(binding: InsertGroupHub): Promise<boolean>;
216
+ deleteGroupHub(params: {
217
+ groupID: string;
218
+ hubID: string;
219
+ }): Promise<boolean>;
220
+ listGroupIDsByHubID(hubID: string): Promise<Array<string>>;
221
+ listHubsByGroupID(groupID: string): Promise<Array<Hub>>;
222
+ listGroupsByHubID(hubID: string): Promise<Array<Group>>;
223
+ deleteGroupHubsByHubID(hubID: string): Promise<void>;
37
224
  };
38
- export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter): P2PStoreAPI;
225
+ export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter, logger?: Logger): P2PStoreAPI;
package/lib/api.js CHANGED
@@ -1 +1 @@
1
- export function createP2PStore(e,r){return{async createGroup(t){await e.insertInto("kubun_p2p_groups").values({id:t.id,name:t.name,description:t.description,created_by:t.created_by,hub_urls:r.encodeJSON(t.hub_urls??[]),hlc:t.hlc}).execute()},getGroup:async r=>await e.selectFrom("kubun_p2p_groups").selectAll().where("id","=",r).executeTakeFirst(),async updateGroup(t,l){let c={};null!=l.name&&(c.name=l.name),null!=l.description&&(c.description=l.description),null!=l.hub_urls&&(c.hub_urls=r.encodeJSON(l.hub_urls)),null!=l.hlc&&(c.hlc=l.hlc),c.updated_at=r.encodeTimestamp(new Date),await e.updateTable("kubun_p2p_groups").set(c).where("id","=",t).execute()},async deleteGroup(r){await e.deleteFrom("kubun_p2p_groups").where("id","=",r).execute()},listGroups:async()=>await e.selectFrom("kubun_p2p_groups").selectAll().execute(),async listGroupsOffsetPaged(r){let{limit:t,offset:l}=r,c=e.selectFrom("kubun_p2p_groups").selectAll().orderBy("name");if(null!=l&&(c=c.offset(l)),null!=t){c=c.limit(t+1);let e=await c.execute(),r=e.length>t;return{entries:r?e.slice(0,t):e,hasMore:r}}return{entries:await c.execute(),hasMore:!1}},async addGroupMember(t){await e.insertInto("kubun_p2p_group_members").values({group_id:t.group_id,member_did:t.member_did,role:t.role}).onConflict(e=>e.columns(["group_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),updated_at:r.encodeTimestamp(new Date)}))).execute()},async removeGroupMember(r,t){await e.deleteFrom("kubun_p2p_group_members").where("group_id","=",r).where("member_did","=",t).execute()},getGroupMember:async(r,t)=>await e.selectFrom("kubun_p2p_group_members").selectAll().where("group_id","=",r).where("member_did","=",t).executeTakeFirst(),isGroupMember:async(r,t)=>null!=await e.selectFrom("kubun_p2p_group_members").select("member_did").where("group_id","=",r).where("member_did","=",t).executeTakeFirst(),listGroupMembers:async r=>await e.selectFrom("kubun_p2p_group_members").selectAll().where("group_id","=",r).execute(),getGroupsForMember:async r=>await e.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","=",r).execute(),async createCircle(t){await e.insertInto("kubun_p2p_circles").values({id:t.id,group_id:t.group_id,name:t.name,description:t.description,catalog_ids:r.encodeJSON(t.catalog_ids),hlc:t.hlc}).execute()},getCircle:async r=>await e.selectFrom("kubun_p2p_circles").selectAll().where("id","=",r).executeTakeFirst(),async updateCircle(t,l){let c={};null!=l.name&&(c.name=l.name),null!=l.description&&(c.description=l.description),null!=l.catalog_ids&&(c.catalog_ids=r.encodeJSON(l.catalog_ids)),null!=l.hlc&&(c.hlc=l.hlc),c.updated_at=r.encodeTimestamp(new Date),await e.updateTable("kubun_p2p_circles").set(c).where("id","=",t).execute()},async deleteCircle(r){await e.deleteFrom("kubun_p2p_circles").where("id","=",r).execute()},listCirclesByGroup:async r=>await e.selectFrom("kubun_p2p_circles").selectAll().where("group_id","=",r).execute(),async addCircleMember(r){await e.insertInto("kubun_p2p_circle_members").values({circle_id:r.circle_id,member_did:r.member_did,role:r.role,hlc:r.hlc}).onConflict(e=>e.columns(["circle_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc")}))).execute()},async removeCircleMember(r,t){await e.deleteFrom("kubun_p2p_circle_members").where("circle_id","=",r).where("member_did","=",t).execute()},listCircleMembers:async r=>await e.selectFrom("kubun_p2p_circle_members").selectAll().where("circle_id","=",r).execute(),getCirclesForMember:async r=>await e.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","=",r).execute(),isMemberOfAnyCircle:async(r,t)=>0!==t.length&&null!=await e.selectFrom("kubun_p2p_circle_members").select("circle_id").where("member_did","=",r).where("circle_id","in",t).limit(1).executeTakeFirst(),async saveMLSState(t){await e.insertInto("kubun_p2p_group_mls_state").values({group_id:t.group_id,device_id:t.device_id,mls_state:r.encodeBinary(t.mls_state),credential:t.credential,epoch:t.epoch,root_capability:t.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:r.encodeTimestamp(new Date)}))).execute()},getMLSState:async(r,t)=>await e.selectFrom("kubun_p2p_group_mls_state").selectAll().where("group_id","=",r).where("device_id","=",t).executeTakeFirst(),async deleteMLSState(r,t){await e.deleteFrom("kubun_p2p_group_mls_state").where("group_id","=",r).where("device_id","=",t).execute()},listMLSStates:async r=>await e.selectFrom("kubun_p2p_group_mls_state").selectAll().where("group_id","=",r).execute(),async deleteGroupMLSStates(r){await e.deleteFrom("kubun_p2p_group_mls_state").where("group_id","=",r).execute()},async addSyncPeer(t){let l=r.encodeJSON(t.allowed_users),c=r.encodeJSON(t.config);await e.insertInto("kubun_p2p_sync_peers").values({...t,allowed_users:l,config:c}).onConflict(e=>e.column("peer_did").doUpdateSet({endpoint:t.endpoint,mode:t.mode,allowed_users:l,priority:t.priority,trust_level:t.trust_level,config:c,updated_at:t.updated_at})).execute()},getSyncPeer:async r=>await e.selectFrom("kubun_p2p_sync_peers").selectAll().where("peer_did","=",r).executeTakeFirst(),listSyncPeers:async()=>await e.selectFrom("kubun_p2p_sync_peers").selectAll().execute(),async updateSyncPeer(t,l){let c={};null!=l.endpoint&&(c.endpoint=l.endpoint),null!=l.mode&&(c.mode=l.mode),null!=l.allowed_users&&(c.allowed_users=r.encodeJSON(l.allowed_users)),null!=l.priority&&(c.priority=l.priority),null!=l.trust_level&&(c.trust_level=l.trust_level),null!=l.config&&(c.config=r.encodeJSON(l.config)),c.updated_at=Date.now(),await e.updateTable("kubun_p2p_sync_peers").set(c).where("peer_did","=",t).execute()},async removeSyncPeer(r){await e.deleteFrom("kubun_p2p_sync_peers").where("peer_did","=",r).execute()}}}
1
+ import{getKubunLogger as e}from"@kubun/logger";export function createP2PStore(t,r,u=e("store-p2p")){let i={async createGroup(e){await t.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 t.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=r.encodeTimestamp(new Date),await t.updateTable("kubun_p2p_groups").set(i).where("id","=",e).execute()},async deleteGroup(e){await t.deleteFrom("kubun_p2p_groups").where("id","=",e).execute()},listGroups:async()=>await t.selectFrom("kubun_p2p_groups").selectAll().execute(),async listGroupsOffsetPaged(e){let{limit:r,offset:u}=e,i=t.selectFrom("kubun_p2p_groups").selectAll().orderBy("name");if(null!=u&&(i=i.offset(u)),null!=r){i=i.limit(r+1);let e=await i.execute(),t=e.length>r;return{entries:t?e.slice(0,r):e,hasMore:t}}return{entries:await i.execute(),hasMore:!1}},async addGroupMember(e){await t.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:r.encodeTimestamp(new Date)}))).execute()},async markMemberRemoved(e,u,i){await t.updateTable("kubun_p2p_group_members").set({removed_at_hlc:i,updated_at:r.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,r){let u=await t.selectFrom("kubun_p2p_group_members").select("removed_at_hlc").where("group_id","=",e).where("member_did","=",r).executeTakeFirst();return null==u?void 0:{removed_at_hlc:u.removed_at_hlc}},async removeGroupMember(e,t,r){await i.markMemberRemoved(e,t,r)},getGroupMember:async(e,r)=>await t.selectFrom("kubun_p2p_group_members").selectAll().where("group_id","=",e).where("member_did","=",r).where("removed_at_hlc","is",null).executeTakeFirst(),isGroupMember:async(e,r)=>null!=await t.selectFrom("kubun_p2p_group_members").select("member_did").where("group_id","=",e).where("member_did","=",r).where("removed_at_hlc","is",null).executeTakeFirst(),isMemberOfAnyGroup:async(e,r)=>0!==r.length&&null!=await t.selectFrom("kubun_p2p_group_members").select("group_id").where("member_did","=",e).where("group_id","in",r).where("removed_at_hlc","is",null).limit(1).executeTakeFirst(),listGroupMembers:async e=>await t.selectFrom("kubun_p2p_group_members").selectAll().where("group_id","=",e).where("removed_at_hlc","is",null).execute(),getGroupsForMember:async e=>await t.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 r=new Map;if(0===e.length)return r;for(let u of(await t.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,...t}=u,i=r.get(e);null!=i?i.push(t):r.set(e,[t])}return r},async createCircle(e){await t.insertInto("kubun_p2p_circles").values({id:e.id,group_id:e.group_id,name:e.name,description:e.description,catalog_ids:r.encodeJSON(e.catalog_ids),hlc:e.hlc}).execute()},getCircle:async e=>await t.selectFrom("kubun_p2p_circles").selectAll().where("id","=",e).executeTakeFirst(),async getCircles(e){let r=new Map;if(0===e.length)return r;for(let u of(await t.selectFrom("kubun_p2p_circles").selectAll().where("id","in",e).execute()))r.set(u.id,u);return r},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=r.encodeJSON(u.catalog_ids)),null!=u.hlc&&(i.hlc=u.hlc),i.updated_at=r.encodeTimestamp(new Date),await t.updateTable("kubun_p2p_circles").set(i).where("id","=",e).execute()},async deleteCircle(e){await t.deleteFrom("kubun_p2p_circles").where("id","=",e).execute()},listCirclesByGroup:async e=>await t.selectFrom("kubun_p2p_circles").selectAll().where("group_id","=",e).execute(),async addCircleMember(e){await t.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")}))).execute()},async removeCircleMember(e,r){await t.deleteFrom("kubun_p2p_circle_members").where("circle_id","=",e).where("member_did","=",r).execute()},getCircleMember:async(e,r)=>await t.selectFrom("kubun_p2p_circle_members").selectAll().where("circle_id","=",e).where("member_did","=",r).executeTakeFirst(),listCircleMembers:async e=>await t.selectFrom("kubun_p2p_circle_members").selectAll().where("circle_id","=",e).execute(),getCirclesForMember:async e=>await t.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).execute(),isMemberOfAnyCircle:async(e,r)=>0!==r.length&&null!=await t.selectFrom("kubun_p2p_circle_members").select("circle_id").where("member_did","=",e).where("circle_id","in",r).limit(1).executeTakeFirst(),async addDelegationToken(e){let a,_=await t.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 t.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:r.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 t.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("grantor","=",e.grantor).where("audience","=",e.audience).execute(),getHeldTokens:async e=>await t.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 t.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("grantor","=",e).execute(),getDelegationTokenByJti:async e=>await t.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("jti","=",e).executeTakeFirst()??null,async addRevocation(e){let u=await t.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 t.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:r.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 t.selectFrom("kubun_p2p_revoked_capabilities").selectAll().where("jti","=",e).executeTakeFirst()??null,isRevoked:async e=>null!=await t.selectFrom("kubun_p2p_revoked_capabilities").select("jti").where("jti","=",e).where("verified_at","is not",null).limit(1).executeTakeFirst(),getHeldRevocations:async e=>(await t.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 t.updateTable("kubun_p2p_revoked_capabilities").set({verified_at:i,cap_exp:u.cap_exp,updated_at:r.encodeTimestamp(new Date)}).where("jti","=",e).where("verified_at","is",null).executeTakeFirst()).numUpdatedRows??0n)>0n},getPendingRevocationByJti:async e=>await t.selectFrom("kubun_p2p_revoked_capabilities").selectAll().where("jti","=",e).where("verified_at","is",null).executeTakeFirst()??null,deletePendingRevocation:async e=>((await t.deleteFrom("kubun_p2p_revoked_capabilities").where("jti","=",e).where("verified_at","is",null).executeTakeFirst()).numDeletedRows??0n)>0n,async purgeExpiredRevocations(e){let r=Math.floor(Date.now()/1e3)-e.graceSeconds;return Number((await t.deleteFrom("kubun_p2p_revoked_capabilities").where("verified_at","is not",null).where("cap_exp","is not",null).where("cap_exp","<",r).executeTakeFirst()).numDeletedRows??0n)},async purgeDeadPendingRevocations(e){let u=r.encodeTimestamp(new Date(Date.now()-1e3*e.maxAgeSeconds));return Number((await t.deleteFrom("kubun_p2p_revoked_capabilities").where("verified_at","is",null).where("created_at","<",u).executeTakeFirst()).numDeletedRows??0n)},async saveMLSState(e){await t.insertInto("kubun_p2p_group_mls_state").values({group_id:e.group_id,device_id:e.device_id,mls_state:r.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:r.encodeTimestamp(new Date)}))).execute()},getMLSState:async(e,r)=>await t.selectFrom("kubun_p2p_group_mls_state").selectAll().where("group_id","=",e).where("device_id","=",r).executeTakeFirst(),async deleteMLSState(e,r){await t.deleteFrom("kubun_p2p_group_mls_state").where("group_id","=",e).where("device_id","=",r).execute()},listMLSStates:async e=>await t.selectFrom("kubun_p2p_group_mls_state").selectAll().where("group_id","=",e).execute(),async deleteGroupMLSStates(e){await t.deleteFrom("kubun_p2p_group_mls_state").where("group_id","=",e).execute()},async addSyncPeer(e){let u=r.encodeJSON(e.allowed_users),i=r.encodeJSON(e.config);await t.insertInto("kubun_p2p_sync_peers").values({...e,allowed_users:u,config:i}).onConflict(t=>t.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 t.selectFrom("kubun_p2p_sync_peers").selectAll().where("peer_did","=",e).executeTakeFirst(),listSyncPeers:async()=>await t.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=r.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=r.encodeJSON(u.config)),i.updated_at=Date.now(),await t.updateTable("kubun_p2p_sync_peers").set(i).where("peer_did","=",e).execute()},async removeSyncPeer(e){await t.deleteFrom("kubun_p2p_sync_peers").where("peer_did","=",e).execute()},recordBroadcast:async e=>((await t.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 t.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 t.insertInto("kubun_p2p_hubs").values({id:e.id,label:e.label,url:e.url,server_did:e.server_did}).execute()},getHub:async e=>await t.selectFrom("kubun_p2p_hubs").selectAll().where("id","=",e).executeTakeFirst(),getHubByURL:async e=>await t.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=r.encodeTimestamp(new Date),await t.updateTable("kubun_p2p_hubs").set(i).where("id","=",e).execute()},async deleteHub(e){await t.deleteFrom("kubun_p2p_hubs").where("id","=",e).execute()},listHubs:async()=>await t.selectFrom("kubun_p2p_hubs").selectAll().execute(),insertGroupHub:async e=>((await t.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 t.deleteFrom("kubun_p2p_group_hubs").where("group_id","=",e.groupID).where("hub_id","=",e.hubID).executeTakeFirst()).numDeletedRows??0n)>0n,listGroupIDsByHubID:async e=>(await t.selectFrom("kubun_p2p_group_hubs").select("group_id").where("hub_id","=",e).execute()).map(e=>e.group_id),listHubsByGroupID:async e=>await t.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 t.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 t.deleteFrom("kubun_p2p_group_hubs").where("hub_id","=",e).execute()}};return i}
package/lib/index.d.ts CHANGED
@@ -3,6 +3,7 @@ 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';
6
7
  export declare const P2P_STORE: "p2p";
7
8
  export declare function getP2PStore(provider: StoreProvider): Promise<P2PStoreAPI>;
8
- export type { Circle, CircleMember, CircleMemberTable, CircleTable, Group, GroupMember, GroupMemberTable, GroupMLSState, GroupMLSStateTable, GroupTable, InsertCircle, InsertCircleMember, InsertGroup, InsertGroupMember, InsertGroupMLSState, InsertSyncCheckpoint, InsertSyncPeer, P2PStoreTables, PaginatedResult, PaginationParams, SyncCheckpoint, SyncCheckpointTable, SyncPeer, SyncPeerTable, UpdateGroupMLSState, } from './tables.js';
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';
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- export{createP2PStore}from"./api.js";export{p2pStoreDefinition}from"./definition.js";export const P2P_STORE="p2p";export function getP2PStore(t){return t.getStore("p2p")}
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,3 +1,3 @@
1
1
  import type { MigrationContext } from '@kubun/db';
2
- import type { Migration } from 'kysely';
2
+ import type { Migration } from 'kysely/migration';
3
3
  export declare function getP2PMigrations(ctx: MigrationContext): Record<string, Migration>;
package/lib/migrations.js CHANGED
@@ -1 +1 @@
1
- export function getP2PMigrations(e){let t=e.types,a=e.functions.now;return{"0-init":{async up(e){await e.schema.createTable("kubun_p2p_groups").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("created_by",t.text,e=>e.notNull()).addColumn("hub_urls",t.json,e=>e.defaultTo("[]").notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createTable("kubun_p2p_group_members").ifNotExists().addColumn("group_id",t.text,e=>e.notNull()).addColumn("member_did",t.text,e=>e.notNull()).addColumn("role",t.text,e=>e.notNull().defaultTo("member")).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_p2p_group_members",["group_id","member_did"]).execute(),await e.schema.createIndex("idx_p2p_group_members_did").ifNotExists().on("kubun_p2p_group_members").column("member_did").execute(),await e.schema.createTable("kubun_p2p_circles").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("group_id",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("catalog_ids",t.json,e=>e.notNull().defaultTo("[]")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createIndex("idx_p2p_circles_group").ifNotExists().on("kubun_p2p_circles").column("group_id").execute(),await e.schema.createTable("kubun_p2p_circle_members").ifNotExists().addColumn("circle_id",t.text,e=>e.notNull()).addColumn("member_did",t.text,e=>e.notNull()).addColumn("role",t.text,e=>e.notNull().defaultTo("member")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_p2p_circle_members",["circle_id","member_did"]).execute(),await e.schema.createIndex("idx_p2p_circle_members_did").ifNotExists().on("kubun_p2p_circle_members").column("member_did").execute(),await e.schema.createTable("kubun_p2p_group_mls_state").ifNotExists().addColumn("group_id",t.text,e=>e.notNull()).addColumn("device_id",t.text,e=>e.notNull()).addColumn("mls_state",t.binary,e=>e.notNull()).addColumn("credential",t.text,e=>e.notNull()).addColumn("epoch","integer",e=>e.notNull()).addColumn("root_capability",t.text,e=>e.notNull().defaultTo("")).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a).notNull()).addPrimaryKeyConstraint("pk_p2p_group_mls_state",["group_id","device_id"]).execute(),await e.schema.createTable("kubun_p2p_sync_peers").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull().unique()).addColumn("endpoint",t.text,e=>e.notNull()).addColumn("mode",t.text,e=>e.notNull()).addColumn("allowed_users",t.json,e=>e.notNull()).addColumn("priority",t.text,e=>e.notNull().defaultTo("0")).addColumn("trust_level",t.text,e=>e.notNull()).addColumn("config",t.json,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a).notNull()).execute(),await e.schema.createTable("kubun_p2p_sync_checkpoints").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull()).addColumn("scope",t.text,e=>e.notNull()).addColumn("checkpoint_hlc",t.text,e=>e.notNull()).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a).notNull()).execute()},async down(e){await e.schema.dropTable("kubun_p2p_sync_checkpoints").ifExists().execute(),await e.schema.dropTable("kubun_p2p_sync_peers").ifExists().execute(),await e.schema.dropTable("kubun_p2p_group_mls_state").ifExists().execute(),await e.schema.dropTable("kubun_p2p_circle_members").ifExists().execute(),await e.schema.dropTable("kubun_p2p_circles").ifExists().execute(),await e.schema.dropTable("kubun_p2p_group_members").ifExists().execute(),await e.schema.dropTable("kubun_p2p_groups").ifExists().execute()}}}}
1
+ export function getP2PMigrations(e){let t=e.types,a=e.functions.now;return{"0-init":{async up(e){await e.schema.createTable("kubun_p2p_groups").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("created_by",t.text,e=>e.notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createTable("kubun_p2p_group_members").ifNotExists().addColumn("group_id",t.text,e=>e.notNull()).addColumn("member_did",t.text,e=>e.notNull()).addColumn("role",t.text,e=>e.notNull().defaultTo("member")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("removed_at_hlc",t.text).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_p2p_group_members",["group_id","member_did"]).execute(),await e.schema.createIndex("idx_p2p_group_members_did").ifNotExists().on("kubun_p2p_group_members").column("member_did").execute(),await e.schema.createTable("kubun_p2p_circles").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("group_id",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("catalog_ids",t.json,e=>e.notNull().defaultTo("[]")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createIndex("idx_p2p_circles_group").ifNotExists().on("kubun_p2p_circles").column("group_id").execute(),await e.schema.createTable("kubun_p2p_circle_members").ifNotExists().addColumn("circle_id",t.text,e=>e.notNull()).addColumn("member_did",t.text,e=>e.notNull()).addColumn("role",t.text,e=>e.notNull().defaultTo("member")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_p2p_circle_members",["circle_id","member_did"]).execute(),await e.schema.createIndex("idx_p2p_circle_members_did").ifNotExists().on("kubun_p2p_circle_members").column("member_did").execute(),await e.schema.createTable("kubun_p2p_delegation_tokens").ifNotExists().addColumn("jti",t.text,e=>e.notNull()).addColumn("grantor",t.text,e=>e.notNull()).addColumn("audience",t.text,e=>e.notNull()).addColumn("token",t.text,e=>e.notNull()).addColumn("resource",t.text,e=>e.notNull()).addColumn("act",t.text,e=>e.notNull()).addColumn("exp","integer",e=>e.notNull()).addColumn("group_id",t.text,e=>e.notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_p2p_delegation_tokens",["grantor","audience","resource"]).execute(),await e.schema.createIndex("idx_p2p_delegation_tokens_grantor_audience").ifNotExists().on("kubun_p2p_delegation_tokens").columns(["grantor","audience"]).execute(),await e.schema.createIndex("idx_p2p_delegation_tokens_audience_exp").ifNotExists().on("kubun_p2p_delegation_tokens").columns(["audience","exp"]).execute(),await e.schema.createIndex("idx_p2p_delegation_tokens_jti").ifNotExists().on("kubun_p2p_delegation_tokens").column("jti").execute(),await e.schema.createTable("kubun_p2p_revoked_capabilities").ifNotExists().addColumn("jti",t.text,e=>e.notNull().primaryKey()).addColumn("revoker_did",t.text,e=>e.notNull()).addColumn("revoked_iat","integer",e=>e.notNull()).addColumn("revocation_token",t.text,e=>e.notNull()).addColumn("verified_at","integer").addColumn("cap_exp","integer").addColumn("group_id",t.text,e=>e.notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createIndex("idx_p2p_revoked_capabilities_verified_cap_exp").ifNotExists().on("kubun_p2p_revoked_capabilities").columns(["verified_at","cap_exp"]).execute(),await e.schema.createTable("kubun_p2p_group_mls_state").ifNotExists().addColumn("group_id",t.text,e=>e.notNull()).addColumn("device_id",t.text,e=>e.notNull()).addColumn("mls_state",t.binary,e=>e.notNull()).addColumn("credential",t.text,e=>e.notNull()).addColumn("epoch","integer",e=>e.notNull()).addColumn("root_capability",t.text,e=>e.notNull().defaultTo("")).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a).notNull()).addPrimaryKeyConstraint("pk_p2p_group_mls_state",["group_id","device_id"]).execute(),await e.schema.createTable("kubun_p2p_sync_peers").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull().unique()).addColumn("endpoint",t.text,e=>e.notNull()).addColumn("mode",t.text,e=>e.notNull()).addColumn("allowed_users",t.json,e=>e.notNull()).addColumn("priority",t.text,e=>e.notNull().defaultTo("0")).addColumn("trust_level",t.text,e=>e.notNull()).addColumn("config",t.json,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a).notNull()).execute(),await e.schema.createTable("kubun_p2p_sync_checkpoints").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull()).addColumn("scope",t.text,e=>e.notNull()).addColumn("checkpoint_hlc",t.text,e=>e.notNull()).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a).notNull()).execute(),await e.schema.createTable("kubun_p2p_broadcast_log").ifNotExists().addColumn("doc_id",t.text,e=>e.notNull()).addColumn("version",t.text,e=>e.notNull()).addColumn("mls_group_id",t.text,e=>e.notNull()).addColumn("broadcast_at",t.timestamp,e=>e.defaultTo(a).notNull()).addPrimaryKeyConstraint("pk_p2p_broadcast_log",["doc_id","version","mls_group_id"]).execute(),await e.schema.createIndex("idx_p2p_broadcast_log_doc").ifNotExists().on("kubun_p2p_broadcast_log").column("doc_id").execute(),await e.schema.createTable("kubun_p2p_hubs").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("label",t.text).addColumn("url",t.text,e=>e.notNull().unique()).addColumn("server_did",t.text).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createTable("kubun_p2p_group_hubs").ifNotExists().addColumn("group_id",t.text,e=>e.notNull()).addColumn("hub_id",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a).notNull()).addPrimaryKeyConstraint("pk_p2p_group_hubs",["group_id","hub_id"]).execute(),await e.schema.createIndex("idx_p2p_group_hubs_hub").ifNotExists().on("kubun_p2p_group_hubs").column("hub_id").execute()},async down(e){await e.schema.dropTable("kubun_p2p_group_hubs").ifExists().execute(),await e.schema.dropTable("kubun_p2p_hubs").ifExists().execute(),await e.schema.dropTable("kubun_p2p_broadcast_log").ifExists().execute(),await e.schema.dropTable("kubun_p2p_sync_checkpoints").ifExists().execute(),await e.schema.dropTable("kubun_p2p_sync_peers").ifExists().execute(),await e.schema.dropTable("kubun_p2p_group_mls_state").ifExists().execute(),await e.schema.dropTable("kubun_p2p_revoked_capabilities").ifExists().execute(),await e.schema.dropTable("kubun_p2p_delegation_tokens").ifExists().execute(),await e.schema.dropTable("kubun_p2p_circle_members").ifExists().execute(),await e.schema.dropTable("kubun_p2p_circles").ifExists().execute(),await e.schema.dropTable("kubun_p2p_group_members").ifExists().execute(),await e.schema.dropTable("kubun_p2p_groups").ifExists().execute()}}}}
@@ -0,0 +1,3 @@
1
+ import type { RevocationBackend } from '@enkaku/capability';
2
+ import type { P2PStoreAPI } from './api.js';
3
+ export declare function createP2PRevocationBackend(api: P2PStoreAPI): RevocationBackend;
@@ -0,0 +1 @@
1
+ export function createP2PRevocationBackend(e){return{async add(e){},isRevoked:async a=>await e.isRevoked(a)}}
package/lib/tables.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CreatedAtColumn, UpdatedAtColumn } from '@kubun/db-adapter';
2
2
  import type { ColumnType, Insertable, Selectable, Updateable } from 'kysely';
3
3
  type DefaultedStringColumn = ColumnType<string, string | undefined, string>;
4
+ type NullableStringColumn = ColumnType<string | null, string | null | undefined, string | null>;
4
5
  type JSONArrayColumn<T> = ColumnType<Array<T>, Array<T> | undefined, Array<T>>;
5
6
  type JSONValueColumn<T> = ColumnType<T, T, T>;
6
7
  export type GroupTable = {
@@ -8,7 +9,6 @@ export type GroupTable = {
8
9
  name: string;
9
10
  description: DefaultedStringColumn;
10
11
  created_by: string;
11
- hub_urls: JSONArrayColumn<string>;
12
12
  hlc: string;
13
13
  created_at: CreatedAtColumn;
14
14
  updated_at: UpdatedAtColumn;
@@ -19,6 +19,8 @@ export type GroupMemberTable = {
19
19
  group_id: string;
20
20
  member_did: string;
21
21
  role: DefaultedStringColumn;
22
+ hlc: string;
23
+ removed_at_hlc: NullableStringColumn;
22
24
  created_at: CreatedAtColumn;
23
25
  updated_at: UpdatedAtColumn;
24
26
  };
@@ -46,6 +48,45 @@ export type CircleMemberTable = {
46
48
  };
47
49
  export type CircleMember = Selectable<CircleMemberTable>;
48
50
  export type InsertCircleMember = Insertable<CircleMemberTable>;
51
+ export type DelegationTokenTable = {
52
+ jti: string;
53
+ grantor: string;
54
+ audience: string;
55
+ token: string;
56
+ resource: string;
57
+ act: string;
58
+ exp: number;
59
+ group_id: string;
60
+ hlc: string;
61
+ created_at: CreatedAtColumn;
62
+ updated_at: UpdatedAtColumn;
63
+ };
64
+ export type DelegationToken = Selectable<DelegationTokenTable>;
65
+ export type InsertDelegationToken = Insertable<DelegationTokenTable>;
66
+ export type RevokedCapabilityTable = {
67
+ jti: string;
68
+ revoker_did: string;
69
+ revoked_iat: number;
70
+ revocation_token: string;
71
+ verified_at: number | null;
72
+ cap_exp: number | null;
73
+ group_id: string;
74
+ hlc: string;
75
+ created_at: CreatedAtColumn;
76
+ updated_at: UpdatedAtColumn;
77
+ };
78
+ export type RevokedCapability = Selectable<RevokedCapabilityTable>;
79
+ export type InsertRevokedCapability = Insertable<RevokedCapabilityTable>;
80
+ export type HeldRevocation = {
81
+ jti: string;
82
+ grantor: string;
83
+ audience: string;
84
+ revoker_did: string;
85
+ revoked_iat: number;
86
+ verified_at: number;
87
+ cap_exp: number | null;
88
+ group_id: string;
89
+ };
49
90
  export type GroupMLSStateTable = {
50
91
  group_id: string;
51
92
  device_id: string;
@@ -83,6 +124,31 @@ export type SyncCheckpointTable = {
83
124
  };
84
125
  export type SyncCheckpoint = Selectable<SyncCheckpointTable>;
85
126
  export type InsertSyncCheckpoint = Insertable<SyncCheckpointTable>;
127
+ export type BroadcastLogTable = {
128
+ doc_id: string;
129
+ version: string;
130
+ mls_group_id: string;
131
+ broadcast_at: CreatedAtColumn;
132
+ };
133
+ export type BroadcastLogEntry = Selectable<BroadcastLogTable>;
134
+ export type InsertBroadcastLogEntry = Insertable<BroadcastLogTable>;
135
+ export type HubTable = {
136
+ id: string;
137
+ label: string | null;
138
+ url: string;
139
+ server_did: string | null;
140
+ created_at: CreatedAtColumn;
141
+ updated_at: UpdatedAtColumn;
142
+ };
143
+ export type Hub = Selectable<HubTable>;
144
+ export type InsertHub = Insertable<HubTable>;
145
+ export type GroupHubTable = {
146
+ group_id: string;
147
+ hub_id: string;
148
+ created_at: CreatedAtColumn;
149
+ };
150
+ export type GroupHub = Selectable<GroupHubTable>;
151
+ export type InsertGroupHub = Insertable<GroupHubTable>;
86
152
  export type PaginationParams = {
87
153
  limit?: number;
88
154
  offset?: number;
@@ -96,8 +162,13 @@ export type P2PStoreTables = {
96
162
  kubun_p2p_group_members: GroupMemberTable;
97
163
  kubun_p2p_circles: CircleTable;
98
164
  kubun_p2p_circle_members: CircleMemberTable;
165
+ kubun_p2p_delegation_tokens: DelegationTokenTable;
166
+ kubun_p2p_revoked_capabilities: RevokedCapabilityTable;
99
167
  kubun_p2p_group_mls_state: GroupMLSStateTable;
100
168
  kubun_p2p_sync_peers: SyncPeerTable;
101
169
  kubun_p2p_sync_checkpoints: SyncCheckpointTable;
170
+ kubun_p2p_broadcast_log: BroadcastLogTable;
171
+ kubun_p2p_hubs: HubTable;
172
+ kubun_p2p_group_hubs: GroupHubTable;
102
173
  };
103
174
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/store-p2p",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "license": "see LICENSE.md",
5
5
  "keywords": [],
6
6
  "type": "module",
@@ -15,14 +15,17 @@
15
15
  ],
16
16
  "sideEffects": false,
17
17
  "dependencies": {
18
- "kysely": "^0.28.16",
19
- "@kubun/db": "^0.8.1",
20
- "@kubun/db-adapter": "^0.8.1"
18
+ "@enkaku/capability": "^0.16.0",
19
+ "kysely": "^0.29.2",
20
+ "@kubun/db": "^0.10.0",
21
+ "@kubun/db-adapter": "^0.10.0",
22
+ "@kubun/logger": "^0.10.0"
21
23
  },
22
24
  "devDependencies": {
23
- "@testcontainers/postgresql": "^11.14.0",
24
- "@kubun/db-better-sqlite": "^0.8.1",
25
- "@kubun/db-postgres": "^0.8.1"
25
+ "@testcontainers/postgresql": "^12.0.1",
26
+ "@kubun/db-better-sqlite": "^0.10.0",
27
+ "@kubun/db-postgres": "^0.10.0",
28
+ "@kubun/hlc": "^0.10.0"
26
29
  },
27
30
  "scripts": {
28
31
  "build:clean": "del lib",