@kubun/store-p2p 0.17.0 → 0.18.1

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.
Files changed (41) hide show
  1. package/lib/api.d.ts +24 -6
  2. package/lib/api.js +17 -2
  3. package/lib/definition.js +1 -1
  4. package/lib/hub-url.d.ts +1 -0
  5. package/lib/hub-url.js +3 -0
  6. package/lib/index.d.ts +8 -2
  7. package/lib/index.js +6 -0
  8. package/lib/migrations.js +153 -9
  9. package/lib/ops/app-lane.d.ts +32 -2
  10. package/lib/ops/app-lane.js +55 -2
  11. package/lib/ops/app-outbox.d.ts +44 -0
  12. package/lib/ops/app-outbox.js +236 -0
  13. package/lib/ops/catalog-offers.d.ts +18 -0
  14. package/lib/ops/catalog-offers.js +59 -0
  15. package/lib/ops/catalog-routes.d.ts +25 -0
  16. package/lib/ops/catalog-routes.js +60 -0
  17. package/lib/ops/catalog-sync.d.ts +122 -0
  18. package/lib/ops/catalog-sync.js +251 -0
  19. package/lib/ops/circles.d.ts +3 -4
  20. package/lib/ops/circles.js +31 -14
  21. package/lib/ops/control.d.ts +6 -0
  22. package/lib/ops/control.js +12 -3
  23. package/lib/ops/delegations.d.ts +31 -0
  24. package/lib/ops/delegations.js +71 -0
  25. package/lib/ops/document-sweep.d.ts +11 -0
  26. package/lib/ops/document-sweep.js +16 -0
  27. package/lib/ops/groups.d.ts +70 -22
  28. package/lib/ops/groups.js +130 -35
  29. package/lib/ops/hubs.js +8 -4
  30. package/lib/ops/log-handler-completion.d.ts +20 -0
  31. package/lib/ops/log-handler-completion.js +41 -0
  32. package/lib/ops/mls.d.ts +39 -6
  33. package/lib/ops/mls.js +115 -21
  34. package/lib/ops/own-agents.d.ts +68 -0
  35. package/lib/ops/own-agents.js +359 -0
  36. package/lib/ops/sync.d.ts +16 -0
  37. package/lib/ops/sync.js +25 -0
  38. package/lib/owner-classification.d.ts +14 -0
  39. package/lib/owner-classification.js +18 -0
  40. package/lib/tables.d.ts +193 -5
  41. package/package.json +16 -11
@@ -0,0 +1,14 @@
1
+ import type { StoreProvider } from '@kubun/db';
2
+ export type DocumentOwnerClass = 'own' | 'foreign' | 'unresolved';
3
+ export type ClassifyDocumentOwnerParams = {
4
+ ownerDID: string;
5
+ /** The controller DID whose documents this device keeps in custody. Not checked here. */
6
+ custodyOwner: string;
7
+ stores: StoreProvider;
8
+ };
9
+ /**
10
+ * Historical, not current eligibility: a revoked or departed device still owns
11
+ * what it wrote. `foreign` needs positive evidence; a missing binding is not
12
+ * evidence, so anything unattributed is `unresolved` and must be kept.
13
+ */
14
+ export declare function classifyDocumentOwner(params: ClassifyDocumentOwnerParams): Promise<DocumentOwnerClass>;
@@ -0,0 +1,18 @@
1
+ import { DID_PREFIX } from '@kokuin/controller';
2
+ import { normalizeDID } from '@kokuin/token';
3
+ /**
4
+ * Historical, not current eligibility: a revoked or departed device still owns
5
+ * what it wrote. `foreign` needs positive evidence; a missing binding is not
6
+ * evidence, so anything unattributed is `unresolved` and must be kept.
7
+ */ export async function classifyDocumentOwner(params) {
8
+ const owner = normalizeDID(params.ownerDID);
9
+ const custody = normalizeDID(params.custodyOwner);
10
+ if (owner === custody) return 'own';
11
+ const store = await params.stores.getStore('p2p');
12
+ const controllers = await store.listBindingControllers(owner);
13
+ // A DID also bound to another controller stays own: a receipt costs less than a lost document.
14
+ if (controllers.includes(custody)) return 'own';
15
+ // `did:kokuin:` is self-certifying: kumiai accepts a leaf binding only to a log folding to it.
16
+ if (owner.startsWith(DID_PREFIX)) return 'foreign';
17
+ return controllers.length > 0 ? 'foreign' : 'unresolved';
18
+ }
package/lib/tables.d.ts CHANGED
@@ -1,24 +1,28 @@
1
1
  import type { CreatedAtColumn, UpdatedAtColumn } from '@kubun/db-adapter';
2
+ import type { AnchorSlot, AppOutboxEntry, CommitCursor } from '@kumiai/rpc';
2
3
  import type { ColumnType, Insertable, Selectable, Updateable } from 'kysely';
3
4
  type DefaultedStringColumn = ColumnType<string, string | undefined, string>;
4
5
  type DefaultedNumberColumn = ColumnType<number, number | undefined, number>;
5
6
  type NullableStringColumn = ColumnType<string | null, string | null | undefined, string | null>;
7
+ type NullableNumberColumn = ColumnType<number | null, number | null | undefined, number | null>;
6
8
  type JSONArrayColumn<T> = ColumnType<Array<T>, Array<T> | undefined, Array<T>>;
7
9
  type JSONValueColumn<T> = ColumnType<T, T, T>;
8
10
  /**
9
11
  * Whether this device belongs to the group right now.
10
12
  *
11
- * Only the two states that have a writer. An `invited` state would have none:
13
+ * An `invited` state would have no writer:
12
14
  * a group row is written when a join COMPLETES, so there is no moment at which
13
15
  * a row exists and the device has merely been asked.
14
16
  */
15
- export type GroupStatus = 'joined' | 'left';
17
+ export type GroupStatus = 'joined' | 'leaving' | 'left';
18
+ export type GroupKind = 'standard' | 'ownAgents';
16
19
  export type GroupTable = {
17
20
  id: string;
18
21
  name: string;
19
22
  description: DefaultedStringColumn;
20
23
  created_by: string;
21
24
  hlc: string;
25
+ kind: ColumnType<GroupKind, GroupKind | undefined, GroupKind>;
22
26
  /**
23
27
  * Leaving keeps the row: its circles, circle members, peer announcements and
24
28
  * hub bindings hang off it, and a device invited back would otherwise have to
@@ -28,11 +32,22 @@ export type GroupTable = {
28
32
  * row for it.
29
33
  */
30
34
  status: DefaultedStringColumn;
35
+ /**
36
+ * Set in the same transaction as every handle advance, to a token naming that
37
+ * advance; cleared only by a projection reconcile that started from the same
38
+ * token. NULL means the control projections match the current handle.
39
+ */
40
+ projections_dirty: NullableStringColumn;
41
+ /**
42
+ * The handle epoch the last successful reconcile caught the projections up
43
+ * to. NULL until a reconcile completes.
44
+ */
45
+ projections_epoch: NullableNumberColumn;
31
46
  created_at: CreatedAtColumn;
32
47
  updated_at: UpdatedAtColumn;
33
48
  };
34
49
  export type Group = Selectable<GroupTable>;
35
- export type InsertGroup = Insertable<GroupTable>;
50
+ export type InsertGroup = Omit<Insertable<GroupTable>, 'kind'>;
36
51
  export type GroupMemberTable = {
37
52
  group_id: string;
38
53
  member_did: string;
@@ -44,12 +59,71 @@ export type GroupMemberTable = {
44
59
  };
45
60
  export type GroupMember = Selectable<GroupMemberTable>;
46
61
  export type InsertGroupMember = Insertable<GroupMemberTable>;
62
+ export type CatalogOfferTable = {
63
+ owner: string;
64
+ exposure_id: string;
65
+ catalog_id: string;
66
+ group_id: string;
67
+ circle_id: string;
68
+ };
69
+ export type CatalogOfferRow = Selectable<CatalogOfferTable>;
70
+ export type CatalogRouteTable = {
71
+ owner: string;
72
+ catalog_id: string;
73
+ group_id: string;
74
+ pull: number;
75
+ push: number;
76
+ };
77
+ /** A group this device has stopped serving document data for; absence means served. */
78
+ export type UnservedGroupTable = {
79
+ group_id: string;
80
+ };
81
+ export type CatalogCursorTable = {
82
+ peer_did: string;
83
+ group_id: string;
84
+ rules_hash: string;
85
+ last_key: NullableStringColumn;
86
+ last_token: NullableStringColumn;
87
+ cycle_clear: DefaultedNumberColumn;
88
+ /** JSON object: bound-free rule key to its absolute `{ from, to }`. */
89
+ bounds: string;
90
+ };
91
+ export type DocumentRepairTable = {
92
+ group_id: string;
93
+ document_id: string;
94
+ listed_by: NullableStringColumn;
95
+ holdings_digest: string;
96
+ created_at: number;
97
+ attempted_at: number;
98
+ };
99
+ export type DocumentRepairAnswerTable = {
100
+ group_id: string;
101
+ document_id: string;
102
+ peer_did: string;
103
+ answer: string;
104
+ };
105
+ export type ObservationRepairScheduleTable = {
106
+ document_id: string;
107
+ fetch_hlc: string;
108
+ attempts: number;
109
+ next_attempt_at: number;
110
+ tried_count: number;
111
+ };
112
+ export type ObservationRepairPeerTable = {
113
+ document_id: string;
114
+ peer_did: string;
115
+ };
116
+ export type GroupLeaveRequestTable = {
117
+ group_id: string;
118
+ member_did: string;
119
+ hlc: string;
120
+ };
121
+ export type GroupLeaveRequest = Selectable<GroupLeaveRequestTable>;
47
122
  export type CircleTable = {
48
123
  id: string;
49
124
  group_id: string;
50
125
  name: string;
51
126
  description: DefaultedStringColumn;
52
- catalog_ids: JSONArrayColumn<string>;
53
127
  hlc: string;
54
128
  removed_at_hlc: NullableStringColumn;
55
129
  open: DefaultedNumberColumn;
@@ -72,6 +146,10 @@ export type InsertCircleMember = Insertable<CircleMemberTable>;
72
146
  export type GroupDelegationTable = {
73
147
  group_id: string;
74
148
  jti: string;
149
+ pending_grant_hlc: NullableStringColumn;
150
+ pending_grant_digest: NullableStringColumn;
151
+ pending_revocation_hlc: NullableStringColumn;
152
+ pending_revocation_digest: NullableStringColumn;
75
153
  };
76
154
  export type GroupDelegation = Selectable<GroupDelegationTable>;
77
155
  export type InsertGroupDelegation = Insertable<GroupDelegationTable>;
@@ -81,6 +159,12 @@ export type GroupMLSStateTable = {
81
159
  mls_state: Uint8Array;
82
160
  credential: string;
83
161
  epoch: number;
162
+ /** Orders secret-tree mutations within one MLS epoch. */
163
+ state_version: DefaultedNumberColumn;
164
+ /** Retained absence marker; preserves the row version across leave and rejoin. */
165
+ is_deleted: DefaultedNumberColumn;
166
+ /** Exclusive upper bound of sender generations reserved durably for this epoch. */
167
+ sender_generation_hwm: DefaultedNumberColumn;
84
168
  /**
85
169
  * The handle's signed control-ledger tokens, JSON-encoded in enactment order.
86
170
  * Replayed into `restoreGroup` so a restored handle folds the same roster it
@@ -120,6 +204,14 @@ export type BroadcastLogTable = {
120
204
  };
121
205
  export type BroadcastLogEntry = Selectable<BroadcastLogTable>;
122
206
  export type InsertBroadcastLogEntry = Insertable<BroadcastLogTable>;
207
+ export type LogHandlerCompletionTable = {
208
+ group_id: string;
209
+ procedure: string;
210
+ entity: string;
211
+ hlc: string;
212
+ notify: DefaultedNumberColumn;
213
+ completed: DefaultedNumberColumn;
214
+ };
123
215
  export type ControlRequestTable = {
124
216
  id: string;
125
217
  group_id: string;
@@ -129,6 +221,7 @@ export type ControlRequestTable = {
129
221
  result: NullableStringColumn;
130
222
  enqueued_at: string;
131
223
  settled_at: NullableStringColumn;
224
+ settled_epoch: NullableNumberColumn;
132
225
  };
133
226
  export type ControlRequest = Selectable<ControlRequestTable>;
134
227
  export type InsertControlRequest = Insertable<ControlRequestTable>;
@@ -142,16 +235,34 @@ export type CommitJournalEntryTable = {
142
235
  bodies: JSONArrayColumn<string>;
143
236
  kind: string;
144
237
  journal: Uint8Array;
238
+ holds_log_sends: DefaultedNumberColumn;
239
+ };
240
+ export type CommitJournalEntry = Omit<Selectable<CommitJournalEntryTable>, 'holds_log_sends'> & {
241
+ holds_log_sends: boolean;
145
242
  };
146
- export type CommitJournalEntry = Selectable<CommitJournalEntryTable>;
147
243
  export type InsertCommitJournalEntry = Insertable<CommitJournalEntryTable>;
148
244
  export type GroupAnchorTable = {
149
245
  group_id: string;
150
246
  secret: Uint8Array;
151
247
  epoch: number;
248
+ pending: ColumnType<AnchorSlot['pending'] | null, AnchorSlot['pending'] | null, AnchorSlot['pending'] | null>;
152
249
  };
153
250
  export type GroupAnchor = Selectable<GroupAnchorTable>;
154
251
  export type InsertGroupAnchor = Insertable<GroupAnchorTable>;
252
+ export type AppOutboxTable = {
253
+ group_id: string;
254
+ seq: number;
255
+ protocol: string;
256
+ prc: string;
257
+ data: Uint8Array;
258
+ iv: Uint8Array;
259
+ last_attempt: JSONValueColumn<AppOutboxEntry['lastAttempt']>;
260
+ };
261
+ export type AppOutboxStateTable = {
262
+ group_id: string;
263
+ key: ColumnType<Uint8Array | null, Uint8Array | null | undefined, Uint8Array | null>;
264
+ commit_cursor: ColumnType<CommitCursor | null, CommitCursor | null | undefined, CommitCursor | null>;
265
+ };
155
266
  export type AppCursorTable = {
156
267
  group_id: string;
157
268
  topic_id: string;
@@ -159,6 +270,19 @@ export type AppCursorTable = {
159
270
  };
160
271
  export type AppCursor = Selectable<AppCursorTable>;
161
272
  export type InsertAppCursor = Insertable<AppCursorTable>;
273
+ export type PendingAppFrameTable = {
274
+ group_id: string;
275
+ device_id: string;
276
+ frame_id: string;
277
+ topic_id: string;
278
+ protocol: string;
279
+ segment: number;
280
+ position: string;
281
+ payload: Uint8Array;
282
+ sender_did: string;
283
+ created_at: CreatedAtColumn;
284
+ };
285
+ export type PendingAppFrameRow = Selectable<PendingAppFrameTable>;
162
286
  export type HubTable = {
163
287
  id: string;
164
288
  label: string | null;
@@ -222,9 +346,69 @@ export type PaginatedResult<T> = {
222
346
  entries: Array<T>;
223
347
  hasMore: boolean;
224
348
  };
349
+ export type OwnAgentsRecordTable = {
350
+ type: string;
351
+ owner: string;
352
+ id: string;
353
+ hlc: string;
354
+ op_jwt: string;
355
+ op_hash: string;
356
+ payload: Uint8Array | null;
357
+ payload_digest: string | null;
358
+ deleted: number;
359
+ meta: JSONValueColumn<Record<string, unknown>>;
360
+ created_at: CreatedAtColumn;
361
+ };
362
+ export type OwnAgentsGrantKind = 'trustedGrant' | 'leaf' | 'leafDirect';
363
+ export type OwnAgentsGrantTable = {
364
+ controller_did: string;
365
+ kind: OwnAgentsGrantKind;
366
+ token: string;
367
+ issued_at: number;
368
+ expires_at: number;
369
+ };
370
+ export type OwnAgentsJoinCapabilityTable = {
371
+ group_id: string;
372
+ recipient_did: string;
373
+ capability_id: string;
374
+ token: string;
375
+ expires_at: number;
376
+ signer_did: NullableStringColumn;
377
+ signer_leaf_key: ColumnType<Uint8Array | null, Uint8Array | null | undefined, Uint8Array | null>;
378
+ signer_capability: NullableStringColumn;
379
+ signer_prefix: ColumnType<{
380
+ events: Array<unknown>;
381
+ } | null, {
382
+ events: Array<unknown>;
383
+ } | null | undefined, {
384
+ events: Array<unknown>;
385
+ } | null>;
386
+ };
387
+ export type BindingEvidenceTable = {
388
+ controller_did: string;
389
+ device_did: string;
390
+ first_seen_at: CreatedAtColumn;
391
+ };
225
392
  export type P2PStoreTables = {
393
+ kubun_p2p_access_default_revocations: {
394
+ controller_did: string;
395
+ device_did: string;
396
+ };
397
+ kubun_p2p_binding_evidence: BindingEvidenceTable;
398
+ kubun_p2p_own_agents_records: OwnAgentsRecordTable;
399
+ kubun_p2p_own_agents_grants: OwnAgentsGrantTable;
400
+ kubun_p2p_own_agents_join_capabilities: OwnAgentsJoinCapabilityTable;
226
401
  kubun_p2p_groups: GroupTable;
227
402
  kubun_p2p_group_members: GroupMemberTable;
403
+ kubun_p2p_catalog_offers: CatalogOfferTable;
404
+ kubun_p2p_catalog_routes: CatalogRouteTable;
405
+ kubun_p2p_unserved_groups: UnservedGroupTable;
406
+ kubun_p2p_catalog_cursors: CatalogCursorTable;
407
+ kubun_p2p_document_repairs: DocumentRepairTable;
408
+ kubun_p2p_document_repair_answers: DocumentRepairAnswerTable;
409
+ kubun_p2p_observation_repairs: ObservationRepairScheduleTable;
410
+ kubun_p2p_observation_repair_peers: ObservationRepairPeerTable;
411
+ kubun_p2p_group_leave_requests: GroupLeaveRequestTable;
228
412
  kubun_p2p_circles: CircleTable;
229
413
  kubun_p2p_circle_members: CircleMemberTable;
230
414
  kubun_p2p_group_delegation: GroupDelegationTable;
@@ -232,10 +416,14 @@ export type P2PStoreTables = {
232
416
  kubun_p2p_sync_peers: SyncPeerTable;
233
417
  kubun_p2p_sync_checkpoints: SyncCheckpointTable;
234
418
  kubun_p2p_broadcast_log: BroadcastLogTable;
419
+ kubun_p2p_log_handler_completion: LogHandlerCompletionTable;
235
420
  kubun_p2p_control_requests: ControlRequestTable;
236
421
  kubun_p2p_commit_journal: CommitJournalEntryTable;
237
422
  kubun_p2p_group_anchor: GroupAnchorTable;
423
+ kubun_p2p_app_outbox: AppOutboxTable;
424
+ kubun_p2p_app_outbox_state: AppOutboxStateTable;
238
425
  kubun_p2p_app_cursor: AppCursorTable;
426
+ kubun_p2p_app_pending: PendingAppFrameTable;
239
427
  kubun_p2p_hubs: HubTable;
240
428
  kubun_p2p_group_hubs: GroupHubTable;
241
429
  kubun_p2p_peer_announcements: PeerAnnouncementTable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/store-p2p",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -15,18 +15,23 @@
15
15
  "LICENSE.md"
16
16
  ],
17
17
  "dependencies": {
18
- "@kokuin/capability": "^0.3.0",
19
- "@kokuin/token": "^0.5.0",
20
- "@kubun/db": "^0.17.0",
21
- "@kubun/db-adapter": "^0.17.0",
22
- "@kubun/logger": "^0.17.0",
23
- "kysely": "^0.29.5"
18
+ "@kokuin/capability": "^0.4.2",
19
+ "@kokuin/controller": "^0.1.0",
20
+ "@kokuin/token": "^0.5.1",
21
+ "@kubun/db": "^0.18.0",
22
+ "@kubun/db-adapter": "^0.18.0",
23
+ "@kubun/logger": "^0.18.0",
24
+ "@kubun/records": "^0.18.0",
25
+ "@kumiai/rpc": "^0.10.9",
26
+ "@noble/ciphers": "^2.4.0",
27
+ "@sozai/runtime": "^0.1.0",
28
+ "kysely": "^0.29.7"
24
29
  },
25
30
  "devDependencies": {
26
- "@kubun/db-better-sqlite": "^0.17.0",
27
- "@kubun/db-postgres": "^0.17.0",
28
- "@kubun/hlc": "^0.17.0",
29
- "@testcontainers/postgresql": "^12.1.0"
31
+ "@kubun/db-better-sqlite": "^0.18.0",
32
+ "@kubun/db-postgres": "^0.18.0",
33
+ "@kubun/hlc": "^0.18.0",
34
+ "@testcontainers/postgresql": "^12.2.1"
30
35
  },
31
36
  "publishConfig": {
32
37
  "access": "public"