agent-comms 8.4.0 → 8.5.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +13 -3
- package/dist/bridges/user/web/dist/mesh-worker.js +1 -1
- package/dist/bridges/user/web/dist/sw.js +1 -1
- package/dist/core/agent-registry.d.ts.map +1 -1
- package/dist/core/agent-registry.js +2 -0
- package/dist/core/agent-registry.js.map +1 -1
- package/dist/core/atomic-file.d.ts +12 -0
- package/dist/core/atomic-file.d.ts.map +1 -0
- package/dist/core/atomic-file.js +37 -0
- package/dist/core/atomic-file.js.map +1 -0
- package/dist/core/bridge-mesh.d.ts.map +1 -1
- package/dist/core/bridge-mesh.js +4 -0
- package/dist/core/bridge-mesh.js.map +1 -1
- package/dist/core/bridge.d.ts +1 -0
- package/dist/core/bridge.d.ts.map +1 -1
- package/dist/core/bridge.js +4 -0
- package/dist/core/bridge.js.map +1 -1
- package/dist/core/device-membership-verification.d.ts +23 -9
- package/dist/core/device-membership-verification.d.ts.map +1 -1
- package/dist/core/device-membership-verification.js +38 -11
- package/dist/core/device-membership-verification.js.map +1 -1
- package/dist/core/device-membership.d.ts +12 -3
- package/dist/core/device-membership.d.ts.map +1 -1
- package/dist/core/device-membership.js +10 -7
- package/dist/core/device-membership.js.map +1 -1
- package/dist/core/directory-admission.d.ts +5 -3
- package/dist/core/directory-admission.d.ts.map +1 -1
- package/dist/core/directory-admission.js +55 -32
- package/dist/core/directory-admission.js.map +1 -1
- package/dist/core/gateway-trust-actions.d.ts +5 -8
- package/dist/core/gateway-trust-actions.d.ts.map +1 -1
- package/dist/core/gateway-trust-actions.js +79 -15
- package/dist/core/gateway-trust-actions.js.map +1 -1
- package/dist/core/gateway-trust.d.ts +38 -17
- package/dist/core/gateway-trust.d.ts.map +1 -1
- package/dist/core/gateway-trust.js +86 -37
- package/dist/core/gateway-trust.js.map +1 -1
- package/dist/core/gossip-directory.d.ts +23 -3
- package/dist/core/gossip-directory.d.ts.map +1 -1
- package/dist/core/gossip-directory.js +45 -6
- package/dist/core/gossip-directory.js.map +1 -1
- package/dist/core/gossip-extensions.d.ts +2 -0
- package/dist/core/gossip-extensions.d.ts.map +1 -1
- package/dist/core/gossip-extensions.js +1 -0
- package/dist/core/gossip-extensions.js.map +1 -1
- package/dist/core/group-proofs.d.ts +43 -0
- package/dist/core/group-proofs.d.ts.map +1 -0
- package/dist/core/group-proofs.js +69 -0
- package/dist/core/group-proofs.js.map +1 -0
- package/dist/core/identity-store.d.ts +5 -4
- package/dist/core/identity-store.d.ts.map +1 -1
- package/dist/core/identity-store.js +13 -16
- package/dist/core/identity-store.js.map +1 -1
- package/dist/core/issuer-identity-file.d.ts +25 -0
- package/dist/core/issuer-identity-file.d.ts.map +1 -0
- package/dist/core/issuer-identity-file.js +124 -0
- package/dist/core/issuer-identity-file.js.map +1 -0
- package/dist/core/machine-grouping.d.ts +34 -0
- package/dist/core/machine-grouping.d.ts.map +1 -0
- package/dist/core/machine-grouping.js +66 -0
- package/dist/core/machine-grouping.js.map +1 -0
- package/dist/core/machine-identity.d.ts +15 -0
- package/dist/core/machine-identity.d.ts.map +1 -0
- package/dist/core/machine-identity.js +18 -0
- package/dist/core/machine-identity.js.map +1 -0
- package/dist/core/machine-list-groups.d.ts +12 -0
- package/dist/core/machine-list-groups.d.ts.map +1 -0
- package/dist/core/machine-list-groups.js +41 -0
- package/dist/core/machine-list-groups.js.map +1 -0
- package/dist/core/membership-proof.d.ts +20 -9
- package/dist/core/membership-proof.d.ts.map +1 -1
- package/dist/core/membership-proof.js +41 -15
- package/dist/core/membership-proof.js.map +1 -1
- package/dist/core/membership-proofs.d.ts +14 -8
- package/dist/core/membership-proofs.d.ts.map +1 -1
- package/dist/core/membership-proofs.js +21 -10
- package/dist/core/membership-proofs.js.map +1 -1
- package/dist/core/mesh-store-shared.d.ts +2 -1
- package/dist/core/mesh-store-shared.d.ts.map +1 -1
- package/dist/core/mesh-store-shared.js.map +1 -1
- package/dist/core/mesh-store.d.ts +17 -10
- package/dist/core/mesh-store.d.ts.map +1 -1
- package/dist/core/mesh-store.js +35 -38
- package/dist/core/mesh-store.js.map +1 -1
- package/dist/core/network-interfaces.d.ts +7 -0
- package/dist/core/network-interfaces.d.ts.map +1 -0
- package/dist/core/network-interfaces.js +23 -0
- package/dist/core/network-interfaces.js.map +1 -0
- package/dist/core/peer-versions.d.ts +6 -0
- package/dist/core/peer-versions.d.ts.map +1 -1
- package/dist/core/peer-versions.js +17 -0
- package/dist/core/peer-versions.js.map +1 -1
- package/dist/core/room-path.d.ts +2 -0
- package/dist/core/room-path.d.ts.map +1 -1
- package/dist/core/room-path.js +5 -1
- package/dist/core/room-path.js.map +1 -1
- package/dist/core/tool.d.ts +10 -5
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +16 -3
- package/dist/core/tool.js.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +4 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/user-identity.d.ts +4 -6
- package/dist/core/user-identity.d.ts.map +1 -1
- package/dist/core/user-identity.js +9 -113
- package/dist/core/user-identity.js.map +1 -1
- package/dist/core/wire-mesh-transport-options.d.ts +2 -2
- package/dist/core/wire-mesh-transport-options.d.ts.map +1 -1
- package/dist/core/wire-mesh-transport.d.ts.map +1 -1
- package/dist/core/wire-mesh-transport.js +7 -7
- package/dist/core/wire-mesh-transport.js.map +1 -1
- package/dist/test/delivery-engine-delivery.unit.test.js +1 -0
- package/dist/test/delivery-engine-delivery.unit.test.js.map +1 -1
- package/dist/test/delivery-engine-directed-notify.unit.test.js +1 -0
- package/dist/test/delivery-engine-directed-notify.unit.test.js.map +1 -1
- package/dist/test/delivery-engine-own-agent.unit.test.js +1 -0
- package/dist/test/delivery-engine-own-agent.unit.test.js.map +1 -1
- package/dist/test/delivery-engine.unit.test.js +1 -0
- package/dist/test/delivery-engine.unit.test.js.map +1 -1
- package/dist/test/device-membership-verification.unit.test.js +25 -25
- package/dist/test/device-membership-verification.unit.test.js.map +1 -1
- package/dist/test/device-membership.unit.test.js +6 -6
- package/dist/test/device-membership.unit.test.js.map +1 -1
- package/dist/test/directory-admission.unit.test.js +103 -7
- package/dist/test/directory-admission.unit.test.js.map +1 -1
- package/dist/test/gateway-trust-tool.integration.test.js +115 -3
- package/dist/test/gateway-trust-tool.integration.test.js.map +1 -1
- package/dist/test/gateway-trust.unit.test.js +62 -13
- package/dist/test/gateway-trust.unit.test.js.map +1 -1
- package/dist/test/gossip-directory.unit.test.js +14 -1
- package/dist/test/gossip-directory.unit.test.js.map +1 -1
- package/dist/test/hub-exposure.integration.test.js +1 -1
- package/dist/test/hub-exposure.integration.test.js.map +1 -1
- package/dist/test/identity-restart.integration.test.js +2 -0
- package/dist/test/identity-restart.integration.test.js.map +1 -1
- package/dist/test/identity-store.integration.test.js +64 -14
- package/dist/test/identity-store.integration.test.js.map +1 -1
- package/dist/test/machine-grouping.unit.test.d.ts +2 -0
- package/dist/test/machine-grouping.unit.test.d.ts.map +1 -0
- package/dist/test/machine-grouping.unit.test.js +61 -0
- package/dist/test/machine-grouping.unit.test.js.map +1 -0
- package/dist/test/machine-identity.unit.test.d.ts +5 -0
- package/dist/test/machine-identity.unit.test.d.ts.map +1 -0
- package/dist/test/machine-identity.unit.test.js +65 -0
- package/dist/test/machine-identity.unit.test.js.map +1 -0
- package/dist/test/machine-list-groups.unit.test.d.ts +5 -0
- package/dist/test/machine-list-groups.unit.test.d.ts.map +1 -0
- package/dist/test/machine-list-groups.unit.test.js +57 -0
- package/dist/test/machine-list-groups.unit.test.js.map +1 -0
- package/dist/test/machine-membership-trust.integration.test.d.ts +5 -0
- package/dist/test/machine-membership-trust.integration.test.d.ts.map +1 -0
- package/dist/test/machine-membership-trust.integration.test.js +151 -0
- package/dist/test/machine-membership-trust.integration.test.js.map +1 -0
- package/dist/test/membership-proof.unit.test.js +105 -13
- package/dist/test/membership-proof.unit.test.js.map +1 -1
- package/dist/test/principal-membership-trust.integration.test.js +2 -1
- package/dist/test/principal-membership-trust.integration.test.js.map +1 -1
- package/dist/test/room-join-admission.integration.test.js +2 -0
- package/dist/test/room-join-admission.integration.test.js.map +1 -1
- package/dist/test/room-lifecycle-bare-name.unit.test.js +1 -0
- package/dist/test/room-lifecycle-bare-name.unit.test.js.map +1 -1
- package/dist/test/room-lifecycle-membership.unit.test.js +1 -0
- package/dist/test/room-lifecycle-membership.unit.test.js.map +1 -1
- package/dist/test/room-lifecycle-remote.unit.test.js +1 -0
- package/dist/test/room-lifecycle-remote.unit.test.js.map +1 -1
- package/dist/test/room-lifecycle.unit.test.js +1 -0
- package/dist/test/room-lifecycle.unit.test.js.map +1 -1
- package/dist/test/room-messaging-durable-send.unit.test.js +1 -0
- package/dist/test/room-messaging-durable-send.unit.test.js.map +1 -1
- package/dist/test/room-messaging.unit.test.js +1 -0
- package/dist/test/room-messaging.unit.test.js.map +1 -1
- package/dist/test/room-protocol-notify.unit.test.js +1 -0
- package/dist/test/room-protocol-notify.unit.test.js.map +1 -1
- package/dist/test/room-protocol.helper.d.ts.map +1 -1
- package/dist/test/room-protocol.helper.js +1 -0
- package/dist/test/room-protocol.helper.js.map +1 -1
- package/dist/test/room-send-retry.integration.test.js +2 -0
- package/dist/test/room-send-retry.integration.test.js.map +1 -1
- package/dist/test/test-transport.d.ts +3 -0
- package/dist/test/test-transport.d.ts.map +1 -1
- package/dist/test/test-transport.js +6 -1
- package/dist/test/test-transport.js.map +1 -1
- package/dist/test/user-identity.unit.test.js +40 -14
- package/dist/test/user-identity.unit.test.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A persisted grouping-issuer keypair: one locally generated key per file, the shape the user principal (user-identity.ts, agent-comms#160) and the machine identity (machine-identity.ts, agent-comms#343) both take. Neither is ever a live mesh peer, so neither needs identity-store.ts's PID-probed slot lock; the only race that matters is which process's key wins at first creation, which an exclusive ("wx") create settles.
|
|
3
|
+
*
|
|
4
|
+
* The key is never derived from anything about the host (platform UUIDs, serials, hostnames): those are linkable, cloned with disk images and swapped with hardware. Moving an identity to a rebuilt host is a deliberate copy of its file, the SSH host-key model.
|
|
5
|
+
*
|
|
6
|
+
* Each file is written with owner-only permissions (0600) inside a 0700 directory, and every write is atomic (atomic-file.ts), since another bridge may be reading the file at that moment: a reader sees a complete record or none, so a file that exists but cannot be parsed is never one of this module's own writes in progress. It is refused loudly rather than replaced with a fresh key, because replacing it silently changes the issuer's id and every peer that trusted the old one loses every device it vouched for. Fields an owning module adds beside the key material (issued-grant bookkeeping, a display name) are preserved by every write here: renewal spreads the stored record before replacing the key fields.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { createFileExclusive, writeFileAtomic } from "./atomic-file.js";
|
|
11
|
+
import { generateIdentity, certifyKeyPair, getCertificateFingerprint, deriveDeviceId, CERTIFICATE_VALIDITY_MS, } from "./identity.js";
|
|
12
|
+
/** Renewal margin is one twelfth of the certificate's total validity period, matching identity-store.ts's own renewal margin so every identity ages out on the same schedule. */
|
|
13
|
+
const RENEWAL_MARGIN_FRACTION = 12;
|
|
14
|
+
const RENEWAL_MARGIN_MS = CERTIFICATE_VALIDITY_MS / RENEWAL_MARGIN_FRACTION;
|
|
15
|
+
/** Owner-only read and write: the file holds a private key. */
|
|
16
|
+
const IDENTITY_FILE_MODE = 0o600;
|
|
17
|
+
/** Owner-only directory, matching identity-store.ts's own base directory. */
|
|
18
|
+
const IDENTITY_DIR_MODE = 0o700;
|
|
19
|
+
/** Whether value holds the key-material fields every issuer identity record carries. */
|
|
20
|
+
export function isStoredIssuerKey(value) {
|
|
21
|
+
if (typeof value !== "object" || value === null)
|
|
22
|
+
return false;
|
|
23
|
+
if (!("privateKey" in value) ||
|
|
24
|
+
!("certificate" in value) ||
|
|
25
|
+
!("expiresAt" in value))
|
|
26
|
+
return false;
|
|
27
|
+
return (typeof value.privateKey === "string" &&
|
|
28
|
+
typeof value.certificate === "string" &&
|
|
29
|
+
typeof value.expiresAt === "string");
|
|
30
|
+
}
|
|
31
|
+
/** Narrows a caught value to Node's own errno-carrying Error subtype, so a specific error code (e.g. ENOENT) can be checked without an `as` assertion. */
|
|
32
|
+
function isErrnoException(value) {
|
|
33
|
+
return value instanceof Error && "code" in value;
|
|
34
|
+
}
|
|
35
|
+
/** Reads the file's raw contents, or undefined if it does not exist yet. Any other filesystem error (permissions, a directory in its place) is surfaced rather than treated as absent. */
|
|
36
|
+
function readRawFile(file) {
|
|
37
|
+
try {
|
|
38
|
+
return fs.readFileSync(file, "utf-8");
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (isErrnoException(err) && err.code === "ENOENT")
|
|
42
|
+
return undefined;
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The file's parsed record, or undefined when the file does not exist. The owning module supplies a guard for its own full record shape. Throws when the file exists but is not JSON or fails the guard (see the module comment for why that is never repaired here). */
|
|
47
|
+
export function readIssuerRecord(file, guard) {
|
|
48
|
+
const raw = readRawFile(file);
|
|
49
|
+
return raw === undefined ? undefined : parseIssuerRecord(file, raw, guard);
|
|
50
|
+
}
|
|
51
|
+
function parseIssuerRecord(file, raw, guard) {
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
throw unusableRecord(file, err);
|
|
58
|
+
}
|
|
59
|
+
if (!guard(parsed))
|
|
60
|
+
throw unusableRecord(file);
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
/** The error for an identity file that exists but holds no usable record. Moving it aside is the operator's decision, since the next load then mints a new key under a new id. */
|
|
64
|
+
function unusableRecord(file, cause) {
|
|
65
|
+
return new Error(`${file} exists but does not hold a usable identity record; move it aside to mint a new key, which changes this identity's id for every peer that trusts it`, { cause });
|
|
66
|
+
}
|
|
67
|
+
function serializeRecord(stored) {
|
|
68
|
+
return `${JSON.stringify(stored, null, 2)}\n`;
|
|
69
|
+
}
|
|
70
|
+
/** Replaces the file with record in full, owner-only. Callers read the current record first and spread it, so no field another writer owns is lost. */
|
|
71
|
+
export function writeIssuerRecord(file, record) {
|
|
72
|
+
writeFileAtomic(file, serializeRecord(record), IDENTITY_FILE_MODE);
|
|
73
|
+
}
|
|
74
|
+
function toPeerIdentity(stored) {
|
|
75
|
+
return {
|
|
76
|
+
privateKey: stored.privateKey,
|
|
77
|
+
certificate: stored.certificate,
|
|
78
|
+
fingerprint: getCertificateFingerprint(stored.certificate),
|
|
79
|
+
deviceId: deriveDeviceId(stored.privateKey),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function keyRecord(identity) {
|
|
83
|
+
return {
|
|
84
|
+
privateKey: identity.privateKey,
|
|
85
|
+
certificate: identity.certificate,
|
|
86
|
+
expiresAt: new Date(Date.now() + CERTIFICATE_VALIDITY_MS).toISOString(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Renews a stored identity nearing certificate expiry by re-certifying its existing key pair, so its device-id is unchanged (see identity.ts's certifyKeyPair for why a fresh key pair would be wrong). Every other field in the record is kept. Returns the identity unchanged when it is not yet near expiry. */
|
|
90
|
+
function renewIfNeeded(file, stored) {
|
|
91
|
+
const expiresAt = Date.parse(stored.expiresAt);
|
|
92
|
+
const needsRenewal = Number.isNaN(expiresAt) || Date.now() > expiresAt - RENEWAL_MARGIN_MS;
|
|
93
|
+
if (!needsRenewal)
|
|
94
|
+
return toPeerIdentity(stored);
|
|
95
|
+
const renewed = certifyKeyPair(stored.privateKey);
|
|
96
|
+
writeIssuerRecord(file, { ...stored, ...keyRecord(renewed) });
|
|
97
|
+
return renewed;
|
|
98
|
+
}
|
|
99
|
+
/** Generates a fresh identity and persists it at file, which does not exist yet. The create is exclusive, so when a concurrent caller creates the file first this caller keeps the winner's key and returns that instead of overwriting it, judging the winner's record with the owning module's guard. */
|
|
100
|
+
function createIssuerIdentity(file, guard) {
|
|
101
|
+
const identity = generateIdentity();
|
|
102
|
+
if (createFileExclusive(file, serializeRecord(keyRecord(identity)), IDENTITY_FILE_MODE)) {
|
|
103
|
+
return identity;
|
|
104
|
+
}
|
|
105
|
+
const winner = readIssuerRecord(file, guard);
|
|
106
|
+
if (winner === undefined) {
|
|
107
|
+
throw new Error(`${file} was created and removed again while loading it`);
|
|
108
|
+
}
|
|
109
|
+
return renewIfNeeded(file, winner);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Loads the issuer identity persisted at file, creating it on first use. A record nearing certificate expiry is renewed in place and a missing file is created exclusively. guard is the owning module's check of its full record shape, the same one its own reads use, so a record that loads here is one every later read-modify-write of that module accepts too. A file that exists but fails guard is an error, never regenerated.
|
|
113
|
+
*/
|
|
114
|
+
export function loadOrCreateIssuerIdentity(file, guard) {
|
|
115
|
+
fs.mkdirSync(path.dirname(file), {
|
|
116
|
+
recursive: true,
|
|
117
|
+
mode: IDENTITY_DIR_MODE,
|
|
118
|
+
});
|
|
119
|
+
const stored = readIssuerRecord(file, guard);
|
|
120
|
+
if (stored === undefined)
|
|
121
|
+
return createIssuerIdentity(file, guard);
|
|
122
|
+
return renewIfNeeded(file, stored);
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=issuer-identity-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issuer-identity-file.js","sourceRoot":"","sources":["../../src/core/issuer-identity-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which machine each listed agent runs on (agent-comms#343), for list_agents' machine-grouped output. This device's own machine is known directly; every other device's is read from the machine proof in its gossiped agent/self advert and identified cryptographically (membership-proof.ts's identifyMembershipProofIssuer), so a device is grouped under a machine only when that machine's key signed a current proof for it. A device with no such proof (a hidden agent, which sends no agent/self advert, or a bridge that predates machine proofs) has no machine here.
|
|
3
|
+
*
|
|
4
|
+
* Grouping is display, not trust: a proof names who vouches for a device id, not that the device gossiping it is that device, so nothing here is used to authorise anything. Identifying a proof is cryptography, so verdicts are remembered per proof text until the proof lapses, and the cache is bounded. For the same reason a failure to identify one proof is reported and leaves that device unplaced rather than failing the listing it feeds.
|
|
5
|
+
*/
|
|
6
|
+
import type { IdentifiedMembershipProof } from "./membership-proof.js";
|
|
7
|
+
export interface MachineGroupingDeps {
|
|
8
|
+
/** Every device this side has heard gossip from, with its latest advert. */
|
|
9
|
+
listKnownDevices: () => readonly {
|
|
10
|
+
deviceId: string;
|
|
11
|
+
advert: Readonly<Record<string, unknown>>;
|
|
12
|
+
}[];
|
|
13
|
+
/** Identifies which issuer a proof shows vouching for a device (MembershipProofs.identify). Called only once getMachineId answers, so the store's identity is attached. */
|
|
14
|
+
identify: (claim: Readonly<{
|
|
15
|
+
proof: string;
|
|
16
|
+
deviceHex: string;
|
|
17
|
+
}>) => Promise<IdentifiedMembershipProof>;
|
|
18
|
+
/** This device's own id (hex). */
|
|
19
|
+
getPeerId: () => string;
|
|
20
|
+
/** This host's own machine id (hex), undefined before the store's identity is attached. */
|
|
21
|
+
getMachineId: () => string | undefined;
|
|
22
|
+
/** Where a proof that could not be identified at all (identify rejected) is reported. */
|
|
23
|
+
onError: (error: Error) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare class MachineGrouping {
|
|
26
|
+
private readonly deps;
|
|
27
|
+
private readonly verdicts;
|
|
28
|
+
constructor(deps: Readonly<MachineGroupingDeps>);
|
|
29
|
+
/** The machine (hex) of each device this side can place on one, keyed by device id: this device's own, and every gossiped device with a current, valid machine proof. */
|
|
30
|
+
machinesByDevice(): Promise<Map<string, string>>;
|
|
31
|
+
private machineFor;
|
|
32
|
+
private remember;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=machine-grouping.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-grouping.d.ts","sourceRoot":"","sources":["../../src/core/machine-grouping.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAKvE,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,SAAS;QAC/B,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KAC3C,EAAE,CAAC;IACJ,2KAA2K;IAC3K,QAAQ,EAAE,CACR,KAAK,EAAE,QAAQ,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,KAClD,OAAO,CAAC,yBAAyB,CAAC,CAAC;IACxC,kCAAkC;IAClC,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,2FAA2F;IAC3F,YAAY,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACvC,yFAAyF;IACzF,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACjC;AAUD,qBAAa,eAAe;IAGd,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;gBAEhC,IAAI,EAAE,QAAQ,CAAC,mBAAmB,CAAC;IAEhE,yKAAyK;IACnK,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAgBxC,UAAU;IA6BxB,OAAO,CAAC,QAAQ;CAQjB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which machine each listed agent runs on (agent-comms#343), for list_agents' machine-grouped output. This device's own machine is known directly; every other device's is read from the machine proof in its gossiped agent/self advert and identified cryptographically (membership-proof.ts's identifyMembershipProofIssuer), so a device is grouped under a machine only when that machine's key signed a current proof for it. A device with no such proof (a hidden agent, which sends no agent/self advert, or a bridge that predates machine proofs) has no machine here.
|
|
3
|
+
*
|
|
4
|
+
* Grouping is display, not trust: a proof names who vouches for a device id, not that the device gossiping it is that device, so nothing here is used to authorise anything. Identifying a proof is cryptography, so verdicts are remembered per proof text until the proof lapses, and the cache is bounded. For the same reason a failure to identify one proof is reported and leaves that device unplaced rather than failing the listing it feeds.
|
|
5
|
+
*/
|
|
6
|
+
import { readMembershipProof } from "./gossip-directory.js";
|
|
7
|
+
/** The most proofs whose verdict is remembered. */
|
|
8
|
+
const VERDICT_CACHE_LIMIT = 1024;
|
|
9
|
+
export class MachineGrouping {
|
|
10
|
+
deps;
|
|
11
|
+
verdicts = new Map();
|
|
12
|
+
constructor(deps) {
|
|
13
|
+
this.deps = deps;
|
|
14
|
+
}
|
|
15
|
+
/** The machine (hex) of each device this side can place on one, keyed by device id: this device's own, and every gossiped device with a current, valid machine proof. */
|
|
16
|
+
async machinesByDevice() {
|
|
17
|
+
const machines = new Map();
|
|
18
|
+
const own = this.deps.getMachineId();
|
|
19
|
+
// Without an identity attached there is no clock or revocation view to identify a proof against, and no own machine either.
|
|
20
|
+
if (own === undefined)
|
|
21
|
+
return machines;
|
|
22
|
+
machines.set(this.deps.getPeerId(), own);
|
|
23
|
+
for (const { deviceId, advert } of this.deps.listKnownDevices()) {
|
|
24
|
+
if (machines.has(deviceId))
|
|
25
|
+
continue;
|
|
26
|
+
const proof = readMembershipProof(advert, "machine");
|
|
27
|
+
if (proof === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
const machine = await this.machineFor(proof, deviceId);
|
|
30
|
+
if (machine !== undefined)
|
|
31
|
+
machines.set(deviceId, machine);
|
|
32
|
+
}
|
|
33
|
+
return machines;
|
|
34
|
+
}
|
|
35
|
+
async machineFor(proof, deviceHex) {
|
|
36
|
+
const cached = this.verdicts.get(proof);
|
|
37
|
+
if (cached?.deviceHex === deviceHex && cached.until > Date.now()) {
|
|
38
|
+
return cached.machine;
|
|
39
|
+
}
|
|
40
|
+
let verdict;
|
|
41
|
+
try {
|
|
42
|
+
verdict = await this.deps.identify({ proof, deviceHex });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Not remembered: the failure is in this side's own view (a revocation view that threw, say), not a verdict on the proof, so the next listing tries again.
|
|
46
|
+
this.deps.onError(new Error(`could not identify the machine proof of ${deviceHex}`, {
|
|
47
|
+
cause: err,
|
|
48
|
+
}));
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
this.remember(proof, verdict.ok
|
|
52
|
+
? { deviceHex, machine: verdict.issuerHex, until: verdict.expires }
|
|
53
|
+
: { deviceHex, machine: undefined, until: Number.POSITIVE_INFINITY });
|
|
54
|
+
return verdict.ok ? verdict.issuerHex : undefined;
|
|
55
|
+
}
|
|
56
|
+
remember(proof, verdict) {
|
|
57
|
+
this.verdicts.delete(proof);
|
|
58
|
+
this.verdicts.set(proof, verdict);
|
|
59
|
+
if (this.verdicts.size > VERDICT_CACHE_LIMIT) {
|
|
60
|
+
const oldest = this.verdicts.keys().next();
|
|
61
|
+
if (oldest.done !== true)
|
|
62
|
+
this.verdicts.delete(oldest.value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=machine-grouping.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-grouping.js","sourceRoot":"","sources":["../../src/core/machine-grouping.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5D,mDAAmD;AACnD,MAAM,mBAAmB,GAAG,IAAI,CAAC;AA4BjC,MAAM,OAAO,eAAe;IAGG;IAFZ,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAE7D,YAA6B,IAAmC;QAAnC,SAAI,GAAJ,IAAI,CAA+B;IAAG,CAAC;IAEpE,yKAAyK;IACzK,KAAK,CAAC,gBAAgB;QACpB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACrC,4HAA4H;QAC5H,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAC;QACvC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,CAAC,CAAC;QACzC,KAAK,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAChE,IAAI,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACrC,MAAM,KAAK,GAAG,mBAAmB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YACrD,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAS;YAClC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YACvD,IAAI,OAAO,KAAK,SAAS;gBAAE,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,KAAK,CAAC,UAAU,CACtB,KAAa,EACb,SAAiB;QAEjB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,MAAM,EAAE,SAAS,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YACjE,OAAO,MAAM,CAAC,OAAO,CAAC;QACxB,CAAC;QACD,IAAI,OAAkC,CAAC;QACvC,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAC3D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,2JAA2J;YAC3J,IAAI,CAAC,IAAI,CAAC,OAAO,CACf,IAAI,KAAK,CAAC,2CAA2C,SAAS,EAAE,EAAE;gBAChE,KAAK,EAAE,GAAG;aACX,CAAC,CACH,CAAC;YACF,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,IAAI,CAAC,QAAQ,CACX,KAAK,EACL,OAAO,CAAC,EAAE;YACR,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;YACnE,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,iBAAiB,EAAE,CACvE,CAAC;QACF,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACpD,CAAC;IAEO,QAAQ,CAAC,KAAa,EAAE,OAAgC;QAC9D,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,mBAAmB,EAAE,CAAC;YAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YAC3C,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI;gBAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent machine identity (agent-comms#343): one keypair per OS account per host, the third identity scope beside a bridge slot's device identity (identity-store.ts) and the account's user principal (user-identity.ts). It is structurally another grouping issuer, like the principal: it vouches for the devices living on this host with the same group:member proofs (membership-proof.ts), so a peer can group devices by machine and trust or stop trusting a whole host in one act.
|
|
3
|
+
*
|
|
4
|
+
* Stored at ~/.agent-comms/machine-identity.json (mode 0600), so it belongs to the OS account whose home directory holds it: every bridge that account runs on the host reads the same file and is vouched for by the same machine key, while another account on the same host mints a machine key of its own and appears as a separate machine. Until the principal becomes portable across hosts (agent-comms#344) this is the same scope as the user principal; a key naming the host across every account needs a signer outside any one account and is agent-comms#367. The key is generated locally on first use and never derived from hardware (issuer-identity-file.ts says why); re-homing a rebuilt host is a deliberate copy of this file.
|
|
5
|
+
*/
|
|
6
|
+
import type { PeerIdentity } from "./identity.js";
|
|
7
|
+
export interface MachineIdentityOptions {
|
|
8
|
+
/** Directory override for tests; defaults to ~/.agent-comms, beside the user identity and every bridge slot. */
|
|
9
|
+
dir?: string;
|
|
10
|
+
}
|
|
11
|
+
/** The machine identity file for options. */
|
|
12
|
+
export declare function machineIdentityFile(options?: Readonly<MachineIdentityOptions>): string;
|
|
13
|
+
/** Loads this account's machine identity for this host, creating it on first use (exclusive create, renewal in place, an unusable file refused rather than regenerated). */
|
|
14
|
+
export declare function loadOrCreateMachineIdentity(options?: Readonly<MachineIdentityOptions>): PeerIdentity;
|
|
15
|
+
//# sourceMappingURL=machine-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../src/core/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAMlD,MAAM,WAAW,sBAAsB;IACrC,gHAAgH;IAChH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,6CAA6C;AAC7C,wBAAgB,mBAAmB,CACjC,OAAO,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GACzC,MAAM,CAGR;AAED,4KAA4K;AAC5K,wBAAgB,2BAA2B,CACzC,OAAO,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GACzC,YAAY,CAKd"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent machine identity (agent-comms#343): one keypair per OS account per host, the third identity scope beside a bridge slot's device identity (identity-store.ts) and the account's user principal (user-identity.ts). It is structurally another grouping issuer, like the principal: it vouches for the devices living on this host with the same group:member proofs (membership-proof.ts), so a peer can group devices by machine and trust or stop trusting a whole host in one act.
|
|
3
|
+
*
|
|
4
|
+
* Stored at ~/.agent-comms/machine-identity.json (mode 0600), so it belongs to the OS account whose home directory holds it: every bridge that account runs on the host reads the same file and is vouched for by the same machine key, while another account on the same host mints a machine key of its own and appears as a separate machine. Until the principal becomes portable across hosts (agent-comms#344) this is the same scope as the user principal; a key naming the host across every account needs a signer outside any one account and is agent-comms#367. The key is generated locally on first use and never derived from hardware (issuer-identity-file.ts says why); re-homing a rebuilt host is a deliberate copy of this file.
|
|
5
|
+
*/
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { isStoredIssuerKey, loadOrCreateIssuerIdentity, } from "./issuer-identity-file.js";
|
|
9
|
+
/** The machine identity file for options. */
|
|
10
|
+
export function machineIdentityFile(options) {
|
|
11
|
+
const dir = options?.dir ?? path.join(os.homedir(), ".agent-comms");
|
|
12
|
+
return path.join(dir, "machine-identity.json");
|
|
13
|
+
}
|
|
14
|
+
/** Loads this account's machine identity for this host, creating it on first use (exclusive create, renewal in place, an unusable file refused rather than regenerated). */
|
|
15
|
+
export function loadOrCreateMachineIdentity(options) {
|
|
16
|
+
return loadOrCreateIssuerIdentity(machineIdentityFile(options), isStoredIssuerKey);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=machine-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-identity.js","sourceRoot":"","sources":["../../src/core/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EACL,iBAAiB,EACjB,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AAOnC,6CAA6C;AAC7C,MAAM,UAAU,mBAAmB,CACjC,OAA0C;IAE1C,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,uBAAuB,CAAC,CAAC;AACjD,CAAC;AAED,4KAA4K;AAC5K,MAAM,UAAU,2BAA2B,CACzC,OAA0C;IAE1C,OAAO,0BAA0B,CAC/B,mBAAmB,CAAC,OAAO,CAAC,EAC5B,iBAAiB,CAClB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list_agents' machine grouping (agent-comms#343): the listed agents split into one group per machine, this host first, then every other machine by id, then the agents no machine proof places. Split out of tool.ts to keep that file under the repo's max-lines cap.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentIdentity } from "./types.js";
|
|
5
|
+
/** One run of listed agents and the heading above it; no heading when nothing is grouped at all. */
|
|
6
|
+
export interface AgentGroup {
|
|
7
|
+
heading: string | undefined;
|
|
8
|
+
agents: AgentIdentity[];
|
|
9
|
+
}
|
|
10
|
+
/** agents grouped by machine, each group keeping the agents' listed order. machines maps device id to machine id (MeshStore.listAgentMachines); undefined on a store with no machine identity, in which case, as when no agent is placed on a machine, the list is one ungrouped run. */
|
|
11
|
+
export declare function groupAgentsByMachine(agents: readonly AgentIdentity[], machines: ReadonlyMap<string, string> | undefined, ownMachine: string | undefined): AgentGroup[];
|
|
12
|
+
//# sourceMappingURL=machine-list-groups.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-list-groups.d.ts","sourceRoot":"","sources":["../../src/core/machine-list-groups.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,oGAAoG;AACpG,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,yRAAyR;AACzR,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EACjD,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,UAAU,EAAE,CAiCd"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list_agents' machine grouping (agent-comms#343): the listed agents split into one group per machine, this host first, then every other machine by id, then the agents no machine proof places. Split out of tool.ts to keep that file under the repo's max-lines cap.
|
|
3
|
+
*/
|
|
4
|
+
/** agents grouped by machine, each group keeping the agents' listed order. machines maps device id to machine id (MeshStore.listAgentMachines); undefined on a store with no machine identity, in which case, as when no agent is placed on a machine, the list is one ungrouped run. */
|
|
5
|
+
export function groupAgentsByMachine(agents, machines, ownMachine) {
|
|
6
|
+
if (machines === undefined ||
|
|
7
|
+
!agents.some((agent) => machines.has(agent.id))) {
|
|
8
|
+
return [{ heading: undefined, agents: [...agents] }];
|
|
9
|
+
}
|
|
10
|
+
const byMachine = new Map();
|
|
11
|
+
const unplaced = [];
|
|
12
|
+
for (const agent of agents) {
|
|
13
|
+
const machine = machines.get(agent.id);
|
|
14
|
+
if (machine === undefined) {
|
|
15
|
+
unplaced.push(agent);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const group = byMachine.get(machine);
|
|
19
|
+
if (group === undefined)
|
|
20
|
+
byMachine.set(machine, [agent]);
|
|
21
|
+
else
|
|
22
|
+
group.push(agent);
|
|
23
|
+
}
|
|
24
|
+
const groups = [...byMachine]
|
|
25
|
+
.sort(([a], [b]) => {
|
|
26
|
+
if (a === ownMachine)
|
|
27
|
+
return -1;
|
|
28
|
+
if (b === ownMachine)
|
|
29
|
+
return 1;
|
|
30
|
+
return a.localeCompare(b);
|
|
31
|
+
})
|
|
32
|
+
.map(([machine, members]) => ({
|
|
33
|
+
heading: `Machine ${machine}${machine === ownMachine ? " (this machine)" : ""}:`,
|
|
34
|
+
agents: members,
|
|
35
|
+
}));
|
|
36
|
+
if (unplaced.length > 0) {
|
|
37
|
+
groups.push({ heading: "Machine not proven:", agents: unplaced });
|
|
38
|
+
}
|
|
39
|
+
return groups;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=machine-list-groups.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-list-groups.js","sourceRoot":"","sources":["../../src/core/machine-list-groups.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,yRAAyR;AACzR,MAAM,UAAU,oBAAoB,CAClC,MAAgC,EAChC,QAAiD,EACjD,UAA8B;IAE9B,IACE,QAAQ,KAAK,SAAS;QACtB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAC/C,CAAC;QACD,OAAO,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;IACrD,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACvC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;;YACpD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IACD,MAAM,MAAM,GAAiB,CAAC,GAAG,SAAS,CAAC;SACxC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;QACjB,IAAI,CAAC,KAAK,UAAU;YAAE,OAAO,CAAC,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK,UAAU;YAAE,OAAO,CAAC,CAAC;QAC/B,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,OAAO,EAAE,WAAW,OAAO,GAAG,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,GAAG;QAChF,MAAM,EAAE,OAAO;KAChB,CAAC,CAAC,CAAC;IACN,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The proof a device gossips to show that
|
|
2
|
+
* The proof a device gossips to show that a grouping issuer vouches for it (agent-comms#266 for the user principal, agent-comms#343 for the machine): a short-lived group:member token, minted by that issuer for that one device, carried as one line of text. A receiver that trusts the issuer verifies it against the gossiping device and then trusts the device without it being listed individually, which is what trusting a principal or a machine is for. A receiver that only wants to know which machine a device runs on reads the issuer out of the proof (identifyMembershipProofIssuer).
|
|
3
3
|
*
|
|
4
|
-
* A proof shows that the
|
|
4
|
+
* A proof shows that the issuer vouches for a device id. It does not show that whoever gossips it is that device: both arrive in the same unauthenticated advert and anyone who has read one can repeat it, so it earns only what GatewayTrust.isReachable grants (see directory-admission.ts). It is also public: it names the issuer, so anyone connected to the hub can see which devices belong to which principal and which run on the same machine. That is why a proof is only ever carried in the agent/self advert, which a hidden or ghost agent never sends.
|
|
5
5
|
*
|
|
6
|
-
* Every bridge of one account shares that account's user identity, so any of them can mint a proof for any device id, which is the same trust the
|
|
6
|
+
* Every bridge of one account shares that account's user identity, and every bridge one account runs on a host shares that account's machine identity for the host (machine-identity.ts), so any of them can mint a proof for any device id, which is the same trust the issuer is given anyway. Proofs are short-lived rather than revocable, so removeDevice (which revokes an admitDevice grant) does not affect them and a removed device holding the issuer's key can keep minting its own: they are minted without recording an issued-grant, so a bridge refreshing its own does not rewrite the shared identity file.
|
|
7
7
|
*/
|
|
8
8
|
import type { Clock } from "wire-mesh-core/ports/clock";
|
|
9
9
|
import type { IdentityPort } from "wire-mesh-core/ports/identity";
|
|
@@ -14,20 +14,20 @@ export declare const MAX_MEMBERSHIP_PROOF_LENGTH = 4096;
|
|
|
14
14
|
/** How long a minted proof stays valid. Long enough to ride out a missed refresh or a gossip gap, short enough that a device an account stops running stops being trusted soon after. */
|
|
15
15
|
export declare const MEMBERSHIP_PROOF_LIFETIME_MS: number;
|
|
16
16
|
export interface MintMembershipProofOptions {
|
|
17
|
-
/** The
|
|
18
|
-
|
|
17
|
+
/** The grouping issuer vouching for the device: the user principal or the machine. */
|
|
18
|
+
issuer: IdentityPort;
|
|
19
19
|
clock: Clock;
|
|
20
20
|
/** The device the proof is for, and the only device it will verify for. */
|
|
21
21
|
deviceId: DeviceId;
|
|
22
22
|
}
|
|
23
|
-
/** A fresh proof that options.
|
|
23
|
+
/** A fresh proof that options.issuer vouches for options.deviceId, as one line of text. Throws MINT_FAILED if the token cannot be minted. */
|
|
24
24
|
export declare function mintMembershipProof(options: Readonly<MintMembershipProofOptions>): Promise<string>;
|
|
25
25
|
export interface VerifyMembershipProofOptions {
|
|
26
26
|
proof: string;
|
|
27
27
|
/** The device the proof is claimed for: the gossiping device the directory entry names. */
|
|
28
28
|
deviceId: DeviceId;
|
|
29
|
-
/** The principal the proof must have been minted by. */
|
|
30
|
-
|
|
29
|
+
/** The issuer (principal or machine) the proof must have been minted by. */
|
|
30
|
+
issuerId: DeviceId;
|
|
31
31
|
/** The verifying node's own identity, clock and revocation view. */
|
|
32
32
|
identity: IdentityPort;
|
|
33
33
|
clock: Clock;
|
|
@@ -40,6 +40,17 @@ export type MembershipProofVerdict = {
|
|
|
40
40
|
ok: false;
|
|
41
41
|
reason: string;
|
|
42
42
|
};
|
|
43
|
-
/** Whether options.proof shows that options.
|
|
43
|
+
/** Whether options.proof shows that options.issuerId vouches for options.deviceId right now. Never throws for a bad proof: text that is not a token, a token for another device or issuer, an expired or revoked one, all come back as a refusal. */
|
|
44
44
|
export declare function verifyMembershipProof(options: Readonly<VerifyMembershipProofOptions>): Promise<MembershipProofVerdict>;
|
|
45
|
+
export type IdentifyMembershipProofOptions = Omit<VerifyMembershipProofOptions, "issuerId">;
|
|
46
|
+
export type IdentifiedMembershipProof = {
|
|
47
|
+
ok: true;
|
|
48
|
+
issuerHex: string;
|
|
49
|
+
expires: number;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
reason: string;
|
|
53
|
+
};
|
|
54
|
+
/** Which issuer options.proof shows vouching for options.deviceId right now, learned from the proof itself rather than checked against one already known: the same refusals as verifyMembershipProof, plus a proof whose named group is not the key that signed it. */
|
|
55
|
+
export declare function identifyMembershipProofIssuer(options: Readonly<IdentifyMembershipProofOptions>): Promise<IdentifiedMembershipProof>;
|
|
45
56
|
//# sourceMappingURL=membership-proof.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership-proof.d.ts","sourceRoot":"","sources":["../../src/core/membership-proof.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"membership-proof.d.ts","sourceRoot":"","sources":["../../src/core/membership-proof.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,KAAK,EAEV,QAAQ,EACT,MAAM,mCAAmC,CAAC;AAa3C,gPAAgP;AAChP,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD,yLAAyL;AACzL,eAAO,MAAM,4BAA4B,QACU,CAAC;AAEpD,MAAM,WAAW,0BAA0B;IACzC,sFAAsF;IACtF,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,KAAK,CAAC;IACb,2EAA2E;IAC3E,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,6IAA6I;AAC7I,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,QAAQ,CAAC,0BAA0B,CAAC,GAC5C,OAAO,CAAC,MAAM,CAAC,CAejB;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,QAAQ,EAAE,QAAQ,CAAC;IACnB,4EAA4E;IAC5E,QAAQ,EAAE,QAAQ,CAAC;IACnB,oEAAoE;IACpE,QAAQ,EAAE,YAAY,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,MAAM,sBAAsB,GAChC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAgBhE,qPAAqP;AACrP,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,QAAQ,CAAC,4BAA4B,CAAC,GAC9C,OAAO,CAAC,sBAAsB,CAAC,CAcjC;AAED,MAAM,MAAM,8BAA8B,GAAG,IAAI,CAC/C,4BAA4B,EAC5B,UAAU,CACX,CAAC;AAEF,MAAM,MAAM,yBAAyB,GACjC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,uQAAuQ;AACvQ,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,QAAQ,CAAC,8BAA8B,CAAC,GAChD,OAAO,CAAC,yBAAyB,CAAC,CAgBpC"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The proof a device gossips to show that
|
|
2
|
+
* The proof a device gossips to show that a grouping issuer vouches for it (agent-comms#266 for the user principal, agent-comms#343 for the machine): a short-lived group:member token, minted by that issuer for that one device, carried as one line of text. A receiver that trusts the issuer verifies it against the gossiping device and then trusts the device without it being listed individually, which is what trusting a principal or a machine is for. A receiver that only wants to know which machine a device runs on reads the issuer out of the proof (identifyMembershipProofIssuer).
|
|
3
3
|
*
|
|
4
|
-
* A proof shows that the
|
|
4
|
+
* A proof shows that the issuer vouches for a device id. It does not show that whoever gossips it is that device: both arrive in the same unauthenticated advert and anyone who has read one can repeat it, so it earns only what GatewayTrust.isReachable grants (see directory-admission.ts). It is also public: it names the issuer, so anyone connected to the hub can see which devices belong to which principal and which run on the same machine. That is why a proof is only ever carried in the agent/self advert, which a hidden or ghost agent never sends.
|
|
5
5
|
*
|
|
6
|
-
* Every bridge of one account shares that account's user identity, so any of them can mint a proof for any device id, which is the same trust the
|
|
6
|
+
* Every bridge of one account shares that account's user identity, and every bridge one account runs on a host shares that account's machine identity for the host (machine-identity.ts), so any of them can mint a proof for any device id, which is the same trust the issuer is given anyway. Proofs are short-lived rather than revocable, so removeDevice (which revokes an admitDevice grant) does not affect them and a removed device holding the issuer's key can keep minting its own: they are minted without recording an issued-grant, so a bridge refreshing its own does not rewrite the shared identity file.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { verifyDeviceMembership } from "./device-membership-verification.js";
|
|
8
|
+
import { mintGroupMembership } from "./device-membership.js";
|
|
9
|
+
import { identifyGroupIssuer, verifyDeviceMembership, } from "./device-membership-verification.js";
|
|
10
10
|
import { randomId } from "./random-id.js";
|
|
11
11
|
import { CommsError } from "./store.js";
|
|
12
12
|
import { decodeTokenText, encodeTokenText } from "./token-text.js";
|
|
@@ -16,10 +16,10 @@ const MEMBERSHIP_PROOF_LIFETIME_MINUTES = 15;
|
|
|
16
16
|
export const MAX_MEMBERSHIP_PROOF_LENGTH = 4096;
|
|
17
17
|
/** How long a minted proof stays valid. Long enough to ride out a missed refresh or a gossip gap, short enough that a device an account stops running stops being trusted soon after. */
|
|
18
18
|
export const MEMBERSHIP_PROOF_LIFETIME_MS = MEMBERSHIP_PROOF_LIFETIME_MINUTES * MS_PER_MINUTE;
|
|
19
|
-
/** A fresh proof that options.
|
|
19
|
+
/** A fresh proof that options.issuer vouches for options.deviceId, as one line of text. Throws MINT_FAILED if the token cannot be minted. */
|
|
20
20
|
export async function mintMembershipProof(options) {
|
|
21
|
-
const verdict = await
|
|
22
|
-
|
|
21
|
+
const verdict = await mintGroupMembership({
|
|
22
|
+
issuer: options.issuer,
|
|
23
23
|
clock: options.clock,
|
|
24
24
|
tokenId: randomId(),
|
|
25
25
|
deviceId: options.deviceId,
|
|
@@ -30,24 +30,29 @@ export async function mintMembershipProof(options) {
|
|
|
30
30
|
}
|
|
31
31
|
return encodeTokenText(verdict.token);
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
if (
|
|
33
|
+
/** The proof text decoded to a token, or the refusal for text that cannot be one. */
|
|
34
|
+
function decodeProof(proof) {
|
|
35
|
+
if (proof.length > MAX_MEMBERSHIP_PROOF_LENGTH) {
|
|
36
36
|
return { ok: false, reason: "too_long" };
|
|
37
37
|
}
|
|
38
|
-
let token;
|
|
39
38
|
try {
|
|
40
|
-
token
|
|
39
|
+
return { ok: true, token: decodeTokenText(proof) };
|
|
41
40
|
}
|
|
42
41
|
catch {
|
|
43
42
|
return { ok: false, reason: "not_a_token" };
|
|
44
43
|
}
|
|
45
|
-
|
|
44
|
+
}
|
|
45
|
+
/** Whether options.proof shows that options.issuerId vouches for options.deviceId right now. Never throws for a bad proof: text that is not a token, a token for another device or issuer, an expired or revoked one, all come back as a refusal. */
|
|
46
|
+
export async function verifyMembershipProof(options) {
|
|
47
|
+
const decoded = decodeProof(options.proof);
|
|
48
|
+
if (!decoded.ok)
|
|
49
|
+
return decoded;
|
|
50
|
+
const verdict = await verifyDeviceMembership(decoded.token, {
|
|
46
51
|
identity: options.identity,
|
|
47
52
|
clock: options.clock,
|
|
48
53
|
revocation: options.revocation,
|
|
49
54
|
expectedBearer: options.deviceId,
|
|
50
|
-
|
|
55
|
+
groupIssuerId: options.issuerId,
|
|
51
56
|
});
|
|
52
57
|
if (!verdict.ok)
|
|
53
58
|
return { ok: false, reason: verdict.reason };
|
|
@@ -56,4 +61,25 @@ export async function verifyMembershipProof(options) {
|
|
|
56
61
|
return { ok: false, reason: "not_root_level" };
|
|
57
62
|
return { ok: true, expires: verdict.claims.expires };
|
|
58
63
|
}
|
|
64
|
+
/** Which issuer options.proof shows vouching for options.deviceId right now, learned from the proof itself rather than checked against one already known: the same refusals as verifyMembershipProof, plus a proof whose named group is not the key that signed it. */
|
|
65
|
+
export async function identifyMembershipProofIssuer(options) {
|
|
66
|
+
const decoded = decodeProof(options.proof);
|
|
67
|
+
if (!decoded.ok)
|
|
68
|
+
return decoded;
|
|
69
|
+
const verdict = await identifyGroupIssuer(decoded.token, {
|
|
70
|
+
identity: options.identity,
|
|
71
|
+
clock: options.clock,
|
|
72
|
+
revocation: options.revocation,
|
|
73
|
+
expectedBearer: options.deviceId,
|
|
74
|
+
});
|
|
75
|
+
if (!verdict.ok)
|
|
76
|
+
return { ok: false, reason: verdict.reason };
|
|
77
|
+
if (verdict.depth !== 0)
|
|
78
|
+
return { ok: false, reason: "not_root_level" };
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
issuerHex: verdict.issuerHex,
|
|
82
|
+
expires: verdict.claims.expires,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
59
85
|
//# sourceMappingURL=membership-proof.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership-proof.js","sourceRoot":"","sources":["../../src/core/membership-proof.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"membership-proof.js","sourceRoot":"","sources":["../../src/core/membership-proof.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A device's own membership proof, kept fresh, and the
|
|
2
|
+
* A device's own membership proof under one grouping issuer, kept fresh, and the checks for other devices' proofs (agent-comms#266, agent-comms#343). MeshStore holds one instance per issuer, its user principal and its machine, and hands each its identity, peer id and error sink. The checks do not depend on which issuer an instance mints under, so either instance can verify or identify any proof.
|
|
3
3
|
*/
|
|
4
|
+
import type { IdentityPort } from "wire-mesh-core/ports/identity";
|
|
4
5
|
import type { MeshStoreIdentity } from "./mesh-store-shared.js";
|
|
5
|
-
import { type MembershipProofVerdict } from "./membership-proof.js";
|
|
6
|
+
import { type IdentifiedMembershipProof, type MembershipProofVerdict } from "./membership-proof.js";
|
|
6
7
|
export interface MembershipProofsDeps {
|
|
7
8
|
/** Throws if the store has no identity yet. */
|
|
8
9
|
getIdentity: () => MeshStoreIdentity;
|
|
10
|
+
/** The issuer this instance mints under, picked out of the store's identity: its user principal or its machine. */
|
|
11
|
+
issuerOf: (identity: Readonly<MeshStoreIdentity>) => IdentityPort;
|
|
9
12
|
/** This device's own id (hex). */
|
|
10
13
|
getPeerId: () => string;
|
|
11
14
|
onError: (error: Error) => void;
|
|
@@ -16,19 +19,22 @@ export declare class MembershipProofs {
|
|
|
16
19
|
private minting;
|
|
17
20
|
private timer;
|
|
18
21
|
constructor(deps: Readonly<MembershipProofsDeps>);
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
membership?: string;
|
|
22
|
-
};
|
|
22
|
+
/** This device's current proof, undefined until the first has been minted. */
|
|
23
|
+
current(): string | undefined;
|
|
23
24
|
/** Mints a proof now and again on an interval. A failed mint is reported and retried on the next tick; the previous proof stays until it lapses. */
|
|
24
25
|
start(): void;
|
|
25
26
|
stop(): void;
|
|
26
|
-
/** Whether a gossiped proof shows that
|
|
27
|
+
/** Whether a gossiped proof shows that issuerHex vouches for deviceHex, as this node sees it: its own identity, clock and revocation view. */
|
|
27
28
|
verify(claim: Readonly<{
|
|
28
29
|
proof: string;
|
|
29
30
|
deviceHex: string;
|
|
30
|
-
|
|
31
|
+
issuerHex: string;
|
|
31
32
|
}>): Promise<MembershipProofVerdict>;
|
|
33
|
+
/** Which issuer a gossiped proof shows vouching for deviceHex, as this node sees it. */
|
|
34
|
+
identify(claim: Readonly<{
|
|
35
|
+
proof: string;
|
|
36
|
+
deviceHex: string;
|
|
37
|
+
}>): Promise<IdentifiedMembershipProof>;
|
|
32
38
|
private refresh;
|
|
33
39
|
}
|
|
34
40
|
//# sourceMappingURL=membership-proofs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership-proofs.d.ts","sourceRoot":"","sources":["../../src/core/membership-proofs.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"membership-proofs.d.ts","sourceRoot":"","sources":["../../src/core/membership-proofs.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAElE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAKL,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAO/B,MAAM,WAAW,oBAAoB;IACnC,+CAA+C;IAC/C,WAAW,EAAE,MAAM,iBAAiB,CAAC;IACrC,mHAAmH;IACnH,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,iBAAiB,CAAC,KAAK,YAAY,CAAC;IAClE,kCAAkC;IAClC,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACjC;AAED,qBAAa,gBAAgB;IAKf,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAA6C;gBAE7B,IAAI,EAAE,QAAQ,CAAC,oBAAoB,CAAC;IAEjE,8EAA8E;IAC9E,OAAO,IAAI,MAAM,GAAG,SAAS;IAI7B,oJAAoJ;IACpJ,KAAK,IAAI,IAAI;IASb,IAAI,IAAI,IAAI;IAKZ,8IAA8I;IACxI,MAAM,CACV,KAAK,EAAE,QAAQ,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,GACvE,OAAO,CAAC,sBAAsB,CAAC;IAYlC,wFAAwF;IAClF,QAAQ,CACZ,KAAK,EAAE,QAAQ,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,GACpD,OAAO,CAAC,yBAAyB,CAAC;IAWrC,OAAO,CAAC,OAAO;CAuBhB"}
|