@nordbyte/nordrelay 0.5.1 → 0.6.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 +65 -11
- package/README.md +97 -23
- package/dist/access-control.js +1 -0
- package/dist/activity-events.js +44 -0
- package/dist/agent-updates.js +18 -2
- package/dist/audit-log.js +40 -2
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +492 -7
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +34 -7
- package/dist/channel-command-service.js +156 -0
- package/dist/channel-turn-service.js +237 -0
- package/dist/codex-cli.js +1 -1
- package/dist/config-metadata.js +80 -13
- package/dist/config.js +77 -7
- package/dist/context-key.js +77 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2014 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +119 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +16 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +41 -0
- package/dist/operations.js +176 -119
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime.js +1003 -268
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/state-backend.js +3 -0
- package/dist/support-bundle.js +18 -1
- 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-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +2 -6
- package/dist/telegram-operational-commands.js +14 -6
- 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 +9 -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 +97 -13
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +149 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +358 -47
- package/package.json +3 -1
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +468 -22
|
@@ -48,12 +48,21 @@ export function registerTelegramQueueCommands(options) {
|
|
|
48
48
|
const minutes = Math.min(7 * 24 * 60, Math.max(1, Number(laterMatch[1])));
|
|
49
49
|
const text = laterMatch[2].trim();
|
|
50
50
|
const notBefore = Date.now() + minutes * 60 * 1000;
|
|
51
|
-
const item = promptStore.enqueue(contextKey,
|
|
51
|
+
const item = promptStore.enqueue(contextKey, {
|
|
52
|
+
...toPromptEnvelope(text),
|
|
53
|
+
activityActor: options.activityActor?.(ctx),
|
|
54
|
+
}, { notBefore });
|
|
52
55
|
const message = `Queued prompt ${item.id} for ${formatLocalDateTime(new Date(notBefore))}.`;
|
|
53
56
|
await safeReply(ctx, escapeHTML(message), {
|
|
54
57
|
fallbackText: message,
|
|
55
58
|
replyMarkup: createQueuedPromptCancelKeyboard(contextKey, item.id),
|
|
56
59
|
});
|
|
60
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
61
|
+
status: "queued",
|
|
62
|
+
type: "prompt_queued",
|
|
63
|
+
prompt: item.description,
|
|
64
|
+
detail: `Queued prompt ${item.id} for ${formatLocalDateTime(new Date(notBefore))}.`,
|
|
65
|
+
});
|
|
57
66
|
options.auditContext(ctx, contextKey, session, {
|
|
58
67
|
action: "prompt_queued",
|
|
59
68
|
status: "ok",
|
|
@@ -81,12 +90,22 @@ export function registerTelegramQueueCommands(options) {
|
|
|
81
90
|
const message = `Queue paused. ${promptStore.list(contextKey).length} queued.`;
|
|
82
91
|
await safeReply(ctx, escapeHTML(message), { fallbackText: message });
|
|
83
92
|
await options.updateQueueStatusMessage(contextKey, message);
|
|
93
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
94
|
+
status: "info",
|
|
95
|
+
type: "queue_pause",
|
|
96
|
+
detail: message,
|
|
97
|
+
});
|
|
84
98
|
return;
|
|
85
99
|
}
|
|
86
100
|
if (/^resume$/i.test(argument)) {
|
|
87
101
|
promptStore.resume(contextKey);
|
|
88
102
|
const message = `Queue resumed. ${promptStore.list(contextKey).length} queued.`;
|
|
89
103
|
await safeReply(ctx, escapeHTML(message), { fallbackText: message });
|
|
104
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
105
|
+
status: "info",
|
|
106
|
+
type: "queue_resume",
|
|
107
|
+
detail: message,
|
|
108
|
+
});
|
|
90
109
|
if (chatId) {
|
|
91
110
|
void options.drainQueuedPrompts(ctx, contextKey, chatId, session).catch((error) => {
|
|
92
111
|
console.error("Failed to drain queue after resume:", error);
|
|
@@ -110,6 +129,12 @@ export function registerTelegramQueueCommands(options) {
|
|
|
110
129
|
}
|
|
111
130
|
const message = `Moved queued prompt ${item.id} ${direction}.`;
|
|
112
131
|
await safeReply(ctx, escapeHTML(message), { fallbackText: message });
|
|
132
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
133
|
+
status: "info",
|
|
134
|
+
type: "queue_move",
|
|
135
|
+
prompt: item.description,
|
|
136
|
+
detail: message,
|
|
137
|
+
});
|
|
113
138
|
return;
|
|
114
139
|
}
|
|
115
140
|
const runMatch = argument.match(/^run\s+([a-z0-9]+)$/i);
|
|
@@ -123,6 +148,12 @@ export function registerTelegramQueueCommands(options) {
|
|
|
123
148
|
}
|
|
124
149
|
promptStore.enqueueFront(contextKey, item);
|
|
125
150
|
promptStore.resume(contextKey);
|
|
151
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
152
|
+
status: "info",
|
|
153
|
+
type: "queue_run",
|
|
154
|
+
prompt: item.description,
|
|
155
|
+
detail: `Queued prompt ${item.id} moved to next.`,
|
|
156
|
+
});
|
|
126
157
|
if (!chatId) {
|
|
127
158
|
return;
|
|
128
159
|
}
|
|
@@ -159,9 +190,15 @@ export function registerTelegramQueueCommands(options) {
|
|
|
159
190
|
if (!contextSession) {
|
|
160
191
|
return;
|
|
161
192
|
}
|
|
162
|
-
const
|
|
193
|
+
const { contextKey, session } = contextSession;
|
|
194
|
+
const count = promptStore.clear(contextKey);
|
|
163
195
|
const message = `Cleared ${count} queued prompt${count === 1 ? "" : "s"}.`;
|
|
164
196
|
await safeReply(ctx, escapeHTML(message), { fallbackText: message });
|
|
197
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
198
|
+
status: "info",
|
|
199
|
+
type: "queue_clear",
|
|
200
|
+
detail: message,
|
|
201
|
+
});
|
|
165
202
|
});
|
|
166
203
|
bot.command("cancel", async (ctx) => {
|
|
167
204
|
const contextSession = await options.getContextSession(ctx, { deferThreadStart: true });
|
|
@@ -176,7 +213,8 @@ export function registerTelegramQueueCommands(options) {
|
|
|
176
213
|
});
|
|
177
214
|
return;
|
|
178
215
|
}
|
|
179
|
-
const
|
|
216
|
+
const { contextKey, session } = contextSession;
|
|
217
|
+
const removed = promptStore.remove(contextKey, id);
|
|
180
218
|
if (!removed) {
|
|
181
219
|
await safeReply(ctx, escapeHTML(`No queued prompt found with id ${id}.`), {
|
|
182
220
|
fallbackText: `No queued prompt found with id ${id}.`,
|
|
@@ -186,6 +224,12 @@ export function registerTelegramQueueCommands(options) {
|
|
|
186
224
|
await safeReply(ctx, escapeHTML(`Cancelled queued prompt ${removed.id}.`), {
|
|
187
225
|
fallbackText: `Cancelled queued prompt ${removed.id}.`,
|
|
188
226
|
});
|
|
227
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
228
|
+
status: "aborted",
|
|
229
|
+
type: "queue_cancel",
|
|
230
|
+
prompt: removed.description,
|
|
231
|
+
detail: `Cancelled queued prompt ${removed.id}.`,
|
|
232
|
+
});
|
|
189
233
|
});
|
|
190
234
|
bot.callbackQuery(/^queue_(cancel|remove|top|up|down|run):(-?\d+(?::\d+)?):([a-z0-9]+)$/, async (ctx) => {
|
|
191
235
|
const action = ctx.match?.[1];
|
|
@@ -209,6 +253,15 @@ export function registerTelegramQueueCommands(options) {
|
|
|
209
253
|
? promptStore.moveUp(contextKey, queueId)
|
|
210
254
|
: promptStore.moveDown(contextKey, queueId);
|
|
211
255
|
await ctx.answerCallbackQuery({ text: item ? `Moved ${queueId} ${action}.` : "Queued prompt not found." });
|
|
256
|
+
const session = item ? options.getSession(contextKey) : undefined;
|
|
257
|
+
if (item && session) {
|
|
258
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
259
|
+
status: "info",
|
|
260
|
+
type: "queue_move",
|
|
261
|
+
prompt: item.description,
|
|
262
|
+
detail: `Moved queued prompt ${item.id} ${action}.`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
212
265
|
if (chatId && messageId) {
|
|
213
266
|
const rendered = renderQueueList(promptStore, contextKey, promptStore.list(contextKey));
|
|
214
267
|
await safeEditMessage(bot, chatId, messageId, rendered.html, {
|
|
@@ -227,6 +280,15 @@ export function registerTelegramQueueCommands(options) {
|
|
|
227
280
|
promptStore.enqueueFront(contextKey, item);
|
|
228
281
|
promptStore.resume(contextKey);
|
|
229
282
|
await ctx.answerCallbackQuery({ text: `Queued prompt ${queueId} moved to next.` });
|
|
283
|
+
const session = options.getSession(contextKey);
|
|
284
|
+
if (session) {
|
|
285
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
286
|
+
status: "info",
|
|
287
|
+
type: "queue_run",
|
|
288
|
+
prompt: item.description,
|
|
289
|
+
detail: `Queued prompt ${item.id} moved to next.`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
230
292
|
if (chatId && messageId) {
|
|
231
293
|
const rendered = renderQueueList(promptStore, contextKey, promptStore.list(contextKey));
|
|
232
294
|
await safeEditMessage(bot, chatId, messageId, rendered.html, {
|
|
@@ -234,7 +296,6 @@ export function registerTelegramQueueCommands(options) {
|
|
|
234
296
|
replyMarkup: rendered.keyboard,
|
|
235
297
|
});
|
|
236
298
|
}
|
|
237
|
-
const session = options.getSession(contextKey);
|
|
238
299
|
if (chatId && session && !options.getBusyReason(contextKey).busy) {
|
|
239
300
|
void options.drainQueuedPrompts(ctx, contextKey, chatId, session).catch((error) => {
|
|
240
301
|
console.error("Failed to drain queue after run-now callback:", error);
|
|
@@ -262,6 +323,15 @@ export function registerTelegramQueueCommands(options) {
|
|
|
262
323
|
}
|
|
263
324
|
const message = `Cancelled queued prompt ${removed.id}.`;
|
|
264
325
|
await ctx.answerCallbackQuery({ text: message });
|
|
326
|
+
const session = options.getSession(contextKey);
|
|
327
|
+
if (session) {
|
|
328
|
+
options.appendActivity?.(ctx, contextKey, session, {
|
|
329
|
+
status: "aborted",
|
|
330
|
+
type: "queue_cancel",
|
|
331
|
+
prompt: removed.description,
|
|
332
|
+
detail: message,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
265
335
|
if (!chatId || !messageId) {
|
|
266
336
|
return;
|
|
267
337
|
}
|
|
@@ -26,6 +26,13 @@ export function registerTelegramSupportCommands(options) {
|
|
|
26
26
|
action: "command",
|
|
27
27
|
status: "ok",
|
|
28
28
|
contextKey,
|
|
29
|
+
actor: {
|
|
30
|
+
channel: "telegram",
|
|
31
|
+
id: ctx.from?.id !== undefined ? `telegram:${ctx.from.id}` : undefined,
|
|
32
|
+
label: ctx.from?.username || ctx.from?.first_name || (ctx.from?.id !== undefined ? String(ctx.from.id) : undefined),
|
|
33
|
+
username: ctx.from?.username,
|
|
34
|
+
channelUserId: ctx.from?.id !== undefined ? String(ctx.from.id) : undefined,
|
|
35
|
+
},
|
|
29
36
|
actorId: ctx.from?.id,
|
|
30
37
|
actorRole: options.getUserRole(ctx),
|
|
31
38
|
description: "export diagnostics bundle",
|
|
@@ -33,12 +33,26 @@ export function registerTelegramUpdateCommands(deps) {
|
|
|
33
33
|
}
|
|
34
34
|
if (subcommand === "cancel" && tokens[1]) {
|
|
35
35
|
const job = agentUpdates.cancel(tokens[1]);
|
|
36
|
+
deps.appendActivity?.(ctx, {
|
|
37
|
+
status: "aborted",
|
|
38
|
+
type: "agent_update_cancel_requested",
|
|
39
|
+
threadId: null,
|
|
40
|
+
agentId: job.agentId,
|
|
41
|
+
detail: `${job.agentLabel} ${job.operation} cancellation requested.`,
|
|
42
|
+
});
|
|
36
43
|
const rendered = renderAgentUpdateJobAction(job);
|
|
37
44
|
await replyChannelAction(ctx, rendered);
|
|
38
45
|
return;
|
|
39
46
|
}
|
|
40
47
|
if ((subcommand === "input" || subcommand === "send") && tokens[1] && tokens.slice(2).join(" ").trim()) {
|
|
41
48
|
const job = agentUpdates.sendInput(tokens[1], tokens.slice(2).join(" "));
|
|
49
|
+
deps.appendActivity?.(ctx, {
|
|
50
|
+
status: "info",
|
|
51
|
+
type: "agent_update_input_sent",
|
|
52
|
+
threadId: null,
|
|
53
|
+
agentId: job.agentId,
|
|
54
|
+
detail: `Input sent to ${job.agentLabel} ${job.operation}.`,
|
|
55
|
+
});
|
|
42
56
|
const rendered = renderAgentUpdateJobAction(job);
|
|
43
57
|
await replyChannelAction(ctx, rendered);
|
|
44
58
|
return;
|
|
@@ -54,6 +68,12 @@ export function registerTelegramUpdateCommands(deps) {
|
|
|
54
68
|
return;
|
|
55
69
|
}
|
|
56
70
|
const update = spawnSelfUpdate();
|
|
71
|
+
deps.appendActivity?.(ctx, {
|
|
72
|
+
status: "info",
|
|
73
|
+
type: "update_started",
|
|
74
|
+
threadId: null,
|
|
75
|
+
detail: `${update.method}: ${update.summary}`,
|
|
76
|
+
});
|
|
57
77
|
const rendered = renderSelfUpdateStartedAction(update);
|
|
58
78
|
await replyChannelAction(ctx, rendered);
|
|
59
79
|
});
|
|
@@ -87,6 +107,13 @@ export function registerTelegramUpdateCommands(deps) {
|
|
|
87
107
|
return;
|
|
88
108
|
}
|
|
89
109
|
const job = agentUpdates.cancel(id);
|
|
110
|
+
deps.appendActivity?.(ctx, {
|
|
111
|
+
status: "aborted",
|
|
112
|
+
type: "agent_update_cancel_requested",
|
|
113
|
+
threadId: null,
|
|
114
|
+
agentId: job.agentId,
|
|
115
|
+
detail: `${job.agentLabel} ${job.operation} cancellation requested.`,
|
|
116
|
+
});
|
|
90
117
|
const rendered = renderAgentUpdateJobAction(job);
|
|
91
118
|
await replyChannelAction(ctx, rendered);
|
|
92
119
|
});
|
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"),
|
|
@@ -23,10 +28,14 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
23
28
|
dynamic("/api/users/:id/sessions/:sessionId", "^/api/users/[^/]+/sessions/[^/]+$", ["DELETE"], "users.write", `/api/users/${stringToken}/sessions/${stringToken}`),
|
|
24
29
|
dynamic("/api/users/:id/telegram", "^/api/users/[^/]+/telegram$", ["POST"], "users.write", `/api/users/${stringToken}/telegram`),
|
|
25
30
|
dynamic("/api/users/:id/telegram/:identityId", "^/api/users/[^/]+/telegram/[^/]+$", ["DELETE"], "users.write", `/api/users/${stringToken}/telegram/${stringToken}`),
|
|
31
|
+
dynamic("/api/users/:id/discord", "^/api/users/[^/]+/discord$", ["POST"], "users.write", `/api/users/${stringToken}/discord`),
|
|
32
|
+
dynamic("/api/users/:id/discord/:identityId", "^/api/users/[^/]+/discord/[^/]+$", ["DELETE"], "users.write", `/api/users/${stringToken}/discord/${stringToken}`),
|
|
26
33
|
exact("/api/groups", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
27
34
|
dynamic("/api/groups/:id", "^/api/groups/[^/]+$", ["PATCH"], "users.write", `/api/groups/${stringToken}`),
|
|
28
35
|
exact("/api/telegram-chats", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
29
36
|
dynamic("/api/telegram-chats/:id", "^/api/telegram-chats/[^/]+$", ["PATCH"], "users.write", `/api/telegram-chats/${stringToken}`),
|
|
37
|
+
exact("/api/discord-channels", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
38
|
+
dynamic("/api/discord-channels/:id", "^/api/discord-channels/[^/]+$", ["PATCH"], "users.write", `/api/discord-channels/${stringToken}`),
|
|
30
39
|
exact("/api/audit", ["GET"], "audit.read"),
|
|
31
40
|
exact("/api/locks", ["GET", "POST", "DELETE"], readWrite("sessions.read", "sessions.write")),
|
|
32
41
|
exact("/api/auth/status", ["GET"], "inspect"),
|