@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/src/registry.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getLogger } from '@auxiora/logger';
|
|
2
|
+
import type { DeviceInfo, DeviceCapability, DeviceConnectionState } from './types.js';
|
|
3
|
+
|
|
4
|
+
const logger = getLogger('bridge:registry');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tracks paired devices, their capabilities, and online status.
|
|
8
|
+
*/
|
|
9
|
+
export class DeviceRegistry {
|
|
10
|
+
private devices = new Map<string, DeviceInfo>();
|
|
11
|
+
private maxDevices: number;
|
|
12
|
+
|
|
13
|
+
constructor(maxDevices = 10) {
|
|
14
|
+
this.maxDevices = maxDevices;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Register a newly paired device. */
|
|
18
|
+
register(device: DeviceInfo): void {
|
|
19
|
+
if (this.devices.size >= this.maxDevices && !this.devices.has(device.id)) {
|
|
20
|
+
throw new Error(`Maximum device limit (${this.maxDevices}) reached`);
|
|
21
|
+
}
|
|
22
|
+
this.devices.set(device.id, { ...device });
|
|
23
|
+
logger.info('Device registered', { deviceId: device.id, name: device.name, platform: device.platform });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Remove a device from the registry. */
|
|
27
|
+
unregister(deviceId: string): boolean {
|
|
28
|
+
const removed = this.devices.delete(deviceId);
|
|
29
|
+
if (removed) {
|
|
30
|
+
logger.info('Device unregistered', { deviceId });
|
|
31
|
+
}
|
|
32
|
+
return removed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Get a device by ID. */
|
|
36
|
+
get(deviceId: string): DeviceInfo | undefined {
|
|
37
|
+
const device = this.devices.get(deviceId);
|
|
38
|
+
return device ? { ...device } : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Get all registered devices. */
|
|
42
|
+
getAll(): DeviceInfo[] {
|
|
43
|
+
return Array.from(this.devices.values()).map((d) => ({ ...d }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Get devices that are currently online. */
|
|
47
|
+
getOnline(): DeviceInfo[] {
|
|
48
|
+
return this.getAll().filter((d) => d.state === 'online');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Get devices that have a specific capability. */
|
|
52
|
+
getByCapability(capability: DeviceCapability): DeviceInfo[] {
|
|
53
|
+
return this.getAll().filter((d) => d.capabilities.includes(capability));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Update a device's connection state. */
|
|
57
|
+
setState(deviceId: string, state: DeviceConnectionState): void {
|
|
58
|
+
const device = this.devices.get(deviceId);
|
|
59
|
+
if (device) {
|
|
60
|
+
device.state = state;
|
|
61
|
+
if (state === 'online') {
|
|
62
|
+
device.lastSeen = Date.now();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Record that a heartbeat was received from a device. */
|
|
68
|
+
heartbeat(deviceId: string): void {
|
|
69
|
+
const device = this.devices.get(deviceId);
|
|
70
|
+
if (device) {
|
|
71
|
+
device.lastSeen = Date.now();
|
|
72
|
+
if (device.state !== 'online') {
|
|
73
|
+
device.state = 'online';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Check for devices that have gone offline based on heartbeat timeout. */
|
|
79
|
+
checkTimeouts(timeoutMs: number): string[] {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const timedOut: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const device of this.devices.values()) {
|
|
84
|
+
if (device.state === 'online' && now - device.lastSeen > timeoutMs) {
|
|
85
|
+
device.state = 'offline';
|
|
86
|
+
timedOut.push(device.id);
|
|
87
|
+
logger.info('Device timed out', { deviceId: device.id });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return timedOut;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Get the total count of devices. */
|
|
95
|
+
get size(): number {
|
|
96
|
+
return this.devices.size;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Check if registry has space for more devices. */
|
|
100
|
+
hasCapacity(): boolean {
|
|
101
|
+
return this.devices.size < this.maxDevices;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Clear all devices. */
|
|
105
|
+
clear(): void {
|
|
106
|
+
this.devices.clear();
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { getLogger } from '@auxiora/logger';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import type {
|
|
4
|
+
BridgeConfig,
|
|
5
|
+
BridgeMessage,
|
|
6
|
+
DeviceCapability,
|
|
7
|
+
DeviceInfo,
|
|
8
|
+
DevicePlatform,
|
|
9
|
+
PairRequestPayload,
|
|
10
|
+
CapabilityRequestPayload,
|
|
11
|
+
CapabilityResponsePayload,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
import { DEFAULT_BRIDGE_CONFIG } from './types.js';
|
|
14
|
+
import { DeviceRegistry } from './registry.js';
|
|
15
|
+
import { PairingFlow } from './pairing.js';
|
|
16
|
+
|
|
17
|
+
const logger = getLogger('bridge:server');
|
|
18
|
+
|
|
19
|
+
/** Minimal WebSocket interface for dependency injection (no tight coupling to 'ws'). */
|
|
20
|
+
export interface BridgeSocket {
|
|
21
|
+
send(data: string): void;
|
|
22
|
+
close(code?: number, reason?: string): void;
|
|
23
|
+
readyState: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** WebSocket readyState constants. */
|
|
27
|
+
export const WS_OPEN = 1;
|
|
28
|
+
|
|
29
|
+
/** Event handler for Bridge server events. */
|
|
30
|
+
export interface BridgeEventHandler {
|
|
31
|
+
onDevicePaired?(device: DeviceInfo): void;
|
|
32
|
+
onDeviceDisconnected?(deviceId: string): void;
|
|
33
|
+
onCapabilityResponse?(deviceId: string, response: CapabilityResponsePayload): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bridge server that manages device connections and the pairing protocol.
|
|
38
|
+
* Designed to be mounted on an existing WebSocket server (e.g., the gateway).
|
|
39
|
+
*/
|
|
40
|
+
export class BridgeServer {
|
|
41
|
+
private config: BridgeConfig;
|
|
42
|
+
readonly registry: DeviceRegistry;
|
|
43
|
+
readonly pairing: PairingFlow;
|
|
44
|
+
private connections = new Map<string, BridgeSocket>();
|
|
45
|
+
private pendingRequests = new Map<string, {
|
|
46
|
+
resolve: (response: CapabilityResponsePayload) => void;
|
|
47
|
+
reject: (error: Error) => void;
|
|
48
|
+
timer: ReturnType<typeof setTimeout>;
|
|
49
|
+
}>();
|
|
50
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
51
|
+
private eventHandler: BridgeEventHandler = {};
|
|
52
|
+
|
|
53
|
+
constructor(config?: Partial<BridgeConfig>) {
|
|
54
|
+
this.config = { ...DEFAULT_BRIDGE_CONFIG, ...config };
|
|
55
|
+
this.registry = new DeviceRegistry(this.config.maxDevices);
|
|
56
|
+
this.pairing = new PairingFlow(this.config);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Set event handler. */
|
|
60
|
+
onEvent(handler: BridgeEventHandler): void {
|
|
61
|
+
this.eventHandler = handler;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Handle a new WebSocket connection from a device. */
|
|
65
|
+
handleConnection(socket: BridgeSocket, connectionId: string): void {
|
|
66
|
+
this.connections.set(connectionId, socket);
|
|
67
|
+
logger.info('New bridge connection', { connectionId });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Handle a message from a connected device. */
|
|
71
|
+
async handleMessage(connectionId: string, raw: string): Promise<void> {
|
|
72
|
+
let message: BridgeMessage;
|
|
73
|
+
try {
|
|
74
|
+
message = JSON.parse(raw) as BridgeMessage;
|
|
75
|
+
} catch {
|
|
76
|
+
this.sendError(connectionId, 'Invalid message format');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
switch (message.type) {
|
|
81
|
+
case 'pair_request':
|
|
82
|
+
await this.handlePairRequest(connectionId, message);
|
|
83
|
+
break;
|
|
84
|
+
case 'heartbeat':
|
|
85
|
+
this.handleHeartbeat(connectionId, message);
|
|
86
|
+
break;
|
|
87
|
+
case 'capability_response':
|
|
88
|
+
this.handleCapabilityResponse(message);
|
|
89
|
+
break;
|
|
90
|
+
case 'device_info':
|
|
91
|
+
this.handleDeviceInfo(connectionId, message);
|
|
92
|
+
break;
|
|
93
|
+
case 'disconnect':
|
|
94
|
+
this.handleDisconnect(connectionId);
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
this.sendError(connectionId, `Unknown message type: ${message.type}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Handle device disconnection. */
|
|
102
|
+
handleDisconnection(connectionId: string): void {
|
|
103
|
+
this.connections.delete(connectionId);
|
|
104
|
+
|
|
105
|
+
// Find device for this connection
|
|
106
|
+
const device = this.findDeviceByConnection(connectionId);
|
|
107
|
+
if (device) {
|
|
108
|
+
this.registry.setState(device.id, 'offline');
|
|
109
|
+
this.eventHandler.onDeviceDisconnected?.(device.id);
|
|
110
|
+
logger.info('Device disconnected', { deviceId: device.id });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Request a capability from a specific device. */
|
|
115
|
+
async requestCapability(
|
|
116
|
+
deviceId: string,
|
|
117
|
+
capability: DeviceCapability,
|
|
118
|
+
action: string,
|
|
119
|
+
params?: Record<string, unknown>,
|
|
120
|
+
timeoutMs = 30_000,
|
|
121
|
+
): Promise<CapabilityResponsePayload> {
|
|
122
|
+
const device = this.registry.get(deviceId);
|
|
123
|
+
if (!device) {
|
|
124
|
+
throw new Error(`Device not found: ${deviceId}`);
|
|
125
|
+
}
|
|
126
|
+
if (device.state !== 'online') {
|
|
127
|
+
throw new Error(`Device is not online: ${deviceId}`);
|
|
128
|
+
}
|
|
129
|
+
if (!device.capabilities.includes(capability)) {
|
|
130
|
+
throw new Error(`Device ${deviceId} does not have capability: ${capability}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const requestId = crypto.randomUUID();
|
|
134
|
+
const request: BridgeMessage = {
|
|
135
|
+
type: 'capability_request',
|
|
136
|
+
id: requestId,
|
|
137
|
+
deviceId,
|
|
138
|
+
payload: { capability, action, params } satisfies CapabilityRequestPayload,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return new Promise<CapabilityResponsePayload>((resolve, reject) => {
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
this.pendingRequests.delete(requestId);
|
|
145
|
+
reject(new Error(`Capability request timed out: ${capability}/${action}`));
|
|
146
|
+
}, timeoutMs);
|
|
147
|
+
|
|
148
|
+
this.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
149
|
+
this.sendToDevice(deviceId, request);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Generate a new pairing code. */
|
|
154
|
+
generatePairingCode(): string {
|
|
155
|
+
const pc = this.pairing.generateCode();
|
|
156
|
+
return pc.code;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Start heartbeat checking. */
|
|
160
|
+
start(): void {
|
|
161
|
+
this.pairing.startCleanup();
|
|
162
|
+
const interval = this.config.heartbeatIntervalMs;
|
|
163
|
+
const timeout = interval * this.config.offlineAfterMissedHeartbeats;
|
|
164
|
+
|
|
165
|
+
this.heartbeatTimer = setInterval(() => {
|
|
166
|
+
const timedOut = this.registry.checkTimeouts(timeout);
|
|
167
|
+
for (const deviceId of timedOut) {
|
|
168
|
+
this.eventHandler.onDeviceDisconnected?.(deviceId);
|
|
169
|
+
}
|
|
170
|
+
}, interval);
|
|
171
|
+
|
|
172
|
+
logger.info('Bridge server started');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Stop the bridge server. */
|
|
176
|
+
stop(): void {
|
|
177
|
+
if (this.heartbeatTimer) {
|
|
178
|
+
clearInterval(this.heartbeatTimer);
|
|
179
|
+
this.heartbeatTimer = null;
|
|
180
|
+
}
|
|
181
|
+
this.pairing.destroy();
|
|
182
|
+
|
|
183
|
+
// Reject all pending requests
|
|
184
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
185
|
+
clearTimeout(pending.timer);
|
|
186
|
+
pending.reject(new Error('Bridge server stopped'));
|
|
187
|
+
this.pendingRequests.delete(id);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Close all connections
|
|
191
|
+
for (const [id, socket] of this.connections) {
|
|
192
|
+
try {
|
|
193
|
+
socket.close(1001, 'Bridge server stopping');
|
|
194
|
+
} catch {
|
|
195
|
+
// Ignore close errors
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.connections.clear();
|
|
199
|
+
|
|
200
|
+
logger.info('Bridge server stopped');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Get the number of active connections. */
|
|
204
|
+
getConnectionCount(): number {
|
|
205
|
+
return this.connections.size;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Private helpers ---
|
|
209
|
+
|
|
210
|
+
private async handlePairRequest(connectionId: string, message: BridgeMessage): Promise<void> {
|
|
211
|
+
const payload = message.payload as PairRequestPayload;
|
|
212
|
+
if (!payload?.code || !payload?.deviceName || !payload?.platform) {
|
|
213
|
+
this.sendError(connectionId, 'Invalid pair request: missing required fields');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const valid = this.pairing.consume(payload.code);
|
|
218
|
+
if (!valid) {
|
|
219
|
+
this.sendTo(connectionId, {
|
|
220
|
+
type: 'pair_rejected',
|
|
221
|
+
id: message.id,
|
|
222
|
+
payload: { reason: 'Invalid or expired pairing code' },
|
|
223
|
+
timestamp: Date.now(),
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const deviceId = crypto.randomUUID();
|
|
229
|
+
const device: DeviceInfo = {
|
|
230
|
+
id: deviceId,
|
|
231
|
+
name: payload.deviceName,
|
|
232
|
+
platform: payload.platform,
|
|
233
|
+
capabilities: payload.capabilities ?? [],
|
|
234
|
+
state: 'online',
|
|
235
|
+
pairedAt: Date.now(),
|
|
236
|
+
lastSeen: Date.now(),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
this.registry.register(device);
|
|
240
|
+
|
|
241
|
+
// Associate this connection with the device
|
|
242
|
+
this.setDeviceConnection(deviceId, connectionId);
|
|
243
|
+
|
|
244
|
+
this.sendTo(connectionId, {
|
|
245
|
+
type: 'pair_accepted',
|
|
246
|
+
id: message.id,
|
|
247
|
+
deviceId,
|
|
248
|
+
payload: { deviceId, name: device.name },
|
|
249
|
+
timestamp: Date.now(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
this.eventHandler.onDevicePaired?.(device);
|
|
253
|
+
logger.info('Device paired', { deviceId, name: device.name, platform: device.platform });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private handleHeartbeat(connectionId: string, message: BridgeMessage): void {
|
|
257
|
+
const device = this.findDeviceByConnection(connectionId);
|
|
258
|
+
if (device) {
|
|
259
|
+
this.registry.heartbeat(device.id);
|
|
260
|
+
this.sendTo(connectionId, {
|
|
261
|
+
type: 'heartbeat_ack',
|
|
262
|
+
id: message.id,
|
|
263
|
+
deviceId: device.id,
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private handleCapabilityResponse(message: BridgeMessage): void {
|
|
270
|
+
if (!message.id) return;
|
|
271
|
+
const pending = this.pendingRequests.get(message.id);
|
|
272
|
+
if (pending) {
|
|
273
|
+
clearTimeout(pending.timer);
|
|
274
|
+
this.pendingRequests.delete(message.id);
|
|
275
|
+
pending.resolve(message.payload as CapabilityResponsePayload);
|
|
276
|
+
this.eventHandler.onCapabilityResponse?.(
|
|
277
|
+
message.deviceId ?? '',
|
|
278
|
+
message.payload as CapabilityResponsePayload,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private handleDeviceInfo(connectionId: string, message: BridgeMessage): void {
|
|
284
|
+
const device = this.findDeviceByConnection(connectionId);
|
|
285
|
+
if (!device) return;
|
|
286
|
+
|
|
287
|
+
const info = message.payload as Partial<{ name: string; capabilities: DeviceCapability[] }>;
|
|
288
|
+
if (info?.capabilities) {
|
|
289
|
+
// Re-register with updated capabilities
|
|
290
|
+
const existing = this.registry.get(device.id);
|
|
291
|
+
if (existing) {
|
|
292
|
+
existing.capabilities = info.capabilities;
|
|
293
|
+
if (info.name) existing.name = info.name;
|
|
294
|
+
this.registry.register(existing);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private handleDisconnect(connectionId: string): void {
|
|
300
|
+
this.handleDisconnection(connectionId);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private sendTo(connectionId: string, message: BridgeMessage): void {
|
|
304
|
+
const socket = this.connections.get(connectionId);
|
|
305
|
+
if (socket && socket.readyState === WS_OPEN) {
|
|
306
|
+
socket.send(JSON.stringify(message));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private sendToDevice(deviceId: string, message: BridgeMessage): void {
|
|
311
|
+
const connectionId = this.deviceConnections.get(deviceId);
|
|
312
|
+
if (connectionId) {
|
|
313
|
+
this.sendTo(connectionId, message);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private sendError(connectionId: string, errorMessage: string): void {
|
|
318
|
+
this.sendTo(connectionId, {
|
|
319
|
+
type: 'error',
|
|
320
|
+
payload: { message: errorMessage },
|
|
321
|
+
timestamp: Date.now(),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Device-to-connection mapping
|
|
326
|
+
private deviceConnections = new Map<string, string>();
|
|
327
|
+
private connectionDevices = new Map<string, string>();
|
|
328
|
+
|
|
329
|
+
private setDeviceConnection(deviceId: string, connectionId: string): void {
|
|
330
|
+
this.deviceConnections.set(deviceId, connectionId);
|
|
331
|
+
this.connectionDevices.set(connectionId, deviceId);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private findDeviceByConnection(connectionId: string): DeviceInfo | undefined {
|
|
335
|
+
const deviceId = this.connectionDevices.get(connectionId);
|
|
336
|
+
return deviceId ? this.registry.get(deviceId) : undefined;
|
|
337
|
+
}
|
|
338
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** Capabilities a device node can provide. */
|
|
2
|
+
export type DeviceCapability =
|
|
3
|
+
| 'camera'
|
|
4
|
+
| 'screen'
|
|
5
|
+
| 'microphone'
|
|
6
|
+
| 'location'
|
|
7
|
+
| 'notifications'
|
|
8
|
+
| 'clipboard'
|
|
9
|
+
| 'sensors';
|
|
10
|
+
|
|
11
|
+
/** Platform of the device. */
|
|
12
|
+
export type DevicePlatform = 'ios' | 'android' | 'macos' | 'windows' | 'linux' | 'web';
|
|
13
|
+
|
|
14
|
+
/** Connection state for a device. */
|
|
15
|
+
export type DeviceConnectionState = 'connecting' | 'paired' | 'online' | 'offline';
|
|
16
|
+
|
|
17
|
+
/** Information about a paired device. */
|
|
18
|
+
export interface DeviceInfo {
|
|
19
|
+
/** Unique device identifier. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** User-facing device name. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Device platform. */
|
|
24
|
+
platform: DevicePlatform;
|
|
25
|
+
/** Capabilities this device supports. */
|
|
26
|
+
capabilities: DeviceCapability[];
|
|
27
|
+
/** Current connection state. */
|
|
28
|
+
state: DeviceConnectionState;
|
|
29
|
+
/** When the device was first paired. */
|
|
30
|
+
pairedAt: number;
|
|
31
|
+
/** When the device was last seen online. */
|
|
32
|
+
lastSeen: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pairing code issued by the server. */
|
|
36
|
+
export interface PairingCode {
|
|
37
|
+
/** The short code the user enters on the device. */
|
|
38
|
+
code: string;
|
|
39
|
+
/** When the code expires (unix ms). */
|
|
40
|
+
expiresAt: number;
|
|
41
|
+
/** Whether the code has been used. */
|
|
42
|
+
used: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Configuration for the Bridge server. */
|
|
46
|
+
export interface BridgeConfig {
|
|
47
|
+
/** Maximum number of paired devices. */
|
|
48
|
+
maxDevices: number;
|
|
49
|
+
/** Pairing code length (digits). */
|
|
50
|
+
codeLength: number;
|
|
51
|
+
/** Pairing code expiry in seconds. */
|
|
52
|
+
codeExpirySeconds: number;
|
|
53
|
+
/** Heartbeat interval in milliseconds. */
|
|
54
|
+
heartbeatIntervalMs: number;
|
|
55
|
+
/** Consider device offline after this many missed heartbeats. */
|
|
56
|
+
offlineAfterMissedHeartbeats: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_BRIDGE_CONFIG: BridgeConfig = {
|
|
60
|
+
maxDevices: 10,
|
|
61
|
+
codeLength: 6,
|
|
62
|
+
codeExpirySeconds: 300,
|
|
63
|
+
heartbeatIntervalMs: 30_000,
|
|
64
|
+
offlineAfterMissedHeartbeats: 3,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Messages sent between Bridge server and device nodes. */
|
|
68
|
+
export type BridgeMessageType =
|
|
69
|
+
| 'pair_request'
|
|
70
|
+
| 'pair_accepted'
|
|
71
|
+
| 'pair_rejected'
|
|
72
|
+
| 'heartbeat'
|
|
73
|
+
| 'heartbeat_ack'
|
|
74
|
+
| 'capability_request'
|
|
75
|
+
| 'capability_response'
|
|
76
|
+
| 'device_info'
|
|
77
|
+
| 'disconnect'
|
|
78
|
+
| 'error';
|
|
79
|
+
|
|
80
|
+
/** A message in the Bridge protocol. */
|
|
81
|
+
export interface BridgeMessage {
|
|
82
|
+
type: BridgeMessageType;
|
|
83
|
+
/** Correlation ID for request/response matching. */
|
|
84
|
+
id?: string;
|
|
85
|
+
/** The sending device ID (set by server for forwarded messages). */
|
|
86
|
+
deviceId?: string;
|
|
87
|
+
/** Message-specific payload. */
|
|
88
|
+
payload?: unknown;
|
|
89
|
+
/** Timestamp (unix ms). */
|
|
90
|
+
timestamp: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Payload for pair_request. */
|
|
94
|
+
export interface PairRequestPayload {
|
|
95
|
+
code: string;
|
|
96
|
+
deviceName: string;
|
|
97
|
+
platform: DevicePlatform;
|
|
98
|
+
capabilities: DeviceCapability[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Payload for capability_request. */
|
|
102
|
+
export interface CapabilityRequestPayload {
|
|
103
|
+
capability: DeviceCapability;
|
|
104
|
+
action: string;
|
|
105
|
+
params?: Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Payload for capability_response. */
|
|
109
|
+
export interface CapabilityResponsePayload {
|
|
110
|
+
capability: DeviceCapability;
|
|
111
|
+
action: string;
|
|
112
|
+
success: boolean;
|
|
113
|
+
data?: unknown;
|
|
114
|
+
error?: string;
|
|
115
|
+
}
|