@fatagnus/remote-cmd-relay-convex 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fatagnus/remote-cmd-relay-convex",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
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 |
@@ -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",
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";