agent-relay-server 0.4.22 → 0.4.24

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/src/routes.ts CHANGED
@@ -36,12 +36,20 @@ import {
36
36
  rejectPair,
37
37
  endPair,
38
38
  sendPairMessage,
39
+ getInboxState,
40
+ setInboxThreadState,
41
+ setInboxDraft,
42
+ deleteInboxDraft,
43
+ listActivityEvents,
44
+ createActivityEvent,
39
45
  createCallbackDelivery,
40
46
  finishCallbackDelivery,
47
+ reapStaleAgents,
48
+ releaseExpiredClaims,
41
49
  validateAgentSession,
42
50
  ValidationError,
43
51
  } from "./db";
44
- import type { AgentSessionGuard, CreatePairInput, IntegrationEventInput, PairActionInput, PairMessageInput, PairStatus, RegisterAgentInput, SendMessageInput, TaskStatus, TaskStatusInput } from "./types";
52
+ import type { ActivityEventInput, ActivityKind, AgentSessionGuard, CreatePairInput, IntegrationEventInput, PairActionInput, PairMessageInput, PairStatus, RegisterAgentInput, SendMessageInput, TaskStatus, TaskStatusInput } from "./types";
45
53
  import { getIntegrationTokens, INTEGRATION_RATE_LIMIT_PER_MINUTE, MAX_BODY_BYTES } from "./config";
46
54
  import {
47
55
  getIntegrationAuth,
@@ -54,6 +62,7 @@ import {
54
62
  emitAgentStatus,
55
63
  emitAgentRemoved,
56
64
  emitMessageClaimed,
65
+ emitMessageClaimReleased,
57
66
  emitMessageDeleted,
58
67
  emitTaskChanged,
59
68
  } from "./sse";
@@ -143,6 +152,7 @@ const VALID_AGENT_STATUSES = ["online", "idle", "busy", "offline"] as const;
143
152
  const VALID_TASK_SEVERITIES = ["info", "warning", "critical"] as const;
144
153
  const VALID_TASK_STATUSES = ["open", "claimed", "in_progress", "blocked", "done", "failed", "canceled"] as const;
145
154
  const VALID_PAIR_STATUSES = ["pending", "active", "ended", "rejected", "expired"] as const;
155
+ const VALID_ACTIVITY_KINDS = ["message", "reply", "question", "operator", "pair", "task", "state"] as const;
146
156
  const integrationRateBuckets = new Map<string, { windowStart: number; count: number }>();
147
157
 
148
158
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -209,6 +219,15 @@ function cleanPositiveId(value: unknown, field: string): number | undefined {
209
219
  return value;
210
220
  }
211
221
 
222
+ function cleanNullablePositiveId(value: unknown, field: string): number | null | undefined {
223
+ if (value === undefined) return undefined;
224
+ if (value === null) return null;
225
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
226
+ throw new ValidationError(`${field} must be a positive integer or null`);
227
+ }
228
+ return value;
229
+ }
230
+
212
231
  function cleanEpoch(value: unknown, field: string): number | undefined {
213
232
  if (value === undefined || value === null) return undefined;
214
233
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
@@ -370,6 +389,70 @@ function normalizePairMessageInput(body: unknown): PairMessageInput {
370
389
  };
371
390
  }
372
391
 
392
+ function cleanOperatorId(value: unknown): string {
393
+ return cleanString(value, "operatorId", { max: 200 }) || "user";
394
+ }
395
+
396
+ function normalizeInboxThreadStateInput(body: unknown): {
397
+ operatorId: string;
398
+ peerId: string;
399
+ readCursorMessageId?: number | null;
400
+ archivedAtMessageId?: number | null;
401
+ } {
402
+ if (!isRecord(body)) throw new ValidationError("JSON object body required");
403
+ const input: {
404
+ operatorId: string;
405
+ peerId: string;
406
+ readCursorMessageId?: number | null;
407
+ archivedAtMessageId?: number | null;
408
+ } = {
409
+ operatorId: cleanOperatorId(body.operatorId),
410
+ peerId: cleanString(body.peerId, "peerId", { required: true, max: 200 })!,
411
+ };
412
+ const readCursorMessageId = cleanNullablePositiveId(body.readCursorMessageId, "readCursorMessageId");
413
+ if (readCursorMessageId !== undefined) input.readCursorMessageId = readCursorMessageId;
414
+ const archivedAtMessageId = cleanNullablePositiveId(body.archivedAtMessageId, "archivedAtMessageId");
415
+ if (archivedAtMessageId !== undefined) input.archivedAtMessageId = archivedAtMessageId;
416
+ return input;
417
+ }
418
+
419
+ function normalizeInboxDraftInput(body: unknown): {
420
+ operatorId: string;
421
+ peerId: string;
422
+ body: string;
423
+ subject?: string;
424
+ channel?: string;
425
+ } {
426
+ if (!isRecord(body)) throw new ValidationError("JSON object body required");
427
+ return {
428
+ operatorId: cleanOperatorId(body.operatorId),
429
+ peerId: cleanString(body.peerId, "peerId", { required: true, max: 200 })!,
430
+ body: cleanString(body.body, "body", { required: true, max: MAX_BODY_BYTES })!,
431
+ subject: cleanString(body.subject, "subject", { max: 200 }),
432
+ channel: cleanString(body.channel, "channel", { max: 120 }),
433
+ };
434
+ }
435
+
436
+ function normalizeActivityInput(body: unknown): ActivityEventInput {
437
+ if (!isRecord(body)) throw new ValidationError("JSON object body required");
438
+ return {
439
+ operatorId: cleanOperatorId(body.operatorId),
440
+ clientId: cleanString(body.clientId, "clientId", { max: 240 }),
441
+ kind: cleanEnum(body.kind, "kind", VALID_ACTIVITY_KINDS, "operator") as ActivityKind,
442
+ title: cleanString(body.title, "title", { required: true, max: 200 })!,
443
+ body: cleanString(body.body, "body", { max: 1000 }),
444
+ meta: cleanString(body.meta, "meta", { max: 500 }),
445
+ icon: cleanString(body.icon, "icon", { max: 80 }),
446
+ view: cleanString(body.view, "view", { max: 80 }),
447
+ peer: cleanString(body.peer, "peer", { max: 200 }),
448
+ messageId: cleanPositiveId(body.messageId, "messageId"),
449
+ pairId: cleanString(body.pairId, "pairId", { max: 120 }),
450
+ taskId: cleanPositiveId(body.taskId, "taskId"),
451
+ agentId: cleanString(body.agentId, "agentId", { max: 200 }),
452
+ metadata: cleanMeta(body.metadata),
453
+ };
454
+ }
455
+
373
456
  function pairErrorStatus(code: string): number {
374
457
  if (code === "not_found") return 404;
375
458
  if (code === "busy") return 409;
@@ -432,14 +515,39 @@ async function postCallback(deliveryId: number, url: string, payload: unknown):
432
515
  }
433
516
  }
434
517
 
518
+ function auditEvent(input: ActivityEventInput): void {
519
+ try {
520
+ createActivityEvent({
521
+ ...input,
522
+ metadata: { source: "server", ...(input.metadata ?? {}) },
523
+ });
524
+ } catch {
525
+ // Audit trail writes must never block relay behavior.
526
+ }
527
+ }
528
+
435
529
  // --- Agent routes ---
436
530
 
437
531
  const postAgent: Handler = async (req) => {
438
532
  const parsed = await parseBody<unknown>(req);
439
533
  if (!parsed.ok) return error(parsed.error, parsed.status);
440
534
  try {
441
- const agent = upsertAgent(normalizeAgentInput(parsed.body));
535
+ const input = normalizeAgentInput(parsed.body);
536
+ const existing = getAgent(input.id);
537
+ const agent = upsertAgent(input);
442
538
  emitAgentStatus(agent.id);
539
+ if (!existing) {
540
+ auditEvent({
541
+ clientId: "server-agent-" + agent.id + "-registered",
542
+ kind: "state",
543
+ title: "Agent registered",
544
+ body: agent.name,
545
+ meta: agent.id,
546
+ icon: "ti-robot",
547
+ view: "agents",
548
+ agentId: agent.id,
549
+ });
550
+ }
443
551
  return json(agent, 201);
444
552
  } catch (e) {
445
553
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -485,6 +593,15 @@ const patchAgentStatus: Handler = async (req, params) => {
485
593
  throw e;
486
594
  }
487
595
  emitAgentStatus(params.id!);
596
+ auditEvent({
597
+ clientId: "server-agent-" + params.id! + "-status-" + body.status + "-" + Date.now(),
598
+ kind: "state",
599
+ title: "Agent " + body.status,
600
+ meta: params.id!,
601
+ icon: body.status === "offline" ? "ti-plug-off" : "ti-activity",
602
+ view: "agents",
603
+ agentId: params.id!,
604
+ });
488
605
  return json({ ok: true });
489
606
  };
490
607
 
@@ -543,6 +660,15 @@ const patchAgentReady: Handler = async (req, params) => {
543
660
  throw e;
544
661
  }
545
662
  emitAgentStatus(params.id!);
663
+ auditEvent({
664
+ clientId: "server-agent-" + params.id! + "-ready-" + body.ready + "-" + Date.now(),
665
+ kind: "state",
666
+ title: body.ready ? "Agent ready" : "Agent not ready",
667
+ meta: params.id!,
668
+ icon: body.ready ? "ti-circle-check" : "ti-loader",
669
+ view: "agents",
670
+ agentId: params.id!,
671
+ });
546
672
  return json({ ok: true });
547
673
  };
548
674
 
@@ -569,7 +695,20 @@ const postMessage: Handler = async (req) => {
569
695
  input.idempotencyKey = cleanString(req.headers.get("Idempotency-Key") ?? undefined, "idempotencyKey", { max: 240 });
570
696
  }
571
697
  const result = sendMessageWithResult(input);
572
- if (result.created) emitNewMessage(result.message);
698
+ if (result.created) {
699
+ emitNewMessage(result.message);
700
+ auditEvent({
701
+ clientId: "server-message-" + result.message.id,
702
+ kind: result.message.claimable ? "task" : "message",
703
+ title: result.message.claimable ? "Claimable message sent" : "Message sent",
704
+ body: result.message.subject || result.message.body,
705
+ meta: `${result.message.from} -> ${result.message.to}`,
706
+ icon: result.message.claimable ? "ti-hand-grab" : "ti-send",
707
+ view: "messages",
708
+ messageId: result.message.id,
709
+ agentId: result.message.from,
710
+ });
711
+ }
573
712
  return json(result.message, result.created ? 201 : 200);
574
713
  } catch (e) {
575
714
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -668,6 +807,17 @@ const postClaimMessage: Handler = async (req, params) => {
668
807
  const result = claimMessage(id, body.agentId, guard);
669
808
  if (result.ok) {
670
809
  emitMessageClaimed(id, body.agentId, getMessage(id)?.claimExpiresAt);
810
+ auditEvent({
811
+ clientId: "server-message-" + id + "-claimed-" + body.agentId,
812
+ kind: "task",
813
+ title: "Message claimed",
814
+ body: "Message #" + id,
815
+ meta: "by " + body.agentId,
816
+ icon: "ti-user-check",
817
+ view: "work",
818
+ messageId: id,
819
+ agentId: body.agentId,
820
+ });
671
821
  if (result.task) {
672
822
  emitTaskChanged(result.task, "task.claimed");
673
823
  void dispatchTaskCallbacks(result.task.id, "task.claimed");
@@ -730,6 +880,80 @@ const deleteMessageById: Handler = (_req, params) => {
730
880
 
731
881
  const getCursorRoute: Handler = () => json({ latestId: getLatestMessageId() });
732
882
 
883
+ // --- Inbox operator state ---
884
+
885
+ const getInboxStateRoute: Handler = (req) => {
886
+ const url = new URL(req.url);
887
+ const operatorId = cleanOperatorId(url.searchParams.get("operatorId") ?? undefined);
888
+ return json(getInboxState(operatorId));
889
+ };
890
+
891
+ const patchInboxThreadState: Handler = async (req) => {
892
+ const parsed = await parseBody<unknown>(req);
893
+ if (!parsed.ok) return error(parsed.error, parsed.status);
894
+ try {
895
+ return json(setInboxThreadState(normalizeInboxThreadStateInput(parsed.body)));
896
+ } catch (e) {
897
+ if (e instanceof ValidationError) return error(e.message, 400);
898
+ throw e;
899
+ }
900
+ };
901
+
902
+ const putInboxDraft: Handler = async (req) => {
903
+ const parsed = await parseBody<unknown>(req);
904
+ if (!parsed.ok) return error(parsed.error, parsed.status);
905
+ try {
906
+ return json(setInboxDraft(normalizeInboxDraftInput(parsed.body)));
907
+ } catch (e) {
908
+ if (e instanceof ValidationError) return error(e.message, 400);
909
+ throw e;
910
+ }
911
+ };
912
+
913
+ const deleteInboxDraftRoute: Handler = (req) => {
914
+ const url = new URL(req.url);
915
+ try {
916
+ const operatorId = cleanOperatorId(url.searchParams.get("operatorId") ?? undefined);
917
+ const peerId = cleanString(url.searchParams.get("peerId") ?? undefined, "peerId", { required: true, max: 200 })!;
918
+ deleteInboxDraft(operatorId, peerId);
919
+ return json({ ok: true });
920
+ } catch (e) {
921
+ if (e instanceof ValidationError) return error(e.message, 400);
922
+ throw e;
923
+ }
924
+ };
925
+
926
+ // --- Shared activity audit trail ---
927
+
928
+ const getActivityEvents: Handler = (req) => {
929
+ const url = new URL(req.url);
930
+ try {
931
+ const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
932
+ if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
933
+ const sinceRaw = parseQueryInt(url.searchParams.get("since"), { min: 0, max: Number.MAX_SAFE_INTEGER });
934
+ if (Number.isNaN(sinceRaw)) return error("since must be a non-negative integer");
935
+ return json(listActivityEvents({
936
+ operatorId: cleanString(url.searchParams.get("operatorId") ?? undefined, "operatorId", { max: 200 }),
937
+ limit: limitRaw ?? 200,
938
+ since: sinceRaw ?? undefined,
939
+ }));
940
+ } catch (e) {
941
+ if (e instanceof ValidationError) return error(e.message, 400);
942
+ throw e;
943
+ }
944
+ };
945
+
946
+ const postActivityEvent: Handler = async (req) => {
947
+ const parsed = await parseBody<unknown>(req);
948
+ if (!parsed.ok) return error(parsed.error, parsed.status);
949
+ try {
950
+ return json(createActivityEvent(normalizeActivityInput(parsed.body)), 201);
951
+ } catch (e) {
952
+ if (e instanceof ValidationError) return error(e.message, 400);
953
+ throw e;
954
+ }
955
+ };
956
+
733
957
  // --- Tasks and integrations ---
734
958
 
735
959
  const postIntegrationEvent: Handler = async (req) => {
@@ -804,6 +1028,17 @@ const postClaimTask: Handler = async (req, params) => {
804
1028
  return error(result.error!, status);
805
1029
  }
806
1030
  emitTaskChanged(result.task!, "task.claimed");
1031
+ auditEvent({
1032
+ clientId: "server-task-" + id + "-claimed-" + agentId,
1033
+ kind: "task",
1034
+ title: "Task claimed",
1035
+ body: result.task!.title,
1036
+ meta: "by " + agentId,
1037
+ icon: "ti-user-check",
1038
+ view: "work",
1039
+ taskId: id,
1040
+ agentId,
1041
+ });
807
1042
  if (result.task!.messageId) {
808
1043
  emitMessageClaimed(result.task!.messageId, agentId, getMessage(result.task!.messageId)?.claimExpiresAt);
809
1044
  }
@@ -846,6 +1081,17 @@ const patchTaskStatus: Handler = async (req, params) => {
846
1081
  const result = updateTaskStatus(id, normalizeTaskStatusInput(parsed.body));
847
1082
  if (!result.ok) return error(result.error!, result.error === "task not found" ? 404 : agentSessionStatus(result.error));
848
1083
  emitTaskChanged(result.task!, "task.status");
1084
+ auditEvent({
1085
+ clientId: "server-task-" + id + "-status-" + result.task!.status + "-" + Date.now(),
1086
+ kind: "task",
1087
+ title: "Task " + result.task!.status,
1088
+ body: result.task!.title,
1089
+ meta: result.task!.claimedBy ? "by " + result.task!.claimedBy : result.task!.target,
1090
+ icon: "ti-arrows-exchange",
1091
+ view: "work",
1092
+ taskId: id,
1093
+ agentId: result.task!.claimedBy,
1094
+ });
849
1095
  void dispatchTaskCallbacks(id, "task.status");
850
1096
  return json({ task: result.task, event: result.event });
851
1097
  } catch (e) {
@@ -863,6 +1109,17 @@ const postPair: Handler = async (req) => {
863
1109
  const result = createPair(normalizeCreatePairInput(parsed.body));
864
1110
  if (!result.ok) return pairError(result);
865
1111
  emitNewMessage(result.invite);
1112
+ auditEvent({
1113
+ clientId: "server-pair-" + result.pair.id + "-invited",
1114
+ kind: "pair",
1115
+ title: "Pair invited",
1116
+ body: result.pair.objective,
1117
+ meta: `${result.pair.requesterId} <-> ${result.pair.targetId}`,
1118
+ icon: "ti-link-plus",
1119
+ view: "pairs",
1120
+ pairId: result.pair.id,
1121
+ agentId: result.pair.requesterId,
1122
+ });
866
1123
  return json({ pair: result.pair, invite: result.invite }, 201);
867
1124
  } catch (e) {
868
1125
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -894,6 +1151,17 @@ const postAcceptPair: Handler = async (req, params) => {
894
1151
  const result = acceptPair(params.id!, normalizePairActionInput(parsed.body));
895
1152
  if (!result.ok) return pairError(result);
896
1153
  for (const notice of result.notices) emitNewMessage(notice);
1154
+ auditEvent({
1155
+ clientId: "server-pair-" + result.pair.id + "-accepted",
1156
+ kind: "pair",
1157
+ title: "Pair accepted",
1158
+ body: result.pair.objective,
1159
+ meta: `${result.pair.requesterId} <-> ${result.pair.targetId}`,
1160
+ icon: "ti-link",
1161
+ view: "pairs",
1162
+ pairId: result.pair.id,
1163
+ agentId: result.pair.targetId,
1164
+ });
897
1165
  return json(result.pair);
898
1166
  } catch (e) {
899
1167
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -908,6 +1176,17 @@ const postRejectPair: Handler = async (req, params) => {
908
1176
  const result = rejectPair(params.id!, normalizePairActionInput(parsed.body));
909
1177
  if (!result.ok) return pairError(result);
910
1178
  emitNewMessage(result.notice);
1179
+ auditEvent({
1180
+ clientId: "server-pair-" + result.pair.id + "-rejected",
1181
+ kind: "pair",
1182
+ title: "Pair rejected",
1183
+ body: result.pair.objective,
1184
+ meta: `${result.pair.requesterId} <-> ${result.pair.targetId}`,
1185
+ icon: "ti-x",
1186
+ view: "pairs",
1187
+ pairId: result.pair.id,
1188
+ agentId: result.pair.targetId,
1189
+ });
911
1190
  return json(result.pair);
912
1191
  } catch (e) {
913
1192
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -922,6 +1201,17 @@ const postHangupPair: Handler = async (req, params) => {
922
1201
  const result = endPair(params.id!, normalizePairActionInput(parsed.body));
923
1202
  if (!result.ok) return pairError(result);
924
1203
  if (result.notice) emitNewMessage(result.notice);
1204
+ auditEvent({
1205
+ clientId: "server-pair-" + result.pair.id + "-ended-" + Date.now(),
1206
+ kind: "pair",
1207
+ title: "Pair ended",
1208
+ body: result.pair.objective,
1209
+ meta: result.pair.endedBy ? "by " + result.pair.endedBy : `${result.pair.requesterId} <-> ${result.pair.targetId}`,
1210
+ icon: "ti-phone-off",
1211
+ view: "pairs",
1212
+ pairId: result.pair.id,
1213
+ agentId: result.pair.endedBy,
1214
+ });
925
1215
  return json(result.pair);
926
1216
  } catch (e) {
927
1217
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -936,6 +1226,18 @@ const postPairMessage: Handler = async (req, params) => {
936
1226
  const result = sendPairMessage(params.id!, normalizePairMessageInput(parsed.body));
937
1227
  if (!result.ok) return pairError(result);
938
1228
  emitNewMessage(result.message);
1229
+ auditEvent({
1230
+ clientId: "server-pair-message-" + result.message.id,
1231
+ kind: "pair",
1232
+ title: "Pair message sent",
1233
+ body: result.message.subject || result.message.body,
1234
+ meta: "from " + result.message.from,
1235
+ icon: "ti-messages",
1236
+ view: "pairs",
1237
+ messageId: result.message.id,
1238
+ pairId: result.pair.id,
1239
+ agentId: result.message.from,
1240
+ });
939
1241
  return json({ pair: result.pair, message: result.message }, 201);
940
1242
  } catch (e) {
941
1243
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -956,6 +1258,28 @@ const getEvents: Handler = (req) => {
956
1258
  const getStatsRoute: Handler = () => json(getStats());
957
1259
  const getHealthRoute: Handler = () => json(getHealth());
958
1260
 
1261
+ const postSystemReap: Handler = () => {
1262
+ const released = releaseExpiredClaims();
1263
+ const reapedAgentIds = reapStaleAgents();
1264
+ for (const id of released.messageIds) emitMessageClaimReleased(id);
1265
+ for (const task of released.tasks) emitTaskChanged(task, "task.updated");
1266
+ for (const id of reapedAgentIds) emitAgentStatus(id);
1267
+ auditEvent({
1268
+ clientId: "server-system-reap-" + Date.now(),
1269
+ kind: "state",
1270
+ title: "Maintenance reaper run",
1271
+ body: `${reapedAgentIds.length} stale agent(s), ${released.messageIds.length} message claim(s), ${released.tasks.length} task claim(s)`,
1272
+ icon: "ti-broom",
1273
+ view: "activity",
1274
+ });
1275
+ return json({
1276
+ ok: true,
1277
+ reapedAgentIds,
1278
+ releasedMessageIds: released.messageIds,
1279
+ releasedTaskIds: released.tasks.map((task) => task.id),
1280
+ });
1281
+ };
1282
+
959
1283
  // --- Router ---
960
1284
 
961
1285
  interface Route {
@@ -1001,6 +1325,14 @@ const routes: Route[] = [
1001
1325
  route("PATCH", "/api/messages/:id", patchMessage),
1002
1326
  route("DELETE", "/api/messages/:id", deleteMessageById),
1003
1327
 
1328
+ route("GET", "/api/inbox/state", getInboxStateRoute),
1329
+ route("PATCH", "/api/inbox/threads", patchInboxThreadState),
1330
+ route("PUT", "/api/inbox/drafts", putInboxDraft),
1331
+ route("DELETE", "/api/inbox/drafts", deleteInboxDraftRoute),
1332
+
1333
+ route("GET", "/api/activity", getActivityEvents),
1334
+ route("POST", "/api/activity", postActivityEvent),
1335
+
1004
1336
  route("POST", "/api/integrations/events", postIntegrationEvent),
1005
1337
  route("GET", "/api/tasks", getTasks),
1006
1338
  route("GET", "/api/tasks/:id", getTaskById),
@@ -1020,6 +1352,7 @@ const routes: Route[] = [
1020
1352
  route("GET", "/api/events", getEvents),
1021
1353
  route("GET", "/api/stats", getStatsRoute),
1022
1354
  route("GET", "/api/health", getHealthRoute),
1355
+ route("POST", "/api/system/reap", postSystemReap),
1023
1356
  ];
1024
1357
 
1025
1358
  export function matchRoute(
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,8 @@ 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/activity")) return method === "GET" ? "activity:read" : "activity:write";
101
+ if (pathname.startsWith("/api/inbox")) return method === "GET" ? "messages:read" : "messages:write";
100
102
  if (pathname.startsWith("/api/messages")) return method === "GET" ? "messages:read" : "messages:write";
101
103
  if (pathname.startsWith("/api/tasks")) return method === "GET" ? "tasks:read" : "tasks:write";
102
104
  if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
package/src/types.ts CHANGED
@@ -184,6 +184,67 @@ 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
+
210
+ export type ActivityKind = "message" | "reply" | "question" | "operator" | "pair" | "task" | "state";
211
+
212
+ export interface ActivityEvent {
213
+ id: number;
214
+ operatorId?: string;
215
+ clientId?: string;
216
+ kind: ActivityKind;
217
+ title: string;
218
+ body?: string;
219
+ meta?: string;
220
+ icon?: string;
221
+ view?: string;
222
+ peer?: string;
223
+ messageId?: number;
224
+ pairId?: string;
225
+ taskId?: number;
226
+ agentId?: string;
227
+ metadata: Record<string, unknown>;
228
+ createdAt: number;
229
+ }
230
+
231
+ export interface ActivityEventInput {
232
+ operatorId?: string;
233
+ clientId?: string;
234
+ kind: ActivityKind;
235
+ title: string;
236
+ body?: string;
237
+ meta?: string;
238
+ icon?: string;
239
+ view?: string;
240
+ peer?: string;
241
+ messageId?: number;
242
+ pairId?: string;
243
+ taskId?: number;
244
+ agentId?: string;
245
+ metadata?: Record<string, unknown>;
246
+ }
247
+
187
248
  export interface HealthCheck {
188
249
  name: string;
189
250
  status: "ok" | "warn" | "error";