@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.
- package/lib/api.d.ts +24 -6
- package/lib/api.js +17 -2
- package/lib/definition.js +1 -1
- package/lib/hub-url.d.ts +1 -0
- package/lib/hub-url.js +3 -0
- package/lib/index.d.ts +8 -2
- package/lib/index.js +6 -0
- package/lib/migrations.js +153 -9
- package/lib/ops/app-lane.d.ts +32 -2
- package/lib/ops/app-lane.js +55 -2
- package/lib/ops/app-outbox.d.ts +44 -0
- package/lib/ops/app-outbox.js +236 -0
- package/lib/ops/catalog-offers.d.ts +18 -0
- package/lib/ops/catalog-offers.js +59 -0
- package/lib/ops/catalog-routes.d.ts +25 -0
- package/lib/ops/catalog-routes.js +60 -0
- package/lib/ops/catalog-sync.d.ts +122 -0
- package/lib/ops/catalog-sync.js +251 -0
- package/lib/ops/circles.d.ts +3 -4
- package/lib/ops/circles.js +31 -14
- package/lib/ops/control.d.ts +6 -0
- package/lib/ops/control.js +12 -3
- package/lib/ops/delegations.d.ts +31 -0
- package/lib/ops/delegations.js +71 -0
- package/lib/ops/document-sweep.d.ts +11 -0
- package/lib/ops/document-sweep.js +16 -0
- package/lib/ops/groups.d.ts +70 -22
- package/lib/ops/groups.js +130 -35
- package/lib/ops/hubs.js +8 -4
- package/lib/ops/log-handler-completion.d.ts +20 -0
- package/lib/ops/log-handler-completion.js +41 -0
- package/lib/ops/mls.d.ts +39 -6
- package/lib/ops/mls.js +115 -21
- package/lib/ops/own-agents.d.ts +68 -0
- package/lib/ops/own-agents.js +359 -0
- package/lib/ops/sync.d.ts +16 -0
- package/lib/ops/sync.js +25 -0
- package/lib/owner-classification.d.ts +14 -0
- package/lib/owner-classification.js +18 -0
- package/lib/tables.d.ts +193 -5
- package/package.json +16 -11
package/lib/api.d.ts
CHANGED
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
import type { Adapter } from '@kubun/db-adapter';
|
|
2
|
+
import { type Runtime } from '@sozai/runtime';
|
|
2
3
|
import type { Kysely } from 'kysely';
|
|
3
4
|
import { type AppLaneOps } from './ops/app-lane.js';
|
|
5
|
+
import { type AppOutboxOps } from './ops/app-outbox.js';
|
|
6
|
+
import { type CatalogOfferOps } from './ops/catalog-offers.js';
|
|
7
|
+
import { type CatalogRouteOps } from './ops/catalog-routes.js';
|
|
8
|
+
import { type CatalogSyncOps } from './ops/catalog-sync.js';
|
|
4
9
|
import { type CircleOps } from './ops/circles.js';
|
|
5
10
|
import { type ControlOps } from './ops/control.js';
|
|
6
11
|
import { type DelegationOps } from './ops/delegations.js';
|
|
7
12
|
import { type DiscoveryOps } from './ops/discovery.js';
|
|
13
|
+
import { type DocumentSweepOps } from './ops/document-sweep.js';
|
|
8
14
|
import { type GroupOps } from './ops/groups.js';
|
|
9
15
|
import { type HubOps } from './ops/hubs.js';
|
|
16
|
+
import { type LogHandlerCompletionOps } from './ops/log-handler-completion.js';
|
|
10
17
|
import { type MLSOps } from './ops/mls.js';
|
|
18
|
+
import { type OwnAgentsOps } from './ops/own-agents.js';
|
|
11
19
|
import { type SyncOps } from './ops/sync.js';
|
|
12
20
|
import type { P2PStoreTables } from './tables.js';
|
|
13
|
-
export type { AppLaneOps } from './ops/app-lane.js';
|
|
21
|
+
export type { AppLaneOps, OpenedAppFrameExpectedVersion, OpenedAppFrameRecord, SaveOpenedAppFrameParams, } from './ops/app-lane.js';
|
|
22
|
+
export type { AppOutboxOps } from './ops/app-outbox.js';
|
|
23
|
+
export type { CatalogOffer, CatalogOfferOps } from './ops/catalog-offers.js';
|
|
24
|
+
export type { CatalogRoute, CatalogRouteOps } from './ops/catalog-routes.js';
|
|
25
|
+
export type { CatalogCursorBounds, CatalogCursorState, CatalogSyncOps, DocumentRepairAnswerKind, DocumentRepairRow, DueObservationRepairQuery, ObservationRepairPosition, ObservationRepairSchedule, RecordDocumentRepairParams, } from './ops/catalog-sync.js';
|
|
14
26
|
export type { CircleOps } from './ops/circles.js';
|
|
15
27
|
export type { CommitJournalEntryKind, ControlOps, ControlRequestKind, ControlRequestOutcome, } from './ops/control.js';
|
|
16
28
|
export type { DelegationOps } from './ops/delegations.js';
|
|
17
29
|
export type { DiscoveryOps, PutPendingJoinRequestParams, SetLocalPeerProfileParams, UpsertPeerAnnouncementParams, } from './ops/discovery.js';
|
|
18
|
-
export type {
|
|
30
|
+
export type { DocumentSweepOps } from './ops/document-sweep.js';
|
|
31
|
+
export type { GetMemberRemovalsParams, GroupOps } from './ops/groups.js';
|
|
19
32
|
export type { HubOps } from './ops/hubs.js';
|
|
20
|
-
export type {
|
|
21
|
-
export type {
|
|
33
|
+
export type { BeginLogHandlerCompletionParams, LogHandlerCompletionKey, LogHandlerCompletionOps, } from './ops/log-handler-completion.js';
|
|
34
|
+
export type { MLSOps, ReserveSenderGenerationsParams } from './ops/mls.js';
|
|
35
|
+
export type { BindingEvidence, OwnAgentsGrant, OwnAgentsJoinCapability, OwnAgentsOps, OwnAgentsRecord, OwnAgentsRecordKey, } from './ops/own-agents.js';
|
|
36
|
+
export type { PruneBroadcastLogParams, SyncOps } from './ops/sync.js';
|
|
22
37
|
/**
|
|
23
38
|
* The store's whole surface, one slice per domain. Each slice module owns its
|
|
24
39
|
* tables and its own helpers; this intersection is the only coupling between
|
|
25
40
|
* them, so a caller still sees one flat API.
|
|
26
41
|
*/
|
|
27
|
-
export type P2PStoreAPI = GroupOps & CircleOps & DelegationOps & MLSOps & SyncOps & ControlOps & AppLaneOps & HubOps & DiscoveryOps;
|
|
28
|
-
export
|
|
42
|
+
export type P2PStoreAPI = GroupOps & CatalogOfferOps & CatalogRouteOps & CatalogSyncOps & CircleOps & DelegationOps & MLSOps & SyncOps & ControlOps & AppLaneOps & AppOutboxOps & LogHandlerCompletionOps & HubOps & DiscoveryOps & DocumentSweepOps & OwnAgentsOps;
|
|
43
|
+
export type CreateP2PStoreOptions = {
|
|
44
|
+
getRandomValues?: Runtime['getRandomValues'];
|
|
45
|
+
};
|
|
46
|
+
export declare function createP2PStore(db: Kysely<P2PStoreTables>, adapter: Adapter, options?: CreateP2PStoreOptions): P2PStoreAPI;
|
package/lib/api.js
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
|
+
import { createRuntime } from '@sozai/runtime';
|
|
1
2
|
import { createAppLaneOps } from './ops/app-lane.js';
|
|
3
|
+
import { createAppOutboxOps } from './ops/app-outbox.js';
|
|
4
|
+
import { createCatalogOfferOps } from './ops/catalog-offers.js';
|
|
5
|
+
import { createCatalogRouteOps } from './ops/catalog-routes.js';
|
|
6
|
+
import { createCatalogSyncOps } from './ops/catalog-sync.js';
|
|
2
7
|
import { createCircleOps } from './ops/circles.js';
|
|
3
8
|
import { createControlOps } from './ops/control.js';
|
|
4
9
|
import { createDelegationOps } from './ops/delegations.js';
|
|
5
10
|
import { createDiscoveryOps } from './ops/discovery.js';
|
|
11
|
+
import { createDocumentSweepOps } from './ops/document-sweep.js';
|
|
6
12
|
import { createGroupOps } from './ops/groups.js';
|
|
7
13
|
import { createHubOps } from './ops/hubs.js';
|
|
14
|
+
import { createLogHandlerCompletionOps } from './ops/log-handler-completion.js';
|
|
8
15
|
import { createMLSOps } from './ops/mls.js';
|
|
16
|
+
import { createOwnAgentsOps } from './ops/own-agents.js';
|
|
9
17
|
import { createSyncOps } from './ops/sync.js';
|
|
10
|
-
export function createP2PStore(db, adapter) {
|
|
18
|
+
export function createP2PStore(db, adapter, options = {}) {
|
|
11
19
|
return {
|
|
12
20
|
...createGroupOps(db, adapter),
|
|
21
|
+
...createOwnAgentsOps(db, adapter),
|
|
22
|
+
...createCatalogOfferOps(db),
|
|
23
|
+
...createCatalogRouteOps(db),
|
|
24
|
+
...createCatalogSyncOps(db, adapter),
|
|
13
25
|
...createCircleOps(db, adapter),
|
|
14
26
|
...createDelegationOps(db),
|
|
15
27
|
...createMLSOps(db, adapter),
|
|
16
28
|
...createSyncOps(db, adapter),
|
|
17
29
|
...createControlOps(db, adapter),
|
|
18
30
|
...createAppLaneOps(db, adapter),
|
|
31
|
+
...createAppOutboxOps(db, adapter, options.getRandomValues ?? createRuntime().getRandomValues),
|
|
19
32
|
...createHubOps(db, adapter),
|
|
20
|
-
...
|
|
33
|
+
...createLogHandlerCompletionOps(db),
|
|
34
|
+
...createDiscoveryOps(db, adapter),
|
|
35
|
+
...createDocumentSweepOps(db)
|
|
21
36
|
};
|
|
22
37
|
}
|
package/lib/definition.js
CHANGED
package/lib/hub-url.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function canonicalHubURL(url: string): string;
|
package/lib/hub-url.js
ADDED
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { StoreProvider } from '@kubun/db';
|
|
2
2
|
import type { P2PStoreAPI } from './api.js';
|
|
3
|
-
export type { ControlRequestKind, ControlRequestOutcome, P2PStoreAPI, PutPendingJoinRequestParams, SetLocalPeerProfileParams, UpsertPeerAnnouncementParams, } from './api.js';
|
|
3
|
+
export type { BeginLogHandlerCompletionParams, BindingEvidence, CatalogCursorBounds, CatalogCursorState, CatalogOffer, CatalogRoute, CatalogSyncOps, ControlRequestKind, ControlRequestOutcome, CreateP2PStoreOptions, DocumentRepairAnswerKind, DocumentRepairRow, DueObservationRepairQuery, GetMemberRemovalsParams, LogHandlerCompletionKey, LogHandlerCompletionOps, ObservationRepairPosition, ObservationRepairSchedule, OpenedAppFrameExpectedVersion, OpenedAppFrameRecord, OwnAgentsGrant, OwnAgentsJoinCapability, OwnAgentsOps, OwnAgentsRecord, OwnAgentsRecordKey, P2PStoreAPI, PruneBroadcastLogParams, PutPendingJoinRequestParams, RecordDocumentRepairParams, ReserveSenderGenerationsParams, SaveOpenedAppFrameParams, SetLocalPeerProfileParams, UpsertPeerAnnouncementParams, } from './api.js';
|
|
4
4
|
export { createP2PStore } from './api.js';
|
|
5
5
|
export { p2pStoreDefinition } from './definition.js';
|
|
6
|
+
export { canonicalHubURL } from './hub-url.js';
|
|
7
|
+
export { AppOutboxDecryptionError, type AppOutboxPortOptions, AppOutboxTransactionError, type ClearAppOutboxRowsOptions, PendingRevocationAdmissionError, UndeliveredRevocationsError, } from './ops/app-outbox.js';
|
|
8
|
+
export { MAX_REPAIRS_PER_LISTER } from './ops/catalog-sync.js';
|
|
9
|
+
export { BroadcastLogPruneParamsError } from './ops/sync.js';
|
|
10
|
+
export { type ClassifyDocumentOwnerParams, classifyDocumentOwner, type DocumentOwnerClass, } from './owner-classification.js';
|
|
6
11
|
export declare const P2P_STORE: "p2p";
|
|
7
12
|
export declare function getP2PStore(provider: StoreProvider): Promise<P2PStoreAPI>;
|
|
8
|
-
export
|
|
13
|
+
export { OwnAgentsGroupNotFoundError, OwnAgentsKindDowngradeError, OwnAgentsRecordError, OwnAgentsTransactionError, ownAgentsRecordLockKey, } from './ops/own-agents.js';
|
|
14
|
+
export type { AppCursor, AppCursorTable, Circle, CircleMember, CircleMemberTable, CircleTable, CommitJournalEntry, ControlRequest, ControlRequestTable, Group, GroupAnchor, GroupAnchorTable, GroupDelegation, GroupDelegationTable, GroupHub, GroupHubTable, GroupKind, GroupLeaveRequest, GroupLeaveRequestTable, GroupMember, GroupMemberTable, GroupMLSState, GroupMLSStateTable, GroupStatus, GroupTable, Hub, HubTable, InsertCircle, InsertCircleMember, InsertGroup, InsertGroupDelegation, InsertGroupHub, InsertGroupMember, InsertGroupMLSState, InsertHub, InsertSyncCheckpoint, InsertSyncPeer, LocalPeerProfile, LocalPeerProfileTable, OwnAgentsGrantKind, OwnAgentsGrantTable, OwnAgentsJoinCapabilityTable, OwnAgentsRecordTable, P2PStoreTables, PaginatedResult, PaginationParams, PeerAnnouncement, PeerAnnouncementTable, PeerCapabilityRecord, PendingAppFrameRow, PendingAppFrameTable, PendingJoinRequestRow, PendingJoinRequestTable, SyncCheckpoint, SyncCheckpointTable, SyncPeer, SyncPeerTable, UpdateGroupMLSState, } from './tables.js';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export { createP2PStore } from './api.js';
|
|
2
2
|
export { p2pStoreDefinition } from './definition.js';
|
|
3
|
+
export { canonicalHubURL } from './hub-url.js';
|
|
4
|
+
export { AppOutboxDecryptionError, AppOutboxTransactionError, PendingRevocationAdmissionError, UndeliveredRevocationsError } from './ops/app-outbox.js';
|
|
5
|
+
export { MAX_REPAIRS_PER_LISTER } from './ops/catalog-sync.js';
|
|
6
|
+
export { BroadcastLogPruneParamsError } from './ops/sync.js';
|
|
7
|
+
export { classifyDocumentOwner } from './owner-classification.js';
|
|
3
8
|
export const P2P_STORE = 'p2p';
|
|
4
9
|
export function getP2PStore(provider) {
|
|
5
10
|
return provider.getStore(P2P_STORE);
|
|
6
11
|
}
|
|
12
|
+
export { OwnAgentsGroupNotFoundError, OwnAgentsKindDowngradeError, OwnAgentsRecordError, OwnAgentsTransactionError, ownAgentsRecordLockKey } from './ops/own-agents.js';
|
package/lib/migrations.js
CHANGED
|
@@ -23,10 +23,35 @@ import { sql } from 'kysely';
|
|
|
23
23
|
const now = ctx.functions.now;
|
|
24
24
|
const init = {
|
|
25
25
|
async up (db) {
|
|
26
|
+
await db.schema.createTable('kubun_p2p_access_default_revocations').ifNotExists().addColumn('controller_did', t.text, (col)=>col.notNull()).addColumn('device_did', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_access_default_revocations', [
|
|
27
|
+
'controller_did',
|
|
28
|
+
'device_did'
|
|
29
|
+
]).execute();
|
|
30
|
+
// Every authenticated device binding this device has observed. Append-only:
|
|
31
|
+
// ownership is historical, so revocation, removal or MLS state loss never
|
|
32
|
+
// takes a row away.
|
|
33
|
+
await db.schema.createTable('kubun_p2p_binding_evidence').ifNotExists().addColumn('controller_did', t.text, (col)=>col.notNull()).addColumn('device_did', t.text, (col)=>col.notNull()).addColumn('first_seen_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addPrimaryKeyConstraint('pk_p2p_binding_evidence', [
|
|
34
|
+
'controller_did',
|
|
35
|
+
'device_did'
|
|
36
|
+
]).execute();
|
|
37
|
+
await db.schema.createIndex('idx_p2p_binding_evidence_device').ifNotExists().on('kubun_p2p_binding_evidence').column('device_did').execute();
|
|
26
38
|
// Groups
|
|
27
|
-
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())
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
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('kind', t.text, (col)=>col.notNull().defaultTo('standard'))// A completed join or local create starts joined; see `GroupStatus`.
|
|
40
|
+
.addColumn('status', t.text, (col)=>col.notNull().defaultTo('joined'))// Token of the last handle advance whose projections no reconcile has
|
|
41
|
+
// caught up with yet; NULL when they match the current handle.
|
|
42
|
+
.addColumn('projections_dirty', t.text)// Epoch the last successful reconcile caught the projections up to.
|
|
43
|
+
.addColumn('projections_epoch', 'integer').addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addColumn('updated_at', t.timestamp).execute();
|
|
44
|
+
await db.schema.createTable('kubun_p2p_own_agents_records').ifNotExists().addColumn('type', t.text, (col)=>col.notNull()).addColumn('owner', t.text, (col)=>col.notNull()).addColumn('id', t.text, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addColumn('op_jwt', t.text, (col)=>col.notNull()).addColumn('op_hash', t.text, (col)=>col.notNull()).addColumn('payload', t.binary).addColumn('payload_digest', t.text).addColumn('deleted', 'integer', (col)=>col.notNull()).addColumn('meta', t.json, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addPrimaryKeyConstraint('pk_p2p_own_agents_records', [
|
|
45
|
+
'type',
|
|
46
|
+
'owner',
|
|
47
|
+
'id',
|
|
48
|
+
'op_hash'
|
|
49
|
+
]).execute();
|
|
50
|
+
await db.schema.createTable('kubun_p2p_own_agents_grants').ifNotExists().addColumn('controller_did', t.text, (col)=>col.notNull()).addColumn('kind', t.text, (col)=>col.notNull()).addColumn('token', t.text, (col)=>col.notNull()).addColumn('issued_at', t.bigint, (col)=>col.notNull()).addColumn('expires_at', t.bigint, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_own_agents_grants', [
|
|
51
|
+
'controller_did',
|
|
52
|
+
'kind'
|
|
53
|
+
]).execute();
|
|
54
|
+
await db.schema.createTable('kubun_p2p_own_agents_join_capabilities').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('recipient_did', t.text, (col)=>col.notNull()).addColumn('capability_id', t.text, (col)=>col.notNull()).addColumn('token', t.text, (col)=>col.notNull()).addColumn('expires_at', t.bigint, (col)=>col.notNull()).addColumn('signer_did', t.text).addColumn('signer_leaf_key', t.binary).addColumn('signer_capability', t.text).addColumn('signer_prefix', t.json).execute();
|
|
30
55
|
// Group members
|
|
31
56
|
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.
|
|
32
57
|
.addColumn('hlc', t.text, (col)=>col.notNull())// HLC at which this peer learned the member was removed from the MLS
|
|
@@ -38,8 +63,78 @@ import { sql } from 'kysely';
|
|
|
38
63
|
'member_did'
|
|
39
64
|
]).execute();
|
|
40
65
|
await db.schema.createIndex('idx_p2p_group_members_did').ifNotExists().on('kubun_p2p_group_members').column('member_did').execute();
|
|
66
|
+
await db.schema.createTable('kubun_p2p_catalog_offers').ifNotExists().addColumn('owner', t.text, (col)=>col.notNull()).addColumn('exposure_id', t.text, (col)=>col.notNull()).addColumn('catalog_id', t.text, (col)=>col.notNull()).addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('circle_id', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_catalog_offers', [
|
|
67
|
+
'owner',
|
|
68
|
+
'exposure_id'
|
|
69
|
+
]).execute();
|
|
70
|
+
await db.schema.createIndex('idx_p2p_catalog_offers_group_circle').ifNotExists().on('kubun_p2p_catalog_offers').columns([
|
|
71
|
+
'group_id',
|
|
72
|
+
'circle_id'
|
|
73
|
+
]).execute();
|
|
74
|
+
await db.schema.createTable('kubun_p2p_catalog_routes').ifNotExists().addColumn('owner', t.text, (col)=>col.notNull()).addColumn('catalog_id', t.text, (col)=>col.notNull()).addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('pull', 'integer', (col)=>col.notNull().defaultTo(0)).addColumn('push', 'integer', (col)=>col.notNull().defaultTo(0)).addPrimaryKeyConstraint('pk_p2p_catalog_routes', [
|
|
75
|
+
'owner',
|
|
76
|
+
'catalog_id',
|
|
77
|
+
'group_id'
|
|
78
|
+
]).execute();
|
|
79
|
+
// Groups whose document data this device does not serve; absence means served.
|
|
80
|
+
await db.schema.createTable('kubun_p2p_unserved_groups').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull().primaryKey()).execute();
|
|
81
|
+
// Document-sync progress with one peer in one group: the last resolved
|
|
82
|
+
// document key, whether the current cycle's examined ranges were all clear,
|
|
83
|
+
// the hash of the accepted rule set without absolute bounds, and the
|
|
84
|
+
// absolute bounds in use until the cursor wraps.
|
|
85
|
+
await db.schema.createTable('kubun_p2p_catalog_cursors').ifNotExists().addColumn('peer_did', t.text, (col)=>col.notNull()).addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('rules_hash', t.text, (col)=>col.notNull()).addColumn('last_key', t.text)// The responder's sealed position for `last_key`; opaque here.
|
|
86
|
+
.addColumn('last_token', t.text).addColumn('cycle_clear', 'integer', (col)=>col.notNull().defaultTo(1)).addColumn('bounds', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_catalog_cursors', [
|
|
87
|
+
'peer_did',
|
|
88
|
+
'group_id'
|
|
89
|
+
]).execute();
|
|
90
|
+
// A document whose difference with some peer is not resolved.
|
|
91
|
+
// `listed_by` is the first peer whose tree listed it, so it is a repair to
|
|
92
|
+
// retry; null when no peer listed it and the row only remembers answers.
|
|
93
|
+
// Rows expire from `created_at`, so none outlives its window whoever
|
|
94
|
+
// listed it.
|
|
95
|
+
await db.schema.createTable('kubun_p2p_document_repairs').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('listed_by', t.text).addColumn('holdings_digest', t.text, (col)=>col.notNull()).addColumn('created_at', t.bigint, (col)=>col.notNull()).addColumn('attempted_at', t.bigint, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_document_repairs', [
|
|
96
|
+
'group_id',
|
|
97
|
+
'document_id'
|
|
98
|
+
]).execute();
|
|
99
|
+
await db.schema.createIndex('idx_p2p_document_repairs_lister').ifNotExists().on('kubun_p2p_document_repairs').columns([
|
|
100
|
+
'group_id',
|
|
101
|
+
'listed_by',
|
|
102
|
+
'attempted_at'
|
|
103
|
+
]).execute();
|
|
104
|
+
await db.schema.createIndex('idx_p2p_document_repairs_created').ifNotExists().on('kubun_p2p_document_repairs').columns([
|
|
105
|
+
'group_id',
|
|
106
|
+
'created_at'
|
|
107
|
+
]).execute();
|
|
108
|
+
// A peer's last answer, "unavailable" or "refused", for a repair row's
|
|
109
|
+
// current holdings. A change of holdings removes the answers.
|
|
110
|
+
await db.schema.createTable('kubun_p2p_document_repair_answers').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('peer_did', t.text, (col)=>col.notNull()).addColumn('answer', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_document_repair_answers', [
|
|
111
|
+
'group_id',
|
|
112
|
+
'document_id',
|
|
113
|
+
'peer_did'
|
|
114
|
+
]).execute();
|
|
115
|
+
// When this device next asks peers for an observed document's value, for
|
|
116
|
+
// the obligation observed at `fetch_hlc`. The connector owns the
|
|
117
|
+
// obligation; a row leaves only when it resolves.
|
|
118
|
+
await db.schema.createTable('kubun_p2p_observation_repairs').ifNotExists().addColumn('document_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('fetch_hlc', t.text, (col)=>col.notNull()).addColumn('attempts', 'integer', (col)=>col.notNull()).addColumn('next_attempt_at', t.bigint, (col)=>col.notNull()).addColumn('tried_count', 'integer', (col)=>col.notNull()).execute();
|
|
119
|
+
await db.schema.createIndex('idx_p2p_observation_repairs_next').ifNotExists().on('kubun_p2p_observation_repairs').columns([
|
|
120
|
+
'next_attempt_at',
|
|
121
|
+
'document_id'
|
|
122
|
+
]).execute();
|
|
123
|
+
// The peers already asked for an obligation that answered without it.
|
|
124
|
+
await db.schema.createTable('kubun_p2p_observation_repair_peers').ifNotExists().addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('peer_did', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_observation_repair_peers', [
|
|
125
|
+
'document_id',
|
|
126
|
+
'peer_did'
|
|
127
|
+
]).execute();
|
|
128
|
+
// Newest leave-request stamp surfaced per (group, member), so a replayed
|
|
129
|
+
// or duplicate request is not surfaced again. Separate from the membership
|
|
130
|
+
// row because it must outlive a removal: a re-added member's old requests
|
|
131
|
+
// stay spent.
|
|
132
|
+
await db.schema.createTable('kubun_p2p_group_leave_requests').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('member_did', t.text, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_group_leave_requests', [
|
|
133
|
+
'group_id',
|
|
134
|
+
'member_did'
|
|
135
|
+
]).execute();
|
|
41
136
|
// Circles
|
|
42
|
-
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('
|
|
137
|
+
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('hlc', t.text, (col)=>col.notNull())// Projection of the group's current `openCircleIDs`: 1 marks the circle
|
|
43
138
|
// open (every group member has implicit access), 0 closed. Integer 0/1
|
|
44
139
|
// mirrors the catalog `active` flag, keeping the read-back value uniform
|
|
45
140
|
// across SQLite and Postgres. A circle is closed until a settings fold
|
|
@@ -61,9 +156,19 @@ import { sql } from 'kysely';
|
|
|
61
156
|
// one group a delegation `jti` was granted in, so the p2p layer can still
|
|
62
157
|
// resolve a cap's group for revoke-broadcast targeting and listing
|
|
63
158
|
// enrichment. One group per `jti`, so `jti` is the primary key.
|
|
64
|
-
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();
|
|
159
|
+
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()).addColumn('pending_grant_hlc', t.text).addColumn('pending_grant_digest', t.text).addColumn('pending_revocation_hlc', t.text).addColumn('pending_revocation_digest', t.text).execute();
|
|
160
|
+
await db.schema.createIndex('idx_p2p_group_delegation_pending_grant').ifNotExists().on('kubun_p2p_group_delegation').columns([
|
|
161
|
+
'group_id',
|
|
162
|
+
'pending_grant_hlc'
|
|
163
|
+
]).execute();
|
|
164
|
+
await db.schema.createIndex('idx_p2p_group_delegation_pending_revocation').ifNotExists().on('kubun_p2p_group_delegation').columns([
|
|
165
|
+
'group_id',
|
|
166
|
+
'pending_revocation_hlc'
|
|
167
|
+
]).execute();
|
|
65
168
|
// Group MLS state
|
|
66
|
-
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())
|
|
169
|
+
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())// Orders in-place secret-tree mutations within one epoch.
|
|
170
|
+
.addColumn('state_version', 'integer', (col)=>col.notNull().defaultTo(0)).addColumn('is_deleted', 'integer', (col)=>col.notNull().defaultTo(0))// Highest sender generation reserved at this epoch; restore skips past it.
|
|
171
|
+
.addColumn('sender_generation_hwm', 'integer', (col)=>col.notNull().defaultTo(0)).addColumn('ledger_entries', t.text, (col)=>col.notNull().defaultTo('[]')).addColumn('updated_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addPrimaryKeyConstraint('pk_p2p_group_mls_state', [
|
|
67
172
|
'group_id',
|
|
68
173
|
'device_id'
|
|
69
174
|
]).execute();
|
|
@@ -86,6 +191,8 @@ import { sql } from 'kysely';
|
|
|
86
191
|
'mls_group_id'
|
|
87
192
|
]).execute();
|
|
88
193
|
await db.schema.createIndex('idx_p2p_broadcast_log_doc').ifNotExists().on('kubun_p2p_broadcast_log').column('doc_id').execute();
|
|
194
|
+
// The broadcast log is pruned by age.
|
|
195
|
+
await db.schema.createIndex('idx_p2p_broadcast_log_broadcast_at').ifNotExists().on('kubun_p2p_broadcast_log').column('broadcast_at').execute();
|
|
89
196
|
// Hubs (device-local, never synced — no hlc column)
|
|
90
197
|
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();
|
|
91
198
|
// Group hubs (join table, device-local — no hlc column). A group commits
|
|
@@ -113,7 +220,8 @@ import { sql } from 'kysely';
|
|
|
113
220
|
// nullable because most requests produce nothing to hand back; it is
|
|
114
221
|
// written on settle and deleted with the row on ack, so the retention of
|
|
115
222
|
// whatever it holds is bounded by the request's own lifetime.
|
|
116
|
-
await db.schema.createTable('kubun_p2p_control_requests').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('kind', t.text, (col)=>col.notNull()).addColumn('status', t.text, (col)=>col.notNull().defaultTo('pending')).addColumn('error', t.text).addColumn('result', t.text).addColumn('enqueued_at', t.text, (col)=>col.notNull()).addColumn('settled_at', t.text).
|
|
223
|
+
await db.schema.createTable('kubun_p2p_control_requests').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('kind', t.text, (col)=>col.notNull()).addColumn('status', t.text, (col)=>col.notNull().defaultTo('pending')).addColumn('error', t.text).addColumn('result', t.text).addColumn('enqueued_at', t.text, (col)=>col.notNull()).addColumn('settled_at', t.text)// Epoch of the advance that settled the request committed.
|
|
224
|
+
.addColumn('settled_epoch', 'integer').execute();
|
|
117
225
|
await db.schema.createIndex('idx_p2p_control_requests_group').ifNotExists().on('kubun_p2p_control_requests').columns([
|
|
118
226
|
'group_id',
|
|
119
227
|
'enqueued_at'
|
|
@@ -121,17 +229,35 @@ import { sql } from 'kysely';
|
|
|
121
229
|
// CommitJournal store (backs the `@kumiai/rpc` CommitJournal port).
|
|
122
230
|
// `group_id` is the primary key: a single slot per group, so a fresh `put`
|
|
123
231
|
// upserts/replaces the whole row.
|
|
124
|
-
await db.schema.createTable('kubun_p2p_commit_journal').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('publish_id', t.text, (col)=>col.notNull()).addColumn('expected_head', t.text).addColumn('epoch', t.bigint, (col)=>col.notNull()).addColumn('accepted_as', t.text).addColumn('commit', t.binary, (col)=>col.notNull()).addColumn('bodies', t.json, (col)=>col.notNull()).addColumn('kind', t.text, (col)=>col.notNull()).addColumn('journal', t.binary, (col)=>col.notNull()).execute();
|
|
232
|
+
await db.schema.createTable('kubun_p2p_commit_journal').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('publish_id', t.text, (col)=>col.notNull()).addColumn('expected_head', t.text).addColumn('epoch', t.bigint, (col)=>col.notNull()).addColumn('accepted_as', t.text).addColumn('commit', t.binary, (col)=>col.notNull()).addColumn('bodies', t.json, (col)=>col.notNull()).addColumn('kind', t.text, (col)=>col.notNull()).addColumn('journal', t.binary, (col)=>col.notNull()).addColumn('holds_log_sends', 'integer', (col)=>col.notNull().defaultTo(0)).execute();
|
|
125
233
|
// App-lane anchor + per-topic read positions (back the `@kumiai/rpc`
|
|
126
234
|
// `AnchorStore` and `AppCursorStore` ports). The anchor table is a single
|
|
127
235
|
// slot per group, so a rotation upserts and replaces the whole row;
|
|
128
236
|
// cursors are keyed by (group, topic) so a deleted group takes its cursors
|
|
129
237
|
// with it.
|
|
130
|
-
await db.schema.createTable('kubun_p2p_group_anchor').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('secret', t.binary, (col)=>col.notNull()).addColumn('epoch', t.bigint, (col)=>col.notNull()).execute();
|
|
238
|
+
await db.schema.createTable('kubun_p2p_group_anchor').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('secret', t.binary, (col)=>col.notNull()).addColumn('epoch', t.bigint, (col)=>col.notNull()).addColumn('pending', t.json).execute();
|
|
239
|
+
await db.schema.createTable('kubun_p2p_app_outbox_state').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('key', t.binary).addColumn('commit_cursor', t.json).execute();
|
|
240
|
+
await db.schema.createTable('kubun_p2p_app_outbox').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('seq', t.bigint, (col)=>col.notNull()).addColumn('protocol', t.text, (col)=>col.notNull()).addColumn('prc', t.text, (col)=>col.notNull()).addColumn('data', t.binary, (col)=>col.notNull()).addColumn('iv', t.binary, (col)=>col.notNull()).addColumn('last_attempt', t.json).addPrimaryKeyConstraint('pk_p2p_app_outbox', [
|
|
241
|
+
'group_id',
|
|
242
|
+
'seq'
|
|
243
|
+
]).execute();
|
|
131
244
|
await db.schema.createTable('kubun_p2p_app_cursor').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('topic_id', t.text, (col)=>col.notNull()).addColumn('position', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('pk_p2p_app_cursor', [
|
|
132
245
|
'group_id',
|
|
133
246
|
'topic_id'
|
|
134
247
|
]).execute();
|
|
248
|
+
// Decrypted app frames awaiting application. The frame ID is stable across
|
|
249
|
+
// redelivery, so the first staged record wins until it is completed.
|
|
250
|
+
await db.schema.createTable('kubun_p2p_app_pending').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('device_id', t.text, (col)=>col.notNull()).addColumn('frame_id', t.text, (col)=>col.notNull()).addColumn('topic_id', t.text, (col)=>col.notNull()).addColumn('protocol', t.text, (col)=>col.notNull()).addColumn('segment', 'integer', (col)=>col.notNull()).addColumn('position', t.text, (col)=>col.notNull()).addColumn('payload', t.binary, (col)=>col.notNull()).addColumn('sender_did', t.text, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).addPrimaryKeyConstraint('pk_p2p_app_pending', [
|
|
251
|
+
'group_id',
|
|
252
|
+
'device_id',
|
|
253
|
+
'frame_id'
|
|
254
|
+
]).execute();
|
|
255
|
+
await db.schema.createTable('kubun_p2p_log_handler_completion').ifNotExists().addColumn('group_id', t.text, (col)=>col.notNull()).addColumn('procedure', t.text, (col)=>col.notNull()).addColumn('entity', t.text, (col)=>col.notNull()).addColumn('hlc', t.text, (col)=>col.notNull()).addColumn('notify', 'integer', (col)=>col.notNull().defaultTo(0)).addColumn('completed', 'integer', (col)=>col.notNull().defaultTo(0)).addPrimaryKeyConstraint('pk_p2p_log_handler_completion', [
|
|
256
|
+
'group_id',
|
|
257
|
+
'procedure',
|
|
258
|
+
'entity',
|
|
259
|
+
'hlc'
|
|
260
|
+
]).execute();
|
|
135
261
|
// The peer-discovery projection: one row per (group, peer) folded from
|
|
136
262
|
// authenticated `peer/announce` frames. `peer_did` is the MLS-authenticated
|
|
137
263
|
// announcer, which is why the primary key is (group, peer) and there is no
|
|
@@ -158,10 +284,27 @@ import { sql } from 'kysely';
|
|
|
158
284
|
.addColumn('payload', t.text, (col)=>col.notNull()).addColumn('created_at', t.timestamp, (col)=>col.defaultTo(now).notNull()).execute();
|
|
159
285
|
},
|
|
160
286
|
async down (db) {
|
|
287
|
+
await db.schema.dropTable('kubun_p2p_access_default_revocations').ifExists().execute();
|
|
288
|
+
await db.schema.dropTable('kubun_p2p_binding_evidence').ifExists().execute();
|
|
289
|
+
await db.schema.dropTable('kubun_p2p_own_agents_join_capabilities').ifExists().execute();
|
|
290
|
+
await db.schema.dropTable('kubun_p2p_own_agents_grants').ifExists().execute();
|
|
291
|
+
await db.schema.dropTable('kubun_p2p_own_agents_records').ifExists().execute();
|
|
292
|
+
await db.schema.dropTable('kubun_p2p_observation_repair_peers').ifExists().execute();
|
|
293
|
+
await db.schema.dropTable('kubun_p2p_observation_repairs').ifExists().execute();
|
|
294
|
+
await db.schema.dropTable('kubun_p2p_document_repair_answers').ifExists().execute();
|
|
295
|
+
await db.schema.dropTable('kubun_p2p_document_repairs').ifExists().execute();
|
|
296
|
+
await db.schema.dropTable('kubun_p2p_catalog_cursors').ifExists().execute();
|
|
297
|
+
await db.schema.dropTable('kubun_p2p_catalog_routes').ifExists().execute();
|
|
298
|
+
await db.schema.dropTable('kubun_p2p_unserved_groups').ifExists().execute();
|
|
299
|
+
await db.schema.dropTable('kubun_p2p_catalog_offers').ifExists().execute();
|
|
161
300
|
await db.schema.dropTable('kubun_p2p_pending_join_requests').ifExists().execute();
|
|
162
301
|
await db.schema.dropTable('kubun_p2p_local_peer_profile').ifExists().execute();
|
|
163
302
|
await db.schema.dropTable('kubun_p2p_peer_announcements').ifExists().execute();
|
|
303
|
+
await db.schema.dropTable('kubun_p2p_app_pending').ifExists().execute();
|
|
304
|
+
await db.schema.dropTable('kubun_p2p_log_handler_completion').ifExists().execute();
|
|
164
305
|
await db.schema.dropTable('kubun_p2p_app_cursor').ifExists().execute();
|
|
306
|
+
await db.schema.dropTable('kubun_p2p_app_outbox').ifExists().execute();
|
|
307
|
+
await db.schema.dropTable('kubun_p2p_app_outbox_state').ifExists().execute();
|
|
165
308
|
await db.schema.dropTable('kubun_p2p_group_anchor').ifExists().execute();
|
|
166
309
|
await db.schema.dropTable('kubun_p2p_commit_journal').ifExists().execute();
|
|
167
310
|
await db.schema.dropTable('kubun_p2p_control_requests').ifExists().execute();
|
|
@@ -175,6 +318,7 @@ import { sql } from 'kysely';
|
|
|
175
318
|
await db.schema.dropTable('kubun_p2p_group_delegation').ifExists().execute();
|
|
176
319
|
await db.schema.dropTable('kubun_p2p_circle_members').ifExists().execute();
|
|
177
320
|
await db.schema.dropTable('kubun_p2p_circles').ifExists().execute();
|
|
321
|
+
await db.schema.dropTable('kubun_p2p_group_leave_requests').ifExists().execute();
|
|
178
322
|
await db.schema.dropTable('kubun_p2p_group_members').ifExists().execute();
|
|
179
323
|
await db.schema.dropTable('kubun_p2p_groups').ifExists().execute();
|
|
180
324
|
}
|
package/lib/ops/app-lane.d.ts
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import type { Adapter } from '@kubun/db-adapter';
|
|
2
|
+
import { type AnchorSlot } from '@kumiai/rpc';
|
|
2
3
|
import type { Kysely } from 'kysely';
|
|
3
|
-
import type { AppCursor, GroupAnchor, P2PStoreTables } from '../tables.js';
|
|
4
|
-
|
|
4
|
+
import type { AppCursor, GroupAnchor, InsertGroupMLSState, P2PStoreTables, PendingAppFrameRow } from '../tables.js';
|
|
5
|
+
export type OpenedAppFrameExpectedVersion = {
|
|
6
|
+
epoch: number;
|
|
7
|
+
stateVersion: number;
|
|
8
|
+
};
|
|
9
|
+
export type OpenedAppFrameRecord = {
|
|
10
|
+
frameID: string;
|
|
11
|
+
topicID: string;
|
|
12
|
+
protocol: string;
|
|
13
|
+
segment: number;
|
|
14
|
+
position: string;
|
|
15
|
+
payload: Uint8Array;
|
|
16
|
+
senderDID: string;
|
|
17
|
+
};
|
|
18
|
+
export type SaveOpenedAppFrameParams = {
|
|
19
|
+
state: InsertGroupMLSState;
|
|
20
|
+
expected: OpenedAppFrameExpectedVersion;
|
|
21
|
+
record: OpenedAppFrameRecord;
|
|
22
|
+
};
|
|
23
|
+
/** App-lane anchors, read cursors, and pending opened frames. */
|
|
5
24
|
export type AppLaneOps = {
|
|
25
|
+
/** Commit the MLS CAS and pending insert together. */
|
|
26
|
+
saveOpenedAppFrame(params: SaveOpenedAppFrameParams): Promise<boolean>;
|
|
27
|
+
listPendingAppFrames(groupID: string, deviceID: string): Promise<Array<PendingAppFrameRow>>;
|
|
28
|
+
completePendingAppFrame(groupID: string, deviceID: string, frameID: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Erase one membership's app-lane state: this device's pending frames, the
|
|
31
|
+
* group's anchor and its read cursors. A later membership must start from no
|
|
32
|
+
* anchor, which the peer reads as first boot and seeds from its live handle.
|
|
33
|
+
*/
|
|
34
|
+
deleteGroupAppLane(groupID: string, deviceID: string): Promise<void>;
|
|
6
35
|
/**
|
|
7
36
|
* Write the group's anchor slot, REPLACING whatever it held. Single slot per
|
|
8
37
|
* group: the anchor moves as a pair (secret + the epoch it was exported at),
|
|
@@ -12,6 +41,7 @@ export type AppLaneOps = {
|
|
|
12
41
|
groupID: string;
|
|
13
42
|
secret: Uint8Array;
|
|
14
43
|
epoch: number;
|
|
44
|
+
pending?: AnchorSlot['pending'];
|
|
15
45
|
}): Promise<void>;
|
|
16
46
|
/** The group's anchor, or `undefined` if the group has never stored one. */
|
|
17
47
|
getGroupAnchor(groupID: string): Promise<GroupAnchor | undefined>;
|
package/lib/ops/app-lane.js
CHANGED
|
@@ -1,10 +1,62 @@
|
|
|
1
|
+
import { sortPendingAppFrames } from '@kumiai/rpc';
|
|
2
|
+
import { createMLSOps } from './mls.js';
|
|
1
3
|
export function createAppLaneOps(db, adapter) {
|
|
2
4
|
return {
|
|
5
|
+
async saveOpenedAppFrame ({ state, expected, record }) {
|
|
6
|
+
const save = async (tx)=>{
|
|
7
|
+
if (!await createMLSOps(tx, adapter).saveMLSState(state, expected)) return false;
|
|
8
|
+
await tx.insertInto('kubun_p2p_app_pending').values({
|
|
9
|
+
group_id: state.group_id,
|
|
10
|
+
device_id: state.device_id,
|
|
11
|
+
frame_id: record.frameID,
|
|
12
|
+
topic_id: record.topicID,
|
|
13
|
+
protocol: record.protocol,
|
|
14
|
+
segment: record.segment,
|
|
15
|
+
position: record.position,
|
|
16
|
+
payload: adapter.encodeBinary(record.payload),
|
|
17
|
+
sender_did: record.senderDID
|
|
18
|
+
}).onConflict((oc)=>oc.columns([
|
|
19
|
+
'group_id',
|
|
20
|
+
'device_id',
|
|
21
|
+
'frame_id'
|
|
22
|
+
]).doNothing()).execute();
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
return db.isTransaction ? await save(db) : await db.transaction().execute(save);
|
|
26
|
+
},
|
|
27
|
+
async listPendingAppFrames (groupID, deviceID) {
|
|
28
|
+
const rows = await db.selectFrom('kubun_p2p_app_pending').selectAll().where('group_id', '=', groupID).where('device_id', '=', deviceID).execute();
|
|
29
|
+
const sorted = sortPendingAppFrames(rows.map((row)=>({
|
|
30
|
+
frame: {
|
|
31
|
+
id: row.frame_id,
|
|
32
|
+
topicID: row.topic_id,
|
|
33
|
+
protocol: row.protocol,
|
|
34
|
+
segment: row.segment,
|
|
35
|
+
position: row.position
|
|
36
|
+
},
|
|
37
|
+
payload: row.payload,
|
|
38
|
+
senderDID: row.sender_did,
|
|
39
|
+
row
|
|
40
|
+
})));
|
|
41
|
+
return sorted.map(({ row })=>row);
|
|
42
|
+
},
|
|
43
|
+
async completePendingAppFrame (groupID, deviceID, frameID) {
|
|
44
|
+
await db.deleteFrom('kubun_p2p_app_pending').where('group_id', '=', groupID).where('device_id', '=', deviceID).where('frame_id', '=', frameID).execute();
|
|
45
|
+
},
|
|
46
|
+
async deleteGroupAppLane (groupID, deviceID) {
|
|
47
|
+
const remove = async (tx)=>{
|
|
48
|
+
await tx.deleteFrom('kubun_p2p_app_pending').where('group_id', '=', groupID).where('device_id', '=', deviceID).execute();
|
|
49
|
+
await tx.deleteFrom('kubun_p2p_group_anchor').where('group_id', '=', groupID).execute();
|
|
50
|
+
await tx.deleteFrom('kubun_p2p_app_cursor').where('group_id', '=', groupID).execute();
|
|
51
|
+
};
|
|
52
|
+
return db.isTransaction ? await remove(db) : await db.transaction().execute(remove);
|
|
53
|
+
},
|
|
3
54
|
async putGroupAnchor (params) {
|
|
4
55
|
const values = {
|
|
5
56
|
group_id: params.groupID,
|
|
6
57
|
secret: adapter.encodeBinary(params.secret),
|
|
7
|
-
epoch: params.epoch
|
|
58
|
+
epoch: params.epoch,
|
|
59
|
+
pending: params.pending == null ? null : adapter.encodeJSON(params.pending)
|
|
8
60
|
};
|
|
9
61
|
await db.insertInto('kubun_p2p_group_anchor')// Single slot per group: a rotation REPLACES both halves together. The
|
|
10
62
|
// secret and the epoch it was exported at both feed topic derivation,
|
|
@@ -12,7 +64,8 @@ export function createAppLaneOps(db, adapter) {
|
|
|
12
64
|
// a topic no member is on.
|
|
13
65
|
.values(values).onConflict((oc)=>oc.column('group_id').doUpdateSet((eb)=>({
|
|
14
66
|
secret: eb.ref('excluded.secret'),
|
|
15
|
-
epoch: eb.ref('excluded.epoch')
|
|
67
|
+
epoch: eb.ref('excluded.epoch'),
|
|
68
|
+
pending: eb.ref('excluded.pending')
|
|
16
69
|
}))).execute();
|
|
17
70
|
},
|
|
18
71
|
async getGroupAnchor (groupID) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Adapter } from '@kubun/db-adapter';
|
|
2
|
+
import { type AppOutbox } from '@kumiai/rpc';
|
|
3
|
+
import type { Runtime } from '@sozai/runtime';
|
|
4
|
+
import type { Kysely } from 'kysely';
|
|
5
|
+
import type { P2PStoreTables } from '../tables.js';
|
|
6
|
+
export declare class AppOutboxDecryptionError extends Error {
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class AppOutboxTransactionError extends Error {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class PendingRevocationAdmissionError extends Error {
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
/** A voluntary leave would erase revocations the hub has not accepted yet. */
|
|
16
|
+
export declare class UndeliveredRevocationsError extends Error {
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
export type ClearAppOutboxRowsOptions = {
|
|
20
|
+
/** Refuse, before writing anything, while revocations remain undelivered. */
|
|
21
|
+
requireRevocationsDelivered?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** Port options matching the peer that owns the outbox. */
|
|
24
|
+
export type AppOutboxPortOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* The peer's entry limit. While a revocation is pending, entries other than
|
|
27
|
+
* revocations stop one short of it, keeping the last slot for revocations.
|
|
28
|
+
*/
|
|
29
|
+
limit?: number;
|
|
30
|
+
};
|
|
31
|
+
export type AppOutboxOps = {
|
|
32
|
+
appOutboxFor(groupID: string, options?: AppOutboxPortOptions): AppOutbox;
|
|
33
|
+
clearAppOutbox(groupID: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the group still holds a revocation marker or a queued revocation entry
|
|
36
|
+
* the hub has not yet accepted.
|
|
37
|
+
*/
|
|
38
|
+
hasUndeliveredRevocations(groupID: string): Promise<boolean>;
|
|
39
|
+
};
|
|
40
|
+
/** Coordinate status transitions with revocation admission and outbox erasure. */
|
|
41
|
+
export declare function lockAppOutboxForErasure(db: Kysely<P2PStoreTables>, groupID: string, options?: ClearAppOutboxRowsOptions): Promise<void>;
|
|
42
|
+
/** The caller's transaction also covers group leave or deletion. */
|
|
43
|
+
export declare function clearAppOutboxRows(db: Kysely<P2PStoreTables>, groupID: string, options?: ClearAppOutboxRowsOptions): Promise<Array<number>>;
|
|
44
|
+
export declare function createAppOutboxOps(db: Kysely<P2PStoreTables>, adapter: Adapter, getRandomValues: Runtime['getRandomValues']): AppOutboxOps;
|