@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 +41 -0
- package/src/README.md +413 -0
- package/src/_generated/api.ts +62 -0
- package/src/_generated/component.ts +565 -0
- package/src/_generated/dataModel.ts +60 -0
- package/src/_generated/server.ts +161 -0
- package/src/assignments.test.ts +233 -0
- package/src/assignments.ts +222 -0
- package/src/commands.test.ts +331 -0
- package/src/commands.ts +286 -0
- package/src/component.ts +11 -0
- package/src/configPush.ts +96 -0
- package/src/convex.config.ts +5 -0
- package/src/credentials.ts +112 -0
- package/src/public.test.ts +576 -0
- package/src/public.ts +436 -0
- package/src/schema.ts +180 -0
- package/src/status.test.ts +308 -0
- package/src/status.ts +172 -0
- package/src/test.helpers.ts +399 -0
- package/src/test.setup.ts +256 -0
package/src/public.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { query, mutation } from "./_generated/server";
|
|
3
|
+
import {
|
|
4
|
+
targetTypeValidator,
|
|
5
|
+
capabilityValidator,
|
|
6
|
+
metricsValidator,
|
|
7
|
+
credentialTypeValidator,
|
|
8
|
+
storageModeValidator,
|
|
9
|
+
} from "./schema";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Verify a relay's API key and return its assignment details
|
|
13
|
+
* This is called by relays on startup to verify their API key
|
|
14
|
+
*/
|
|
15
|
+
export const verifyRelay = query({
|
|
16
|
+
args: {
|
|
17
|
+
apiKeyId: v.string(),
|
|
18
|
+
},
|
|
19
|
+
returns: v.union(
|
|
20
|
+
v.object({
|
|
21
|
+
valid: v.literal(true),
|
|
22
|
+
assignmentId: v.string(),
|
|
23
|
+
machineId: v.string(),
|
|
24
|
+
name: v.string(),
|
|
25
|
+
}),
|
|
26
|
+
v.object({
|
|
27
|
+
valid: v.literal(false),
|
|
28
|
+
error: v.string(),
|
|
29
|
+
})
|
|
30
|
+
),
|
|
31
|
+
handler: async (ctx, args) => {
|
|
32
|
+
const assignment = await ctx.db
|
|
33
|
+
.query("relayAssignments")
|
|
34
|
+
.withIndex("by_apiKeyId", (q) => q.eq("apiKeyId", args.apiKeyId))
|
|
35
|
+
.first();
|
|
36
|
+
|
|
37
|
+
if (!assignment) {
|
|
38
|
+
return { valid: false as const, error: "No relay assignment found for this API key" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!assignment.enabled) {
|
|
42
|
+
return { valid: false as const, error: "Relay assignment is disabled" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
valid: true as const,
|
|
47
|
+
assignmentId: assignment._id,
|
|
48
|
+
machineId: assignment.machineId,
|
|
49
|
+
name: assignment.name,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get pending commands for a relay to execute
|
|
56
|
+
*/
|
|
57
|
+
export const getPendingCommands = query({
|
|
58
|
+
args: {
|
|
59
|
+
machineId: v.string(),
|
|
60
|
+
},
|
|
61
|
+
returns: v.array(
|
|
62
|
+
v.object({
|
|
63
|
+
_id: v.id("commandQueue"),
|
|
64
|
+
command: v.string(),
|
|
65
|
+
targetType: targetTypeValidator,
|
|
66
|
+
targetHost: v.optional(v.string()),
|
|
67
|
+
targetPort: v.optional(v.number()),
|
|
68
|
+
targetUsername: v.optional(v.string()),
|
|
69
|
+
timeoutMs: v.number(),
|
|
70
|
+
createdAt: v.number(),
|
|
71
|
+
})
|
|
72
|
+
),
|
|
73
|
+
handler: async (ctx, args) => {
|
|
74
|
+
const commands = await ctx.db
|
|
75
|
+
.query("commandQueue")
|
|
76
|
+
.withIndex("by_machineId_status", (q) =>
|
|
77
|
+
q.eq("machineId", args.machineId).eq("status", "pending")
|
|
78
|
+
)
|
|
79
|
+
.order("asc")
|
|
80
|
+
.take(10);
|
|
81
|
+
|
|
82
|
+
return commands.map((c) => ({
|
|
83
|
+
_id: c._id,
|
|
84
|
+
command: c.command,
|
|
85
|
+
targetType: c.targetType,
|
|
86
|
+
targetHost: c.targetHost,
|
|
87
|
+
targetPort: c.targetPort,
|
|
88
|
+
targetUsername: c.targetUsername,
|
|
89
|
+
timeoutMs: c.timeoutMs,
|
|
90
|
+
createdAt: c.createdAt,
|
|
91
|
+
}));
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Claim a command for execution (atomic operation)
|
|
97
|
+
*/
|
|
98
|
+
export const claimCommand = mutation({
|
|
99
|
+
args: {
|
|
100
|
+
commandId: v.id("commandQueue"),
|
|
101
|
+
assignmentId: v.string(),
|
|
102
|
+
},
|
|
103
|
+
returns: v.union(
|
|
104
|
+
v.object({
|
|
105
|
+
success: v.literal(true),
|
|
106
|
+
command: v.object({
|
|
107
|
+
_id: v.id("commandQueue"),
|
|
108
|
+
command: v.string(),
|
|
109
|
+
targetType: targetTypeValidator,
|
|
110
|
+
targetHost: v.optional(v.string()),
|
|
111
|
+
targetPort: v.optional(v.number()),
|
|
112
|
+
targetUsername: v.optional(v.string()),
|
|
113
|
+
timeoutMs: v.number(),
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
v.object({
|
|
117
|
+
success: v.literal(false),
|
|
118
|
+
error: v.string(),
|
|
119
|
+
})
|
|
120
|
+
),
|
|
121
|
+
handler: async (ctx, args) => {
|
|
122
|
+
const cmd = await ctx.db.get(args.commandId);
|
|
123
|
+
if (!cmd) {
|
|
124
|
+
return { success: false as const, error: "Command not found" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (cmd.status !== "pending") {
|
|
128
|
+
return { success: false as const, error: "Command is not pending" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await ctx.db.patch(args.commandId, {
|
|
132
|
+
status: "claimed",
|
|
133
|
+
claimedBy: args.assignmentId,
|
|
134
|
+
claimedAt: Date.now(),
|
|
135
|
+
updatedAt: Date.now(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
success: true as const,
|
|
140
|
+
command: {
|
|
141
|
+
_id: cmd._id,
|
|
142
|
+
command: cmd.command,
|
|
143
|
+
targetType: cmd.targetType,
|
|
144
|
+
targetHost: cmd.targetHost,
|
|
145
|
+
targetPort: cmd.targetPort,
|
|
146
|
+
targetUsername: cmd.targetUsername,
|
|
147
|
+
timeoutMs: cmd.timeoutMs,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Submit command execution results
|
|
155
|
+
*/
|
|
156
|
+
export const submitResult = mutation({
|
|
157
|
+
args: {
|
|
158
|
+
commandId: v.id("commandQueue"),
|
|
159
|
+
success: v.boolean(),
|
|
160
|
+
output: v.optional(v.string()),
|
|
161
|
+
stderr: v.optional(v.string()),
|
|
162
|
+
exitCode: v.optional(v.number()),
|
|
163
|
+
error: v.optional(v.string()),
|
|
164
|
+
durationMs: v.optional(v.number()),
|
|
165
|
+
},
|
|
166
|
+
returns: v.object({
|
|
167
|
+
success: v.boolean(),
|
|
168
|
+
}),
|
|
169
|
+
handler: async (ctx, args) => {
|
|
170
|
+
const cmd = await ctx.db.get(args.commandId);
|
|
171
|
+
if (!cmd) {
|
|
172
|
+
return { success: false };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
|
|
177
|
+
await ctx.db.patch(args.commandId, {
|
|
178
|
+
status: args.success ? "completed" : "failed",
|
|
179
|
+
output: args.output,
|
|
180
|
+
stderr: args.stderr,
|
|
181
|
+
exitCode: args.exitCode,
|
|
182
|
+
error: args.error,
|
|
183
|
+
durationMs: args.durationMs,
|
|
184
|
+
completedAt: now,
|
|
185
|
+
updatedAt: now,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return { success: true };
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Send heartbeat from relay to update last seen
|
|
194
|
+
*/
|
|
195
|
+
export const sendHeartbeat = mutation({
|
|
196
|
+
args: {
|
|
197
|
+
apiKeyId: v.string(),
|
|
198
|
+
},
|
|
199
|
+
returns: v.object({
|
|
200
|
+
success: v.boolean(),
|
|
201
|
+
}),
|
|
202
|
+
handler: async (ctx, args) => {
|
|
203
|
+
const assignment = await ctx.db
|
|
204
|
+
.query("relayAssignments")
|
|
205
|
+
.withIndex("by_apiKeyId", (q) => q.eq("apiKeyId", args.apiKeyId))
|
|
206
|
+
.first();
|
|
207
|
+
|
|
208
|
+
if (!assignment) {
|
|
209
|
+
return { success: false };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await ctx.db.patch(assignment._id, {
|
|
213
|
+
lastSeenAt: Date.now(),
|
|
214
|
+
updatedAt: Date.now(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return { success: true };
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ===== Status and Capability Reporting =====
|
|
222
|
+
|
|
223
|
+
// Credential inventory item for reporting
|
|
224
|
+
const credentialInventoryItemValidator = v.object({
|
|
225
|
+
credentialName: v.string(),
|
|
226
|
+
credentialType: credentialTypeValidator,
|
|
227
|
+
targetHost: v.optional(v.string()),
|
|
228
|
+
storageMode: storageModeValidator,
|
|
229
|
+
lastUpdatedAt: v.number(),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Full status report from relay (called on startup and periodically)
|
|
234
|
+
* Includes capabilities, metrics, and credential inventory
|
|
235
|
+
*/
|
|
236
|
+
export const reportFullStatus = mutation({
|
|
237
|
+
args: {
|
|
238
|
+
relayId: v.string(),
|
|
239
|
+
capabilities: v.array(capabilityValidator),
|
|
240
|
+
metrics: v.optional(metricsValidator),
|
|
241
|
+
version: v.optional(v.string()),
|
|
242
|
+
hostname: v.optional(v.string()),
|
|
243
|
+
platform: v.optional(v.string()),
|
|
244
|
+
credentials: v.array(credentialInventoryItemValidator),
|
|
245
|
+
},
|
|
246
|
+
returns: v.object({
|
|
247
|
+
success: v.boolean(),
|
|
248
|
+
pendingConfigPushes: v.number(),
|
|
249
|
+
sharedCredentialsCount: v.number(),
|
|
250
|
+
}),
|
|
251
|
+
handler: async (ctx, args) => {
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
|
|
254
|
+
// Update relay status
|
|
255
|
+
const existingStatus = await ctx.db
|
|
256
|
+
.query("relayStatus")
|
|
257
|
+
.withIndex("by_relayId", (q) => q.eq("relayId", args.relayId))
|
|
258
|
+
.first();
|
|
259
|
+
|
|
260
|
+
if (existingStatus) {
|
|
261
|
+
await ctx.db.patch(existingStatus._id, {
|
|
262
|
+
capabilities: args.capabilities,
|
|
263
|
+
metrics: args.metrics,
|
|
264
|
+
version: args.version,
|
|
265
|
+
hostname: args.hostname,
|
|
266
|
+
platform: args.platform,
|
|
267
|
+
lastHeartbeatAt: now,
|
|
268
|
+
updatedAt: now,
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
await ctx.db.insert("relayStatus", {
|
|
272
|
+
relayId: args.relayId,
|
|
273
|
+
capabilities: args.capabilities,
|
|
274
|
+
metrics: args.metrics,
|
|
275
|
+
version: args.version,
|
|
276
|
+
hostname: args.hostname,
|
|
277
|
+
platform: args.platform,
|
|
278
|
+
lastHeartbeatAt: now,
|
|
279
|
+
createdAt: now,
|
|
280
|
+
updatedAt: now,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Sync credential inventory
|
|
285
|
+
const existingCreds = await ctx.db
|
|
286
|
+
.query("relayCredentialInventory")
|
|
287
|
+
.withIndex("by_relayId", (q) => q.eq("relayId", args.relayId))
|
|
288
|
+
.collect();
|
|
289
|
+
|
|
290
|
+
const existingCredsMap = new Map(existingCreds.map((c) => [c.credentialName, c]));
|
|
291
|
+
const reportedNames = new Set(args.credentials.map((c) => c.credentialName));
|
|
292
|
+
|
|
293
|
+
// Delete credentials no longer reported
|
|
294
|
+
for (const cred of existingCreds) {
|
|
295
|
+
if (!reportedNames.has(cred.credentialName)) {
|
|
296
|
+
await ctx.db.delete(cred._id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Update or insert reported credentials
|
|
301
|
+
for (const cred of args.credentials) {
|
|
302
|
+
const existingCred = existingCredsMap.get(cred.credentialName);
|
|
303
|
+
|
|
304
|
+
if (existingCred) {
|
|
305
|
+
await ctx.db.patch(existingCred._id, {
|
|
306
|
+
credentialType: cred.credentialType,
|
|
307
|
+
targetHost: cred.targetHost,
|
|
308
|
+
storageMode: cred.storageMode,
|
|
309
|
+
lastUpdatedAt: cred.lastUpdatedAt,
|
|
310
|
+
reportedAt: now,
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
await ctx.db.insert("relayCredentialInventory", {
|
|
314
|
+
relayId: args.relayId,
|
|
315
|
+
credentialName: cred.credentialName,
|
|
316
|
+
credentialType: cred.credentialType,
|
|
317
|
+
targetHost: cred.targetHost,
|
|
318
|
+
storageMode: cred.storageMode,
|
|
319
|
+
lastUpdatedAt: cred.lastUpdatedAt,
|
|
320
|
+
reportedAt: now,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Count pending config pushes for this relay
|
|
326
|
+
const pendingPushes = await ctx.db
|
|
327
|
+
.query("configPushQueue")
|
|
328
|
+
.withIndex("by_relayId_status", (q) =>
|
|
329
|
+
q.eq("relayId", args.relayId).eq("status", "pending")
|
|
330
|
+
)
|
|
331
|
+
.collect();
|
|
332
|
+
|
|
333
|
+
// Count shared credentials assigned to this relay
|
|
334
|
+
const sharedCreds = await ctx.db.query("sharedCredentials").collect();
|
|
335
|
+
const assignedSharedCreds = sharedCreds.filter((c) =>
|
|
336
|
+
c.assignedRelays.includes(args.relayId)
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
success: true,
|
|
341
|
+
pendingConfigPushes: pendingPushes.length,
|
|
342
|
+
sharedCredentialsCount: assignedSharedCreds.length,
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get pending config pushes for relay
|
|
349
|
+
*/
|
|
350
|
+
export const getPendingConfigPushes = query({
|
|
351
|
+
args: {
|
|
352
|
+
relayId: v.string(),
|
|
353
|
+
},
|
|
354
|
+
returns: v.array(
|
|
355
|
+
v.object({
|
|
356
|
+
_id: v.id("configPushQueue"),
|
|
357
|
+
pushType: v.string(),
|
|
358
|
+
payload: v.string(),
|
|
359
|
+
createdAt: v.number(),
|
|
360
|
+
})
|
|
361
|
+
),
|
|
362
|
+
handler: async (ctx, args) => {
|
|
363
|
+
const pushes = await ctx.db
|
|
364
|
+
.query("configPushQueue")
|
|
365
|
+
.withIndex("by_relayId_status", (q) =>
|
|
366
|
+
q.eq("relayId", args.relayId).eq("status", "pending")
|
|
367
|
+
)
|
|
368
|
+
.collect();
|
|
369
|
+
|
|
370
|
+
return pushes.map((p) => ({
|
|
371
|
+
_id: p._id,
|
|
372
|
+
pushType: p.pushType,
|
|
373
|
+
payload: p.payload,
|
|
374
|
+
createdAt: p.createdAt,
|
|
375
|
+
}));
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Acknowledge a config push
|
|
381
|
+
*/
|
|
382
|
+
export const acknowledgeConfigPush = mutation({
|
|
383
|
+
args: {
|
|
384
|
+
pushId: v.id("configPushQueue"),
|
|
385
|
+
success: v.boolean(),
|
|
386
|
+
errorMessage: v.optional(v.string()),
|
|
387
|
+
},
|
|
388
|
+
returns: v.object({
|
|
389
|
+
success: v.boolean(),
|
|
390
|
+
}),
|
|
391
|
+
handler: async (ctx, args) => {
|
|
392
|
+
const push = await ctx.db.get(args.pushId);
|
|
393
|
+
if (!push) {
|
|
394
|
+
return { success: false };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
await ctx.db.patch(args.pushId, {
|
|
398
|
+
status: args.success ? "acked" : "failed",
|
|
399
|
+
ackedAt: Date.now(),
|
|
400
|
+
errorMessage: args.errorMessage,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return { success: true };
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get shared credentials assigned to this relay
|
|
409
|
+
*/
|
|
410
|
+
export const getSharedCredentials = query({
|
|
411
|
+
args: {
|
|
412
|
+
relayId: v.string(),
|
|
413
|
+
},
|
|
414
|
+
returns: v.array(
|
|
415
|
+
v.object({
|
|
416
|
+
name: v.string(),
|
|
417
|
+
credentialType: credentialTypeValidator,
|
|
418
|
+
targetHost: v.optional(v.string()),
|
|
419
|
+
encryptedValue: v.string(),
|
|
420
|
+
updatedAt: v.number(),
|
|
421
|
+
})
|
|
422
|
+
),
|
|
423
|
+
handler: async (ctx, args) => {
|
|
424
|
+
const creds = await ctx.db.query("sharedCredentials").collect();
|
|
425
|
+
|
|
426
|
+
return creds
|
|
427
|
+
.filter((c) => c.assignedRelays.includes(args.relayId))
|
|
428
|
+
.map((c) => ({
|
|
429
|
+
name: c.name,
|
|
430
|
+
credentialType: c.credentialType,
|
|
431
|
+
targetHost: c.targetHost,
|
|
432
|
+
encryptedValue: c.encryptedValue,
|
|
433
|
+
updatedAt: c.updatedAt,
|
|
434
|
+
}));
|
|
435
|
+
},
|
|
436
|
+
});
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
// Command status
|
|
5
|
+
export const commandStatusValidator = v.union(
|
|
6
|
+
v.literal("pending"),
|
|
7
|
+
v.literal("claimed"),
|
|
8
|
+
v.literal("executing"),
|
|
9
|
+
v.literal("completed"),
|
|
10
|
+
v.literal("failed"),
|
|
11
|
+
v.literal("timeout")
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Target type for command execution
|
|
15
|
+
export const targetTypeValidator = v.union(
|
|
16
|
+
v.literal("local"),
|
|
17
|
+
v.literal("ssh")
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Relay capabilities
|
|
21
|
+
export const capabilityValidator = v.union(
|
|
22
|
+
v.literal("ssh"),
|
|
23
|
+
v.literal("local_cmd"),
|
|
24
|
+
v.literal("perf_metrics")
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Credential types
|
|
28
|
+
export const credentialTypeValidator = v.union(
|
|
29
|
+
v.literal("ssh_key"),
|
|
30
|
+
v.literal("password"),
|
|
31
|
+
v.literal("api_key")
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Credential storage mode
|
|
35
|
+
export const storageModeValidator = v.union(
|
|
36
|
+
v.literal("relay_only"),
|
|
37
|
+
v.literal("shared")
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Config push types
|
|
41
|
+
export const configPushTypeValidator = v.union(
|
|
42
|
+
v.literal("credential"),
|
|
43
|
+
v.literal("ssh_targets"),
|
|
44
|
+
v.literal("allowed_commands"),
|
|
45
|
+
v.literal("metrics_interval")
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Config push status
|
|
49
|
+
export const configPushStatusValidator = v.union(
|
|
50
|
+
v.literal("pending"),
|
|
51
|
+
v.literal("sent"),
|
|
52
|
+
v.literal("acked"),
|
|
53
|
+
v.literal("failed")
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Performance metrics object
|
|
57
|
+
export const metricsValidator = v.object({
|
|
58
|
+
cpuPercent: v.optional(v.number()),
|
|
59
|
+
memoryPercent: v.optional(v.number()),
|
|
60
|
+
memoryUsedMb: v.optional(v.number()),
|
|
61
|
+
memoryTotalMb: v.optional(v.number()),
|
|
62
|
+
diskPercent: v.optional(v.number()),
|
|
63
|
+
diskUsedGb: v.optional(v.number()),
|
|
64
|
+
diskTotalGb: v.optional(v.number()),
|
|
65
|
+
loadAvg1m: v.optional(v.number()),
|
|
66
|
+
loadAvg5m: v.optional(v.number()),
|
|
67
|
+
loadAvg15m: v.optional(v.number()),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const tables = {
|
|
71
|
+
// Relay assignments - links API keys to machines
|
|
72
|
+
relayAssignments: defineTable({
|
|
73
|
+
apiKeyId: v.string(), // Better Auth API key ID
|
|
74
|
+
machineId: v.string(), // Reference to machine in main app
|
|
75
|
+
name: v.string(), // Friendly name for the relay
|
|
76
|
+
enabled: v.boolean(),
|
|
77
|
+
lastSeenAt: v.optional(v.number()),
|
|
78
|
+
createdBy: v.string(),
|
|
79
|
+
createdAt: v.number(),
|
|
80
|
+
updatedAt: v.number(),
|
|
81
|
+
})
|
|
82
|
+
.index("by_apiKeyId", ["apiKeyId"])
|
|
83
|
+
.index("by_machineId", ["machineId"])
|
|
84
|
+
.index("by_enabled", ["enabled"]),
|
|
85
|
+
|
|
86
|
+
// Relay status - capabilities, metrics, heartbeat
|
|
87
|
+
relayStatus: defineTable({
|
|
88
|
+
relayId: v.string(), // Reference to relayAssignments._id
|
|
89
|
+
capabilities: v.array(capabilityValidator),
|
|
90
|
+
metrics: v.optional(metricsValidator),
|
|
91
|
+
version: v.optional(v.string()), // Relay binary version
|
|
92
|
+
hostname: v.optional(v.string()), // Relay host machine name
|
|
93
|
+
platform: v.optional(v.string()), // OS platform
|
|
94
|
+
lastHeartbeatAt: v.number(),
|
|
95
|
+
createdAt: v.number(),
|
|
96
|
+
updatedAt: v.number(),
|
|
97
|
+
})
|
|
98
|
+
.index("by_relayId", ["relayId"])
|
|
99
|
+
.index("by_lastHeartbeat", ["lastHeartbeatAt"]),
|
|
100
|
+
|
|
101
|
+
// Relay credential inventory - what credentials each relay reports it has
|
|
102
|
+
relayCredentialInventory: defineTable({
|
|
103
|
+
relayId: v.string(), // Reference to relayAssignments._id
|
|
104
|
+
credentialName: v.string(), // Name/identifier of the credential
|
|
105
|
+
credentialType: credentialTypeValidator,
|
|
106
|
+
targetHost: v.optional(v.string()), // What host/machine this credential is for
|
|
107
|
+
storageMode: storageModeValidator, // relay_only or shared
|
|
108
|
+
lastUpdatedAt: v.number(), // When the credential was last updated on relay
|
|
109
|
+
reportedAt: v.number(), // When relay reported this credential
|
|
110
|
+
})
|
|
111
|
+
.index("by_relayId", ["relayId"])
|
|
112
|
+
.index("by_relayId_name", ["relayId", "credentialName"])
|
|
113
|
+
.index("by_targetHost", ["targetHost"]),
|
|
114
|
+
|
|
115
|
+
// Shared credentials - backup copies for shared mode creds (encrypted)
|
|
116
|
+
sharedCredentials: defineTable({
|
|
117
|
+
name: v.string(), // Credential name
|
|
118
|
+
credentialType: credentialTypeValidator,
|
|
119
|
+
targetHost: v.optional(v.string()), // What host/machine this credential is for
|
|
120
|
+
encryptedValue: v.string(), // Encrypted credential value
|
|
121
|
+
assignedRelays: v.array(v.string()), // List of relay IDs this is assigned to
|
|
122
|
+
createdBy: v.string(),
|
|
123
|
+
createdAt: v.number(),
|
|
124
|
+
updatedAt: v.number(),
|
|
125
|
+
})
|
|
126
|
+
.index("by_name", ["name"])
|
|
127
|
+
.index("by_targetHost", ["targetHost"]),
|
|
128
|
+
|
|
129
|
+
// Config push queue - pending configuration pushes to relays
|
|
130
|
+
configPushQueue: defineTable({
|
|
131
|
+
relayId: v.string(), // Target relay
|
|
132
|
+
pushType: configPushTypeValidator,
|
|
133
|
+
payload: v.string(), // JSON-encoded payload
|
|
134
|
+
status: configPushStatusValidator,
|
|
135
|
+
createdBy: v.string(),
|
|
136
|
+
createdAt: v.number(),
|
|
137
|
+
sentAt: v.optional(v.number()),
|
|
138
|
+
ackedAt: v.optional(v.number()),
|
|
139
|
+
errorMessage: v.optional(v.string()),
|
|
140
|
+
})
|
|
141
|
+
.index("by_relayId", ["relayId"])
|
|
142
|
+
.index("by_relayId_status", ["relayId", "status"])
|
|
143
|
+
.index("by_status", ["status"]),
|
|
144
|
+
|
|
145
|
+
// Command queue - commands waiting to be executed by relays
|
|
146
|
+
commandQueue: defineTable({
|
|
147
|
+
machineId: v.string(), // Target machine (relay assignment)
|
|
148
|
+
targetType: targetTypeValidator, // local or ssh
|
|
149
|
+
// SSH target details (only if targetType is "ssh")
|
|
150
|
+
targetHost: v.optional(v.string()),
|
|
151
|
+
targetPort: v.optional(v.number()),
|
|
152
|
+
targetUsername: v.optional(v.string()),
|
|
153
|
+
// Command details
|
|
154
|
+
command: v.string(),
|
|
155
|
+
timeoutMs: v.number(), // Command timeout
|
|
156
|
+
// Status tracking
|
|
157
|
+
status: commandStatusValidator,
|
|
158
|
+
claimedBy: v.optional(v.string()), // Relay assignment ID that claimed
|
|
159
|
+
claimedAt: v.optional(v.number()),
|
|
160
|
+
// Results
|
|
161
|
+
output: v.optional(v.string()),
|
|
162
|
+
stderr: v.optional(v.string()),
|
|
163
|
+
exitCode: v.optional(v.number()),
|
|
164
|
+
error: v.optional(v.string()),
|
|
165
|
+
durationMs: v.optional(v.number()),
|
|
166
|
+
completedAt: v.optional(v.number()),
|
|
167
|
+
// Metadata
|
|
168
|
+
createdBy: v.string(),
|
|
169
|
+
createdAt: v.number(),
|
|
170
|
+
updatedAt: v.number(),
|
|
171
|
+
})
|
|
172
|
+
.index("by_machineId", ["machineId"])
|
|
173
|
+
.index("by_status", ["status"])
|
|
174
|
+
.index("by_machineId_status", ["machineId", "status"])
|
|
175
|
+
.index("by_createdAt", ["createdAt"]),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const schema = defineSchema(tables);
|
|
179
|
+
|
|
180
|
+
export default schema;
|