@delego/runner 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 ADDED
@@ -0,0 +1,64 @@
1
+ # @delego/sdk
2
+
3
+ Delego SDK for registering and executing actions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @delego/sdk
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { DelegoClient } from '@delego/sdk';
15
+
16
+ const client = new DelegoClient({
17
+ apiKey: process.env.DELEGO_API_KEY!,
18
+ baseUrl: 'http://localhost:4000', // optional, defaults to localhost:4000
19
+ runnerName: 'my-runner', // optional, defaults to hostname
20
+ });
21
+
22
+ // Register an action
23
+ client.registerAction({
24
+ name: 'reset_password',
25
+ description: 'Reset a user password',
26
+ parameters: [
27
+ {
28
+ name: 'email',
29
+ type: 'string',
30
+ description: 'User email',
31
+ required: true,
32
+ },
33
+ ],
34
+ handler: async ({ email }) => {
35
+ // Your handler logic here
36
+ return { success: true };
37
+ },
38
+ });
39
+
40
+ // Connect to Delego (registers actions and opens WebSocket)
41
+ await client.connect();
42
+ ```
43
+
44
+ ## API Reference
45
+
46
+ ### `DelegoClient`
47
+
48
+ Main client class for interacting with Delego.
49
+
50
+ #### Constructor
51
+
52
+ ```typescript
53
+ new DelegoClient(config: DelegoConfig)
54
+ ```
55
+
56
+ #### Methods
57
+
58
+ - `registerAction(action: Action): void` - Register an action with a handler
59
+ - `connect(): Promise<void>` - Connect to Delego and register actions
60
+ - `disconnect(): Promise<void>` - Disconnect from Delego
61
+
62
+ ## Types
63
+
64
+ See `src/types.ts` for full type definitions.
@@ -0,0 +1,28 @@
1
+ import { Action, DelegoConfig } from './types';
2
+ export declare class DelegoClient {
3
+ private runnerId;
4
+ private runnerIdPromise;
5
+ private config;
6
+ private actions;
7
+ private connected;
8
+ private encoreClient;
9
+ private actionIds;
10
+ private wsConnection;
11
+ constructor(config: DelegoConfig);
12
+ /**
13
+ * Get the runner ID, ensuring it's initialized if needed
14
+ */
15
+ getRunnerId(): Promise<string>;
16
+ registerAction(action: Action): void;
17
+ getRegisteredActions(): Action[];
18
+ getHandler(actionName: string): Action['handler'] | undefined;
19
+ connect(): Promise<void>;
20
+ /**
21
+ * Validates if user has required roles to execute an action.
22
+ * Returns true if action is unrestricted (no requiredRoles) or user has at least one required role.
23
+ */
24
+ private validateRoles;
25
+ private handleInvocation;
26
+ private connectWebSocket;
27
+ disconnect(): Promise<void>;
28
+ }
package/dist/client.js ADDED
@@ -0,0 +1,242 @@
1
+ import os from 'os';
2
+ import Client from './generated-client';
3
+ import { WebSocketConnection } from './connection';
4
+ import { getOrCreateRunnerId } from './runner-id';
5
+ export class DelegoClient {
6
+ runnerId = null;
7
+ runnerIdPromise = null;
8
+ config;
9
+ actions = new Map();
10
+ connected = false;
11
+ encoreClient = null;
12
+ actionIds = [];
13
+ wsConnection = null;
14
+ constructor(config) {
15
+ // If runnerId is explicitly provided, use it immediately
16
+ if (config.runnerId) {
17
+ this.runnerId = config.runnerId;
18
+ }
19
+ else {
20
+ // Otherwise, initialize async (will be resolved when first accessed)
21
+ this.runnerIdPromise = getOrCreateRunnerId().then((id) => {
22
+ this.runnerId = id;
23
+ return id;
24
+ });
25
+ }
26
+ this.config = {
27
+ apiKey: config.apiKey,
28
+ baseUrl: config.baseUrl || 'https://production-delego-backend-trui.encr.app',
29
+ runnerName: config.runnerName || os.hostname(),
30
+ debug: config.debug ?? false,
31
+ emailRoles: config.emailRoles,
32
+ };
33
+ }
34
+ /**
35
+ * Get the runner ID, ensuring it's initialized if needed
36
+ */
37
+ async getRunnerId() {
38
+ if (this.runnerId !== null) {
39
+ return this.runnerId;
40
+ }
41
+ if (this.runnerIdPromise) {
42
+ return await this.runnerIdPromise;
43
+ }
44
+ // Fallback (shouldn't happen)
45
+ this.runnerIdPromise = getOrCreateRunnerId().then((id) => {
46
+ this.runnerId = id;
47
+ return id;
48
+ });
49
+ return await this.runnerIdPromise;
50
+ }
51
+ registerAction(action) {
52
+ // Validate required fields
53
+ if (!action.name) {
54
+ throw new Error('Action name is required');
55
+ }
56
+ if (!action.description) {
57
+ throw new Error('Action description is required');
58
+ }
59
+ if (!action.handler || typeof action.handler !== 'function') {
60
+ throw new Error('Action handler must be a function');
61
+ }
62
+ // Check for duplicates
63
+ if (this.actions.has(action.name)) {
64
+ throw new Error(`Action "${action.name}" already registered`);
65
+ }
66
+ // Store action
67
+ this.actions.set(action.name, action);
68
+ }
69
+ getRegisteredActions() {
70
+ return Array.from(this.actions.values());
71
+ }
72
+ getHandler(actionName) {
73
+ return this.actions.get(actionName)?.handler;
74
+ }
75
+ async connect() {
76
+ if (this.connected) {
77
+ throw new Error('Already connected');
78
+ }
79
+ if (this.actions.size === 0) {
80
+ throw new Error('No actions registered. Call registerAction() first.');
81
+ }
82
+ // Ensure runner ID is initialized before connecting
83
+ const runnerId = await this.getRunnerId();
84
+ // Create Encore client instance
85
+ this.encoreClient = new Client(this.config.baseUrl);
86
+ try {
87
+ // 1. Establish WebSocket connection first (this creates the runner record)
88
+ await this.connectWebSocket();
89
+ // 2. Register actions via REST using generated client (runner now exists)
90
+ const actions = Array.from(this.actions.values()).map((a) => ({
91
+ name: a.name,
92
+ description: a.description,
93
+ parameters: a.parameters || [],
94
+ requiredRoles: a.requiredRoles,
95
+ }));
96
+ // Set up timeout for registration request
97
+ const timeoutPromise = new Promise((_, reject) => {
98
+ setTimeout(() => reject(new Error('Registration request timed out after 30 seconds')), 30000);
99
+ });
100
+ try {
101
+ const result = await Promise.race([
102
+ this.encoreClient.core.registerActions({
103
+ runnerId,
104
+ actions,
105
+ apiKey: this.config.apiKey,
106
+ emailRoles: this.config.emailRoles,
107
+ }),
108
+ timeoutPromise,
109
+ ]);
110
+ // Store action IDs if needed
111
+ this.actionIds = result.actionIds;
112
+ }
113
+ catch (error) {
114
+ // Check if it's a timeout error
115
+ if (error.message === 'Registration request timed out after 30 seconds') {
116
+ throw error;
117
+ }
118
+ // Handle API errors
119
+ if (error.status) {
120
+ if (error.status === 401) {
121
+ throw new Error('Invalid API key');
122
+ }
123
+ else if (error.status === 404) {
124
+ throw new Error(`Runner ${this.runnerId} not found`);
125
+ }
126
+ else if (error.status === 403) {
127
+ throw new Error('Permission denied: Runner does not belong to tenant');
128
+ }
129
+ else if (error.status === 400) {
130
+ throw new Error(`Invalid request: ${error.message || 'Bad request'}`);
131
+ }
132
+ else {
133
+ throw new Error(`Registration failed: ${error.message || `HTTP ${error.status}`}`);
134
+ }
135
+ }
136
+ throw new Error(`Failed to register actions: ${error.message || String(error)}`);
137
+ }
138
+ this.connected = true;
139
+ console.log(`Connected to Delego as runner ${runnerId}`);
140
+ }
141
+ catch (error) {
142
+ // Clean up on failure
143
+ this.connected = false;
144
+ if (this.wsConnection) {
145
+ this.wsConnection.disconnect();
146
+ this.wsConnection = null;
147
+ }
148
+ throw error;
149
+ }
150
+ }
151
+ /**
152
+ * Validates if user has required roles to execute an action.
153
+ * Returns true if action is unrestricted (no requiredRoles) or user has at least one required role.
154
+ */
155
+ validateRoles(action, userContext) {
156
+ console.log('validateRoles', action, userContext);
157
+ // No required roles = unrestricted (allow all users)
158
+ if (!action.requiredRoles || action.requiredRoles.length === 0) {
159
+ return true;
160
+ }
161
+ // Get user's roles from email mapping
162
+ const userEmail = userContext?.email;
163
+ if (!userEmail) {
164
+ // User has no email = no roles
165
+ return false;
166
+ }
167
+ const userRoles = this.config.emailRoles?.[userEmail] ?? [];
168
+ // User needs at least one of the required roles (OR logic)
169
+ return action.requiredRoles.some((role) => userRoles.includes(role));
170
+ }
171
+ async handleInvocation(invocationId, actionName, parameters, userContext) {
172
+ const action = this.actions.get(actionName);
173
+ if (!action) {
174
+ // Unknown action - send error response immediately
175
+ await this.wsConnection?.sendResult(invocationId, false, undefined, `Unknown action: ${actionName}`);
176
+ return;
177
+ }
178
+ // Validate roles before executing handler
179
+ if (!this.validateRoles(action, userContext)) {
180
+ // User is unauthorized - send special error format
181
+ const errorMessage = JSON.stringify({
182
+ success: false,
183
+ error: 'UNAUTHORIZED',
184
+ message: 'You do not have permission to execute this action',
185
+ requiredRoles: action.requiredRoles,
186
+ });
187
+ await this.wsConnection?.sendResult(invocationId, false, undefined, errorMessage);
188
+ return;
189
+ }
190
+ try {
191
+ // Construct InvocationContext - user context defaults to empty if not provided
192
+ const context = {
193
+ invocationId,
194
+ parameters,
195
+ user: userContext || {
196
+ slackUserId: '',
197
+ groupIds: [],
198
+ },
199
+ };
200
+ console.log(`Executing action: ${actionName}`, context);
201
+ const result = await action.handler(context);
202
+ await this.wsConnection?.sendResult(invocationId, true, result);
203
+ console.log(`Action completed: ${actionName}`, result);
204
+ }
205
+ catch (error) {
206
+ const errorMessage = error instanceof Error ? error.message : String(error);
207
+ console.error(`Action failed: ${actionName}`, errorMessage);
208
+ await this.wsConnection?.sendResult(invocationId, false, undefined, errorMessage);
209
+ }
210
+ }
211
+ async connectWebSocket() {
212
+ // Ensure runner ID is initialized
213
+ const runnerId = await this.getRunnerId();
214
+ const connectionOptions = {
215
+ baseUrl: this.config.baseUrl,
216
+ apiKey: this.config.apiKey,
217
+ runnerId,
218
+ runnerName: this.config.runnerName,
219
+ debug: this.config.debug,
220
+ onInvoke: (invocationId, actionName, params, userContext) => {
221
+ // Handle async without blocking - fire and forget pattern
222
+ this.handleInvocation(invocationId, actionName, params, userContext).catch((err) => {
223
+ console.error('Unexpected error handling invocation:', err);
224
+ });
225
+ },
226
+ onDisconnect: () => {
227
+ this.connected = false;
228
+ console.log('Disconnected from Delego');
229
+ },
230
+ };
231
+ this.wsConnection = new WebSocketConnection(connectionOptions);
232
+ await this.wsConnection.connect();
233
+ }
234
+ async disconnect() {
235
+ if (this.wsConnection) {
236
+ this.wsConnection.disconnect();
237
+ this.wsConnection = null;
238
+ }
239
+ this.connected = false;
240
+ }
241
+ }
242
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,336 @@
1
+ // WebSocket connection management for Delego SDK
2
+ import { WebSocket as WS } from 'ws';
3
+ import { StreamInOut } from './generated-client';
4
+ if (typeof globalThis.WebSocket === 'undefined') {
5
+ globalThis.WebSocket = WS;
6
+ }
7
+ // ============================================================================
8
+ // URL HELPERS
9
+ // ============================================================================
10
+ function buildWebSocketUrl(baseUrl) {
11
+ return baseUrl
12
+ .replace(/^http:\/\//, 'ws://')
13
+ .replace(/^https:\/\//, 'wss://');
14
+ }
15
+ function buildConnectionUrl(baseUrl, apiKey, runnerId, runnerName) {
16
+ const wsUrl = buildWebSocketUrl(baseUrl);
17
+ const queryParams = new URLSearchParams({ apiKey, runnerId });
18
+ if (runnerName) {
19
+ queryParams.set('runnerName', runnerName);
20
+ }
21
+ return `${wsUrl}/core/runners/connect?${queryParams.toString()}`;
22
+ }
23
+ // ============================================================================
24
+ // MESSAGE PARSER
25
+ // ============================================================================
26
+ class MessageParser {
27
+ parse(data) {
28
+ const rawData = this.extractRawData(data);
29
+ return this.parseJson(rawData);
30
+ }
31
+ extractRawData(data) {
32
+ if (data && typeof data === 'object' && 'data' in data) {
33
+ return data.data;
34
+ }
35
+ return data;
36
+ }
37
+ parseJson(rawData) {
38
+ if (typeof rawData === 'string') {
39
+ return JSON.parse(rawData);
40
+ }
41
+ if (rawData instanceof Buffer || ArrayBuffer.isView(rawData)) {
42
+ return JSON.parse(rawData.toString());
43
+ }
44
+ throw new Error('Unable to parse message data');
45
+ }
46
+ }
47
+ // ============================================================================
48
+ // CONNECTION LOGGER
49
+ // ============================================================================
50
+ class ConnectionLogger {
51
+ debug;
52
+ constructor(debug = false) {
53
+ this.debug = debug;
54
+ }
55
+ log(message, ...args) {
56
+ if (this.debug) {
57
+ console.log(`[Delego] ${message}`, ...args);
58
+ }
59
+ }
60
+ error(message, ...args) {
61
+ console.error(`[Delego] ${message}`, ...args);
62
+ }
63
+ }
64
+ class HeartbeatManager {
65
+ callbacks;
66
+ logger;
67
+ interval = null;
68
+ intervalMs = 10000;
69
+ constructor(callbacks, logger) {
70
+ this.callbacks = callbacks;
71
+ this.logger = logger;
72
+ }
73
+ start() {
74
+ this.stop();
75
+ this.logger.log('Starting heartbeat (every 10s)');
76
+ this.interval = setInterval(() => this.tick(), this.intervalMs);
77
+ }
78
+ stop() {
79
+ if (this.interval) {
80
+ clearInterval(this.interval);
81
+ this.interval = null;
82
+ }
83
+ }
84
+ async tick() {
85
+ this.logger.log('Sending heartbeat...');
86
+ try {
87
+ await this.callbacks.sendHeartbeat();
88
+ this.logger.log('Heartbeat sent');
89
+ }
90
+ catch (error) {
91
+ this.logger.error('Failed to send heartbeat:', error);
92
+ this.stop();
93
+ this.callbacks.onHeartbeatFailed(error);
94
+ }
95
+ }
96
+ }
97
+ class ReconnectionManager {
98
+ logger;
99
+ attempts = 0;
100
+ stopped = false;
101
+ config;
102
+ constructor(config = {}, logger) {
103
+ this.logger = logger;
104
+ this.config = {
105
+ initialDelayMs: config.initialDelayMs ?? 1000,
106
+ maxDelayMs: config.maxDelayMs ?? 30000,
107
+ };
108
+ }
109
+ reset() {
110
+ this.attempts = 0;
111
+ this.stopped = false;
112
+ }
113
+ canRetry() {
114
+ return !this.stopped;
115
+ }
116
+ stop() {
117
+ this.stopped = true;
118
+ }
119
+ async waitAndIncrement() {
120
+ this.attempts++;
121
+ const delay = Math.min(this.config.initialDelayMs * Math.pow(2, this.attempts - 1), this.config.maxDelayMs);
122
+ this.logger.log(`Waiting ${delay}ms before reconnecting (attempt ${this.attempts})...`);
123
+ await new Promise((resolve) => setTimeout(resolve, delay));
124
+ }
125
+ getAttemptCount() {
126
+ return this.attempts;
127
+ }
128
+ }
129
+ // ============================================================================
130
+ // MAIN CLASS
131
+ // ============================================================================
132
+ export class WebSocketConnection {
133
+ stream = null;
134
+ state = 'disconnected';
135
+ options;
136
+ logger;
137
+ messageParser = new MessageParser();
138
+ heartbeat;
139
+ reconnection;
140
+ constructor(options) {
141
+ this.options = options;
142
+ this.logger = new ConnectionLogger(options.debug ?? false);
143
+ this.heartbeat = new HeartbeatManager({
144
+ sendHeartbeat: () => this.sendHeartbeat(),
145
+ onHeartbeatFailed: () => this.handleHeartbeatFailure(),
146
+ }, this.logger);
147
+ this.reconnection = new ReconnectionManager({}, this.logger);
148
+ }
149
+ getState() {
150
+ return this.state;
151
+ }
152
+ async connect() {
153
+ this.guardNotAlreadyConnecting();
154
+ this.state = 'connecting';
155
+ this.reconnection.reset();
156
+ await this.attemptConnection();
157
+ }
158
+ async attemptConnection() {
159
+ try {
160
+ this.stream = this.createStream();
161
+ await this.waitForReady();
162
+ this.startMessageLoop();
163
+ }
164
+ catch (error) {
165
+ this.state = 'disconnected';
166
+ if (!this.reconnection.canRetry()) {
167
+ throw error;
168
+ }
169
+ this.logger.error('Connection failed:', error);
170
+ await this.reconnection.waitAndIncrement();
171
+ this.state = 'connecting';
172
+ await this.attemptConnection();
173
+ }
174
+ }
175
+ async send(msg) {
176
+ if (!this.stream) {
177
+ throw new Error('Not connected');
178
+ }
179
+ if (this.state !== 'connected') {
180
+ throw new Error('Connection not established');
181
+ }
182
+ await this.stream.send(msg);
183
+ }
184
+ async sendResult(invocationId, success, result, error) {
185
+ await this.send({
186
+ type: 'invocation_result',
187
+ invocationId,
188
+ success,
189
+ result,
190
+ error,
191
+ });
192
+ }
193
+ disconnect() {
194
+ this.state = 'disconnected';
195
+ this.heartbeat.stop();
196
+ this.reconnection.stop();
197
+ if (this.stream) {
198
+ this.stream.close();
199
+ this.stream = null;
200
+ }
201
+ }
202
+ // ===========================================================================
203
+ // Private: Connection Setup
204
+ // ===========================================================================
205
+ guardNotAlreadyConnecting() {
206
+ if (this.state === 'connected' || this.state === 'connecting') {
207
+ throw new Error('Already connected or connecting');
208
+ }
209
+ }
210
+ createStream() {
211
+ const url = buildConnectionUrl(this.options.baseUrl, this.options.apiKey, this.options.runnerId, this.options.runnerName);
212
+ return new StreamInOut(url);
213
+ }
214
+ waitForReady() {
215
+ return new Promise((resolve, reject) => {
216
+ const timeout = setTimeout(() => {
217
+ reject(new Error('WebSocket connection timeout'));
218
+ }, 10000);
219
+ const cleanup = () => clearTimeout(timeout);
220
+ let isOpen = false;
221
+ this.stream.socket.on('open', () => {
222
+ isOpen = true;
223
+ this.logger.log('WebSocket opened, waiting for ready message...');
224
+ });
225
+ this.stream.socket.on('message', (data) => {
226
+ if (!isOpen)
227
+ return;
228
+ try {
229
+ const msg = this.messageParser.parse(data);
230
+ if (msg.type === 'ready') {
231
+ cleanup();
232
+ this.onReady();
233
+ resolve();
234
+ }
235
+ }
236
+ catch {
237
+ // Ignore parse errors for non-ready messages
238
+ }
239
+ });
240
+ this.stream.socket.on('error', (error) => {
241
+ cleanup();
242
+ this.state = 'disconnected';
243
+ reject(error);
244
+ });
245
+ this.stream.socket.on('close', () => {
246
+ if (this.state !== 'connected') {
247
+ cleanup();
248
+ this.state = 'disconnected';
249
+ reject(new Error('WebSocket closed before ready'));
250
+ }
251
+ });
252
+ });
253
+ }
254
+ onReady() {
255
+ this.state = 'connected';
256
+ this.reconnection.reset();
257
+ this.logger.log('Connected - received ready message');
258
+ this.heartbeat.start();
259
+ }
260
+ // ===========================================================================
261
+ // Private: Message Loop
262
+ // ===========================================================================
263
+ async startMessageLoop() {
264
+ if (!this.stream)
265
+ return;
266
+ try {
267
+ for await (const msg of this.stream) {
268
+ this.handleMessage(msg);
269
+ }
270
+ this.logger.log('Stream ended');
271
+ }
272
+ catch (error) {
273
+ this.logger.error('Stream error:', error);
274
+ }
275
+ await this.handleDisconnection();
276
+ }
277
+ handleMessage(msg) {
278
+ switch (msg.type) {
279
+ case 'heartbeat_ack':
280
+ this.logger.log('Heartbeat acknowledged');
281
+ break;
282
+ case 'ready':
283
+ // Already handled during connection setup, ignore
284
+ break;
285
+ case 'invoke':
286
+ if (msg.invocationId && msg.actionName) {
287
+ this.logger.log(`Received invocation: ${msg.actionName} (${msg.invocationId})`);
288
+ this.options.onInvoke(msg.invocationId, msg.actionName, msg.parameters || {}, msg.userContext);
289
+ }
290
+ break;
291
+ }
292
+ }
293
+ // ===========================================================================
294
+ // Private: Heartbeat
295
+ // ===========================================================================
296
+ async sendHeartbeat() {
297
+ if (this.stream && this.state === 'connected') {
298
+ await this.send({ type: 'heartbeat' });
299
+ }
300
+ }
301
+ handleHeartbeatFailure() {
302
+ this.state = 'disconnected';
303
+ this.options.onDisconnect?.();
304
+ }
305
+ // ===========================================================================
306
+ // Private: Reconnection
307
+ // ===========================================================================
308
+ async handleDisconnection() {
309
+ this.logger.log('Disconnected');
310
+ this.state = 'disconnected';
311
+ this.heartbeat.stop();
312
+ this.options.onDisconnect?.();
313
+ if (this.reconnection.canRetry()) {
314
+ this.logger.log('Will attempt to reconnect...');
315
+ await this.attemptReconnect();
316
+ }
317
+ }
318
+ async attemptReconnect() {
319
+ await this.reconnection.waitAndIncrement();
320
+ try {
321
+ this.state = 'connecting';
322
+ this.stream = this.createStream();
323
+ await this.waitForReady();
324
+ this.logger.log('Reconnected successfully');
325
+ this.startMessageLoop();
326
+ }
327
+ catch (error) {
328
+ this.state = 'disconnected';
329
+ if (this.reconnection.canRetry()) {
330
+ this.logger.error('Reconnection failed:', error);
331
+ await this.attemptReconnect();
332
+ }
333
+ }
334
+ }
335
+ }
336
+ //# sourceMappingURL=connection.js.map