@fatagnus/remote-cmd-relay-convex 1.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 ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@fatagnus/remote-cmd-relay-convex",
3
+ "version": "1.0.0",
4
+ "description": "Convex component for managing remote command relays - assignment management, command queuing, status tracking, and credential inventory",
5
+ "author": "Ozan Turksever <ozan.turksever@gmail.com>",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/fatagnus/remote-cmd-relay.git",
10
+ "directory": "packages/convex"
11
+ },
12
+ "type": "module",
13
+ "main": "./src/component.ts",
14
+ "files": [
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "vitest run"
20
+ },
21
+ "peerDependencies": {
22
+ "convex": "^1.17.0"
23
+ },
24
+ "devDependencies": {
25
+ "convex": "^1.29.3",
26
+ "convex-test": "^0.0.41",
27
+ "typescript": "^5.7.2",
28
+ "vitest": "^3.0.5"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "keywords": [
34
+ "convex",
35
+ "component",
36
+ "remote",
37
+ "command",
38
+ "relay",
39
+ "ssh"
40
+ ]
41
+ }
package/src/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # Remote Command Relay - Convex Component
2
+
3
+ A Convex component that provides the backend infrastructure for managing remote command relays, including assignment management, command queuing, status tracking, credential inventory, and configuration push.
4
+
5
+ ## Overview
6
+
7
+ This component enables remote command execution on machines in restricted network segments by:
8
+
9
+ 1. **Managing relay assignments** - Link API keys to machines
10
+ 2. **Queuing commands** - Store pending commands for relay execution
11
+ 3. **Tracking status** - Monitor relay health, capabilities, and metrics
12
+ 4. **Credential inventory** - Track what credentials each relay has (metadata only)
13
+ 5. **Configuration push** - Push config updates to relays
14
+
15
+ ## Installation
16
+
17
+ The component is registered in your app's `convex/convex.config.ts`:
18
+
19
+ ```typescript
20
+ import { defineApp } from "convex/server";
21
+ import remoteCmdRelay from "./remoteCmdRelay/convex.config";
22
+
23
+ const app = defineApp();
24
+ app.use(remoteCmdRelay);
25
+
26
+ export default app;
27
+ ```
28
+
29
+ ## Schema
30
+
31
+ ### Tables
32
+
33
+ #### `relayAssignments`
34
+
35
+ Links Better Auth API keys to machines.
36
+
37
+ | Field | Type | Description |
38
+ |-------|------|-------------|
39
+ | `apiKeyId` | string | Better Auth API key ID |
40
+ | `machineId` | string | Reference to machine in main app |
41
+ | `name` | string | Friendly name for the relay |
42
+ | `enabled` | boolean | Whether the relay is enabled |
43
+ | `lastSeenAt` | number? | Last heartbeat timestamp |
44
+ | `createdBy` | string | User who created the assignment |
45
+ | `createdAt` | number | Creation timestamp |
46
+ | `updatedAt` | number | Last update timestamp |
47
+
48
+ #### `relayStatus`
49
+
50
+ Tracks relay health, capabilities, and metrics.
51
+
52
+ | Field | Type | Description |
53
+ |-------|------|-------------|
54
+ | `relayId` | string | Reference to relayAssignments._id |
55
+ | `capabilities` | array | List of capabilities (ssh, local_cmd, perf_metrics) |
56
+ | `metrics` | object? | Performance metrics (CPU, memory, disk) |
57
+ | `version` | string? | Relay binary version |
58
+ | `hostname` | string? | Relay host machine name |
59
+ | `platform` | string? | OS platform |
60
+ | `lastHeartbeatAt` | number | Last heartbeat timestamp |
61
+
62
+ #### `relayCredentialInventory`
63
+
64
+ Tracks what credentials each relay reports it has (metadata only, not values).
65
+
66
+ | Field | Type | Description |
67
+ |-------|------|-------------|
68
+ | `relayId` | string | Reference to relayAssignments._id |
69
+ | `credentialName` | string | Name/identifier of the credential |
70
+ | `credentialType` | string | Type: ssh_key, password, api_key |
71
+ | `targetHost` | string? | What host this credential is for |
72
+ | `storageMode` | string | relay_only or shared |
73
+ | `lastUpdatedAt` | number | When credential was last updated on relay |
74
+ | `reportedAt` | number | When relay reported this credential |
75
+
76
+ #### `sharedCredentials`
77
+
78
+ Backup storage for shared mode credentials (encrypted).
79
+
80
+ | Field | Type | Description |
81
+ |-------|------|-------------|
82
+ | `name` | string | Credential name |
83
+ | `credentialType` | string | Type: ssh_key, password, api_key |
84
+ | `targetHost` | string? | What host this credential is for |
85
+ | `encryptedValue` | string | Encrypted credential value |
86
+ | `assignedRelays` | array | List of relay IDs this is assigned to |
87
+ | `createdBy` | string | User who created the credential |
88
+
89
+ #### `configPushQueue`
90
+
91
+ Queue for pushing configuration updates to relays.
92
+
93
+ | Field | Type | Description |
94
+ |-------|------|-------------|
95
+ | `relayId` | string | Target relay |
96
+ | `pushType` | string | Type: credential, ssh_targets, allowed_commands, metrics_interval |
97
+ | `payload` | string | JSON-encoded payload |
98
+ | `status` | string | Status: pending, sent, acked, failed |
99
+ | `createdBy` | string | User who created the push |
100
+ | `errorMessage` | string? | Error message if failed |
101
+
102
+ #### `commandQueue`
103
+
104
+ Commands waiting to be executed by relays.
105
+
106
+ | Field | Type | Description |
107
+ |-------|------|-------------|
108
+ | `machineId` | string | Target machine (relay assignment) |
109
+ | `targetType` | string | local or ssh |
110
+ | `targetHost` | string? | SSH target host |
111
+ | `targetPort` | number? | SSH target port |
112
+ | `targetUsername` | string? | SSH username |
113
+ | `command` | string | Command to execute |
114
+ | `timeoutMs` | number | Command timeout |
115
+ | `status` | string | pending, claimed, executing, completed, failed, timeout |
116
+ | `output` | string? | Command stdout |
117
+ | `stderr` | string? | Command stderr |
118
+ | `exitCode` | number? | Exit code |
119
+ | `error` | string? | Error message |
120
+ | `durationMs` | number? | Execution duration |
121
+
122
+ ## Functions
123
+
124
+ ### Assignments (`assignments.ts`)
125
+
126
+ ```typescript
127
+ // Create a new relay assignment
128
+ await ctx.runMutation(components.remoteCmdRelay.assignments.create, {
129
+ apiKeyId: "api_key_id",
130
+ machineId: "machine_id",
131
+ name: "Production Relay",
132
+ createdBy: "user_id",
133
+ });
134
+
135
+ // List all assignments
136
+ const assignments = await ctx.runQuery(
137
+ components.remoteCmdRelay.assignments.listAll,
138
+ {}
139
+ );
140
+
141
+ // Update assignment
142
+ await ctx.runMutation(components.remoteCmdRelay.assignments.update, {
143
+ id: assignmentId,
144
+ enabled: false,
145
+ });
146
+
147
+ // Delete assignment
148
+ await ctx.runMutation(components.remoteCmdRelay.assignments.remove, {
149
+ id: assignmentId,
150
+ });
151
+ ```
152
+
153
+ ### Commands (`commands.ts`)
154
+
155
+ ```typescript
156
+ // Queue a command
157
+ await ctx.runMutation(components.remoteCmdRelay.commands.queue, {
158
+ machineId: "machine_id",
159
+ command: "df -h",
160
+ targetType: "local",
161
+ timeoutMs: 30000,
162
+ createdBy: "user_id",
163
+ });
164
+
165
+ // Queue SSH command
166
+ await ctx.runMutation(components.remoteCmdRelay.commands.queue, {
167
+ machineId: "machine_id",
168
+ command: "systemctl status nginx",
169
+ targetType: "ssh",
170
+ targetHost: "192.168.1.100",
171
+ targetPort: 22,
172
+ targetUsername: "admin",
173
+ timeoutMs: 30000,
174
+ createdBy: "user_id",
175
+ });
176
+
177
+ // Get pending commands for a machine
178
+ const commands = await ctx.runQuery(
179
+ components.remoteCmdRelay.commands.getPending,
180
+ { machineId: "machine_id" }
181
+ );
182
+
183
+ // Claim a command (relay calls this)
184
+ await ctx.runMutation(components.remoteCmdRelay.commands.claim, {
185
+ commandId: "command_id",
186
+ claimedBy: "relay_id",
187
+ });
188
+
189
+ // Complete a command with results
190
+ await ctx.runMutation(components.remoteCmdRelay.commands.complete, {
191
+ commandId: "command_id",
192
+ success: true,
193
+ output: "Filesystem Size Used Avail Use% Mounted on\n...",
194
+ exitCode: 0,
195
+ durationMs: 150,
196
+ });
197
+ ```
198
+
199
+ ### Status (`status.ts`)
200
+
201
+ ```typescript
202
+ // Report relay status (relay calls this)
203
+ await ctx.runMutation(components.remoteCmdRelay.status.reportStatus, {
204
+ relayId: "relay_id",
205
+ capabilities: ["ssh", "local_cmd", "perf_metrics"],
206
+ metrics: {
207
+ cpuPercent: 25.5,
208
+ memoryPercent: 60.2,
209
+ diskPercent: 45.0,
210
+ },
211
+ version: "1.0.0",
212
+ hostname: "prod-server-01",
213
+ platform: "linux",
214
+ });
215
+
216
+ // Get status for a relay
217
+ const status = await ctx.runQuery(
218
+ components.remoteCmdRelay.status.getByRelayId,
219
+ { relayId: "relay_id" }
220
+ );
221
+
222
+ // List all relay statuses
223
+ const statuses = await ctx.runQuery(
224
+ components.remoteCmdRelay.status.listAll,
225
+ {}
226
+ );
227
+
228
+ // Find relays with specific capability
229
+ const sshRelays = await ctx.runQuery(
230
+ components.remoteCmdRelay.status.findByCapability,
231
+ { capability: "ssh" }
232
+ );
233
+ ```
234
+
235
+ ### Credentials (`credentials.ts`)
236
+
237
+ ```typescript
238
+ // Report credential inventory (relay calls this)
239
+ await ctx.runMutation(components.remoteCmdRelay.credentials.reportInventory, {
240
+ relayId: "relay_id",
241
+ credentials: [
242
+ {
243
+ credentialName: "prod-server-ssh",
244
+ credentialType: "ssh_key",
245
+ targetHost: "192.168.1.100",
246
+ storageMode: "relay_only",
247
+ lastUpdatedAt: Date.now(),
248
+ },
249
+ ],
250
+ });
251
+
252
+ // Get credential inventory for a relay
253
+ const inventory = await ctx.runQuery(
254
+ components.remoteCmdRelay.credentials.getInventoryByRelayId,
255
+ { relayId: "relay_id" }
256
+ );
257
+
258
+ // Find relays that have credentials for a target
259
+ const relays = await ctx.runQuery(
260
+ components.remoteCmdRelay.credentials.findRelaysForTarget,
261
+ { targetHost: "192.168.1.100" }
262
+ );
263
+
264
+ // Create shared credential (stored on center)
265
+ await ctx.runMutation(components.remoteCmdRelay.credentials.createSharedCredential, {
266
+ name: "shared-api-key",
267
+ credentialType: "api_key",
268
+ encryptedValue: "encrypted...",
269
+ assignedRelays: ["relay_id_1", "relay_id_2"],
270
+ createdBy: "user_id",
271
+ });
272
+ ```
273
+
274
+ ### Config Push (`configPush.ts`)
275
+
276
+ ```typescript
277
+ // Queue a config push
278
+ await ctx.runMutation(components.remoteCmdRelay.configPush.queuePush, {
279
+ relayId: "relay_id",
280
+ pushType: "credential",
281
+ payload: JSON.stringify({ name: "new-credential", ... }),
282
+ createdBy: "user_id",
283
+ });
284
+
285
+ // Get pending pushes for a relay
286
+ const pending = await ctx.runQuery(
287
+ components.remoteCmdRelay.configPush.getPendingForRelay,
288
+ { relayId: "relay_id" }
289
+ );
290
+
291
+ // Acknowledge a push
292
+ await ctx.runMutation(components.remoteCmdRelay.configPush.acknowledge, {
293
+ pushId: "push_id",
294
+ success: true,
295
+ });
296
+ ```
297
+
298
+ ### Public API (`public.ts`)
299
+
300
+ HTTP-accessible functions for relay communication:
301
+
302
+ ```typescript
303
+ // Verify relay API key
304
+ const result = await ctx.runQuery(
305
+ components.remoteCmdRelay.public.verifyRelay,
306
+ { apiKeyId: "api_key_id" }
307
+ );
308
+
309
+ // Get pending commands
310
+ const commands = await ctx.runQuery(
311
+ components.remoteCmdRelay.public.getPendingCommands,
312
+ { machineId: "machine_id" }
313
+ );
314
+
315
+ // Claim a command
316
+ await ctx.runMutation(components.remoteCmdRelay.public.claimCommand, {
317
+ commandId: "command_id",
318
+ assignmentId: "assignment_id",
319
+ });
320
+
321
+ // Submit command result
322
+ await ctx.runMutation(components.remoteCmdRelay.public.submitResult, {
323
+ commandId: "command_id",
324
+ success: true,
325
+ output: "...",
326
+ exitCode: 0,
327
+ });
328
+
329
+ // Send heartbeat
330
+ await ctx.runMutation(components.remoteCmdRelay.public.sendHeartbeat, {
331
+ apiKeyId: "api_key_id",
332
+ });
333
+ ```
334
+
335
+ ## HTTP Routes
336
+
337
+ The component exposes HTTP endpoints in `convex/http.ts`:
338
+
339
+ | Method | Path | Description |
340
+ |--------|------|-------------|
341
+ | POST | `/relay/verify` | Verify API key and get assignment |
342
+ | GET | `/relay/commands` | Get pending commands |
343
+ | POST | `/relay/commands/claim` | Claim a command |
344
+ | POST | `/relay/commands/result` | Submit command result |
345
+ | POST | `/relay/heartbeat` | Send heartbeat |
346
+ | POST | `/relay/status` | Report full status |
347
+
348
+ All endpoints require the `X-API-Key` header (except `/relay/verify` which takes it in the body).
349
+
350
+ ## Types
351
+
352
+ ### Capabilities
353
+
354
+ ```typescript
355
+ type Capability = "ssh" | "local_cmd" | "perf_metrics";
356
+ ```
357
+
358
+ ### Credential Types
359
+
360
+ ```typescript
361
+ type CredentialType = "ssh_key" | "password" | "api_key";
362
+ ```
363
+
364
+ ### Storage Modes
365
+
366
+ ```typescript
367
+ type StorageMode = "relay_only" | "shared";
368
+ ```
369
+
370
+ ### Command Status
371
+
372
+ ```typescript
373
+ type CommandStatus =
374
+ | "pending"
375
+ | "claimed"
376
+ | "executing"
377
+ | "completed"
378
+ | "failed"
379
+ | "timeout";
380
+ ```
381
+
382
+ ### Config Push Types
383
+
384
+ ```typescript
385
+ type ConfigPushType =
386
+ | "credential"
387
+ | "ssh_targets"
388
+ | "allowed_commands"
389
+ | "metrics_interval";
390
+ ```
391
+
392
+ ## Security Considerations
393
+
394
+ 1. **API Key Validation**: All relay communication is authenticated via Better Auth API keys
395
+ 2. **Credential Security**:
396
+ - `relay_only` credentials never leave the relay
397
+ - `shared` credentials are stored encrypted on center
398
+ - Credential inventory only reports metadata (names), not values
399
+ 3. **Admin Access**: All management functions require admin role
400
+ 4. **Command Routing**: Commands are only sent to relays assigned to the target machine
401
+
402
+ ## Files
403
+
404
+ | File | Description |
405
+ |------|-------------|
406
+ | `convex.config.ts` | Component definition |
407
+ | `schema.ts` | Database schema and validators |
408
+ | `assignments.ts` | Relay assignment CRUD |
409
+ | `commands.ts` | Command queue management |
410
+ | `status.ts` | Relay status and capability tracking |
411
+ | `credentials.ts` | Credential inventory and shared credentials |
412
+ | `configPush.ts` | Configuration push queue |
413
+ | `public.ts` | HTTP-accessible functions for relays |
@@ -0,0 +1,62 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type * as assignments from "../assignments.js";
12
+ import type * as commands from "../commands.js";
13
+ import type * as component from "../component.js";
14
+ import type * as configPush from "../configPush.js";
15
+ import type * as credentials from "../credentials.js";
16
+ import type * as public_ from "../public.js";
17
+ import type * as status from "../status.js";
18
+
19
+ import type {
20
+ ApiFromModules,
21
+ FilterApi,
22
+ FunctionReference,
23
+ } from "convex/server";
24
+ import { anyApi, componentsGeneric } from "convex/server";
25
+
26
+ const fullApi: ApiFromModules<{
27
+ assignments: typeof assignments;
28
+ commands: typeof commands;
29
+ component: typeof component;
30
+ configPush: typeof configPush;
31
+ credentials: typeof credentials;
32
+ public: typeof public_;
33
+ status: typeof status;
34
+ }> = anyApi as any;
35
+
36
+ /**
37
+ * A utility for referencing Convex functions in your app's public API.
38
+ *
39
+ * Usage:
40
+ * ```js
41
+ * const myFunctionReference = api.myModule.myFunction;
42
+ * ```
43
+ */
44
+ export const api: FilterApi<
45
+ typeof fullApi,
46
+ FunctionReference<any, "public">
47
+ > = anyApi as any;
48
+
49
+ /**
50
+ * A utility for referencing Convex functions in your app's internal API.
51
+ *
52
+ * Usage:
53
+ * ```js
54
+ * const myFunctionReference = internal.myModule.myFunction;
55
+ * ```
56
+ */
57
+ export const internal: FilterApi<
58
+ typeof fullApi,
59
+ FunctionReference<any, "internal">
60
+ > = anyApi as any;
61
+
62
+ export const components = componentsGeneric() as unknown as {};