anchordb-relay 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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -0
  3. package/dist/cjs/cli.d.ts +3 -0
  4. package/dist/cjs/cli.d.ts.map +1 -0
  5. package/dist/cjs/cli.js +57 -0
  6. package/dist/cjs/cli.js.map +1 -0
  7. package/dist/cjs/index.d.ts +7 -0
  8. package/dist/cjs/index.d.ts.map +1 -0
  9. package/dist/cjs/index.js +10 -0
  10. package/dist/cjs/index.js.map +1 -0
  11. package/dist/cjs/mongo-bridge.d.ts +65 -0
  12. package/dist/cjs/mongo-bridge.d.ts.map +1 -0
  13. package/dist/cjs/mongo-bridge.js +289 -0
  14. package/dist/cjs/mongo-bridge.js.map +1 -0
  15. package/dist/cjs/package.json +3 -0
  16. package/dist/cjs/relay.d.ts +78 -0
  17. package/dist/cjs/relay.d.ts.map +1 -0
  18. package/dist/cjs/relay.js +117 -0
  19. package/dist/cjs/relay.js.map +1 -0
  20. package/dist/cjs/server.d.ts +30 -0
  21. package/dist/cjs/server.d.ts.map +1 -0
  22. package/dist/cjs/server.js +97 -0
  23. package/dist/cjs/server.js.map +1 -0
  24. package/dist/esm/cli.d.ts +3 -0
  25. package/dist/esm/cli.d.ts.map +1 -0
  26. package/dist/esm/cli.js +55 -0
  27. package/dist/esm/cli.js.map +1 -0
  28. package/dist/esm/index.d.ts +7 -0
  29. package/dist/esm/index.d.ts.map +1 -0
  30. package/dist/esm/index.js +4 -0
  31. package/dist/esm/index.js.map +1 -0
  32. package/dist/esm/mongo-bridge.d.ts +65 -0
  33. package/dist/esm/mongo-bridge.d.ts.map +1 -0
  34. package/dist/esm/mongo-bridge.js +252 -0
  35. package/dist/esm/mongo-bridge.js.map +1 -0
  36. package/dist/esm/package.json +3 -0
  37. package/dist/esm/relay.d.ts +78 -0
  38. package/dist/esm/relay.d.ts.map +1 -0
  39. package/dist/esm/relay.js +113 -0
  40. package/dist/esm/relay.js.map +1 -0
  41. package/dist/esm/server.d.ts +30 -0
  42. package/dist/esm/server.d.ts.map +1 -0
  43. package/dist/esm/server.js +61 -0
  44. package/dist/esm/server.js.map +1 -0
  45. package/package.json +65 -0
@@ -0,0 +1,252 @@
1
+ import { BRIDGE_PROTOCOL_VERSION, redactMongoUri, } from "anchordb";
2
+ /** Documents per streamed frame. Large enough to be efficient, small enough to stay under limits. */
3
+ const READ_BATCH = 200;
4
+ class BridgeFailure extends Error {
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ }
9
+ }
10
+ export class MongoBridge {
11
+ constructor(options) {
12
+ /** One MongoDB connection per socket, closed with it. */
13
+ this.sessions = new Map();
14
+ this.enabled = options.enabled;
15
+ this.loadDriver = options.driver ?? loadMongoDriver;
16
+ this.onLog = options.onLog ?? (() => undefined);
17
+ }
18
+ /** Attach a socket that dialled `/bridge`. */
19
+ join(socket, room, validRoom) {
20
+ if (!this.enabled) {
21
+ socket.close(4403, "the MongoDB bridge is disabled; start the relay with --mongo-bridge");
22
+ return;
23
+ }
24
+ if (!room || !validRoom(room)) {
25
+ socket.close(4404, "a valid room is required to use the bridge");
26
+ return;
27
+ }
28
+ this.onLog("bridge: client joined");
29
+ socket.on("message", (raw) => {
30
+ void this.handle(socket, String(raw));
31
+ });
32
+ socket.on("close", () => {
33
+ void this.release(socket);
34
+ });
35
+ }
36
+ async release(socket) {
37
+ const session = this.sessions.get(socket);
38
+ if (!session)
39
+ return;
40
+ this.sessions.delete(socket);
41
+ // The credential dies with the socket — there is nowhere else it is held.
42
+ await session.connection.close().catch(() => undefined);
43
+ this.onLog("bridge: connection closed");
44
+ }
45
+ send(socket, response) {
46
+ socket.send(JSON.stringify(response));
47
+ }
48
+ async handle(socket, raw) {
49
+ let request;
50
+ try {
51
+ request = JSON.parse(raw);
52
+ }
53
+ catch {
54
+ return; // not a bridge frame
55
+ }
56
+ if (!request || typeof request.id !== "string" || typeof request.op !== "string")
57
+ return;
58
+ const base = { v: BRIDGE_PROTOCOL_VERSION, id: request.id };
59
+ try {
60
+ switch (request.op) {
61
+ case "connect":
62
+ this.send(socket, { ...base, ok: true, result: await this.connect(socket, request) });
63
+ return;
64
+ case "read":
65
+ await this.read(socket, request, base);
66
+ return;
67
+ case "write":
68
+ this.send(socket, { ...base, ok: true, result: await this.write(socket, request) });
69
+ return;
70
+ case "disconnect":
71
+ await this.release(socket);
72
+ this.send(socket, { ...base, ok: true, result: { closed: true } });
73
+ return;
74
+ default:
75
+ throw new BridgeFailure("bad_request", `Unknown bridge operation "${String(request.op)}"`);
76
+ }
77
+ }
78
+ catch (err) {
79
+ const failure = err instanceof BridgeFailure
80
+ ? err
81
+ : new BridgeFailure("failed", err.message ?? "The bridge failed.");
82
+ this.onLog(`bridge: ${request.op} failed — ${failure.message}`);
83
+ this.send(socket, { ...base, ok: false, error: { code: failure.code, message: failure.message } });
84
+ }
85
+ }
86
+ async connect(socket, request) {
87
+ const uri = String(request.uri ?? "");
88
+ const database = String(request.database ?? "");
89
+ if (!uri || !database) {
90
+ throw new BridgeFailure("bad_request", "A connection string and a database name are required.");
91
+ }
92
+ let driver;
93
+ try {
94
+ driver = await this.loadDriver();
95
+ }
96
+ catch {
97
+ throw new BridgeFailure("driver_missing", "The mongodb driver is not installed. Run `npm install mongodb` where the relay runs.");
98
+ }
99
+ await this.release(socket); // one connection per socket; a second connect replaces the first
100
+ let connection;
101
+ try {
102
+ connection = await driver.connect(uri);
103
+ }
104
+ catch (err) {
105
+ // The URI is in scope here and must not reach the message.
106
+ throw new BridgeFailure("connect_failed", `Could not connect to ${redactMongoUri(uri)} — ${err.message}`);
107
+ }
108
+ this.sessions.set(socket, { connection, database });
109
+ this.onLog(`bridge: connected to ${redactMongoUri(uri)}`);
110
+ return {
111
+ database,
112
+ serverVersion: await connection.serverVersion(),
113
+ collections: await connection.listCollections(database),
114
+ };
115
+ }
116
+ session(socket) {
117
+ const session = this.sessions.get(socket);
118
+ if (!session)
119
+ throw new BridgeFailure("not_connected", "Connect to a MongoDB first.");
120
+ return session;
121
+ }
122
+ /** Stream a collection out as newline-delimited Extended JSON. */
123
+ async read(socket, request, base) {
124
+ const { connection, database } = this.session(socket);
125
+ const collection = String(request.collection ?? "");
126
+ if (!collection)
127
+ throw new BridgeFailure("bad_request", "A collection name is required.");
128
+ const driver = await this.loadDriver();
129
+ let sent = 0;
130
+ // Batched rather than one frame: a large collection would exceed any sane frame limit, and
131
+ // holding the whole thing as a single string on both sides at once is avoidable.
132
+ for await (const batch of connection.find(database, collection, READ_BATCH)) {
133
+ const chunk = batch.map((doc) => driver.stringifyEJSON(doc)).join("\n");
134
+ sent += batch.length;
135
+ this.send(socket, { ...base, ok: true, chunk, done: false, sent });
136
+ }
137
+ this.send(socket, {
138
+ ...base,
139
+ ok: true,
140
+ chunk: "",
141
+ done: true,
142
+ sent,
143
+ result: { collection, count: sent },
144
+ });
145
+ }
146
+ async write(socket, request) {
147
+ const { connection, database } = this.session(socket);
148
+ const collection = String(request.collection ?? "");
149
+ if (!collection)
150
+ throw new BridgeFailure("bad_request", "A collection name is required.");
151
+ const mode = (request.mode ?? "insert");
152
+ const driver = await this.loadDriver();
153
+ // Decoded with MongoDB's own Extended JSON codec, so an $oid becomes a real driver ObjectId
154
+ // rather than something that would land in the database as a string.
155
+ const documents = String(request.data ?? "")
156
+ .split("\n")
157
+ .map((line) => line.trim())
158
+ .filter(Boolean)
159
+ .map((line, index) => {
160
+ try {
161
+ return driver.parseEJSON(line);
162
+ }
163
+ catch (err) {
164
+ throw new BridgeFailure("bad_request", `Malformed Extended JSON on line ${index + 1}: ${err.message}`);
165
+ }
166
+ });
167
+ if (documents.length === 0)
168
+ return { collection, inserted: 0, updated: 0, skipped: 0 };
169
+ const result = await connection.write(database, collection, documents, mode);
170
+ this.onLog(`bridge: wrote ${documents.length} document(s) to ${collection}`);
171
+ return { collection, ...result };
172
+ }
173
+ }
174
+ /**
175
+ * Load the real driver.
176
+ *
177
+ * A lazy import so the relay stays usable — and installable — without `mongodb`. It is an optional
178
+ * dependency: most people running `npx anchor-relay` only want the inspector.
179
+ */
180
+ async function loadMongoDriver() {
181
+ // mongodb is an OPTIONAL dependency: the relay must install and run without it, and most
182
+ // people running the inspector never want the driver. MongoModule above types the slice used
183
+ // here, so absence is a runtime condition rather than a compile error.
184
+ // @ts-ignore -- resolved at runtime; may legitimately not be installed
185
+ const mongo = (await import("mongodb"));
186
+ const EJSON = mongo.EJSON ?? mongo.BSON?.EJSON;
187
+ if (!EJSON)
188
+ throw new Error("This mongodb driver does not expose Extended JSON.");
189
+ return {
190
+ stringifyEJSON: (value) => EJSON.stringify(value, { relaxed: false }),
191
+ parseEJSON: (text) => EJSON.parse(text, { relaxed: false }),
192
+ async connect(uri) {
193
+ const client = new mongo.MongoClient(uri, { serverSelectionTimeoutMS: 10_000 });
194
+ await client.connect();
195
+ return {
196
+ async serverVersion() {
197
+ const info = (await client.db("admin").command({ buildInfo: 1 }));
198
+ return info.version ?? "unknown";
199
+ },
200
+ async listCollections(database) {
201
+ const db = client.db(database);
202
+ const names = await db.listCollections({}, { nameOnly: true }).toArray();
203
+ const out = [];
204
+ for (const { name } of names) {
205
+ // Estimated: instant, where an exact count is a full scan on a large collection.
206
+ out.push({ name, count: await db.collection(name).estimatedDocumentCount() });
207
+ }
208
+ return out.sort((a, b) => a.name.localeCompare(b.name));
209
+ },
210
+ async *find(database, collection, batchSize) {
211
+ const cursor = client.db(database).collection(collection).find({}, { batchSize });
212
+ let batch = [];
213
+ for await (const doc of cursor) {
214
+ batch.push(doc);
215
+ if (batch.length >= batchSize) {
216
+ yield batch;
217
+ batch = [];
218
+ }
219
+ }
220
+ if (batch.length > 0)
221
+ yield batch;
222
+ },
223
+ async write(database, collection, documents, mode) {
224
+ const target = client.db(database).collection(collection);
225
+ if (mode === "replace")
226
+ await target.deleteMany({});
227
+ const summary = { inserted: 0, updated: 0, skipped: 0 };
228
+ const withId = documents;
229
+ if (mode === "insert") {
230
+ // $setOnInsert leaves an existing document untouched, which is "skip" without needing a
231
+ // read first or a thrown duplicate-key error to catch.
232
+ const result = await target.bulkWrite(withId.map((doc) => ({
233
+ updateOne: { filter: { _id: doc._id }, update: { $setOnInsert: doc }, upsert: true },
234
+ })), { ordered: false });
235
+ summary.inserted = result.upsertedCount ?? 0;
236
+ summary.skipped = (result.matchedCount ?? 0);
237
+ }
238
+ else {
239
+ const result = await target.bulkWrite(withId.map((doc) => ({
240
+ replaceOne: { filter: { _id: doc._id }, replacement: doc, upsert: true },
241
+ })), { ordered: false });
242
+ summary.inserted = result.upsertedCount ?? 0;
243
+ summary.updated = result.modifiedCount ?? 0;
244
+ }
245
+ return summary;
246
+ },
247
+ close: () => client.close(),
248
+ };
249
+ },
250
+ };
251
+ }
252
+ //# sourceMappingURL=mongo-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mongo-bridge.js","sourceRoot":"","sources":["../../src/mongo-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The Anchor Inspector Relay — development only.
3
+ *
4
+ * ## Why a relay exists at all
5
+ *
6
+ * React Native (and every browser) can open a WebSocket **client** but cannot **listen**. Neither
7
+ * the app being inspected nor Anchor Lens can host a server without a native TCP module, which
8
+ * would force an Expo dev build and rule out Expo Go entirely.
9
+ *
10
+ * So both sides dial out to this tiny Node process, which pairs them by room and copies frames
11
+ * across. Nothing here understands the inspector protocol — it is a dumb pipe on purpose, so the
12
+ * protocol can evolve without the relay ever needing an update.
13
+ *
14
+ * ## What it deliberately does NOT do
15
+ *
16
+ * It does not authenticate the inspector session. Pairing is end-to-end between the agent and Lens
17
+ * (HMAC over a challenge), so a compromised or malicious relay still cannot issue itself a session.
18
+ * The room token here only decides *who can be connected to whom*; it is not the security boundary.
19
+ */
20
+ export type RelayRole = "agent" | "client";
21
+ export interface RelaySocket {
22
+ send(data: string): void;
23
+ close(code?: number, reason?: string): void;
24
+ on(event: "message", handler: (data: unknown) => void): void;
25
+ on(event: "close", handler: () => void): void;
26
+ on(event: "error", handler: (err: Error) => void): void;
27
+ }
28
+ export interface RelayOptions {
29
+ /** Rooms with no agent for this long are reclaimed. */
30
+ roomTtlMs?: number;
31
+ /** Frames larger than this are refused rather than forwarded. */
32
+ maxFrameBytes?: number;
33
+ onLog?: (message: string) => void;
34
+ }
35
+ export interface RoomInfo {
36
+ id: string;
37
+ agentName: string;
38
+ agentConnected: boolean;
39
+ clientConnected: boolean;
40
+ ageMs: number;
41
+ }
42
+ /**
43
+ * Transport-agnostic relay core.
44
+ *
45
+ * Kept free of any `ws` import so it can be driven by a real WebSocket server, by a test double, or
46
+ * by any other transport, and so the routing logic is testable without opening a port.
47
+ */
48
+ export declare class InspectorRelay {
49
+ private readonly rooms;
50
+ private readonly opts;
51
+ constructor(options?: RelayOptions);
52
+ /** Create a room for an app to publish itself into. The id goes into the QR code. */
53
+ createRoom(agentName?: string): string;
54
+ /** Does this room exist? The bridge uses it as an access token. */
55
+ hasRoom(id: string): boolean;
56
+ rooms_(): RoomInfo[];
57
+ /**
58
+ * Join a socket to a room in a given role.
59
+ *
60
+ * A second socket claiming an occupied role is refused rather than replacing the incumbent:
61
+ * silently swapping would let anyone who learns a room id evict the real Lens mid-session.
62
+ */
63
+ join(roomId: string, role: RelayRole, socket: RelaySocket): {
64
+ ok: true;
65
+ } | {
66
+ ok: false;
67
+ reason: string;
68
+ };
69
+ closeRoom(roomId: string): void;
70
+ private reap;
71
+ /** Connection details a QR code encodes. The pairing code is NOT included — see below. */
72
+ connectionInfo(host: string, port: number, roomId: string): {
73
+ url: string;
74
+ room: string;
75
+ deepLink: string;
76
+ };
77
+ }
78
+ //# sourceMappingURL=relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/relay.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE3C,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7D,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC9C,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;CACzD;AAUD,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2B;IACjD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyE;gBAElF,OAAO,GAAE,YAAiB;IAQtC,qFAAqF;IACrF,UAAU,CAAC,SAAS,SAAa,GAAG,MAAM;IAQ1C,mEAAmE;IACnE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAK5B,MAAM,IAAI,QAAQ,EAAE;IAWpB;;;;;OAKG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,GAAG;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAqCxG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQ/B,OAAO,CAAC,IAAI;IAQZ,0FAA0F;IAC1F,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;QAC1D,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB;CAaF"}
@@ -0,0 +1,113 @@
1
+ import { randomBytes } from "node:crypto";
2
+ /**
3
+ * Transport-agnostic relay core.
4
+ *
5
+ * Kept free of any `ws` import so it can be driven by a real WebSocket server, by a test double, or
6
+ * by any other transport, and so the routing logic is testable without opening a port.
7
+ */
8
+ export class InspectorRelay {
9
+ constructor(options = {}) {
10
+ this.rooms = new Map();
11
+ this.opts = {
12
+ roomTtlMs: options.roomTtlMs ?? 30 * 60_000,
13
+ maxFrameBytes: options.maxFrameBytes ?? 4 * 1024 * 1024,
14
+ onLog: options.onLog ?? (() => undefined),
15
+ };
16
+ }
17
+ /** Create a room for an app to publish itself into. The id goes into the QR code. */
18
+ createRoom(agentName = "AnchorDB") {
19
+ this.reap();
20
+ const id = randomBytes(9).toString("base64url");
21
+ this.rooms.set(id, { id, agent: null, client: null, createdAt: Date.now(), agentName });
22
+ this.opts.onLog(`room ${id} created for "${agentName}"`);
23
+ return id;
24
+ }
25
+ /** Does this room exist? The bridge uses it as an access token. */
26
+ hasRoom(id) {
27
+ this.reap();
28
+ return this.rooms.has(id);
29
+ }
30
+ rooms_() {
31
+ this.reap();
32
+ return [...this.rooms.values()].map((r) => ({
33
+ id: r.id,
34
+ agentName: r.agentName,
35
+ agentConnected: r.agent !== null,
36
+ clientConnected: r.client !== null,
37
+ ageMs: Date.now() - r.createdAt,
38
+ }));
39
+ }
40
+ /**
41
+ * Join a socket to a room in a given role.
42
+ *
43
+ * A second socket claiming an occupied role is refused rather than replacing the incumbent:
44
+ * silently swapping would let anyone who learns a room id evict the real Lens mid-session.
45
+ */
46
+ join(roomId, role, socket) {
47
+ this.reap();
48
+ const room = this.rooms.get(roomId);
49
+ if (!room)
50
+ return { ok: false, reason: "unknown room" };
51
+ if (room[role])
52
+ return { ok: false, reason: `a ${role} is already connected to this room` };
53
+ room[role] = socket;
54
+ this.opts.onLog(`room ${roomId}: ${role} joined`);
55
+ socket.on("message", (data) => {
56
+ const frame = typeof data === "string" ? data : String(data);
57
+ if (frame.length > this.opts.maxFrameBytes) {
58
+ this.opts.onLog(`room ${roomId}: frame of ${frame.length} bytes refused`);
59
+ return;
60
+ }
61
+ const peer = role === "agent" ? room.client : room.agent;
62
+ // Frames are forwarded verbatim. The relay never parses the protocol.
63
+ if (peer)
64
+ peer.send(frame);
65
+ });
66
+ socket.on("close", () => {
67
+ if (room[role] === socket)
68
+ room[role] = null;
69
+ this.opts.onLog(`room ${roomId}: ${role} left`);
70
+ // Closing the agent ends the session for the client too — there is nothing left to inspect.
71
+ if (role === "agent" && room.client) {
72
+ room.client.close(4001, "agent disconnected");
73
+ room.client = null;
74
+ }
75
+ });
76
+ socket.on("error", () => {
77
+ if (room[role] === socket)
78
+ room[role] = null;
79
+ });
80
+ return { ok: true };
81
+ }
82
+ closeRoom(roomId) {
83
+ const room = this.rooms.get(roomId);
84
+ if (!room)
85
+ return;
86
+ room.agent?.close(4000, "room closed");
87
+ room.client?.close(4000, "room closed");
88
+ this.rooms.delete(roomId);
89
+ }
90
+ reap() {
91
+ const now = Date.now();
92
+ for (const [id, room] of this.rooms) {
93
+ const stale = now - room.createdAt > this.opts.roomTtlMs;
94
+ if (stale && !room.agent && !room.client)
95
+ this.rooms.delete(id);
96
+ }
97
+ }
98
+ /** Connection details a QR code encodes. The pairing code is NOT included — see below. */
99
+ connectionInfo(host, port, roomId) {
100
+ const url = `ws://${host}:${port}/relay?room=${roomId}&role=client`;
101
+ return {
102
+ url,
103
+ room: roomId,
104
+ /**
105
+ * The QR carries the relay address and room only. The pairing code is displayed separately
106
+ * and typed by the developer, so photographing the screen is not enough to pair — which is
107
+ * the whole point of keeping the code out of the QR (§51).
108
+ */
109
+ deepLink: `anchor-lens://connect?relay=${encodeURIComponent(url)}&room=${roomId}`,
110
+ };
111
+ }
112
+ }
113
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.js","sourceRoot":"","sources":["../../src/relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { InspectorRelay, type RelayOptions } from "./relay.js";
2
+ import { MongoBridge, type MongoDriver } from "./mongo-bridge.js";
3
+ /**
4
+ * A runnable relay over `ws`.
5
+ *
6
+ * `ws` is loaded lazily so `InspectorRelay` itself stays dependency-free and testable without a
7
+ * socket library — the routing logic is the part worth testing, and it should not require a port.
8
+ */
9
+ export interface RelayServerOptions extends RelayOptions {
10
+ port?: number;
11
+ host?: string;
12
+ /**
13
+ * Enable the MongoDB bridge on `/bridge`.
14
+ *
15
+ * Off by default and deliberately so: a relay that will dial any MongoDB on request lets anything
16
+ * that can reach the port use this machine to reach a database.
17
+ */
18
+ mongoBridge?: boolean;
19
+ /** Supply a driver instead of importing `mongodb` — used by the tests. */
20
+ mongoDriver?: () => Promise<MongoDriver>;
21
+ }
22
+ export interface RelayServer {
23
+ relay: InspectorRelay;
24
+ bridge: MongoBridge;
25
+ port: number;
26
+ host: string;
27
+ close(): Promise<void>;
28
+ }
29
+ export declare function startRelayServer(options?: RelayServerOptions): Promise<RelayServer>;
30
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAoB,MAAM,YAAY,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAElE;;;;;GAKG;AACH,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAoE7F"}
@@ -0,0 +1,61 @@
1
+ import { createServer } from "node:http";
2
+ import { InspectorRelay } from "./relay.js";
3
+ import { MongoBridge } from "./mongo-bridge.js";
4
+ export async function startRelayServer(options = {}) {
5
+ const port = options.port ?? 9440;
6
+ const host = options.host ?? "127.0.0.1";
7
+ const relay = new InspectorRelay(options);
8
+ const bridge = new MongoBridge({
9
+ enabled: options.mongoBridge ?? false,
10
+ ...(options.mongoDriver ? { driver: options.mongoDriver } : {}),
11
+ ...(options.onLog ? { onLog: options.onLog } : {}),
12
+ });
13
+ const { WebSocketServer } = (await import("ws"));
14
+ const http = createServer((req, res) => {
15
+ // A tiny control surface: create a room, list rooms, health.
16
+ if (req.url?.startsWith("/rooms") && req.method === "POST") {
17
+ const id = relay.createRoom("AnchorDB");
18
+ res.writeHead(200, { "content-type": "application/json" });
19
+ // connectionInfo already carries `room`; spreading after it would silently overwrite.
20
+ res.end(JSON.stringify(relay.connectionInfo(host, port, id)));
21
+ return;
22
+ }
23
+ if (req.url?.startsWith("/rooms") && req.method === "GET") {
24
+ res.writeHead(200, { "content-type": "application/json" });
25
+ res.end(JSON.stringify(relay.rooms_()));
26
+ return;
27
+ }
28
+ res.writeHead(200, { "content-type": "application/json" });
29
+ res.end(JSON.stringify({ ok: true, service: "anchor-inspector-relay" }));
30
+ });
31
+ const wss = new WebSocketServer({ server: http });
32
+ wss.on("connection", (socket, request) => {
33
+ const url = new URL(request.url ?? "/", `http://${host}:${port}`);
34
+ const room = url.searchParams.get("room");
35
+ const role = url.searchParams.get("role");
36
+ // The bridge is a separate endpoint, not a room member: the relay answers it rather than
37
+ // forwarding it to the app.
38
+ if (url.pathname === "/bridge") {
39
+ bridge.join(socket, room, (id) => relay.hasRoom(id));
40
+ return;
41
+ }
42
+ if (!room || (role !== "agent" && role !== "client")) {
43
+ socket.close(4400, "room and role=agent|client are required");
44
+ return;
45
+ }
46
+ const result = relay.join(room, role, socket);
47
+ if (!result.ok)
48
+ socket.close(4404, result.reason);
49
+ });
50
+ await new Promise((resolve) => http.listen(port, host, resolve));
51
+ return {
52
+ relay,
53
+ bridge,
54
+ port,
55
+ host,
56
+ close: () => new Promise((resolve) => {
57
+ wss.close(() => http.close(() => resolve()));
58
+ }),
59
+ };
60
+ }
61
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "anchordb-relay",
3
+ "version": "0.2.0",
4
+ "description": "Development-only relay that brokers Anchor Inspector connections between a mobile app and Anchor Lens.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "bin": {
12
+ "anchor-relay": "./dist/esm/cli.js"
13
+ },
14
+ "main": "./dist/cjs/index.js",
15
+ "module": "./dist/esm/index.js",
16
+ "types": "./dist/esm/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/esm/index.d.ts",
21
+ "default": "./dist/esm/index.js"
22
+ },
23
+ "require": {
24
+ "types": "./dist/cjs/index.d.ts",
25
+ "default": "./dist/cjs/index.js"
26
+ }
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "rimraf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.build.cjs.json && node ../core/scripts/finalize-dist.mjs",
37
+ "typecheck": "tsc --noEmit -p tsconfig.json",
38
+ "check:package": "publint && attw --pack . --profile node16",
39
+ "prepublishOnly": "npm run typecheck && npm run build"
40
+ },
41
+ "dependencies": {
42
+ "anchordb": "^0.2.0",
43
+ "ws": "^8.18.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/ws": "^8.5.13",
47
+ "rimraf": "^6.0.1"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/knnadeera/anchordb.git",
55
+ "directory": "packages/inspector-relay"
56
+ },
57
+ "homepage": "https://github.com/knnadeera/anchordb#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/knnadeera/anchordb/issues"
60
+ },
61
+ "optionalDependencies": {
62
+ "mongodb": "^6.10.0"
63
+ },
64
+ "author": "Nisala Nadeera <knnadeera@gmail.com>"
65
+ }