@scp-dev/broker 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +38 -2
- package/dist/index.d.ts +422 -0
- package/dist/index.js +2514 -0
- package/dist/testing.d.ts +30 -0
- package/dist/testing.js +131 -0
- package/package.json +48 -4
- package/src/bridge.ts +642 -0
- package/src/client.ts +336 -0
- package/src/index.ts +6 -0
- package/src/judge-gate.ts +552 -0
- package/src/lock.ts +113 -0
- package/src/ndjson.ts +95 -0
- package/src/paths.ts +132 -0
- package/src/server.ts +1124 -0
- package/src/socket.ts +57 -0
- package/src/sqlite.ts +90 -0
- package/src/testing.ts +168 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RelayHub, HubSocket } from '@scp-dev/hub';
|
|
2
|
+
|
|
3
|
+
interface FakeToken {
|
|
4
|
+
tokenId: string;
|
|
5
|
+
/** the account handle the token acts as */
|
|
6
|
+
user: string;
|
|
7
|
+
}
|
|
8
|
+
interface FakeHubServer {
|
|
9
|
+
/** the http origin, as a bridge's url takes it */
|
|
10
|
+
url: string;
|
|
11
|
+
hub(account: string): RelayHub;
|
|
12
|
+
/** closes every socket of `machine` (code 1012), as a dropped network would */
|
|
13
|
+
drop(account: string, machine: string): void;
|
|
14
|
+
/** the next frame calling `method` from `machine` closes its socket instead of reaching the hub; resolves once it did */
|
|
15
|
+
dropOn(account: string, machine: string, method: string): Promise<void>;
|
|
16
|
+
/** the open sockets of an account, with their state */
|
|
17
|
+
sockets(account: string): HubSocket[];
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* a stand-in for a hosted hub's /v1/relay routes: one RelayHub per account over bun:sqlite, bearer credentials from a table; `contacts` pairs
|
|
22
|
+
* account handles that are active contacts both ways (default allowlist: every root), their relays linked directly
|
|
23
|
+
*/
|
|
24
|
+
declare function startFakeHubServer(options: {
|
|
25
|
+
tokens: Record<string, FakeToken>;
|
|
26
|
+
now?: (() => number) | undefined;
|
|
27
|
+
contacts?: [string, string][] | undefined;
|
|
28
|
+
}): FakeHubServer;
|
|
29
|
+
|
|
30
|
+
export { type FakeHubServer, type FakeToken, startFakeHubServer };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// src/testing.ts
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import { PASS_GATE, sha256Hex } from "@scp-dev/core";
|
|
4
|
+
import { RelayHub } from "@scp-dev/hub";
|
|
5
|
+
function startFakeHubServer(options) {
|
|
6
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
7
|
+
function linkOf(name) {
|
|
8
|
+
const peers = () => (options.contacts ?? []).flatMap(([a, b]) => a === name ? [b] : b === name ? [a] : []);
|
|
9
|
+
const contact = (peer) => ({
|
|
10
|
+
peerId: `usr_${peer}`,
|
|
11
|
+
peerHandle: peer,
|
|
12
|
+
allow: [{ machine: "*", sel: "tree:root" }],
|
|
13
|
+
dir: "both",
|
|
14
|
+
limits: null
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
self: { id: `usr_${name}`, handle: name },
|
|
18
|
+
contactByHandle: async (handle) => peers().includes(handle) ? contact(handle) : null,
|
|
19
|
+
contactById: async (id) => id.startsWith("usr_") && peers().includes(id.slice(4)) ? contact(id.slice(4)) : null,
|
|
20
|
+
contacts: async () => peers().map(contact),
|
|
21
|
+
deliver: async (peer, msg) => account(peer.peerHandle).hub.xreceive(`usr_${name}`, JSON.parse(JSON.stringify(msg))),
|
|
22
|
+
list: async (peer) => account(peer.peerHandle).hub.xlist(`usr_${name}`),
|
|
23
|
+
gate: PASS_GATE
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function account(name) {
|
|
27
|
+
let found = accounts.get(name);
|
|
28
|
+
if (found) return found;
|
|
29
|
+
const db = new Database(":memory:");
|
|
30
|
+
const sql = { all: (query, ...params) => db.query(query).all(...params) };
|
|
31
|
+
const sockets = /* @__PURE__ */ new Map();
|
|
32
|
+
const hub = new RelayHub({
|
|
33
|
+
sql,
|
|
34
|
+
hash: sha256Hex,
|
|
35
|
+
transaction: (fn) => db.transaction(fn)(),
|
|
36
|
+
...options.now ? { now: options.now } : {},
|
|
37
|
+
sockets: () => [...sockets.values()],
|
|
38
|
+
save: () => {
|
|
39
|
+
},
|
|
40
|
+
...options.contacts ? { crossAccount: linkOf(name) } : {}
|
|
41
|
+
});
|
|
42
|
+
hub.init({ user: name });
|
|
43
|
+
found = { hub, sockets, traps: /* @__PURE__ */ new Map() };
|
|
44
|
+
accounts.set(name, found);
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
function grantOf(request) {
|
|
48
|
+
const match = /^Bearer (\S+)$/.exec(request.headers.get("authorization") ?? "");
|
|
49
|
+
const token = match ? options.tokens[match[1]] : void 0;
|
|
50
|
+
if (!token) return null;
|
|
51
|
+
return {
|
|
52
|
+
account: token.user,
|
|
53
|
+
grant: { tokenId: token.tokenId, userId: `usr_${token.user}`, actorId: null, expiresAt: Date.now() + 36e5, scoped: false, role: null }
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const server = Bun.serve({
|
|
57
|
+
hostname: "127.0.0.1",
|
|
58
|
+
port: 0,
|
|
59
|
+
async fetch(request, srv) {
|
|
60
|
+
const path = new URL(request.url).pathname;
|
|
61
|
+
if (path !== "/v1/relay/ws" && path !== "/v1/relay/rpc") return Response.json({ error: "not_found" }, { status: 404 });
|
|
62
|
+
const principal = grantOf(request);
|
|
63
|
+
if (!principal) return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
64
|
+
const { hub } = account(principal.account);
|
|
65
|
+
if (path === "/v1/relay/ws") {
|
|
66
|
+
if (hub.full()) return Response.json({ error: "too_many_sockets" }, { status: 429 });
|
|
67
|
+
if (srv.upgrade(request, { data: principal })) return void 0;
|
|
68
|
+
return Response.json({ error: "upgrade_required" }, { status: 426 });
|
|
69
|
+
}
|
|
70
|
+
if (request.method !== "POST") return Response.json({ error: "not_found" }, { status: 404 });
|
|
71
|
+
let body;
|
|
72
|
+
try {
|
|
73
|
+
body = JSON.parse(await request.text());
|
|
74
|
+
} catch {
|
|
75
|
+
return Response.json({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } });
|
|
76
|
+
}
|
|
77
|
+
return Response.json(await hub.batch(principal.grant, body));
|
|
78
|
+
},
|
|
79
|
+
websocket: {
|
|
80
|
+
open(ws) {
|
|
81
|
+
const state = account(ws.data.account);
|
|
82
|
+
const socket = {
|
|
83
|
+
conn: RelayHub.connection(ws.data.grant),
|
|
84
|
+
send: (text) => void ws.send(text),
|
|
85
|
+
close: (code, reason) => ws.close(code, reason)
|
|
86
|
+
};
|
|
87
|
+
ws.data.socket = socket;
|
|
88
|
+
state.sockets.set(ws, socket);
|
|
89
|
+
},
|
|
90
|
+
async message(ws, message) {
|
|
91
|
+
const state = account(ws.data.account);
|
|
92
|
+
const socket = ws.data.socket;
|
|
93
|
+
const text = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
94
|
+
const machine = socket.conn.machine;
|
|
95
|
+
if (machine && state.traps.size) {
|
|
96
|
+
const method = JSON.parse(text).method;
|
|
97
|
+
const trap = state.traps.get(machine);
|
|
98
|
+
if (method && trap?.method === method) {
|
|
99
|
+
state.traps.delete(machine);
|
|
100
|
+
ws.close(1012, "dropped by the test");
|
|
101
|
+
trap.fired();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await state.hub.message(socket, text);
|
|
106
|
+
},
|
|
107
|
+
close(ws) {
|
|
108
|
+
const state = account(ws.data.account);
|
|
109
|
+
state.sockets.delete(ws);
|
|
110
|
+
if (ws.data.socket) state.hub.closed(ws.data.socket);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
116
|
+
hub: (name) => account(name).hub,
|
|
117
|
+
drop(name, machine) {
|
|
118
|
+
for (const [ws, socket] of account(name).sockets) if (socket.conn.machine === machine) ws.close(1012, "dropped by the test");
|
|
119
|
+
},
|
|
120
|
+
dropOn(name, machine, method) {
|
|
121
|
+
return new Promise((fired) => account(name).traps.set(machine, { method, fired }));
|
|
122
|
+
},
|
|
123
|
+
sockets: (name) => [...account(name).sockets.values()],
|
|
124
|
+
async stop() {
|
|
125
|
+
await server.stop(true);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export {
|
|
130
|
+
startFakeHubServer
|
|
131
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scp-dev/broker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://scp.dev",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/scphq/scp.git",
|
|
10
|
+
"directory": "packages/broker"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"bun": "./src/index.ts",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./testing": {
|
|
21
|
+
"bun": "./src/testing.ts",
|
|
22
|
+
"types": "./dist/testing.d.ts",
|
|
23
|
+
"default": "./dist/testing.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.13"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"!src/**/*.test.ts"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup src/index.ts src/testing.ts --format esm --dts --clean --external bun:sqlite",
|
|
37
|
+
"typecheck": "tsc -p ."
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@scp-dev/core": "0.1.1",
|
|
41
|
+
"@scp-dev/hub": "0.1.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"tsup": "^8.5.1"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"provenance": true
|
|
49
|
+
}
|
|
50
|
+
}
|