@anycast/agent 0.1.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/README.md +143 -0
- package/dist/index.d.mts +135 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +303 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +294 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @anycast/agent
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [Anycast Agents](https://agents.anycast.com) P2P connectivity.
|
|
4
|
+
|
|
5
|
+
Connect agents to the Anycast network for peer-to-peer communication with relay fallback, powered by the global Anycast edge infrastructure.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @anycast/agent
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @anycast/agent
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { AnycastAgent } from '@anycast/agent';
|
|
19
|
+
|
|
20
|
+
const agent = new AnycastAgent({
|
|
21
|
+
rendezvousUrl: 'wss://agents.anycast.com/rendezvous',
|
|
22
|
+
token: 'agt_your_token_here',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
agent.on('connect', ({ agentId }) => {
|
|
26
|
+
console.log(`Connected as ${agentId}`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
agent.on('peer', (peerId, status) => {
|
|
30
|
+
console.log(`Peer ${peerId} is ${status}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
agent.on('error', (err) => {
|
|
34
|
+
console.error(`Error: ${err.message} (${err.code})`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await agent.connect();
|
|
38
|
+
|
|
39
|
+
// Request a connection to another agent
|
|
40
|
+
const sessionId = agent.requestConnection('target-agent-id');
|
|
41
|
+
|
|
42
|
+
// Handle incoming WebRTC signals
|
|
43
|
+
agent.on('signal', (sessionId, fromAgentId, signal) => {
|
|
44
|
+
// Forward to your WebRTC implementation
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Send WebRTC signals
|
|
48
|
+
agent.sendSignal(sessionId, 'target-agent-id', {
|
|
49
|
+
type: 'offer',
|
|
50
|
+
data: JSON.stringify(sdpOffer),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Disconnect when done
|
|
54
|
+
await agent.disconnect();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### `new AnycastAgent(options)`
|
|
60
|
+
|
|
61
|
+
Create a new agent instance.
|
|
62
|
+
|
|
63
|
+
| Option | Type | Default | Description |
|
|
64
|
+
|--------|------|---------|-------------|
|
|
65
|
+
| `rendezvousUrl` | `string` | required | WebSocket URL of the rendezvous server |
|
|
66
|
+
| `token` | `string` | required | Agent auth token from the Anycast portal |
|
|
67
|
+
| `machineId` | `string` | auto-generated | Unique machine identifier |
|
|
68
|
+
| `publicKey` | `string` | auto-generated | Public key for E2E encryption |
|
|
69
|
+
| `version` | `string` | — | Agent version string |
|
|
70
|
+
| `reconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
|
|
71
|
+
| `reconnectInterval` | `number` | `3000` | Base reconnect interval (ms) |
|
|
72
|
+
| `timeout` | `number` | `30000` | Connection timeout (ms) |
|
|
73
|
+
| `heartbeatInterval` | `number` | `25000` | Heartbeat interval (ms) |
|
|
74
|
+
|
|
75
|
+
### Properties
|
|
76
|
+
|
|
77
|
+
| Property | Type | Description |
|
|
78
|
+
|----------|------|-------------|
|
|
79
|
+
| `agentId` | `string \| null` | Assigned agent ID (set after connect) |
|
|
80
|
+
| `tenantId` | `string \| null` | Tenant ID (set after connect) |
|
|
81
|
+
| `connected` | `boolean` | Whether the agent is connected |
|
|
82
|
+
| `connectionState` | `ConnectionState` | Current connection state |
|
|
83
|
+
|
|
84
|
+
### Methods
|
|
85
|
+
|
|
86
|
+
#### `connect(): Promise<void>`
|
|
87
|
+
Connect to the rendezvous server. Resolves when registered.
|
|
88
|
+
|
|
89
|
+
#### `disconnect(): Promise<void>`
|
|
90
|
+
Disconnect from the server.
|
|
91
|
+
|
|
92
|
+
#### `requestConnection(targetAgentId: string): string`
|
|
93
|
+
Request a P2P connection to another agent. Returns a session ID.
|
|
94
|
+
|
|
95
|
+
#### `sendSignal(sessionId, targetAgentId, signal)`
|
|
96
|
+
Send a WebRTC signal (offer/answer/ICE candidate) via the rendezvous server.
|
|
97
|
+
|
|
98
|
+
#### `disconnectSession(sessionId: string)`
|
|
99
|
+
Close a specific session.
|
|
100
|
+
|
|
101
|
+
### Events
|
|
102
|
+
|
|
103
|
+
| Event | Payload | Description |
|
|
104
|
+
|-------|---------|-------------|
|
|
105
|
+
| `connect` | `{ agentId, tenantId, name }` | Connected and registered |
|
|
106
|
+
| `disconnect` | `reason: string` | Disconnected |
|
|
107
|
+
| `reconnecting` | `attempt: number` | Attempting reconnection |
|
|
108
|
+
| `peer` | `agentId, status` | Peer came online/offline |
|
|
109
|
+
| `connect_incoming` | `sessionId, fromAgentId, fromAgentName` | Incoming connection request |
|
|
110
|
+
| `connect_accepted` | `sessionId, agentId, publicKey` | Connection accepted by peer |
|
|
111
|
+
| `connect_rejected` | `sessionId, reason` | Connection rejected |
|
|
112
|
+
| `signal` | `sessionId, fromAgentId, signal` | WebRTC signal from peer |
|
|
113
|
+
| `relay_assigned` | `sessionId, host, port, token` | Relay server assigned |
|
|
114
|
+
| `error` | `AnycastError` | Error occurred |
|
|
115
|
+
| `stateChange` | `ConnectionState` | Connection state changed |
|
|
116
|
+
|
|
117
|
+
### ConnectionState
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
enum ConnectionState {
|
|
121
|
+
DISCONNECTED = 'DISCONNECTED',
|
|
122
|
+
CONNECTING = 'CONNECTING',
|
|
123
|
+
CONNECTED = 'CONNECTED',
|
|
124
|
+
RECONNECTING = 'RECONNECTING',
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### AnycastError
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
class AnycastError extends Error {
|
|
132
|
+
code: string; // Error code (e.g., 'AUTH_FAILED', 'TIMEOUT')
|
|
133
|
+
fatal: boolean; // If true, reconnection won't help
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Auto-Reconnection
|
|
138
|
+
|
|
139
|
+
By default, the agent automatically reconnects with exponential backoff (3s, 6s, 12s, ..., max 30s). Disable with `reconnect: false`.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import EventEmitter from 'eventemitter3';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent SDK types — mirrors the rendezvous server protocol.
|
|
5
|
+
*/
|
|
6
|
+
interface AnycastAgentOptions {
|
|
7
|
+
/** WebSocket URL of the rendezvous server (e.g., wss://agents.anycast.com/rendezvous) */
|
|
8
|
+
rendezvousUrl: string;
|
|
9
|
+
/** Agent auth token from the Anycast portal */
|
|
10
|
+
token: string;
|
|
11
|
+
/** Unique machine identifier (auto-generated if not provided) */
|
|
12
|
+
machineId?: string;
|
|
13
|
+
/** Public key for end-to-end encryption */
|
|
14
|
+
publicKey?: string;
|
|
15
|
+
/** Agent version string */
|
|
16
|
+
version?: string;
|
|
17
|
+
/** Auto-reconnect on disconnect (default: true) */
|
|
18
|
+
reconnect?: boolean;
|
|
19
|
+
/** Reconnect interval in ms (default: 3000) */
|
|
20
|
+
reconnectInterval?: number;
|
|
21
|
+
/** Connection timeout in ms (default: 30000) */
|
|
22
|
+
timeout?: number;
|
|
23
|
+
/** Heartbeat interval in ms (default: 25000) */
|
|
24
|
+
heartbeatInterval?: number;
|
|
25
|
+
}
|
|
26
|
+
declare enum ConnectionState {
|
|
27
|
+
DISCONNECTED = "DISCONNECTED",
|
|
28
|
+
CONNECTING = "CONNECTING",
|
|
29
|
+
CONNECTED = "CONNECTED",
|
|
30
|
+
RECONNECTING = "RECONNECTING"
|
|
31
|
+
}
|
|
32
|
+
interface AnycastAgentEvents {
|
|
33
|
+
connect: (info: {
|
|
34
|
+
agentId: string;
|
|
35
|
+
tenantId: string;
|
|
36
|
+
name: string;
|
|
37
|
+
}) => void;
|
|
38
|
+
disconnect: (reason: string) => void;
|
|
39
|
+
reconnecting: (attempt: number) => void;
|
|
40
|
+
message: (fromAgentId: string, data: string) => void;
|
|
41
|
+
peer: (agentId: string, status: 'online' | 'offline') => void;
|
|
42
|
+
'connect_incoming': (sessionId: string, fromAgentId: string, fromAgentName: string) => void;
|
|
43
|
+
'connect_accepted': (sessionId: string, agentId: string, publicKey: string) => void;
|
|
44
|
+
'connect_rejected': (sessionId: string, reason: string) => void;
|
|
45
|
+
signal: (sessionId: string, fromAgentId: string, signal: {
|
|
46
|
+
type: string;
|
|
47
|
+
data: string;
|
|
48
|
+
}) => void;
|
|
49
|
+
'relay_assigned': (sessionId: string, relayHost: string, relayPort: number, relayToken: string) => void;
|
|
50
|
+
error: (error: AnycastError) => void;
|
|
51
|
+
stateChange: (state: ConnectionState) => void;
|
|
52
|
+
}
|
|
53
|
+
declare class AnycastError extends Error {
|
|
54
|
+
code: string;
|
|
55
|
+
fatal: boolean;
|
|
56
|
+
constructor(message: string, code: string, fatal?: boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* AnycastAgent — connects to the Anycast Agents rendezvous server
|
|
61
|
+
* for P2P connectivity, signaling, and relay fallback.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const agent = new AnycastAgent({
|
|
66
|
+
* rendezvousUrl: 'wss://agents.anycast.com/rendezvous',
|
|
67
|
+
* token: 'agt_...',
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* agent.on('connect', ({ agentId }) => {
|
|
71
|
+
* console.log(`Connected as ${agentId}`);
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* agent.on('peer', (peerId, status) => {
|
|
75
|
+
* console.log(`Peer ${peerId} is ${status}`);
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* await agent.connect();
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
declare class AnycastAgent extends EventEmitter<AnycastAgentEvents> {
|
|
82
|
+
private ws;
|
|
83
|
+
private state;
|
|
84
|
+
private heartbeatTimer;
|
|
85
|
+
private reconnectTimer;
|
|
86
|
+
private reconnectAttempt;
|
|
87
|
+
private intentionalClose;
|
|
88
|
+
/** Agent ID assigned by the server after registration */
|
|
89
|
+
agentId: string | null;
|
|
90
|
+
/** Tenant ID from the server */
|
|
91
|
+
tenantId: string | null;
|
|
92
|
+
/** Agent name from the server */
|
|
93
|
+
agentName: string | null;
|
|
94
|
+
private readonly opts;
|
|
95
|
+
constructor(options: AnycastAgentOptions);
|
|
96
|
+
/** Current connection state */
|
|
97
|
+
get connectionState(): ConnectionState;
|
|
98
|
+
/** Whether the agent is connected */
|
|
99
|
+
get connected(): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Connect to the rendezvous server.
|
|
102
|
+
* Resolves when registered, rejects on timeout or fatal error.
|
|
103
|
+
*/
|
|
104
|
+
connect(): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Disconnect from the rendezvous server.
|
|
107
|
+
*/
|
|
108
|
+
disconnect(): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Request a connection to a peer agent.
|
|
111
|
+
* @returns The session ID for this connection
|
|
112
|
+
*/
|
|
113
|
+
requestConnection(targetAgentId: string): string;
|
|
114
|
+
/**
|
|
115
|
+
* Send a WebRTC signal to a peer via the rendezvous server.
|
|
116
|
+
*/
|
|
117
|
+
sendSignal(sessionId: string, targetAgentId: string, signal: {
|
|
118
|
+
type: 'offer' | 'answer' | 'ice';
|
|
119
|
+
data: string;
|
|
120
|
+
}): void;
|
|
121
|
+
/**
|
|
122
|
+
* Disconnect a session.
|
|
123
|
+
*/
|
|
124
|
+
disconnectSession(sessionId: string): void;
|
|
125
|
+
private handleMessage;
|
|
126
|
+
private send;
|
|
127
|
+
private startHeartbeat;
|
|
128
|
+
private stopHeartbeat;
|
|
129
|
+
private scheduleReconnect;
|
|
130
|
+
private cleanup;
|
|
131
|
+
private setState;
|
|
132
|
+
private assertConnected;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { AnycastAgent, type AnycastAgentEvents, type AnycastAgentOptions, AnycastError, ConnectionState };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import EventEmitter from 'eventemitter3';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent SDK types — mirrors the rendezvous server protocol.
|
|
5
|
+
*/
|
|
6
|
+
interface AnycastAgentOptions {
|
|
7
|
+
/** WebSocket URL of the rendezvous server (e.g., wss://agents.anycast.com/rendezvous) */
|
|
8
|
+
rendezvousUrl: string;
|
|
9
|
+
/** Agent auth token from the Anycast portal */
|
|
10
|
+
token: string;
|
|
11
|
+
/** Unique machine identifier (auto-generated if not provided) */
|
|
12
|
+
machineId?: string;
|
|
13
|
+
/** Public key for end-to-end encryption */
|
|
14
|
+
publicKey?: string;
|
|
15
|
+
/** Agent version string */
|
|
16
|
+
version?: string;
|
|
17
|
+
/** Auto-reconnect on disconnect (default: true) */
|
|
18
|
+
reconnect?: boolean;
|
|
19
|
+
/** Reconnect interval in ms (default: 3000) */
|
|
20
|
+
reconnectInterval?: number;
|
|
21
|
+
/** Connection timeout in ms (default: 30000) */
|
|
22
|
+
timeout?: number;
|
|
23
|
+
/** Heartbeat interval in ms (default: 25000) */
|
|
24
|
+
heartbeatInterval?: number;
|
|
25
|
+
}
|
|
26
|
+
declare enum ConnectionState {
|
|
27
|
+
DISCONNECTED = "DISCONNECTED",
|
|
28
|
+
CONNECTING = "CONNECTING",
|
|
29
|
+
CONNECTED = "CONNECTED",
|
|
30
|
+
RECONNECTING = "RECONNECTING"
|
|
31
|
+
}
|
|
32
|
+
interface AnycastAgentEvents {
|
|
33
|
+
connect: (info: {
|
|
34
|
+
agentId: string;
|
|
35
|
+
tenantId: string;
|
|
36
|
+
name: string;
|
|
37
|
+
}) => void;
|
|
38
|
+
disconnect: (reason: string) => void;
|
|
39
|
+
reconnecting: (attempt: number) => void;
|
|
40
|
+
message: (fromAgentId: string, data: string) => void;
|
|
41
|
+
peer: (agentId: string, status: 'online' | 'offline') => void;
|
|
42
|
+
'connect_incoming': (sessionId: string, fromAgentId: string, fromAgentName: string) => void;
|
|
43
|
+
'connect_accepted': (sessionId: string, agentId: string, publicKey: string) => void;
|
|
44
|
+
'connect_rejected': (sessionId: string, reason: string) => void;
|
|
45
|
+
signal: (sessionId: string, fromAgentId: string, signal: {
|
|
46
|
+
type: string;
|
|
47
|
+
data: string;
|
|
48
|
+
}) => void;
|
|
49
|
+
'relay_assigned': (sessionId: string, relayHost: string, relayPort: number, relayToken: string) => void;
|
|
50
|
+
error: (error: AnycastError) => void;
|
|
51
|
+
stateChange: (state: ConnectionState) => void;
|
|
52
|
+
}
|
|
53
|
+
declare class AnycastError extends Error {
|
|
54
|
+
code: string;
|
|
55
|
+
fatal: boolean;
|
|
56
|
+
constructor(message: string, code: string, fatal?: boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* AnycastAgent — connects to the Anycast Agents rendezvous server
|
|
61
|
+
* for P2P connectivity, signaling, and relay fallback.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const agent = new AnycastAgent({
|
|
66
|
+
* rendezvousUrl: 'wss://agents.anycast.com/rendezvous',
|
|
67
|
+
* token: 'agt_...',
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* agent.on('connect', ({ agentId }) => {
|
|
71
|
+
* console.log(`Connected as ${agentId}`);
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* agent.on('peer', (peerId, status) => {
|
|
75
|
+
* console.log(`Peer ${peerId} is ${status}`);
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* await agent.connect();
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
declare class AnycastAgent extends EventEmitter<AnycastAgentEvents> {
|
|
82
|
+
private ws;
|
|
83
|
+
private state;
|
|
84
|
+
private heartbeatTimer;
|
|
85
|
+
private reconnectTimer;
|
|
86
|
+
private reconnectAttempt;
|
|
87
|
+
private intentionalClose;
|
|
88
|
+
/** Agent ID assigned by the server after registration */
|
|
89
|
+
agentId: string | null;
|
|
90
|
+
/** Tenant ID from the server */
|
|
91
|
+
tenantId: string | null;
|
|
92
|
+
/** Agent name from the server */
|
|
93
|
+
agentName: string | null;
|
|
94
|
+
private readonly opts;
|
|
95
|
+
constructor(options: AnycastAgentOptions);
|
|
96
|
+
/** Current connection state */
|
|
97
|
+
get connectionState(): ConnectionState;
|
|
98
|
+
/** Whether the agent is connected */
|
|
99
|
+
get connected(): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Connect to the rendezvous server.
|
|
102
|
+
* Resolves when registered, rejects on timeout or fatal error.
|
|
103
|
+
*/
|
|
104
|
+
connect(): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Disconnect from the rendezvous server.
|
|
107
|
+
*/
|
|
108
|
+
disconnect(): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Request a connection to a peer agent.
|
|
111
|
+
* @returns The session ID for this connection
|
|
112
|
+
*/
|
|
113
|
+
requestConnection(targetAgentId: string): string;
|
|
114
|
+
/**
|
|
115
|
+
* Send a WebRTC signal to a peer via the rendezvous server.
|
|
116
|
+
*/
|
|
117
|
+
sendSignal(sessionId: string, targetAgentId: string, signal: {
|
|
118
|
+
type: 'offer' | 'answer' | 'ice';
|
|
119
|
+
data: string;
|
|
120
|
+
}): void;
|
|
121
|
+
/**
|
|
122
|
+
* Disconnect a session.
|
|
123
|
+
*/
|
|
124
|
+
disconnectSession(sessionId: string): void;
|
|
125
|
+
private handleMessage;
|
|
126
|
+
private send;
|
|
127
|
+
private startHeartbeat;
|
|
128
|
+
private stopHeartbeat;
|
|
129
|
+
private scheduleReconnect;
|
|
130
|
+
private cleanup;
|
|
131
|
+
private setState;
|
|
132
|
+
private assertConnected;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { AnycastAgent, type AnycastAgentEvents, type AnycastAgentOptions, AnycastError, ConnectionState };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var WebSocket = require('ws');
|
|
4
|
+
var EventEmitter = require('eventemitter3');
|
|
5
|
+
var crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
|
|
10
|
+
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
11
|
+
|
|
12
|
+
// src/agent.ts
|
|
13
|
+
|
|
14
|
+
// src/types.ts
|
|
15
|
+
var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
|
|
16
|
+
ConnectionState2["DISCONNECTED"] = "DISCONNECTED";
|
|
17
|
+
ConnectionState2["CONNECTING"] = "CONNECTING";
|
|
18
|
+
ConnectionState2["CONNECTED"] = "CONNECTED";
|
|
19
|
+
ConnectionState2["RECONNECTING"] = "RECONNECTING";
|
|
20
|
+
return ConnectionState2;
|
|
21
|
+
})(ConnectionState || {});
|
|
22
|
+
var AnycastError = class extends Error {
|
|
23
|
+
constructor(message, code, fatal = false) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.fatal = fatal;
|
|
27
|
+
this.name = "AnycastError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/agent.ts
|
|
32
|
+
var DEFAULT_OPTIONS = {
|
|
33
|
+
reconnect: true,
|
|
34
|
+
reconnectInterval: 3e3,
|
|
35
|
+
timeout: 3e4,
|
|
36
|
+
heartbeatInterval: 25e3
|
|
37
|
+
};
|
|
38
|
+
var AnycastAgent = class extends EventEmitter__default.default {
|
|
39
|
+
constructor(options) {
|
|
40
|
+
super();
|
|
41
|
+
this.ws = null;
|
|
42
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
43
|
+
this.heartbeatTimer = null;
|
|
44
|
+
this.reconnectTimer = null;
|
|
45
|
+
this.reconnectAttempt = 0;
|
|
46
|
+
this.intentionalClose = false;
|
|
47
|
+
/** Agent ID assigned by the server after registration */
|
|
48
|
+
this.agentId = null;
|
|
49
|
+
/** Tenant ID from the server */
|
|
50
|
+
this.tenantId = null;
|
|
51
|
+
/** Agent name from the server */
|
|
52
|
+
this.agentName = null;
|
|
53
|
+
this.opts = {
|
|
54
|
+
rendezvousUrl: options.rendezvousUrl,
|
|
55
|
+
token: options.token,
|
|
56
|
+
machineId: options.machineId || crypto.randomBytes(16).toString("hex"),
|
|
57
|
+
publicKey: options.publicKey || crypto.randomBytes(32).toString("base64"),
|
|
58
|
+
version: options.version,
|
|
59
|
+
reconnect: options.reconnect ?? DEFAULT_OPTIONS.reconnect,
|
|
60
|
+
reconnectInterval: options.reconnectInterval ?? DEFAULT_OPTIONS.reconnectInterval,
|
|
61
|
+
timeout: options.timeout ?? DEFAULT_OPTIONS.timeout,
|
|
62
|
+
heartbeatInterval: options.heartbeatInterval ?? DEFAULT_OPTIONS.heartbeatInterval
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Current connection state */
|
|
66
|
+
get connectionState() {
|
|
67
|
+
return this.state;
|
|
68
|
+
}
|
|
69
|
+
/** Whether the agent is connected */
|
|
70
|
+
get connected() {
|
|
71
|
+
return this.state === "CONNECTED" /* CONNECTED */;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Connect to the rendezvous server.
|
|
75
|
+
* Resolves when registered, rejects on timeout or fatal error.
|
|
76
|
+
*/
|
|
77
|
+
async connect() {
|
|
78
|
+
if (this.state === "CONNECTED" /* CONNECTED */ || this.state === "CONNECTING" /* CONNECTING */) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.intentionalClose = false;
|
|
82
|
+
this.setState("CONNECTING" /* CONNECTING */);
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const timeout = setTimeout(() => {
|
|
85
|
+
this.cleanup();
|
|
86
|
+
const err = new AnycastError("Connection timeout", "TIMEOUT", false);
|
|
87
|
+
this.emit("error", err);
|
|
88
|
+
reject(err);
|
|
89
|
+
}, this.opts.timeout);
|
|
90
|
+
try {
|
|
91
|
+
this.ws = new WebSocket__default.default(this.opts.rendezvousUrl);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
const error = new AnycastError(
|
|
95
|
+
`Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`,
|
|
96
|
+
"CONNECTION_FAILED",
|
|
97
|
+
false
|
|
98
|
+
);
|
|
99
|
+
this.setState("DISCONNECTED" /* DISCONNECTED */);
|
|
100
|
+
reject(error);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.ws.on("open", () => {
|
|
104
|
+
this.send({
|
|
105
|
+
type: "register",
|
|
106
|
+
token: this.opts.token,
|
|
107
|
+
machineId: this.opts.machineId,
|
|
108
|
+
publicKey: this.opts.publicKey,
|
|
109
|
+
version: this.opts.version
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
this.ws.on("message", (data) => {
|
|
113
|
+
let msg;
|
|
114
|
+
try {
|
|
115
|
+
msg = JSON.parse(data.toString());
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (msg.type === "registered") {
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
this.agentId = msg.agentId;
|
|
122
|
+
this.tenantId = msg.tenantId;
|
|
123
|
+
this.agentName = msg.name;
|
|
124
|
+
this.reconnectAttempt = 0;
|
|
125
|
+
this.setState("CONNECTED" /* CONNECTED */);
|
|
126
|
+
this.startHeartbeat();
|
|
127
|
+
this.emit("connect", { agentId: msg.agentId, tenantId: msg.tenantId, name: msg.name });
|
|
128
|
+
resolve();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (msg.type === "error" && msg.fatal) {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
const err = new AnycastError(msg.message, msg.code, true);
|
|
134
|
+
this.emit("error", err);
|
|
135
|
+
this.cleanup();
|
|
136
|
+
reject(err);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.handleMessage(msg);
|
|
140
|
+
});
|
|
141
|
+
this.ws.on("close", (code, reason) => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
this.stopHeartbeat();
|
|
144
|
+
const wasConnected = this.state === "CONNECTED" /* CONNECTED */;
|
|
145
|
+
this.setState("DISCONNECTED" /* DISCONNECTED */);
|
|
146
|
+
if (wasConnected) {
|
|
147
|
+
this.emit("disconnect", reason?.toString() || `Code ${code}`);
|
|
148
|
+
}
|
|
149
|
+
if (!this.intentionalClose && this.opts.reconnect) {
|
|
150
|
+
this.scheduleReconnect();
|
|
151
|
+
} else if (this.state !== "CONNECTED" /* CONNECTED */) {
|
|
152
|
+
reject(new AnycastError("Connection closed", "CLOSED", false));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
this.ws.on("error", (err) => {
|
|
156
|
+
const error = new AnycastError(err.message, "WS_ERROR", false);
|
|
157
|
+
this.emit("error", error);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Disconnect from the rendezvous server.
|
|
163
|
+
*/
|
|
164
|
+
async disconnect() {
|
|
165
|
+
this.intentionalClose = true;
|
|
166
|
+
this.cleanup();
|
|
167
|
+
this.agentId = null;
|
|
168
|
+
this.tenantId = null;
|
|
169
|
+
this.agentName = null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Request a connection to a peer agent.
|
|
173
|
+
* @returns The session ID for this connection
|
|
174
|
+
*/
|
|
175
|
+
requestConnection(targetAgentId) {
|
|
176
|
+
this.assertConnected();
|
|
177
|
+
const sessionId = crypto.randomBytes(16).toString("hex");
|
|
178
|
+
this.send({
|
|
179
|
+
type: "connect_request",
|
|
180
|
+
targetAgentId,
|
|
181
|
+
sessionId
|
|
182
|
+
});
|
|
183
|
+
return sessionId;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Send a WebRTC signal to a peer via the rendezvous server.
|
|
187
|
+
*/
|
|
188
|
+
sendSignal(sessionId, targetAgentId, signal) {
|
|
189
|
+
this.assertConnected();
|
|
190
|
+
this.send({
|
|
191
|
+
type: "signal",
|
|
192
|
+
sessionId,
|
|
193
|
+
targetAgentId,
|
|
194
|
+
signal
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Disconnect a session.
|
|
199
|
+
*/
|
|
200
|
+
disconnectSession(sessionId) {
|
|
201
|
+
this.assertConnected();
|
|
202
|
+
this.send({ type: "disconnect", sessionId });
|
|
203
|
+
}
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Private
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
handleMessage(msg) {
|
|
208
|
+
switch (msg.type) {
|
|
209
|
+
case "heartbeat_ack":
|
|
210
|
+
break;
|
|
211
|
+
case "agent_status":
|
|
212
|
+
this.emit("peer", msg.agentId, msg.status);
|
|
213
|
+
break;
|
|
214
|
+
case "connect_incoming":
|
|
215
|
+
this.emit("connect_incoming", msg.sessionId, msg.fromAgentId, msg.fromAgentName);
|
|
216
|
+
break;
|
|
217
|
+
case "connect_accepted":
|
|
218
|
+
this.emit("connect_accepted", msg.sessionId, msg.agentId, msg.publicKey);
|
|
219
|
+
break;
|
|
220
|
+
case "connect_rejected":
|
|
221
|
+
this.emit("connect_rejected", msg.sessionId, msg.reason);
|
|
222
|
+
break;
|
|
223
|
+
case "signal_forward":
|
|
224
|
+
this.emit("signal", msg.sessionId, msg.fromAgentId, msg.signal);
|
|
225
|
+
break;
|
|
226
|
+
case "relay_assigned":
|
|
227
|
+
this.emit("relay_assigned", msg.sessionId, msg.relayHost, msg.relayPort, msg.relayToken);
|
|
228
|
+
break;
|
|
229
|
+
case "error":
|
|
230
|
+
this.emit("error", new AnycastError(msg.message, msg.code, msg.fatal || false));
|
|
231
|
+
if (msg.fatal) {
|
|
232
|
+
this.cleanup();
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
send(msg) {
|
|
238
|
+
if (this.ws?.readyState === WebSocket__default.default.OPEN) {
|
|
239
|
+
this.ws.send(JSON.stringify(msg));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
startHeartbeat() {
|
|
243
|
+
this.stopHeartbeat();
|
|
244
|
+
this.heartbeatTimer = setInterval(() => {
|
|
245
|
+
this.send({ type: "heartbeat" });
|
|
246
|
+
}, this.opts.heartbeatInterval);
|
|
247
|
+
}
|
|
248
|
+
stopHeartbeat() {
|
|
249
|
+
if (this.heartbeatTimer) {
|
|
250
|
+
clearInterval(this.heartbeatTimer);
|
|
251
|
+
this.heartbeatTimer = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
scheduleReconnect() {
|
|
255
|
+
if (this.reconnectTimer) return;
|
|
256
|
+
this.reconnectAttempt++;
|
|
257
|
+
const delay = Math.min(
|
|
258
|
+
this.opts.reconnectInterval * Math.pow(2, this.reconnectAttempt - 1),
|
|
259
|
+
3e4
|
|
260
|
+
);
|
|
261
|
+
this.setState("RECONNECTING" /* RECONNECTING */);
|
|
262
|
+
this.emit("reconnecting", this.reconnectAttempt);
|
|
263
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
264
|
+
this.reconnectTimer = null;
|
|
265
|
+
try {
|
|
266
|
+
await this.connect();
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}, delay);
|
|
270
|
+
}
|
|
271
|
+
cleanup() {
|
|
272
|
+
this.stopHeartbeat();
|
|
273
|
+
if (this.reconnectTimer) {
|
|
274
|
+
clearTimeout(this.reconnectTimer);
|
|
275
|
+
this.reconnectTimer = null;
|
|
276
|
+
}
|
|
277
|
+
if (this.ws) {
|
|
278
|
+
this.ws.removeAllListeners();
|
|
279
|
+
if (this.ws.readyState === WebSocket__default.default.OPEN || this.ws.readyState === WebSocket__default.default.CONNECTING) {
|
|
280
|
+
this.ws.close(1e3, "Client disconnect");
|
|
281
|
+
}
|
|
282
|
+
this.ws = null;
|
|
283
|
+
}
|
|
284
|
+
this.setState("DISCONNECTED" /* DISCONNECTED */);
|
|
285
|
+
}
|
|
286
|
+
setState(state) {
|
|
287
|
+
if (this.state !== state) {
|
|
288
|
+
this.state = state;
|
|
289
|
+
this.emit("stateChange", state);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
assertConnected() {
|
|
293
|
+
if (this.state !== "CONNECTED" /* CONNECTED */ || !this.ws) {
|
|
294
|
+
throw new AnycastError("Not connected", "NOT_CONNECTED", false);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
exports.AnycastAgent = AnycastAgent;
|
|
300
|
+
exports.AnycastError = AnycastError;
|
|
301
|
+
exports.ConnectionState = ConnectionState;
|
|
302
|
+
//# sourceMappingURL=index.js.map
|
|
303
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/agent.ts"],"names":["ConnectionState","EventEmitter","randomBytes","WebSocket"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Agent SDK types — mirrors the rendezvous server protocol.\n */\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface AnycastAgentOptions {\n /** WebSocket URL of the rendezvous server (e.g., wss://agents.anycast.com/rendezvous) */\n rendezvousUrl: string;\n /** Agent auth token from the Anycast portal */\n token: string;\n /** Unique machine identifier (auto-generated if not provided) */\n machineId?: string;\n /** Public key for end-to-end encryption */\n publicKey?: string;\n /** Agent version string */\n version?: string;\n /** Auto-reconnect on disconnect (default: true) */\n reconnect?: boolean;\n /** Reconnect interval in ms (default: 3000) */\n reconnectInterval?: number;\n /** Connection timeout in ms (default: 30000) */\n timeout?: number;\n /** Heartbeat interval in ms (default: 25000) */\n heartbeatInterval?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Connection state\n// ---------------------------------------------------------------------------\n\nexport enum ConnectionState {\n DISCONNECTED = 'DISCONNECTED',\n CONNECTING = 'CONNECTING',\n CONNECTED = 'CONNECTED',\n RECONNECTING = 'RECONNECTING',\n}\n\n// ---------------------------------------------------------------------------\n// Client → Server messages\n// ---------------------------------------------------------------------------\n\nexport interface RegisterMessage {\n type: 'register';\n token: string;\n machineId: string;\n publicKey: string;\n version?: string;\n os?: string;\n arch?: string;\n}\n\nexport interface HeartbeatMessage {\n type: 'heartbeat';\n}\n\nexport interface ConnectRequestMessage {\n type: 'connect_request';\n targetAgentId: string;\n sessionId: string;\n}\n\nexport interface SignalMessage {\n type: 'signal';\n sessionId: string;\n targetAgentId: string;\n signal: {\n type: 'offer' | 'answer' | 'ice';\n data: string;\n };\n}\n\nexport interface DisconnectMessage {\n type: 'disconnect';\n sessionId: string;\n}\n\nexport type ClientMessage =\n | RegisterMessage\n | HeartbeatMessage\n | ConnectRequestMessage\n | SignalMessage\n | DisconnectMessage;\n\n// ---------------------------------------------------------------------------\n// Server → Client messages\n// ---------------------------------------------------------------------------\n\nexport interface RegisteredMessage {\n type: 'registered';\n agentId: string;\n tenantId: string;\n name: string;\n}\n\nexport interface HeartbeatAckMessage {\n type: 'heartbeat_ack';\n timestamp: number;\n}\n\nexport interface ConnectIncomingMessage {\n type: 'connect_incoming';\n sessionId: string;\n fromAgentId: string;\n fromAgentName: string;\n}\n\nexport interface ConnectAcceptedMessage {\n type: 'connect_accepted';\n sessionId: string;\n agentId: string;\n agentName: string;\n publicKey: string;\n}\n\nexport interface ConnectRejectedMessage {\n type: 'connect_rejected';\n sessionId: string;\n reason: string;\n}\n\nexport interface SignalForwardMessage {\n type: 'signal_forward';\n sessionId: string;\n fromAgentId: string;\n signal: {\n type: 'offer' | 'answer' | 'ice';\n data: string;\n };\n}\n\nexport interface RelayAssignedMessage {\n type: 'relay_assigned';\n sessionId: string;\n relayHost: string;\n relayPort: number;\n relayToken: string;\n}\n\nexport interface AgentStatusMessage {\n type: 'agent_status';\n agentId: string;\n status: 'online' | 'offline';\n}\n\nexport interface ErrorMessage {\n type: 'error';\n code: string;\n message: string;\n fatal?: boolean;\n}\n\nexport type ServerMessage =\n | RegisteredMessage\n | HeartbeatAckMessage\n | ConnectIncomingMessage\n | ConnectAcceptedMessage\n | ConnectRejectedMessage\n | SignalForwardMessage\n | RelayAssignedMessage\n | AgentStatusMessage\n | ErrorMessage;\n\n// ---------------------------------------------------------------------------\n// Event map for typed event emitter\n// ---------------------------------------------------------------------------\n\nexport interface AnycastAgentEvents {\n connect: (info: { agentId: string; tenantId: string; name: string }) => void;\n disconnect: (reason: string) => void;\n reconnecting: (attempt: number) => void;\n message: (fromAgentId: string, data: string) => void;\n peer: (agentId: string, status: 'online' | 'offline') => void;\n 'connect_incoming': (sessionId: string, fromAgentId: string, fromAgentName: string) => void;\n 'connect_accepted': (sessionId: string, agentId: string, publicKey: string) => void;\n 'connect_rejected': (sessionId: string, reason: string) => void;\n signal: (sessionId: string, fromAgentId: string, signal: { type: string; data: string }) => void;\n 'relay_assigned': (sessionId: string, relayHost: string, relayPort: number, relayToken: string) => void;\n error: (error: AnycastError) => void;\n stateChange: (state: ConnectionState) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Error\n// ---------------------------------------------------------------------------\n\nexport class AnycastError extends Error {\n constructor(\n message: string,\n public code: string,\n public fatal: boolean = false\n ) {\n super(message);\n this.name = 'AnycastError';\n }\n}\n","import WebSocket from 'ws';\nimport EventEmitter from 'eventemitter3';\nimport { randomBytes } from 'crypto';\nimport {\n type AnycastAgentOptions,\n type AnycastAgentEvents,\n type ServerMessage,\n type ClientMessage,\n ConnectionState,\n AnycastError,\n} from './types.js';\n\nconst DEFAULT_OPTIONS = {\n reconnect: true,\n reconnectInterval: 3000,\n timeout: 30000,\n heartbeatInterval: 25000,\n};\n\n/**\n * AnycastAgent — connects to the Anycast Agents rendezvous server\n * for P2P connectivity, signaling, and relay fallback.\n *\n * @example\n * ```typescript\n * const agent = new AnycastAgent({\n * rendezvousUrl: 'wss://agents.anycast.com/rendezvous',\n * token: 'agt_...',\n * });\n *\n * agent.on('connect', ({ agentId }) => {\n * console.log(`Connected as ${agentId}`);\n * });\n *\n * agent.on('peer', (peerId, status) => {\n * console.log(`Peer ${peerId} is ${status}`);\n * });\n *\n * await agent.connect();\n * ```\n */\nexport class AnycastAgent extends EventEmitter<AnycastAgentEvents> {\n private ws: WebSocket | null = null;\n private state: ConnectionState = ConnectionState.DISCONNECTED;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private reconnectAttempt = 0;\n private intentionalClose = false;\n\n /** Agent ID assigned by the server after registration */\n public agentId: string | null = null;\n /** Tenant ID from the server */\n public tenantId: string | null = null;\n /** Agent name from the server */\n public agentName: string | null = null;\n\n private readonly opts: Required<\n Pick<AnycastAgentOptions, 'rendezvousUrl' | 'token' | 'machineId' | 'publicKey' | 'reconnect' | 'reconnectInterval' | 'timeout' | 'heartbeatInterval'>\n > & Pick<AnycastAgentOptions, 'version'>;\n\n constructor(options: AnycastAgentOptions) {\n super();\n\n this.opts = {\n rendezvousUrl: options.rendezvousUrl,\n token: options.token,\n machineId: options.machineId || randomBytes(16).toString('hex'),\n publicKey: options.publicKey || randomBytes(32).toString('base64'),\n version: options.version,\n reconnect: options.reconnect ?? DEFAULT_OPTIONS.reconnect,\n reconnectInterval: options.reconnectInterval ?? DEFAULT_OPTIONS.reconnectInterval,\n timeout: options.timeout ?? DEFAULT_OPTIONS.timeout,\n heartbeatInterval: options.heartbeatInterval ?? DEFAULT_OPTIONS.heartbeatInterval,\n };\n }\n\n /** Current connection state */\n get connectionState(): ConnectionState {\n return this.state;\n }\n\n /** Whether the agent is connected */\n get connected(): boolean {\n return this.state === ConnectionState.CONNECTED;\n }\n\n /**\n * Connect to the rendezvous server.\n * Resolves when registered, rejects on timeout or fatal error.\n */\n async connect(): Promise<void> {\n if (this.state === ConnectionState.CONNECTED || this.state === ConnectionState.CONNECTING) {\n return;\n }\n\n this.intentionalClose = false;\n this.setState(ConnectionState.CONNECTING);\n\n return new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.cleanup();\n const err = new AnycastError('Connection timeout', 'TIMEOUT', false);\n this.emit('error', err);\n reject(err);\n }, this.opts.timeout);\n\n try {\n this.ws = new WebSocket(this.opts.rendezvousUrl);\n } catch (err) {\n clearTimeout(timeout);\n const error = new AnycastError(\n `Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`,\n 'CONNECTION_FAILED',\n false\n );\n this.setState(ConnectionState.DISCONNECTED);\n reject(error);\n return;\n }\n\n this.ws.on('open', () => {\n this.send({\n type: 'register',\n token: this.opts.token,\n machineId: this.opts.machineId,\n publicKey: this.opts.publicKey,\n version: this.opts.version,\n });\n });\n\n this.ws.on('message', (data: WebSocket.Data) => {\n let msg: ServerMessage;\n try {\n msg = JSON.parse(data.toString());\n } catch {\n return;\n }\n\n if (msg.type === 'registered') {\n clearTimeout(timeout);\n this.agentId = msg.agentId;\n this.tenantId = msg.tenantId;\n this.agentName = msg.name;\n this.reconnectAttempt = 0;\n this.setState(ConnectionState.CONNECTED);\n this.startHeartbeat();\n this.emit('connect', { agentId: msg.agentId, tenantId: msg.tenantId, name: msg.name });\n resolve();\n return;\n }\n\n if (msg.type === 'error' && msg.fatal) {\n clearTimeout(timeout);\n const err = new AnycastError(msg.message, msg.code, true);\n this.emit('error', err);\n this.cleanup();\n reject(err);\n return;\n }\n\n // Route other messages after registration\n this.handleMessage(msg);\n });\n\n this.ws.on('close', (code, reason) => {\n clearTimeout(timeout);\n this.stopHeartbeat();\n const wasConnected = this.state === ConnectionState.CONNECTED;\n this.setState(ConnectionState.DISCONNECTED);\n\n if (wasConnected) {\n this.emit('disconnect', reason?.toString() || `Code ${code}`);\n }\n\n if (!this.intentionalClose && this.opts.reconnect) {\n this.scheduleReconnect();\n } else if (this.state !== ConnectionState.CONNECTED) {\n reject(new AnycastError('Connection closed', 'CLOSED', false));\n }\n });\n\n this.ws.on('error', (err) => {\n const error = new AnycastError(err.message, 'WS_ERROR', false);\n this.emit('error', error);\n });\n });\n }\n\n /**\n * Disconnect from the rendezvous server.\n */\n async disconnect(): Promise<void> {\n this.intentionalClose = true;\n this.cleanup();\n this.agentId = null;\n this.tenantId = null;\n this.agentName = null;\n }\n\n /**\n * Request a connection to a peer agent.\n * @returns The session ID for this connection\n */\n requestConnection(targetAgentId: string): string {\n this.assertConnected();\n const sessionId = randomBytes(16).toString('hex');\n this.send({\n type: 'connect_request',\n targetAgentId,\n sessionId,\n });\n return sessionId;\n }\n\n /**\n * Send a WebRTC signal to a peer via the rendezvous server.\n */\n sendSignal(\n sessionId: string,\n targetAgentId: string,\n signal: { type: 'offer' | 'answer' | 'ice'; data: string }\n ): void {\n this.assertConnected();\n this.send({\n type: 'signal',\n sessionId,\n targetAgentId,\n signal,\n });\n }\n\n /**\n * Disconnect a session.\n */\n disconnectSession(sessionId: string): void {\n this.assertConnected();\n this.send({ type: 'disconnect', sessionId });\n }\n\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n\n private handleMessage(msg: ServerMessage): void {\n switch (msg.type) {\n case 'heartbeat_ack':\n // No action needed\n break;\n\n case 'agent_status':\n this.emit('peer', msg.agentId, msg.status);\n break;\n\n case 'connect_incoming':\n this.emit('connect_incoming', msg.sessionId, msg.fromAgentId, msg.fromAgentName);\n break;\n\n case 'connect_accepted':\n this.emit('connect_accepted', msg.sessionId, msg.agentId, msg.publicKey);\n break;\n\n case 'connect_rejected':\n this.emit('connect_rejected', msg.sessionId, msg.reason);\n break;\n\n case 'signal_forward':\n this.emit('signal', msg.sessionId, msg.fromAgentId, msg.signal);\n break;\n\n case 'relay_assigned':\n this.emit('relay_assigned', msg.sessionId, msg.relayHost, msg.relayPort, msg.relayToken);\n break;\n\n case 'error':\n this.emit('error', new AnycastError(msg.message, msg.code, msg.fatal || false));\n if (msg.fatal) {\n this.cleanup();\n }\n break;\n }\n }\n\n private send(msg: ClientMessage): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n this.heartbeatTimer = setInterval(() => {\n this.send({ type: 'heartbeat' });\n }, this.opts.heartbeatInterval);\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer) return;\n\n this.reconnectAttempt++;\n // Exponential backoff: base * 2^attempt, capped at 30s\n const delay = Math.min(\n this.opts.reconnectInterval * Math.pow(2, this.reconnectAttempt - 1),\n 30000\n );\n\n this.setState(ConnectionState.RECONNECTING);\n this.emit('reconnecting', this.reconnectAttempt);\n\n this.reconnectTimer = setTimeout(async () => {\n this.reconnectTimer = null;\n try {\n await this.connect();\n } catch {\n // connect() will schedule another reconnect if needed\n }\n }, delay);\n }\n\n private cleanup(): void {\n this.stopHeartbeat();\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n this.ws.removeAllListeners();\n if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {\n this.ws.close(1000, 'Client disconnect');\n }\n this.ws = null;\n }\n this.setState(ConnectionState.DISCONNECTED);\n }\n\n private setState(state: ConnectionState): void {\n if (this.state !== state) {\n this.state = state;\n this.emit('stateChange', state);\n }\n }\n\n private assertConnected(): void {\n if (this.state !== ConnectionState.CONNECTED || !this.ws) {\n throw new AnycastError('Not connected', 'NOT_CONNECTED', false);\n }\n }\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import EventEmitter from 'eventemitter3';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
|
|
5
|
+
// src/agent.ts
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
|
|
9
|
+
ConnectionState2["DISCONNECTED"] = "DISCONNECTED";
|
|
10
|
+
ConnectionState2["CONNECTING"] = "CONNECTING";
|
|
11
|
+
ConnectionState2["CONNECTED"] = "CONNECTED";
|
|
12
|
+
ConnectionState2["RECONNECTING"] = "RECONNECTING";
|
|
13
|
+
return ConnectionState2;
|
|
14
|
+
})(ConnectionState || {});
|
|
15
|
+
var AnycastError = class extends Error {
|
|
16
|
+
constructor(message, code, fatal = false) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.fatal = fatal;
|
|
20
|
+
this.name = "AnycastError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/agent.ts
|
|
25
|
+
var DEFAULT_OPTIONS = {
|
|
26
|
+
reconnect: true,
|
|
27
|
+
reconnectInterval: 3e3,
|
|
28
|
+
timeout: 3e4,
|
|
29
|
+
heartbeatInterval: 25e3
|
|
30
|
+
};
|
|
31
|
+
var AnycastAgent = class extends EventEmitter {
|
|
32
|
+
constructor(options) {
|
|
33
|
+
super();
|
|
34
|
+
this.ws = null;
|
|
35
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
36
|
+
this.heartbeatTimer = null;
|
|
37
|
+
this.reconnectTimer = null;
|
|
38
|
+
this.reconnectAttempt = 0;
|
|
39
|
+
this.intentionalClose = false;
|
|
40
|
+
/** Agent ID assigned by the server after registration */
|
|
41
|
+
this.agentId = null;
|
|
42
|
+
/** Tenant ID from the server */
|
|
43
|
+
this.tenantId = null;
|
|
44
|
+
/** Agent name from the server */
|
|
45
|
+
this.agentName = null;
|
|
46
|
+
this.opts = {
|
|
47
|
+
rendezvousUrl: options.rendezvousUrl,
|
|
48
|
+
token: options.token,
|
|
49
|
+
machineId: options.machineId || randomBytes(16).toString("hex"),
|
|
50
|
+
publicKey: options.publicKey || randomBytes(32).toString("base64"),
|
|
51
|
+
version: options.version,
|
|
52
|
+
reconnect: options.reconnect ?? DEFAULT_OPTIONS.reconnect,
|
|
53
|
+
reconnectInterval: options.reconnectInterval ?? DEFAULT_OPTIONS.reconnectInterval,
|
|
54
|
+
timeout: options.timeout ?? DEFAULT_OPTIONS.timeout,
|
|
55
|
+
heartbeatInterval: options.heartbeatInterval ?? DEFAULT_OPTIONS.heartbeatInterval
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Current connection state */
|
|
59
|
+
get connectionState() {
|
|
60
|
+
return this.state;
|
|
61
|
+
}
|
|
62
|
+
/** Whether the agent is connected */
|
|
63
|
+
get connected() {
|
|
64
|
+
return this.state === "CONNECTED" /* CONNECTED */;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Connect to the rendezvous server.
|
|
68
|
+
* Resolves when registered, rejects on timeout or fatal error.
|
|
69
|
+
*/
|
|
70
|
+
async connect() {
|
|
71
|
+
if (this.state === "CONNECTED" /* CONNECTED */ || this.state === "CONNECTING" /* CONNECTING */) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.intentionalClose = false;
|
|
75
|
+
this.setState("CONNECTING" /* CONNECTING */);
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const timeout = setTimeout(() => {
|
|
78
|
+
this.cleanup();
|
|
79
|
+
const err = new AnycastError("Connection timeout", "TIMEOUT", false);
|
|
80
|
+
this.emit("error", err);
|
|
81
|
+
reject(err);
|
|
82
|
+
}, this.opts.timeout);
|
|
83
|
+
try {
|
|
84
|
+
this.ws = new WebSocket(this.opts.rendezvousUrl);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
const error = new AnycastError(
|
|
88
|
+
`Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`,
|
|
89
|
+
"CONNECTION_FAILED",
|
|
90
|
+
false
|
|
91
|
+
);
|
|
92
|
+
this.setState("DISCONNECTED" /* DISCONNECTED */);
|
|
93
|
+
reject(error);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.ws.on("open", () => {
|
|
97
|
+
this.send({
|
|
98
|
+
type: "register",
|
|
99
|
+
token: this.opts.token,
|
|
100
|
+
machineId: this.opts.machineId,
|
|
101
|
+
publicKey: this.opts.publicKey,
|
|
102
|
+
version: this.opts.version
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
this.ws.on("message", (data) => {
|
|
106
|
+
let msg;
|
|
107
|
+
try {
|
|
108
|
+
msg = JSON.parse(data.toString());
|
|
109
|
+
} catch {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (msg.type === "registered") {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
this.agentId = msg.agentId;
|
|
115
|
+
this.tenantId = msg.tenantId;
|
|
116
|
+
this.agentName = msg.name;
|
|
117
|
+
this.reconnectAttempt = 0;
|
|
118
|
+
this.setState("CONNECTED" /* CONNECTED */);
|
|
119
|
+
this.startHeartbeat();
|
|
120
|
+
this.emit("connect", { agentId: msg.agentId, tenantId: msg.tenantId, name: msg.name });
|
|
121
|
+
resolve();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (msg.type === "error" && msg.fatal) {
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
const err = new AnycastError(msg.message, msg.code, true);
|
|
127
|
+
this.emit("error", err);
|
|
128
|
+
this.cleanup();
|
|
129
|
+
reject(err);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.handleMessage(msg);
|
|
133
|
+
});
|
|
134
|
+
this.ws.on("close", (code, reason) => {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
this.stopHeartbeat();
|
|
137
|
+
const wasConnected = this.state === "CONNECTED" /* CONNECTED */;
|
|
138
|
+
this.setState("DISCONNECTED" /* DISCONNECTED */);
|
|
139
|
+
if (wasConnected) {
|
|
140
|
+
this.emit("disconnect", reason?.toString() || `Code ${code}`);
|
|
141
|
+
}
|
|
142
|
+
if (!this.intentionalClose && this.opts.reconnect) {
|
|
143
|
+
this.scheduleReconnect();
|
|
144
|
+
} else if (this.state !== "CONNECTED" /* CONNECTED */) {
|
|
145
|
+
reject(new AnycastError("Connection closed", "CLOSED", false));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
this.ws.on("error", (err) => {
|
|
149
|
+
const error = new AnycastError(err.message, "WS_ERROR", false);
|
|
150
|
+
this.emit("error", error);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Disconnect from the rendezvous server.
|
|
156
|
+
*/
|
|
157
|
+
async disconnect() {
|
|
158
|
+
this.intentionalClose = true;
|
|
159
|
+
this.cleanup();
|
|
160
|
+
this.agentId = null;
|
|
161
|
+
this.tenantId = null;
|
|
162
|
+
this.agentName = null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Request a connection to a peer agent.
|
|
166
|
+
* @returns The session ID for this connection
|
|
167
|
+
*/
|
|
168
|
+
requestConnection(targetAgentId) {
|
|
169
|
+
this.assertConnected();
|
|
170
|
+
const sessionId = randomBytes(16).toString("hex");
|
|
171
|
+
this.send({
|
|
172
|
+
type: "connect_request",
|
|
173
|
+
targetAgentId,
|
|
174
|
+
sessionId
|
|
175
|
+
});
|
|
176
|
+
return sessionId;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Send a WebRTC signal to a peer via the rendezvous server.
|
|
180
|
+
*/
|
|
181
|
+
sendSignal(sessionId, targetAgentId, signal) {
|
|
182
|
+
this.assertConnected();
|
|
183
|
+
this.send({
|
|
184
|
+
type: "signal",
|
|
185
|
+
sessionId,
|
|
186
|
+
targetAgentId,
|
|
187
|
+
signal
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Disconnect a session.
|
|
192
|
+
*/
|
|
193
|
+
disconnectSession(sessionId) {
|
|
194
|
+
this.assertConnected();
|
|
195
|
+
this.send({ type: "disconnect", sessionId });
|
|
196
|
+
}
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Private
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
handleMessage(msg) {
|
|
201
|
+
switch (msg.type) {
|
|
202
|
+
case "heartbeat_ack":
|
|
203
|
+
break;
|
|
204
|
+
case "agent_status":
|
|
205
|
+
this.emit("peer", msg.agentId, msg.status);
|
|
206
|
+
break;
|
|
207
|
+
case "connect_incoming":
|
|
208
|
+
this.emit("connect_incoming", msg.sessionId, msg.fromAgentId, msg.fromAgentName);
|
|
209
|
+
break;
|
|
210
|
+
case "connect_accepted":
|
|
211
|
+
this.emit("connect_accepted", msg.sessionId, msg.agentId, msg.publicKey);
|
|
212
|
+
break;
|
|
213
|
+
case "connect_rejected":
|
|
214
|
+
this.emit("connect_rejected", msg.sessionId, msg.reason);
|
|
215
|
+
break;
|
|
216
|
+
case "signal_forward":
|
|
217
|
+
this.emit("signal", msg.sessionId, msg.fromAgentId, msg.signal);
|
|
218
|
+
break;
|
|
219
|
+
case "relay_assigned":
|
|
220
|
+
this.emit("relay_assigned", msg.sessionId, msg.relayHost, msg.relayPort, msg.relayToken);
|
|
221
|
+
break;
|
|
222
|
+
case "error":
|
|
223
|
+
this.emit("error", new AnycastError(msg.message, msg.code, msg.fatal || false));
|
|
224
|
+
if (msg.fatal) {
|
|
225
|
+
this.cleanup();
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
send(msg) {
|
|
231
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
232
|
+
this.ws.send(JSON.stringify(msg));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
startHeartbeat() {
|
|
236
|
+
this.stopHeartbeat();
|
|
237
|
+
this.heartbeatTimer = setInterval(() => {
|
|
238
|
+
this.send({ type: "heartbeat" });
|
|
239
|
+
}, this.opts.heartbeatInterval);
|
|
240
|
+
}
|
|
241
|
+
stopHeartbeat() {
|
|
242
|
+
if (this.heartbeatTimer) {
|
|
243
|
+
clearInterval(this.heartbeatTimer);
|
|
244
|
+
this.heartbeatTimer = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
scheduleReconnect() {
|
|
248
|
+
if (this.reconnectTimer) return;
|
|
249
|
+
this.reconnectAttempt++;
|
|
250
|
+
const delay = Math.min(
|
|
251
|
+
this.opts.reconnectInterval * Math.pow(2, this.reconnectAttempt - 1),
|
|
252
|
+
3e4
|
|
253
|
+
);
|
|
254
|
+
this.setState("RECONNECTING" /* RECONNECTING */);
|
|
255
|
+
this.emit("reconnecting", this.reconnectAttempt);
|
|
256
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
257
|
+
this.reconnectTimer = null;
|
|
258
|
+
try {
|
|
259
|
+
await this.connect();
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}, delay);
|
|
263
|
+
}
|
|
264
|
+
cleanup() {
|
|
265
|
+
this.stopHeartbeat();
|
|
266
|
+
if (this.reconnectTimer) {
|
|
267
|
+
clearTimeout(this.reconnectTimer);
|
|
268
|
+
this.reconnectTimer = null;
|
|
269
|
+
}
|
|
270
|
+
if (this.ws) {
|
|
271
|
+
this.ws.removeAllListeners();
|
|
272
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
273
|
+
this.ws.close(1e3, "Client disconnect");
|
|
274
|
+
}
|
|
275
|
+
this.ws = null;
|
|
276
|
+
}
|
|
277
|
+
this.setState("DISCONNECTED" /* DISCONNECTED */);
|
|
278
|
+
}
|
|
279
|
+
setState(state) {
|
|
280
|
+
if (this.state !== state) {
|
|
281
|
+
this.state = state;
|
|
282
|
+
this.emit("stateChange", state);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
assertConnected() {
|
|
286
|
+
if (this.state !== "CONNECTED" /* CONNECTED */ || !this.ws) {
|
|
287
|
+
throw new AnycastError("Not connected", "NOT_CONNECTED", false);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export { AnycastAgent, AnycastError, ConnectionState };
|
|
293
|
+
//# sourceMappingURL=index.mjs.map
|
|
294
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/agent.ts"],"names":["ConnectionState"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Agent SDK types — mirrors the rendezvous server protocol.\n */\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface AnycastAgentOptions {\n /** WebSocket URL of the rendezvous server (e.g., wss://agents.anycast.com/rendezvous) */\n rendezvousUrl: string;\n /** Agent auth token from the Anycast portal */\n token: string;\n /** Unique machine identifier (auto-generated if not provided) */\n machineId?: string;\n /** Public key for end-to-end encryption */\n publicKey?: string;\n /** Agent version string */\n version?: string;\n /** Auto-reconnect on disconnect (default: true) */\n reconnect?: boolean;\n /** Reconnect interval in ms (default: 3000) */\n reconnectInterval?: number;\n /** Connection timeout in ms (default: 30000) */\n timeout?: number;\n /** Heartbeat interval in ms (default: 25000) */\n heartbeatInterval?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Connection state\n// ---------------------------------------------------------------------------\n\nexport enum ConnectionState {\n DISCONNECTED = 'DISCONNECTED',\n CONNECTING = 'CONNECTING',\n CONNECTED = 'CONNECTED',\n RECONNECTING = 'RECONNECTING',\n}\n\n// ---------------------------------------------------------------------------\n// Client → Server messages\n// ---------------------------------------------------------------------------\n\nexport interface RegisterMessage {\n type: 'register';\n token: string;\n machineId: string;\n publicKey: string;\n version?: string;\n os?: string;\n arch?: string;\n}\n\nexport interface HeartbeatMessage {\n type: 'heartbeat';\n}\n\nexport interface ConnectRequestMessage {\n type: 'connect_request';\n targetAgentId: string;\n sessionId: string;\n}\n\nexport interface SignalMessage {\n type: 'signal';\n sessionId: string;\n targetAgentId: string;\n signal: {\n type: 'offer' | 'answer' | 'ice';\n data: string;\n };\n}\n\nexport interface DisconnectMessage {\n type: 'disconnect';\n sessionId: string;\n}\n\nexport type ClientMessage =\n | RegisterMessage\n | HeartbeatMessage\n | ConnectRequestMessage\n | SignalMessage\n | DisconnectMessage;\n\n// ---------------------------------------------------------------------------\n// Server → Client messages\n// ---------------------------------------------------------------------------\n\nexport interface RegisteredMessage {\n type: 'registered';\n agentId: string;\n tenantId: string;\n name: string;\n}\n\nexport interface HeartbeatAckMessage {\n type: 'heartbeat_ack';\n timestamp: number;\n}\n\nexport interface ConnectIncomingMessage {\n type: 'connect_incoming';\n sessionId: string;\n fromAgentId: string;\n fromAgentName: string;\n}\n\nexport interface ConnectAcceptedMessage {\n type: 'connect_accepted';\n sessionId: string;\n agentId: string;\n agentName: string;\n publicKey: string;\n}\n\nexport interface ConnectRejectedMessage {\n type: 'connect_rejected';\n sessionId: string;\n reason: string;\n}\n\nexport interface SignalForwardMessage {\n type: 'signal_forward';\n sessionId: string;\n fromAgentId: string;\n signal: {\n type: 'offer' | 'answer' | 'ice';\n data: string;\n };\n}\n\nexport interface RelayAssignedMessage {\n type: 'relay_assigned';\n sessionId: string;\n relayHost: string;\n relayPort: number;\n relayToken: string;\n}\n\nexport interface AgentStatusMessage {\n type: 'agent_status';\n agentId: string;\n status: 'online' | 'offline';\n}\n\nexport interface ErrorMessage {\n type: 'error';\n code: string;\n message: string;\n fatal?: boolean;\n}\n\nexport type ServerMessage =\n | RegisteredMessage\n | HeartbeatAckMessage\n | ConnectIncomingMessage\n | ConnectAcceptedMessage\n | ConnectRejectedMessage\n | SignalForwardMessage\n | RelayAssignedMessage\n | AgentStatusMessage\n | ErrorMessage;\n\n// ---------------------------------------------------------------------------\n// Event map for typed event emitter\n// ---------------------------------------------------------------------------\n\nexport interface AnycastAgentEvents {\n connect: (info: { agentId: string; tenantId: string; name: string }) => void;\n disconnect: (reason: string) => void;\n reconnecting: (attempt: number) => void;\n message: (fromAgentId: string, data: string) => void;\n peer: (agentId: string, status: 'online' | 'offline') => void;\n 'connect_incoming': (sessionId: string, fromAgentId: string, fromAgentName: string) => void;\n 'connect_accepted': (sessionId: string, agentId: string, publicKey: string) => void;\n 'connect_rejected': (sessionId: string, reason: string) => void;\n signal: (sessionId: string, fromAgentId: string, signal: { type: string; data: string }) => void;\n 'relay_assigned': (sessionId: string, relayHost: string, relayPort: number, relayToken: string) => void;\n error: (error: AnycastError) => void;\n stateChange: (state: ConnectionState) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Error\n// ---------------------------------------------------------------------------\n\nexport class AnycastError extends Error {\n constructor(\n message: string,\n public code: string,\n public fatal: boolean = false\n ) {\n super(message);\n this.name = 'AnycastError';\n }\n}\n","import WebSocket from 'ws';\nimport EventEmitter from 'eventemitter3';\nimport { randomBytes } from 'crypto';\nimport {\n type AnycastAgentOptions,\n type AnycastAgentEvents,\n type ServerMessage,\n type ClientMessage,\n ConnectionState,\n AnycastError,\n} from './types.js';\n\nconst DEFAULT_OPTIONS = {\n reconnect: true,\n reconnectInterval: 3000,\n timeout: 30000,\n heartbeatInterval: 25000,\n};\n\n/**\n * AnycastAgent — connects to the Anycast Agents rendezvous server\n * for P2P connectivity, signaling, and relay fallback.\n *\n * @example\n * ```typescript\n * const agent = new AnycastAgent({\n * rendezvousUrl: 'wss://agents.anycast.com/rendezvous',\n * token: 'agt_...',\n * });\n *\n * agent.on('connect', ({ agentId }) => {\n * console.log(`Connected as ${agentId}`);\n * });\n *\n * agent.on('peer', (peerId, status) => {\n * console.log(`Peer ${peerId} is ${status}`);\n * });\n *\n * await agent.connect();\n * ```\n */\nexport class AnycastAgent extends EventEmitter<AnycastAgentEvents> {\n private ws: WebSocket | null = null;\n private state: ConnectionState = ConnectionState.DISCONNECTED;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private reconnectAttempt = 0;\n private intentionalClose = false;\n\n /** Agent ID assigned by the server after registration */\n public agentId: string | null = null;\n /** Tenant ID from the server */\n public tenantId: string | null = null;\n /** Agent name from the server */\n public agentName: string | null = null;\n\n private readonly opts: Required<\n Pick<AnycastAgentOptions, 'rendezvousUrl' | 'token' | 'machineId' | 'publicKey' | 'reconnect' | 'reconnectInterval' | 'timeout' | 'heartbeatInterval'>\n > & Pick<AnycastAgentOptions, 'version'>;\n\n constructor(options: AnycastAgentOptions) {\n super();\n\n this.opts = {\n rendezvousUrl: options.rendezvousUrl,\n token: options.token,\n machineId: options.machineId || randomBytes(16).toString('hex'),\n publicKey: options.publicKey || randomBytes(32).toString('base64'),\n version: options.version,\n reconnect: options.reconnect ?? DEFAULT_OPTIONS.reconnect,\n reconnectInterval: options.reconnectInterval ?? DEFAULT_OPTIONS.reconnectInterval,\n timeout: options.timeout ?? DEFAULT_OPTIONS.timeout,\n heartbeatInterval: options.heartbeatInterval ?? DEFAULT_OPTIONS.heartbeatInterval,\n };\n }\n\n /** Current connection state */\n get connectionState(): ConnectionState {\n return this.state;\n }\n\n /** Whether the agent is connected */\n get connected(): boolean {\n return this.state === ConnectionState.CONNECTED;\n }\n\n /**\n * Connect to the rendezvous server.\n * Resolves when registered, rejects on timeout or fatal error.\n */\n async connect(): Promise<void> {\n if (this.state === ConnectionState.CONNECTED || this.state === ConnectionState.CONNECTING) {\n return;\n }\n\n this.intentionalClose = false;\n this.setState(ConnectionState.CONNECTING);\n\n return new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.cleanup();\n const err = new AnycastError('Connection timeout', 'TIMEOUT', false);\n this.emit('error', err);\n reject(err);\n }, this.opts.timeout);\n\n try {\n this.ws = new WebSocket(this.opts.rendezvousUrl);\n } catch (err) {\n clearTimeout(timeout);\n const error = new AnycastError(\n `Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`,\n 'CONNECTION_FAILED',\n false\n );\n this.setState(ConnectionState.DISCONNECTED);\n reject(error);\n return;\n }\n\n this.ws.on('open', () => {\n this.send({\n type: 'register',\n token: this.opts.token,\n machineId: this.opts.machineId,\n publicKey: this.opts.publicKey,\n version: this.opts.version,\n });\n });\n\n this.ws.on('message', (data: WebSocket.Data) => {\n let msg: ServerMessage;\n try {\n msg = JSON.parse(data.toString());\n } catch {\n return;\n }\n\n if (msg.type === 'registered') {\n clearTimeout(timeout);\n this.agentId = msg.agentId;\n this.tenantId = msg.tenantId;\n this.agentName = msg.name;\n this.reconnectAttempt = 0;\n this.setState(ConnectionState.CONNECTED);\n this.startHeartbeat();\n this.emit('connect', { agentId: msg.agentId, tenantId: msg.tenantId, name: msg.name });\n resolve();\n return;\n }\n\n if (msg.type === 'error' && msg.fatal) {\n clearTimeout(timeout);\n const err = new AnycastError(msg.message, msg.code, true);\n this.emit('error', err);\n this.cleanup();\n reject(err);\n return;\n }\n\n // Route other messages after registration\n this.handleMessage(msg);\n });\n\n this.ws.on('close', (code, reason) => {\n clearTimeout(timeout);\n this.stopHeartbeat();\n const wasConnected = this.state === ConnectionState.CONNECTED;\n this.setState(ConnectionState.DISCONNECTED);\n\n if (wasConnected) {\n this.emit('disconnect', reason?.toString() || `Code ${code}`);\n }\n\n if (!this.intentionalClose && this.opts.reconnect) {\n this.scheduleReconnect();\n } else if (this.state !== ConnectionState.CONNECTED) {\n reject(new AnycastError('Connection closed', 'CLOSED', false));\n }\n });\n\n this.ws.on('error', (err) => {\n const error = new AnycastError(err.message, 'WS_ERROR', false);\n this.emit('error', error);\n });\n });\n }\n\n /**\n * Disconnect from the rendezvous server.\n */\n async disconnect(): Promise<void> {\n this.intentionalClose = true;\n this.cleanup();\n this.agentId = null;\n this.tenantId = null;\n this.agentName = null;\n }\n\n /**\n * Request a connection to a peer agent.\n * @returns The session ID for this connection\n */\n requestConnection(targetAgentId: string): string {\n this.assertConnected();\n const sessionId = randomBytes(16).toString('hex');\n this.send({\n type: 'connect_request',\n targetAgentId,\n sessionId,\n });\n return sessionId;\n }\n\n /**\n * Send a WebRTC signal to a peer via the rendezvous server.\n */\n sendSignal(\n sessionId: string,\n targetAgentId: string,\n signal: { type: 'offer' | 'answer' | 'ice'; data: string }\n ): void {\n this.assertConnected();\n this.send({\n type: 'signal',\n sessionId,\n targetAgentId,\n signal,\n });\n }\n\n /**\n * Disconnect a session.\n */\n disconnectSession(sessionId: string): void {\n this.assertConnected();\n this.send({ type: 'disconnect', sessionId });\n }\n\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n\n private handleMessage(msg: ServerMessage): void {\n switch (msg.type) {\n case 'heartbeat_ack':\n // No action needed\n break;\n\n case 'agent_status':\n this.emit('peer', msg.agentId, msg.status);\n break;\n\n case 'connect_incoming':\n this.emit('connect_incoming', msg.sessionId, msg.fromAgentId, msg.fromAgentName);\n break;\n\n case 'connect_accepted':\n this.emit('connect_accepted', msg.sessionId, msg.agentId, msg.publicKey);\n break;\n\n case 'connect_rejected':\n this.emit('connect_rejected', msg.sessionId, msg.reason);\n break;\n\n case 'signal_forward':\n this.emit('signal', msg.sessionId, msg.fromAgentId, msg.signal);\n break;\n\n case 'relay_assigned':\n this.emit('relay_assigned', msg.sessionId, msg.relayHost, msg.relayPort, msg.relayToken);\n break;\n\n case 'error':\n this.emit('error', new AnycastError(msg.message, msg.code, msg.fatal || false));\n if (msg.fatal) {\n this.cleanup();\n }\n break;\n }\n }\n\n private send(msg: ClientMessage): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n this.heartbeatTimer = setInterval(() => {\n this.send({ type: 'heartbeat' });\n }, this.opts.heartbeatInterval);\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer) return;\n\n this.reconnectAttempt++;\n // Exponential backoff: base * 2^attempt, capped at 30s\n const delay = Math.min(\n this.opts.reconnectInterval * Math.pow(2, this.reconnectAttempt - 1),\n 30000\n );\n\n this.setState(ConnectionState.RECONNECTING);\n this.emit('reconnecting', this.reconnectAttempt);\n\n this.reconnectTimer = setTimeout(async () => {\n this.reconnectTimer = null;\n try {\n await this.connect();\n } catch {\n // connect() will schedule another reconnect if needed\n }\n }, delay);\n }\n\n private cleanup(): void {\n this.stopHeartbeat();\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n this.ws.removeAllListeners();\n if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {\n this.ws.close(1000, 'Client disconnect');\n }\n this.ws = null;\n }\n this.setState(ConnectionState.DISCONNECTED);\n }\n\n private setState(state: ConnectionState): void {\n if (this.state !== state) {\n this.state = state;\n this.emit('stateChange', state);\n }\n }\n\n private assertConnected(): void {\n if (this.state !== ConnectionState.CONNECTED || !this.ws) {\n throw new AnycastError('Not connected', 'NOT_CONNECTED', false);\n }\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anycast/agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for Anycast Agents P2P connectivity",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"lint": "tsc --noEmit",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"eventemitter3": "^5.0.1",
|
|
29
|
+
"ws": "^8.16.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.11.0",
|
|
33
|
+
"@types/ws": "^8.5.10",
|
|
34
|
+
"tsup": "^8.0.1",
|
|
35
|
+
"typescript": "^5.3.3",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"anycast",
|
|
40
|
+
"p2p",
|
|
41
|
+
"agents",
|
|
42
|
+
"webrtc",
|
|
43
|
+
"relay",
|
|
44
|
+
"connectivity"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|