@kubun/plugin-p2p 0.15.1 → 0.16.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/index.js CHANGED
@@ -107,11 +107,10 @@ export function createP2PPlugin(options) {
107
107
  }
108
108
  const identity = params.identity;
109
109
  // The engine's single device-wide clock. Sharing it across the p2p group/
110
- // circle/member metadata paths (context, GroupManager, BroadcastService)
111
- // keeps every write from this device on one monotonic counter, so two
112
- // writes in the same millisecond mint strictly-increasing timestamps rather
113
- // than identical ones (the second of which would silently lose under
114
- // last-writer-wins).
110
+ // circle/member metadata paths keeps every write from this device on one
111
+ // monotonic counter, so two writes in the same millisecond mint
112
+ // strictly-increasing timestamps rather than identical ones (the second of
113
+ // which would silently lose under last-writer-wins).
115
114
  const hlc = params.hlc;
116
115
  // Computed up front so SyncManager + sync handlers + the hub-relay
117
116
  // BroadcastService all see the same receive policy. Hot-path invariant:
@@ -298,18 +297,33 @@ export function createP2PPlugin(options) {
298
297
  runtime: params.runtime,
299
298
  db: params.db,
300
299
  hlc,
300
+ // The engine's identity gate, forwarded to the presence coordinator so a
301
+ // self-started announce waits for verification before it mints.
302
+ ready: ()=>params.engine.ready(),
303
+ // A thunk, not the resolved value: `getWorkflowAPI` is defined further down
304
+ // this factory body, so it is only in the temporal dead zone until the body
305
+ // finishes — and the manager invokes this on peer creation, long after.
306
+ getWorkflowAPI: ()=>getWorkflowAPI(),
301
307
  maxDriftMS: params.maxDriftMS,
302
308
  graph: params.graph,
303
309
  emitter,
304
310
  createHubClient,
305
311
  registry,
306
312
  // Re-drive a lost `ledger` commit's surviving tokens through the group
307
- // manager's own ledger write path, so the seam stays a consumer of it.
308
- buildLedgerRedrive: (groupID, tokens, requestID)=>groupManager.buildEnactLedgerCommit({
313
+ // manager's own ledger write path, so the seam stays a consumer of it. The
314
+ // redrive is a self-start (the hub relay, not a request), so its commit
315
+ // build waits behind `ready()` before it mints under the engine's clock.
316
+ buildLedgerRedrive: (groupID, tokens, requestID)=>{
317
+ const build = groupManager.buildEnactLedgerCommit({
309
318
  groupID,
310
319
  tokens,
311
320
  requestID
312
- }),
321
+ });
322
+ return async ()=>{
323
+ await params.engine.ready();
324
+ return build();
325
+ };
326
+ },
313
327
  logger: hubRelayLogger,
314
328
  storeUnreadable: receiveConfig.storeUnreadable,
315
329
  defaultAccessLevel,
@@ -434,30 +448,47 @@ export function createP2PPlugin(options) {
434
448
  // use, executed on behalf of the verified caller against the plugin's own
435
449
  // DB (peer procedures run outside an engine transaction; `joinGroup` manages
436
450
  // its own inner transaction). No MLS logic is reimplemented here.
451
+ // The inbound peer dance stamps group/roster/ledger metadata under the
452
+ // engine's clock outside any GraphQL write entry, so each runner is a
453
+ // self-start driver: it awaits `ready()` before touching the deps that mint.
454
+ // `ready()` is resolved for a booted engine, so this only delays a dance that
455
+ // arrives in the window before identity verification settles.
437
456
  const peerHandlers = createPeerHandlers({
438
457
  db: params.db,
439
458
  identity,
440
459
  logger: params.getLogger('peer-handlers'),
441
460
  autoAcceptPeers: options?.autoAcceptPeers,
442
- runPrepareJoin: (callerDID)=>createJoinContext(// Out-of-band peer-handler context for the verified caller; the join ops
461
+ runPrepareJoin: async (callerDID)=>{
462
+ await params.engine.ready();
463
+ return createJoinContext(// Out-of-band peer-handler context for the verified caller; the join ops
443
464
  // never consult the authority, so a self-scoped device authority fits.
444
465
  {
445
466
  viewerDID: callerDID,
446
467
  credentialAuthority: createDeviceAuthority(callerDID)
447
- }, buildContextDeps(params.db)).prepareRequest(),
448
- runCompleteJoin: (callerDID, invitePayload)=>createJoinContext({
468
+ }, buildContextDeps(params.db)).prepareRequest();
469
+ },
470
+ runCompleteJoin: async (callerDID, invitePayload)=>{
471
+ await params.engine.ready();
472
+ return createJoinContext({
449
473
  viewerDID: callerDID,
450
474
  credentialAuthority: createDeviceAuthority(callerDID)
451
- }, buildContextDeps(params.db)).complete(invitePayload),
452
- runInvite: (callerDID, groupID, joinRequest)=>serveGroupInvite(buildContextDeps(params.db), {
475
+ }, buildContextDeps(params.db)).complete(invitePayload);
476
+ },
477
+ runInvite: async (callerDID, groupID, joinRequest)=>{
478
+ await params.engine.ready();
479
+ return serveGroupInvite(buildContextDeps(params.db), {
453
480
  callerDID,
454
481
  groupID,
455
482
  joinRequest
456
- }),
457
- runPushControl: (callerDID, param)=>servePushControl(buildContextDeps(params.db), {
483
+ });
484
+ },
485
+ runPushControl: async (callerDID, param)=>{
486
+ await params.engine.ready();
487
+ return servePushControl(buildContextDeps(params.db), {
458
488
  callerDID,
459
489
  ...param
460
- })
490
+ });
491
+ }
461
492
  });
462
493
  // Blob transfer control-lane handlers, mounted on the same peer server.
463
494
  const blobHandlers = createBlobHandlers({
@@ -567,6 +598,7 @@ export function createP2PPlugin(options) {
567
598
  // runners do: the scopes an automatic catch-up asks for cannot drift from
568
599
  // the ones an explicit one asks for if there is only one implementation.
569
600
  catchUpWithBestPeer: (groupID)=>createSyncContext(null, buildContextDeps(params.db)).catchUpWithBestPeer(groupID),
601
+ reauthorize: (groupID)=>hub.reauthorize(groupID),
570
602
  listPeerDevices: (groupID)=>createSyncContext(null, buildContextDeps(params.db)).listPeerDevices(groupID),
571
603
  onSyncEvent: (callback)=>syncManager.onSyncEvent(callback),
572
604
  onHubServerDIDChanged: (listener)=>emitter.on('hubServerDIDChanged', listener),
@@ -577,6 +609,37 @@ export function createP2PPlugin(options) {
577
609
  await (await hub.presence()).setProfile(profile);
578
610
  },
579
611
  getLocalPeerProfile: async ()=>await (await hub.presence()).getProfile(),
612
+ // The typed workflow control plane: `groupID` selects the peers, the hub
613
+ // wiring handles the multi-hub fan-out (discover) and directed `.to()`
614
+ // request (the rest). The wire results are shaped to the contract types
615
+ // here; a business refusal on a command travels as data, and a caller
616
+ // helper (plugin-workflow) reconstitutes it into a throw.
617
+ discoverWorkflows: (groupID, options)=>hub.discoverWorkflows(groupID, options),
618
+ listWorkflows: async (groupID, targetDID)=>// `workflow/list` takes no fields but its param schema is a (closed) object,
619
+ // so an omitted param serializes to an invalid request frame — pass `{}`.
620
+ await hub.requestWorkflow(groupID, targetDID, 'workflow/list', {}),
621
+ workflowStatus: async (groupID, targetDID, instanceID)=>await hub.requestWorkflow(groupID, targetDID, 'workflow/status', {
622
+ instanceID
623
+ }),
624
+ enqueueWorkflow: async (groupID, targetDID, name, params, opts)=>await hub.requestWorkflow(groupID, targetDID, 'workflow/enqueue', {
625
+ name,
626
+ params,
627
+ // A stable idempotency key makes the cross-hub retry safe: a directed
628
+ // request can throw on the RESPONSE leg (the target already enqueued,
629
+ // but the reply was lost), and the retry re-sends to the same mailbox.
630
+ // Under one key the target returns the existing instance instead of
631
+ // creating a second. A caller-supplied key wins; else one per call.
632
+ opts: {
633
+ ...opts,
634
+ idempotencyKey: opts?.idempotencyKey ?? crypto.randomUUID()
635
+ }
636
+ }),
637
+ cancelWorkflow: async (groupID, targetDID, instanceID)=>await hub.requestWorkflow(groupID, targetDID, 'workflow/cancel', {
638
+ instanceID
639
+ }),
640
+ retryWorkflow: async (groupID, targetDID, instanceID)=>await hub.requestWorkflow(groupID, targetDID, 'workflow/retry', {
641
+ instanceID
642
+ }),
580
643
  gatherPeers: async (groupID, options)=>await (await hub.presence()).gather(groupID, options),
581
644
  refreshPeerPresence: async (groupID, options)=>await (await hub.presence()).refresh(groupID, options),
582
645
  retryHubConnection: async ()=>await hub.retryHubs(),
package/lib/schema.js CHANGED
@@ -726,7 +726,7 @@ extend type Mutation {
726
726
  createGroup(input: CreateGroupInput!): CreateGroupPayload!
727
727
  requestCreateCircle(input: RequestCreateCircleInput!): RequestCreateCirclePayload!
728
728
  requestAddCircleMember(input: RequestAddCircleMemberInput!): RequestAddCircleMemberPayload!
729
- createCatalog(name: String!, description: String, filterCriteria: String!): PeerCatalog!
729
+ createCatalog(catalogID: ID, name: String!, description: String, filterCriteria: String!): PeerCatalog!
730
730
  requestInviteToGroup(input: RequestInviteToGroupInput!): RequestInviteToGroupPayload!
731
731
  joinGroup(invitePayload: String!, joinRequestPayload: String!): JoinGroupPayload!
732
732
  prepareJoinRequest: PrepareJoinRequestPayload!
@@ -1101,6 +1101,7 @@ export function createP2PSchemaExtension(emitter, syncManager, logger = getKubun
1101
1101
  },
1102
1102
  createCatalog: async (_source, args, context)=>{
1103
1103
  return await requireP2P(context).group.createCatalog({
1104
+ catalogID: args.catalogID,
1104
1105
  name: args.name,
1105
1106
  description: args.description,
1106
1107
  filterCriteria: args.filterCriteria
@@ -1,4 +1,4 @@
1
- import { type Identity, type SigningIdentity } from '@kokuin/token';
1
+ import { type Identity } from '@kokuin/token';
2
2
  import type { StoreProvider } from '@kubun/db';
3
3
  import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
4
4
  import type { Logger } from '@kubun/logger';
@@ -76,10 +76,6 @@ export declare class SyncManager {
76
76
  #private;
77
77
  constructor(params: SyncManagerParams);
78
78
  get peerRegistry(): PeerRegistry;
79
- /**
80
- * Configure the signing identity used for sync operations.
81
- */
82
- setIdentity(identity: SigningIdentity): void;
83
79
  /**
84
80
  * Configure the resolver for the transports built locally rather than dialled:
85
81
  * `direct://` (in-process) and `tunnel://` (relayed by the group's hub, whose
@@ -36,11 +36,6 @@ export class SyncManager {
36
36
  return this.#peerRegistry;
37
37
  }
38
38
  /**
39
- * Configure the signing identity used for sync operations.
40
- */ setIdentity(identity) {
41
- this.#identity = identity;
42
- }
43
- /**
44
39
  * Configure the resolver for the transports built locally rather than dialled:
45
40
  * `direct://` (in-process) and `tunnel://` (relayed by the group's hub, whose
46
41
  * group id arrives as the resolver's `route`).
package/lib/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ClientTransportOf } from '@enkaku/protocol';
2
+ import type { WorkflowControlPlaneAPI } from '@kubun/plugin-p2p-api';
2
3
  import type { ServiceProtocol } from '@kubun/plugin-service-api';
3
4
  import type { AdaptivePolicy } from '@kubun/plugin-workflow-api';
4
5
  import type { AccessLevel } from '@kubun/store-graph';
@@ -1082,7 +1083,7 @@ export type PeerGatherOptions = {
1082
1083
  * of the logs, and nothing branches on it.
1083
1084
  */
1084
1085
  export type PresenceReason = 'hub-connected' | 'epoch-changed' | 'app-window-pruned' | 'requested';
1085
- export type SyncPluginAPI = {
1086
+ export type SyncPluginAPI = WorkflowControlPlaneAPI & {
1086
1087
  /** Resolves when the HTTP sync transport is registered (only present when http option is enabled). */
1087
1088
  syncReady?: Promise<void>;
1088
1089
  /**
@@ -1109,6 +1110,19 @@ export type SyncPluginAPI = {
1109
1110
  * decides who, and an explicit `syncPeer` ignores the ranking entirely.
1110
1111
  */
1111
1112
  catchUpWithBestPeer(groupID: string): Promise<PeerCatchUpData>;
1113
+ /**
1114
+ * Re-drive any group subscription this device's hub-peers latched as refused.
1115
+ *
1116
+ * A device subscribes its group topics the instant it joins, before the
1117
+ * APPLICATION-level authorization that gates them at the hub has landed — so
1118
+ * the hub answers `AuthorizationDeniedError` and the peer's mux latches it
1119
+ * permanent (a busy retry against an answer would be worse). This layer cannot
1120
+ * see the app's authorization state, so it cannot know on its own when to ask
1121
+ * again; the app calls this once it knows the device is authorized (e.g. its
1122
+ * membership row has replicated after pairing completes). Idempotent and a
1123
+ * no-op when nothing is refused — safe to call on every authorization change.
1124
+ */
1125
+ reauthorize(groupID: string): Promise<void>;
1112
1126
  /**
1113
1127
  * Every device this one has heard announce in the group — the projection, and
1114
1128
  * never a liveness answer. {@link SyncPluginAPI.gatherPeers} is what says who
package/lib/types.js CHANGED
@@ -1 +1,5 @@
1
+ // The six caller-facing workflow control-plane methods (discover/list/status/
2
+ // enqueue/cancel/retry, each keyed by `groupID`) are contributed by
3
+ // WorkflowControlPlaneAPI so this plugin's public surface and the type-only
4
+ // contract plugin-workflow imports cannot drift.
1
5
  export { };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/plugin-p2p",
3
- "version": "0.15.1",
3
+ "version": "0.16.1",
4
4
  "license": "see LICENSE.md",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -23,35 +23,36 @@
23
23
  "@kokuin/capability": "^0.3.0",
24
24
  "@kokuin/controller": "^0.1.0",
25
25
  "@kokuin/token": "^0.5.0",
26
- "@kubun/credential": "^0.15.0",
27
- "@kubun/db": "^0.15.0",
28
- "@kubun/db-adapter": "^0.15.0",
29
- "@kubun/engine": "^0.15.0",
30
- "@kubun/graphql": "^0.15.0",
31
- "@kubun/hlc": "^0.15.0",
32
- "@kubun/http-util": "^0.15.0",
33
- "@kubun/id": "^0.15.0",
34
- "@kubun/logger": "^0.15.0",
35
- "@kubun/mutation": "^0.15.0",
36
- "@kubun/plugin-blob-api": "^0.15.0",
37
- "@kubun/plugin-http": "^0.15.0",
38
- "@kubun/plugin-http-api": "^0.15.0",
39
- "@kubun/plugin-service-api": "^0.15.1",
40
- "@kubun/plugin-workflow-api": "^0.15.0",
41
- "@kubun/protocol": "^0.15.0",
42
- "@kubun/store-blob": "^0.15.0",
43
- "@kubun/store-controller": "^0.15.0",
44
- "@kubun/store-credential": "^0.15.0",
45
- "@kubun/store-delegation": "^0.15.0",
46
- "@kubun/store-graph": "^0.15.0",
47
- "@kubun/store-p2p": "^0.15.0",
48
- "@kumiai/broadcast": "^0.8.0",
49
- "@kumiai/hub-protocol": "^0.8.0",
50
- "@kumiai/hub-server": "^0.8.0",
51
- "@kumiai/hub-tunnel": "^0.8.0",
52
- "@kumiai/mls": "^0.8.0",
53
- "@kumiai/mls-rpc": "^0.8.0",
54
- "@kumiai/rpc": "^0.8.0",
26
+ "@kubun/credential": "^0.16.0",
27
+ "@kubun/db": "^0.16.0",
28
+ "@kubun/db-adapter": "^0.16.0",
29
+ "@kubun/engine": "^0.16.0",
30
+ "@kubun/graphql": "^0.16.0",
31
+ "@kubun/hlc": "^0.16.0",
32
+ "@kubun/http-util": "^0.16.0",
33
+ "@kubun/id": "^0.16.0",
34
+ "@kubun/logger": "^0.16.0",
35
+ "@kubun/mutation": "^0.16.0",
36
+ "@kubun/plugin-blob-api": "^0.16.0",
37
+ "@kubun/plugin-http": "^0.16.0",
38
+ "@kubun/plugin-http-api": "^0.16.0",
39
+ "@kubun/plugin-p2p-api": "^0.16.0",
40
+ "@kubun/plugin-service-api": "^0.16.0",
41
+ "@kubun/plugin-workflow-api": "^0.16.0",
42
+ "@kubun/protocol": "^0.16.0",
43
+ "@kubun/store-blob": "^0.16.0",
44
+ "@kubun/store-controller": "^0.16.0",
45
+ "@kubun/store-credential": "^0.16.0",
46
+ "@kubun/store-delegation": "^0.16.0",
47
+ "@kubun/store-graph": "^0.16.0",
48
+ "@kubun/store-p2p": "^0.16.0",
49
+ "@kumiai/broadcast": "^0.9.0",
50
+ "@kumiai/hub-protocol": "^0.9.0",
51
+ "@kumiai/hub-server": "^0.9.0",
52
+ "@kumiai/hub-tunnel": "^0.9.0",
53
+ "@kumiai/mls": "^0.9.0",
54
+ "@kumiai/mls-rpc": "^0.9.0",
55
+ "@kumiai/rpc": "^0.9.0",
55
56
  "@noble/ciphers": "^2.4.0",
56
57
  "@noble/hashes": "^2.4.0",
57
58
  "@sozai/async": "^0.2.1",
@@ -66,21 +67,21 @@
66
67
  "ts-mls": "2.0.0-rc.13"
67
68
  },
68
69
  "devDependencies": {
69
- "@kubun/blob-backend": "^0.15.0",
70
- "@kubun/client": "^0.15.0",
71
- "@kubun/db-better-sqlite": "^0.15.0",
72
- "@kubun/db-node-sqlite": "^0.15.0",
73
- "@kubun/db-postgres": "^0.15.0",
74
- "@kubun/hub": "^0.15.0",
75
- "@kubun/plugin-blob": "^0.15.0",
76
- "@kubun/plugin-connector": "^0.15.0",
77
- "@kubun/plugin-service-server": "^0.15.1",
78
- "@kubun/plugin-workflow": "^0.15.0",
79
- "@kubun/service-graph-api": "^0.15.0",
80
- "@kubun/store-workflow": "^0.15.0",
81
- "@kubun/test-utils": "^0.13.0",
82
- "@kumiai/hub-conformance": "^0.8.0",
83
- "@kumiai/rpc-conformance": "^0.8.0",
70
+ "@kubun/blob-backend": "^0.16.0",
71
+ "@kubun/client": "^0.16.0",
72
+ "@kubun/db-better-sqlite": "^0.16.0",
73
+ "@kubun/db-node-sqlite": "^0.16.0",
74
+ "@kubun/db-postgres": "^0.16.0",
75
+ "@kubun/hub": "^0.16.0",
76
+ "@kubun/plugin-blob": "^0.16.0",
77
+ "@kubun/plugin-connector": "^0.16.0",
78
+ "@kubun/plugin-service-server": "^0.16.0",
79
+ "@kubun/plugin-workflow": "^0.16.0",
80
+ "@kubun/service-graph-api": "^0.16.0",
81
+ "@kubun/store-workflow": "^0.16.0",
82
+ "@kubun/test-utils": "^0.0.0",
83
+ "@kumiai/hub-conformance": "^0.9.0",
84
+ "@kumiai/rpc-conformance": "^0.9.0",
84
85
  "@testcontainers/postgresql": "^12.1.0",
85
86
  "get-port": "^7.2.0"
86
87
  },
@@ -90,8 +91,8 @@
90
91
  "scripts": {
91
92
  "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
92
93
  "build:clean": "del lib",
93
- "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
94
- "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
94
+ "build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
95
+ "build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
95
96
  "test": "pnpm run test:types && pnpm run test:unit",
96
97
  "test:types": "tsc --noEmit -p tsconfig.test.json",
97
98
  "test:unit": "vitest run"