@kubun/store-p2p 0.10.1 → 0.11.0

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