@auxiora/bridge 1.0.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/LICENSE +191 -0
- package/dist/__tests__/bridge.test.d.ts +2 -0
- package/dist/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/__tests__/bridge.test.js +340 -0
- package/dist/__tests__/bridge.test.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/pairing.d.ts +32 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +111 -0
- package/dist/pairing.js.map +1 -0
- package/dist/registry.d.ts +34 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +91 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +62 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +265 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +25 -0
- package/src/__tests__/bridge.test.ts +411 -0
- package/src/index.ts +22 -0
- package/src/pairing.ts +129 -0
- package/src/registry.ts +108 -0
- package/src/server.ts +338 -0
- package/src/types.ts +115 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
package/dist/pairing.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getLogger } from '@auxiora/logger';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import { DEFAULT_BRIDGE_CONFIG } from './types.js';
|
|
4
|
+
const logger = getLogger('bridge:pairing');
|
|
5
|
+
/**
|
|
6
|
+
* Manages pairing code generation, validation, and expiry
|
|
7
|
+
* for the device pairing flow.
|
|
8
|
+
*/
|
|
9
|
+
export class PairingFlow {
|
|
10
|
+
activeCodes = new Map();
|
|
11
|
+
config;
|
|
12
|
+
cleanupTimer = null;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = { ...DEFAULT_BRIDGE_CONFIG, ...config };
|
|
15
|
+
}
|
|
16
|
+
/** Generate a new pairing code. */
|
|
17
|
+
generateCode() {
|
|
18
|
+
const code = this.makeCode(this.config.codeLength);
|
|
19
|
+
const pairingCode = {
|
|
20
|
+
code,
|
|
21
|
+
expiresAt: Date.now() + this.config.codeExpirySeconds * 1000,
|
|
22
|
+
used: false,
|
|
23
|
+
};
|
|
24
|
+
this.activeCodes.set(code, pairingCode);
|
|
25
|
+
logger.info('Pairing code generated', { code });
|
|
26
|
+
return { ...pairingCode };
|
|
27
|
+
}
|
|
28
|
+
/** Validate a pairing code. Returns true if the code is valid and not expired. */
|
|
29
|
+
validate(code) {
|
|
30
|
+
const pairingCode = this.activeCodes.get(code);
|
|
31
|
+
if (!pairingCode) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (pairingCode.used) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (Date.now() > pairingCode.expiresAt) {
|
|
38
|
+
this.activeCodes.delete(code);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/** Consume a pairing code, marking it as used. Returns true if successful. */
|
|
44
|
+
consume(code) {
|
|
45
|
+
if (!this.validate(code)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const pairingCode = this.activeCodes.get(code);
|
|
49
|
+
pairingCode.used = true;
|
|
50
|
+
logger.info('Pairing code consumed', { code });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
/** Revoke an active pairing code. */
|
|
54
|
+
revoke(code) {
|
|
55
|
+
const removed = this.activeCodes.delete(code);
|
|
56
|
+
if (removed) {
|
|
57
|
+
logger.info('Pairing code revoked', { code });
|
|
58
|
+
}
|
|
59
|
+
return removed;
|
|
60
|
+
}
|
|
61
|
+
/** Get all active (non-expired, non-used) codes. */
|
|
62
|
+
getActiveCodes() {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const active = [];
|
|
65
|
+
for (const pc of this.activeCodes.values()) {
|
|
66
|
+
if (!pc.used && now <= pc.expiresAt) {
|
|
67
|
+
active.push({ ...pc });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return active;
|
|
71
|
+
}
|
|
72
|
+
/** Remove expired codes. */
|
|
73
|
+
cleanup() {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
let removed = 0;
|
|
76
|
+
for (const [code, pc] of this.activeCodes) {
|
|
77
|
+
if (pc.used || now > pc.expiresAt) {
|
|
78
|
+
this.activeCodes.delete(code);
|
|
79
|
+
removed++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return removed;
|
|
83
|
+
}
|
|
84
|
+
/** Start automatic cleanup timer. */
|
|
85
|
+
startCleanup(intervalMs = 60_000) {
|
|
86
|
+
this.stopCleanup();
|
|
87
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), intervalMs);
|
|
88
|
+
}
|
|
89
|
+
/** Stop automatic cleanup timer. */
|
|
90
|
+
stopCleanup() {
|
|
91
|
+
if (this.cleanupTimer) {
|
|
92
|
+
clearInterval(this.cleanupTimer);
|
|
93
|
+
this.cleanupTimer = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Destroy the pairing flow, clearing all codes and stopping timers. */
|
|
97
|
+
destroy() {
|
|
98
|
+
this.stopCleanup();
|
|
99
|
+
this.activeCodes.clear();
|
|
100
|
+
}
|
|
101
|
+
/** Generate a random numeric code of the given length. */
|
|
102
|
+
makeCode(length) {
|
|
103
|
+
const bytes = crypto.randomBytes(length);
|
|
104
|
+
let code = '';
|
|
105
|
+
for (let i = 0; i < length; i++) {
|
|
106
|
+
code += (bytes[i] % 10).toString();
|
|
107
|
+
}
|
|
108
|
+
return code;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=pairing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DeviceInfo, DeviceCapability, DeviceConnectionState } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Tracks paired devices, their capabilities, and online status.
|
|
4
|
+
*/
|
|
5
|
+
export declare class DeviceRegistry {
|
|
6
|
+
private devices;
|
|
7
|
+
private maxDevices;
|
|
8
|
+
constructor(maxDevices?: number);
|
|
9
|
+
/** Register a newly paired device. */
|
|
10
|
+
register(device: DeviceInfo): void;
|
|
11
|
+
/** Remove a device from the registry. */
|
|
12
|
+
unregister(deviceId: string): boolean;
|
|
13
|
+
/** Get a device by ID. */
|
|
14
|
+
get(deviceId: string): DeviceInfo | undefined;
|
|
15
|
+
/** Get all registered devices. */
|
|
16
|
+
getAll(): DeviceInfo[];
|
|
17
|
+
/** Get devices that are currently online. */
|
|
18
|
+
getOnline(): DeviceInfo[];
|
|
19
|
+
/** Get devices that have a specific capability. */
|
|
20
|
+
getByCapability(capability: DeviceCapability): DeviceInfo[];
|
|
21
|
+
/** Update a device's connection state. */
|
|
22
|
+
setState(deviceId: string, state: DeviceConnectionState): void;
|
|
23
|
+
/** Record that a heartbeat was received from a device. */
|
|
24
|
+
heartbeat(deviceId: string): void;
|
|
25
|
+
/** Check for devices that have gone offline based on heartbeat timeout. */
|
|
26
|
+
checkTimeouts(timeoutMs: number): string[];
|
|
27
|
+
/** Get the total count of devices. */
|
|
28
|
+
get size(): number;
|
|
29
|
+
/** Check if registry has space for more devices. */
|
|
30
|
+
hasCapacity(): boolean;
|
|
31
|
+
/** Clear all devices. */
|
|
32
|
+
clear(): void;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAItF;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,UAAU,CAAS;gBAEf,UAAU,SAAK;IAI3B,sCAAsC;IACtC,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAQlC,yCAAyC;IACzC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAQrC,0BAA0B;IAC1B,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAK7C,kCAAkC;IAClC,MAAM,IAAI,UAAU,EAAE;IAItB,6CAA6C;IAC7C,SAAS,IAAI,UAAU,EAAE;IAIzB,mDAAmD;IACnD,eAAe,CAAC,UAAU,EAAE,gBAAgB,GAAG,UAAU,EAAE;IAI3D,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,IAAI;IAU9D,0DAA0D;IAC1D,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAUjC,2EAA2E;IAC3E,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE;IAe1C,sCAAsC;IACtC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,oDAAoD;IACpD,WAAW,IAAI,OAAO;IAItB,yBAAyB;IACzB,KAAK,IAAI,IAAI;CAGd"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getLogger } from '@auxiora/logger';
|
|
2
|
+
const logger = getLogger('bridge:registry');
|
|
3
|
+
/**
|
|
4
|
+
* Tracks paired devices, their capabilities, and online status.
|
|
5
|
+
*/
|
|
6
|
+
export class DeviceRegistry {
|
|
7
|
+
devices = new Map();
|
|
8
|
+
maxDevices;
|
|
9
|
+
constructor(maxDevices = 10) {
|
|
10
|
+
this.maxDevices = maxDevices;
|
|
11
|
+
}
|
|
12
|
+
/** Register a newly paired device. */
|
|
13
|
+
register(device) {
|
|
14
|
+
if (this.devices.size >= this.maxDevices && !this.devices.has(device.id)) {
|
|
15
|
+
throw new Error(`Maximum device limit (${this.maxDevices}) reached`);
|
|
16
|
+
}
|
|
17
|
+
this.devices.set(device.id, { ...device });
|
|
18
|
+
logger.info('Device registered', { deviceId: device.id, name: device.name, platform: device.platform });
|
|
19
|
+
}
|
|
20
|
+
/** Remove a device from the registry. */
|
|
21
|
+
unregister(deviceId) {
|
|
22
|
+
const removed = this.devices.delete(deviceId);
|
|
23
|
+
if (removed) {
|
|
24
|
+
logger.info('Device unregistered', { deviceId });
|
|
25
|
+
}
|
|
26
|
+
return removed;
|
|
27
|
+
}
|
|
28
|
+
/** Get a device by ID. */
|
|
29
|
+
get(deviceId) {
|
|
30
|
+
const device = this.devices.get(deviceId);
|
|
31
|
+
return device ? { ...device } : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Get all registered devices. */
|
|
34
|
+
getAll() {
|
|
35
|
+
return Array.from(this.devices.values()).map((d) => ({ ...d }));
|
|
36
|
+
}
|
|
37
|
+
/** Get devices that are currently online. */
|
|
38
|
+
getOnline() {
|
|
39
|
+
return this.getAll().filter((d) => d.state === 'online');
|
|
40
|
+
}
|
|
41
|
+
/** Get devices that have a specific capability. */
|
|
42
|
+
getByCapability(capability) {
|
|
43
|
+
return this.getAll().filter((d) => d.capabilities.includes(capability));
|
|
44
|
+
}
|
|
45
|
+
/** Update a device's connection state. */
|
|
46
|
+
setState(deviceId, state) {
|
|
47
|
+
const device = this.devices.get(deviceId);
|
|
48
|
+
if (device) {
|
|
49
|
+
device.state = state;
|
|
50
|
+
if (state === 'online') {
|
|
51
|
+
device.lastSeen = Date.now();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Record that a heartbeat was received from a device. */
|
|
56
|
+
heartbeat(deviceId) {
|
|
57
|
+
const device = this.devices.get(deviceId);
|
|
58
|
+
if (device) {
|
|
59
|
+
device.lastSeen = Date.now();
|
|
60
|
+
if (device.state !== 'online') {
|
|
61
|
+
device.state = 'online';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Check for devices that have gone offline based on heartbeat timeout. */
|
|
66
|
+
checkTimeouts(timeoutMs) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const timedOut = [];
|
|
69
|
+
for (const device of this.devices.values()) {
|
|
70
|
+
if (device.state === 'online' && now - device.lastSeen > timeoutMs) {
|
|
71
|
+
device.state = 'offline';
|
|
72
|
+
timedOut.push(device.id);
|
|
73
|
+
logger.info('Device timed out', { deviceId: device.id });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return timedOut;
|
|
77
|
+
}
|
|
78
|
+
/** Get the total count of devices. */
|
|
79
|
+
get size() {
|
|
80
|
+
return this.devices.size;
|
|
81
|
+
}
|
|
82
|
+
/** Check if registry has space for more devices. */
|
|
83
|
+
hasCapacity() {
|
|
84
|
+
return this.devices.size < this.maxDevices;
|
|
85
|
+
}
|
|
86
|
+
/** Clear all devices. */
|
|
87
|
+
clear() {
|
|
88
|
+
this.devices.clear();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { BridgeConfig, DeviceCapability, DeviceInfo, CapabilityResponsePayload } from './types.js';
|
|
2
|
+
import { DeviceRegistry } from './registry.js';
|
|
3
|
+
import { PairingFlow } from './pairing.js';
|
|
4
|
+
/** Minimal WebSocket interface for dependency injection (no tight coupling to 'ws'). */
|
|
5
|
+
export interface BridgeSocket {
|
|
6
|
+
send(data: string): void;
|
|
7
|
+
close(code?: number, reason?: string): void;
|
|
8
|
+
readyState: number;
|
|
9
|
+
}
|
|
10
|
+
/** WebSocket readyState constants. */
|
|
11
|
+
export declare const WS_OPEN = 1;
|
|
12
|
+
/** Event handler for Bridge server events. */
|
|
13
|
+
export interface BridgeEventHandler {
|
|
14
|
+
onDevicePaired?(device: DeviceInfo): void;
|
|
15
|
+
onDeviceDisconnected?(deviceId: string): void;
|
|
16
|
+
onCapabilityResponse?(deviceId: string, response: CapabilityResponsePayload): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Bridge server that manages device connections and the pairing protocol.
|
|
20
|
+
* Designed to be mounted on an existing WebSocket server (e.g., the gateway).
|
|
21
|
+
*/
|
|
22
|
+
export declare class BridgeServer {
|
|
23
|
+
private config;
|
|
24
|
+
readonly registry: DeviceRegistry;
|
|
25
|
+
readonly pairing: PairingFlow;
|
|
26
|
+
private connections;
|
|
27
|
+
private pendingRequests;
|
|
28
|
+
private heartbeatTimer;
|
|
29
|
+
private eventHandler;
|
|
30
|
+
constructor(config?: Partial<BridgeConfig>);
|
|
31
|
+
/** Set event handler. */
|
|
32
|
+
onEvent(handler: BridgeEventHandler): void;
|
|
33
|
+
/** Handle a new WebSocket connection from a device. */
|
|
34
|
+
handleConnection(socket: BridgeSocket, connectionId: string): void;
|
|
35
|
+
/** Handle a message from a connected device. */
|
|
36
|
+
handleMessage(connectionId: string, raw: string): Promise<void>;
|
|
37
|
+
/** Handle device disconnection. */
|
|
38
|
+
handleDisconnection(connectionId: string): void;
|
|
39
|
+
/** Request a capability from a specific device. */
|
|
40
|
+
requestCapability(deviceId: string, capability: DeviceCapability, action: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<CapabilityResponsePayload>;
|
|
41
|
+
/** Generate a new pairing code. */
|
|
42
|
+
generatePairingCode(): string;
|
|
43
|
+
/** Start heartbeat checking. */
|
|
44
|
+
start(): void;
|
|
45
|
+
/** Stop the bridge server. */
|
|
46
|
+
stop(): void;
|
|
47
|
+
/** Get the number of active connections. */
|
|
48
|
+
getConnectionCount(): number;
|
|
49
|
+
private handlePairRequest;
|
|
50
|
+
private handleHeartbeat;
|
|
51
|
+
private handleCapabilityResponse;
|
|
52
|
+
private handleDeviceInfo;
|
|
53
|
+
private handleDisconnect;
|
|
54
|
+
private sendTo;
|
|
55
|
+
private sendToDevice;
|
|
56
|
+
private sendError;
|
|
57
|
+
private deviceConnections;
|
|
58
|
+
private connectionDevices;
|
|
59
|
+
private setDeviceConnection;
|
|
60
|
+
private findDeviceByConnection;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAEZ,gBAAgB,EAChB,UAAU,EAIV,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAI3C,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,sCAAsC;AACtC,eAAO,MAAM,OAAO,IAAI,CAAC;AAEzB,8CAA8C;AAC9C,MAAM,WAAW,kBAAkB;IACjC,cAAc,CAAC,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1C,oBAAoB,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,oBAAoB,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,yBAAyB,GAAG,IAAI,CAAC;CACpF;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAe;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,eAAe,CAIlB;IACL,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,YAAY,CAA0B;gBAElC,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC;IAM1C,yBAAyB;IACzB,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAI1C,uDAAuD;IACvD,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAKlE,gDAAgD;IAC1C,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BrE,mCAAmC;IACnC,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;IAY/C,mDAAmD;IAC7C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,SAAS,SAAS,GACjB,OAAO,CAAC,yBAAyB,CAAC;IAgCrC,mCAAmC;IACnC,mBAAmB,IAAI,MAAM;IAK7B,gCAAgC;IAChC,KAAK,IAAI,IAAI;IAeb,8BAA8B;IAC9B,IAAI,IAAI,IAAI;IA2BZ,4CAA4C;IAC5C,kBAAkB,IAAI,MAAM;YAMd,iBAAiB;IA8C/B,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,wBAAwB;IAchC,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,MAAM;IAOd,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,iBAAiB,CAA6B;IAEtD,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,sBAAsB;CAI/B"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { getLogger } from '@auxiora/logger';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import { DEFAULT_BRIDGE_CONFIG } from './types.js';
|
|
4
|
+
import { DeviceRegistry } from './registry.js';
|
|
5
|
+
import { PairingFlow } from './pairing.js';
|
|
6
|
+
const logger = getLogger('bridge:server');
|
|
7
|
+
/** WebSocket readyState constants. */
|
|
8
|
+
export const WS_OPEN = 1;
|
|
9
|
+
/**
|
|
10
|
+
* Bridge server that manages device connections and the pairing protocol.
|
|
11
|
+
* Designed to be mounted on an existing WebSocket server (e.g., the gateway).
|
|
12
|
+
*/
|
|
13
|
+
export class BridgeServer {
|
|
14
|
+
config;
|
|
15
|
+
registry;
|
|
16
|
+
pairing;
|
|
17
|
+
connections = new Map();
|
|
18
|
+
pendingRequests = new Map();
|
|
19
|
+
heartbeatTimer = null;
|
|
20
|
+
eventHandler = {};
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.config = { ...DEFAULT_BRIDGE_CONFIG, ...config };
|
|
23
|
+
this.registry = new DeviceRegistry(this.config.maxDevices);
|
|
24
|
+
this.pairing = new PairingFlow(this.config);
|
|
25
|
+
}
|
|
26
|
+
/** Set event handler. */
|
|
27
|
+
onEvent(handler) {
|
|
28
|
+
this.eventHandler = handler;
|
|
29
|
+
}
|
|
30
|
+
/** Handle a new WebSocket connection from a device. */
|
|
31
|
+
handleConnection(socket, connectionId) {
|
|
32
|
+
this.connections.set(connectionId, socket);
|
|
33
|
+
logger.info('New bridge connection', { connectionId });
|
|
34
|
+
}
|
|
35
|
+
/** Handle a message from a connected device. */
|
|
36
|
+
async handleMessage(connectionId, raw) {
|
|
37
|
+
let message;
|
|
38
|
+
try {
|
|
39
|
+
message = JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
this.sendError(connectionId, 'Invalid message format');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
switch (message.type) {
|
|
46
|
+
case 'pair_request':
|
|
47
|
+
await this.handlePairRequest(connectionId, message);
|
|
48
|
+
break;
|
|
49
|
+
case 'heartbeat':
|
|
50
|
+
this.handleHeartbeat(connectionId, message);
|
|
51
|
+
break;
|
|
52
|
+
case 'capability_response':
|
|
53
|
+
this.handleCapabilityResponse(message);
|
|
54
|
+
break;
|
|
55
|
+
case 'device_info':
|
|
56
|
+
this.handleDeviceInfo(connectionId, message);
|
|
57
|
+
break;
|
|
58
|
+
case 'disconnect':
|
|
59
|
+
this.handleDisconnect(connectionId);
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
this.sendError(connectionId, `Unknown message type: ${message.type}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Handle device disconnection. */
|
|
66
|
+
handleDisconnection(connectionId) {
|
|
67
|
+
this.connections.delete(connectionId);
|
|
68
|
+
// Find device for this connection
|
|
69
|
+
const device = this.findDeviceByConnection(connectionId);
|
|
70
|
+
if (device) {
|
|
71
|
+
this.registry.setState(device.id, 'offline');
|
|
72
|
+
this.eventHandler.onDeviceDisconnected?.(device.id);
|
|
73
|
+
logger.info('Device disconnected', { deviceId: device.id });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Request a capability from a specific device. */
|
|
77
|
+
async requestCapability(deviceId, capability, action, params, timeoutMs = 30_000) {
|
|
78
|
+
const device = this.registry.get(deviceId);
|
|
79
|
+
if (!device) {
|
|
80
|
+
throw new Error(`Device not found: ${deviceId}`);
|
|
81
|
+
}
|
|
82
|
+
if (device.state !== 'online') {
|
|
83
|
+
throw new Error(`Device is not online: ${deviceId}`);
|
|
84
|
+
}
|
|
85
|
+
if (!device.capabilities.includes(capability)) {
|
|
86
|
+
throw new Error(`Device ${deviceId} does not have capability: ${capability}`);
|
|
87
|
+
}
|
|
88
|
+
const requestId = crypto.randomUUID();
|
|
89
|
+
const request = {
|
|
90
|
+
type: 'capability_request',
|
|
91
|
+
id: requestId,
|
|
92
|
+
deviceId,
|
|
93
|
+
payload: { capability, action, params },
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
};
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
this.pendingRequests.delete(requestId);
|
|
99
|
+
reject(new Error(`Capability request timed out: ${capability}/${action}`));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
this.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
102
|
+
this.sendToDevice(deviceId, request);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Generate a new pairing code. */
|
|
106
|
+
generatePairingCode() {
|
|
107
|
+
const pc = this.pairing.generateCode();
|
|
108
|
+
return pc.code;
|
|
109
|
+
}
|
|
110
|
+
/** Start heartbeat checking. */
|
|
111
|
+
start() {
|
|
112
|
+
this.pairing.startCleanup();
|
|
113
|
+
const interval = this.config.heartbeatIntervalMs;
|
|
114
|
+
const timeout = interval * this.config.offlineAfterMissedHeartbeats;
|
|
115
|
+
this.heartbeatTimer = setInterval(() => {
|
|
116
|
+
const timedOut = this.registry.checkTimeouts(timeout);
|
|
117
|
+
for (const deviceId of timedOut) {
|
|
118
|
+
this.eventHandler.onDeviceDisconnected?.(deviceId);
|
|
119
|
+
}
|
|
120
|
+
}, interval);
|
|
121
|
+
logger.info('Bridge server started');
|
|
122
|
+
}
|
|
123
|
+
/** Stop the bridge server. */
|
|
124
|
+
stop() {
|
|
125
|
+
if (this.heartbeatTimer) {
|
|
126
|
+
clearInterval(this.heartbeatTimer);
|
|
127
|
+
this.heartbeatTimer = null;
|
|
128
|
+
}
|
|
129
|
+
this.pairing.destroy();
|
|
130
|
+
// Reject all pending requests
|
|
131
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
132
|
+
clearTimeout(pending.timer);
|
|
133
|
+
pending.reject(new Error('Bridge server stopped'));
|
|
134
|
+
this.pendingRequests.delete(id);
|
|
135
|
+
}
|
|
136
|
+
// Close all connections
|
|
137
|
+
for (const [id, socket] of this.connections) {
|
|
138
|
+
try {
|
|
139
|
+
socket.close(1001, 'Bridge server stopping');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Ignore close errors
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
this.connections.clear();
|
|
146
|
+
logger.info('Bridge server stopped');
|
|
147
|
+
}
|
|
148
|
+
/** Get the number of active connections. */
|
|
149
|
+
getConnectionCount() {
|
|
150
|
+
return this.connections.size;
|
|
151
|
+
}
|
|
152
|
+
// --- Private helpers ---
|
|
153
|
+
async handlePairRequest(connectionId, message) {
|
|
154
|
+
const payload = message.payload;
|
|
155
|
+
if (!payload?.code || !payload?.deviceName || !payload?.platform) {
|
|
156
|
+
this.sendError(connectionId, 'Invalid pair request: missing required fields');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const valid = this.pairing.consume(payload.code);
|
|
160
|
+
if (!valid) {
|
|
161
|
+
this.sendTo(connectionId, {
|
|
162
|
+
type: 'pair_rejected',
|
|
163
|
+
id: message.id,
|
|
164
|
+
payload: { reason: 'Invalid or expired pairing code' },
|
|
165
|
+
timestamp: Date.now(),
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const deviceId = crypto.randomUUID();
|
|
170
|
+
const device = {
|
|
171
|
+
id: deviceId,
|
|
172
|
+
name: payload.deviceName,
|
|
173
|
+
platform: payload.platform,
|
|
174
|
+
capabilities: payload.capabilities ?? [],
|
|
175
|
+
state: 'online',
|
|
176
|
+
pairedAt: Date.now(),
|
|
177
|
+
lastSeen: Date.now(),
|
|
178
|
+
};
|
|
179
|
+
this.registry.register(device);
|
|
180
|
+
// Associate this connection with the device
|
|
181
|
+
this.setDeviceConnection(deviceId, connectionId);
|
|
182
|
+
this.sendTo(connectionId, {
|
|
183
|
+
type: 'pair_accepted',
|
|
184
|
+
id: message.id,
|
|
185
|
+
deviceId,
|
|
186
|
+
payload: { deviceId, name: device.name },
|
|
187
|
+
timestamp: Date.now(),
|
|
188
|
+
});
|
|
189
|
+
this.eventHandler.onDevicePaired?.(device);
|
|
190
|
+
logger.info('Device paired', { deviceId, name: device.name, platform: device.platform });
|
|
191
|
+
}
|
|
192
|
+
handleHeartbeat(connectionId, message) {
|
|
193
|
+
const device = this.findDeviceByConnection(connectionId);
|
|
194
|
+
if (device) {
|
|
195
|
+
this.registry.heartbeat(device.id);
|
|
196
|
+
this.sendTo(connectionId, {
|
|
197
|
+
type: 'heartbeat_ack',
|
|
198
|
+
id: message.id,
|
|
199
|
+
deviceId: device.id,
|
|
200
|
+
timestamp: Date.now(),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
handleCapabilityResponse(message) {
|
|
205
|
+
if (!message.id)
|
|
206
|
+
return;
|
|
207
|
+
const pending = this.pendingRequests.get(message.id);
|
|
208
|
+
if (pending) {
|
|
209
|
+
clearTimeout(pending.timer);
|
|
210
|
+
this.pendingRequests.delete(message.id);
|
|
211
|
+
pending.resolve(message.payload);
|
|
212
|
+
this.eventHandler.onCapabilityResponse?.(message.deviceId ?? '', message.payload);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
handleDeviceInfo(connectionId, message) {
|
|
216
|
+
const device = this.findDeviceByConnection(connectionId);
|
|
217
|
+
if (!device)
|
|
218
|
+
return;
|
|
219
|
+
const info = message.payload;
|
|
220
|
+
if (info?.capabilities) {
|
|
221
|
+
// Re-register with updated capabilities
|
|
222
|
+
const existing = this.registry.get(device.id);
|
|
223
|
+
if (existing) {
|
|
224
|
+
existing.capabilities = info.capabilities;
|
|
225
|
+
if (info.name)
|
|
226
|
+
existing.name = info.name;
|
|
227
|
+
this.registry.register(existing);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
handleDisconnect(connectionId) {
|
|
232
|
+
this.handleDisconnection(connectionId);
|
|
233
|
+
}
|
|
234
|
+
sendTo(connectionId, message) {
|
|
235
|
+
const socket = this.connections.get(connectionId);
|
|
236
|
+
if (socket && socket.readyState === WS_OPEN) {
|
|
237
|
+
socket.send(JSON.stringify(message));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
sendToDevice(deviceId, message) {
|
|
241
|
+
const connectionId = this.deviceConnections.get(deviceId);
|
|
242
|
+
if (connectionId) {
|
|
243
|
+
this.sendTo(connectionId, message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
sendError(connectionId, errorMessage) {
|
|
247
|
+
this.sendTo(connectionId, {
|
|
248
|
+
type: 'error',
|
|
249
|
+
payload: { message: errorMessage },
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
// Device-to-connection mapping
|
|
254
|
+
deviceConnections = new Map();
|
|
255
|
+
connectionDevices = new Map();
|
|
256
|
+
setDeviceConnection(deviceId, connectionId) {
|
|
257
|
+
this.deviceConnections.set(deviceId, connectionId);
|
|
258
|
+
this.connectionDevices.set(connectionId, deviceId);
|
|
259
|
+
}
|
|
260
|
+
findDeviceByConnection(connectionId) {
|
|
261
|
+
const deviceId = this.connectionDevices.get(connectionId);
|
|
262
|
+
return deviceId ? this.registry.get(deviceId) : undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|