@scp-dev/hub 0.4.0 → 0.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/dist/index.d.ts +20 -2
- package/dist/index.js +69 -8
- package/package.json +2 -2
- package/src/federation.ts +5 -3
- package/src/hub.ts +72 -6
- package/src/index.ts +3 -0
package/dist/index.d.ts
CHANGED
|
@@ -583,10 +583,14 @@ type GraphEdge = {
|
|
|
583
583
|
count: number;
|
|
584
584
|
last_at: string;
|
|
585
585
|
};
|
|
586
|
+
/** a machine as the graph draws it: `actor` is the member who bound it, null for the personal account's owner or a revoked name */
|
|
587
|
+
interface GraphMachine extends MachineRow {
|
|
588
|
+
actor: string | null;
|
|
589
|
+
}
|
|
586
590
|
interface HubGraph {
|
|
587
591
|
/** the window message edges count over */
|
|
588
592
|
minutes: number;
|
|
589
|
-
machines:
|
|
593
|
+
machines: GraphMachine[];
|
|
590
594
|
nodes: GraphNode[];
|
|
591
595
|
edges: GraphEdge[];
|
|
592
596
|
}
|
|
@@ -606,6 +610,7 @@ type DashboardEvent = {
|
|
|
606
610
|
kind: string;
|
|
607
611
|
from: string;
|
|
608
612
|
to: string[];
|
|
613
|
+
local?: true;
|
|
609
614
|
} | {
|
|
610
615
|
type: "endpoints";
|
|
611
616
|
at: string;
|
|
@@ -668,6 +673,14 @@ declare const HUB_LIMITS: {
|
|
|
668
673
|
type HubLimits = {
|
|
669
674
|
[K in keyof typeof HUB_LIMITS]: number;
|
|
670
675
|
};
|
|
676
|
+
/** kinds a bridge reports for a delivery its broker made on its own machine */
|
|
677
|
+
declare const LOCAL_EVENT_KINDS: readonly ["message", "request", "reply", "status", "handback", "spawn"];
|
|
678
|
+
/** events per `local_events` frame, recipients per event, and how far back an event's `at` may lie */
|
|
679
|
+
declare const LOCAL_EVENTS: {
|
|
680
|
+
readonly perFrame: 200;
|
|
681
|
+
readonly recipients: 64;
|
|
682
|
+
readonly maxAgeMs: 300000;
|
|
683
|
+
};
|
|
671
684
|
/** the hosted relay of one account: machines, bridges, direct clients, push and replay over a FederatedCore; pure, sockets are injected */
|
|
672
685
|
declare class RelayHub {
|
|
673
686
|
private readonly deps;
|
|
@@ -799,6 +812,11 @@ declare class RelayHub {
|
|
|
799
812
|
private deliverOn;
|
|
800
813
|
private readonly deliverJudged;
|
|
801
814
|
private delivered;
|
|
815
|
+
/**
|
|
816
|
+
* a bridge's metadata for deliveries its broker made on its own machine: counted for the graph and streamed to watchers and the
|
|
817
|
+
* dashboard as `local`; unknown endpoints and malformed events are skipped
|
|
818
|
+
*/
|
|
819
|
+
private localEvents;
|
|
802
820
|
/** the body's bytes when the account can hold them; refuses a send past the `storedBytes` limit */
|
|
803
821
|
private room;
|
|
804
822
|
/** bytes of message bodies held, and of the messages from other accounts */
|
|
@@ -865,4 +883,4 @@ declare class RelayHub {
|
|
|
865
883
|
xnotice(endpoint: string, text: string): void;
|
|
866
884
|
}
|
|
867
885
|
|
|
868
|
-
export { type DashboardEvent, type Deliver, type Delivery, FederatedCore, type FederatedOptions, GRAPH_MINUTES, GRAPH_MINUTES_MAX, type GraphEdge, type GraphNode, HUB_LIMITS, type HoldView, type HubAuth, type HubConn, type HubDeps, type HubGrant, type HubGraph, type HubJudgeOptions, type HubLimits, type HubRole, type HubSocket, JUDGE_LIMITS, type JudgeClientFactory, JudgeGate, type JudgeGateOptions, type JudgeLimits, type Origin as JudgeOrigin, MACHINE_NAME, type MachineRow, type MirrorCaps, type MirrorChange, type MirrorRules, type MirrorScope, type MirrorView, type RelayAuth, RelayHub, type RemoteView, type XEndpoint, dashboardEvent };
|
|
886
|
+
export { type DashboardEvent, type Deliver, type Delivery, FederatedCore, type FederatedOptions, GRAPH_MINUTES, GRAPH_MINUTES_MAX, type GraphEdge, type GraphMachine, type GraphNode, HUB_LIMITS, type HoldView, type HubAuth, type HubConn, type HubDeps, type HubGrant, type HubGraph, type HubJudgeOptions, type HubLimits, type HubRole, type HubSocket, JUDGE_LIMITS, type JudgeClientFactory, JudgeGate, type JudgeGateOptions, type JudgeLimits, type Origin as JudgeOrigin, LOCAL_EVENTS, LOCAL_EVENT_KINDS, MACHINE_NAME, type MachineRow, type MirrorCaps, type MirrorChange, type MirrorRules, type MirrorScope, type MirrorView, type RelayAuth, RelayHub, type RemoteView, type XEndpoint, dashboardEvent };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
SYSTEM_HANDLE,
|
|
21
21
|
seqList,
|
|
22
22
|
unreachable,
|
|
23
|
+
harnessFits,
|
|
24
|
+
keptHarness,
|
|
23
25
|
validHandle,
|
|
24
26
|
validHarness,
|
|
25
27
|
validSelector,
|
|
@@ -497,9 +499,9 @@ var FederatedCore = class extends RelayCore {
|
|
|
497
499
|
this.checkCap();
|
|
498
500
|
row = this.byId(this.insertRemote(`${machine}/${handle}`, "agent", harness, auth, now));
|
|
499
501
|
created = true;
|
|
500
|
-
} else if (held.kind === "agent" && held.connections === 0 && held.harness
|
|
502
|
+
} else if (held.kind === "agent" && held.connections === 0 && harnessFits(held.harness, harness)) {
|
|
501
503
|
row = held;
|
|
502
|
-
} else if (exact && held.kind === "agent" && held.token_hash !== null && held.process === null && held.session === null && held.parent_ep === null && held.harness
|
|
504
|
+
} else if (exact && held.kind === "agent" && held.token_hash !== null && held.process === null && held.session === null && held.parent_ep === null && harnessFits(held.harness, harness)) {
|
|
503
505
|
row = held;
|
|
504
506
|
joined = true;
|
|
505
507
|
} else if (held.kind === "agent" && held.connections === 0 && (held.last_seen ?? held.created_at) < now - LIMITS.handleBoundS * 1e3) {
|
|
@@ -519,7 +521,7 @@ var FederatedCore = class extends RelayCore {
|
|
|
519
521
|
const token = joined ? void 0 : `kre_${randomHex(32)}`;
|
|
520
522
|
this.sql.all(
|
|
521
523
|
`UPDATE endpoints SET harness = ?, model = ?, cwd = ?, parent = ?, labels = ?, token_hash = ${token ? "?" : "token_hash"}, auth = ?, connections = connections + 1, last_seen = ? WHERE id = ?`,
|
|
522
|
-
harness,
|
|
524
|
+
keptHarness(row.harness, harness),
|
|
523
525
|
fields.model,
|
|
524
526
|
fields.cwd,
|
|
525
527
|
fields.parent,
|
|
@@ -2017,7 +2019,8 @@ function dashboardEvent(event, at) {
|
|
|
2017
2019
|
if (event.type === "message") {
|
|
2018
2020
|
const from = str(event.from);
|
|
2019
2021
|
const to = Array.isArray(event.to) ? event.to.filter((a) => typeof a === "string") : [];
|
|
2020
|
-
|
|
2022
|
+
if (!from || !to.length) return null;
|
|
2023
|
+
return { type: "message", at, kind: str(event.kind) ?? "message", from, to, ...event.local === true ? { local: true } : {} };
|
|
2021
2024
|
}
|
|
2022
2025
|
if (event.type === "presence") {
|
|
2023
2026
|
const endpoint = str(event.endpoint);
|
|
@@ -2055,8 +2058,14 @@ var TOUCH_MS = 1e4;
|
|
|
2055
2058
|
var HUB_MIGRATION = [
|
|
2056
2059
|
`CREATE TABLE IF NOT EXISTS machines (name TEXT PRIMARY KEY, token_id TEXT NOT NULL, kind TEXT NOT NULL, created_at INTEGER NOT NULL, last_seen INTEGER)`,
|
|
2057
2060
|
`CREATE TABLE IF NOT EXISTS hub_state (key TEXT PRIMARY KEY, value INTEGER NOT NULL)`,
|
|
2058
|
-
`CREATE TABLE IF NOT EXISTS machine_tombstones (name TEXT PRIMARY KEY, actor_id TEXT, user_id TEXT, at INTEGER NOT NULL)
|
|
2061
|
+
`CREATE TABLE IF NOT EXISTS machine_tombstones (name TEXT PRIMARY KEY, actor_id TEXT, user_id TEXT, at INTEGER NOT NULL)`,
|
|
2062
|
+
`CREATE TABLE IF NOT EXISTS local_events (from_ep TEXT NOT NULL, to_ep TEXT NOT NULL, at INTEGER NOT NULL)`,
|
|
2063
|
+
`CREATE INDEX IF NOT EXISTS local_events_at ON local_events (at)`
|
|
2059
2064
|
];
|
|
2065
|
+
var LOCAL_EVENT_KINDS = ["message", "request", "reply", "status", "handback", "spawn"];
|
|
2066
|
+
var LOCAL_EVENTS = { perFrame: 200, recipients: 64, maxAgeMs: 3e5 };
|
|
2067
|
+
var LOCAL_KEEP_MS = 36e5;
|
|
2068
|
+
var HASH_ID = /^[0-9a-f]{8,64}$/;
|
|
2060
2069
|
var OBSERVER_METHODS = /* @__PURE__ */ new Set(["list", "snapshot", "watch", "log", "machines", "machines_rm"]);
|
|
2061
2070
|
var CLIENT_METHODS = /* @__PURE__ */ new Set(["whoami", "send", "ask", "ask_wait", "inbox", "ack", "status", "event", ...OBSERVER_METHODS]);
|
|
2062
2071
|
var iso2 = (ms) => ms === null ? null : new Date(ms).toISOString();
|
|
@@ -2335,6 +2344,7 @@ var RelayHub = class _RelayHub {
|
|
|
2335
2344
|
this.setState("last_validated", now);
|
|
2336
2345
|
if (now - (this.state("last_sweep") ?? 0) >= SWEEP_MS) {
|
|
2337
2346
|
this.relay.sweep();
|
|
2347
|
+
this.deps.sql.all(`DELETE FROM local_events WHERE at <= ?`, now - LOCAL_KEEP_MS);
|
|
2338
2348
|
this.expireMachines(now);
|
|
2339
2349
|
this.forgetTombstones(now);
|
|
2340
2350
|
const denied = this.deniedGrants();
|
|
@@ -2790,6 +2800,8 @@ var RelayHub = class _RelayHub {
|
|
|
2790
2800
|
this.fleet({ type: "activity", machine, ...row });
|
|
2791
2801
|
return row;
|
|
2792
2802
|
}
|
|
2803
|
+
case "local_events":
|
|
2804
|
+
return { recorded: this.localEvents(machine, params.events) };
|
|
2793
2805
|
case "list":
|
|
2794
2806
|
if (params.contacts === true) return { contacts: await this.contactList() };
|
|
2795
2807
|
return { endpoints: core.list(listFilters(params)) };
|
|
@@ -3137,6 +3149,47 @@ var RelayHub = class _RelayHub {
|
|
|
3137
3149
|
hop: result.hop
|
|
3138
3150
|
});
|
|
3139
3151
|
}
|
|
3152
|
+
/**
|
|
3153
|
+
* a bridge's metadata for deliveries its broker made on its own machine: counted for the graph and streamed to watchers and the
|
|
3154
|
+
* dashboard as `local`; unknown endpoints and malformed events are skipped
|
|
3155
|
+
*/
|
|
3156
|
+
localEvents(machine, value) {
|
|
3157
|
+
if (!Array.isArray(value) || value.length > LOCAL_EVENTS.perFrame)
|
|
3158
|
+
throw new RelayError3("invalid_params", `events is a list of at most ${LOCAL_EVENTS.perFrame}`);
|
|
3159
|
+
const core = this.relay;
|
|
3160
|
+
const now = this.now();
|
|
3161
|
+
const resolve = (handle) => {
|
|
3162
|
+
try {
|
|
3163
|
+
return core.remoteEndpoint(machine, handle);
|
|
3164
|
+
} catch {
|
|
3165
|
+
return null;
|
|
3166
|
+
}
|
|
3167
|
+
};
|
|
3168
|
+
let recorded = 0;
|
|
3169
|
+
for (const raw of value) {
|
|
3170
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
3171
|
+
const event = raw;
|
|
3172
|
+
const kind = LOCAL_EVENT_KINDS.includes(event.kind) ? event.kind : null;
|
|
3173
|
+
const at = typeof event.at === "number" && Number.isFinite(event.at) ? Math.min(Math.max(event.at, now - LOCAL_EVENTS.maxAgeMs), now) : null;
|
|
3174
|
+
const from = resolve(event.from);
|
|
3175
|
+
if (!kind || at === null || !from || !Array.isArray(event.to) || event.to.length > LOCAL_EVENTS.recipients) continue;
|
|
3176
|
+
const to = [...new Set(event.to.map(resolve).filter((id) => id !== null && id !== from))];
|
|
3177
|
+
if (!to.length) continue;
|
|
3178
|
+
for (const ep of to) this.deps.sql.all(`INSERT INTO local_events (from_ep, to_ep, at) VALUES (?, ?, ?)`, from, ep, at);
|
|
3179
|
+
this.fleet({
|
|
3180
|
+
type: "message",
|
|
3181
|
+
machine,
|
|
3182
|
+
local: true,
|
|
3183
|
+
...typeof event.id === "string" && HASH_ID.test(event.id) ? { id: event.id } : {},
|
|
3184
|
+
...typeof event.thread === "string" && HASH_ID.test(event.thread) ? { thread: event.thread } : {},
|
|
3185
|
+
kind,
|
|
3186
|
+
from: core.addressOf(from),
|
|
3187
|
+
to: to.map((ep) => core.addressOf(ep))
|
|
3188
|
+
});
|
|
3189
|
+
recorded++;
|
|
3190
|
+
}
|
|
3191
|
+
return recorded;
|
|
3192
|
+
}
|
|
3140
3193
|
/** the body's bytes when the account can hold them; refuses a send past the `storedBytes` limit */
|
|
3141
3194
|
room(sender, body) {
|
|
3142
3195
|
if (typeof body !== "string") return 0;
|
|
@@ -3354,9 +3407,13 @@ var RelayHub = class _RelayHub {
|
|
|
3354
3407
|
const edges = nodes.flatMap(
|
|
3355
3408
|
(n) => n.parent && n.parent !== n.address && listed.has(n.parent) ? [{ kind: "spawn", from: n.parent, to: n.address }] : []
|
|
3356
3409
|
);
|
|
3410
|
+
const since = this.now() - window * 6e4;
|
|
3357
3411
|
const pairs = this.deps.sql.all(
|
|
3358
|
-
`SELECT from_ep, to_ep, COUNT(*) AS n, MAX(at) AS last FROM
|
|
3359
|
-
|
|
3412
|
+
`SELECT from_ep, to_ep, COUNT(*) AS n, MAX(at) AS last FROM (
|
|
3413
|
+
SELECT from_ep, to_ep, at FROM pair_events WHERE at > ? UNION ALL SELECT from_ep, to_ep, at FROM local_events WHERE at > ?
|
|
3414
|
+
) GROUP BY from_ep, to_ep`,
|
|
3415
|
+
since,
|
|
3416
|
+
since
|
|
3360
3417
|
);
|
|
3361
3418
|
const traffic = pairs.flatMap((row) => {
|
|
3362
3419
|
const from = byId.get(row.from_ep);
|
|
@@ -3364,7 +3421,9 @@ var RelayHub = class _RelayHub {
|
|
|
3364
3421
|
return from && to && from !== to ? [{ kind: "message", from, to, count: row.n, last_at: iso2(row.last) }] : [];
|
|
3365
3422
|
});
|
|
3366
3423
|
edges.push(...traffic.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)));
|
|
3367
|
-
|
|
3424
|
+
const actors = new Map(this.deps.sql.all(`SELECT name, actor_id FROM machines`).map((r) => [r.name, r.actor_id]));
|
|
3425
|
+
const machines = this.machineList().map((m) => ({ ...m, actor: actors.get(m.name) ?? null }));
|
|
3426
|
+
return { minutes: window, machines, nodes, edges };
|
|
3368
3427
|
}
|
|
3369
3428
|
machineList() {
|
|
3370
3429
|
const sockets = this.deps.sockets().filter((s) => !this.gone.has(s.conn.id));
|
|
@@ -3522,6 +3581,8 @@ export {
|
|
|
3522
3581
|
HUB_LIMITS,
|
|
3523
3582
|
JUDGE_LIMITS,
|
|
3524
3583
|
JudgeGate,
|
|
3584
|
+
LOCAL_EVENTS,
|
|
3585
|
+
LOCAL_EVENT_KINDS,
|
|
3525
3586
|
MACHINE_NAME,
|
|
3526
3587
|
RelayHub,
|
|
3527
3588
|
dashboardEvent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scp-dev/hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Swarm Communication Protocol federation and account hub: mirrored endpoints across machines, bridges, direct clients and cross-account delivery.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://scp.dev",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"typecheck": "tsc -p ."
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@scp-dev/core": "0.1.
|
|
32
|
+
"@scp-dev/core": "0.1.5"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"tsup": "^8.5.1"
|
package/src/federation.ts
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
SYSTEM_HANDLE,
|
|
41
41
|
seqList,
|
|
42
42
|
unreachable,
|
|
43
|
+
harnessFits,
|
|
44
|
+
keptHarness,
|
|
43
45
|
validHandle,
|
|
44
46
|
validHarness,
|
|
45
47
|
validSelector,
|
|
@@ -734,7 +736,7 @@ export class FederatedCore extends RelayCore {
|
|
|
734
736
|
this.checkCap()
|
|
735
737
|
row = this.byId(this.insertRemote(`${machine}/${handle}`, "agent", harness, auth, now)) as FederatedRow
|
|
736
738
|
created = true
|
|
737
|
-
} else if (held.kind === "agent" && held.connections === 0 && held.harness
|
|
739
|
+
} else if (held.kind === "agent" && held.connections === 0 && harnessFits(held.harness, harness)) {
|
|
738
740
|
row = held
|
|
739
741
|
} else if (
|
|
740
742
|
exact &&
|
|
@@ -743,7 +745,7 @@ export class FederatedCore extends RelayCore {
|
|
|
743
745
|
held.process === null &&
|
|
744
746
|
held.session === null &&
|
|
745
747
|
held.parent_ep === null &&
|
|
746
|
-
held.harness
|
|
748
|
+
harnessFits(held.harness, harness)
|
|
747
749
|
) {
|
|
748
750
|
row = held
|
|
749
751
|
joined = true
|
|
@@ -764,7 +766,7 @@ export class FederatedCore extends RelayCore {
|
|
|
764
766
|
const token = joined ? undefined : `kre_${randomHex(32)}`
|
|
765
767
|
this.sql.all(
|
|
766
768
|
`UPDATE endpoints SET harness = ?, model = ?, cwd = ?, parent = ?, labels = ?, token_hash = ${token ? "?" : "token_hash"}, auth = ?, connections = connections + 1, last_seen = ? WHERE id = ?`,
|
|
767
|
-
harness,
|
|
769
|
+
keptHarness(row.harness, harness),
|
|
768
770
|
fields.model,
|
|
769
771
|
fields.cwd,
|
|
770
772
|
fields.parent,
|
package/src/hub.ts
CHANGED
|
@@ -111,10 +111,15 @@ export interface GraphNode {
|
|
|
111
111
|
/** `spawn`: parent to child; `message`: sender to recipient, with the count and newest send in the graph's window */
|
|
112
112
|
export type GraphEdge = { kind: "spawn"; from: string; to: string } | { kind: "message"; from: string; to: string; count: number; last_at: string }
|
|
113
113
|
|
|
114
|
+
/** a machine as the graph draws it: `actor` is the member who bound it, null for the personal account's owner or a revoked name */
|
|
115
|
+
export interface GraphMachine extends MachineRow {
|
|
116
|
+
actor: string | null
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
export interface HubGraph {
|
|
115
120
|
/** the window message edges count over */
|
|
116
121
|
minutes: number
|
|
117
|
-
machines:
|
|
122
|
+
machines: GraphMachine[]
|
|
118
123
|
nodes: GraphNode[]
|
|
119
124
|
edges: GraphEdge[]
|
|
120
125
|
}
|
|
@@ -126,7 +131,7 @@ export const GRAPH_MINUTES_MAX = 60
|
|
|
126
131
|
/** what a read-only dashboard stream sees as it happens: addresses, kinds and presence, never a body, note, summary or label */
|
|
127
132
|
export type DashboardEvent =
|
|
128
133
|
| { type: "presence"; at: string; machine: string | null; endpoint: string; presence: Presence }
|
|
129
|
-
| { type: "message"; at: string; kind: string; from: string; to: string[] }
|
|
134
|
+
| { type: "message"; at: string; kind: string; from: string; to: string[]; local?: true }
|
|
130
135
|
| { type: "endpoints"; at: string; machine: string }
|
|
131
136
|
|
|
132
137
|
const str = (v: unknown): string | null => (typeof v === "string" ? v : null)
|
|
@@ -136,7 +141,8 @@ export function dashboardEvent(event: Record<string, unknown>, at: string): Dash
|
|
|
136
141
|
if (event.type === "message") {
|
|
137
142
|
const from = str(event.from)
|
|
138
143
|
const to = Array.isArray(event.to) ? event.to.filter((a): a is string => typeof a === "string") : []
|
|
139
|
-
|
|
144
|
+
if (!from || !to.length) return null
|
|
145
|
+
return { type: "message", at, kind: str(event.kind) ?? "message", from, to, ...(event.local === true ? { local: true as const } : {}) }
|
|
140
146
|
}
|
|
141
147
|
if (event.type === "presence") {
|
|
142
148
|
const endpoint = str(event.endpoint)
|
|
@@ -218,8 +224,17 @@ const HUB_MIGRATION = [
|
|
|
218
224
|
`CREATE TABLE IF NOT EXISTS machines (name TEXT PRIMARY KEY, token_id TEXT NOT NULL, kind TEXT NOT NULL, created_at INTEGER NOT NULL, last_seen INTEGER)`,
|
|
219
225
|
`CREATE TABLE IF NOT EXISTS hub_state (key TEXT PRIMARY KEY, value INTEGER NOT NULL)`,
|
|
220
226
|
`CREATE TABLE IF NOT EXISTS machine_tombstones (name TEXT PRIMARY KEY, actor_id TEXT, user_id TEXT, at INTEGER NOT NULL)`,
|
|
227
|
+
`CREATE TABLE IF NOT EXISTS local_events (from_ep TEXT NOT NULL, to_ep TEXT NOT NULL, at INTEGER NOT NULL)`,
|
|
228
|
+
`CREATE INDEX IF NOT EXISTS local_events_at ON local_events (at)`,
|
|
221
229
|
]
|
|
222
230
|
|
|
231
|
+
/** kinds a bridge reports for a delivery its broker made on its own machine */
|
|
232
|
+
export const LOCAL_EVENT_KINDS = ["message", "request", "reply", "status", "handback", "spawn"] as const
|
|
233
|
+
/** events per `local_events` frame, recipients per event, and how far back an event's `at` may lie */
|
|
234
|
+
export const LOCAL_EVENTS = { perFrame: 200, recipients: 64, maxAgeMs: 300_000 } as const
|
|
235
|
+
const LOCAL_KEEP_MS = 3_600_000
|
|
236
|
+
const HASH_ID = /^[0-9a-f]{8,64}$/
|
|
237
|
+
|
|
223
238
|
const OBSERVER_METHODS = new Set(["list", "snapshot", "watch", "log", "machines", "machines_rm"])
|
|
224
239
|
const CLIENT_METHODS = new Set(["whoami", "send", "ask", "ask_wait", "inbox", "ack", "status", "event", ...OBSERVER_METHODS])
|
|
225
240
|
|
|
@@ -542,6 +557,7 @@ export class RelayHub {
|
|
|
542
557
|
this.setState("last_validated", now)
|
|
543
558
|
if (now - (this.state("last_sweep") ?? 0) >= SWEEP_MS) {
|
|
544
559
|
this.relay.sweep()
|
|
560
|
+
this.deps.sql.all(`DELETE FROM local_events WHERE at <= ?`, now - LOCAL_KEEP_MS)
|
|
545
561
|
this.expireMachines(now)
|
|
546
562
|
this.forgetTombstones(now)
|
|
547
563
|
const denied = this.deniedGrants()
|
|
@@ -1024,6 +1040,8 @@ export class RelayHub {
|
|
|
1024
1040
|
this.fleet({ type: "activity", machine, ...row })
|
|
1025
1041
|
return row
|
|
1026
1042
|
}
|
|
1043
|
+
case "local_events":
|
|
1044
|
+
return { recorded: this.localEvents(machine, params.events) }
|
|
1027
1045
|
case "list":
|
|
1028
1046
|
if (params.contacts === true) return { contacts: await this.contactList() }
|
|
1029
1047
|
return { endpoints: core.list(listFilters(params)) }
|
|
@@ -1405,6 +1423,48 @@ export class RelayHub {
|
|
|
1405
1423
|
})
|
|
1406
1424
|
}
|
|
1407
1425
|
|
|
1426
|
+
/**
|
|
1427
|
+
* a bridge's metadata for deliveries its broker made on its own machine: counted for the graph and streamed to watchers and the
|
|
1428
|
+
* dashboard as `local`; unknown endpoints and malformed events are skipped
|
|
1429
|
+
*/
|
|
1430
|
+
private localEvents(machine: string, value: unknown): number {
|
|
1431
|
+
if (!Array.isArray(value) || value.length > LOCAL_EVENTS.perFrame)
|
|
1432
|
+
throw new RelayError("invalid_params", `events is a list of at most ${LOCAL_EVENTS.perFrame}`)
|
|
1433
|
+
const core = this.relay
|
|
1434
|
+
const now = this.now()
|
|
1435
|
+
const resolve = (handle: unknown): string | null => {
|
|
1436
|
+
try {
|
|
1437
|
+
return core.remoteEndpoint(machine, handle)
|
|
1438
|
+
} catch {
|
|
1439
|
+
return null
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
let recorded = 0
|
|
1443
|
+
for (const raw of value) {
|
|
1444
|
+
if (typeof raw !== "object" || raw === null) continue
|
|
1445
|
+
const event = raw as Params
|
|
1446
|
+
const kind = (LOCAL_EVENT_KINDS as readonly unknown[]).includes(event.kind) ? (event.kind as string) : null
|
|
1447
|
+
const at = typeof event.at === "number" && Number.isFinite(event.at) ? Math.min(Math.max(event.at, now - LOCAL_EVENTS.maxAgeMs), now) : null
|
|
1448
|
+
const from = resolve(event.from)
|
|
1449
|
+
if (!kind || at === null || !from || !Array.isArray(event.to) || event.to.length > LOCAL_EVENTS.recipients) continue
|
|
1450
|
+
const to = [...new Set(event.to.map(resolve).filter((id): id is string => id !== null && id !== from))]
|
|
1451
|
+
if (!to.length) continue
|
|
1452
|
+
for (const ep of to) this.deps.sql.all(`INSERT INTO local_events (from_ep, to_ep, at) VALUES (?, ?, ?)`, from, ep, at)
|
|
1453
|
+
this.fleet({
|
|
1454
|
+
type: "message",
|
|
1455
|
+
machine,
|
|
1456
|
+
local: true,
|
|
1457
|
+
...(typeof event.id === "string" && HASH_ID.test(event.id) ? { id: event.id } : {}),
|
|
1458
|
+
...(typeof event.thread === "string" && HASH_ID.test(event.thread) ? { thread: event.thread } : {}),
|
|
1459
|
+
kind,
|
|
1460
|
+
from: core.addressOf(from),
|
|
1461
|
+
to: to.map((ep) => core.addressOf(ep)),
|
|
1462
|
+
})
|
|
1463
|
+
recorded++
|
|
1464
|
+
}
|
|
1465
|
+
return recorded
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1408
1468
|
/** the body's bytes when the account can hold them; refuses a send past the `storedBytes` limit */
|
|
1409
1469
|
private room(sender: string, body: unknown): number {
|
|
1410
1470
|
if (typeof body !== "string") return 0
|
|
@@ -1647,9 +1707,13 @@ export class RelayHub {
|
|
|
1647
1707
|
const edges: GraphEdge[] = nodes.flatMap((n) =>
|
|
1648
1708
|
n.parent && n.parent !== n.address && listed.has(n.parent) ? [{ kind: "spawn" as const, from: n.parent, to: n.address }] : [],
|
|
1649
1709
|
)
|
|
1710
|
+
const since = this.now() - window * 60_000
|
|
1650
1711
|
const pairs = this.deps.sql.all<{ from_ep: string; to_ep: string; n: number; last: number }>(
|
|
1651
|
-
`SELECT from_ep, to_ep, COUNT(*) AS n, MAX(at) AS last FROM
|
|
1652
|
-
|
|
1712
|
+
`SELECT from_ep, to_ep, COUNT(*) AS n, MAX(at) AS last FROM (
|
|
1713
|
+
SELECT from_ep, to_ep, at FROM pair_events WHERE at > ? UNION ALL SELECT from_ep, to_ep, at FROM local_events WHERE at > ?
|
|
1714
|
+
) GROUP BY from_ep, to_ep`,
|
|
1715
|
+
since,
|
|
1716
|
+
since,
|
|
1653
1717
|
)
|
|
1654
1718
|
const traffic: GraphEdge[] = pairs.flatMap((row) => {
|
|
1655
1719
|
const from = byId.get(row.from_ep)
|
|
@@ -1657,7 +1721,9 @@ export class RelayHub {
|
|
|
1657
1721
|
return from && to && from !== to ? [{ kind: "message" as const, from, to, count: row.n, last_at: iso(row.last)! }] : []
|
|
1658
1722
|
})
|
|
1659
1723
|
edges.push(...traffic.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)))
|
|
1660
|
-
|
|
1724
|
+
const actors = new Map(this.deps.sql.all<{ name: string; actor_id: string | null }>(`SELECT name, actor_id FROM machines`).map((r) => [r.name, r.actor_id]))
|
|
1725
|
+
const machines = this.machineList().map((m) => ({ ...m, actor: actors.get(m.name) ?? null }))
|
|
1726
|
+
return { minutes: window, machines, nodes, edges }
|
|
1661
1727
|
}
|
|
1662
1728
|
|
|
1663
1729
|
private machineList(): MachineRow[] {
|
package/src/index.ts
CHANGED