@fatagnus/remote-cmd-relay-convex 1.0.0 → 2.0.1
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/package.json +1 -1
- package/src/README.md +169 -0
- package/src/_generated/api.ts +4 -0
- package/src/_generated/component.ts +40 -0
- package/src/_generated/dataModel.ts +1 -1
- package/src/component.ts +5 -0
- package/src/execHelper.test.ts +815 -0
- package/src/execHelper.ts +359 -0
- package/src/rpc.test.ts +495 -0
- package/src/rpc.ts +97 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fatagnus/remote-cmd-relay-convex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Convex component for managing remote command relays - assignment management, command queuing, status tracking, and credential inventory",
|
|
5
5
|
"author": "Ozan Turksever <ozan.turksever@gmail.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
package/src/README.md
CHANGED
|
@@ -295,6 +295,158 @@ await ctx.runMutation(components.remoteCmdRelay.configPush.acknowledge, {
|
|
|
295
295
|
});
|
|
296
296
|
```
|
|
297
297
|
|
|
298
|
+
### RPC (`rpc.ts`)
|
|
299
|
+
|
|
300
|
+
Synchronous command execution API for calling relay functions from Convex actions:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Queue a command and get command ID for polling
|
|
304
|
+
const result = await ctx.runMutation(
|
|
305
|
+
components.remoteCmdRelay.rpc.queueRpcCommand,
|
|
306
|
+
{
|
|
307
|
+
machineId: "machine_id",
|
|
308
|
+
command: "df -h",
|
|
309
|
+
targetType: "local",
|
|
310
|
+
timeoutMs: 30000,
|
|
311
|
+
createdBy: "user_id",
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
// result: { success: true, commandId: "cmd_xxx" }
|
|
315
|
+
|
|
316
|
+
// Poll for command result
|
|
317
|
+
const status = await ctx.runQuery(
|
|
318
|
+
components.remoteCmdRelay.rpc.getCommandResult,
|
|
319
|
+
{ commandId: result.commandId }
|
|
320
|
+
);
|
|
321
|
+
// status: { found: true, status: "completed", output: "...", exitCode: 0 }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
#### Using the `exec` Helper (Recommended)
|
|
325
|
+
|
|
326
|
+
For a simpler synchronous RPC-like interface, use the `exec` helper in your Convex actions:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { exec } from "@fatagnus/remote-cmd-relay-convex";
|
|
330
|
+
import { components } from "./_generated/api";
|
|
331
|
+
import { action } from "./_generated/server";
|
|
332
|
+
|
|
333
|
+
export const runCommand = action({
|
|
334
|
+
args: { machineId: v.string(), command: v.string() },
|
|
335
|
+
handler: async (ctx, args) => {
|
|
336
|
+
const result = await exec(ctx, components.remoteCmdRelay.rpc, {
|
|
337
|
+
machineId: args.machineId,
|
|
338
|
+
command: args.command,
|
|
339
|
+
targetType: "local",
|
|
340
|
+
createdBy: "system",
|
|
341
|
+
timeoutMs: 30000,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (result.success) {
|
|
345
|
+
return { output: result.output };
|
|
346
|
+
} else {
|
|
347
|
+
throw new Error(result.error);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The `exec` helper:
|
|
354
|
+
- Queues the command
|
|
355
|
+
- Polls until completion or timeout
|
|
356
|
+
- Returns the full result with output, stderr, exit code, and duration
|
|
357
|
+
- Supports automatic retries for transient failures
|
|
358
|
+
|
|
359
|
+
#### Retry Configuration
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
const result = await exec(ctx, components.remoteCmdRelay.rpc, {
|
|
363
|
+
machineId: "my-machine",
|
|
364
|
+
command: "curl https://api.example.com/data",
|
|
365
|
+
targetType: "local",
|
|
366
|
+
createdBy: "system",
|
|
367
|
+
// Retry options
|
|
368
|
+
retries: 3, // Retry up to 3 times on transient failures
|
|
369
|
+
retryDelayMs: 1000, // Wait 1 second between retries
|
|
370
|
+
// Optional: custom retry logic
|
|
371
|
+
shouldRetry: (error, attempt) => {
|
|
372
|
+
return error.message.includes("network") && attempt < 3;
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
console.log(`Completed in ${result.attempts} attempt(s)`);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Transient failures that are automatically retried:
|
|
380
|
+
- Network errors (connection reset, refused)
|
|
381
|
+
- Timeout errors
|
|
382
|
+
- Rate limiting (429, 503)
|
|
383
|
+
- Temporary unavailability
|
|
384
|
+
|
|
385
|
+
#### Using `execAsync` for Fire-and-Forget
|
|
386
|
+
|
|
387
|
+
For long-running commands where you don't want to wait:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { execAsync } from "@fatagnus/remote-cmd-relay-convex";
|
|
391
|
+
|
|
392
|
+
export const startBackup = action({
|
|
393
|
+
handler: async (ctx) => {
|
|
394
|
+
const { commandId } = await execAsync(ctx, components.remoteCmdRelay.rpc, {
|
|
395
|
+
machineId: "backup-server",
|
|
396
|
+
command: "./run-backup.sh",
|
|
397
|
+
targetType: "local",
|
|
398
|
+
createdBy: "system",
|
|
399
|
+
timeoutMs: 3600000, // 1 hour
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Return immediately, check status later
|
|
403
|
+
return { commandId };
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### SSH Commands via RPC
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
const result = await exec(ctx, components.remoteCmdRelay.rpc, {
|
|
412
|
+
machineId: "relay-machine",
|
|
413
|
+
command: "systemctl status nginx",
|
|
414
|
+
targetType: "ssh",
|
|
415
|
+
targetHost: "192.168.1.100",
|
|
416
|
+
targetPort: 22,
|
|
417
|
+
targetUsername: "admin",
|
|
418
|
+
createdBy: "user_id",
|
|
419
|
+
timeoutMs: 30000,
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
#### RPC Response Types
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// ExecResult from exec()
|
|
427
|
+
interface ExecResult {
|
|
428
|
+
success: boolean; // true if exitCode === 0
|
|
429
|
+
output?: string; // stdout
|
|
430
|
+
stderr?: string; // stderr
|
|
431
|
+
exitCode?: number; // process exit code
|
|
432
|
+
error?: string; // error message if failed
|
|
433
|
+
durationMs?: number; // execution duration
|
|
434
|
+
timedOut?: boolean; // true if timed out
|
|
435
|
+
attempts?: number; // number of attempts made
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// getCommandResult response
|
|
439
|
+
interface CommandResult {
|
|
440
|
+
found: boolean;
|
|
441
|
+
status: "pending" | "claimed" | "executing" | "completed" | "failed" | "timeout";
|
|
442
|
+
output?: string;
|
|
443
|
+
stderr?: string;
|
|
444
|
+
exitCode?: number;
|
|
445
|
+
error?: string;
|
|
446
|
+
durationMs?: number;
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
298
450
|
### Public API (`public.ts`)
|
|
299
451
|
|
|
300
452
|
HTTP-accessible functions for relay communication:
|
|
@@ -389,6 +541,21 @@ type ConfigPushType =
|
|
|
389
541
|
| "metrics_interval";
|
|
390
542
|
```
|
|
391
543
|
|
|
544
|
+
## RPC Mode Setup
|
|
545
|
+
|
|
546
|
+
To enable fast RPC command execution, run the relay in **subscription mode**:
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
# Standard polling mode (5 second latency)
|
|
550
|
+
remote-cmd-relay API_KEY https://app.convex.site
|
|
551
|
+
|
|
552
|
+
# Subscription mode (sub-second latency for RPC)
|
|
553
|
+
remote-cmd-relay API_KEY https://app.convex.site \
|
|
554
|
+
--deployment-url https://app.convex.cloud
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
In subscription mode, the relay uses Convex WebSocket subscriptions to receive commands instantly instead of polling.
|
|
558
|
+
|
|
392
559
|
## Security Considerations
|
|
393
560
|
|
|
394
561
|
1. **API Key Validation**: All relay communication is authenticated via Better Auth API keys
|
|
@@ -411,3 +578,5 @@ type ConfigPushType =
|
|
|
411
578
|
| `credentials.ts` | Credential inventory and shared credentials |
|
|
412
579
|
| `configPush.ts` | Configuration push queue |
|
|
413
580
|
| `public.ts` | HTTP-accessible functions for relays |
|
|
581
|
+
| `rpc.ts` | RPC interface for synchronous command execution |
|
|
582
|
+
| `execHelper.ts` | Helper functions (`exec`, `execAsync`) for actions |
|
package/src/_generated/api.ts
CHANGED
|
@@ -13,7 +13,9 @@ import type * as commands from "../commands.js";
|
|
|
13
13
|
import type * as component from "../component.js";
|
|
14
14
|
import type * as configPush from "../configPush.js";
|
|
15
15
|
import type * as credentials from "../credentials.js";
|
|
16
|
+
import type * as execHelper from "../execHelper.js";
|
|
16
17
|
import type * as public_ from "../public.js";
|
|
18
|
+
import type * as rpc from "../rpc.js";
|
|
17
19
|
import type * as status from "../status.js";
|
|
18
20
|
|
|
19
21
|
import type {
|
|
@@ -29,7 +31,9 @@ const fullApi: ApiFromModules<{
|
|
|
29
31
|
component: typeof component;
|
|
30
32
|
configPush: typeof configPush;
|
|
31
33
|
credentials: typeof credentials;
|
|
34
|
+
execHelper: typeof execHelper;
|
|
32
35
|
public: typeof public_;
|
|
36
|
+
rpc: typeof rpc;
|
|
33
37
|
status: typeof status;
|
|
34
38
|
}> = anyApi as any;
|
|
35
39
|
|
|
@@ -466,6 +466,46 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
466
466
|
Name
|
|
467
467
|
>;
|
|
468
468
|
};
|
|
469
|
+
rpc: {
|
|
470
|
+
getCommandResult: FunctionReference<
|
|
471
|
+
"query",
|
|
472
|
+
"internal",
|
|
473
|
+
{ commandId: string },
|
|
474
|
+
| {
|
|
475
|
+
durationMs?: number;
|
|
476
|
+
error?: string;
|
|
477
|
+
exitCode?: number;
|
|
478
|
+
found: true;
|
|
479
|
+
output?: string;
|
|
480
|
+
status:
|
|
481
|
+
| "pending"
|
|
482
|
+
| "claimed"
|
|
483
|
+
| "executing"
|
|
484
|
+
| "completed"
|
|
485
|
+
| "failed"
|
|
486
|
+
| "timeout";
|
|
487
|
+
stderr?: string;
|
|
488
|
+
}
|
|
489
|
+
| { found: false },
|
|
490
|
+
Name
|
|
491
|
+
>;
|
|
492
|
+
queueRpcCommand: FunctionReference<
|
|
493
|
+
"mutation",
|
|
494
|
+
"internal",
|
|
495
|
+
{
|
|
496
|
+
command: string;
|
|
497
|
+
createdBy: string;
|
|
498
|
+
machineId: string;
|
|
499
|
+
targetHost?: string;
|
|
500
|
+
targetPort?: number;
|
|
501
|
+
targetType: "local" | "ssh";
|
|
502
|
+
targetUsername?: string;
|
|
503
|
+
timeoutMs?: number;
|
|
504
|
+
},
|
|
505
|
+
{ commandId?: string; error?: string; success: boolean },
|
|
506
|
+
Name
|
|
507
|
+
>;
|
|
508
|
+
};
|
|
469
509
|
status: {
|
|
470
510
|
findByCapability: FunctionReference<
|
|
471
511
|
"query",
|
|
@@ -38,7 +38,7 @@ export type Doc<TableName extends TableNames> = DocumentByName<
|
|
|
38
38
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
|
39
39
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
|
40
40
|
*
|
|
41
|
-
* Documents can be loaded using `db.get(
|
|
41
|
+
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
|
42
42
|
*
|
|
43
43
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
|
44
44
|
* strings when type checking.
|
package/src/component.ts
CHANGED
|
@@ -9,3 +9,8 @@ export * as status from "./status.js";
|
|
|
9
9
|
export * as credentials from "./credentials.js";
|
|
10
10
|
export * as configPush from "./configPush.js";
|
|
11
11
|
export * as publicApi from "./public.js";
|
|
12
|
+
export * as rpc from "./rpc.js";
|
|
13
|
+
|
|
14
|
+
// Export RPC helper functions for use in actions
|
|
15
|
+
export { exec, execAsync, isTransientError } from "./execHelper.js";
|
|
16
|
+
export type { ExecOptions, ExecResult, RelayRpcApi } from "./execHelper.js";
|