agent-comms 8.15.9 → 8.15.11
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/bridges/user/web/server.d.ts.map +1 -1
- package/dist/bridges/user/web/server.js +44 -24
- package/dist/bridges/user/web/server.js.map +1 -1
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
- package/dist/bridges/user/web/web-assets.d.ts +28 -0
- package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
- package/dist/bridges/user/web/web-assets.js +67 -0
- package/dist/bridges/user/web/web-assets.js.map +1 -0
- package/dist/test/pi-manifest-packed.integration.test.d.ts +2 -0
- package/dist/test/pi-manifest-packed.integration.test.d.ts.map +1 -0
- package/dist/test/pi-manifest-packed.integration.test.js +68 -0
- package/dist/test/pi-manifest-packed.integration.test.js.map +1 -0
- package/package.json +8 -2
- package/src/account-cli.ts +195 -0
- package/src/account-join-cli.ts +191 -0
- package/src/bridges/cc-peer/approval-commands.ts +82 -0
- package/src/bridges/cc-peer/bridge.ts +138 -0
- package/src/bridges/cc-peer/default-front.ts +58 -0
- package/src/bridges/cc-peer/front-controller.ts +160 -0
- package/src/bridges/cc-peer/front-relay.ts +191 -0
- package/src/bridges/cc-peer/front-runtime.ts +167 -0
- package/src/bridges/cc-peer/front.ts +59 -0
- package/src/bridges/cc-peer/reply-aliases.ts +90 -0
- package/src/bridges/cc-peer/run.ts +100 -0
- package/src/bridges/cc-peer/shared-peer.ts +28 -0
- package/src/bridges/claude-code/channel.ts +318 -0
- package/src/bridges/codex/tool.ts +132 -0
- package/src/bridges/mcp/server.ts +137 -0
- package/src/bridges/opencode/plugin.ts +130 -0
- package/src/bridges/pi/extension.ts +330 -0
- package/src/bridges/registry.ts +17 -0
- package/src/bridges/user/cli.ts +87 -0
- package/src/bridges/user/controller.ts +345 -0
- package/src/bridges/user/tui.ts +300 -0
- package/src/bridges/user/web/actions.ts +173 -0
- package/src/bridges/user/web/contract.ts +222 -0
- package/src/bridges/user/web/dist/bundle.js +49 -0
- package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
- package/src/bridges/user/web/dist/index.html +21 -0
- package/src/bridges/user/web/dist/manifest.json +26 -0
- package/src/bridges/user/web/dist/mesh-worker.js +64 -0
- package/src/bridges/user/web/dist/sw.js +1 -0
- package/src/bridges/user/web/event-publisher.ts +114 -0
- package/src/bridges/user/web/port-discovery.ts +49 -0
- package/src/bridges/user/web/reads.ts +35 -0
- package/src/bridges/user/web/router.ts +158 -0
- package/src/bridges/user/web/server.ts +782 -0
- package/src/bridges/user/web/vite.config.ts +71 -0
- package/src/bridges/user/web/web-access.ts +404 -0
- package/src/bridges/user/web/web-assets.ts +90 -0
- package/src/bridges/user/web/web-beacon.ts +69 -0
- package/src/bridges/user/web/web-console-static.ts +90 -0
- package/src/cli-main.ts +635 -0
- package/src/cli.ts +13 -0
- package/src/core/account-bundle.ts +264 -0
- package/src/core/account-join.ts +289 -0
- package/src/core/account-ledger-crypto.ts +154 -0
- package/src/core/account-ledger-store.ts +66 -0
- package/src/core/account-ledger.ts +385 -0
- package/src/core/agent-details.ts +216 -0
- package/src/core/agent-list-format.ts +97 -0
- package/src/core/agent-registry.ts +282 -0
- package/src/core/agent-table.ts +58 -0
- package/src/core/atomic-file.ts +46 -0
- package/src/core/bind-retry.ts +38 -0
- package/src/core/bridge-mesh.ts +176 -0
- package/src/core/bridge.ts +718 -0
- package/src/core/capability-ask.ts +162 -0
- package/src/core/capability-bubble-up.ts +237 -0
- package/src/core/comms-store.ts +130 -0
- package/src/core/connection-approval.ts +134 -0
- package/src/core/connection-code-tool.ts +92 -0
- package/src/core/connection-code.ts +296 -0
- package/src/core/coordinator-role.ts +224 -0
- package/src/core/data-directory.ts +32 -0
- package/src/core/data-frame-routing.ts +309 -0
- package/src/core/default-command.ts +12 -0
- package/src/core/delegation-policy.ts +41 -0
- package/src/core/delivery-engine.ts +649 -0
- package/src/core/device-membership-verification.ts +125 -0
- package/src/core/device-membership.ts +130 -0
- package/src/core/directory-admission.ts +155 -0
- package/src/core/discovery-mdns.ts +247 -0
- package/src/core/discovery-tailscale.ts +224 -0
- package/src/core/discovery.ts +303 -0
- package/src/core/display-name.ts +73 -0
- package/src/core/dm-grant-actions.ts +72 -0
- package/src/core/dm-send-delegation.ts +73 -0
- package/src/core/dm-token-verification.ts +76 -0
- package/src/core/election-sessions.ts +135 -0
- package/src/core/error-reporting.ts +85 -0
- package/src/core/first-contact.ts +242 -0
- package/src/core/gateway-trust-actions.ts +184 -0
- package/src/core/gateway-trust.ts +305 -0
- package/src/core/gossip-directory.ts +276 -0
- package/src/core/gossip-extensions.ts +123 -0
- package/src/core/group-proofs.ts +205 -0
- package/src/core/handshake.ts +354 -0
- package/src/core/hub-forwarding.ts +24 -0
- package/src/core/hub-link.ts +176 -0
- package/src/core/hub-session.ts +374 -0
- package/src/core/identity-store.ts +715 -0
- package/src/core/identity.ts +489 -0
- package/src/core/index.ts +65 -0
- package/src/core/issuer-grouping.ts +165 -0
- package/src/core/issuer-identity-file.ts +187 -0
- package/src/core/lan-web-actions.ts +34 -0
- package/src/core/lan-web-uis.ts +55 -0
- package/src/core/ledger-lock.ts +81 -0
- package/src/core/listener-registry.ts +133 -0
- package/src/core/loopback-address.ts +22 -0
- package/src/core/machine-identity.ts +77 -0
- package/src/core/machine-list-groups.ts +53 -0
- package/src/core/mcp-registry-retry.ts +69 -0
- package/src/core/mcp-registry-version.ts +18 -0
- package/src/core/membership-proof.ts +139 -0
- package/src/core/membership-proofs.ts +149 -0
- package/src/core/mesh-graph-trace-tool.ts +81 -0
- package/src/core/mesh-graph.ts +89 -0
- package/src/core/mesh-network-actions.ts +183 -0
- package/src/core/mesh-store-options.ts +23 -0
- package/src/core/mesh-store-shared.ts +134 -0
- package/src/core/mesh-store.ts +1114 -0
- package/src/core/name-claim.ts +145 -0
- package/src/core/naming-actions.ts +90 -0
- package/src/core/naming.ts +138 -0
- package/src/core/nanoid.ts +20 -0
- package/src/core/network-interfaces.ts +24 -0
- package/src/core/package-version.ts +34 -0
- package/src/core/peer-lifecycle.ts +377 -0
- package/src/core/peer-link-error.ts +41 -0
- package/src/core/peer-versions.ts +98 -0
- package/src/core/pending-connection.ts +53 -0
- package/src/core/petnames.ts +121 -0
- package/src/core/pgp-keyserver.ts +32 -0
- package/src/core/pid-lock-file.ts +122 -0
- package/src/core/push-manager.ts +76 -0
- package/src/core/random-id.ts +9 -0
- package/src/core/receiver-set.ts +42 -0
- package/src/core/relay-role.ts +182 -0
- package/src/core/relay-selection.ts +120 -0
- package/src/core/relay-server.ts +198 -0
- package/src/core/relay-uplink.ts +78 -0
- package/src/core/request-timeouts.ts +37 -0
- package/src/core/room-discovery.ts +48 -0
- package/src/core/room-lifecycle.ts +838 -0
- package/src/core/room-lookup.ts +41 -0
- package/src/core/room-messaging.ts +247 -0
- package/src/core/room-notice-log.ts +61 -0
- package/src/core/room-path.ts +84 -0
- package/src/core/room-protocol.ts +893 -0
- package/src/core/room-router.ts +180 -0
- package/src/core/room-token-verification.ts +85 -0
- package/src/core/room-wire-extensions.ts +60 -0
- package/src/core/self-name-claim.ts +67 -0
- package/src/core/send-outcome.ts +69 -0
- package/src/core/shutdown-signals.ts +57 -0
- package/src/core/stale-agent-checker.ts +108 -0
- package/src/core/store.ts +704 -0
- package/src/core/token-text.ts +40 -0
- package/src/core/tool.ts +853 -0
- package/src/core/transport-events.ts +73 -0
- package/src/core/transport-queries.ts +54 -0
- package/src/core/transport.ts +367 -0
- package/src/core/types.ts +550 -0
- package/src/core/user-identity.ts +290 -0
- package/src/core/vapid.ts +81 -0
- package/src/core/version-check.ts +147 -0
- package/src/core/version-report-actions.ts +79 -0
- package/src/core/web-beacon.ts +31 -0
- package/src/core/web-push.ts +322 -0
- package/src/core/wire-mesh-core-version.ts +25 -0
- package/src/core/wire-mesh-identity.ts +36 -0
- package/src/core/wire-mesh-transport-options.ts +82 -0
- package/src/core/wire-mesh-transport.ts +1007 -0
- package/src/core/wire-protocol.ts +153 -0
- package/src/core/ws-dial.ts +191 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gossip-directory — the mesh-wide gossip-directory aggregation WireMeshTransport's own listKnownDevices reads from, plus per-event directory lookups (a specific peer's presence advert). Split out purely to keep wire-mesh-transport.ts under the repo's max-lines cap, the same reason connection-approval.ts, room-router.ts, hub-session.ts, and peer-lifecycle.ts were each split from their own owning file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TransportEvents } from "./transport.js";
|
|
6
|
+
import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
|
|
7
|
+
import type {
|
|
8
|
+
AcceptedMeshSession,
|
|
9
|
+
DirectoryEntry,
|
|
10
|
+
} from "wire-mesh-core/domain/mesh-session";
|
|
11
|
+
import type { PeerAdvert } from "wire-mesh-core/generated/protocol";
|
|
12
|
+
import { buildRelayOfferExtension } from "wire-mesh-core/domain/relay-advert";
|
|
13
|
+
import { AgentStatus } from "./types.js";
|
|
14
|
+
import { getOwnPackageVersion } from "./package-version.js";
|
|
15
|
+
import {
|
|
16
|
+
AGENT_SELF_GOSSIP_KEY,
|
|
17
|
+
HOSTED_ROOMS_GOSSIP_KEY,
|
|
18
|
+
PRESENCE_GOSSIP_KEY,
|
|
19
|
+
summariseAgentSelfAdvert,
|
|
20
|
+
summariseHostedRooms,
|
|
21
|
+
type AgentSelfAdvert,
|
|
22
|
+
type HostedRoomAdvert,
|
|
23
|
+
} from "./gossip-extensions.js";
|
|
24
|
+
import {
|
|
25
|
+
AGENT_COMMS_VERSION_GOSSIP_KEY,
|
|
26
|
+
type AgentCommsVersionsAdvert,
|
|
27
|
+
} from "./peer-versions.js";
|
|
28
|
+
import type { HubSession } from "./hub-session.js";
|
|
29
|
+
|
|
30
|
+
/** Merges one session event's own directory into the mesh-wide knownDevices view (mutated in place), keeping the newer advert (by snapshot-seconds) whenever a device-id is already known from an earlier event or a different session. An equal snapshot replaces the held advert, unlike the relay hub, which lets an advert take a device's registration over from a different connection only when it is strictly newer: the hub is guarding a route against a replayed advert, whereas every entry here has already been verified by the session that produced it, so an equal one carries the same content and the freshest copy is the one to keep. */
|
|
31
|
+
export function mergeKnownDevices(
|
|
32
|
+
knownDevices: Map<string, PeerAdvert>,
|
|
33
|
+
directory: readonly DirectoryEntry[],
|
|
34
|
+
): void {
|
|
35
|
+
for (const entry of directory) {
|
|
36
|
+
const deviceIdHex = deviceIdToHex(entry.device);
|
|
37
|
+
const existing = knownDevices.get(deviceIdHex);
|
|
38
|
+
if (
|
|
39
|
+
existing === undefined ||
|
|
40
|
+
entry.advert["snapshot-seconds"] >= existing["snapshot-seconds"]
|
|
41
|
+
) {
|
|
42
|
+
knownDevices.set(deviceIdHex, entry.advert);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The mesh-wide gossip directory a transport reads (agent-comms#48): the latest advert of every device it has heard of, from its local peer sessions and from the hub, kept across disconnects so a device's last-known advert stays queryable while its session is down.
|
|
49
|
+
*
|
|
50
|
+
* A device heard of through a local peer session is kept for good, since the local mesh admits its peers by its own approval rather than by gateway trust. A device heard of only through the hub is kept only while the hub would still admit it (admitted, the same decision directoryAdmission makes), and that is checked on every read (agent-comms#343): withdrawing trust from a device, principal or machine, in this process or in another that shares the trust file, removes from the view every device that trust alone let in, and so does a membership proof lapsing. Nothing re-adds such a device unless the hub admits it again.
|
|
51
|
+
*/
|
|
52
|
+
export class KnownDevices {
|
|
53
|
+
private readonly adverts = new Map<string, PeerAdvert>();
|
|
54
|
+
private readonly heardLocally = new Set<string>();
|
|
55
|
+
|
|
56
|
+
/** admitted answers whether the hub would still admit the device (hex) today. */
|
|
57
|
+
constructor(private readonly admitted: (deviceHex: string) => boolean) {}
|
|
58
|
+
|
|
59
|
+
/** Merges a local peer session's directory (see mergeKnownDevices). */
|
|
60
|
+
mergeLocal(directory: readonly DirectoryEntry[]): void {
|
|
61
|
+
mergeKnownDevices(this.adverts, directory);
|
|
62
|
+
for (const entry of directory) {
|
|
63
|
+
this.heardLocally.add(deviceIdToHex(entry.device));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Merges the hub session's admitted directory (see mergeKnownDevices). */
|
|
68
|
+
mergeHub(directory: readonly DirectoryEntry[]): void {
|
|
69
|
+
mergeKnownDevices(this.adverts, directory);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The current view, keyed by device-id hex, after dropping every hub-only device the hub would no longer admit. */
|
|
73
|
+
current(): ReadonlyMap<string, Readonly<PeerAdvert>> {
|
|
74
|
+
for (const deviceHex of [...this.adverts.keys()]) {
|
|
75
|
+
if (!this.heardLocally.has(deviceHex) && !this.admitted(deviceHex)) {
|
|
76
|
+
this.adverts.delete(deviceHex);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return this.adverts;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Merges a hub session's own admitted directory into knownDevices and announces each device in it as reachable. Every entry the hub hands over here has already passed admitEntries, so a device named in one is both present on the hub and trusted or vouched for by this side: exactly the moment a request queued for it becomes worth retrying, and the only such moment for a device that has no local peer session of its own to connect. */
|
|
84
|
+
export function mergeAndAnnounceReachable(
|
|
85
|
+
knownDevices: KnownDevices,
|
|
86
|
+
directory: readonly DirectoryEntry[],
|
|
87
|
+
events: Readonly<Pick<TransportEvents, "onDeviceReachable">>,
|
|
88
|
+
): void {
|
|
89
|
+
knownDevices.mergeHub(directory);
|
|
90
|
+
for (const entry of directory) {
|
|
91
|
+
events.onDeviceReachable(deviceIdToHex(entry.device));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Every device this side has ever heard gossip from, mesh-wide -- not just its own directly-connected peers -- with each one's own latest full advert (addresses, snapshot-seconds, and every open-extension field such as presence/status). The trivial array-conversion half of WireMeshTransport's own listKnownDevices, split out purely to keep that file under the repo's max-lines cap, the same reason mergeKnownDevices above already lives here rather than there. */
|
|
96
|
+
export function listKnownDevicesEntries(
|
|
97
|
+
knownDevices: ReadonlyMap<string, Readonly<PeerAdvert>>,
|
|
98
|
+
): readonly { deviceId: string; advert: Readonly<PeerAdvert> }[] {
|
|
99
|
+
return Array.from(knownDevices, ([deviceId, advert]) => ({
|
|
100
|
+
deviceId,
|
|
101
|
+
advert,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Reads a presence extension from one specific device's own gossiped self-advert, if this event's directory carries a fresh one for exactly that device-id -- never for any other device-id a multi-hop directory might mention, since only the session's own authenticated peer's advert is that session's business to report. Returns undefined for a missing presence/status key, or a value that isn't a recognised AgentStatus -- an advert simply not participating in this convention, not an error (the same verifier obligation peer-advert's own open extension tail is documented under). */
|
|
106
|
+
export function findPresenceAdvert(
|
|
107
|
+
deviceIdHex: string,
|
|
108
|
+
directory: readonly DirectoryEntry[],
|
|
109
|
+
): AgentStatus | undefined {
|
|
110
|
+
const entry = directory.find(
|
|
111
|
+
(candidate) => deviceIdToHex(candidate.device) === deviceIdHex,
|
|
112
|
+
);
|
|
113
|
+
if (entry === undefined) return undefined;
|
|
114
|
+
const status: unknown = entry.advert[PRESENCE_GOSSIP_KEY];
|
|
115
|
+
return AgentStatus.is(status) ? status : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Which agent/self field carries which issuer's proof: `membership` the user principal's, `machine` the machine's. */
|
|
119
|
+
export type MembershipProofField = "membership" | "machine";
|
|
120
|
+
|
|
121
|
+
/** The membership proof a peer gossiped in its agent/self advert under field, or undefined when it carries none. The advert is self-asserted by whichever peer sent it, so this only reads it; nothing about the proof is believed until it is verified. */
|
|
122
|
+
export function readMembershipProof(
|
|
123
|
+
advert: Readonly<Record<string, unknown>>,
|
|
124
|
+
field: MembershipProofField,
|
|
125
|
+
): string | undefined {
|
|
126
|
+
const self: unknown = advert[AGENT_SELF_GOSSIP_KEY];
|
|
127
|
+
if (typeof self !== "object" || self === null) return undefined;
|
|
128
|
+
if (field === "membership") {
|
|
129
|
+
return "membership" in self && typeof self.membership === "string"
|
|
130
|
+
? self.membership
|
|
131
|
+
: undefined;
|
|
132
|
+
}
|
|
133
|
+
return "machine" in self && typeof self.machine === "string"
|
|
134
|
+
? self.machine
|
|
135
|
+
: undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Which agent/self field carries which issuer's name claim: `machineName` the machine's, `principalName` the user principal's. */
|
|
139
|
+
export type NameClaimField = "machineName" | "principalName";
|
|
140
|
+
|
|
141
|
+
/** The name claim a peer gossiped in its agent/self advert under field (name-claim.ts), or undefined when it carries none. Only read here; nothing it says is believed until it is verified against the issuer its proof names. */
|
|
142
|
+
export function readNameClaim(
|
|
143
|
+
advert: Readonly<Record<string, unknown>>,
|
|
144
|
+
field: NameClaimField,
|
|
145
|
+
): string | undefined {
|
|
146
|
+
const self: unknown = advert[AGENT_SELF_GOSSIP_KEY];
|
|
147
|
+
if (typeof self !== "object" || self === null) return undefined;
|
|
148
|
+
const claim: unknown = field in self ? Reflect.get(self, field) : undefined;
|
|
149
|
+
return typeof claim === "string" ? claim : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The one thing gossip re-advertisement needs from a session: somewhere to send the extension bag. */
|
|
153
|
+
export type GossipSession = Pick<AcceptedMeshSession, "sendGossipUpdate">;
|
|
154
|
+
|
|
155
|
+
/** Which session, if any, is the hub's. */
|
|
156
|
+
export interface HubOwnership {
|
|
157
|
+
ownsSession: (session: Readonly<GossipSession>) => boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Re-sends this side's own current presence status, currently-hosted rooms, self-agent identity, and package versions, together, onto every live session's gossip self-advert -- one gossip frame per tick carrying whichever facts are actually known, rather than a separate frame per fact. Split out of wire-mesh-transport.ts's own WireMeshTransport class purely to keep that file under the repo's max-lines cap, the same reason mergeKnownDevices/findPresenceAdvert above already live here rather than there. A session that fails to send (mid-disconnect, most likely -- watchForDisconnect will independently notice and clean it up) is reported via onError and skipped, not allowed to stop the tick from reaching the rest of allSessions: a periodic broadcast to N peers is N independent operations, not one atomic unit. Unlike presence/hostedRooms/selfAgentAdvert, the versions extension is never actually absent (this side's own agent-comms package version is always known), so every tick this function is called for sends at least that one fact -- there is no "nothing to report" case left to short-circuit on. The hub's own session (agent-comms#156) is gated separately from every ordinary local-peer session in allSessions: local mesh trust is a different layer (connect_request/introduce approval already gated it before it ever joined allSessions), but the hub session is a broadcast to every connected hub peer, trusted or not, and would otherwise leak this side's own presence/hosted-rooms/self-agent/versions advert onto the hub regardless of GatewayTrust -- so this side's own advert waits on the same hasAny gate. It also waits for a visible agent (selfAgentAdvert is only ever defined for one): presence, hosted rooms and versions describe the agent and its process, so a hidden or unregistered store puts none of them on the hub, and a hidden agent is left with only the device advert every session opens with, which is what lets it be reached by device id. What the hub session does carry is reduced to what is safe to hand to strangers (presence, the agent's name, harness and membership proof, the public rooms it hosts by path and name, and the package versions): the hub relays it to every client that connects, so the working directory, process id, tags and rooms go only to peers this side is connected to directly (agent-comms#322). */
|
|
161
|
+
export function readvertiseGossip(options: {
|
|
162
|
+
allSessions: ReadonlySet<GossipSession>;
|
|
163
|
+
hub: Readonly<HubOwnership>;
|
|
164
|
+
hasAnyTrustedGateway: () => boolean;
|
|
165
|
+
onError: ((error: Error) => void) | undefined;
|
|
166
|
+
getCurrentPresence: (() => AgentStatus | undefined) | undefined;
|
|
167
|
+
getHostedRooms: (() => readonly HostedRoomAdvert[]) | undefined;
|
|
168
|
+
getSelfAgentAdvert: (() => AgentSelfAdvert | undefined) | undefined;
|
|
169
|
+
/** Reads this side's own currently-running cc-peer version, when this process is fronting/bridging one -- folded into the same AGENT_COMMS_VERSION_GOSSIP_KEY advert as this side's own always-known agent-comms package version (agent-comms#198). Unlike presence/hostedRooms/selfAgentAdvert, agent-comms' own version is never genuinely absent (getOwnPackageVersion() always answers), so the versions extension is built and included unconditionally whenever this function runs at all -- there is no "no version to report" case the way there is for the other three optional facts. */
|
|
170
|
+
getCcPeerVersion: (() => string | undefined) | undefined;
|
|
171
|
+
/** The addresses this side serves a relay at (agent-comms#342), undefined while it serves none. */
|
|
172
|
+
getRelayOffer: () => readonly string[] | undefined;
|
|
173
|
+
}): void {
|
|
174
|
+
const {
|
|
175
|
+
allSessions,
|
|
176
|
+
hub,
|
|
177
|
+
hasAnyTrustedGateway,
|
|
178
|
+
onError,
|
|
179
|
+
getCurrentPresence,
|
|
180
|
+
getHostedRooms,
|
|
181
|
+
getSelfAgentAdvert,
|
|
182
|
+
getCcPeerVersion,
|
|
183
|
+
getRelayOffer,
|
|
184
|
+
} = options;
|
|
185
|
+
const status = getCurrentPresence?.();
|
|
186
|
+
const selfAgentAdvert = getSelfAgentAdvert?.();
|
|
187
|
+
const ccPeerVersion = getCcPeerVersion?.();
|
|
188
|
+
const versionsAdvert: AgentCommsVersionsAdvert = {
|
|
189
|
+
agentComms: getOwnPackageVersion(),
|
|
190
|
+
...(ccPeerVersion !== undefined ? { ccPeer: ccPeerVersion } : {}),
|
|
191
|
+
};
|
|
192
|
+
// A hub relays whatever it is given to every client that connects, so a hub session carries only what is safe for strangers: presence, the agent's summary, the public rooms it hosts (without their descriptions) and the package versions. Directly connected peers are ones this side deals with itself, and get everything.
|
|
193
|
+
const hubExtensions: Record<string, unknown> = {};
|
|
194
|
+
if (status !== undefined) hubExtensions[PRESENCE_GOSSIP_KEY] = status;
|
|
195
|
+
if (selfAgentAdvert !== undefined)
|
|
196
|
+
hubExtensions[AGENT_SELF_GOSSIP_KEY] =
|
|
197
|
+
summariseAgentSelfAdvert(selfAgentAdvert);
|
|
198
|
+
hubExtensions[AGENT_COMMS_VERSION_GOSSIP_KEY] = versionsAdvert;
|
|
199
|
+
const hostedRooms = getHostedRooms?.();
|
|
200
|
+
if (hostedRooms !== undefined)
|
|
201
|
+
hubExtensions[HOSTED_ROOMS_GOSSIP_KEY] = summariseHostedRooms(hostedRooms);
|
|
202
|
+
// Offered to hub clients as well as peers, since a trusted device on another machine learns the offer through the hub; that exposes the relay's addresses to every hub client, part of the metadata a relay's operator accepts in serving one.
|
|
203
|
+
const relayOffer = getRelayOffer();
|
|
204
|
+
if (relayOffer !== undefined) {
|
|
205
|
+
for (const [key, value] of Object.entries(
|
|
206
|
+
buildRelayOfferExtension(relayOffer),
|
|
207
|
+
)) {
|
|
208
|
+
hubExtensions[key] = value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const peerExtensions: Record<string, unknown> = { ...hubExtensions };
|
|
212
|
+
if (hostedRooms !== undefined)
|
|
213
|
+
peerExtensions[HOSTED_ROOMS_GOSSIP_KEY] = hostedRooms;
|
|
214
|
+
if (selfAgentAdvert !== undefined)
|
|
215
|
+
peerExtensions[AGENT_SELF_GOSSIP_KEY] = selfAgentAdvert;
|
|
216
|
+
for (const session of allSessions) {
|
|
217
|
+
const viaHub = hub.ownsSession(session);
|
|
218
|
+
if (viaHub && (!hasAnyTrustedGateway() || selfAgentAdvert === undefined)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
session
|
|
222
|
+
.sendGossipUpdate(viaHub ? hubExtensions : peerExtensions)
|
|
223
|
+
.catch((error: unknown) => {
|
|
224
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Arms readvertiseGossip's own periodic tick, or does nothing at all when neither presence, hosted-rooms, nor self-agent-identity has a source -- WireMeshTransport's own constructor logic, split out here purely to keep that file under the repo's max-lines cap, the same reason mergeKnownDevices/findPresenceAdvert/readvertiseGossip above already live here rather than there. getCcPeerVersion never gates whether the interval starts at all (unlike the other three): it is only ever wired alongside getSelfAgentAdvert in real use (bridge-mesh.ts always supplies that one), so a construction site relying on getCcPeerVersion alone to start ticking isn't a real scenario worth its own branch. Unref'd immediately, matching every other timer this transport owns, so it never keeps the process alive on its own. */
|
|
230
|
+
export function startGossipInterval(options: {
|
|
231
|
+
allSessions: ReadonlySet<GossipSession>;
|
|
232
|
+
hub: Readonly<Pick<HubSession, "ownsSession">>;
|
|
233
|
+
hasAnyTrustedGateway: () => boolean;
|
|
234
|
+
onError: ((error: Error) => void) | undefined;
|
|
235
|
+
getCurrentPresence: (() => AgentStatus | undefined) | undefined;
|
|
236
|
+
getHostedRooms: (() => readonly HostedRoomAdvert[]) | undefined;
|
|
237
|
+
getSelfAgentAdvert: (() => AgentSelfAdvert | undefined) | undefined;
|
|
238
|
+
getCcPeerVersion: (() => string | undefined) | undefined;
|
|
239
|
+
getRelayOffer: () => readonly string[] | undefined;
|
|
240
|
+
intervalMs: number;
|
|
241
|
+
}): ReturnType<typeof setInterval> | undefined {
|
|
242
|
+
const {
|
|
243
|
+
allSessions,
|
|
244
|
+
hub,
|
|
245
|
+
hasAnyTrustedGateway,
|
|
246
|
+
onError,
|
|
247
|
+
getCurrentPresence,
|
|
248
|
+
getHostedRooms,
|
|
249
|
+
getSelfAgentAdvert,
|
|
250
|
+
getCcPeerVersion,
|
|
251
|
+
getRelayOffer,
|
|
252
|
+
intervalMs,
|
|
253
|
+
} = options;
|
|
254
|
+
if (
|
|
255
|
+
getCurrentPresence === undefined &&
|
|
256
|
+
getHostedRooms === undefined &&
|
|
257
|
+
getSelfAgentAdvert === undefined
|
|
258
|
+
) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const interval = setInterval(() => {
|
|
262
|
+
readvertiseGossip({
|
|
263
|
+
allSessions,
|
|
264
|
+
hub,
|
|
265
|
+
hasAnyTrustedGateway,
|
|
266
|
+
onError,
|
|
267
|
+
getCurrentPresence,
|
|
268
|
+
getHostedRooms,
|
|
269
|
+
getSelfAgentAdvert,
|
|
270
|
+
getCcPeerVersion,
|
|
271
|
+
getRelayOffer,
|
|
272
|
+
});
|
|
273
|
+
}, intervalMs);
|
|
274
|
+
interval.unref();
|
|
275
|
+
return interval;
|
|
276
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The domain-qualified gossip extension keys this package reads and writes on a peer's own gossiped advert, and the gossip-safe shapes two of them carry -- split out of wire-mesh-transport.ts (which writes them) so gossip-directory.ts, agent-registry.ts, room-lifecycle.ts, mesh-store.ts and hub-forwarding.ts (which read or merge them) no longer import their own vocabulary from the transport that depends on them, and to keep that file under the repo's max-lines cap. Also the shared home for the re-advertise cadence itself and the staleness window agent-registry.ts derives from it (agent-comms#301): both sides of "how fresh must a gossiped fact be to trust it" belong together, not split across the file that writes the cadence and the file that judges freshness against it.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Room } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const MS_PER_SECOND = 1000;
|
|
8
|
+
|
|
9
|
+
/** How often a live device re-sends its own self-advert (WireMeshTransport's own presence-readvertisement timer) -- the producer half of agent-comms#301's own liveness signal. Chosen generously enough to avoid chattiness on an idle mesh while still keeping a remote peer's own picture of this device fresh well within the tens-of-minutes staleness window a status change is actually meaningful over. */
|
|
10
|
+
const PRESENCE_READVERTISE_INTERVAL_SECONDS = 20;
|
|
11
|
+
export const PRESENCE_READVERTISE_INTERVAL_MS =
|
|
12
|
+
PRESENCE_READVERTISE_INTERVAL_SECONDS * MS_PER_SECOND;
|
|
13
|
+
|
|
14
|
+
/** Missed re-advertise ticks tolerated before a gossip-discovered device (one this side has never held a live session with, only ever heard about through the hub's directory) is presumed offline rather than trusted at whatever presence value its own last-heard advert happened to carry. 3 is the conventional "missed heartbeats" threshold distributed liveness checks converge on (enough to absorb one dropped gossip tick and one slow one without a false positive, not so many that a genuinely departed device is still reported reachable for minutes). There is no push notice of a device's own departure to react to instead (wire-mesh#230 covers the relay-pairing half of that; the hub's directory carries no equivalent for a lost connection) -- see agent-registry.ts's own listDiscoverableAgents for where this is applied. */
|
|
15
|
+
const PRESENCE_STALE_MISSED_TICKS = 3;
|
|
16
|
+
/** Default staleness window agent-comms#301's own check uses: agent-registry.ts's caller may override it (MeshStoreOptions.presenceStaleAfterMs), but every real construction site gets this unless it deliberately asks for something else. */
|
|
17
|
+
export const DEFAULT_PRESENCE_STALE_AFTER_MS =
|
|
18
|
+
PRESENCE_READVERTISE_INTERVAL_MS * PRESENCE_STALE_MISSED_TICKS;
|
|
19
|
+
|
|
20
|
+
/** The domain-qualified gossip extension key this transport reads/writes presence under, per wire-mesh's own gossip-extension-namespacing convention (spec/CONVENTIONS.md): `<domain>/<field>`, never a bare name a second application's own extension could collide with. */
|
|
21
|
+
export const PRESENCE_GOSSIP_KEY = "presence/status";
|
|
22
|
+
|
|
23
|
+
/** The domain-qualified gossip extension key this transport writes this side's own currently-hosted public/private rooms under -- the write half of P3.8's room-discovery replacement for createRoom's own broadcastPatch (agent-comms#48). Same namespacing convention as PRESENCE_GOSSIP_KEY. */
|
|
24
|
+
export const HOSTED_ROOMS_GOSSIP_KEY = "room/hosted";
|
|
25
|
+
|
|
26
|
+
/** The lightweight, gossip-safe shape a room advertises itself under: enough for a peer to display "this device hosts a discoverable room here" without exposing anything membership- or grant-related. Deliberately excludes secret rooms (never worth advertising at all) and every CRDT membership field a real Room carries -- a gossip-discovered entry is a hint pointing at a room to join, not a substitute for the real Room object join/admission still produces. */
|
|
27
|
+
export interface HostedRoomAdvert {
|
|
28
|
+
path: string;
|
|
29
|
+
name: string;
|
|
30
|
+
type: "public" | "private";
|
|
31
|
+
description: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The rooms ownerId itself owns that are worth advertising, in the shape HostedRoomAdvert gives them: only its own public and private rooms. */
|
|
35
|
+
export function hostedRoomAdverts(
|
|
36
|
+
rooms: ReadonlyMap<string, Room>,
|
|
37
|
+
ownerId: string,
|
|
38
|
+
): HostedRoomAdvert[] {
|
|
39
|
+
const result: HostedRoomAdvert[] = [];
|
|
40
|
+
for (const room of rooms.values()) {
|
|
41
|
+
if (room.owner !== ownerId) continue;
|
|
42
|
+
if (room.type !== "public" && room.type !== "private") continue;
|
|
43
|
+
result.push({
|
|
44
|
+
path: room.id,
|
|
45
|
+
name: room.name,
|
|
46
|
+
type: room.type,
|
|
47
|
+
description: room.description,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What a hub is told about a room: where it is and what it is called, for a public room only. The description is left out because a project room's default one names the directory it was made for. */
|
|
54
|
+
export type HostedRoomSummary = Pick<
|
|
55
|
+
HostedRoomAdvert,
|
|
56
|
+
"path" | "name" | "type"
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
/** A hosted-room advert as read off the wire: the summary always, the description only when the sender shared it. */
|
|
60
|
+
export type ReceivedHostedRoomAdvert = HostedRoomSummary &
|
|
61
|
+
Partial<Pick<HostedRoomAdvert, "description">>;
|
|
62
|
+
|
|
63
|
+
/** The rooms that may be advertised through a hub: public ones, reduced to their summary. */
|
|
64
|
+
export function summariseHostedRooms(
|
|
65
|
+
rooms: readonly HostedRoomAdvert[],
|
|
66
|
+
): HostedRoomSummary[] {
|
|
67
|
+
return rooms
|
|
68
|
+
.filter((room) => room.type === "public")
|
|
69
|
+
.map(({ path, name, type }) => ({ path, name, type }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The domain-qualified gossip extension key this transport writes this side's own agent identity facts under -- the write half of P3.8's eventual agent register/update/offline retirement (agent-comms#48). Same namespacing convention as PRESENCE_GOSSIP_KEY/HOSTED_ROOMS_GOSSIP_KEY. */
|
|
73
|
+
export const AGENT_SELF_GOSSIP_KEY = "agent/self";
|
|
74
|
+
|
|
75
|
+
/** What an agent advertises about itself to any peer, a hub included: enough for a peer with no prior record of this device to list it, and nothing about where or how it runs. */
|
|
76
|
+
export interface AgentSelfSummary {
|
|
77
|
+
name: string;
|
|
78
|
+
harness: string;
|
|
79
|
+
/** A proof that this device's user principal vouches for it (membership-proof.ts), so a peer that trusts that principal trusts this device without it being listed individually. Absent until the first proof has been minted. */
|
|
80
|
+
membership?: string;
|
|
81
|
+
/** A proof that this device's machine vouches for it (membership-proof.ts, agent-comms#343), so a peer can tell which devices run on the same host and one that trusts the machine trusts this device. Absent until the first proof has been minted. Like membership, it names its issuer in the clear, so it is only ever sent in this advert, which a hidden or ghost agent never sends. */
|
|
82
|
+
machine?: string;
|
|
83
|
+
/** This device's machine's self display name claim (name-claim.ts, agent-comms#345), signed by the machine key, so a peer can show the name the machine chose for itself. Absent while the machine has no name. Travels with the machine proof, under the same visibility rule. */
|
|
84
|
+
machineName?: string;
|
|
85
|
+
/** This device's user principal's self display name claim (name-claim.ts, agent-comms#359), signed by the user key, so a peer can show the name the account chose for itself. Absent while the account has no name. Travels with the membership proof, under the same visibility rule. */
|
|
86
|
+
principalName?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What a bridge knows about itself beyond its summary, and shares only with peers it deals with directly: a hub relays what it is given to every client that connects, so none of this goes to one. */
|
|
90
|
+
export interface AgentSelfDetails {
|
|
91
|
+
cwd: string;
|
|
92
|
+
pid: number;
|
|
93
|
+
startedAt: string;
|
|
94
|
+
tags: string[];
|
|
95
|
+
subscribedRooms: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The lightweight, gossip-safe shape an agent advertises itself under: enough for a peer with no prior local record of this device to construct a real AgentIdentity-shaped discovery entry. Deliberately excludes status (already carried separately under presence/status, no need to duplicate it here) and visibility (this field is only ever populated for a "visible" agent in the first place -- see MeshStore's own selfAgentAdvert getter -- so a discovered entry's visibility is always exactly "visible" by construction, never something this advert needs to assert itself). */
|
|
99
|
+
export interface AgentSelfAdvert extends AgentSelfSummary, AgentSelfDetails {}
|
|
100
|
+
|
|
101
|
+
/** An agent advert as read off the wire: the summary always, the details only when the sender chose to share them. */
|
|
102
|
+
export type ReceivedAgentSelfAdvert = AgentSelfSummary &
|
|
103
|
+
Partial<AgentSelfDetails>;
|
|
104
|
+
|
|
105
|
+
/** The part of an advert that is safe to publish through a hub. */
|
|
106
|
+
export function summariseAgentSelfAdvert(
|
|
107
|
+
advert: Readonly<AgentSelfAdvert>,
|
|
108
|
+
): AgentSelfSummary {
|
|
109
|
+
return {
|
|
110
|
+
name: advert.name,
|
|
111
|
+
harness: advert.harness,
|
|
112
|
+
...(advert.membership !== undefined
|
|
113
|
+
? { membership: advert.membership }
|
|
114
|
+
: {}),
|
|
115
|
+
...(advert.machine !== undefined ? { machine: advert.machine } : {}),
|
|
116
|
+
...(advert.machineName !== undefined
|
|
117
|
+
? { machineName: advert.machineName }
|
|
118
|
+
: {}),
|
|
119
|
+
...(advert.principalName !== undefined
|
|
120
|
+
? { principalName: advert.principalName }
|
|
121
|
+
: {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This device's membership proofs under both of its grouping issuers, its user principal (agent-comms#266) and its machine (agent-comms#343), and the self display name claims of both (agent-comms#345 for the machine, agent-comms#359 for the principal), with the checks for other devices' proofs and claims and the machine grouping and names list_agents shows. Split out of MeshStore, which hands it its identity, peer id, transport and error sink.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
|
|
6
|
+
import type { MeshStoreIdentity } from "./mesh-store-shared.js";
|
|
7
|
+
import type { MeshTransport } from "./transport.js";
|
|
8
|
+
import { MembershipProofs } from "./membership-proofs.js";
|
|
9
|
+
import { IssuerGrouping } from "./issuer-grouping.js";
|
|
10
|
+
import { SelfNameClaim } from "./self-name-claim.js";
|
|
11
|
+
import {
|
|
12
|
+
loadMachineDisplayName,
|
|
13
|
+
saveMachineDisplayName,
|
|
14
|
+
} from "./machine-identity.js";
|
|
15
|
+
import { readNameClaim } from "./gossip-directory.js";
|
|
16
|
+
import { verifyNameClaim } from "./name-claim.js";
|
|
17
|
+
import { loadUserDisplayName, saveUserDisplayName } from "./user-identity.js";
|
|
18
|
+
import type { AgentSelfSummary } from "./gossip-extensions.js";
|
|
19
|
+
import type {
|
|
20
|
+
IdentifiedMembershipProof,
|
|
21
|
+
MembershipProofVerdict,
|
|
22
|
+
} from "./membership-proof.js";
|
|
23
|
+
|
|
24
|
+
export interface GroupProofsDeps {
|
|
25
|
+
/** Throws if the store has no identity yet. */
|
|
26
|
+
getIdentity: () => MeshStoreIdentity;
|
|
27
|
+
/** This store's identity, or undefined before it is attached. */
|
|
28
|
+
peekIdentity: () => MeshStoreIdentity | undefined;
|
|
29
|
+
/** This device's own id (hex). */
|
|
30
|
+
getPeerId: () => string;
|
|
31
|
+
/** Throws if the store has no transport yet. */
|
|
32
|
+
requireTransport: () => MeshTransport;
|
|
33
|
+
onError: (error: Error) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class GroupProofs {
|
|
37
|
+
private readonly principal: MembershipProofs;
|
|
38
|
+
private readonly machine: MembershipProofs;
|
|
39
|
+
private readonly machineGrouping: IssuerGrouping;
|
|
40
|
+
private readonly principalGrouping: IssuerGrouping;
|
|
41
|
+
private readonly machineName: SelfNameClaim;
|
|
42
|
+
private readonly principalName: SelfNameClaim;
|
|
43
|
+
private readonly getIdentity: () => MeshStoreIdentity;
|
|
44
|
+
|
|
45
|
+
constructor(deps: Readonly<GroupProofsDeps>) {
|
|
46
|
+
this.getIdentity = deps.getIdentity;
|
|
47
|
+
const shared = {
|
|
48
|
+
getIdentity: deps.getIdentity,
|
|
49
|
+
getPeerId: deps.getPeerId,
|
|
50
|
+
onError: deps.onError,
|
|
51
|
+
};
|
|
52
|
+
this.principal = new MembershipProofs({
|
|
53
|
+
...shared,
|
|
54
|
+
issuerOf: (identity) => identity.userIdentity,
|
|
55
|
+
});
|
|
56
|
+
this.machine = new MembershipProofs({
|
|
57
|
+
...shared,
|
|
58
|
+
issuerOf: (identity) => identity.machineIdentity,
|
|
59
|
+
});
|
|
60
|
+
const grouping = {
|
|
61
|
+
listKnownDevices: () =>
|
|
62
|
+
deps.requireTransport().listKnownDevices?.() ?? [],
|
|
63
|
+
identify: async (claim: Readonly<{ proof: string; deviceHex: string }>) =>
|
|
64
|
+
this.identify(claim),
|
|
65
|
+
getPeerId: deps.getPeerId,
|
|
66
|
+
onError: deps.onError,
|
|
67
|
+
verifyName: async (
|
|
68
|
+
claim: Readonly<{ claim: string; subject: string }>,
|
|
69
|
+
) => {
|
|
70
|
+
const { identity, clock } = deps.getIdentity();
|
|
71
|
+
return verifyNameClaim({
|
|
72
|
+
claim: claim.claim,
|
|
73
|
+
subject: claim.subject,
|
|
74
|
+
identity,
|
|
75
|
+
clock,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
this.machineGrouping = new IssuerGrouping({
|
|
80
|
+
...grouping,
|
|
81
|
+
field: "machine",
|
|
82
|
+
readNameClaim: (advert) => readNameClaim(advert, "machineName"),
|
|
83
|
+
getOwnIssuerId: () => {
|
|
84
|
+
const identity = deps.peekIdentity();
|
|
85
|
+
return identity === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: deviceIdToHex(identity.machineIdentity.deviceId);
|
|
88
|
+
},
|
|
89
|
+
getOwnName: () => {
|
|
90
|
+
const identity = deps.peekIdentity();
|
|
91
|
+
return identity === undefined
|
|
92
|
+
? undefined
|
|
93
|
+
: loadMachineDisplayName(identity.machineIdentityOptions);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
this.principalGrouping = new IssuerGrouping({
|
|
97
|
+
...grouping,
|
|
98
|
+
field: "membership",
|
|
99
|
+
readNameClaim: (advert) => readNameClaim(advert, "principalName"),
|
|
100
|
+
getOwnIssuerId: () => {
|
|
101
|
+
const identity = deps.peekIdentity();
|
|
102
|
+
return identity === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: deviceIdToHex(identity.userIdentity.deviceId);
|
|
105
|
+
},
|
|
106
|
+
getOwnName: () => {
|
|
107
|
+
const identity = deps.peekIdentity();
|
|
108
|
+
return identity === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: loadUserDisplayName(identity.userIdentityOptions);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
this.machineName = new SelfNameClaim({
|
|
114
|
+
getIdentity: deps.getIdentity,
|
|
115
|
+
issuerOf: (identity) => identity.machineIdentity,
|
|
116
|
+
loadName: (identity) =>
|
|
117
|
+
loadMachineDisplayName(identity.machineIdentityOptions),
|
|
118
|
+
onError: deps.onError,
|
|
119
|
+
});
|
|
120
|
+
this.principalName = new SelfNameClaim({
|
|
121
|
+
getIdentity: deps.getIdentity,
|
|
122
|
+
issuerOf: (identity) => identity.userIdentity,
|
|
123
|
+
loadName: (identity) => loadUserDisplayName(identity.userIdentityOptions),
|
|
124
|
+
onError: deps.onError,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Whether a gossiped proof shows that deviceHex belongs to the account this store holds, judged against the user principal's own proofs (MembershipProofs.isAccountMember). */
|
|
129
|
+
async isAccountMember(
|
|
130
|
+
claim: Readonly<{ proof: string; deviceHex: string }>,
|
|
131
|
+
): Promise<boolean> {
|
|
132
|
+
return this.principal.isAccountMember(claim);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The agent/self advert fields carrying this device's current proofs, each absent until it has first been minted. */
|
|
136
|
+
advertFields(): Pick<
|
|
137
|
+
AgentSelfSummary,
|
|
138
|
+
"membership" | "machine" | "machineName" | "principalName"
|
|
139
|
+
> {
|
|
140
|
+
const membership = this.principal.current();
|
|
141
|
+
const machine = this.machine.current();
|
|
142
|
+
const machineName = this.machineName.current();
|
|
143
|
+
const principalName = this.principalName.current();
|
|
144
|
+
return {
|
|
145
|
+
...(membership !== undefined ? { membership } : {}),
|
|
146
|
+
...(machine !== undefined ? { machine } : {}),
|
|
147
|
+
...(machineName !== undefined ? { machineName } : {}),
|
|
148
|
+
...(principalName !== undefined ? { principalName } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Starts keeping both proofs and both name claims fresh; called once the store's identity is attached. */
|
|
153
|
+
start(): void {
|
|
154
|
+
this.principal.start();
|
|
155
|
+
this.machine.start();
|
|
156
|
+
this.machineName.start();
|
|
157
|
+
this.principalName.start();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
stop(): void {
|
|
161
|
+
this.principal.stop();
|
|
162
|
+
this.machine.stop();
|
|
163
|
+
this.machineName.stop();
|
|
164
|
+
this.principalName.stop();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Sets (or, given undefined, clears) this machine's self display name and re-mints its claim now rather than on the next refresh. Throws if the store has no identity yet. */
|
|
168
|
+
async saveMachineName(name: string | undefined): Promise<void> {
|
|
169
|
+
saveMachineDisplayName(this.getIdentity().machineIdentityOptions, name);
|
|
170
|
+
return this.machineName.refresh();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Sets (or, given undefined, clears) this account's self display name and re-mints its claim now rather than on the next refresh. Throws if the store has no identity yet. */
|
|
174
|
+
async saveUserName(name: string | undefined): Promise<void> {
|
|
175
|
+
saveUserDisplayName(this.getIdentity().userIdentityOptions, name);
|
|
176
|
+
return this.principalName.refresh();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Whether a gossiped proof shows that issuerHex, a principal or a machine, vouches for deviceHex. */
|
|
180
|
+
async verify(
|
|
181
|
+
claim: Readonly<{ proof: string; deviceHex: string; issuerHex: string }>,
|
|
182
|
+
): Promise<MembershipProofVerdict> {
|
|
183
|
+
return this.principal.verify(claim);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Which issuer a gossiped proof shows vouching for deviceHex. Verification does not depend on which issuer an instance mints under, so the principal's instance serves for both. */
|
|
187
|
+
async identify(
|
|
188
|
+
claim: Readonly<{ proof: string; deviceHex: string }>,
|
|
189
|
+
): Promise<IdentifiedMembershipProof> {
|
|
190
|
+
return this.principal.identify(claim);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The machine (hex) of each device this side can place on one, keyed by device id. */
|
|
194
|
+
async machinesByDevice(): Promise<Map<string, string>> {
|
|
195
|
+
return this.machineGrouping.issuersByDevice();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The self display name of each grouping issuer this side can place a device under, machines and user principals together, keyed by issuer id. The two kinds of id never collide, since each is the hash of a different key. */
|
|
199
|
+
async issuerNames(): Promise<Map<string, string>> {
|
|
200
|
+
return new Map([
|
|
201
|
+
...(await this.machineGrouping.issuerNames()),
|
|
202
|
+
...(await this.principalGrouping.issuerNames()),
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
}
|