@principal-ai/control-tower-core 0.2.1 → 0.2.2
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/generated/client-connection-auth.types.d.ts +312 -0
- package/dist/generated/client-connection-auth.types.d.ts.map +1 -0
- package/dist/generated/client-connection-auth.types.js +11 -0
- package/dist/generated/control-tower-execution.types.d.ts +445 -0
- package/dist/generated/control-tower-execution.types.d.ts.map +1 -0
- package/dist/generated/control-tower-execution.types.js +11 -0
- package/dist/index.js.map +3 -3
- package/dist/index.mjs +39 -6
- package/dist/index.mjs.map +3 -3
- package/dist/server/BaseServer.d.ts +22 -2
- package/dist/server/BaseServer.d.ts.map +1 -1
- package/dist/server/BaseServer.js +63 -8
- package/dist/telemetry/EventValidationIntegration.d.ts +135 -0
- package/dist/telemetry/EventValidationIntegration.d.ts.map +1 -0
- package/dist/telemetry/EventValidationIntegration.js +253 -0
- package/dist/telemetry/EventValidationIntegration.test.d.ts +7 -0
- package/dist/telemetry/EventValidationIntegration.test.d.ts.map +1 -0
- package/dist/telemetry/EventValidationIntegration.test.js +322 -0
- package/dist/telemetry/TelemetryCapture.d.ts +268 -0
- package/dist/telemetry/TelemetryCapture.d.ts.map +1 -0
- package/dist/telemetry/TelemetryCapture.js +263 -0
- package/dist/telemetry/TelemetryCapture.test.d.ts +7 -0
- package/dist/telemetry/TelemetryCapture.test.d.ts.map +1 -0
- package/dist/telemetry/TelemetryCapture.test.js +396 -0
- package/dist/telemetry-example.d.ts +33 -0
- package/dist/telemetry-example.d.ts.map +1 -0
- package/dist/telemetry-example.js +124 -0
- package/package.json +1 -1
- package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts +0 -60
- package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts.map +0 -1
- package/dist/adapters/websocket/WebSocketTransportAdapter.js +0 -386
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Using Generated Event Types
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates how to use the generated types for type-safe telemetry
|
|
5
|
+
* in the Control Tower server.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Example usage in message handler
|
|
9
|
+
*/
|
|
10
|
+
export declare class MessageHandlerWithTelemetry {
|
|
11
|
+
private emit;
|
|
12
|
+
handleMessage(clientId: string, messageType: string, messageSize: number): void;
|
|
13
|
+
private validateMessage;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Example usage in client lifecycle manager
|
|
17
|
+
*/
|
|
18
|
+
export declare class ClientLifecycleWithTelemetry {
|
|
19
|
+
private emit;
|
|
20
|
+
handleConnect(clientId: string, transportType: string): void;
|
|
21
|
+
handleAuthentication(clientId: string, userId: string, authMethod: string): void;
|
|
22
|
+
handleDisconnect(clientId: string, reason?: string, duration?: number): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Example with multiple event type handlers
|
|
26
|
+
*/
|
|
27
|
+
export declare class ControlTowerWithTelemetry {
|
|
28
|
+
private messageEmit;
|
|
29
|
+
private clientEmit;
|
|
30
|
+
handleNewClient(clientId: string, transportType: string): void;
|
|
31
|
+
handleMessage(clientId: string, messageType: string): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=telemetry-example.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-example.d.ts","sourceRoot":"","sources":["../src/telemetry-example.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA+BH;;GAEG;AACH,qBAAa,2BAA2B;IACtC,OAAO,CAAC,IAAI,CAAiC;IAE7C,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;IA0BxE,OAAO,CAAC,eAAe;CAIxB;AAED;;GAEG;AACH,qBAAa,4BAA4B;IACvC,OAAO,CAAC,IAAI,CAAkC;IAE9C,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;IASrD,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IASzE,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;CAQtE;AAED;;GAEG;AACH,qBAAa,yBAAyB;IACpC,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,UAAU,CAAkC;IAEpD,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;IASvD,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;CAWpD"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Example: Using Generated Event Types
|
|
4
|
+
*
|
|
5
|
+
* This demonstrates how to use the generated types for type-safe telemetry
|
|
6
|
+
* in the Control Tower server.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ControlTowerWithTelemetry = exports.ClientLifecycleWithTelemetry = exports.MessageHandlerWithTelemetry = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Type-safe emitter for message handler events
|
|
12
|
+
*/
|
|
13
|
+
function createMessageHandlerEmitter() {
|
|
14
|
+
return (eventName, attributes) => {
|
|
15
|
+
// In production, this would emit to OpenTelemetry or your observability platform
|
|
16
|
+
console.log(`[MessageHandler] ${eventName}`, attributes);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Type-safe emitter for client lifecycle events
|
|
21
|
+
*/
|
|
22
|
+
function createClientLifecycleEmitter() {
|
|
23
|
+
return (eventName, attributes) => {
|
|
24
|
+
console.log(`[ClientLifecycle] ${eventName}`, attributes);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Example usage in message handler
|
|
29
|
+
*/
|
|
30
|
+
class MessageHandlerWithTelemetry {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.emit = createMessageHandlerEmitter();
|
|
33
|
+
}
|
|
34
|
+
handleMessage(clientId, messageType, messageSize) {
|
|
35
|
+
// ✅ Type-safe: TypeScript knows the exact attributes for this event
|
|
36
|
+
this.emit('message.received', {
|
|
37
|
+
'client.id': clientId,
|
|
38
|
+
'message.type': messageType,
|
|
39
|
+
'message.size': messageSize, // Optional field
|
|
40
|
+
});
|
|
41
|
+
// Validate message...
|
|
42
|
+
const isValid = this.validateMessage(messageType);
|
|
43
|
+
if (isValid) {
|
|
44
|
+
this.emit('message.validated', {
|
|
45
|
+
'client.id': clientId,
|
|
46
|
+
'message.type': messageType,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.emit('message.validation_failed', {
|
|
51
|
+
'client.id': clientId,
|
|
52
|
+
'message.type': messageType,
|
|
53
|
+
'error.message': 'Invalid message schema',
|
|
54
|
+
'error.field': 'payload', // Optional field
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
validateMessage(messageType) {
|
|
59
|
+
// Mock validation
|
|
60
|
+
return messageType !== 'invalid';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.MessageHandlerWithTelemetry = MessageHandlerWithTelemetry;
|
|
64
|
+
/**
|
|
65
|
+
* Example usage in client lifecycle manager
|
|
66
|
+
*/
|
|
67
|
+
class ClientLifecycleWithTelemetry {
|
|
68
|
+
constructor() {
|
|
69
|
+
this.emit = createClientLifecycleEmitter();
|
|
70
|
+
}
|
|
71
|
+
handleConnect(clientId, transportType) {
|
|
72
|
+
// ✅ Type-safe connection event
|
|
73
|
+
this.emit('client.connected', {
|
|
74
|
+
'client.id': clientId,
|
|
75
|
+
'transport.type': transportType,
|
|
76
|
+
'connection.time': Date.now(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
handleAuthentication(clientId, userId, authMethod) {
|
|
80
|
+
// ✅ Type-safe authentication event
|
|
81
|
+
this.emit('client.authenticated', {
|
|
82
|
+
'client.id': clientId,
|
|
83
|
+
'user.id': userId,
|
|
84
|
+
'auth.method': authMethod, // Optional field
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
handleDisconnect(clientId, reason, duration) {
|
|
88
|
+
// ✅ Type-safe disconnection event
|
|
89
|
+
this.emit('client.disconnected', {
|
|
90
|
+
'client.id': clientId,
|
|
91
|
+
'disconnect.reason': reason, // Optional field
|
|
92
|
+
'session.duration': duration, // Optional field
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.ClientLifecycleWithTelemetry = ClientLifecycleWithTelemetry;
|
|
97
|
+
/**
|
|
98
|
+
* Example with multiple event type handlers
|
|
99
|
+
*/
|
|
100
|
+
class ControlTowerWithTelemetry {
|
|
101
|
+
constructor() {
|
|
102
|
+
this.messageEmit = createMessageHandlerEmitter();
|
|
103
|
+
this.clientEmit = createClientLifecycleEmitter();
|
|
104
|
+
// ❌ TypeScript error: can't use client events with message emitter
|
|
105
|
+
// handleWrongEvent() {
|
|
106
|
+
// this.messageEmit('client.connected', { ... }); // Error!
|
|
107
|
+
// }
|
|
108
|
+
}
|
|
109
|
+
handleNewClient(clientId, transportType) {
|
|
110
|
+
// Each emitter is type-safe to its specific events
|
|
111
|
+
this.clientEmit('client.connected', {
|
|
112
|
+
'client.id': clientId,
|
|
113
|
+
'transport.type': transportType,
|
|
114
|
+
'connection.time': Date.now(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
handleMessage(clientId, messageType) {
|
|
118
|
+
this.messageEmit('message.received', {
|
|
119
|
+
'client.id': clientId,
|
|
120
|
+
'message.type': messageType,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.ControlTowerWithTelemetry = ControlTowerWithTelemetry;
|
package/package.json
CHANGED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { ITransportAdapter } from '../../abstractions/TransportAdapter.js';
|
|
2
|
-
import type { IAuthAdapter } from '../../abstractions/AuthAdapter.js';
|
|
3
|
-
import type { ConnectionState, ConnectionOptions, Message, MessageHandler, ErrorHandler, CloseHandler } from '../../types/index.js';
|
|
4
|
-
import type { Server as HttpServer } from 'http';
|
|
5
|
-
import type { Server as HttpsServer } from 'https';
|
|
6
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
|
-
interface ClientConnection {
|
|
8
|
-
id: string;
|
|
9
|
-
ws: WebSocket;
|
|
10
|
-
userId?: string;
|
|
11
|
-
authenticated: boolean;
|
|
12
|
-
metadata?: Record<string, unknown>;
|
|
13
|
-
authTimeout?: NodeJS.Timeout;
|
|
14
|
-
connectedAt: number;
|
|
15
|
-
}
|
|
16
|
-
export interface WebSocketTransportConfig {
|
|
17
|
-
authTimeout?: number;
|
|
18
|
-
closeOnAuthFailure?: boolean;
|
|
19
|
-
requireAuth?: boolean;
|
|
20
|
-
}
|
|
21
|
-
export declare class WebSocketTransportAdapter implements ITransportAdapter {
|
|
22
|
-
private state;
|
|
23
|
-
private messageHandlers;
|
|
24
|
-
private errorHandlers;
|
|
25
|
-
private closeHandlers;
|
|
26
|
-
private wss?;
|
|
27
|
-
private serverUrl?;
|
|
28
|
-
private attachedServer?;
|
|
29
|
-
private attachedWss?;
|
|
30
|
-
private webSocketPath?;
|
|
31
|
-
private clients;
|
|
32
|
-
private mode;
|
|
33
|
-
private authAdapter?;
|
|
34
|
-
private config;
|
|
35
|
-
constructor(config?: WebSocketTransportConfig);
|
|
36
|
-
setAuthAdapter(auth: IAuthAdapter): void;
|
|
37
|
-
connect(url: string, _options?: ConnectionOptions): Promise<void>;
|
|
38
|
-
attach(server: HttpServer | HttpsServer, path?: string): Promise<void>;
|
|
39
|
-
attachToWebSocketServer(wss: WebSocketServer): Promise<void>;
|
|
40
|
-
private handleConnection;
|
|
41
|
-
private handleClientMessage;
|
|
42
|
-
private handleAuthMessage;
|
|
43
|
-
private extractBearerToken;
|
|
44
|
-
private sendToClient;
|
|
45
|
-
disconnect(): Promise<void>;
|
|
46
|
-
send(message: Message): Promise<void>;
|
|
47
|
-
onMessage(handler: MessageHandler): void;
|
|
48
|
-
onError(handler: ErrorHandler): void;
|
|
49
|
-
onClose(handler: CloseHandler): void;
|
|
50
|
-
getState(): ConnectionState;
|
|
51
|
-
isConnected(): boolean;
|
|
52
|
-
getConnectedClients(): ClientConnection[];
|
|
53
|
-
getAuthenticatedClients(): ClientConnection[];
|
|
54
|
-
getClientCount(): number;
|
|
55
|
-
getMode(): 'standalone' | 'integration';
|
|
56
|
-
isAuthRequired(): boolean;
|
|
57
|
-
private generateId;
|
|
58
|
-
}
|
|
59
|
-
export {};
|
|
60
|
-
//# sourceMappingURL=WebSocketTransportAdapter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WebSocketTransportAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/websocket/WebSocketTransportAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,YAAY,EACZ,YAAY,EAGb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,MAAM,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,IAAI,WAAW,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEhD,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,yBAA0B,YAAW,iBAAiB;IACjE,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,aAAa,CAAgC;IAGrD,OAAO,CAAC,GAAG,CAAC,CAAkB;IAC9B,OAAO,CAAC,SAAS,CAAC,CAAS;IAG3B,OAAO,CAAC,cAAc,CAAC,CAA2B;IAClD,OAAO,CAAC,WAAW,CAAC,CAAkB;IACtC,OAAO,CAAC,aAAa,CAAC,CAAS;IAG/B,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,IAAI,CAA8C;IAG1D,OAAO,CAAC,WAAW,CAAC,CAAe;IACnC,OAAO,CAAC,MAAM,CAA2B;gBAE7B,MAAM,CAAC,EAAE,wBAAwB;IAU7C,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAclC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCjE,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,WAAW,EAAE,IAAI,GAAE,MAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAiC7E,uBAAuB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;YA2BpD,gBAAgB;YA6EhB,mBAAmB;YAuCnB,iBAAiB;IA4E/B,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,YAAY;IAMd,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiC3B,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B3C,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAIxC,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAIpC,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAIpC,QAAQ,IAAI,eAAe;IAI3B,WAAW,IAAI,OAAO;IAKtB,mBAAmB,IAAI,gBAAgB,EAAE;IAIzC,uBAAuB,IAAI,gBAAgB,EAAE;IAI7C,cAAc,IAAI,MAAM;IAIxB,OAAO,IAAI,YAAY,GAAG,aAAa;IAIvC,cAAc,IAAI,OAAO;IAIzB,OAAO,CAAC,UAAU;CAGnB"}
|
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WebSocketTransportAdapter = void 0;
|
|
4
|
-
const ws_1 = require("ws");
|
|
5
|
-
class WebSocketTransportAdapter {
|
|
6
|
-
constructor(config) {
|
|
7
|
-
this.state = 'disconnected';
|
|
8
|
-
this.messageHandlers = new Set();
|
|
9
|
-
this.errorHandlers = new Set();
|
|
10
|
-
this.closeHandlers = new Set();
|
|
11
|
-
// Client management
|
|
12
|
-
this.clients = new Map();
|
|
13
|
-
this.mode = 'standalone';
|
|
14
|
-
this.config = {
|
|
15
|
-
authTimeout: config?.authTimeout ?? 5000,
|
|
16
|
-
closeOnAuthFailure: config?.closeOnAuthFailure ?? false,
|
|
17
|
-
requireAuth: config?.requireAuth ?? false,
|
|
18
|
-
...config
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
// Set the auth adapter
|
|
22
|
-
setAuthAdapter(auth) {
|
|
23
|
-
this.authAdapter = auth;
|
|
24
|
-
// Use auth adapter's preference only if requireAuth is not already true
|
|
25
|
-
// This allows explicit requireAuth: true in config to take precedence
|
|
26
|
-
if (auth.isAuthRequired && !this.config.requireAuth) {
|
|
27
|
-
this.config.requireAuth = auth.isAuthRequired();
|
|
28
|
-
}
|
|
29
|
-
// For authTimeout, use adapter's value if not explicitly set (not matching default)
|
|
30
|
-
if (auth.getAuthTimeout && this.config.authTimeout === 5000) {
|
|
31
|
-
this.config.authTimeout = auth.getAuthTimeout();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
// Standalone mode implementation
|
|
35
|
-
async connect(url, _options) {
|
|
36
|
-
if (this.state === 'connected' || this.state === 'connecting') {
|
|
37
|
-
throw new Error('Already connected or connecting');
|
|
38
|
-
}
|
|
39
|
-
this.state = 'connecting';
|
|
40
|
-
this.serverUrl = url;
|
|
41
|
-
this.mode = 'standalone';
|
|
42
|
-
try {
|
|
43
|
-
// Extract port from URL
|
|
44
|
-
const urlObj = new URL(url);
|
|
45
|
-
const port = parseInt(urlObj.port) || (urlObj.protocol === 'wss:' ? 443 : 80);
|
|
46
|
-
// Create WebSocket server
|
|
47
|
-
this.wss = new ws_1.WebSocketServer({ port });
|
|
48
|
-
this.wss.on('connection', (ws, req) => {
|
|
49
|
-
this.handleConnection(ws, req);
|
|
50
|
-
});
|
|
51
|
-
this.wss.on('error', (error) => {
|
|
52
|
-
this.errorHandlers.forEach(handler => handler(error));
|
|
53
|
-
});
|
|
54
|
-
this.wss.on('close', () => {
|
|
55
|
-
this.state = 'disconnected';
|
|
56
|
-
this.closeHandlers.forEach(handler => handler(1000, 'Server closed'));
|
|
57
|
-
});
|
|
58
|
-
this.state = 'connected';
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
this.state = 'disconnected';
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// Integration mode implementation
|
|
66
|
-
async attach(server, path = '/ws') {
|
|
67
|
-
if (this.state === 'connected' || this.state === 'connecting') {
|
|
68
|
-
throw new Error('Already connected or connecting');
|
|
69
|
-
}
|
|
70
|
-
this.state = 'connecting';
|
|
71
|
-
this.attachedServer = server;
|
|
72
|
-
this.webSocketPath = path;
|
|
73
|
-
this.mode = 'integration';
|
|
74
|
-
try {
|
|
75
|
-
// Create WebSocket server attached to the existing HTTP server
|
|
76
|
-
this.wss = new ws_1.WebSocketServer({
|
|
77
|
-
server,
|
|
78
|
-
path,
|
|
79
|
-
perMessageDeflate: false
|
|
80
|
-
});
|
|
81
|
-
this.wss.on('connection', (ws, req) => {
|
|
82
|
-
this.handleConnection(ws, req);
|
|
83
|
-
});
|
|
84
|
-
this.wss.on('error', (error) => {
|
|
85
|
-
this.errorHandlers.forEach(handler => handler(error));
|
|
86
|
-
});
|
|
87
|
-
this.state = 'connected';
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
this.state = 'disconnected';
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
async attachToWebSocketServer(wss) {
|
|
95
|
-
if (this.state === 'connected' || this.state === 'connecting') {
|
|
96
|
-
throw new Error('Already connected or connecting');
|
|
97
|
-
}
|
|
98
|
-
this.state = 'connecting';
|
|
99
|
-
this.attachedWss = wss;
|
|
100
|
-
this.mode = 'integration';
|
|
101
|
-
try {
|
|
102
|
-
this.wss = wss;
|
|
103
|
-
this.wss.on('connection', (ws, req) => {
|
|
104
|
-
this.handleConnection(ws, req);
|
|
105
|
-
});
|
|
106
|
-
this.wss.on('error', (error) => {
|
|
107
|
-
this.errorHandlers.forEach(handler => handler(error));
|
|
108
|
-
});
|
|
109
|
-
this.state = 'connected';
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
this.state = 'disconnected';
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
async handleConnection(ws, req) {
|
|
117
|
-
const clientId = this.generateId();
|
|
118
|
-
// Step 1: Try header authentication if auth adapter is available
|
|
119
|
-
let authenticated = false;
|
|
120
|
-
let authResult = null;
|
|
121
|
-
if (this.authAdapter && req.headers.authorization) {
|
|
122
|
-
const token = this.extractBearerToken(req.headers.authorization);
|
|
123
|
-
if (token) {
|
|
124
|
-
try {
|
|
125
|
-
authResult = await this.authAdapter.authenticate({
|
|
126
|
-
type: 'bearer',
|
|
127
|
-
token: token
|
|
128
|
-
});
|
|
129
|
-
authenticated = authResult.success;
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// Header auth failed but don't reject connection
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Step 2: Create client with auth state
|
|
137
|
-
const client = {
|
|
138
|
-
id: clientId,
|
|
139
|
-
ws,
|
|
140
|
-
authenticated,
|
|
141
|
-
userId: authResult?.userId || (authResult?.user?.userId),
|
|
142
|
-
metadata: authResult?.metadata || authResult?.user?.metadata,
|
|
143
|
-
connectedAt: Date.now()
|
|
144
|
-
};
|
|
145
|
-
this.clients.set(clientId, client);
|
|
146
|
-
// Step 3: Set up message handling
|
|
147
|
-
ws.on('message', (data) => {
|
|
148
|
-
this.handleClientMessage(clientId, data);
|
|
149
|
-
});
|
|
150
|
-
ws.on('error', (error) => {
|
|
151
|
-
this.errorHandlers.forEach(handler => handler(error));
|
|
152
|
-
});
|
|
153
|
-
ws.on('close', (_code, _reason) => {
|
|
154
|
-
// Clear auth timeout if exists
|
|
155
|
-
if (client.authTimeout) {
|
|
156
|
-
clearTimeout(client.authTimeout);
|
|
157
|
-
}
|
|
158
|
-
this.clients.delete(clientId);
|
|
159
|
-
});
|
|
160
|
-
// Step 4: Set auth timeout if not authenticated and auth is required
|
|
161
|
-
if (!authenticated && this.config.requireAuth && this.authAdapter) {
|
|
162
|
-
client.authTimeout = setTimeout(() => {
|
|
163
|
-
if (!client.authenticated) {
|
|
164
|
-
ws.close(1008, 'Authentication timeout');
|
|
165
|
-
this.clients.delete(clientId);
|
|
166
|
-
}
|
|
167
|
-
}, this.config.authTimeout);
|
|
168
|
-
}
|
|
169
|
-
// Step 5: Notify about connection (with auth status)
|
|
170
|
-
const connectionMessage = {
|
|
171
|
-
id: this.generateId(),
|
|
172
|
-
type: 'connection',
|
|
173
|
-
payload: {
|
|
174
|
-
clientId,
|
|
175
|
-
authenticated,
|
|
176
|
-
userId: client.userId,
|
|
177
|
-
metadata: client.metadata
|
|
178
|
-
},
|
|
179
|
-
timestamp: Date.now()
|
|
180
|
-
};
|
|
181
|
-
this.messageHandlers.forEach(handler => handler(connectionMessage));
|
|
182
|
-
}
|
|
183
|
-
async handleClientMessage(clientId, data) {
|
|
184
|
-
const client = this.clients.get(clientId);
|
|
185
|
-
if (!client)
|
|
186
|
-
return;
|
|
187
|
-
try {
|
|
188
|
-
const message = JSON.parse(data.toString());
|
|
189
|
-
// Special handling for authentication messages
|
|
190
|
-
if (message.type === 'authenticate' && !client.authenticated && this.authAdapter) {
|
|
191
|
-
await this.handleAuthMessage(client, message);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
// Reject messages from unauthenticated clients if auth is required
|
|
195
|
-
if (this.config.requireAuth && !client.authenticated) {
|
|
196
|
-
this.sendToClient(client, {
|
|
197
|
-
id: this.generateId(),
|
|
198
|
-
type: 'error',
|
|
199
|
-
payload: { error: 'Authentication required' },
|
|
200
|
-
timestamp: Date.now()
|
|
201
|
-
});
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
// Add clientId to message payload for routing
|
|
205
|
-
const enrichedMessage = {
|
|
206
|
-
...message,
|
|
207
|
-
payload: {
|
|
208
|
-
...message.payload,
|
|
209
|
-
clientId
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
this.messageHandlers.forEach(handler => handler(enrichedMessage));
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
this.errorHandlers.forEach(handler => handler(error));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
async handleAuthMessage(client, message) {
|
|
219
|
-
if (!this.authAdapter) {
|
|
220
|
-
this.sendToClient(client, {
|
|
221
|
-
id: this.generateId(),
|
|
222
|
-
type: 'auth_error',
|
|
223
|
-
payload: { error: 'No auth adapter configured' },
|
|
224
|
-
timestamp: Date.now()
|
|
225
|
-
});
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
const credentials = message.payload;
|
|
230
|
-
const result = await this.authAdapter.authenticate(credentials);
|
|
231
|
-
if (result.success) {
|
|
232
|
-
// Clear auth timeout
|
|
233
|
-
if (client.authTimeout) {
|
|
234
|
-
clearTimeout(client.authTimeout);
|
|
235
|
-
client.authTimeout = undefined;
|
|
236
|
-
}
|
|
237
|
-
// Update client state
|
|
238
|
-
client.authenticated = true;
|
|
239
|
-
client.userId = result.userId || result.user?.userId;
|
|
240
|
-
client.metadata = result.metadata || result.user?.metadata;
|
|
241
|
-
// Send success response
|
|
242
|
-
this.sendToClient(client, {
|
|
243
|
-
id: this.generateId(),
|
|
244
|
-
type: 'auth_success',
|
|
245
|
-
payload: {
|
|
246
|
-
userId: client.userId,
|
|
247
|
-
metadata: client.metadata
|
|
248
|
-
},
|
|
249
|
-
timestamp: Date.now()
|
|
250
|
-
});
|
|
251
|
-
// Notify handlers about authentication
|
|
252
|
-
const authMessage = {
|
|
253
|
-
id: this.generateId(),
|
|
254
|
-
type: 'client_authenticated',
|
|
255
|
-
payload: {
|
|
256
|
-
clientId: client.id,
|
|
257
|
-
userId: client.userId,
|
|
258
|
-
metadata: client.metadata
|
|
259
|
-
},
|
|
260
|
-
timestamp: Date.now()
|
|
261
|
-
};
|
|
262
|
-
this.messageHandlers.forEach(handler => handler(authMessage));
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
// Send error response
|
|
266
|
-
this.sendToClient(client, {
|
|
267
|
-
id: this.generateId(),
|
|
268
|
-
type: 'auth_error',
|
|
269
|
-
payload: { error: result.error || 'Authentication failed' },
|
|
270
|
-
timestamp: Date.now()
|
|
271
|
-
});
|
|
272
|
-
// Optionally close connection
|
|
273
|
-
if (this.config.closeOnAuthFailure) {
|
|
274
|
-
client.ws.close(1008, 'Authentication failed');
|
|
275
|
-
this.clients.delete(client.id);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
this.sendToClient(client, {
|
|
281
|
-
id: this.generateId(),
|
|
282
|
-
type: 'auth_error',
|
|
283
|
-
payload: { error: error.message },
|
|
284
|
-
timestamp: Date.now()
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
extractBearerToken(authHeader) {
|
|
289
|
-
if (authHeader.startsWith('Bearer ')) {
|
|
290
|
-
return authHeader.substring(7);
|
|
291
|
-
}
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
sendToClient(client, message) {
|
|
295
|
-
if (client.ws.readyState === ws_1.WebSocket.OPEN) {
|
|
296
|
-
client.ws.send(JSON.stringify(message));
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
async disconnect() {
|
|
300
|
-
if (this.state === 'disconnected') {
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
this.state = 'disconnecting';
|
|
304
|
-
// Close all client connections
|
|
305
|
-
for (const [_clientId, client] of this.clients) {
|
|
306
|
-
try {
|
|
307
|
-
if (client.authTimeout) {
|
|
308
|
-
clearTimeout(client.authTimeout);
|
|
309
|
-
}
|
|
310
|
-
client.ws.close(1000, 'Server shutting down');
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
// Ignore errors when closing individual connections
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
this.clients.clear();
|
|
317
|
-
// Close the WebSocket server (only if we created it)
|
|
318
|
-
if (this.wss && this.mode === 'standalone') {
|
|
319
|
-
await new Promise((resolve) => {
|
|
320
|
-
this.wss.close(() => resolve());
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
this.state = 'disconnected';
|
|
324
|
-
this.wss = undefined;
|
|
325
|
-
this.attachedServer = undefined;
|
|
326
|
-
this.attachedWss = undefined;
|
|
327
|
-
}
|
|
328
|
-
async send(message) {
|
|
329
|
-
if (this.state !== 'connected') {
|
|
330
|
-
throw new Error('Not connected');
|
|
331
|
-
}
|
|
332
|
-
// Extract clientId from message payload for routing
|
|
333
|
-
const payload = message.payload;
|
|
334
|
-
const { clientId, ...clientMessage } = payload;
|
|
335
|
-
if (!clientId) {
|
|
336
|
-
throw new Error('Message must contain clientId in payload for routing');
|
|
337
|
-
}
|
|
338
|
-
const client = this.clients.get(clientId);
|
|
339
|
-
if (!client) {
|
|
340
|
-
throw new Error(`Client ${clientId} not found`);
|
|
341
|
-
}
|
|
342
|
-
if (client.ws.readyState !== ws_1.WebSocket.OPEN) {
|
|
343
|
-
throw new Error(`Client ${clientId} connection is not open`);
|
|
344
|
-
}
|
|
345
|
-
const messageToSend = {
|
|
346
|
-
...message,
|
|
347
|
-
payload: clientMessage
|
|
348
|
-
};
|
|
349
|
-
client.ws.send(JSON.stringify(messageToSend));
|
|
350
|
-
}
|
|
351
|
-
onMessage(handler) {
|
|
352
|
-
this.messageHandlers.add(handler);
|
|
353
|
-
}
|
|
354
|
-
onError(handler) {
|
|
355
|
-
this.errorHandlers.add(handler);
|
|
356
|
-
}
|
|
357
|
-
onClose(handler) {
|
|
358
|
-
this.closeHandlers.add(handler);
|
|
359
|
-
}
|
|
360
|
-
getState() {
|
|
361
|
-
return this.state;
|
|
362
|
-
}
|
|
363
|
-
isConnected() {
|
|
364
|
-
return this.state === 'connected';
|
|
365
|
-
}
|
|
366
|
-
// Utility methods
|
|
367
|
-
getConnectedClients() {
|
|
368
|
-
return Array.from(this.clients.values());
|
|
369
|
-
}
|
|
370
|
-
getAuthenticatedClients() {
|
|
371
|
-
return Array.from(this.clients.values()).filter(c => c.authenticated);
|
|
372
|
-
}
|
|
373
|
-
getClientCount() {
|
|
374
|
-
return this.clients.size;
|
|
375
|
-
}
|
|
376
|
-
getMode() {
|
|
377
|
-
return this.mode;
|
|
378
|
-
}
|
|
379
|
-
isAuthRequired() {
|
|
380
|
-
return this.config.requireAuth ?? false;
|
|
381
|
-
}
|
|
382
|
-
generateId() {
|
|
383
|
-
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
exports.WebSocketTransportAdapter = WebSocketTransportAdapter;
|