@scp-dev/broker 0.1.5 → 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/dist/index.d.ts +16 -1
- package/dist/index.js +62 -11
- package/dist/testing.d.ts +3 -1
- package/dist/testing.js +2 -1
- package/package.json +3 -3
- package/src/bridge.ts +56 -2
- package/src/server.ts +19 -1
- package/src/testing.ts +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ScopePreset, Claim, EndpointView, ScopeView, ScopeChange, Envelope, StopWaitResult, PeerView, WhyResult, SendParams, SendResult, ActivityRow, XListed, Selector } from '@scp-dev/core';
|
|
2
|
-
import { JudgeGate as JudgeGate$1, JudgeGateOptions, FederatedCore, Deliver, JudgeClientFactory } from '@scp-dev/hub';
|
|
2
|
+
import { LOCAL_EVENT_KINDS, JudgeGate as JudgeGate$1, JudgeGateOptions, FederatedCore, Deliver, JudgeClientFactory } from '@scp-dev/hub';
|
|
3
3
|
export { Deliver } from '@scp-dev/hub';
|
|
4
4
|
|
|
5
5
|
interface Hello {
|
|
@@ -203,6 +203,10 @@ interface BridgeConfig {
|
|
|
203
203
|
redact?: boolean | undefined;
|
|
204
204
|
/** let messages marked private leave this machine */
|
|
205
205
|
privateOk?: boolean | undefined;
|
|
206
|
+
/** report metadata of deliveries made on this machine to the hub, never bodies (default true) */
|
|
207
|
+
mirrorLocal?: boolean | undefined;
|
|
208
|
+
/** how often reported local deliveries are sent, batched (default 1 s) */
|
|
209
|
+
localFlushMs?: number | undefined;
|
|
206
210
|
/** a WebSocket constructor (tests) */
|
|
207
211
|
webSocket?: WebSocketWithHeaders | undefined;
|
|
208
212
|
/** reconnect backoff bounds (default 1 s to 60 s) */
|
|
@@ -236,6 +240,8 @@ interface Bridge {
|
|
|
236
240
|
renamed(endpoint: string, was: string): void;
|
|
237
241
|
/** a local endpoint's activity, posted to the hub */
|
|
238
242
|
activity(endpoint: string, row: ActivityRow): void;
|
|
243
|
+
/** a delivery between endpoints of this machine: its metadata is reported to the hub in the next batch */
|
|
244
|
+
local?(event: LocalDelivery): void;
|
|
239
245
|
/** the endpoints of other accounts this account's contacts let it reach */
|
|
240
246
|
contacts(): Promise<XListed[]>;
|
|
241
247
|
status(): BridgeStatus;
|
|
@@ -243,6 +249,15 @@ interface Bridge {
|
|
|
243
249
|
relink(): void;
|
|
244
250
|
stop(): Promise<void>;
|
|
245
251
|
}
|
|
252
|
+
/** a delivery the broker made on its own machine, by local endpoint ids */
|
|
253
|
+
interface LocalDelivery {
|
|
254
|
+
id: string;
|
|
255
|
+
thread: string;
|
|
256
|
+
kind: (typeof LOCAL_EVENT_KINDS)[number];
|
|
257
|
+
from: string;
|
|
258
|
+
to: string[];
|
|
259
|
+
at: number;
|
|
260
|
+
}
|
|
246
261
|
/** a WebSocket constructor that takes request headers, as Bun's and Node's do */
|
|
247
262
|
type WebSocketWithHeaders = new (url: string, options: {
|
|
248
263
|
headers: Record<string, string>;
|
package/dist/index.js
CHANGED
|
@@ -524,6 +524,7 @@ async function connectBroker(dir, start, timeoutMs = 5e3) {
|
|
|
524
524
|
}
|
|
525
525
|
|
|
526
526
|
// src/bridge.ts
|
|
527
|
+
import { createHash as createHash2 } from "crypto";
|
|
527
528
|
import {
|
|
528
529
|
findSecret,
|
|
529
530
|
matches,
|
|
@@ -532,7 +533,9 @@ import {
|
|
|
532
533
|
RelayError as RelayError2,
|
|
533
534
|
XLIMITS
|
|
534
535
|
} from "@scp-dev/core";
|
|
535
|
-
import { HUB_LIMITS } from "@scp-dev/hub";
|
|
536
|
+
import { HUB_LIMITS, LOCAL_EVENTS } from "@scp-dev/hub";
|
|
537
|
+
var LOCAL_BUFFER = 1e3;
|
|
538
|
+
var idHash = (id) => createHash2("sha256").update(id).digest("hex").slice(0, 32);
|
|
536
539
|
var CONNECT_TIMEOUT_MS = 15e3;
|
|
537
540
|
var MIRROR_FRAME_BYTES = HUB_LIMITS.frameBytes - 1024;
|
|
538
541
|
var DEAD_LETTER_ATTEMPTS = 3;
|
|
@@ -578,6 +581,10 @@ function startBridge(options) {
|
|
|
578
581
|
const inflight = /* @__PURE__ */ new Set();
|
|
579
582
|
const injectFailures = /* @__PURE__ */ new Map();
|
|
580
583
|
const fromHub = /* @__PURE__ */ new WeakSet();
|
|
584
|
+
const mirrorLocal = options.mirrorLocal !== false;
|
|
585
|
+
const localFlushMs = options.localFlushMs ?? 1e3;
|
|
586
|
+
let localQueue = [];
|
|
587
|
+
let localTimer;
|
|
581
588
|
const setState = (up, error) => {
|
|
582
589
|
if (up !== connected || error !== lastError) since = (/* @__PURE__ */ new Date()).toISOString();
|
|
583
590
|
connected = up;
|
|
@@ -887,21 +894,21 @@ function startBridge(options) {
|
|
|
887
894
|
if (event.machine === machine) return;
|
|
888
895
|
if (event.type === "message" && Array.isArray(event.to) && account && event.to.some((to) => typeof to === "string" && to.startsWith(`${account}/${machine}/`)))
|
|
889
896
|
return;
|
|
890
|
-
const
|
|
897
|
+
const local2 = {
|
|
891
898
|
...event,
|
|
892
899
|
...event.from !== void 0 ? { from: localAddress(event.from) } : {},
|
|
893
900
|
...event.endpoint !== void 0 ? { endpoint: localAddress(event.endpoint) } : {}
|
|
894
901
|
};
|
|
895
|
-
if (Array.isArray(event.to))
|
|
902
|
+
if (Array.isArray(event.to)) local2.to = event.to.map(localAddress);
|
|
896
903
|
if (event.type === "activity" && typeof event.machine === "string" && typeof event.endpoint === "string") {
|
|
897
904
|
try {
|
|
898
905
|
const proxy = core.remoteEndpoint(event.machine, event.endpoint.slice(event.endpoint.lastIndexOf("/") + 1));
|
|
899
906
|
const row = core.event(proxy, { kind: event.kind, tool: event.tool, summary: event.summary });
|
|
900
|
-
|
|
907
|
+
local2.id = row.id;
|
|
901
908
|
} catch {
|
|
902
909
|
}
|
|
903
910
|
}
|
|
904
|
-
hooks.fleet(
|
|
911
|
+
hooks.fleet(local2);
|
|
905
912
|
}
|
|
906
913
|
function linkUp(ms) {
|
|
907
914
|
if (connected) return Promise.resolve(true);
|
|
@@ -1017,6 +1024,30 @@ function startBridge(options) {
|
|
|
1017
1024
|
if (handle === null) return;
|
|
1018
1025
|
fireAndForget("event", { from: handle, kind: row.kind, tool: row.tool ?? void 0, summary: options.redact ? void 0 : row.summary ?? void 0 });
|
|
1019
1026
|
}
|
|
1027
|
+
function local(event) {
|
|
1028
|
+
if (!mirrorLocal || !ready || localQueue.length >= LOCAL_BUFFER) return;
|
|
1029
|
+
let from;
|
|
1030
|
+
let to;
|
|
1031
|
+
try {
|
|
1032
|
+
from = handleOf(event.from);
|
|
1033
|
+
to = event.to.flatMap((endpoint) => {
|
|
1034
|
+
const handle = handleOf(endpoint);
|
|
1035
|
+
return handle === null ? [] : [handle];
|
|
1036
|
+
});
|
|
1037
|
+
} catch {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (from === null || !to.length) return;
|
|
1041
|
+
localQueue.push({ from, to: to.slice(0, LOCAL_EVENTS.recipients), kind: event.kind, at: event.at, id: idHash(event.id), thread: idHash(event.thread) });
|
|
1042
|
+
localTimer ??= setTimeout(flushLocal, localFlushMs);
|
|
1043
|
+
}
|
|
1044
|
+
function flushLocal() {
|
|
1045
|
+
localTimer = void 0;
|
|
1046
|
+
const batch = localQueue;
|
|
1047
|
+
localQueue = [];
|
|
1048
|
+
if (!ready) return;
|
|
1049
|
+
for (let i = 0; i < batch.length; i += LOCAL_EVENTS.perFrame) fireAndForget("local_events", { events: batch.slice(i, i + LOCAL_EVENTS.perFrame) });
|
|
1050
|
+
}
|
|
1020
1051
|
async function contacts() {
|
|
1021
1052
|
if (!connected) throw unavailable();
|
|
1022
1053
|
return (await request("list", { contacts: true })).contacts;
|
|
@@ -1038,6 +1069,8 @@ function startBridge(options) {
|
|
|
1038
1069
|
stopped = true;
|
|
1039
1070
|
shutDown = true;
|
|
1040
1071
|
clearTimeout(retryTimer);
|
|
1072
|
+
clearTimeout(localTimer);
|
|
1073
|
+
localQueue = [];
|
|
1041
1074
|
for (const fn of waitingForLink.splice(0)) fn();
|
|
1042
1075
|
const socket = ws;
|
|
1043
1076
|
ws = null;
|
|
@@ -1048,11 +1081,11 @@ function startBridge(options) {
|
|
|
1048
1081
|
if (socket && socket.readyState <= 1) socket.close(1e3, "the broker stopped");
|
|
1049
1082
|
}
|
|
1050
1083
|
connect();
|
|
1051
|
-
return { forward, changed, resync, renamed, activity, contacts, status, relink, stop };
|
|
1084
|
+
return { forward, changed, resync, renamed, activity, local, contacts, status, relink, stop };
|
|
1052
1085
|
}
|
|
1053
1086
|
|
|
1054
1087
|
// src/server.ts
|
|
1055
|
-
import { createHash as
|
|
1088
|
+
import { createHash as createHash4 } from "crypto";
|
|
1056
1089
|
import { chmodSync as chmodSync2, existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync6, realpathSync, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
1057
1090
|
import { createServer } from "net";
|
|
1058
1091
|
import { homedir as homedir2 } from "os";
|
|
@@ -1089,11 +1122,11 @@ var JudgeGate = class extends HubJudgeGate {
|
|
|
1089
1122
|
};
|
|
1090
1123
|
|
|
1091
1124
|
// src/sessions.ts
|
|
1092
|
-
import { createHash as
|
|
1125
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
1093
1126
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync, writeFileSync } from "fs";
|
|
1094
1127
|
import { join as join3 } from "path";
|
|
1095
1128
|
function markerPath(dir, session) {
|
|
1096
|
-
return join3(dir, "sessions", `${
|
|
1129
|
+
return join3(dir, "sessions", `${createHash3("sha256").update(session).digest("hex").slice(0, 32)}.json`);
|
|
1097
1130
|
}
|
|
1098
1131
|
function readSessionMarker(dir, session) {
|
|
1099
1132
|
try {
|
|
@@ -1282,7 +1315,7 @@ async function startBroker(options) {
|
|
|
1282
1315
|
core = new FederatedCore(sql, {
|
|
1283
1316
|
...compact({ user: options.user, machine, now: options.now }),
|
|
1284
1317
|
hostname: shortHostname(),
|
|
1285
|
-
hash: (value) =>
|
|
1318
|
+
hash: (value) => createHash4("sha256").update(value).digest("hex"),
|
|
1286
1319
|
transaction: (fn) => db.transaction(fn),
|
|
1287
1320
|
alive: running
|
|
1288
1321
|
});
|
|
@@ -1981,13 +2014,23 @@ async function startBroker(options) {
|
|
|
1981
2014
|
}
|
|
1982
2015
|
const deliver = async (from, send, origin) => {
|
|
1983
2016
|
let result;
|
|
1984
|
-
|
|
2017
|
+
const remote = remoteSend(core, send.to);
|
|
2018
|
+
if (remote) {
|
|
1985
2019
|
if (!bridge)
|
|
1986
2020
|
throw new RelayError3("not_found", `no endpoint ${[send.to].flat().join(", ")}: this broker is not linked to a hub (${CLI_NAME} cloud enable)`);
|
|
1987
2021
|
result = await bridge.forward(from, send, origin);
|
|
1988
2022
|
} else {
|
|
1989
2023
|
result = core.send(from, send, origin);
|
|
1990
2024
|
}
|
|
2025
|
+
if (!result.duplicate && !remote)
|
|
2026
|
+
bridge?.local?.({
|
|
2027
|
+
id: result.id,
|
|
2028
|
+
thread: result.thread,
|
|
2029
|
+
kind: send.kind ?? "message",
|
|
2030
|
+
from,
|
|
2031
|
+
to: result.deliveries.map((d) => d.endpoint),
|
|
2032
|
+
at: (options.now ?? Date.now)()
|
|
2033
|
+
});
|
|
1991
2034
|
if (!result.duplicate) {
|
|
1992
2035
|
for (const delivery of result.deliveries) wake(delivery.endpoint);
|
|
1993
2036
|
notifyFleet({
|
|
@@ -2113,6 +2156,14 @@ async function startBroker(options) {
|
|
|
2113
2156
|
if (result.handback) {
|
|
2114
2157
|
const message = core.delivered(spawner, result.handback);
|
|
2115
2158
|
wake(spawner);
|
|
2159
|
+
bridge?.local?.({
|
|
2160
|
+
id: message.id,
|
|
2161
|
+
thread: message.thread,
|
|
2162
|
+
kind: "handback",
|
|
2163
|
+
from: message.from.endpoint,
|
|
2164
|
+
to: [spawner],
|
|
2165
|
+
at: (options.now ?? Date.now)()
|
|
2166
|
+
});
|
|
2116
2167
|
notifyFleet({
|
|
2117
2168
|
type: "message",
|
|
2118
2169
|
id: message.id,
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RelayHub, HubSocket } from '@scp-dev/hub';
|
|
1
|
+
import { RelayHub, HubSocket, DashboardEvent } from '@scp-dev/hub';
|
|
2
2
|
|
|
3
3
|
interface FakeToken {
|
|
4
4
|
tokenId: string;
|
|
@@ -27,6 +27,8 @@ declare function startFakeHubServer(options: {
|
|
|
27
27
|
tokens: Record<string, FakeToken>;
|
|
28
28
|
now?: (() => number) | undefined;
|
|
29
29
|
contacts?: [string, string][] | undefined;
|
|
30
|
+
/** receives every account's dashboard events */
|
|
31
|
+
dashboard?: ((account: string, event: DashboardEvent) => void) | undefined;
|
|
30
32
|
}): FakeHubServer;
|
|
31
33
|
|
|
32
34
|
export { type FakeHubServer, type FakeToken, startFakeHubServer };
|
package/dist/testing.js
CHANGED
|
@@ -37,7 +37,8 @@ function startFakeHubServer(options) {
|
|
|
37
37
|
sockets: () => [...sockets.values()],
|
|
38
38
|
save: () => {
|
|
39
39
|
},
|
|
40
|
-
...options.contacts ? { crossAccount: linkOf(name) } : {}
|
|
40
|
+
...options.contacts ? { crossAccount: linkOf(name) } : {},
|
|
41
|
+
...options.dashboard ? { dashboard: (event) => options.dashboard(name, event) } : {}
|
|
41
42
|
});
|
|
42
43
|
hub.init({ user: name });
|
|
43
44
|
found = { hub, sockets, traps: /* @__PURE__ */ new Map() };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scp-dev/broker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Swarm Communication Protocol local broker: a unix socket JSON-RPC daemon over the relay core, its client and the bridge to an account hub.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://scp.dev",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"typecheck": "tsc -p ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@scp-dev/core": "0.1.
|
|
41
|
-
"@scp-dev/hub": "0.
|
|
40
|
+
"@scp-dev/core": "0.1.5",
|
|
41
|
+
"@scp-dev/hub": "0.5.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"tsup": "^8.5.1"
|
package/src/bridge.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
1
2
|
import {
|
|
2
3
|
type ActivityRow,
|
|
3
4
|
type EndpointView,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
XLIMITS,
|
|
14
15
|
type XListed,
|
|
15
16
|
} from "@scp-dev/core"
|
|
16
|
-
import { type FederatedCore, HUB_LIMITS, type RemoteView } from "@scp-dev/hub"
|
|
17
|
+
import { type FederatedCore, HUB_LIMITS, type LOCAL_EVENT_KINDS, LOCAL_EVENTS, type RemoteView } from "@scp-dev/hub"
|
|
17
18
|
|
|
18
19
|
/** what the broker gives the bridge: waking local waiters, telling local watchers, the local policy's state */
|
|
19
20
|
export interface BridgeHooks {
|
|
@@ -42,6 +43,10 @@ export interface BridgeConfig {
|
|
|
42
43
|
redact?: boolean | undefined
|
|
43
44
|
/** let messages marked private leave this machine */
|
|
44
45
|
privateOk?: boolean | undefined
|
|
46
|
+
/** report metadata of deliveries made on this machine to the hub, never bodies (default true) */
|
|
47
|
+
mirrorLocal?: boolean | undefined
|
|
48
|
+
/** how often reported local deliveries are sent, batched (default 1 s) */
|
|
49
|
+
localFlushMs?: number | undefined
|
|
45
50
|
/** a WebSocket constructor (tests) */
|
|
46
51
|
webSocket?: WebSocketWithHeaders | undefined
|
|
47
52
|
/** reconnect backoff bounds (default 1 s to 60 s) */
|
|
@@ -77,6 +82,8 @@ export interface Bridge {
|
|
|
77
82
|
renamed(endpoint: string, was: string): void
|
|
78
83
|
/** a local endpoint's activity, posted to the hub */
|
|
79
84
|
activity(endpoint: string, row: ActivityRow): void
|
|
85
|
+
/** a delivery between endpoints of this machine: its metadata is reported to the hub in the next batch */
|
|
86
|
+
local?(event: LocalDelivery): void
|
|
80
87
|
/** the endpoints of other accounts this account's contacts let it reach */
|
|
81
88
|
contacts(): Promise<XListed[]>
|
|
82
89
|
status(): BridgeStatus
|
|
@@ -85,6 +92,21 @@ export interface Bridge {
|
|
|
85
92
|
stop(): Promise<void>
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
/** a delivery the broker made on its own machine, by local endpoint ids */
|
|
96
|
+
export interface LocalDelivery {
|
|
97
|
+
id: string
|
|
98
|
+
thread: string
|
|
99
|
+
kind: (typeof LOCAL_EVENT_KINDS)[number]
|
|
100
|
+
from: string
|
|
101
|
+
to: string[]
|
|
102
|
+
at: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** local deliveries waiting for the next batch; past this, new ones are dropped until it is sent */
|
|
106
|
+
const LOCAL_BUFFER = 1000
|
|
107
|
+
|
|
108
|
+
const idHash = (id: string): string => createHash("sha256").update(id).digest("hex").slice(0, 32)
|
|
109
|
+
|
|
88
110
|
/** a WebSocket constructor that takes request headers, as Bun's and Node's do */
|
|
89
111
|
export type WebSocketWithHeaders = new (url: string, options: { headers: Record<string, string> }) => WebSocket
|
|
90
112
|
|
|
@@ -153,6 +175,10 @@ export function startBridge(options: BridgeOptions): Bridge {
|
|
|
153
175
|
const inflight = new Set<number>()
|
|
154
176
|
const injectFailures = new Map<number, number>()
|
|
155
177
|
const fromHub = new WeakSet<Error>()
|
|
178
|
+
const mirrorLocal = options.mirrorLocal !== false
|
|
179
|
+
const localFlushMs = options.localFlushMs ?? 1000
|
|
180
|
+
let localQueue: object[] = []
|
|
181
|
+
let localTimer: ReturnType<typeof setTimeout> | undefined
|
|
156
182
|
|
|
157
183
|
const setState = (up: boolean, error: string | null) => {
|
|
158
184
|
if (up !== connected || error !== lastError) since = new Date().toISOString()
|
|
@@ -636,6 +662,32 @@ export function startBridge(options: BridgeOptions): Bridge {
|
|
|
636
662
|
fireAndForget("event", { from: handle, kind: row.kind, tool: row.tool ?? undefined, summary: options.redact ? undefined : (row.summary ?? undefined) })
|
|
637
663
|
}
|
|
638
664
|
|
|
665
|
+
function local(event: LocalDelivery) {
|
|
666
|
+
if (!mirrorLocal || !ready || localQueue.length >= LOCAL_BUFFER) return
|
|
667
|
+
let from: string | null
|
|
668
|
+
let to: string[]
|
|
669
|
+
try {
|
|
670
|
+
from = handleOf(event.from)
|
|
671
|
+
to = event.to.flatMap((endpoint) => {
|
|
672
|
+
const handle = handleOf(endpoint)
|
|
673
|
+
return handle === null ? [] : [handle]
|
|
674
|
+
})
|
|
675
|
+
} catch {
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
if (from === null || !to.length) return
|
|
679
|
+
localQueue.push({ from, to: to.slice(0, LOCAL_EVENTS.recipients), kind: event.kind, at: event.at, id: idHash(event.id), thread: idHash(event.thread) })
|
|
680
|
+
localTimer ??= setTimeout(flushLocal, localFlushMs)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function flushLocal() {
|
|
684
|
+
localTimer = undefined
|
|
685
|
+
const batch = localQueue
|
|
686
|
+
localQueue = []
|
|
687
|
+
if (!ready) return
|
|
688
|
+
for (let i = 0; i < batch.length; i += LOCAL_EVENTS.perFrame) fireAndForget("local_events", { events: batch.slice(i, i + LOCAL_EVENTS.perFrame) })
|
|
689
|
+
}
|
|
690
|
+
|
|
639
691
|
async function contacts(): Promise<XListed[]> {
|
|
640
692
|
if (!connected) throw unavailable()
|
|
641
693
|
return (await request<{ contacts: XListed[] }>("list", { contacts: true })).contacts
|
|
@@ -660,6 +712,8 @@ export function startBridge(options: BridgeOptions): Bridge {
|
|
|
660
712
|
stopped = true
|
|
661
713
|
shutDown = true
|
|
662
714
|
clearTimeout(retryTimer)
|
|
715
|
+
clearTimeout(localTimer)
|
|
716
|
+
localQueue = []
|
|
663
717
|
for (const fn of waitingForLink.splice(0)) fn()
|
|
664
718
|
const socket = ws
|
|
665
719
|
ws = null
|
|
@@ -671,5 +725,5 @@ export function startBridge(options: BridgeOptions): Bridge {
|
|
|
671
725
|
}
|
|
672
726
|
|
|
673
727
|
connect()
|
|
674
|
-
return { forward, changed, resync, renamed, activity, contacts, status, relink, stop }
|
|
728
|
+
return { forward, changed, resync, renamed, activity, local, contacts, status, relink, stop }
|
|
675
729
|
}
|
package/src/server.ts
CHANGED
|
@@ -996,13 +996,23 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
996
996
|
/** a send as `from` to local endpoints or, through the bridge, other machines; wakes the recipients and tells the watchers */
|
|
997
997
|
const deliver: Deliver = async (from, send, origin) => {
|
|
998
998
|
let result: SendResult
|
|
999
|
-
|
|
999
|
+
const remote = remoteSend(core, send.to)
|
|
1000
|
+
if (remote) {
|
|
1000
1001
|
if (!bridge)
|
|
1001
1002
|
throw new RelayError("not_found", `no endpoint ${[send.to].flat().join(", ")}: this broker is not linked to a hub (${CLI_NAME} cloud enable)`)
|
|
1002
1003
|
result = await bridge.forward(from, send, origin)
|
|
1003
1004
|
} else {
|
|
1004
1005
|
result = core.send(from, send, origin)
|
|
1005
1006
|
}
|
|
1007
|
+
if (!result.duplicate && !remote)
|
|
1008
|
+
bridge?.local?.({
|
|
1009
|
+
id: result.id,
|
|
1010
|
+
thread: result.thread,
|
|
1011
|
+
kind: send.kind ?? "message",
|
|
1012
|
+
from,
|
|
1013
|
+
to: result.deliveries.map((d) => d.endpoint),
|
|
1014
|
+
at: (options.now ?? Date.now)(),
|
|
1015
|
+
})
|
|
1006
1016
|
if (!result.duplicate) {
|
|
1007
1017
|
for (const delivery of result.deliveries) wake(delivery.endpoint)
|
|
1008
1018
|
notifyFleet({
|
|
@@ -1138,6 +1148,14 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
1138
1148
|
if (result.handback) {
|
|
1139
1149
|
const message = core.delivered(spawner, result.handback)!
|
|
1140
1150
|
wake(spawner)
|
|
1151
|
+
bridge?.local?.({
|
|
1152
|
+
id: message.id,
|
|
1153
|
+
thread: message.thread,
|
|
1154
|
+
kind: "handback",
|
|
1155
|
+
from: message.from.endpoint,
|
|
1156
|
+
to: [spawner],
|
|
1157
|
+
at: (options.now ?? Date.now)(),
|
|
1158
|
+
})
|
|
1141
1159
|
notifyFleet({
|
|
1142
1160
|
type: "message",
|
|
1143
1161
|
id: message.id,
|
package/src/testing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite"
|
|
2
2
|
import type { Server, ServerWebSocket } from "bun"
|
|
3
3
|
import { type CrossAccountLink, PASS_GATE, sha256Hex, type Sql, type SqlValue, type XContact } from "@scp-dev/core"
|
|
4
|
-
import { type HubGrant, type HubSocket, RelayHub } from "@scp-dev/hub"
|
|
4
|
+
import { type DashboardEvent, type HubGrant, type HubSocket, RelayHub } from "@scp-dev/hub"
|
|
5
5
|
|
|
6
6
|
export interface FakeToken {
|
|
7
7
|
tokenId: string
|
|
@@ -40,6 +40,8 @@ export function startFakeHubServer(options: {
|
|
|
40
40
|
tokens: Record<string, FakeToken>
|
|
41
41
|
now?: (() => number) | undefined
|
|
42
42
|
contacts?: [string, string][] | undefined
|
|
43
|
+
/** receives every account's dashboard events */
|
|
44
|
+
dashboard?: ((account: string, event: DashboardEvent) => void) | undefined
|
|
43
45
|
}): FakeHubServer {
|
|
44
46
|
const accounts = new Map<string, Account>()
|
|
45
47
|
|
|
@@ -77,6 +79,7 @@ export function startFakeHubServer(options: {
|
|
|
77
79
|
sockets: () => [...sockets.values()],
|
|
78
80
|
save: () => {},
|
|
79
81
|
...(options.contacts ? { crossAccount: linkOf(name) } : {}),
|
|
82
|
+
...(options.dashboard ? { dashboard: (event: DashboardEvent) => options.dashboard!(name, event) } : {}),
|
|
80
83
|
})
|
|
81
84
|
hub.init({ user: name })
|
|
82
85
|
found = { hub, sockets, traps: new Map() }
|