@kubun/store-p2p 0.10.0 → 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 +148 -119
- package/lib/api.js +593 -1
- package/lib/definition.js +7 -1
- package/lib/index.d.ts +1 -2
- package/lib/index.js +6 -1
- package/lib/migrations.js +105 -1
- package/lib/tables.d.ts +29 -38
- package/lib/tables.js +2 -1
- package/package.json +11 -10
- package/lib/revocation-backend.d.ts +0 -3
- package/lib/revocation-backend.js +0 -1
package/lib/api.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Adapter } from '@kubun/db-adapter';
|
|
2
|
-
import { type Logger } from '@kubun/logger';
|
|
3
2
|
import type { Kysely } from 'kysely';
|
|
4
|
-
import type { Circle, CircleMember,
|
|
3
|
+
import type { Circle, CircleMember, ControlLedgerEntry, Group, GroupMember, GroupMLSState, Hub, InsertCircle, InsertCircleMember, InsertControlLedgerEntry, InsertGroup, InsertGroupDelegation, InsertGroupHub, InsertGroupMember, InsertGroupMLSState, InsertHub, InsertSyncPeer, P2PStoreTables, PaginatedResult, PaginationParams, SyncPeer } from './tables.js';
|
|
5
4
|
export type P2PStoreAPI = {
|
|
6
5
|
createGroup(group: InsertGroup): Promise<void>;
|
|
7
6
|
getGroup(id: string): Promise<Group | undefined>;
|
|
@@ -10,6 +9,13 @@ export type P2PStoreAPI = {
|
|
|
10
9
|
listGroups(): Promise<Array<Group>>;
|
|
11
10
|
listGroupsOffsetPaged(params: PaginationParams): Promise<PaginatedResult<Group>>;
|
|
12
11
|
addGroupMember(member: InsertGroupMember): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Add (or revive) several members in a single multi-row insert. Same
|
|
14
|
+
* re-add-clears-tombstone semantics as {@link addGroupMember}, applied to every
|
|
15
|
+
* row. A single statement keeps the whole batch atomic and avoids issuing one
|
|
16
|
+
* round-trip per member on a single-connection adapter. A no-op for an empty list.
|
|
17
|
+
*/
|
|
18
|
+
addGroupMembers(members: Array<InsertGroupMember>): Promise<void>;
|
|
13
19
|
/**
|
|
14
20
|
* Soft-delete a member by tombstoning the row with `hlc`. Delegates to
|
|
15
21
|
* `markMemberRemoved`: earliest-wins LWW, ignore-if-absent. The row stays in
|
|
@@ -36,6 +42,34 @@ export type P2PStoreAPI = {
|
|
|
36
42
|
} | undefined>;
|
|
37
43
|
getGroupMember(groupID: string, memberDID: string): Promise<GroupMember | undefined>;
|
|
38
44
|
isGroupMember(groupID: string, memberDID: string): Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* Whether `did` was a member of `groupID` at the given HLC, ignoring role.
|
|
47
|
+
* Mirrors {@link isGroupAdmin}'s removal-boundary semantics without the
|
|
48
|
+
* `role = 'admin'` filter: with no `atHLC`, tombstoned rows are excluded
|
|
49
|
+
* (`removed_at_hlc IS NULL`); with `atHLC`, a member removed at HLC `R` still
|
|
50
|
+
* counts iff `atHLC < R`. Conjoined with ledger admin authority so an
|
|
51
|
+
* MLS-removed member loses admin regardless of the ledger.
|
|
52
|
+
*/
|
|
53
|
+
isGroupMemberAtHLC(groupID: string, did: string, atHLC?: string): Promise<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* Whether `did` holds an active `admin` role in `groupID`. With no `atHLC`,
|
|
56
|
+
* tombstoned rows are ignored (`removed_at_hlc IS NULL`) — the apply-time
|
|
57
|
+
* admin check. With `atHLC`, an admin removed at HLC `R` still counts iff the
|
|
58
|
+
* supplied HLC strictly precedes `R` (`atHLC < R`): a write claiming a moment
|
|
59
|
+
* before the removal is admitted, one at or after it is denied. This mirrors
|
|
60
|
+
* the membership boundary in `checkMembership` (which denies at
|
|
61
|
+
* `removed_at_hlc <= mutationHLC`), so the two checks agree on the
|
|
62
|
+
* remove-boundary instant.
|
|
63
|
+
*/
|
|
64
|
+
isGroupAdmin(groupID: string, did: string, atHLC?: string): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Overlay the role column for an existing member. Update-only: a grant for a
|
|
67
|
+
* DID with no membership row is a no-op, since membership is MLS-native and a
|
|
68
|
+
* non-member can hold no role. Never touches `hlc` or `removed_at_hlc`, so it
|
|
69
|
+
* neither churns membership LWW nor resurrects a tombstone — it is purely the
|
|
70
|
+
* projection target for the admin-roster fold.
|
|
71
|
+
*/
|
|
72
|
+
setGroupMemberRole(groupID: string, memberDID: string, role: string): Promise<void>;
|
|
39
73
|
isMemberOfAnyGroup(viewerDID: string, groupIDs: Array<string>): Promise<boolean>;
|
|
40
74
|
listGroupMembers(groupID: string): Promise<Array<GroupMember>>;
|
|
41
75
|
getGroupsForMember(memberDID: string): Promise<Array<Group>>;
|
|
@@ -50,7 +84,15 @@ export type P2PStoreAPI = {
|
|
|
50
84
|
*/
|
|
51
85
|
getGroupsForMembers(memberDIDs: Array<string>): Promise<Map<string, Array<Group>>>;
|
|
52
86
|
createCircle(circle: InsertCircle): Promise<void>;
|
|
53
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Active-only by default (`removed_at_hlc IS NULL`). Pass
|
|
89
|
+
* `{ includeRemoved: true }` to also see tombstoned circles — used by the
|
|
90
|
+
* apply path so a `circle:create`/`circle:update` can HLC-compare against a
|
|
91
|
+
* tombstoned row and resurrect it, and by the catch-up responder.
|
|
92
|
+
*/
|
|
93
|
+
getCircle(id: string, options?: {
|
|
94
|
+
includeRemoved?: boolean;
|
|
95
|
+
}): Promise<Circle | undefined>;
|
|
54
96
|
/**
|
|
55
97
|
* Batch fetch circles by ID. Returns a Map keyed by ID; missing IDs are
|
|
56
98
|
* absent from the map. Single SQL query with `WHERE id IN (...)`. Empty
|
|
@@ -61,131 +103,93 @@ export type P2PStoreAPI = {
|
|
|
61
103
|
getCircles(ids: Array<string>): Promise<Map<string, Circle>>;
|
|
62
104
|
updateCircle(id: string, update: Partial<Pick<InsertCircle, 'name' | 'description' | 'catalog_ids' | 'hlc'>>): Promise<void>;
|
|
63
105
|
deleteCircle(id: string): Promise<void>;
|
|
64
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Tombstone an existing circle row at `hlc` using earliest-wins LWW: the
|
|
108
|
+
* removal sticks only when no removal is recorded yet, or when `hlc` precedes
|
|
109
|
+
* the stored removal HLC. A removal for a `circleID` with no local row is a
|
|
110
|
+
* no-op — absence means there is no circle to tombstone, and no row is
|
|
111
|
+
* created.
|
|
112
|
+
*/
|
|
113
|
+
markCircleRemoved(circleID: string, hlc: string): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Active-only by default (`removed_at_hlc IS NULL`). Pass
|
|
116
|
+
* `{ includeRemoved: true }` to include tombstoned circles for the catch-up
|
|
117
|
+
* responder.
|
|
118
|
+
*/
|
|
119
|
+
listCirclesByGroup(groupID: string, options?: {
|
|
120
|
+
includeRemoved?: boolean;
|
|
121
|
+
}): Promise<Array<Circle>>;
|
|
65
122
|
addCircleMember(member: InsertCircleMember): Promise<void>;
|
|
66
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Soft-delete a circle member by tombstoning the row with `hlc`: earliest-wins
|
|
125
|
+
* LWW, ignore-if-absent. The row stays in the table so the removal HLC stays
|
|
126
|
+
* comparable.
|
|
127
|
+
*/
|
|
128
|
+
removeCircleMember(circleID: string, memberDID: string, hlc: string): Promise<void>;
|
|
67
129
|
getCircleMember(circleID: string, memberDID: string): Promise<CircleMember | undefined>;
|
|
68
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Active-only by default (`removed_at_hlc IS NULL`). Pass
|
|
132
|
+
* `{ includeRemoved: true }` to include tombstoned members for the catch-up
|
|
133
|
+
* responder.
|
|
134
|
+
*/
|
|
135
|
+
listCircleMembers(circleID: string, options?: {
|
|
136
|
+
includeRemoved?: boolean;
|
|
137
|
+
}): Promise<Array<CircleMember>>;
|
|
69
138
|
getCirclesForMember(memberDID: string): Promise<Array<Circle>>;
|
|
70
139
|
isMemberOfAnyCircle(memberDID: string, circleIDs: Array<string>): Promise<boolean>;
|
|
71
140
|
/**
|
|
72
|
-
*
|
|
73
|
-
* `
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* and
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}): Promise<
|
|
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>;
|
|
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>;
|
|
155
160
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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.
|
|
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.
|
|
165
166
|
*/
|
|
166
|
-
|
|
167
|
+
reconcileCircleMemberProjection(params: {
|
|
168
|
+
circleID: string;
|
|
169
|
+
memberDID: string;
|
|
170
|
+
role: string;
|
|
171
|
+
hlc: string;
|
|
172
|
+
removedAtHLC: string | null;
|
|
173
|
+
}): Promise<void>;
|
|
167
174
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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.
|
|
172
179
|
*/
|
|
173
|
-
|
|
174
|
-
graceSeconds: number;
|
|
175
|
-
}): Promise<number>;
|
|
180
|
+
addGroupDelegation(edge: InsertGroupDelegation): Promise<void>;
|
|
176
181
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* their
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}): Promise<number>;
|
|
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>>;
|
|
189
193
|
saveMLSState(state: InsertGroupMLSState): Promise<void>;
|
|
190
194
|
getMLSState(groupID: string, deviceID: string): Promise<GroupMLSState | undefined>;
|
|
191
195
|
deleteMLSState(groupID: string, deviceID: string): Promise<void>;
|
|
@@ -206,6 +210,31 @@ export type P2PStoreAPI = {
|
|
|
206
210
|
version: string;
|
|
207
211
|
mlsGroupID: string;
|
|
208
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>;
|
|
209
238
|
insertHub(hub: InsertHub): Promise<void>;
|
|
210
239
|
getHub(id: string): Promise<Hub | undefined>;
|
|
211
240
|
getHubByURL(url: string): Promise<Hub | undefined>;
|
|
@@ -222,4 +251,4 @@ export type P2PStoreAPI = {
|
|
|
222
251
|
listGroupsByHubID(hubID: string): Promise<Array<Group>>;
|
|
223
252
|
deleteGroupHubsByHubID(hubID: string): Promise<void>;
|
|
224
253
|
};
|
|
225
|
-
export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter
|
|
254
|
+
export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter): P2PStoreAPI;
|