remote-pi 0.1.2 → 0.2.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/README.md +195 -36
- package/dist/bin/supervisord.d.ts +2 -0
- package/dist/bin/supervisord.js +44 -0
- package/dist/bin/supervisord.js.map +1 -0
- package/dist/config.d.ts +49 -5
- package/dist/config.js +73 -9
- package/dist/config.js.map +1 -1
- package/dist/daemon/client.d.ts +20 -0
- package/dist/daemon/client.js +128 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/control_protocol.d.ts +100 -0
- package/dist/daemon/control_protocol.js +63 -0
- package/dist/daemon/control_protocol.js.map +1 -0
- package/dist/daemon/id.d.ts +18 -0
- package/dist/daemon/id.js +30 -0
- package/dist/daemon/id.js.map +1 -0
- package/dist/daemon/install.d.ts +132 -0
- package/dist/daemon/install.js +312 -0
- package/dist/daemon/install.js.map +1 -0
- package/dist/daemon/registry.d.ts +47 -0
- package/dist/daemon/registry.js +123 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/rpc_child.d.ts +76 -0
- package/dist/daemon/rpc_child.js +130 -0
- package/dist/daemon/rpc_child.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +38 -0
- package/dist/daemon/supervisor.js +301 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/index.d.ts +62 -8
- package/dist/index.js +1232 -304
- package/dist/index.js.map +1 -1
- package/dist/mesh/canonical.d.ts +30 -0
- package/dist/mesh/canonical.js +61 -0
- package/dist/mesh/canonical.js.map +1 -0
- package/dist/mesh/client.d.ts +31 -0
- package/dist/mesh/client.js +56 -0
- package/dist/mesh/client.js.map +1 -0
- package/dist/mesh/encoding.d.ts +36 -0
- package/dist/mesh/encoding.js +53 -0
- package/dist/mesh/encoding.js.map +1 -0
- package/dist/mesh/self_revoke.d.ts +111 -0
- package/dist/mesh/self_revoke.js +182 -0
- package/dist/mesh/self_revoke.js.map +1 -0
- package/dist/mesh/siblings.d.ts +62 -0
- package/dist/mesh/siblings.js +95 -0
- package/dist/mesh/siblings.js.map +1 -0
- package/dist/mesh/types.d.ts +34 -0
- package/dist/mesh/types.js +11 -0
- package/dist/mesh/types.js.map +1 -0
- package/dist/mesh/verify.d.ts +17 -0
- package/dist/mesh/verify.js +77 -0
- package/dist/mesh/verify.js.map +1 -0
- package/dist/pairing/qr.d.ts +16 -5
- package/dist/pairing/qr.js +27 -8
- package/dist/pairing/qr.js.map +1 -1
- package/dist/pairing/storage.d.ts +41 -0
- package/dist/pairing/storage.js +158 -21
- package/dist/pairing/storage.js.map +1 -1
- package/dist/protocol/types.d.ts +23 -0
- package/dist/session/broker.d.ts +74 -0
- package/dist/session/broker.js +142 -4
- package/dist/session/broker.js.map +1 -1
- package/dist/session/broker_remote.d.ts +110 -0
- package/dist/session/broker_remote.js +397 -0
- package/dist/session/broker_remote.js.map +1 -0
- package/dist/session/cwd_lock.d.ts +28 -0
- package/dist/session/cwd_lock.js +89 -0
- package/dist/session/cwd_lock.js.map +1 -0
- package/dist/session/global_config.d.ts +9 -0
- package/dist/session/global_config.js +9 -0
- package/dist/session/global_config.js.map +1 -1
- package/dist/session/leader_election.d.ts +16 -0
- package/dist/session/leader_election.js +22 -0
- package/dist/session/leader_election.js.map +1 -1
- package/dist/session/local_config.d.ts +12 -5
- package/dist/session/local_config.js +24 -3
- package/dist/session/local_config.js.map +1 -1
- package/dist/session/peer.d.ts +28 -1
- package/dist/session/peer.js +69 -2
- package/dist/session/peer.js.map +1 -1
- package/dist/session/peer_inventory.d.ts +13 -0
- package/dist/session/peer_inventory.js +48 -0
- package/dist/session/peer_inventory.js.map +1 -0
- package/dist/session/setup_wizard.d.ts +32 -8
- package/dist/session/setup_wizard.js +45 -33
- package/dist/session/setup_wizard.js.map +1 -1
- package/dist/session/tools.d.ts +15 -7
- package/dist/session/tools.js +145 -31
- package/dist/session/tools.js.map +1 -1
- package/dist/transport/pi_forward_client.d.ts +29 -0
- package/dist/transport/pi_forward_client.js +62 -0
- package/dist/transport/pi_forward_client.js.map +1 -0
- package/dist/ui/footer.js +8 -6
- package/dist/ui/footer.js.map +1 -1
- package/docs/daemon.md +289 -0
- package/package.json +10 -3
- package/service-templates/launchd.plist.template +35 -0
- package/service-templates/systemd.service.template +19 -0
- package/skills/agent-network/SKILL.md +273 -294
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { MeshClient } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Background poller that watches each Owner's `mesh_versions` envelope on
|
|
4
|
+
* the relay and self-revokes this Pi from any Owner that no longer lists
|
|
5
|
+
* it as a member.
|
|
6
|
+
*
|
|
7
|
+
* Behavior per sweep (one entry per unique Owner in peers.json):
|
|
8
|
+
* 1. Compute `hash = sha256(ownerPk)` (lowercase hex) — the URL slug.
|
|
9
|
+
* MUST match the format the relay stores and the app publishes;
|
|
10
|
+
* mismatch results in silent 404 forever.
|
|
11
|
+
* 2. GET /mesh/<hash>?since=<lastSeenVersion>
|
|
12
|
+
* 3. `null` (304/404) → skip silently (no update, or owner never published)
|
|
13
|
+
* 4. Verify Ed25519 signature against the embedded `owner_pk`
|
|
14
|
+
* 5. Defense-in-depth: confirm the blob's `owner_pk` matches what we
|
|
15
|
+
* expected (otherwise a malicious relay could swap blobs across slots)
|
|
16
|
+
* 6. Anti-rollback: drop versions < our last-seen for this Owner
|
|
17
|
+
* 7. Membership check: decode every `members[].remote_epk` to bytes and
|
|
18
|
+
* compare against this Pi's pubkey bytes. Critical: comparing the
|
|
19
|
+
* base64 strings directly would falsely revoke when the app emits
|
|
20
|
+
* url-safe (`-`/`_`, no padding) and the Pi emits standard
|
|
21
|
+
* (`+`/`/`, padded) — same 32 bytes, different strings. See
|
|
22
|
+
* `encoding.ts` for the helpers and `plan/24` Wave 3 fix history.
|
|
23
|
+
* 8. If not a member → `storage.removePeer(ownerEpk)` and fire
|
|
24
|
+
* `onRevoke(ownerEpk)` so the caller can tear down any live WS
|
|
25
|
+
* sessions for that Owner.
|
|
26
|
+
*
|
|
27
|
+
* Backward-compat: an Owner who never published a mesh blob returns 404
|
|
28
|
+
* forever, which we treat as "no update". Old clients keep working
|
|
29
|
+
* untouched until they upgrade.
|
|
30
|
+
*
|
|
31
|
+
* Spec: plan/24-mesh-membership.md Wave 3.
|
|
32
|
+
*/
|
|
33
|
+
/** Minimum storage surface the poller needs. Concrete impl lives in
|
|
34
|
+
* `src/pairing/storage.ts` — injecting it via constructor keeps the class
|
|
35
|
+
* testable without filesystem mocking. */
|
|
36
|
+
export interface SelfRevokeStorage {
|
|
37
|
+
listOwnerPubkeys(): Promise<string[]>;
|
|
38
|
+
removePeer(remoteEpk: string): Promise<boolean>;
|
|
39
|
+
}
|
|
40
|
+
export interface SelfRevokeOptions {
|
|
41
|
+
client: MeshClient;
|
|
42
|
+
storage: SelfRevokeStorage;
|
|
43
|
+
/** This Pi's long-term Ed25519 pubkey, raw 32 bytes. */
|
|
44
|
+
myPubkey: Uint8Array;
|
|
45
|
+
/** Polling cadence. Default 60s — matches the app side (plan/24 Q1). */
|
|
46
|
+
intervalMs?: number;
|
|
47
|
+
/** Fired after `storage.removePeer` succeeds, so callers can tear down
|
|
48
|
+
* any active WS channel for the revoked owner. Receives the base64
|
|
49
|
+
* (standard) of the Owner pubkey that revoked us. */
|
|
50
|
+
onRevoke?: (ownerEpk: string) => void | Promise<void>;
|
|
51
|
+
/** Plan/25 Wave D: fired whenever the set of Pi-pubkeys present in any
|
|
52
|
+
* Owner's mesh_versions changes (membership added, removed, or
|
|
53
|
+
* relabeled). The callback receives the **union** of all current
|
|
54
|
+
* Pi-pubkeys across every known Owner, minus this Pi's own pubkey,
|
|
55
|
+
* so callers can keep `broker_remote.setSiblings()` in sync without
|
|
56
|
+
* re-running discovery themselves. Fires once per `checkOnce()` sweep
|
|
57
|
+
* only when the set genuinely differs from the previous sweep. */
|
|
58
|
+
onMembersChanged?: (siblings: SiblingInfo[]) => void | Promise<void>;
|
|
59
|
+
/** Logging surface — defaults to `console.*`. Tests inject a fake. */
|
|
60
|
+
log?: {
|
|
61
|
+
info(msg: string): void;
|
|
62
|
+
warn(msg: string): void;
|
|
63
|
+
error(msg: string): void;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Sibling info surfaced by `onMembersChanged`. Stays bit-identical to the
|
|
67
|
+
* shape `BrokerRemote.setSiblings` accepts so callers can pass through. */
|
|
68
|
+
export interface SiblingInfo {
|
|
69
|
+
pcLabel: string;
|
|
70
|
+
pcPubkey: string;
|
|
71
|
+
}
|
|
72
|
+
export declare class SelfRevoke {
|
|
73
|
+
private readonly client;
|
|
74
|
+
private readonly storage;
|
|
75
|
+
/** Raw Ed25519 pubkey bytes (32 B). Membership checks decode each
|
|
76
|
+
* `members[].remote_epk` and compare byte-wise — avoids the base64
|
|
77
|
+
* encoding-variant trap (standard vs url-safe). */
|
|
78
|
+
private readonly myPubkey;
|
|
79
|
+
private readonly intervalMs;
|
|
80
|
+
private readonly onRevoke?;
|
|
81
|
+
private readonly onMembersChanged?;
|
|
82
|
+
private readonly log;
|
|
83
|
+
/** Anti-rollback floor: never accept a version <= lastSeen per Owner. */
|
|
84
|
+
private readonly lastSeenVersion;
|
|
85
|
+
/** Plan/25 Wave D: snapshot of the sibling union from the previous
|
|
86
|
+
* sweep, used to detect changes without re-firing `onMembersChanged`
|
|
87
|
+
* on every poll. Keyed by `pcPubkey`. */
|
|
88
|
+
private prevSiblings;
|
|
89
|
+
/** Latest member raw data per owner, captured during `_checkOwner`.
|
|
90
|
+
* Stored as `(pcPubkey, nickname?)` (NOT pre-resolved label) so
|
|
91
|
+
* `_computeSiblingUnion` can pick the best nickname across owners
|
|
92
|
+
* — nickname always wins over fallback, regardless of owner iteration
|
|
93
|
+
* order. See `siblings.ts::discoverSiblings` for the same rule. */
|
|
94
|
+
private readonly membersByOwner;
|
|
95
|
+
private timer;
|
|
96
|
+
constructor(opts: SelfRevokeOptions);
|
|
97
|
+
/** Starts the periodic sweep. Idempotent — a second call is a no-op.
|
|
98
|
+
* Fires one sweep immediately so we don't wait `intervalMs` for the
|
|
99
|
+
* first check. */
|
|
100
|
+
start(): void;
|
|
101
|
+
/** Stops the periodic sweep. In-flight `checkOnce()` calls complete
|
|
102
|
+
* normally — only the timer is cleared. */
|
|
103
|
+
stop(): void;
|
|
104
|
+
/** One sweep across all known Owners. Per-Owner errors are logged but
|
|
105
|
+
* do not stop iteration — we want to keep checking other Owners even
|
|
106
|
+
* if one relay times out or one envelope is malformed. */
|
|
107
|
+
checkOnce(): Promise<void>;
|
|
108
|
+
private _computeSiblingUnion;
|
|
109
|
+
private _siblingSetChanged;
|
|
110
|
+
private _checkOwner;
|
|
111
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { verifyEnvelope } from "./verify.js";
|
|
3
|
+
import { bytesEqual, decodeB64Any } from "./encoding.js";
|
|
4
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
5
|
+
const FALLBACK_LABEL_LEN = 8;
|
|
6
|
+
export class SelfRevoke {
|
|
7
|
+
client;
|
|
8
|
+
storage;
|
|
9
|
+
/** Raw Ed25519 pubkey bytes (32 B). Membership checks decode each
|
|
10
|
+
* `members[].remote_epk` and compare byte-wise — avoids the base64
|
|
11
|
+
* encoding-variant trap (standard vs url-safe). */
|
|
12
|
+
myPubkey;
|
|
13
|
+
intervalMs;
|
|
14
|
+
onRevoke;
|
|
15
|
+
onMembersChanged;
|
|
16
|
+
log;
|
|
17
|
+
/** Anti-rollback floor: never accept a version <= lastSeen per Owner. */
|
|
18
|
+
lastSeenVersion = new Map();
|
|
19
|
+
/** Plan/25 Wave D: snapshot of the sibling union from the previous
|
|
20
|
+
* sweep, used to detect changes without re-firing `onMembersChanged`
|
|
21
|
+
* on every poll. Keyed by `pcPubkey`. */
|
|
22
|
+
prevSiblings = new Map();
|
|
23
|
+
/** Latest member raw data per owner, captured during `_checkOwner`.
|
|
24
|
+
* Stored as `(pcPubkey, nickname?)` (NOT pre-resolved label) so
|
|
25
|
+
* `_computeSiblingUnion` can pick the best nickname across owners
|
|
26
|
+
* — nickname always wins over fallback, regardless of owner iteration
|
|
27
|
+
* order. See `siblings.ts::discoverSiblings` for the same rule. */
|
|
28
|
+
membersByOwner = new Map();
|
|
29
|
+
timer = null;
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
this.client = opts.client;
|
|
32
|
+
this.storage = opts.storage;
|
|
33
|
+
this.myPubkey = opts.myPubkey;
|
|
34
|
+
this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
35
|
+
this.onRevoke = opts.onRevoke;
|
|
36
|
+
this.onMembersChanged = opts.onMembersChanged;
|
|
37
|
+
this.log = opts.log ?? {
|
|
38
|
+
info: (msg) => console.info(msg),
|
|
39
|
+
warn: (msg) => console.warn(msg),
|
|
40
|
+
error: (msg) => console.error(msg),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Starts the periodic sweep. Idempotent — a second call is a no-op.
|
|
44
|
+
* Fires one sweep immediately so we don't wait `intervalMs` for the
|
|
45
|
+
* first check. */
|
|
46
|
+
start() {
|
|
47
|
+
if (this.timer !== null)
|
|
48
|
+
return;
|
|
49
|
+
void this.checkOnce();
|
|
50
|
+
this.timer = setInterval(() => { void this.checkOnce(); }, this.intervalMs);
|
|
51
|
+
}
|
|
52
|
+
/** Stops the periodic sweep. In-flight `checkOnce()` calls complete
|
|
53
|
+
* normally — only the timer is cleared. */
|
|
54
|
+
stop() {
|
|
55
|
+
if (this.timer !== null) {
|
|
56
|
+
clearInterval(this.timer);
|
|
57
|
+
this.timer = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** One sweep across all known Owners. Per-Owner errors are logged but
|
|
61
|
+
* do not stop iteration — we want to keep checking other Owners even
|
|
62
|
+
* if one relay times out or one envelope is malformed. */
|
|
63
|
+
async checkOnce() {
|
|
64
|
+
const owners = await this.storage.listOwnerPubkeys();
|
|
65
|
+
for (const ownerEpk of owners) {
|
|
66
|
+
try {
|
|
67
|
+
await this._checkOwner(ownerEpk);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
this.log.error(`[mesh] self-revoke check failed for ${ownerEpk.slice(0, 8)}…: ${String(err)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Plan/25 Wave D: fire onMembersChanged if the union of siblings
|
|
74
|
+
// across all owners changed since the last sweep. Built outside the
|
|
75
|
+
// per-owner loop so a single owner removing a member doesn't fire
|
|
76
|
+
// until we've seen the other owners (which may still list that Pi
|
|
77
|
+
// and keep it as a sibling overall).
|
|
78
|
+
if (this.onMembersChanged) {
|
|
79
|
+
const union = this._computeSiblingUnion();
|
|
80
|
+
if (this._siblingSetChanged(union)) {
|
|
81
|
+
this.prevSiblings = union;
|
|
82
|
+
try {
|
|
83
|
+
await this.onMembersChanged([...union.values()]);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
this.log.error(`[mesh] onMembersChanged callback threw: ${String(err)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
_computeSiblingUnion() {
|
|
92
|
+
const myB64 = Buffer.from(this.myPubkey).toString("base64");
|
|
93
|
+
// pcPubkey → first nickname seen across owners (undefined when no
|
|
94
|
+
// owner has labeled this Pi yet).
|
|
95
|
+
const nicknames = new Map();
|
|
96
|
+
for (const members of this.membersByOwner.values()) {
|
|
97
|
+
for (const m of members) {
|
|
98
|
+
if (m.pcPubkey === myB64)
|
|
99
|
+
continue;
|
|
100
|
+
if (nicknames.get(m.pcPubkey))
|
|
101
|
+
continue; // existing nickname wins
|
|
102
|
+
if (m.nickname) {
|
|
103
|
+
nicknames.set(m.pcPubkey, m.nickname);
|
|
104
|
+
}
|
|
105
|
+
else if (!nicknames.has(m.pcPubkey)) {
|
|
106
|
+
nicknames.set(m.pcPubkey, undefined);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const out = new Map();
|
|
111
|
+
for (const [pcPubkey, nickname] of nicknames) {
|
|
112
|
+
out.set(pcPubkey, {
|
|
113
|
+
pcPubkey,
|
|
114
|
+
pcLabel: nickname ?? pcPubkey.slice(0, FALLBACK_LABEL_LEN),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
_siblingSetChanged(next) {
|
|
120
|
+
if (next.size !== this.prevSiblings.size)
|
|
121
|
+
return true;
|
|
122
|
+
for (const [pk, info] of next) {
|
|
123
|
+
const prior = this.prevSiblings.get(pk);
|
|
124
|
+
if (!prior)
|
|
125
|
+
return true;
|
|
126
|
+
if (prior.pcLabel !== info.pcLabel)
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
async _checkOwner(ownerEpk) {
|
|
132
|
+
const ownerPk = Uint8Array.from(Buffer.from(ownerEpk, "base64"));
|
|
133
|
+
// Lowercase hex per the cross-language contract (relay + app). Node's
|
|
134
|
+
// `digest('hex')` already produces lowercase by default.
|
|
135
|
+
const hash = createHash("sha256").update(ownerPk).digest("hex");
|
|
136
|
+
const since = this.lastSeenVersion.get(ownerEpk);
|
|
137
|
+
const env = await this.client.get(hash, since);
|
|
138
|
+
if (!env)
|
|
139
|
+
return; // 304 or 404 — nothing to do
|
|
140
|
+
const header = await verifyEnvelope(env);
|
|
141
|
+
// Defense-in-depth: a malicious relay could return a valid-but-different
|
|
142
|
+
// owner's envelope at our slot. The relay should reject this on upload
|
|
143
|
+
// (per plan/24), but we double-check here.
|
|
144
|
+
const headerOwnerB64 = Buffer.from(header.ownerPk).toString("base64");
|
|
145
|
+
if (headerOwnerB64 !== ownerEpk) {
|
|
146
|
+
this.log.warn(`[mesh] owner_pk mismatch for slot ${ownerEpk.slice(0, 8)}…: blob says ${headerOwnerB64.slice(0, 8)}… — ignoring`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const lastSeen = this.lastSeenVersion.get(ownerEpk) ?? 0;
|
|
150
|
+
if (header.version < lastSeen) {
|
|
151
|
+
this.log.warn(`[mesh] anti-rollback: dropped v${header.version} < lastSeen v${lastSeen} for ${ownerEpk.slice(0, 8)}…`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.lastSeenVersion.set(ownerEpk, header.version);
|
|
155
|
+
// Plan/25 Wave D: capture raw nickname (NOT pre-resolved label) per
|
|
156
|
+
// owner. `_computeSiblingUnion` picks the best label across owners
|
|
157
|
+
// — nickname always beats fallback. Pre-resolving here was the bug
|
|
158
|
+
// that produced different pc_labels on different Pis when Owner A
|
|
159
|
+
// labeled but Owner B didn't.
|
|
160
|
+
this.membersByOwner.set(ownerEpk, header.members.map((m) => ({
|
|
161
|
+
pcPubkey: m.remoteEpk,
|
|
162
|
+
...(m.nickname ? { nickname: m.nickname } : {}),
|
|
163
|
+
})));
|
|
164
|
+
// Decode every member's pubkey to bytes and compare against our own.
|
|
165
|
+
// The app may emit base64 url-safe (`-`/`_`, no padding) while the Pi
|
|
166
|
+
// emits standard (`+`/`/`, padded) — same bytes, different strings.
|
|
167
|
+
// String equality on those would falsely revoke us. See `encoding.ts`.
|
|
168
|
+
const stillMember = header.members.some((m) => bytesEqual(decodeB64Any(m.remoteEpk), this.myPubkey));
|
|
169
|
+
if (stillMember)
|
|
170
|
+
return;
|
|
171
|
+
// Log the EXACT version observed (from the relay's blob) plus the
|
|
172
|
+
// `since` cursor we sent — disambiguates poll-time state from any
|
|
173
|
+
// SQLite snapshot the operator might take later.
|
|
174
|
+
this.log.info(`[mesh] self-revoked from owner ${ownerEpk.slice(0, 8)}… ` +
|
|
175
|
+
`(received v${header.version}, since=${since ?? "<none>"}, ` +
|
|
176
|
+
`members=${header.members.length})`);
|
|
177
|
+
await this.storage.removePeer(ownerEpk);
|
|
178
|
+
if (this.onRevoke)
|
|
179
|
+
await this.onRevoke(ownerEpk);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=self_revoke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self_revoke.js","sourceRoot":"","sources":["../../src/mesh/self_revoke.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AA4EzD,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAE7B,MAAM,OAAO,UAAU;IACJ,MAAM,CAAa;IACnB,OAAO,CAAoB;IAC5C;;wDAEoD;IACnC,QAAQ,CAAa;IACrB,UAAU,CAAS;IACnB,QAAQ,CAAiC;IACzC,gBAAgB,CAAyC;IACzD,GAAG,CAAwC;IAC5D,yEAAyE;IACxD,eAAe,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7D;;8CAE0C;IAClC,YAAY,GAAG,IAAI,GAAG,EAAuB,CAAC;IACtD;;;;wEAIoE;IACnD,cAAc,GAAG,IAAI,GAAG,EAGtC,CAAC;IACI,KAAK,GAA0C,IAAI,CAAC;IAE5D,YAAY,IAAuB;QACjC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,mBAAmB,CAAC;QACzD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,CAAC;QAC9C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI;YACrB,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;YAChC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;YAChC,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;SACnC,CAAC;IACJ,CAAC;IAED;;uBAEmB;IACnB,KAAK;QACH,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI;YAAE,OAAO;QAChC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;QACtB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9E,CAAC;IAED;gDAC4C;IAC5C,IAAI;QACF,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YACxB,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;IAED;;+DAE2D;IAC3D,KAAK,CAAC,SAAS;QACb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC;QACrD,KAAK,MAAM,QAAQ,IAAI,MAAM,EAAE,CAAC;YAC9B,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YACnC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,GAAG,CAAC,KAAK,CACZ,uCAAuC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,EAAE,CAC/E,CAAC;YACJ,CAAC;QACH,CAAC;QAED,iEAAiE;QACjE,oEAAoE;QACpE,kEAAkE;QAClE,kEAAkE;QAClE,qCAAqC;QACrC,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC1C,IAAI,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;gBAC1B,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,gBAAgB,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;gBACnD,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,2CAA2C,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC3E,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAEO,oBAAoB;QAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC5D,kEAAkE;QAClE,kCAAkC;QAClC,MAAM,SAAS,GAAG,IAAI,GAAG,EAA8B,CAAC;QACxD,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,EAAE,CAAC;YACnD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;gBACxB,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK;oBAAE,SAAS;gBACnC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;oBAAE,SAAS,CAAE,yBAAyB;gBACnE,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;oBACf,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;gBACxC,CAAC;qBAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACtC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;gBACvC,CAAC;YACH,CAAC;QACH,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAC;QAC3C,KAAK,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,SAAS,EAAE,CAAC;YAC7C,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE;gBAChB,QAAQ;gBACR,OAAO,EAAE,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,kBAAkB,CAAC;aAC3D,CAAC,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAEO,kBAAkB,CAAC,IAA8B;QACvD,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,YAAY,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACtD,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACxC,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAC;YACxB,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;QAClD,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,QAAgB;QACxC,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;QACjE,sEAAsE;QACtE,yDAAyD;QACzD,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAChE,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAEjD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC/C,IAAI,CAAC,GAAG;YAAE,OAAO,CAAE,6BAA6B;QAEhD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;QAEzC,yEAAyE;QACzE,uEAAuE;QACvE,2CAA2C;QAC3C,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACtE,IAAI,cAAc,KAAK,QAAQ,EAAE,CAAC;YAChC,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,qCAAqC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,gBAAgB,cAAc,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,cAAc,CAClH,CAAC;YACF,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzD,IAAI,MAAM,CAAC,OAAO,GAAG,QAAQ,EAAE,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,kCAAkC,MAAM,CAAC,OAAO,gBAAgB,QAAQ,QAAQ,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CACxG,CAAC;YACF,OAAO;QACT,CAAC;QACD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QAEnD,oEAAoE;QACpE,mEAAmE;QACnE,mEAAmE;QACnE,kEAAkE;QAClE,8BAA8B;QAC9B,IAAI,CAAC,cAAc,CAAC,GAAG,CACrB,QAAQ,EACR,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzB,QAAQ,EAAE,CAAC,CAAC,SAAS;YACrB,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAChD,CAAC,CAAC,CACJ,CAAC;QAEF,qEAAqE;QACrE,sEAAsE;QACtE,oEAAoE;QACpE,uEAAuE;QACvE,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAC5C,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CACrD,CAAC;QACF,IAAI,WAAW;YAAE,OAAO;QAExB,kEAAkE;QAClE,kEAAkE;QAClE,iDAAiD;QACjD,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,kCAAkC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI;YAC1D,cAAc,MAAM,CAAC,OAAO,WAAW,KAAK,IAAI,QAAQ,IAAI;YAC5D,WAAW,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CACpC,CAAC;QACF,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACnD,CAAC;CACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { MeshClient } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Plan/25 — discover Pis-irmãos of every Owner this Pi is paired with.
|
|
4
|
+
*
|
|
5
|
+
* For each Owner pubkey (from `peers.json`), pulls the latest signed
|
|
6
|
+
* `mesh_versions` blob from the relay, verifies it, and walks the members
|
|
7
|
+
* list to extract all Pi-pubkeys other than this one. The same member may
|
|
8
|
+
* appear under multiple Owners — we de-dupe by Pi-pubkey.
|
|
9
|
+
*
|
|
10
|
+
* Returned `pcLabel` priority:
|
|
11
|
+
* 1. `member.nickname` (set by the Owner at pairing time)
|
|
12
|
+
* 2. First 8 chars of the base64-encoded Pi-pubkey (defensive fallback —
|
|
13
|
+
* keeps cross-PC addressing working even when nicknames are missing)
|
|
14
|
+
*
|
|
15
|
+
* Tolerates per-owner errors: a missing/malformed blob for one Owner does
|
|
16
|
+
* NOT prevent siblings of other Owners from being discovered. Logs and
|
|
17
|
+
* continues.
|
|
18
|
+
*/
|
|
19
|
+
export interface SiblingPi {
|
|
20
|
+
pcLabel: string;
|
|
21
|
+
pcPubkey: string;
|
|
22
|
+
}
|
|
23
|
+
export interface DiscoverSelfLabelResult {
|
|
24
|
+
/** This Pi's effective `pc_label` (nickname when any Owner has set one;
|
|
25
|
+
* pubkey prefix fallback otherwise). */
|
|
26
|
+
selfPcLabel: string;
|
|
27
|
+
}
|
|
28
|
+
export interface DiscoverOptions {
|
|
29
|
+
client: MeshClient;
|
|
30
|
+
ownerEpks: string[];
|
|
31
|
+
myPubkey: Uint8Array;
|
|
32
|
+
log?: {
|
|
33
|
+
warn(msg: string): void;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Derive the fallback label from a base64-encoded Pi pubkey. */
|
|
37
|
+
export declare function fallbackLabel(pcPubkey: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve self pc_label by scanning every Owner's mesh blob for an entry
|
|
40
|
+
* matching `myPubkey`. Returns the first nickname found; falls back to the
|
|
41
|
+
* base64 prefix when no Owner has labeled us.
|
|
42
|
+
*/
|
|
43
|
+
export declare function discoverSelfLabel(opts: DiscoverOptions): Promise<DiscoverSelfLabelResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Enumerate Pis-irmãos across all Owners. De-duplicated by `pcPubkey`.
|
|
46
|
+
* Excludes `myPubkey`.
|
|
47
|
+
*
|
|
48
|
+
* Label resolution rule (anti-asymmetry — see plan/25 Wave D fix):
|
|
49
|
+
* 1. Scan EVERY Owner blob first, collecting all distinct sibling
|
|
50
|
+
* pubkeys and any nicknames seen for each.
|
|
51
|
+
* 2. For each pubkey, pick label as: first nickname encountered (if
|
|
52
|
+
* any Owner labeled this Pi), else `fallbackLabel(pubkey)`.
|
|
53
|
+
*
|
|
54
|
+
* Why: if two Pis are paired to the same set of Owners but only some
|
|
55
|
+
* Owners labeled them, naive first-wins dedup can pick the unlabeled
|
|
56
|
+
* occurrence and discard the labeled one — producing different
|
|
57
|
+
* `pc_label`s between Pis (PC-A's `discoverSelfLabel` skips non-labeled,
|
|
58
|
+
* but old `discoverSiblings` didn't). The asymmetry triggers anti-spoof
|
|
59
|
+
* drops in `broker_remote.handleIncoming`. This rule keeps both sides in
|
|
60
|
+
* sync: nickname always wins over fallback.
|
|
61
|
+
*/
|
|
62
|
+
export declare function discoverSiblings(opts: DiscoverOptions): Promise<SiblingPi[]>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { verifyEnvelope } from "./verify.js";
|
|
3
|
+
import { bytesEqual, decodeB64Any } from "./encoding.js";
|
|
4
|
+
const FALLBACK_LABEL_LEN = 8;
|
|
5
|
+
/** Derive the fallback label from a base64-encoded Pi pubkey. */
|
|
6
|
+
export function fallbackLabel(pcPubkey) {
|
|
7
|
+
return pcPubkey.slice(0, FALLBACK_LABEL_LEN);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve self pc_label by scanning every Owner's mesh blob for an entry
|
|
11
|
+
* matching `myPubkey`. Returns the first nickname found; falls back to the
|
|
12
|
+
* base64 prefix when no Owner has labeled us.
|
|
13
|
+
*/
|
|
14
|
+
export async function discoverSelfLabel(opts) {
|
|
15
|
+
const log = opts.log ?? { warn: (m) => console.warn(m) };
|
|
16
|
+
const myB64 = Buffer.from(opts.myPubkey).toString("base64");
|
|
17
|
+
for (const ownerEpk of opts.ownerEpks) {
|
|
18
|
+
try {
|
|
19
|
+
const env = await _fetchOwnerBlob(opts.client, ownerEpk);
|
|
20
|
+
if (!env)
|
|
21
|
+
continue;
|
|
22
|
+
const header = await verifyEnvelope(env);
|
|
23
|
+
for (const m of header.members) {
|
|
24
|
+
if (bytesEqual(decodeB64Any(m.remoteEpk), opts.myPubkey) && m.nickname) {
|
|
25
|
+
return { selfPcLabel: m.nickname };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
log.warn(`[siblings] self-label fetch failed for owner ${ownerEpk.slice(0, 8)}…: ${String(err)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { selfPcLabel: fallbackLabel(myB64) };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Enumerate Pis-irmãos across all Owners. De-duplicated by `pcPubkey`.
|
|
37
|
+
* Excludes `myPubkey`.
|
|
38
|
+
*
|
|
39
|
+
* Label resolution rule (anti-asymmetry — see plan/25 Wave D fix):
|
|
40
|
+
* 1. Scan EVERY Owner blob first, collecting all distinct sibling
|
|
41
|
+
* pubkeys and any nicknames seen for each.
|
|
42
|
+
* 2. For each pubkey, pick label as: first nickname encountered (if
|
|
43
|
+
* any Owner labeled this Pi), else `fallbackLabel(pubkey)`.
|
|
44
|
+
*
|
|
45
|
+
* Why: if two Pis are paired to the same set of Owners but only some
|
|
46
|
+
* Owners labeled them, naive first-wins dedup can pick the unlabeled
|
|
47
|
+
* occurrence and discard the labeled one — producing different
|
|
48
|
+
* `pc_label`s between Pis (PC-A's `discoverSelfLabel` skips non-labeled,
|
|
49
|
+
* but old `discoverSiblings` didn't). The asymmetry triggers anti-spoof
|
|
50
|
+
* drops in `broker_remote.handleIncoming`. This rule keeps both sides in
|
|
51
|
+
* sync: nickname always wins over fallback.
|
|
52
|
+
*/
|
|
53
|
+
export async function discoverSiblings(opts) {
|
|
54
|
+
const log = opts.log ?? { warn: (m) => console.warn(m) };
|
|
55
|
+
// pcPubkey → first nickname seen across owners (or undefined if none).
|
|
56
|
+
const labels = new Map();
|
|
57
|
+
for (const ownerEpk of opts.ownerEpks) {
|
|
58
|
+
try {
|
|
59
|
+
const env = await _fetchOwnerBlob(opts.client, ownerEpk);
|
|
60
|
+
if (!env)
|
|
61
|
+
continue;
|
|
62
|
+
const header = await verifyEnvelope(env);
|
|
63
|
+
for (const m of header.members) {
|
|
64
|
+
if (bytesEqual(decodeB64Any(m.remoteEpk), opts.myPubkey))
|
|
65
|
+
continue;
|
|
66
|
+
const existing = labels.get(m.remoteEpk);
|
|
67
|
+
if (existing)
|
|
68
|
+
continue; // first-nickname wins; never overwrite a real label
|
|
69
|
+
if (m.nickname) {
|
|
70
|
+
labels.set(m.remoteEpk, m.nickname);
|
|
71
|
+
}
|
|
72
|
+
else if (!labels.has(m.remoteEpk)) {
|
|
73
|
+
// Seen pubkey for the first time, no nickname yet — record the
|
|
74
|
+
// slot so later iterations may upgrade it via the `if (existing)
|
|
75
|
+
// continue` check above only matching truthy nicknames.
|
|
76
|
+
labels.set(m.remoteEpk, undefined);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
log.warn(`[siblings] discover failed for owner ${ownerEpk.slice(0, 8)}…: ${String(err)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const [pcPubkey, nickname] of labels) {
|
|
86
|
+
out.push({ pcPubkey, pcLabel: nickname ?? fallbackLabel(pcPubkey) });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
async function _fetchOwnerBlob(client, ownerEpk) {
|
|
91
|
+
const ownerPk = Uint8Array.from(Buffer.from(ownerEpk, "base64"));
|
|
92
|
+
const hash = createHash("sha256").update(ownerPk).digest("hex");
|
|
93
|
+
return client.get(hash);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=siblings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"siblings.js","sourceRoot":"","sources":["../../src/mesh/siblings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh-membership types — pi-extension side. Mirrors the wire shape defined
|
|
3
|
+
* in plan/24-mesh-membership.md and must stay bit-compatible with the Dart
|
|
4
|
+
* (app) and Rust (relay) implementations of the same protocol.
|
|
5
|
+
*
|
|
6
|
+
* On the wire, JSON field names are `snake_case` (per the plan); this module
|
|
7
|
+
* exposes `camelCase` for ergonomic TS use. Conversion happens at the
|
|
8
|
+
* (de)serialization boundary in `verify.ts` / `canonical.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export interface MeshMember {
|
|
11
|
+
/** Pi long-term Ed25519 pubkey, base64 standard (matches PeerRecord.remote_epk). */
|
|
12
|
+
remoteEpk: string;
|
|
13
|
+
/** Relay URL where this member registers. */
|
|
14
|
+
relayUrl: string;
|
|
15
|
+
/** ISO-8601 timestamp of when the pairing happened. */
|
|
16
|
+
pairedAt: string;
|
|
17
|
+
/** Optional Owner-set label. */
|
|
18
|
+
nickname?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface MeshHeader {
|
|
21
|
+
/** Owner-scoped monotonic counter. Higher = newer. */
|
|
22
|
+
version: number;
|
|
23
|
+
/** Issued-at, ms since epoch. */
|
|
24
|
+
issuedAt: number;
|
|
25
|
+
/** Owner's Ed25519 pubkey, raw 32 bytes. */
|
|
26
|
+
ownerPk: Uint8Array;
|
|
27
|
+
members: MeshMember[];
|
|
28
|
+
}
|
|
29
|
+
export interface MeshEnvelope {
|
|
30
|
+
/** Canonical JSON bytes of the header (snake_case keys, sorted, no whitespace). */
|
|
31
|
+
blob: Uint8Array;
|
|
32
|
+
/** Ed25519 signature of `blob`, 64 bytes. */
|
|
33
|
+
sig: Uint8Array;
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh-membership types — pi-extension side. Mirrors the wire shape defined
|
|
3
|
+
* in plan/24-mesh-membership.md and must stay bit-compatible with the Dart
|
|
4
|
+
* (app) and Rust (relay) implementations of the same protocol.
|
|
5
|
+
*
|
|
6
|
+
* On the wire, JSON field names are `snake_case` (per the plan); this module
|
|
7
|
+
* exposes `camelCase` for ergonomic TS use. Conversion happens at the
|
|
8
|
+
* (de)serialization boundary in `verify.ts` / `canonical.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/mesh/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MeshEnvelope, MeshHeader } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verifies the Ed25519 signature on a mesh envelope and decodes the blob
|
|
4
|
+
* into a typed `MeshHeader`.
|
|
5
|
+
*
|
|
6
|
+
* The verification key is extracted *from the blob* (`owner_pk` field) —
|
|
7
|
+
* the caller MUST then check that `sha256(header.ownerPk)` matches the
|
|
8
|
+
* URL hash they queried with. Otherwise a malicious relay could serve a
|
|
9
|
+
* valid-but-different-owner blob at our hash slot.
|
|
10
|
+
*
|
|
11
|
+
* Throws on:
|
|
12
|
+
* - JSON parse failure
|
|
13
|
+
* - Missing or wrong-type required fields
|
|
14
|
+
* - `owner_pk` not 32 bytes
|
|
15
|
+
* - Signature mismatch
|
|
16
|
+
*/
|
|
17
|
+
export declare function verifyEnvelope(env: MeshEnvelope): Promise<MeshHeader>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ed25519Verify } from "../pairing/crypto.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verifies the Ed25519 signature on a mesh envelope and decodes the blob
|
|
4
|
+
* into a typed `MeshHeader`.
|
|
5
|
+
*
|
|
6
|
+
* The verification key is extracted *from the blob* (`owner_pk` field) —
|
|
7
|
+
* the caller MUST then check that `sha256(header.ownerPk)` matches the
|
|
8
|
+
* URL hash they queried with. Otherwise a malicious relay could serve a
|
|
9
|
+
* valid-but-different-owner blob at our hash slot.
|
|
10
|
+
*
|
|
11
|
+
* Throws on:
|
|
12
|
+
* - JSON parse failure
|
|
13
|
+
* - Missing or wrong-type required fields
|
|
14
|
+
* - `owner_pk` not 32 bytes
|
|
15
|
+
* - Signature mismatch
|
|
16
|
+
*/
|
|
17
|
+
export async function verifyEnvelope(env) {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(new TextDecoder().decode(env.blob));
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
throw new Error(`mesh: blob is not valid JSON: ${e.message}`);
|
|
24
|
+
}
|
|
25
|
+
if (!parsed || typeof parsed !== "object") {
|
|
26
|
+
throw new Error("mesh: blob is not a JSON object");
|
|
27
|
+
}
|
|
28
|
+
const o = parsed;
|
|
29
|
+
if (typeof o["owner_pk"] !== "string") {
|
|
30
|
+
throw new Error("mesh: owner_pk missing or not a string");
|
|
31
|
+
}
|
|
32
|
+
if (typeof o["version"] !== "number" || !Number.isInteger(o["version"])) {
|
|
33
|
+
throw new Error("mesh: version missing or not an integer");
|
|
34
|
+
}
|
|
35
|
+
if (typeof o["issued_at"] !== "number" || !Number.isInteger(o["issued_at"])) {
|
|
36
|
+
throw new Error("mesh: issued_at missing or not an integer");
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(o["members"])) {
|
|
39
|
+
throw new Error("mesh: members missing or not an array");
|
|
40
|
+
}
|
|
41
|
+
const ownerPk = Uint8Array.from(Buffer.from(o["owner_pk"], "base64"));
|
|
42
|
+
if (ownerPk.length !== 32) {
|
|
43
|
+
throw new Error(`mesh: owner_pk wrong length (${ownerPk.length}, expected 32)`);
|
|
44
|
+
}
|
|
45
|
+
if (!ed25519Verify(ownerPk, env.blob, env.sig)) {
|
|
46
|
+
throw new Error("mesh: signature verification failed");
|
|
47
|
+
}
|
|
48
|
+
const members = o["members"].map((raw, i) => {
|
|
49
|
+
if (!raw || typeof raw !== "object") {
|
|
50
|
+
throw new Error(`mesh: members[${i}] is not an object`);
|
|
51
|
+
}
|
|
52
|
+
const m = raw;
|
|
53
|
+
if (typeof m["remote_epk"] !== "string") {
|
|
54
|
+
throw new Error(`mesh: members[${i}].remote_epk invalid`);
|
|
55
|
+
}
|
|
56
|
+
if (typeof m["relay_url"] !== "string") {
|
|
57
|
+
throw new Error(`mesh: members[${i}].relay_url invalid`);
|
|
58
|
+
}
|
|
59
|
+
if (typeof m["paired_at"] !== "string") {
|
|
60
|
+
throw new Error(`mesh: members[${i}].paired_at invalid`);
|
|
61
|
+
}
|
|
62
|
+
const nickname = m["nickname"];
|
|
63
|
+
return {
|
|
64
|
+
remoteEpk: m["remote_epk"],
|
|
65
|
+
relayUrl: m["relay_url"],
|
|
66
|
+
pairedAt: m["paired_at"],
|
|
67
|
+
...(typeof nickname === "string" ? { nickname } : {}),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
version: o["version"],
|
|
72
|
+
issuedAt: o["issued_at"],
|
|
73
|
+
ownerPk,
|
|
74
|
+
members,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/mesh/verify.ts"],"names":[],"mappings":"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"}
|
package/dist/pairing/qr.d.ts
CHANGED
|
@@ -26,11 +26,22 @@ sessionName: string,
|
|
|
26
26
|
*/
|
|
27
27
|
roomId?: string): string;
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* Returns the QR ASCII as a string (pure Unicode block characters —
|
|
30
|
+
* `█ ▀ ▄` and space, NO ANSI escapes — qrcode-terminal v0.12 small mode
|
|
31
|
+
* is escape-free, see lib/main.js:48-53).
|
|
32
|
+
*
|
|
33
|
+
* The caller can either write the string to stderr (legacy path, breaks
|
|
34
|
+
* the Pi TUI layout) or inject it via `pi.sendMessage` (renders inside
|
|
35
|
+
* the chat panel as proper content).
|
|
36
|
+
*/
|
|
37
|
+
export declare function renderQRAscii(uri: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Legacy stderr writer — kept for the standalone CLI mode
|
|
40
|
+
* (`pi-extension/src/index.ts` bottom block, which runs outside a Pi TUI).
|
|
41
|
+
* Inside the Pi TUI extension flow, use `renderQRAscii` + `pi.sendMessage`
|
|
42
|
+
* instead — direct stderr writes from inside an extension break the TUI's
|
|
43
|
+
* scrollable output widget (the QR overflows the panel and other writes
|
|
44
|
+
* collide with the prompt area).
|
|
34
45
|
*/
|
|
35
46
|
export declare function displayQR(uri: string): void;
|
|
36
47
|
/**
|