@kubun/plugin-p2p 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/group.js +1073 -746
- package/lib/context/hub.js +2 -1
- package/lib/context/join.js +50 -15
- package/lib/context/peer.d.ts +92 -0
- package/lib/context/peer.js +1216 -0
- package/lib/context/require-admin.d.ts +57 -0
- package/lib/context/require-admin.js +57 -0
- package/lib/context/sync.d.ts +12 -0
- package/lib/context/sync.js +234 -1
- package/lib/context/types.d.ts +56 -20
- package/lib/context/types.js +23 -3
- package/lib/groups/access-default-token.d.ts +44 -0
- package/lib/groups/access-default-token.js +103 -0
- package/lib/groups/admin-roster.d.ts +42 -117
- package/lib/groups/admin-roster.js +53 -168
- package/lib/groups/anchor-store.d.ts +15 -0
- package/lib/groups/anchor-store.js +37 -0
- package/lib/groups/app-cursor-store.d.ts +17 -0
- package/lib/groups/app-cursor-store.js +34 -0
- package/lib/groups/broadcast.d.ts +199 -81
- package/lib/groups/broadcast.js +443 -368
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +32 -64
- package/lib/groups/circle-projection.js +95 -181
- package/lib/groups/circle-reducers.d.ts +71 -110
- package/lib/groups/circle-reducers.js +57 -127
- package/lib/groups/commit-adoption.d.ts +77 -0
- package/lib/groups/commit-adoption.js +108 -0
- package/lib/groups/commit-journal.d.ts +14 -0
- package/lib/groups/commit-journal.js +65 -0
- package/lib/groups/control-request.d.ts +66 -0
- package/lib/groups/control-request.js +101 -0
- package/lib/groups/events.d.ts +14 -1
- package/lib/groups/group-anchor.d.ts +18 -57
- package/lib/groups/group-anchor.js +15 -100
- package/lib/groups/group-crypto.d.ts +36 -13
- package/lib/groups/group-crypto.js +124 -50
- package/lib/groups/group-handle-registry.d.ts +106 -36
- package/lib/groups/group-handle-registry.js +351 -61
- package/lib/groups/group-handlers.d.ts +5 -4
- package/lib/groups/group-handlers.js +155 -29
- package/lib/groups/group-health-monitor.d.ts +1 -1
- package/lib/groups/group-health-monitor.js +1 -1
- package/lib/groups/group-mls.d.ts +35 -4
- package/lib/groups/group-mls.js +594 -62
- package/lib/groups/group-peer-manager.d.ts +212 -19
- package/lib/groups/group-peer-manager.js +1061 -69
- package/lib/groups/group-protocols.d.ts +485 -28
- package/lib/groups/group-protocols.js +289 -15
- package/lib/groups/invite-payload.d.ts +48 -0
- package/lib/groups/join-request-identity.d.ts +21 -0
- package/lib/groups/join-request-identity.js +43 -0
- package/lib/groups/join-utils.d.ts +60 -0
- package/lib/groups/join-utils.js +262 -0
- package/lib/groups/ledger-affected-events.d.ts +11 -6
- package/lib/groups/ledger-affected-events.js +15 -26
- package/lib/groups/ledger-commit-fold.d.ts +107 -0
- package/lib/groups/ledger-commit-fold.js +167 -0
- package/lib/groups/ledger-ingest.d.ts +124 -0
- package/lib/groups/ledger-ingest.js +212 -0
- package/lib/groups/ledger.d.ts +44 -0
- package/lib/groups/ledger.js +30 -0
- package/lib/groups/manager.d.ts +202 -19
- package/lib/groups/manager.js +623 -106
- package/lib/groups/mls-codec.d.ts +27 -6
- package/lib/groups/mls-codec.js +30 -15
- package/lib/groups/mls-encryptor.js +9 -6
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +12 -4
- package/lib/groups/mls-receive-errors.d.ts +0 -2
- package/lib/groups/mls-receive-errors.js +3 -6
- package/lib/groups/mls-state.d.ts +5 -3
- package/lib/groups/mls-state.js +27 -4
- package/lib/groups/peer-presence.d.ts +90 -0
- package/lib/groups/peer-presence.js +295 -0
- package/lib/groups/peer-selection.d.ts +37 -0
- package/lib/groups/peer-selection.js +72 -0
- package/lib/groups/roster-projection.d.ts +53 -0
- package/lib/groups/roster-projection.js +65 -0
- package/lib/groups/store-received-grant.d.ts +4 -3
- package/lib/groups/store-received-grant.js +80 -35
- package/lib/groups/store-received-revocation.d.ts +16 -13
- package/lib/groups/store-received-revocation.js +23 -22
- package/lib/hub/http-client.d.ts +10 -27
- package/lib/hub/http-client.js +2 -19
- package/lib/hub/hub-like.d.ts +57 -6
- package/lib/hub/hub-like.js +539 -52
- package/lib/hub/loopback-log-hub.d.ts +20 -0
- package/lib/hub/loopback-log-hub.js +178 -0
- package/lib/hub/manager.d.ts +25 -40
- package/lib/hub/manager.js +51 -94
- package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
- package/lib/hub/peer-scoped-hub-view.js +92 -0
- package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
- package/lib/hub/sender-scoped-hub-view.js +59 -0
- package/lib/hub/server-did.d.ts +67 -0
- package/lib/hub/server-did.js +131 -0
- package/lib/hub/wiring.d.ts +71 -14
- package/lib/hub/wiring.js +80 -23
- package/lib/index.d.ts +48 -13
- package/lib/index.js +257 -86
- package/lib/peer/connection-registry.d.ts +45 -0
- package/lib/peer/connection-registry.js +52 -0
- package/lib/peer/handlers.d.ts +69 -0
- package/lib/peer/handlers.js +110 -0
- package/lib/protocol.d.ts +213 -0
- package/lib/protocol.js +268 -0
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +805 -131
- package/lib/sync/access-default-sender.d.ts +23 -0
- package/lib/sync/access-default-sender.js +80 -0
- package/lib/sync/authorize.d.ts +57 -3
- package/lib/sync/authorize.js +45 -4
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +2 -3
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +55 -39
- package/lib/sync/errors.d.ts +9 -0
- package/lib/sync/errors.js +10 -0
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +13 -19
- package/lib/sync/handlers.js +54 -14
- package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
- package/lib/sync/hub-tunnel-sync-listener.js +126 -37
- package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
- package/lib/sync/hub-tunnel-sync-provider.js +68 -19
- package/lib/sync/merkle-channel.d.ts +9 -1
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/peer-registry.d.ts +14 -10
- package/lib/sync/peer-registry.js +0 -25
- package/lib/sync/sync-client.d.ts +11 -1
- package/lib/sync/sync-client.js +72 -10
- package/lib/sync/sync-manager.d.ts +22 -4
- package/lib/sync/sync-manager.js +39 -10
- package/lib/sync/tunnel-listeners.d.ts +52 -0
- package/lib/sync/tunnel-listeners.js +175 -0
- package/lib/sync/tunnel-observability.d.ts +25 -0
- package/lib/sync/tunnel-observability.js +63 -0
- package/lib/sync/tunnel-topics.d.ts +20 -0
- package/lib/sync/tunnel-topics.js +28 -0
- package/lib/types.d.ts +699 -28
- package/lib/types.js +0 -1
- package/lib/util/handler-error.d.ts +16 -0
- package/lib/util/handler-error.js +64 -0
- package/lib/util/mutex.d.ts +14 -1
- package/lib/util/mutex.js +37 -3
- package/package.json +56 -52
- package/lib/groups/ledger-entry.d.ts +0 -37
- package/lib/groups/ledger-entry.js +0 -56
- package/lib/groups/ledger-fold.d.ts +0 -56
- package/lib/groups/ledger-fold.js +0 -53
- package/lib/hub/did-observing-transport.d.ts +0 -75
- package/lib/hub/did-observing-transport.js +0 -126
- package/lib/hub/errors.d.ts +0 -24
- package/lib/hub/errors.js +0 -33
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { verifyToken } from '@kokuin/token';
|
|
1
|
+
import { normalizeDID, verifyToken } from '@kokuin/token';
|
|
2
|
+
import { MAX_CAP_TTL_SECONDS } from '@kubun/store-delegation';
|
|
2
3
|
/**
|
|
3
4
|
* Verify a received capability grant and, when it is addressed to this device,
|
|
4
5
|
* store it as a held row so the engine's auto-attach can reuse it on later
|
|
@@ -6,8 +7,9 @@ import { verifyToken } from '@kokuin/token';
|
|
|
6
7
|
*
|
|
7
8
|
* Returns `true` when the held row was inserted or materially changed (LWW
|
|
8
9
|
* upsert with a newer hlc and at least one differing content field). Returns
|
|
9
|
-
* `false` when no row change occurred — verification failure,
|
|
10
|
-
*
|
|
10
|
+
* `false` when no row change occurred — verification failure, a subject that
|
|
11
|
+
* is not the signer, audience mismatch, missing required claims, a lifetime
|
|
12
|
+
* above `MAX_CAP_TTL_SECONDS`, LWW lost against an existing row, or an
|
|
11
13
|
* idempotent re-broadcast with identical content. Never throws on a forged or
|
|
12
14
|
* malformed token — it is skipped like an unverifiable broadcast entry.
|
|
13
15
|
*
|
|
@@ -28,16 +30,56 @@ import { verifyToken } from '@kokuin/token';
|
|
|
28
30
|
});
|
|
29
31
|
return false;
|
|
30
32
|
}
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
+
// Sender-bound: `sub` is the grantor the held row is filed under, and a
|
|
34
|
+
// signature only proves `iss`. Without this any co-member could file a row
|
|
35
|
+
// attributed to another peer — including one colliding with that peer's own
|
|
36
|
+
// `jti`, which is what makes the grantor-scoped revocation lookups
|
|
37
|
+
// unanswerable.
|
|
38
|
+
//
|
|
39
|
+
// The consequence is that **sub-delegation is not supported over this path**:
|
|
40
|
+
// a re-delegated grant, whose `sub` is the original grantor and whose `iss`
|
|
41
|
+
// is the re-delegator, is dropped here. Nothing mints one today
|
|
42
|
+
// (`grantWriteCapability` signs only for `identity.id`), but `checkCapability`
|
|
43
|
+
// does verify multi-hop chains, so enforcement would accept what this refuses
|
|
44
|
+
// to store. Supporting it means carrying and verifying the parent chain at
|
|
45
|
+
// this point, not relaxing the predicate — see
|
|
46
|
+
// `docs/agents/plans/backlog/sub-delegation-over-broadcast.md`.
|
|
47
|
+
if (normalizeDID(payload.sub) !== normalizeDID(payload.iss)) {
|
|
48
|
+
logger?.warn('received grant subject is not its signer, skipping', {
|
|
49
|
+
groupID
|
|
50
|
+
});
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
// Only store grants addressed to this device. Both sides are folded: a
|
|
54
|
+
// `did:peer:4` audience reaches us in long form on first contact and short
|
|
55
|
+
// form thereafter, and a raw compare would silently discard our own grant.
|
|
56
|
+
if (normalizeDID(payload.aud) !== normalizeDID(selfDID)) {
|
|
33
57
|
return false;
|
|
34
58
|
}
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
// `iat` joins the guard because the TTL bound below is unanswerable without
|
|
60
|
+
// it, and an unanswerable bound is the hole this check exists to close. Both
|
|
61
|
+
// producers (`grantWriteCapability`, `KubunClient.mintWriteGrant`) stamp it.
|
|
62
|
+
if (payload.jti == null || payload.exp == null || payload.iat == null) {
|
|
63
|
+
logger?.warn('received grant token missing jti/exp/iat, skipping', {
|
|
37
64
|
groupID
|
|
38
65
|
});
|
|
39
66
|
return false;
|
|
40
67
|
}
|
|
68
|
+
// A capability accepted here can be revoked, and a co-member retains that
|
|
69
|
+
// revocation only for `MAX_CAP_TTL_SECONDS` past the revocation's own `iat` —
|
|
70
|
+
// it holds no copy of the capability to read an expiry from. Accepting a
|
|
71
|
+
// longer-lived grant would let it outlive its revocation and start being
|
|
72
|
+
// honored again. Skipped like any other unusable broadcast, not thrown: the
|
|
73
|
+
// sender is not necessarily this device's resolver.
|
|
74
|
+
if (payload.exp - payload.iat > MAX_CAP_TTL_SECONDS) {
|
|
75
|
+
logger?.warn('received grant lifetime exceeds the maximum, skipping', {
|
|
76
|
+
groupID,
|
|
77
|
+
jti: payload.jti,
|
|
78
|
+
ttl: payload.exp - payload.iat,
|
|
79
|
+
max: MAX_CAP_TTL_SECONDS
|
|
80
|
+
});
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
41
83
|
const resource = typeof payload.res === 'string' ? payload.res : JSON.stringify(payload.res);
|
|
42
84
|
const act = typeof payload.act === 'string' ? payload.act : JSON.stringify(payload.act);
|
|
43
85
|
const changed = await delegationStore.addDelegationToken({
|
|
@@ -63,40 +105,43 @@ import { verifyToken } from '@kokuin/token';
|
|
|
63
105
|
// `delegationTokenAdded` emission so subscribers see consistent ground
|
|
64
106
|
// truth from the revocation backend on the same tick.
|
|
65
107
|
if (changed) {
|
|
66
|
-
const pending = await delegationStore.
|
|
108
|
+
const pending = await delegationStore.getPendingRevocationByIssuer(payload.jti, payload.sub);
|
|
67
109
|
if (pending != null) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
await emitter.emit('delegationTokenRevoked', {
|
|
80
|
-
jti: payload.jti,
|
|
81
|
-
grantor: payload.sub,
|
|
82
|
-
audience: payload.aud,
|
|
83
|
-
revokerDID: pending.revoker_did,
|
|
84
|
-
revokedAt: pending.revoked_iat,
|
|
85
|
-
verifiedAt,
|
|
86
|
-
capExp: payload.exp,
|
|
87
|
-
groupID
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
logger?.warn('pending revocation iss does not match arriving cap, dropping', {
|
|
110
|
+
// Pin the `verified_at` we send to subscribers to the same value the
|
|
111
|
+
// store writes; passing it explicitly avoids a millisecond-scale
|
|
112
|
+
// skew between the row and the emitted event under clock-tick
|
|
113
|
+
// boundaries.
|
|
114
|
+
const verifiedAt = Math.floor(Date.now() / 1000);
|
|
115
|
+
const flipped = await delegationStore.markRevocationVerified(payload.jti, payload.sub, {
|
|
116
|
+
cap_exp: payload.exp,
|
|
117
|
+
verified_at: verifiedAt
|
|
118
|
+
});
|
|
119
|
+
if (flipped && emitter != null) {
|
|
120
|
+
await emitter.emit('delegationTokenRevoked', {
|
|
92
121
|
jti: payload.jti,
|
|
93
|
-
|
|
94
|
-
|
|
122
|
+
grantor: payload.sub,
|
|
123
|
+
audience: payload.aud,
|
|
124
|
+
revokerDID: pending.revoker_did,
|
|
125
|
+
revokedAt: pending.revoked_iat,
|
|
126
|
+
verifiedAt,
|
|
127
|
+
capExp: payload.exp,
|
|
95
128
|
groupID
|
|
96
129
|
});
|
|
97
|
-
await delegationStore.deletePendingRevocation(payload.jti);
|
|
98
130
|
}
|
|
99
131
|
}
|
|
132
|
+
// A `jti` names a capability only within one grantor's namespace, and it
|
|
133
|
+
// travels to the whole group — so a pending row from another author is
|
|
134
|
+
// either a forgery or a claim about a different capability. Either way this
|
|
135
|
+
// grant cannot cross-check it and it can never bind here.
|
|
136
|
+
const dropped = await delegationStore.deletePendingRevocationsFromOtherIssuers(payload.jti, payload.sub);
|
|
137
|
+
if (dropped > 0) {
|
|
138
|
+
logger?.warn('dropped pending revocations not authored by the arriving cap’s issuer', {
|
|
139
|
+
jti: payload.jti,
|
|
140
|
+
dropped,
|
|
141
|
+
capGrantor: payload.sub,
|
|
142
|
+
groupID
|
|
143
|
+
});
|
|
144
|
+
}
|
|
100
145
|
}
|
|
101
146
|
// Emit only when the held row was actually inserted or materially changed.
|
|
102
147
|
// Idempotent re-broadcasts (LWW lost, or won with byte-identical content)
|
|
@@ -21,26 +21,29 @@ export type StoreReceivedRevocationParams = {
|
|
|
21
21
|
* This helper does not emit; downstream subscription wiring lives elsewhere.
|
|
22
22
|
*/
|
|
23
23
|
emitter?: P2PEventEmitter;
|
|
24
|
-
/** Optional logger — debug on verification failure or malformed payload
|
|
24
|
+
/** Optional logger — debug on verification failure or malformed payload. */
|
|
25
25
|
logger?: Logger;
|
|
26
26
|
};
|
|
27
27
|
/**
|
|
28
28
|
* Verify a received revocation token and write its row to the P2P store.
|
|
29
29
|
*
|
|
30
|
-
* Verified-on-arrival: when
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
30
|
+
* Verified-on-arrival: when this device already holds a capability the
|
|
31
|
+
* record's own `iss` granted under that `jti`, the row is stored with
|
|
32
|
+
* `verified_at` populated so the hot-path `isRevoked` gate fires
|
|
33
|
+
* immediately. Otherwise the row is stored as pending (`verified_at` null)
|
|
34
|
+
* until a later `delegation:share` from that same issuer triggers a
|
|
35
|
+
* cross-check that flips it via `markRevocationVerified`.
|
|
36
|
+
*
|
|
37
|
+
* Holding someone else's capability under the same `jti` is not evidence
|
|
38
|
+
* that this record is forged — a `jti` is namespaced by its grantor and any
|
|
39
|
+
* co-member can reuse one — so such a record is stored pending like any
|
|
40
|
+
* other unmatched one. It binds nothing: every enforcement read is scoped to
|
|
41
|
+
* the revoker, and `storeReceivedGrant` drops it when the issuer's own
|
|
42
|
+
* capability arrives.
|
|
39
43
|
*
|
|
40
44
|
* Returns `true` when the row was inserted or materially changed (LWW
|
|
41
45
|
* upsert). Returns `false` on verification failure, non-revocation
|
|
42
|
-
* payload,
|
|
43
|
-
*
|
|
44
|
-
* unverifiable broadcast entry.
|
|
46
|
+
* payload, or a lost/idempotent LWW upsert. Never throws on a forged or
|
|
47
|
+
* malformed token — it is skipped like an unverifiable broadcast entry.
|
|
45
48
|
*/
|
|
46
49
|
export declare function storeReceivedRevocation(params: StoreReceivedRevocationParams): Promise<boolean>;
|
|
@@ -2,21 +2,24 @@ import { verifyToken } from '@kokuin/token';
|
|
|
2
2
|
/**
|
|
3
3
|
* Verify a received revocation token and write its row to the P2P store.
|
|
4
4
|
*
|
|
5
|
-
* Verified-on-arrival: when
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
5
|
+
* Verified-on-arrival: when this device already holds a capability the
|
|
6
|
+
* record's own `iss` granted under that `jti`, the row is stored with
|
|
7
|
+
* `verified_at` populated so the hot-path `isRevoked` gate fires
|
|
8
|
+
* immediately. Otherwise the row is stored as pending (`verified_at` null)
|
|
9
|
+
* until a later `delegation:share` from that same issuer triggers a
|
|
10
|
+
* cross-check that flips it via `markRevocationVerified`.
|
|
11
|
+
*
|
|
12
|
+
* Holding someone else's capability under the same `jti` is not evidence
|
|
13
|
+
* that this record is forged — a `jti` is namespaced by its grantor and any
|
|
14
|
+
* co-member can reuse one — so such a record is stored pending like any
|
|
15
|
+
* other unmatched one. It binds nothing: every enforcement read is scoped to
|
|
16
|
+
* the revoker, and `storeReceivedGrant` drops it when the issuer's own
|
|
17
|
+
* capability arrives.
|
|
14
18
|
*
|
|
15
19
|
* Returns `true` when the row was inserted or materially changed (LWW
|
|
16
20
|
* upsert). Returns `false` on verification failure, non-revocation
|
|
17
|
-
* payload,
|
|
18
|
-
*
|
|
19
|
-
* unverifiable broadcast entry.
|
|
21
|
+
* payload, or a lost/idempotent LWW upsert. Never throws on a forged or
|
|
22
|
+
* malformed token — it is skipped like an unverifiable broadcast entry.
|
|
20
23
|
*/ export async function storeReceivedRevocation(params) {
|
|
21
24
|
const { delegationStore, token, groupID, hlc, emitter, logger } = params;
|
|
22
25
|
let payload;
|
|
@@ -37,19 +40,17 @@ import { verifyToken } from '@kokuin/token';
|
|
|
37
40
|
return false;
|
|
38
41
|
}
|
|
39
42
|
const { jti, iss, iat } = payload;
|
|
40
|
-
|
|
43
|
+
// Scoped to the record's own issuer. A co-member can mint a capability
|
|
44
|
+
// reusing a `jti` it saw broadcast, so asking for "the" capability named by a
|
|
45
|
+
// `jti` returns an arbitrary grantor's row — and the grantor's genuine
|
|
46
|
+
// revocation is the one with something to lose when the answer is wrong.
|
|
47
|
+
const cap = await delegationStore.getDelegationTokenByGrantorJTI({
|
|
48
|
+
jti,
|
|
49
|
+
grantor: iss
|
|
50
|
+
});
|
|
41
51
|
let verifiedAt = null;
|
|
42
52
|
let capExp = null;
|
|
43
53
|
if (cap != null) {
|
|
44
|
-
if (cap.grantor !== iss) {
|
|
45
|
-
logger?.warn('received revocation iss does not match known capability grantor, skipping', {
|
|
46
|
-
groupID,
|
|
47
|
-
jti,
|
|
48
|
-
iss,
|
|
49
|
-
grantor: cap.grantor
|
|
50
|
-
});
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
54
|
verifiedAt = Math.floor(Date.now() / 1000);
|
|
54
55
|
capExp = cap.exp;
|
|
55
56
|
}
|
package/lib/hub/http-client.d.ts
CHANGED
|
@@ -1,42 +1,25 @@
|
|
|
1
1
|
import { Client } from '@enkaku/client';
|
|
2
2
|
import type { Identity } from '@kokuin/token';
|
|
3
3
|
import type { HubProtocol } from '@kumiai/hub-protocol';
|
|
4
|
-
import { type ServerDIDMismatchObserver, type ServerDIDObserver } from './did-observing-transport.js';
|
|
5
4
|
export type CreateHubClientOptions = {
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* The hub's DID, resolved per (re)connect by the spawning caller — from the
|
|
7
|
+
* local hub row when it records one, otherwise from the hub's `/info`. Every
|
|
8
|
+
* token this client signs names it as the audience, and a hub always refuses a
|
|
9
|
+
* token that names nobody. Absent only when discovery failed: the connection
|
|
10
|
+
* is then attempted anyway and refused by the hub, which is what triggers the
|
|
11
|
+
* next discovery.
|
|
11
12
|
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Pinned `server_did` resolved from the local hub row at spawn time. When set,
|
|
15
|
-
* the underlying `DIDObservingTransport` enforces it: a non-matching first
|
|
16
|
-
* response raises `HubServerDIDMismatchError` and no message is delivered to
|
|
17
|
-
* the inner Client (hard-fail path). When unset, the wrapper falls back to
|
|
18
|
-
* TOFU capture via `onServerDID`.
|
|
19
|
-
*/
|
|
20
|
-
expectedServerDID?: string;
|
|
21
|
-
/**
|
|
22
|
-
* Side-channel notifier fired when a pinned-DID mismatch is detected. Lets the
|
|
23
|
-
* caller log + evict on top of the typed error that propagates through the
|
|
24
|
-
* read loop.
|
|
25
|
-
*/
|
|
26
|
-
onMismatch?: ServerDIDMismatchObserver;
|
|
13
|
+
serverID?: string;
|
|
27
14
|
};
|
|
28
15
|
export type CreateHubClient = (hubURL: string, opts?: CreateHubClientOptions) => Promise<Client<HubProtocol>>;
|
|
29
16
|
export type CreateHTTPHubClientParams = {
|
|
30
17
|
identity: Identity;
|
|
31
18
|
fetch?: typeof globalThis.fetch;
|
|
32
|
-
serverID?: string;
|
|
33
19
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* re-fire this hook, even if `iss` differs. Used by `HubConnectionPool` to
|
|
37
|
-
* drive TOFU pinning into the hub manager. Per-call `onServerDID` from
|
|
38
|
-
* `CreateHubClientOptions` overrides this factory-level default.
|
|
20
|
+
* Fallback audience for every client this factory builds, for a caller that
|
|
21
|
+
* knows the hub's DID up front. Per-call `serverID` wins when set.
|
|
39
22
|
*/
|
|
40
|
-
|
|
23
|
+
serverID?: string;
|
|
41
24
|
};
|
|
42
25
|
export declare function createHTTPHubClient(params: CreateHTTPHubClientParams): CreateHubClient;
|
package/lib/hub/http-client.js
CHANGED
|
@@ -1,32 +1,15 @@
|
|
|
1
1
|
import { Client } from '@enkaku/client';
|
|
2
2
|
import { ClientTransport } from '@enkaku/http-fetch';
|
|
3
|
-
import { DIDObservingTransport } from './did-observing-transport.js';
|
|
4
3
|
export function createHTTPHubClient(params) {
|
|
5
4
|
return async (hubURL, opts)=>{
|
|
6
|
-
const
|
|
5
|
+
const transport = new ClientTransport({
|
|
7
6
|
url: hubURL,
|
|
8
7
|
fetch: params.fetch
|
|
9
8
|
});
|
|
10
|
-
const observer = opts?.onServerDID ?? params.onServerDID;
|
|
11
|
-
const expectedServerDID = opts?.expectedServerDID;
|
|
12
|
-
const onMismatch = opts?.onMismatch;
|
|
13
|
-
// Wrap whenever any DID-aware behaviour is needed: TOFU capture (observer),
|
|
14
|
-
// pinned-DID enforcement (expectedServerDID), or mismatch side-channel.
|
|
15
|
-
const needsWrapper = observer != null || expectedServerDID != null || onMismatch != null;
|
|
16
|
-
const transport = needsWrapper ? new DIDObservingTransport({
|
|
17
|
-
inner,
|
|
18
|
-
// Wrapper requires an observer; supply a no-op when only enforcement
|
|
19
|
-
// is requested. The TOFU branch in the wrapper is gated by
|
|
20
|
-
// `expectedServerDID == null` regardless.
|
|
21
|
-
onServerDID: observer ?? (()=>{}),
|
|
22
|
-
expectedServerDID,
|
|
23
|
-
hubURL,
|
|
24
|
-
onMismatch
|
|
25
|
-
}) : inner;
|
|
26
9
|
return new Client({
|
|
27
10
|
transport,
|
|
28
11
|
identity: params.identity,
|
|
29
|
-
serverID: params.serverID
|
|
12
|
+
serverID: opts?.serverID ?? params.serverID
|
|
30
13
|
});
|
|
31
14
|
};
|
|
32
15
|
}
|
package/lib/hub/hub-like.d.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Client } from '@enkaku/client';
|
|
2
2
|
import type { Logger } from '@kubun/logger';
|
|
3
|
-
import type
|
|
4
|
-
import type {
|
|
3
|
+
import { type HubProtocol } from '@kumiai/hub-protocol';
|
|
4
|
+
import type { HubReceiveSubscription, LogHub, MailboxHub } from '@kumiai/hub-tunnel';
|
|
5
5
|
export type CreateHubLikeParams = {
|
|
6
6
|
/**
|
|
7
7
|
* Builds a fresh `Client<HubProtocol>` bound to one hub URL + device identity.
|
|
8
8
|
* Called once on the first connect and again on every reconnect. Injected
|
|
9
9
|
* rather than constructed here so the adapter is testable against a fake
|
|
10
|
-
* client without standing up HTTP.
|
|
10
|
+
* client without standing up HTTP. The signal is the adapter's own: a factory
|
|
11
|
+
* that reaches the network (hub discovery) must abandon it on dispose, or
|
|
12
|
+
* teardown waits out a request to a hub that is not answering.
|
|
11
13
|
*/
|
|
12
|
-
createClient: () => Client<HubProtocol> | Promise<Client<HubProtocol>>;
|
|
14
|
+
createClient: (signal: AbortSignal) => Client<HubProtocol> | Promise<Client<HubProtocol>>;
|
|
13
15
|
/** Authenticated DID of this device — drains receive and stamps publishes. */
|
|
14
16
|
localDID: string;
|
|
17
|
+
/**
|
|
18
|
+
* Called when the hub refused a request with `ACCESS_DENIED`, awaited before
|
|
19
|
+
* the reconnect backoff so anything it corrects is in place for the next dial.
|
|
20
|
+
* That code conflates a refused audience with a refused caller, so which one it
|
|
21
|
+
* was is the handler's to work out, not the adapter's.
|
|
22
|
+
*/
|
|
23
|
+
onAccessDenied?: () => Promise<void>;
|
|
15
24
|
logger?: Logger;
|
|
16
25
|
ackFlushMs?: number;
|
|
17
26
|
ackFlushMax?: number;
|
|
@@ -19,6 +28,48 @@ export type CreateHubLikeParams = {
|
|
|
19
28
|
backoffMaxMs?: number;
|
|
20
29
|
backoffJitter?: number;
|
|
21
30
|
};
|
|
22
|
-
|
|
31
|
+
/** What a subscriber may narrow its own drain to, beyond its DID. */
|
|
32
|
+
export type DeviceHubReceiveOptions = {
|
|
33
|
+
/**
|
|
34
|
+
* Deliver only frames the hub attested as sent by this DID.
|
|
35
|
+
*
|
|
36
|
+
* The narrowing happens on the fan-out, before anything is queued and long
|
|
37
|
+
* before anything is decrypted, which is what a directed session needs: MLS
|
|
38
|
+
* consumes a per-sender ratchet generation on open, so a session that opens a
|
|
39
|
+
* frame belonging to another has DESTROYED it — the session it was for waits
|
|
40
|
+
* out its idle timeout with nothing raised anywhere.
|
|
41
|
+
*/
|
|
42
|
+
senderDID?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Deliver only frames published to this topic.
|
|
45
|
+
*
|
|
46
|
+
* The tunnel transport already asks for this (`hub.receive(localDID, {
|
|
47
|
+
* topicID: receiveTopicID })`), and until it was honoured here the request
|
|
48
|
+
* went nowhere. It has to be applied on the fan-out for the same reason
|
|
49
|
+
* `senderDID` does: the encrypting transport decrypts a frame before the
|
|
50
|
+
* topic demux one layer in ever sees it, so a sink handed another lane's
|
|
51
|
+
* frame has consumed a ratchet generation and destroyed it.
|
|
52
|
+
*/
|
|
53
|
+
topicID?: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The device hub as this package uses it: a `MailboxHub` whose `receive` also
|
|
57
|
+
* takes {@link DeviceHubReceiveOptions}. Declared because the tunnel transport
|
|
58
|
+
* takes the narrower `MailboxHub`, so a caller that needs a filtered drain has
|
|
59
|
+
* to hold this type to ask for one.
|
|
60
|
+
*/
|
|
61
|
+
export type DeviceHub = MailboxHub & {
|
|
62
|
+
receive: (subscriberDID: string, options?: DeviceHubReceiveOptions) => HubReceiveSubscription;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The reconnecting adapter itself, as its owner holds it: a hub, plus the two
|
|
66
|
+
* things only whoever built the connection can do — retry it now, and close it.
|
|
67
|
+
* Kept off {@link DeviceHub} because that is the surface a tunnel session
|
|
68
|
+
* consumes, and a session has no connection of its own to retry.
|
|
69
|
+
*/
|
|
70
|
+
export type ReconnectingDeviceHub = LogHub & DeviceHub & {
|
|
71
|
+
/** Try the hub now rather than when the backoff says so. */
|
|
72
|
+
retryNow: () => Promise<boolean>;
|
|
23
73
|
dispose: () => Promise<void>;
|
|
24
74
|
};
|
|
75
|
+
export declare function createHubLike(params: CreateHubLikeParams): ReconnectingDeviceHub;
|