@kubun/plugin-p2p 0.14.0 → 0.15.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/context/peer.js +250 -3
- package/lib/context/sync.js +134 -25
- package/lib/context/types.d.ts +7 -0
- package/lib/groups/broadcast-message.d.ts +29 -0
- package/lib/groups/broadcast.d.ts +11 -1
- package/lib/groups/broadcast.js +44 -2
- package/lib/groups/credential-apply.d.ts +64 -2
- package/lib/groups/credential-apply.js +215 -30
- package/lib/groups/credential-grant.d.ts +22 -0
- package/lib/groups/credential-grant.js +76 -2
- package/lib/groups/credential-manifest-token.d.ts +31 -0
- package/lib/groups/credential-manifest-token.js +49 -0
- package/lib/groups/credential-readiness.d.ts +69 -0
- package/lib/groups/credential-readiness.js +172 -0
- package/lib/groups/credential-wrapping-deps.d.ts +23 -0
- package/lib/groups/credential-wrapping-deps.js +25 -0
- package/lib/groups/did-cache-seed.d.ts +26 -0
- package/lib/groups/did-cache-seed.js +38 -0
- package/lib/groups/grantor-authority.d.ts +65 -0
- package/lib/groups/grantor-authority.js +107 -0
- package/lib/groups/group-handlers.js +7 -0
- package/lib/groups/group-peer-manager.d.ts +32 -1
- package/lib/groups/group-peer-manager.js +77 -0
- package/lib/groups/group-protocols.d.ts +47 -0
- package/lib/groups/group-protocols.js +28 -0
- package/lib/hub/http-client.js +6 -1
- package/lib/hub/hub-like.js +5 -2
- package/lib/hub/peer-scoped-hub-view.d.ts +6 -0
- package/lib/hub/peer-scoped-hub-view.js +11 -1
- package/lib/hub/wiring.d.ts +31 -1
- package/lib/hub/wiring.js +20 -1
- package/lib/index.d.ts +14 -0
- package/lib/index.js +72 -4
- package/lib/protocol.d.ts +30 -0
- package/lib/protocol.js +36 -0
- package/lib/schema.js +78 -1
- package/lib/sync/handlers.js +21 -2
- package/lib/sync/held-delegations.d.ts +14 -0
- package/lib/sync/held-delegations.js +34 -0
- package/lib/sync/hub-tunnel-service-listener.d.ts +84 -0
- package/lib/sync/hub-tunnel-service-listener.js +294 -0
- package/lib/sync/hub-tunnel-service-provider.d.ts +46 -0
- package/lib/sync/hub-tunnel-service-provider.js +100 -0
- package/lib/sync/hub-tunnel-sync-listener.d.ts +8 -1
- package/lib/sync/hub-tunnel-sync-listener.js +6 -1
- package/lib/sync/service-tunnel-listeners.d.ts +41 -0
- package/lib/sync/service-tunnel-listeners.js +183 -0
- package/lib/sync/sync-manager.d.ts +7 -0
- package/lib/sync/sync-manager.js +4 -1
- package/lib/sync/tunnel-listeners.d.ts +7 -1
- package/lib/sync/tunnel-listeners.js +18 -0
- package/lib/sync/tunnel-topics.d.ts +19 -1
- package/lib/sync/tunnel-topics.js +7 -3
- package/lib/types.d.ts +172 -0
- package/package.json +49 -48
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ServerTransport } from '@enkaku/http-serve';
|
|
2
2
|
import { Server } from '@enkaku/server';
|
|
3
3
|
import { DirectTransports } from '@enkaku/transport';
|
|
4
|
-
import { isFullIdentity } from '@kokuin/token';
|
|
4
|
+
import { createInMemoryDIDCache, isFullIdentity } from '@kokuin/token';
|
|
5
5
|
import { createDeviceAuthority } from '@kubun/credential';
|
|
6
6
|
import { resolveAllowedOrigin } from '@kubun/http-util';
|
|
7
7
|
import { getGraphStore } from '@kubun/store-graph';
|
|
@@ -30,6 +30,7 @@ import { createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG } from './sync/bro
|
|
|
30
30
|
import { DEFAULT_PUSH_SYNC_CONFIG, wireBroadcastSender } from './sync/broadcast-sender.js';
|
|
31
31
|
import { createGroupSyncWorkflow, GROUP_SYNC_CONCURRENCY, GROUP_SYNC_WORKFLOW, wireGroupPeriodicSyncArming } from './sync/group-sync-workflow.js';
|
|
32
32
|
import { createSyncHandlers } from './sync/handlers.js';
|
|
33
|
+
import { ServiceServeNotReadyError, ServiceServeUnavailableError } from './sync/hub-tunnel-service-listener.js';
|
|
33
34
|
import { SyncManager } from './sync/sync-manager.js';
|
|
34
35
|
export { GROUP_CONTROL_DENIED, LAST_GROUP_ADMIN, NOT_GROUP_ADMIN, requireGroupAdmin } from './context/require-admin.js';
|
|
35
36
|
export { ADMIN_ROLE_ENTRY_TYPE, foldAdminRoster } from './groups/admin-roster.js';
|
|
@@ -198,6 +199,13 @@ export function createP2PPlugin(options) {
|
|
|
198
199
|
const peerConnections = new PeerConnectionRegistry({
|
|
199
200
|
logger: params.getLogger('peer-connections')
|
|
200
201
|
});
|
|
202
|
+
// One plugin-lifetime DID cache, shared by every peer-lane Server. The
|
|
203
|
+
// tunnel listeners seed it from each group's MLS roster (a co-member's
|
|
204
|
+
// authenticated leaf long form), so a freshly-spawned per-session Server
|
|
205
|
+
// resolves a co-member's short-form `did:peer:4` issuer instead of defaulting
|
|
206
|
+
// its own empty cache. Content-addressed and monotonic — a stale entry is
|
|
207
|
+
// harmless, and it survives no process restart by design (re-seeded on bind).
|
|
208
|
+
const didCache = createInMemoryDIDCache();
|
|
201
209
|
// The p2p plugin manages its own Enkaku server for sync handlers,
|
|
202
210
|
// separate from the graph service's server.
|
|
203
211
|
const syncServers = [];
|
|
@@ -221,6 +229,7 @@ export function createP2PPlugin(options) {
|
|
|
221
229
|
transports: [
|
|
222
230
|
directTransports.server
|
|
223
231
|
],
|
|
232
|
+
cache: didCache,
|
|
224
233
|
signal
|
|
225
234
|
});
|
|
226
235
|
syncServers.push(server);
|
|
@@ -243,6 +252,47 @@ export function createP2PPlugin(options) {
|
|
|
243
252
|
fetch: params.runtime.fetch
|
|
244
253
|
});
|
|
245
254
|
const hubReconnectBackoff = typeof options?.hub === 'object' ? options.hub.reconnectBackoff : undefined;
|
|
255
|
+
// Which services (if any) this device answers on an inbound service-lane
|
|
256
|
+
// tunnel session; `undefined` means the service lane is not configured.
|
|
257
|
+
const serviceConfig = options?.serviceTunnel == null || options.serviceTunnel === false ? undefined : options.serviceTunnel === true ? {
|
|
258
|
+
'controller-log': true
|
|
259
|
+
} : options.serviceTunnel.services ?? {
|
|
260
|
+
'controller-log': true
|
|
261
|
+
};
|
|
262
|
+
// `getAPI` only resolves once every plugin factory has returned (see
|
|
263
|
+
// `Registry.closeGate` in `@kubun/engine`), so it can't be awaited here.
|
|
264
|
+
// `resolvedServe` fills in later, before any service session actually
|
|
265
|
+
// spawns (spawning waits on `setupHubRelay`'s internal `ready`, which
|
|
266
|
+
// resolves after every factory returns) — `serviceServe` below is thus a
|
|
267
|
+
// synchronous `ServeService` even though its target resolves lazily.
|
|
268
|
+
let resolvedServe;
|
|
269
|
+
// A permanent resolution failure (service-server not installed) must not
|
|
270
|
+
// read as transient "not ready" — that would retry a plugin that will
|
|
271
|
+
// never arrive. Track it so the wrapper throws the terminal error instead.
|
|
272
|
+
let serveResolutionFailed = false;
|
|
273
|
+
if (serviceConfig != null) {
|
|
274
|
+
void params.engine.getAPI('service-server').then((api)=>{
|
|
275
|
+
resolvedServe = api.serve;
|
|
276
|
+
}).catch((error)=>{
|
|
277
|
+
serveResolutionFailed = true;
|
|
278
|
+
hubRelayLogger.error('serviceTunnel is enabled but the "service-server" plugin API could not be resolved; the service lane will not serve', {
|
|
279
|
+
error
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const serviceServe = serviceConfig == null ? undefined : (serveParams)=>{
|
|
284
|
+
if (resolvedServe != null) {
|
|
285
|
+
return resolvedServe(serveParams);
|
|
286
|
+
}
|
|
287
|
+
// Terminal: the plugin API rejected and will not resolve. Stops the
|
|
288
|
+
// listener rather than letting it retry a plugin that never arrives.
|
|
289
|
+
if (serveResolutionFailed) {
|
|
290
|
+
throw new ServiceServeUnavailableError('serviceTunnel is enabled but the "service-server" plugin API could not be resolved');
|
|
291
|
+
}
|
|
292
|
+
// Transient: the API is still resolving (an early-spawn race). The
|
|
293
|
+
// listener retries on backoff and the next spawn finds it.
|
|
294
|
+
throw new ServiceServeNotReadyError('serviceTunnel is enabled but the "service-server" plugin API is not ready yet');
|
|
295
|
+
};
|
|
246
296
|
const hub = setupHubRelay({
|
|
247
297
|
identity,
|
|
248
298
|
runtime: params.runtime,
|
|
@@ -264,6 +314,10 @@ export function createP2PPlugin(options) {
|
|
|
264
314
|
storeUnreadable: receiveConfig.storeUnreadable,
|
|
265
315
|
defaultAccessLevel,
|
|
266
316
|
forwarding: options?.forwarding,
|
|
317
|
+
// The engine's controller-resolver seam, so a delegate-signed
|
|
318
|
+
// `credential:key-grant` wrapping received over the live broadcast lane can
|
|
319
|
+
// be authorized against the grantor's administer chain.
|
|
320
|
+
controllerResolverFor: params.controllerResolverFor,
|
|
267
321
|
...hubReconnectBackoff != null ? {
|
|
268
322
|
hubReconnectBackoff
|
|
269
323
|
} : {},
|
|
@@ -272,7 +326,15 @@ export function createP2PPlugin(options) {
|
|
|
272
326
|
} : {},
|
|
273
327
|
// The same handlers the direct and HTTP transports serve — a tunnel is a
|
|
274
328
|
// route, so it must not reach a different sync implementation.
|
|
275
|
-
syncHandlers: syncHandlers
|
|
329
|
+
syncHandlers: syncHandlers,
|
|
330
|
+
// The service lane's answering half; absent, `setupHubRelay` stands up
|
|
331
|
+
// no service listeners, so a device without `serviceTunnel` is unchanged.
|
|
332
|
+
...serviceServe != null ? {
|
|
333
|
+
serviceServe,
|
|
334
|
+
services: serviceConfig
|
|
335
|
+
} : {},
|
|
336
|
+
// The shared roster-seeded cache reaches the tunnel listeners here.
|
|
337
|
+
cache: didCache
|
|
276
338
|
});
|
|
277
339
|
// Turn a `tunnel://<groupID>/<peerDID>` route into a live session over the
|
|
278
340
|
// group's hub. Until this existed the scheme parsed and then failed at
|
|
@@ -350,6 +412,9 @@ export function createP2PPlugin(options) {
|
|
|
350
412
|
adapter,
|
|
351
413
|
hlc,
|
|
352
414
|
maxDriftMS: params.maxDriftMS,
|
|
415
|
+
// The engine's controller-resolver seam, so the credential reconcile-apply
|
|
416
|
+
// lane (catch-up) can authorize a delegate-signed wrapping it pulls back.
|
|
417
|
+
controllerResolverFor: params.controllerResolverFor,
|
|
353
418
|
emitter,
|
|
354
419
|
runtime: params.runtime,
|
|
355
420
|
autoAcceptPeers: options?.autoAcceptPeers,
|
|
@@ -507,6 +572,7 @@ export function createP2PPlugin(options) {
|
|
|
507
572
|
onHubServerDIDChanged: (listener)=>emitter.on('hubServerDIDChanged', listener),
|
|
508
573
|
createSyncTransport,
|
|
509
574
|
requestLedgerCatchup: (groupID, options)=>hub.requestLedgerCatchup(groupID, options),
|
|
575
|
+
serviceTransportTo: (groupID, peerDID)=>hub.serviceTransportTo(groupID, peerDID),
|
|
510
576
|
setLocalPeerProfile: async (profile)=>{
|
|
511
577
|
await (await hub.presence()).setProfile(profile);
|
|
512
578
|
},
|
|
@@ -573,7 +639,8 @@ export function createP2PPlugin(options) {
|
|
|
573
639
|
logger: params.getLogger('sync-http-server'),
|
|
574
640
|
transports: [
|
|
575
641
|
httpSyncTransport
|
|
576
|
-
]
|
|
642
|
+
],
|
|
643
|
+
cache: didCache
|
|
577
644
|
});
|
|
578
645
|
syncServers.push(httpSyncServer);
|
|
579
646
|
httpAPI.registerProtocol(protocolName, httpSyncTransport.fetch.bind(httpSyncTransport));
|
|
@@ -616,7 +683,8 @@ export function createP2PPlugin(options) {
|
|
|
616
683
|
logger: params.getLogger('peer-http-server'),
|
|
617
684
|
transports: [
|
|
618
685
|
httpPeerTransport
|
|
619
|
-
]
|
|
686
|
+
],
|
|
687
|
+
cache: didCache
|
|
620
688
|
});
|
|
621
689
|
httpAPI.registerProtocol('peer', httpPeerTransport.fetch.bind(httpPeerTransport));
|
|
622
690
|
})();
|
package/lib/protocol.d.ts
CHANGED
|
@@ -229,6 +229,13 @@ export declare const syncProtocol: {
|
|
|
229
229
|
};
|
|
230
230
|
readonly description: "Signed subject states the caller holds, for updates and revocation GC";
|
|
231
231
|
};
|
|
232
|
+
readonly owners: {
|
|
233
|
+
readonly type: "array";
|
|
234
|
+
readonly items: {
|
|
235
|
+
readonly type: "string";
|
|
236
|
+
};
|
|
237
|
+
readonly description: "Owner DIDs to fetch the latest durable manifest for, addressed to the caller — the PULL repair channel independent of LIVE broadcast.";
|
|
238
|
+
};
|
|
232
239
|
};
|
|
233
240
|
readonly required: readonly ["held"];
|
|
234
241
|
readonly additionalProperties: false;
|
|
@@ -327,6 +334,9 @@ export declare const syncProtocol: {
|
|
|
327
334
|
readonly additionalProperties: false;
|
|
328
335
|
};
|
|
329
336
|
};
|
|
337
|
+
readonly grantorAuthority: {
|
|
338
|
+
readonly type: "string";
|
|
339
|
+
};
|
|
330
340
|
};
|
|
331
341
|
readonly required: readonly ["keyID", "keyVersion", "suite", "ownerDID", "keyOp", "keyBranches", "wrapping", "entries"];
|
|
332
342
|
readonly additionalProperties: false;
|
|
@@ -362,6 +372,26 @@ export declare const syncProtocol: {
|
|
|
362
372
|
};
|
|
363
373
|
readonly description: "Tombstones for advertised held IDs that are now revoked (GC)";
|
|
364
374
|
};
|
|
375
|
+
readonly manifests: {
|
|
376
|
+
readonly type: "array";
|
|
377
|
+
readonly items: {
|
|
378
|
+
readonly type: "object";
|
|
379
|
+
readonly properties: {
|
|
380
|
+
readonly manifest: {
|
|
381
|
+
readonly type: "string";
|
|
382
|
+
};
|
|
383
|
+
readonly delegationTokens: {
|
|
384
|
+
readonly type: "array";
|
|
385
|
+
readonly items: {
|
|
386
|
+
readonly type: "string";
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
readonly required: readonly ["manifest", "delegationTokens"];
|
|
391
|
+
readonly additionalProperties: false;
|
|
392
|
+
};
|
|
393
|
+
readonly description: "The latest stored manifest addressed to the caller for each requested owner — the durable repair channel independent of LIVE broadcast.";
|
|
394
|
+
};
|
|
365
395
|
};
|
|
366
396
|
readonly required: readonly ["bundles", "tombstones"];
|
|
367
397
|
readonly additionalProperties: false;
|
package/lib/protocol.js
CHANGED
|
@@ -290,6 +290,13 @@ export const syncProtocol = {
|
|
|
290
290
|
additionalProperties: false
|
|
291
291
|
},
|
|
292
292
|
description: 'Signed subject states the caller holds, for updates and revocation GC'
|
|
293
|
+
},
|
|
294
|
+
owners: {
|
|
295
|
+
type: 'array',
|
|
296
|
+
items: {
|
|
297
|
+
type: 'string'
|
|
298
|
+
},
|
|
299
|
+
description: 'Owner DIDs to fetch the latest durable manifest for, addressed to the caller — the PULL repair channel independent of LIVE broadcast.'
|
|
293
300
|
}
|
|
294
301
|
},
|
|
295
302
|
required: [
|
|
@@ -406,6 +413,12 @@ export const syncProtocol = {
|
|
|
406
413
|
],
|
|
407
414
|
additionalProperties: false
|
|
408
415
|
}
|
|
416
|
+
},
|
|
417
|
+
// Persisted grantor-authority envelope for a delegate-signed
|
|
418
|
+
// wrapping (absent for owner-signed); declared so it isn't an
|
|
419
|
+
// `additionalProperties: false` violation if result validation is enabled.
|
|
420
|
+
grantorAuthority: {
|
|
421
|
+
type: 'string'
|
|
409
422
|
}
|
|
410
423
|
},
|
|
411
424
|
required: [
|
|
@@ -457,6 +470,29 @@ export const syncProtocol = {
|
|
|
457
470
|
additionalProperties: false
|
|
458
471
|
},
|
|
459
472
|
description: 'Tombstones for advertised held IDs that are now revoked (GC)'
|
|
473
|
+
},
|
|
474
|
+
manifests: {
|
|
475
|
+
type: 'array',
|
|
476
|
+
items: {
|
|
477
|
+
type: 'object',
|
|
478
|
+
properties: {
|
|
479
|
+
manifest: {
|
|
480
|
+
type: 'string'
|
|
481
|
+
},
|
|
482
|
+
delegationTokens: {
|
|
483
|
+
type: 'array',
|
|
484
|
+
items: {
|
|
485
|
+
type: 'string'
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
required: [
|
|
490
|
+
'manifest',
|
|
491
|
+
'delegationTokens'
|
|
492
|
+
],
|
|
493
|
+
additionalProperties: false
|
|
494
|
+
},
|
|
495
|
+
description: 'The latest stored manifest addressed to the caller for each requested owner — the durable repair channel independent of LIVE broadcast.'
|
|
460
496
|
}
|
|
461
497
|
},
|
|
462
498
|
required: [
|
package/lib/schema.js
CHANGED
|
@@ -581,6 +581,12 @@ extend type Query {
|
|
|
581
581
|
one group. The store is per-device.
|
|
582
582
|
"""
|
|
583
583
|
controlRequests(groupID: ID): [ControlRequest!]!
|
|
584
|
+
"""
|
|
585
|
+
Prove this device holds the complete credential set the owner's manifests
|
|
586
|
+
promised it: the union of every per-grantor manifest's keys, minus those that
|
|
587
|
+
actually decrypt-verify here. An empty missing list means the pull is complete.
|
|
588
|
+
"""
|
|
589
|
+
credentialProvisioningStatus(ownerDID: DID!): CredentialProvisioningStatus!
|
|
584
590
|
}
|
|
585
591
|
|
|
586
592
|
enum GroupHealthCondition {
|
|
@@ -644,6 +650,48 @@ type ControlRequest implements Node {
|
|
|
644
650
|
settledAt: DateTimeISO
|
|
645
651
|
}
|
|
646
652
|
|
|
653
|
+
"""
|
|
654
|
+
One key's outcome from a grantDeviceCredentials batch: what happened when this
|
|
655
|
+
device tried to hand one of the owner's active credential keys to the recipient
|
|
656
|
+
device. Never an error thrown out of the batch — every key reports its own fate.
|
|
657
|
+
"""
|
|
658
|
+
type CredentialGrantOutcome {
|
|
659
|
+
keyID: ID!
|
|
660
|
+
"""
|
|
661
|
+
The key's version after this call, or null when nothing was granted
|
|
662
|
+
(not-held / error outcomes carry no version).
|
|
663
|
+
"""
|
|
664
|
+
version: Int
|
|
665
|
+
outcome: String!
|
|
666
|
+
"""Present on not-held and error; the caught error's message."""
|
|
667
|
+
reason: String
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
type GrantDeviceCredentialsPayload {
|
|
671
|
+
results: [CredentialGrantOutcome!]!
|
|
672
|
+
"""A grantor-signed manifest claiming exactly the granted/refreshed keys above."""
|
|
673
|
+
manifest: String!
|
|
674
|
+
"""The provisioning epoch this manifest was allocated under."""
|
|
675
|
+
epoch: Int!
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
"""
|
|
679
|
+
Readiness of this device's credential provisioning for one owner. A key counts
|
|
680
|
+
materialized only when its wrapping decrypt-verifies (or, for a zero-entry key,
|
|
681
|
+
merely opens) — presence and a valid signature alone never suffice.
|
|
682
|
+
"""
|
|
683
|
+
type CredentialProvisioningStatus {
|
|
684
|
+
complete: Boolean!
|
|
685
|
+
"""The expected keyIDs not yet materialized on this device."""
|
|
686
|
+
missing: [ID!]!
|
|
687
|
+
"""
|
|
688
|
+
Why complete is false: not-initiated (no expectation on record yet),
|
|
689
|
+
pending (an attempt is in flight, its floor not yet bound), or incomplete
|
|
690
|
+
(asked for, not yet fully materialized/proven). Absent when complete.
|
|
691
|
+
"""
|
|
692
|
+
reason: String
|
|
693
|
+
}
|
|
694
|
+
|
|
647
695
|
extend type Mutation {
|
|
648
696
|
connectPeer(url: String!): ConnectPeerPayload!
|
|
649
697
|
sharePeerGroup(peerDID: ID!, groupID: ID, name: String, send: ShareInput, receive: ShareReceiveInput): SharePeerGroupPayload!
|
|
@@ -654,7 +702,15 @@ extend type Mutation {
|
|
|
654
702
|
DID comes from the request's KeyPackage credential; a disagreeing did field, or a
|
|
655
703
|
DID already on the roster, is refused.
|
|
656
704
|
"""
|
|
657
|
-
admitJoinRequest(groupID: ID!, joinRequest: String!, send: ShareInput, receive: ShareReceiveInput): AdmitJoinRequestPayload!
|
|
705
|
+
admitJoinRequest(groupID: ID!, joinRequest: String!, grants: [String!], send: ShareInput, receive: ShareReceiveInput): AdmitJoinRequestPayload!
|
|
706
|
+
"""
|
|
707
|
+
Grant a device recipientDID this device's active credential keys for ownerDID,
|
|
708
|
+
resolving the recipient from the group's MLS roster. Returns every key's
|
|
709
|
+
outcome plus a grantor-signed manifest the recipient checks its store against.
|
|
710
|
+
minEpoch floors the allocated epoch at the recipient's held epoch, so a
|
|
711
|
+
reprovision attempt always supersedes a manifest it already holds. Defaults to 0.
|
|
712
|
+
"""
|
|
713
|
+
grantDeviceCredentials(groupID: ID!, recipientDID: DID!, ownerDID: DID!, minEpoch: Int): GrantDeviceCredentialsPayload!
|
|
658
714
|
joinPeerGroup(peerDID: ID!, groupID: ID!): JoinPeerGroupPayload!
|
|
659
715
|
"""
|
|
660
716
|
Apply one circle's desired sync end state. A null argument leaves that dimension
|
|
@@ -880,6 +936,11 @@ export function createP2PSchemaExtension(emitter, syncManager, logger = getKubun
|
|
|
880
936
|
const requests = await requireP2P(context).group.listControlRequests(args.groupID);
|
|
881
937
|
return requests.map(toControlRequestSDL);
|
|
882
938
|
},
|
|
939
|
+
credentialProvisioningStatus: async (_source, args, context)=>{
|
|
940
|
+
return await requireP2P(context).peer.credentialProvisioningStatus({
|
|
941
|
+
ownerDID: args.ownerDID
|
|
942
|
+
});
|
|
943
|
+
},
|
|
883
944
|
ownDelegationTokens: async (_source, args, context)=>{
|
|
884
945
|
const viewerDID = context.getViewer();
|
|
885
946
|
if (viewerDID == null || context.p2p == null) {
|
|
@@ -948,6 +1009,7 @@ export function createP2PSchemaExtension(emitter, syncManager, logger = getKubun
|
|
|
948
1009
|
return await requireP2P(context).peer.admitJoinRequest({
|
|
949
1010
|
groupID: args.groupID,
|
|
950
1011
|
joinRequest: args.joinRequest,
|
|
1012
|
+
grants: args.grants ?? undefined,
|
|
951
1013
|
sendModels: send?.models ?? null,
|
|
952
1014
|
receiveActivate: receive?.activate ?? false,
|
|
953
1015
|
// Same owner-signed executor `sharePeerGroup` binds, so the rows this
|
|
@@ -966,6 +1028,21 @@ export function createP2PSchemaExtension(emitter, syncManager, logger = getKubun
|
|
|
966
1028
|
}
|
|
967
1029
|
});
|
|
968
1030
|
},
|
|
1031
|
+
grantDeviceCredentials: async (_source, args, context)=>{
|
|
1032
|
+
const { results, manifest, epoch } = await requireP2P(context).peer.grantDeviceCredentials({
|
|
1033
|
+
groupID: args.groupID,
|
|
1034
|
+
recipientDID: args.recipientDID,
|
|
1035
|
+
ownerDID: args.ownerDID,
|
|
1036
|
+
...args.minEpoch == null ? {} : {
|
|
1037
|
+
minEpoch: args.minEpoch
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
return {
|
|
1041
|
+
results,
|
|
1042
|
+
manifest,
|
|
1043
|
+
epoch
|
|
1044
|
+
};
|
|
1045
|
+
},
|
|
969
1046
|
joinPeerGroup: async (_source, args, context)=>{
|
|
970
1047
|
return await requireP2P(context).peer.joinPeerGroup({
|
|
971
1048
|
peerDID: args.peerDID,
|
package/lib/sync/handlers.js
CHANGED
|
@@ -283,14 +283,33 @@ export { checkSyncDelegation };
|
|
|
283
283
|
...toWireOp(op),
|
|
284
284
|
subjectID: op.subject_id
|
|
285
285
|
}));
|
|
286
|
+
// Durable PULL repair, independent of live broadcast: manifests addressed
|
|
287
|
+
// to the authenticated `callerDID`, never an owner-supplied recipient,
|
|
288
|
+
// so a manifest never leaks to a device other than the one it names.
|
|
289
|
+
let manifests;
|
|
290
|
+
if (ctx.param.owners != null && ctx.param.owners.length > 0) {
|
|
291
|
+
// Independent per-owner lookups — query concurrently, preserve owner order.
|
|
292
|
+
const perOwner = await Promise.all(ctx.param.owners.map((owner)=>store.listManifests({
|
|
293
|
+
ownerDID: owner,
|
|
294
|
+
recipientDID: callerDID
|
|
295
|
+
})));
|
|
296
|
+
manifests = perOwner.flatMap((rows)=>rows.map((row)=>({
|
|
297
|
+
manifest: row.token,
|
|
298
|
+
delegationTokens: row.delegationTokens
|
|
299
|
+
})));
|
|
300
|
+
}
|
|
286
301
|
logger.debug('sync/reconcile-credentials served', {
|
|
287
302
|
callerDID,
|
|
288
303
|
bundles: bundles.length,
|
|
289
|
-
tombstones: tombstones.length
|
|
304
|
+
tombstones: tombstones.length,
|
|
305
|
+
manifests: manifests?.length ?? 0
|
|
290
306
|
});
|
|
291
307
|
return {
|
|
292
308
|
bundles,
|
|
293
|
-
tombstones
|
|
309
|
+
tombstones,
|
|
310
|
+
...manifests != null && {
|
|
311
|
+
manifests
|
|
312
|
+
}
|
|
294
313
|
};
|
|
295
314
|
}
|
|
296
315
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StoreProvider } from '@kubun/db';
|
|
2
|
+
/**
|
|
3
|
+
* The device's own held read delegations for a set of scope owners, as the JWT
|
|
4
|
+
* strings an outbound catch-up rides in `delegationTokens`.
|
|
5
|
+
*
|
|
6
|
+
* Each token is a capability the owner (grantor) issued to this device
|
|
7
|
+
* (audience); presenting it lets the peer's `checkSyncDelegation` arm of
|
|
8
|
+
* `authorizeScope` authorize a pull of that owner's documents — e.g. a paired
|
|
9
|
+
* device reading a controller's documents via the pairing-time delegation.
|
|
10
|
+
*
|
|
11
|
+
* Keyed `grantor: ownerDID, audience: selfDID` (the direction the gate checks).
|
|
12
|
+
* Owners equal to `selfDID` are skipped; results are de-duplicated.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveHeldDelegationTokens(stores: StoreProvider, selfDID: string, owners: Iterable<string>): Promise<Array<string>>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DELEGATION_STORE, getDelegationStore } from '@kubun/store-delegation';
|
|
2
|
+
/**
|
|
3
|
+
* The device's own held read delegations for a set of scope owners, as the JWT
|
|
4
|
+
* strings an outbound catch-up rides in `delegationTokens`.
|
|
5
|
+
*
|
|
6
|
+
* Each token is a capability the owner (grantor) issued to this device
|
|
7
|
+
* (audience); presenting it lets the peer's `checkSyncDelegation` arm of
|
|
8
|
+
* `authorizeScope` authorize a pull of that owner's documents — e.g. a paired
|
|
9
|
+
* device reading a controller's documents via the pairing-time delegation.
|
|
10
|
+
*
|
|
11
|
+
* Keyed `grantor: ownerDID, audience: selfDID` (the direction the gate checks).
|
|
12
|
+
* Owners equal to `selfDID` are skipped; results are de-duplicated.
|
|
13
|
+
*/ export async function resolveHeldDelegationTokens(stores, selfDID, owners) {
|
|
14
|
+
const distinctOwners = new Set();
|
|
15
|
+
for (const owner of owners){
|
|
16
|
+
if (owner !== selfDID) {
|
|
17
|
+
distinctOwners.add(owner);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (distinctOwners.size === 0 || !stores.hasStore(DELEGATION_STORE)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const delegationStore = await getDelegationStore(stores);
|
|
24
|
+
// Independent per-owner lookups — query concurrently; the Set dedups regardless of order.
|
|
25
|
+
const perOwner = await Promise.all([
|
|
26
|
+
...distinctOwners
|
|
27
|
+
].map((ownerDID)=>delegationStore.getDelegationTokens({
|
|
28
|
+
grantor: ownerDID,
|
|
29
|
+
audience: selfDID
|
|
30
|
+
})));
|
|
31
|
+
return [
|
|
32
|
+
...new Set(perOwner.flat().map((row)=>row.token))
|
|
33
|
+
];
|
|
34
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ServerTransportOf } from '@enkaku/protocol';
|
|
2
|
+
import type { Server } from '@enkaku/server';
|
|
3
|
+
import type { DIDCache } from '@kokuin/token';
|
|
4
|
+
import type { Logger } from '@kubun/logger';
|
|
5
|
+
import type { ServiceConfig, ServiceProtocol } from '@kubun/plugin-service-api';
|
|
6
|
+
import { type MailboxHub } from '@kumiai/hub-tunnel';
|
|
7
|
+
import type { Runtime } from '@sozai/runtime';
|
|
8
|
+
import type { GroupHandleRegistry } from '../groups/group-handle-registry.js';
|
|
9
|
+
/**
|
|
10
|
+
* Mirrors `ServiceServerPluginAPI['serve']` (`@kubun/plugin-service-server`)
|
|
11
|
+
* without importing it, so this listener has no direct dependency on that
|
|
12
|
+
* plugin. `serve()` applies provider discovery, handler/access-rule merge,
|
|
13
|
+
* handler-authorization wrapping, and controller-DID verification that a bare
|
|
14
|
+
* `new Server` would not.
|
|
15
|
+
*/
|
|
16
|
+
export type ServeService = (params: {
|
|
17
|
+
transport: ServerTransportOf<ServiceProtocol>;
|
|
18
|
+
services: Record<string, ServiceConfig>;
|
|
19
|
+
requireAuth?: false;
|
|
20
|
+
/** Shared, roster-seeded DID cache forwarded to the served Server. */
|
|
21
|
+
cache?: DIDCache;
|
|
22
|
+
}) => Server<ServiceProtocol>;
|
|
23
|
+
/**
|
|
24
|
+
* The injected {@link ServeService} was called before the `service-server`
|
|
25
|
+
* plugin API had resolved. Transient — the listener retries on its normal backoff.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ServiceServeNotReadyError extends Error {
|
|
28
|
+
constructor(message: string);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The injected {@link ServeService} failed for a reason a retry cannot change
|
|
32
|
+
* (plugin absent, unknown/colliding service name). Terminal — the listener
|
|
33
|
+
* stops answering for this peer instead of spinning on backoff.
|
|
34
|
+
*/
|
|
35
|
+
export declare class ServiceServeUnavailableError extends Error {
|
|
36
|
+
constructor(message: string, options?: {
|
|
37
|
+
cause?: unknown;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export type HubTunnelServiceListenerParams = {
|
|
41
|
+
/**
|
|
42
|
+
* Shared multi-subscriber device hub. Every listener on a device receives on
|
|
43
|
+
* the same topic, so the hub is fronted by a per-peer view that drops other
|
|
44
|
+
* peers' frames before they reach the cipher.
|
|
45
|
+
*/
|
|
46
|
+
hub: MailboxHub;
|
|
47
|
+
registry: GroupHandleRegistry;
|
|
48
|
+
groupID: string;
|
|
49
|
+
localDID: string;
|
|
50
|
+
peerDID: string;
|
|
51
|
+
/**
|
|
52
|
+
* Platform primitives, including the per-session id generator. Threaded in
|
|
53
|
+
* from the caller since this package runs on React Native and browser too,
|
|
54
|
+
* not just node.
|
|
55
|
+
*/
|
|
56
|
+
runtime: Runtime;
|
|
57
|
+
/** Injected `plugin-service-server` `serve()` — see {@link ServeService}. */
|
|
58
|
+
serve: ServeService;
|
|
59
|
+
/** Which services to serve, forwarded verbatim to {@link serve} on every spawn. */
|
|
60
|
+
services: Record<string, ServiceConfig>;
|
|
61
|
+
/**
|
|
62
|
+
* Shared, roster-seeded DID cache forwarded to {@link serve} on every spawn,
|
|
63
|
+
* so a co-member's short-form `did:peer:4` issuer resolves on the served
|
|
64
|
+
* Server.
|
|
65
|
+
*/
|
|
66
|
+
cache?: DIDCache;
|
|
67
|
+
idleTimeoutMs?: number;
|
|
68
|
+
logger?: Logger;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Answer side of the service tunnel — the same directed hub-tunnel machinery as
|
|
72
|
+
* `HubTunnelSyncListener`, on a distinct lane so a service session never
|
|
73
|
+
* collides with a sync session's topics or ratchet generation.
|
|
74
|
+
*
|
|
75
|
+
* Unlike the sync listener, this does not build a bare `Server`: it hands the
|
|
76
|
+
* spawned transport to the injected {@link ServeService} callback so
|
|
77
|
+
* provider discovery, handler wrapping, and controller-DID verification apply.
|
|
78
|
+
*/
|
|
79
|
+
export declare class HubTunnelServiceListener {
|
|
80
|
+
#private;
|
|
81
|
+
constructor(params: HubTunnelServiceListenerParams);
|
|
82
|
+
start(): void;
|
|
83
|
+
stop(): Promise<void>;
|
|
84
|
+
}
|