@nordbyte/nordrelay 0.5.2 → 0.7.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/.env.example +80 -11
- package/README.md +154 -22
- package/dist/access-control.js +7 -1
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +535 -11
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +40 -7
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +369 -0
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-service.js +237 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +93 -13
- package/dist/config.js +103 -8
- package/dist/context-key.js +87 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2073 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +57 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +36 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +87 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +897 -394
- package/dist/remote-prompt.js +98 -0
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/support-bundle.js +1 -0
- package/dist/telegram-access-commands.js +15 -2
- package/dist/telegram-access-middleware.js +16 -3
- package/dist/telegram-agent-commands.js +25 -0
- package/dist/telegram-artifact-commands.js +46 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +16 -6
- package/dist/telegram-operational-commands.js +14 -6
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/telegram-queue-commands.js +74 -4
- package/dist/telegram-support-command.js +7 -0
- package/dist/telegram-update-commands.js +27 -0
- package/dist/user-management.js +208 -0
- package/dist/web-api-contract.js +17 -0
- package/dist/web-dashboard-access-routes.js +74 -1
- package/dist/web-dashboard-artifact-routes.js +3 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-pages.js +109 -13
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +2 -0
- package/dist/web-dashboard.js +160 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +779 -55
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
package/dist/user-management.js
CHANGED
|
@@ -25,12 +25,14 @@ export class UserStore {
|
|
|
25
25
|
...user,
|
|
26
26
|
groups: this.groupsForUser(payload, user.id),
|
|
27
27
|
telegramIdentities: payload.telegramIdentities.filter((identity) => identity.userId === user.id),
|
|
28
|
+
discordIdentities: payload.discordIdentities.filter((identity) => identity.userId === user.id),
|
|
28
29
|
webSessions: payload.webSessions
|
|
29
30
|
.filter((session) => session.userId === user.id)
|
|
30
31
|
.map(publicWebSession),
|
|
31
32
|
})),
|
|
32
33
|
groups: payload.groups,
|
|
33
34
|
telegramChats: payload.telegramChats,
|
|
35
|
+
discordChannels: payload.discordChannels,
|
|
34
36
|
adminConfigured: payload.users.some((user) => user.active && this.groupIdsForUser(payload, user.id).includes(ADMIN_GROUP_ID)),
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -83,6 +85,11 @@ export class UserStore {
|
|
|
83
85
|
telegramUserId: input.telegramUserId,
|
|
84
86
|
});
|
|
85
87
|
}
|
|
88
|
+
if (input.discordUserId !== undefined) {
|
|
89
|
+
this.upsertDiscordIdentityInPayload(payload, user.id, {
|
|
90
|
+
discordUserId: input.discordUserId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
86
93
|
return this.authenticatedUser(payload, user);
|
|
87
94
|
});
|
|
88
95
|
}
|
|
@@ -229,6 +236,19 @@ export class UserStore {
|
|
|
229
236
|
const user = payload.users.find((candidate) => candidate.id === identity.userId && candidate.active);
|
|
230
237
|
return user ? this.authenticatedUser(payload, user) : null;
|
|
231
238
|
}
|
|
239
|
+
resolveDiscordUser(discordUserId) {
|
|
240
|
+
const normalized = normalizeDiscordId(discordUserId);
|
|
241
|
+
if (!normalized) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const payload = this.readPayload();
|
|
245
|
+
const identity = payload.discordIdentities.find((candidate) => candidate.discordUserId === normalized && candidate.active);
|
|
246
|
+
if (!identity) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const user = payload.users.find((candidate) => candidate.id === identity.userId && candidate.active);
|
|
250
|
+
return user ? this.authenticatedUser(payload, user) : null;
|
|
251
|
+
}
|
|
232
252
|
linkTelegramUser(userId, input) {
|
|
233
253
|
return this.mutatePayload((payload) => {
|
|
234
254
|
const user = payload.users.find((candidate) => candidate.id === userId);
|
|
@@ -245,6 +265,22 @@ export class UserStore {
|
|
|
245
265
|
return payload.telegramIdentities.length !== before;
|
|
246
266
|
});
|
|
247
267
|
}
|
|
268
|
+
linkDiscordUser(userId, input) {
|
|
269
|
+
return this.mutatePayload((payload) => {
|
|
270
|
+
const user = payload.users.find((candidate) => candidate.id === userId);
|
|
271
|
+
if (!user) {
|
|
272
|
+
throw new Error("User not found.");
|
|
273
|
+
}
|
|
274
|
+
return this.upsertDiscordIdentityInPayload(payload, userId, input);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
unlinkDiscordIdentity(identityId) {
|
|
278
|
+
return this.mutatePayload((payload) => {
|
|
279
|
+
const before = payload.discordIdentities.length;
|
|
280
|
+
payload.discordIdentities = payload.discordIdentities.filter((identity) => identity.id !== identityId);
|
|
281
|
+
return payload.discordIdentities.length !== before;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
248
284
|
createTelegramLinkCode(userId) {
|
|
249
285
|
return this.mutatePayload((payload) => {
|
|
250
286
|
if (!payload.users.some((user) => user.id === userId && user.active)) {
|
|
@@ -262,6 +298,23 @@ export class UserStore {
|
|
|
262
298
|
return code;
|
|
263
299
|
});
|
|
264
300
|
}
|
|
301
|
+
createDiscordLinkCode(userId) {
|
|
302
|
+
return this.mutatePayload((payload) => {
|
|
303
|
+
if (!payload.users.some((user) => user.id === userId && user.active)) {
|
|
304
|
+
throw new Error("Active user not found.");
|
|
305
|
+
}
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
payload.discordLinkCodes = payload.discordLinkCodes.filter((code) => new Date(code.expiresAt).getTime() > now);
|
|
308
|
+
const code = {
|
|
309
|
+
code: randomLinkCode(),
|
|
310
|
+
userId,
|
|
311
|
+
createdAt: new Date(now).toISOString(),
|
|
312
|
+
expiresAt: new Date(now + LINK_CODE_TTL_MS).toISOString(),
|
|
313
|
+
};
|
|
314
|
+
payload.discordLinkCodes.push(code);
|
|
315
|
+
return code;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
265
318
|
consumeTelegramLinkCode(code, input) {
|
|
266
319
|
return this.mutatePayload((payload) => {
|
|
267
320
|
const normalized = code.trim().toUpperCase();
|
|
@@ -279,6 +332,23 @@ export class UserStore {
|
|
|
279
332
|
return this.authenticatedUser(payload, user);
|
|
280
333
|
});
|
|
281
334
|
}
|
|
335
|
+
consumeDiscordLinkCode(code, input) {
|
|
336
|
+
return this.mutatePayload((payload) => {
|
|
337
|
+
const normalized = code.trim().toUpperCase();
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
const link = payload.discordLinkCodes.find((candidate) => candidate.code === normalized && new Date(candidate.expiresAt).getTime() > now);
|
|
340
|
+
if (!link) {
|
|
341
|
+
throw new Error("Invalid or expired link code.");
|
|
342
|
+
}
|
|
343
|
+
const user = payload.users.find((candidate) => candidate.id === link.userId && candidate.active);
|
|
344
|
+
if (!user) {
|
|
345
|
+
throw new Error("Linked user is not active.");
|
|
346
|
+
}
|
|
347
|
+
this.upsertDiscordIdentityInPayload(payload, user.id, input);
|
|
348
|
+
payload.discordLinkCodes = payload.discordLinkCodes.filter((candidate) => candidate.code !== normalized);
|
|
349
|
+
return this.authenticatedUser(payload, user);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
282
352
|
registerTelegramChat(input) {
|
|
283
353
|
return this.mutatePayload((payload) => {
|
|
284
354
|
const now = new Date().toISOString();
|
|
@@ -322,6 +392,55 @@ export class UserStore {
|
|
|
322
392
|
return chat;
|
|
323
393
|
});
|
|
324
394
|
}
|
|
395
|
+
registerDiscordChannel(input) {
|
|
396
|
+
return this.mutatePayload((payload) => {
|
|
397
|
+
const now = new Date().toISOString();
|
|
398
|
+
const channelId = normalizeDiscordId(input.channelId);
|
|
399
|
+
if (!channelId) {
|
|
400
|
+
throw new Error("Discord channel id is required.");
|
|
401
|
+
}
|
|
402
|
+
const guildId = normalizeDiscordId(input.guildId);
|
|
403
|
+
const existing = payload.discordChannels.find((channel) => channel.channelId === channelId && channel.guildId === guildId);
|
|
404
|
+
const allowedGroupIds = normalizeGroupIds(payload, input.allowedGroupIds ?? [], null);
|
|
405
|
+
if (existing) {
|
|
406
|
+
existing.title = input.title ?? existing.title;
|
|
407
|
+
existing.type = input.type ?? existing.type;
|
|
408
|
+
existing.enabled = input.enabled ?? existing.enabled;
|
|
409
|
+
existing.allowedGroupIds = allowedGroupIds;
|
|
410
|
+
existing.updatedAt = now;
|
|
411
|
+
return existing;
|
|
412
|
+
}
|
|
413
|
+
const channel = {
|
|
414
|
+
id: randomId(),
|
|
415
|
+
guildId,
|
|
416
|
+
channelId,
|
|
417
|
+
title: input.title,
|
|
418
|
+
type: input.type,
|
|
419
|
+
enabled: input.enabled ?? true,
|
|
420
|
+
allowedGroupIds,
|
|
421
|
+
createdAt: now,
|
|
422
|
+
updatedAt: now,
|
|
423
|
+
};
|
|
424
|
+
payload.discordChannels.push(channel);
|
|
425
|
+
return channel;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
updateDiscordChannel(id, patch) {
|
|
429
|
+
return this.mutatePayload((payload) => {
|
|
430
|
+
const channel = payload.discordChannels.find((candidate) => candidate.id === id);
|
|
431
|
+
if (!channel) {
|
|
432
|
+
throw new Error("Discord channel not found.");
|
|
433
|
+
}
|
|
434
|
+
if (patch.enabled !== undefined)
|
|
435
|
+
channel.enabled = patch.enabled;
|
|
436
|
+
if (patch.title !== undefined)
|
|
437
|
+
channel.title = patch.title;
|
|
438
|
+
if (patch.allowedGroupIds !== undefined)
|
|
439
|
+
channel.allowedGroupIds = normalizeGroupIds(payload, patch.allowedGroupIds, null);
|
|
440
|
+
channel.updatedAt = new Date().toISOString();
|
|
441
|
+
return channel;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
325
444
|
isTelegramChatAllowed(chatId, chatType, user) {
|
|
326
445
|
if (chatId === undefined) {
|
|
327
446
|
return false;
|
|
@@ -340,6 +459,26 @@ export class UserStore {
|
|
|
340
459
|
const userGroupIds = new Set(user.groups.map((group) => group.id));
|
|
341
460
|
return access.allowedGroupIds.some((groupId) => userGroupIds.has(groupId)) && this.canUseTelegramChat(user, chatId);
|
|
342
461
|
}
|
|
462
|
+
isDiscordChannelAllowed(input, user) {
|
|
463
|
+
const channelId = normalizeDiscordId(input.channelId);
|
|
464
|
+
if (!channelId) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (input.isDirectMessage) {
|
|
468
|
+
return this.canUseDiscordChannel(user, channelId);
|
|
469
|
+
}
|
|
470
|
+
const guildId = normalizeDiscordId(input.guildId);
|
|
471
|
+
const payload = this.readPayload();
|
|
472
|
+
const access = payload.discordChannels.find((channel) => channel.channelId === channelId && channel.guildId === guildId);
|
|
473
|
+
if (!access?.enabled) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
if (access.allowedGroupIds.length === 0) {
|
|
477
|
+
return this.canUseDiscordChannel(user, channelId);
|
|
478
|
+
}
|
|
479
|
+
const userGroupIds = new Set(user.groups.map((group) => group.id));
|
|
480
|
+
return access.allowedGroupIds.some((groupId) => userGroupIds.has(groupId)) && this.canUseDiscordChannel(user, channelId);
|
|
481
|
+
}
|
|
343
482
|
hasPermission(user, permission) {
|
|
344
483
|
return Boolean(permission && user?.permissions.includes(permission));
|
|
345
484
|
}
|
|
@@ -363,6 +502,13 @@ export class UserStore {
|
|
|
363
502
|
}
|
|
364
503
|
return user.groups.some((group) => group.telegramChatIds.length === 0 || group.telegramChatIds.includes(chatId));
|
|
365
504
|
}
|
|
505
|
+
canUseDiscordChannel(user, channelId) {
|
|
506
|
+
const normalized = normalizeDiscordId(channelId);
|
|
507
|
+
if (!user || !normalized) {
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
return user.groups.some((group) => group.discordChannelIds.length === 0 || group.discordChannelIds.includes(normalized));
|
|
511
|
+
}
|
|
366
512
|
createGroup(input) {
|
|
367
513
|
return this.mutatePayload((payload) => {
|
|
368
514
|
const now = new Date().toISOString();
|
|
@@ -382,6 +528,7 @@ export class UserStore {
|
|
|
382
528
|
agentIds: normalizeStringList(input.agentIds ?? []),
|
|
383
529
|
workspaceRoots: normalizeStringList(input.workspaceRoots ?? []),
|
|
384
530
|
telegramChatIds: normalizeNumberList(input.telegramChatIds ?? []),
|
|
531
|
+
discordChannelIds: normalizeStringList(input.discordChannelIds ?? []),
|
|
385
532
|
createdAt: now,
|
|
386
533
|
updatedAt: now,
|
|
387
534
|
};
|
|
@@ -411,6 +558,8 @@ export class UserStore {
|
|
|
411
558
|
group.workspaceRoots = normalizeStringList(patch.workspaceRoots);
|
|
412
559
|
if (patch.telegramChatIds !== undefined)
|
|
413
560
|
group.telegramChatIds = normalizeNumberList(patch.telegramChatIds);
|
|
561
|
+
if (patch.discordChannelIds !== undefined)
|
|
562
|
+
group.discordChannelIds = normalizeStringList(patch.discordChannelIds);
|
|
414
563
|
group.updatedAt = new Date().toISOString();
|
|
415
564
|
return group;
|
|
416
565
|
});
|
|
@@ -460,6 +609,38 @@ export class UserStore {
|
|
|
460
609
|
payload.telegramIdentities.push(identity);
|
|
461
610
|
return identity;
|
|
462
611
|
}
|
|
612
|
+
upsertDiscordIdentityInPayload(payload, userId, input) {
|
|
613
|
+
const discordUserId = normalizeDiscordId(input.discordUserId);
|
|
614
|
+
if (!discordUserId) {
|
|
615
|
+
throw new Error("Discord user id is required.");
|
|
616
|
+
}
|
|
617
|
+
const now = new Date().toISOString();
|
|
618
|
+
for (const identity of payload.discordIdentities) {
|
|
619
|
+
if (identity.discordUserId === discordUserId && identity.userId !== userId) {
|
|
620
|
+
identity.active = false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const existing = payload.discordIdentities.find((identity) => identity.userId === userId && identity.discordUserId === discordUserId);
|
|
624
|
+
if (existing) {
|
|
625
|
+
existing.username = input.username ?? existing.username;
|
|
626
|
+
existing.globalName = input.globalName ?? existing.globalName;
|
|
627
|
+
existing.active = true;
|
|
628
|
+
existing.updatedAt = now;
|
|
629
|
+
return existing;
|
|
630
|
+
}
|
|
631
|
+
const identity = {
|
|
632
|
+
id: randomId(),
|
|
633
|
+
userId,
|
|
634
|
+
discordUserId,
|
|
635
|
+
username: input.username,
|
|
636
|
+
globalName: input.globalName,
|
|
637
|
+
active: true,
|
|
638
|
+
linkedAt: now,
|
|
639
|
+
updatedAt: now,
|
|
640
|
+
};
|
|
641
|
+
payload.discordIdentities.push(identity);
|
|
642
|
+
return identity;
|
|
643
|
+
}
|
|
463
644
|
pruneExpiredSessionsInPayload(payload) {
|
|
464
645
|
const now = Date.now();
|
|
465
646
|
payload.webSessions = payload.webSessions.filter((session) => new Date(session.expiresAt).getTime() > now);
|
|
@@ -551,6 +732,7 @@ function normalizePayload(payload) {
|
|
|
551
732
|
agentIds: [],
|
|
552
733
|
workspaceRoots: [],
|
|
553
734
|
telegramChatIds: [],
|
|
735
|
+
discordChannelIds: [],
|
|
554
736
|
createdAt: now,
|
|
555
737
|
updatedAt: now,
|
|
556
738
|
});
|
|
@@ -565,6 +747,7 @@ function normalizePayload(payload) {
|
|
|
565
747
|
agentIds: normalizeStringList(group.agentIds),
|
|
566
748
|
workspaceRoots: normalizeStringList(group.workspaceRoots),
|
|
567
749
|
telegramChatIds: normalizeNumberList(group.telegramChatIds),
|
|
750
|
+
discordChannelIds: normalizeStringList(group.discordChannelIds),
|
|
568
751
|
});
|
|
569
752
|
}
|
|
570
753
|
const groups = Array.from(groupsById.values());
|
|
@@ -581,8 +764,14 @@ function normalizePayload(payload) {
|
|
|
581
764
|
...chat,
|
|
582
765
|
allowedGroupIds: chat.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
|
|
583
766
|
})),
|
|
767
|
+
discordIdentities: (payload?.discordIdentities ?? []).filter((item) => isDiscordIdentityRecord(item) && userIds.has(item.userId)),
|
|
768
|
+
discordChannels: (payload?.discordChannels ?? []).filter(isDiscordChannelAccessRecord).map((channel) => ({
|
|
769
|
+
...channel,
|
|
770
|
+
allowedGroupIds: channel.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
|
|
771
|
+
})),
|
|
584
772
|
webSessions: (payload?.webSessions ?? []).filter((item) => isWebSessionRecord(item) && userIds.has(item.userId)),
|
|
585
773
|
telegramLinkCodes: (payload?.telegramLinkCodes ?? []).filter((item) => isTelegramLinkCodeRecord(item) && userIds.has(item.userId)),
|
|
774
|
+
discordLinkCodes: (payload?.discordLinkCodes ?? []).filter((item) => isDiscordLinkCodeRecord(item) && userIds.has(item.userId)),
|
|
586
775
|
};
|
|
587
776
|
}
|
|
588
777
|
function normalizeEmail(email) {
|
|
@@ -619,6 +808,10 @@ function normalizeStringList(values) {
|
|
|
619
808
|
function normalizeNumberList(values) {
|
|
620
809
|
return Array.from(new Set((values ?? []).filter((value) => Number.isInteger(value))));
|
|
621
810
|
}
|
|
811
|
+
function normalizeDiscordId(value) {
|
|
812
|
+
const normalized = String(value ?? "").trim();
|
|
813
|
+
return normalized || undefined;
|
|
814
|
+
}
|
|
622
815
|
function assertActiveAdminExists(payload) {
|
|
623
816
|
const hasAdmin = payload.users.some((user) => user.active && payload.userGroups.some((item) => item.userId === user.id && item.groupId === ADMIN_GROUP_ID));
|
|
624
817
|
if (!hasAdmin) {
|
|
@@ -696,6 +889,16 @@ function isTelegramChatAccessRecord(value) {
|
|
|
696
889
|
return Boolean(candidate) && typeof candidate.id === "string" && Number.isInteger(candidate.chatId) &&
|
|
697
890
|
typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
|
|
698
891
|
}
|
|
892
|
+
function isDiscordIdentityRecord(value) {
|
|
893
|
+
const candidate = value;
|
|
894
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
|
|
895
|
+
typeof candidate.discordUserId === "string" && typeof candidate.active === "boolean";
|
|
896
|
+
}
|
|
897
|
+
function isDiscordChannelAccessRecord(value) {
|
|
898
|
+
const candidate = value;
|
|
899
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.channelId === "string" &&
|
|
900
|
+
typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
|
|
901
|
+
}
|
|
699
902
|
function isWebSessionRecord(value) {
|
|
700
903
|
const candidate = value;
|
|
701
904
|
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
|
|
@@ -706,3 +909,8 @@ function isTelegramLinkCodeRecord(value) {
|
|
|
706
909
|
return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
|
|
707
910
|
typeof candidate.expiresAt === "string";
|
|
708
911
|
}
|
|
912
|
+
function isDiscordLinkCodeRecord(value) {
|
|
913
|
+
const candidate = value;
|
|
914
|
+
return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
|
|
915
|
+
typeof candidate.expiresAt === "string";
|
|
916
|
+
}
|
package/dist/web-api-contract.js
CHANGED
|
@@ -7,6 +7,11 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
7
7
|
exact("/api/snapshot", ["GET"], "inspect"),
|
|
8
8
|
exact("/api/tasks", ["GET"], "inspect"),
|
|
9
9
|
exact("/api/progress", ["GET"], "inspect"),
|
|
10
|
+
exact("/api/metrics", ["GET"], "inspect"),
|
|
11
|
+
exact("/api/jobs", ["GET"], "inspect"),
|
|
12
|
+
dynamic("/api/jobs/:id/log", "^/api/jobs/[^/]+/log$", ["GET"], "inspect", `/api/jobs/${stringToken}/log`),
|
|
13
|
+
dynamic("/api/jobs/:id/action", "^/api/jobs/[^/]+/action$", ["POST"], "inspect", `/api/jobs/${stringToken}/action`),
|
|
14
|
+
exact("/api/active-sessions", ["GET"], "sessions.read"),
|
|
10
15
|
exact("/api/version", ["GET"], "inspect"),
|
|
11
16
|
exact("/api/update", ["POST"], "updates.run"),
|
|
12
17
|
exact("/api/agent-updates", ["GET"], "updates.run"),
|
|
@@ -15,6 +20,14 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
15
20
|
dynamic("/api/agent-update/:id/input", "^/api/agent-update/[^/]+/input$", ["POST"], "updates.run", `/api/agent-update/${stringToken}/input`),
|
|
16
21
|
dynamic("/api/agent-update/:id/cancel", "^/api/agent-update/[^/]+/cancel$", ["POST"], "updates.run", `/api/agent-update/${stringToken}/cancel`),
|
|
17
22
|
exact("/api/adapters/health", ["GET"], "inspect"),
|
|
23
|
+
exact("/api/peers", ["GET", "POST"], readWrite("peers.read", "peers.write")),
|
|
24
|
+
exact("/api/peers/invite", ["POST"], "peers.write"),
|
|
25
|
+
exact("/api/peers/pair", ["POST"], "peers.write"),
|
|
26
|
+
exact("/api/peers/global-sessions", ["GET"], "sessions.read"),
|
|
27
|
+
dynamic("/api/peers/:id/health", "^/api/peers/[^/]+/health$", ["GET"], "peers.connect", `/api/peers/${stringToken}/health`),
|
|
28
|
+
dynamic("/api/peers/:id", "^/api/peers/[^/]+$", ["PATCH", "DELETE"], "peers.write", `/api/peers/${stringToken}`),
|
|
29
|
+
dynamic("/api/peers/:id/proxy", "^/api/peers/[^/]+/proxy$", ["POST"], "peers.connect", `/api/peers/${stringToken}/proxy`),
|
|
30
|
+
dynamic("/api/peers/:id/events", "^/api/peers/[^/]+/events$", ["GET"], "peers.connect", `/api/peers/${stringToken}/events`),
|
|
18
31
|
exact("/api/permissions", ["GET"], "users.read"),
|
|
19
32
|
exact("/api/users", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
20
33
|
dynamic("/api/users/:id", "^/api/users/[^/]+$", ["PATCH"], "users.write", `/api/users/${stringToken}`),
|
|
@@ -23,10 +36,14 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
23
36
|
dynamic("/api/users/:id/sessions/:sessionId", "^/api/users/[^/]+/sessions/[^/]+$", ["DELETE"], "users.write", `/api/users/${stringToken}/sessions/${stringToken}`),
|
|
24
37
|
dynamic("/api/users/:id/telegram", "^/api/users/[^/]+/telegram$", ["POST"], "users.write", `/api/users/${stringToken}/telegram`),
|
|
25
38
|
dynamic("/api/users/:id/telegram/:identityId", "^/api/users/[^/]+/telegram/[^/]+$", ["DELETE"], "users.write", `/api/users/${stringToken}/telegram/${stringToken}`),
|
|
39
|
+
dynamic("/api/users/:id/discord", "^/api/users/[^/]+/discord$", ["POST"], "users.write", `/api/users/${stringToken}/discord`),
|
|
40
|
+
dynamic("/api/users/:id/discord/:identityId", "^/api/users/[^/]+/discord/[^/]+$", ["DELETE"], "users.write", `/api/users/${stringToken}/discord/${stringToken}`),
|
|
26
41
|
exact("/api/groups", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
27
42
|
dynamic("/api/groups/:id", "^/api/groups/[^/]+$", ["PATCH"], "users.write", `/api/groups/${stringToken}`),
|
|
28
43
|
exact("/api/telegram-chats", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
29
44
|
dynamic("/api/telegram-chats/:id", "^/api/telegram-chats/[^/]+$", ["PATCH"], "users.write", `/api/telegram-chats/${stringToken}`),
|
|
45
|
+
exact("/api/discord-channels", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
46
|
+
dynamic("/api/discord-channels/:id", "^/api/discord-channels/[^/]+$", ["PATCH"], "users.write", `/api/discord-channels/${stringToken}`),
|
|
30
47
|
exact("/api/audit", ["GET"], "audit.read"),
|
|
31
48
|
exact("/api/locks", ["GET", "POST", "DELETE"], readWrite("sessions.read", "sessions.write")),
|
|
32
49
|
exact("/api/auth/status", ["GET"], "inspect"),
|
|
@@ -20,6 +20,7 @@ export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
|
20
20
|
groupIds: arrayStringField(body, "groupIds"),
|
|
21
21
|
active: optionalBooleanField(body, "active") ?? true,
|
|
22
22
|
telegramUserId: optionalNumberField(body, "telegramUserId"),
|
|
23
|
+
discordUserId: optionalStringField(body, "discordUserId"),
|
|
23
24
|
});
|
|
24
25
|
options.auditUserAction(authUser, "user_created", user.user.email);
|
|
25
26
|
sendJson(res, 201, { user: publicUser(user.user), groups: user.groups });
|
|
@@ -93,6 +94,33 @@ export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
|
93
94
|
sendJson(res, 200, { removed });
|
|
94
95
|
return true;
|
|
95
96
|
}
|
|
97
|
+
const discordLinkMatch = url.pathname.match(/^\/api\/users\/([^/]+)\/discord$/);
|
|
98
|
+
if (discordLinkMatch?.[1] && req.method === "POST") {
|
|
99
|
+
const body = await readJsonBody(req);
|
|
100
|
+
if (body.createCode === true) {
|
|
101
|
+
const userId = decodeURIComponent(discordLinkMatch[1]);
|
|
102
|
+
const linkCode = users.createDiscordLinkCode(userId);
|
|
103
|
+
options.auditUserAction(authUser, "discord_link_created", userId);
|
|
104
|
+
sendJson(res, 201, { linkCode });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const identity = users.linkDiscordUser(decodeURIComponent(discordLinkMatch[1]), {
|
|
108
|
+
discordUserId: stringField(body, "discordUserId"),
|
|
109
|
+
username: optionalStringField(body, "username"),
|
|
110
|
+
globalName: optionalStringField(body, "globalName"),
|
|
111
|
+
});
|
|
112
|
+
options.auditUserAction(authUser, "discord_linked", identity.discordUserId);
|
|
113
|
+
sendJson(res, 201, { identity });
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const discordUnlinkMatch = url.pathname.match(/^\/api\/users\/[^/]+\/discord\/([^/]+)$/);
|
|
117
|
+
if (discordUnlinkMatch?.[1] && req.method === "DELETE") {
|
|
118
|
+
const identityId = decodeURIComponent(discordUnlinkMatch[1]);
|
|
119
|
+
const removed = users.unlinkDiscordIdentity(identityId);
|
|
120
|
+
options.auditUserAction(authUser, "discord_unlinked", identityId);
|
|
121
|
+
sendJson(res, 200, { removed });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
96
124
|
if (req.method === "GET" && url.pathname === "/api/groups") {
|
|
97
125
|
sendJson(res, 200, { groups: users.listGroups() });
|
|
98
126
|
return true;
|
|
@@ -106,6 +134,7 @@ export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
|
106
134
|
agentIds: arrayStringField(body, "agentIds"),
|
|
107
135
|
workspaceRoots: arrayStringField(body, "workspaceRoots"),
|
|
108
136
|
telegramChatIds: arrayNumberField(body, "telegramChatIds"),
|
|
137
|
+
discordChannelIds: arrayStringField(body, "discordChannelIds"),
|
|
109
138
|
});
|
|
110
139
|
options.auditUserAction(authUser, "group_created", group.id);
|
|
111
140
|
sendJson(res, 201, { group });
|
|
@@ -121,6 +150,7 @@ export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
|
121
150
|
agentIds: body.agentIds === undefined ? undefined : arrayStringField(body, "agentIds"),
|
|
122
151
|
workspaceRoots: body.workspaceRoots === undefined ? undefined : arrayStringField(body, "workspaceRoots"),
|
|
123
152
|
telegramChatIds: body.telegramChatIds === undefined ? undefined : arrayNumberField(body, "telegramChatIds"),
|
|
153
|
+
discordChannelIds: body.discordChannelIds === undefined ? undefined : arrayStringField(body, "discordChannelIds"),
|
|
124
154
|
});
|
|
125
155
|
options.auditUserAction(authUser, "group_updated", group.id);
|
|
126
156
|
sendJson(res, 200, { group });
|
|
@@ -155,8 +185,51 @@ export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
|
155
185
|
sendJson(res, 200, { chat });
|
|
156
186
|
return true;
|
|
157
187
|
}
|
|
188
|
+
if (req.method === "GET" && url.pathname === "/api/discord-channels") {
|
|
189
|
+
sendJson(res, 200, { channels: users.snapshot().discordChannels });
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (req.method === "POST" && url.pathname === "/api/discord-channels") {
|
|
193
|
+
const body = await readJsonBody(req);
|
|
194
|
+
const channel = users.registerDiscordChannel({
|
|
195
|
+
guildId: optionalStringField(body, "guildId"),
|
|
196
|
+
channelId: stringField(body, "channelId"),
|
|
197
|
+
title: optionalStringField(body, "title"),
|
|
198
|
+
type: optionalStringField(body, "type"),
|
|
199
|
+
enabled: optionalBooleanField(body, "enabled") ?? true,
|
|
200
|
+
allowedGroupIds: arrayStringField(body, "allowedGroupIds"),
|
|
201
|
+
});
|
|
202
|
+
options.auditUserAction(authUser, "discord_channel_updated", channel.channelId);
|
|
203
|
+
sendJson(res, 201, { channel });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
const discordChannelMatch = url.pathname.match(/^\/api\/discord-channels\/([^/]+)$/);
|
|
207
|
+
if (discordChannelMatch?.[1] && req.method === "PATCH") {
|
|
208
|
+
const body = await readJsonBody(req);
|
|
209
|
+
const channel = users.updateDiscordChannel(decodeURIComponent(discordChannelMatch[1]), {
|
|
210
|
+
enabled: optionalBooleanField(body, "enabled"),
|
|
211
|
+
title: optionalStringField(body, "title"),
|
|
212
|
+
allowedGroupIds: body.allowedGroupIds === undefined ? undefined : arrayStringField(body, "allowedGroupIds"),
|
|
213
|
+
});
|
|
214
|
+
options.auditUserAction(authUser, "discord_channel_updated", channel.channelId);
|
|
215
|
+
sendJson(res, 200, { channel });
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
158
218
|
if (req.method === "GET" && url.pathname === "/api/audit") {
|
|
159
|
-
sendJson(res, 200, {
|
|
219
|
+
sendJson(res, 200, {
|
|
220
|
+
events: runtime.audit({
|
|
221
|
+
limit: numberParam(url, "limit", 50),
|
|
222
|
+
channelId: (url.searchParams.get("channel") || "all"),
|
|
223
|
+
category: (url.searchParams.get("category") || "all"),
|
|
224
|
+
status: (url.searchParams.get("status") || "all"),
|
|
225
|
+
action: url.searchParams.get("action") || "all",
|
|
226
|
+
actor: url.searchParams.get("actor") || undefined,
|
|
227
|
+
agentId: url.searchParams.get("agent") || "all",
|
|
228
|
+
threadId: url.searchParams.get("thread") || undefined,
|
|
229
|
+
workspace: url.searchParams.get("workspace") || undefined,
|
|
230
|
+
since: url.searchParams.get("since") || undefined,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
160
233
|
return true;
|
|
161
234
|
}
|
|
162
235
|
return false;
|
|
@@ -8,7 +8,7 @@ export async function handleDashboardArtifactRoute(req, res, url, options) {
|
|
|
8
8
|
}
|
|
9
9
|
if (req.method === "DELETE" && url.pathname === "/api/artifacts") {
|
|
10
10
|
await options.assertCurrentSessionScope(authUser);
|
|
11
|
-
sendJson(res, 200, { removed: await runtime.deleteArtifact(requiredSearch(url, "turnId")) });
|
|
11
|
+
sendJson(res, 200, { removed: await runtime.deleteArtifact(requiredSearch(url, "turnId"), options.activityActor) });
|
|
12
12
|
return true;
|
|
13
13
|
}
|
|
14
14
|
if (req.method === "POST" && url.pathname === "/api/artifacts/bulk") {
|
|
@@ -21,7 +21,7 @@ export async function handleDashboardArtifactRoute(req, res, url, options) {
|
|
|
21
21
|
}
|
|
22
22
|
const removed = [];
|
|
23
23
|
for (const turnId of turnIds) {
|
|
24
|
-
if (await runtime.deleteArtifact(turnId)) {
|
|
24
|
+
if (await runtime.deleteArtifact(turnId, options.activityActor)) {
|
|
25
25
|
removed.push(turnId);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -30,7 +30,7 @@ export async function handleDashboardArtifactRoute(req, res, url, options) {
|
|
|
30
30
|
}
|
|
31
31
|
if (req.method === "GET" && url.pathname === "/api/artifacts/zip") {
|
|
32
32
|
await options.assertCurrentSessionScope(authUser);
|
|
33
|
-
const bundle = await runtime.createArtifactZip(requiredSearch(url, "turnId"));
|
|
33
|
+
const bundle = await runtime.createArtifactZip(requiredSearch(url, "turnId"), options.activityActor);
|
|
34
34
|
if (!bundle) {
|
|
35
35
|
sendJson(res, 404, { error: "Artifact turn not found or ZIP could not be created" });
|
|
36
36
|
return true;
|