agent-relay-server 0.4.22 → 0.4.23
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/public/dashboard.js +730 -9
- package/public/index.html +678 -5
- package/src/db.ts +154 -1
- package/src/routes.ts +105 -0
- package/src/security.ts +2 -1
- package/src/types.ts +23 -0
package/src/db.ts
CHANGED
|
@@ -21,6 +21,9 @@ import type {
|
|
|
21
21
|
TaskSeverity,
|
|
22
22
|
TaskStatus,
|
|
23
23
|
IntegrationEventInput,
|
|
24
|
+
InboxDraft,
|
|
25
|
+
InboxState,
|
|
26
|
+
InboxThreadState,
|
|
24
27
|
TaskStatusInput,
|
|
25
28
|
} from "./types";
|
|
26
29
|
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS } from "./config";
|
|
@@ -144,6 +147,27 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
144
147
|
CREATE INDEX IF NOT EXISTS idx_pairs_requester ON pairs(requester_id);
|
|
145
148
|
CREATE INDEX IF NOT EXISTS idx_pairs_target ON pairs(target_id);
|
|
146
149
|
CREATE INDEX IF NOT EXISTS idx_pairs_status ON pairs(status);
|
|
150
|
+
|
|
151
|
+
CREATE TABLE IF NOT EXISTS inbox_thread_state (
|
|
152
|
+
operator_id TEXT NOT NULL,
|
|
153
|
+
peer_id TEXT NOT NULL,
|
|
154
|
+
read_cursor_message_id INTEGER,
|
|
155
|
+
archived_at_message_id INTEGER,
|
|
156
|
+
updated_at INTEGER NOT NULL,
|
|
157
|
+
PRIMARY KEY (operator_id, peer_id)
|
|
158
|
+
);
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_thread_operator ON inbox_thread_state(operator_id);
|
|
160
|
+
|
|
161
|
+
CREATE TABLE IF NOT EXISTS inbox_drafts (
|
|
162
|
+
operator_id TEXT NOT NULL,
|
|
163
|
+
peer_id TEXT NOT NULL,
|
|
164
|
+
body TEXT NOT NULL,
|
|
165
|
+
subject TEXT,
|
|
166
|
+
channel TEXT,
|
|
167
|
+
updated_at INTEGER NOT NULL,
|
|
168
|
+
PRIMARY KEY (operator_id, peer_id)
|
|
169
|
+
);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_drafts_operator ON inbox_drafts(operator_id);
|
|
147
171
|
`);
|
|
148
172
|
|
|
149
173
|
// Migrations
|
|
@@ -257,6 +281,44 @@ function parseStringArray(raw: string): string[] {
|
|
|
257
281
|
return parsed.filter((value): value is string => typeof value === "string");
|
|
258
282
|
}
|
|
259
283
|
|
|
284
|
+
function normalizeTags(tags: string[] | undefined): string[] {
|
|
285
|
+
return [...new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function stringValue(value: unknown): string | undefined {
|
|
289
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function inferProviderTag(input: RegisterAgentInput): "claude" | "codex" | undefined {
|
|
293
|
+
const meta = input.meta ?? {};
|
|
294
|
+
const values = [
|
|
295
|
+
input.id,
|
|
296
|
+
input.name,
|
|
297
|
+
input.rig,
|
|
298
|
+
...(input.tags ?? []),
|
|
299
|
+
stringValue(meta.provider),
|
|
300
|
+
stringValue(meta.client),
|
|
301
|
+
stringValue(meta.runtime),
|
|
302
|
+
stringValue(meta.agentType),
|
|
303
|
+
].filter((value): value is string => typeof value === "string");
|
|
304
|
+
|
|
305
|
+
if (values.some((value) => value.toLowerCase().includes("codex"))) return "codex";
|
|
306
|
+
if (values.some((value) => value.toLowerCase().includes("claude"))) return "claude";
|
|
307
|
+
|
|
308
|
+
// Older Claude hooks did not always send an explicit provider tag, but did
|
|
309
|
+
// send Claude-style approval metadata. Codex also sends approvalMode, so this
|
|
310
|
+
// fallback only runs after Codex signals have been ruled out.
|
|
311
|
+
if (Object.prototype.hasOwnProperty.call(meta, "approvalMode")) return "claude";
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function tagsWithProvider(input: RegisterAgentInput): string[] {
|
|
316
|
+
const tags = normalizeTags(input.tags);
|
|
317
|
+
const provider = inferProviderTag(input);
|
|
318
|
+
if (!provider || tags.includes(provider)) return tags;
|
|
319
|
+
return [provider, ...tags];
|
|
320
|
+
}
|
|
321
|
+
|
|
260
322
|
function rowToAgent(row: any): AgentCard {
|
|
261
323
|
return {
|
|
262
324
|
id: row.id,
|
|
@@ -355,6 +417,27 @@ function rowToPair(row: any): PairSession {
|
|
|
355
417
|
};
|
|
356
418
|
}
|
|
357
419
|
|
|
420
|
+
function rowToInboxThreadState(row: any): InboxThreadState {
|
|
421
|
+
return {
|
|
422
|
+
operatorId: row.operator_id,
|
|
423
|
+
peerId: row.peer_id,
|
|
424
|
+
readCursorMessageId: row.read_cursor_message_id ?? undefined,
|
|
425
|
+
archivedAtMessageId: row.archived_at_message_id ?? undefined,
|
|
426
|
+
updatedAt: row.updated_at,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function rowToInboxDraft(row: any): InboxDraft {
|
|
431
|
+
return {
|
|
432
|
+
operatorId: row.operator_id,
|
|
433
|
+
peerId: row.peer_id,
|
|
434
|
+
body: row.body,
|
|
435
|
+
subject: row.subject ?? undefined,
|
|
436
|
+
channel: row.channel ?? undefined,
|
|
437
|
+
updatedAt: row.updated_at,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
358
441
|
const MSG_SELECT = `SELECT m.*, (
|
|
359
442
|
SELECT json_group_array(agent_id) FROM message_reads WHERE message_id = m.id
|
|
360
443
|
) AS read_by_agents FROM messages m`;
|
|
@@ -363,6 +446,7 @@ const MSG_SELECT = `SELECT m.*, (
|
|
|
363
446
|
|
|
364
447
|
export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
365
448
|
const now = Date.now();
|
|
449
|
+
const tags = tagsWithProvider(input);
|
|
366
450
|
// Preserve the existing label across re-registrations unless the caller
|
|
367
451
|
// explicitly sends one (including null to clear).
|
|
368
452
|
const labelProvided = Object.prototype.hasOwnProperty.call(input, "label");
|
|
@@ -394,7 +478,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
|
394
478
|
$name: input.name,
|
|
395
479
|
$label: input.label ?? null,
|
|
396
480
|
$labelProvided: labelProvided ? 1 : 0,
|
|
397
|
-
$tags: JSON.stringify(
|
|
481
|
+
$tags: JSON.stringify(tags),
|
|
398
482
|
$machine: input.machine ?? null,
|
|
399
483
|
$rig: input.rig ?? null,
|
|
400
484
|
$capabilities: JSON.stringify(input.capabilities ?? []),
|
|
@@ -1423,6 +1507,75 @@ export function markRead(messageId: number, agentId: string): boolean {
|
|
|
1423
1507
|
return true;
|
|
1424
1508
|
}
|
|
1425
1509
|
|
|
1510
|
+
export function getInboxState(operatorId: string): InboxState {
|
|
1511
|
+
const threads = (db.prepare(
|
|
1512
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? ORDER BY updated_at DESC",
|
|
1513
|
+
).all(operatorId) as any[]).map(rowToInboxThreadState);
|
|
1514
|
+
const drafts = (db.prepare(
|
|
1515
|
+
"SELECT * FROM inbox_drafts WHERE operator_id = ? ORDER BY updated_at DESC",
|
|
1516
|
+
).all(operatorId) as any[]).map(rowToInboxDraft);
|
|
1517
|
+
return { operatorId, threads, drafts };
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
export function setInboxThreadState(input: {
|
|
1521
|
+
operatorId: string;
|
|
1522
|
+
peerId: string;
|
|
1523
|
+
readCursorMessageId?: number | null;
|
|
1524
|
+
archivedAtMessageId?: number | null;
|
|
1525
|
+
}): InboxThreadState {
|
|
1526
|
+
const now = Date.now();
|
|
1527
|
+
const current = db.prepare(
|
|
1528
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
|
|
1529
|
+
).get(input.operatorId, input.peerId) as any | undefined;
|
|
1530
|
+
|
|
1531
|
+
const readCursorMessageId = Object.prototype.hasOwnProperty.call(input, "readCursorMessageId")
|
|
1532
|
+
? input.readCursorMessageId ?? null
|
|
1533
|
+
: current?.read_cursor_message_id ?? null;
|
|
1534
|
+
const archivedAtMessageId = Object.prototype.hasOwnProperty.call(input, "archivedAtMessageId")
|
|
1535
|
+
? input.archivedAtMessageId ?? null
|
|
1536
|
+
: current?.archived_at_message_id ?? null;
|
|
1537
|
+
|
|
1538
|
+
db.prepare(`
|
|
1539
|
+
INSERT INTO inbox_thread_state (operator_id, peer_id, read_cursor_message_id, archived_at_message_id, updated_at)
|
|
1540
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1541
|
+
ON CONFLICT(operator_id, peer_id) DO UPDATE SET
|
|
1542
|
+
read_cursor_message_id = excluded.read_cursor_message_id,
|
|
1543
|
+
archived_at_message_id = excluded.archived_at_message_id,
|
|
1544
|
+
updated_at = excluded.updated_at
|
|
1545
|
+
`).run(input.operatorId, input.peerId, readCursorMessageId, archivedAtMessageId, now);
|
|
1546
|
+
|
|
1547
|
+
return rowToInboxThreadState(db.prepare(
|
|
1548
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
|
|
1549
|
+
).get(input.operatorId, input.peerId));
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
export function setInboxDraft(input: {
|
|
1553
|
+
operatorId: string;
|
|
1554
|
+
peerId: string;
|
|
1555
|
+
body: string;
|
|
1556
|
+
subject?: string | null;
|
|
1557
|
+
channel?: string | null;
|
|
1558
|
+
}): InboxDraft {
|
|
1559
|
+
const now = Date.now();
|
|
1560
|
+
db.prepare(`
|
|
1561
|
+
INSERT INTO inbox_drafts (operator_id, peer_id, body, subject, channel, updated_at)
|
|
1562
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1563
|
+
ON CONFLICT(operator_id, peer_id) DO UPDATE SET
|
|
1564
|
+
body = excluded.body,
|
|
1565
|
+
subject = excluded.subject,
|
|
1566
|
+
channel = excluded.channel,
|
|
1567
|
+
updated_at = excluded.updated_at
|
|
1568
|
+
`).run(input.operatorId, input.peerId, input.body, input.subject ?? null, input.channel ?? null, now);
|
|
1569
|
+
|
|
1570
|
+
return rowToInboxDraft(db.prepare(
|
|
1571
|
+
"SELECT * FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?",
|
|
1572
|
+
).get(input.operatorId, input.peerId));
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
export function deleteInboxDraft(operatorId: string, peerId: string): boolean {
|
|
1576
|
+
return db.prepare("DELETE FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?").run(operatorId, peerId).changes > 0;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1426
1579
|
export function deleteMessage(id: number): boolean {
|
|
1427
1580
|
return db.transaction(() => {
|
|
1428
1581
|
// Break reply_to references from children so the FK doesn't block delete.
|
package/src/routes.ts
CHANGED
|
@@ -36,6 +36,10 @@ import {
|
|
|
36
36
|
rejectPair,
|
|
37
37
|
endPair,
|
|
38
38
|
sendPairMessage,
|
|
39
|
+
getInboxState,
|
|
40
|
+
setInboxThreadState,
|
|
41
|
+
setInboxDraft,
|
|
42
|
+
deleteInboxDraft,
|
|
39
43
|
createCallbackDelivery,
|
|
40
44
|
finishCallbackDelivery,
|
|
41
45
|
validateAgentSession,
|
|
@@ -209,6 +213,15 @@ function cleanPositiveId(value: unknown, field: string): number | undefined {
|
|
|
209
213
|
return value;
|
|
210
214
|
}
|
|
211
215
|
|
|
216
|
+
function cleanNullablePositiveId(value: unknown, field: string): number | null | undefined {
|
|
217
|
+
if (value === undefined) return undefined;
|
|
218
|
+
if (value === null) return null;
|
|
219
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
220
|
+
throw new ValidationError(`${field} must be a positive integer or null`);
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
212
225
|
function cleanEpoch(value: unknown, field: string): number | undefined {
|
|
213
226
|
if (value === undefined || value === null) return undefined;
|
|
214
227
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
@@ -370,6 +383,50 @@ function normalizePairMessageInput(body: unknown): PairMessageInput {
|
|
|
370
383
|
};
|
|
371
384
|
}
|
|
372
385
|
|
|
386
|
+
function cleanOperatorId(value: unknown): string {
|
|
387
|
+
return cleanString(value, "operatorId", { max: 200 }) || "user";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeInboxThreadStateInput(body: unknown): {
|
|
391
|
+
operatorId: string;
|
|
392
|
+
peerId: string;
|
|
393
|
+
readCursorMessageId?: number | null;
|
|
394
|
+
archivedAtMessageId?: number | null;
|
|
395
|
+
} {
|
|
396
|
+
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
397
|
+
const input: {
|
|
398
|
+
operatorId: string;
|
|
399
|
+
peerId: string;
|
|
400
|
+
readCursorMessageId?: number | null;
|
|
401
|
+
archivedAtMessageId?: number | null;
|
|
402
|
+
} = {
|
|
403
|
+
operatorId: cleanOperatorId(body.operatorId),
|
|
404
|
+
peerId: cleanString(body.peerId, "peerId", { required: true, max: 200 })!,
|
|
405
|
+
};
|
|
406
|
+
const readCursorMessageId = cleanNullablePositiveId(body.readCursorMessageId, "readCursorMessageId");
|
|
407
|
+
if (readCursorMessageId !== undefined) input.readCursorMessageId = readCursorMessageId;
|
|
408
|
+
const archivedAtMessageId = cleanNullablePositiveId(body.archivedAtMessageId, "archivedAtMessageId");
|
|
409
|
+
if (archivedAtMessageId !== undefined) input.archivedAtMessageId = archivedAtMessageId;
|
|
410
|
+
return input;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function normalizeInboxDraftInput(body: unknown): {
|
|
414
|
+
operatorId: string;
|
|
415
|
+
peerId: string;
|
|
416
|
+
body: string;
|
|
417
|
+
subject?: string;
|
|
418
|
+
channel?: string;
|
|
419
|
+
} {
|
|
420
|
+
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
421
|
+
return {
|
|
422
|
+
operatorId: cleanOperatorId(body.operatorId),
|
|
423
|
+
peerId: cleanString(body.peerId, "peerId", { required: true, max: 200 })!,
|
|
424
|
+
body: cleanString(body.body, "body", { required: true, max: MAX_BODY_BYTES })!,
|
|
425
|
+
subject: cleanString(body.subject, "subject", { max: 200 }),
|
|
426
|
+
channel: cleanString(body.channel, "channel", { max: 120 }),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
373
430
|
function pairErrorStatus(code: string): number {
|
|
374
431
|
if (code === "not_found") return 404;
|
|
375
432
|
if (code === "busy") return 409;
|
|
@@ -730,6 +787,49 @@ const deleteMessageById: Handler = (_req, params) => {
|
|
|
730
787
|
|
|
731
788
|
const getCursorRoute: Handler = () => json({ latestId: getLatestMessageId() });
|
|
732
789
|
|
|
790
|
+
// --- Inbox operator state ---
|
|
791
|
+
|
|
792
|
+
const getInboxStateRoute: Handler = (req) => {
|
|
793
|
+
const url = new URL(req.url);
|
|
794
|
+
const operatorId = cleanOperatorId(url.searchParams.get("operatorId") ?? undefined);
|
|
795
|
+
return json(getInboxState(operatorId));
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const patchInboxThreadState: Handler = async (req) => {
|
|
799
|
+
const parsed = await parseBody<unknown>(req);
|
|
800
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
801
|
+
try {
|
|
802
|
+
return json(setInboxThreadState(normalizeInboxThreadStateInput(parsed.body)));
|
|
803
|
+
} catch (e) {
|
|
804
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
805
|
+
throw e;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const putInboxDraft: Handler = async (req) => {
|
|
810
|
+
const parsed = await parseBody<unknown>(req);
|
|
811
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
812
|
+
try {
|
|
813
|
+
return json(setInboxDraft(normalizeInboxDraftInput(parsed.body)));
|
|
814
|
+
} catch (e) {
|
|
815
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
816
|
+
throw e;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
const deleteInboxDraftRoute: Handler = (req) => {
|
|
821
|
+
const url = new URL(req.url);
|
|
822
|
+
try {
|
|
823
|
+
const operatorId = cleanOperatorId(url.searchParams.get("operatorId") ?? undefined);
|
|
824
|
+
const peerId = cleanString(url.searchParams.get("peerId") ?? undefined, "peerId", { required: true, max: 200 })!;
|
|
825
|
+
deleteInboxDraft(operatorId, peerId);
|
|
826
|
+
return json({ ok: true });
|
|
827
|
+
} catch (e) {
|
|
828
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
829
|
+
throw e;
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
733
833
|
// --- Tasks and integrations ---
|
|
734
834
|
|
|
735
835
|
const postIntegrationEvent: Handler = async (req) => {
|
|
@@ -1001,6 +1101,11 @@ const routes: Route[] = [
|
|
|
1001
1101
|
route("PATCH", "/api/messages/:id", patchMessage),
|
|
1002
1102
|
route("DELETE", "/api/messages/:id", deleteMessageById),
|
|
1003
1103
|
|
|
1104
|
+
route("GET", "/api/inbox/state", getInboxStateRoute),
|
|
1105
|
+
route("PATCH", "/api/inbox/threads", patchInboxThreadState),
|
|
1106
|
+
route("PUT", "/api/inbox/drafts", putInboxDraft),
|
|
1107
|
+
route("DELETE", "/api/inbox/drafts", deleteInboxDraftRoute),
|
|
1108
|
+
|
|
1004
1109
|
route("POST", "/api/integrations/events", postIntegrationEvent),
|
|
1005
1110
|
route("GET", "/api/tasks", getTasks),
|
|
1006
1111
|
route("GET", "/api/tasks/:id", getTaskById),
|
package/src/security.ts
CHANGED
|
@@ -55,7 +55,7 @@ export function corsPreflight(req: Request): Response {
|
|
|
55
55
|
|
|
56
56
|
const response = new Response(null, {
|
|
57
57
|
headers: {
|
|
58
|
-
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
58
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
59
59
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Agent-Relay-Token, Idempotency-Key, X-Agent-Relay-Instance-Id, X-Agent-Relay-Epoch",
|
|
60
60
|
"Access-Control-Max-Age": "600",
|
|
61
61
|
},
|
|
@@ -97,6 +97,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
97
97
|
if (pathname === "/api/events") return "events:read";
|
|
98
98
|
if (pathname.startsWith("/api/integrations/")) return method === "GET" ? "integrations:read" : "integrations:write";
|
|
99
99
|
if (pathname.startsWith("/api/agents")) return method === "GET" ? "agents:read" : "agents:write";
|
|
100
|
+
if (pathname.startsWith("/api/inbox")) return method === "GET" ? "messages:read" : "messages:write";
|
|
100
101
|
if (pathname.startsWith("/api/messages")) return method === "GET" ? "messages:read" : "messages:write";
|
|
101
102
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "tasks:read" : "tasks:write";
|
|
102
103
|
if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
|
package/src/types.ts
CHANGED
|
@@ -184,6 +184,29 @@ export interface TaskStatusInput {
|
|
|
184
184
|
metadata?: Record<string, unknown>;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
export interface InboxThreadState {
|
|
188
|
+
operatorId: string;
|
|
189
|
+
peerId: string;
|
|
190
|
+
readCursorMessageId?: number;
|
|
191
|
+
archivedAtMessageId?: number;
|
|
192
|
+
updatedAt: number;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface InboxDraft {
|
|
196
|
+
operatorId: string;
|
|
197
|
+
peerId: string;
|
|
198
|
+
body: string;
|
|
199
|
+
subject?: string;
|
|
200
|
+
channel?: string;
|
|
201
|
+
updatedAt: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface InboxState {
|
|
205
|
+
operatorId: string;
|
|
206
|
+
threads: InboxThreadState[];
|
|
207
|
+
drafts: InboxDraft[];
|
|
208
|
+
}
|
|
209
|
+
|
|
187
210
|
export interface HealthCheck {
|
|
188
211
|
name: string;
|
|
189
212
|
status: "ok" | "warn" | "error";
|