@0xmaxma/claude-gateway 1.3.23 → 1.3.25
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/README.md +125 -49
- package/dist/api/line-pending-senders.d.ts +7 -2
- package/dist/api/line-pending-senders.d.ts.map +1 -1
- package/dist/api/line-pending-senders.js +7 -2
- package/dist/api/line-pending-senders.js.map +1 -1
- package/dist/api/packages.d.ts.map +1 -1
- package/dist/api/packages.js +92 -16
- package/dist/api/packages.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +563 -172
- package/dist/api/router.js.map +1 -1
- package/dist/api/wizard-state.d.ts +1 -4
- package/dist/api/wizard-state.d.ts.map +1 -1
- package/dist/api/wizard-state.js.map +1 -1
- package/dist/discord/receiver.d.ts +1 -0
- package/dist/discord/receiver.d.ts.map +1 -1
- package/dist/discord/receiver.js +24 -6
- package/dist/discord/receiver.js.map +1 -1
- package/dist/telegram/receiver.d.ts +1 -0
- package/dist/telegram/receiver.d.ts.map +1 -1
- package/dist/telegram/receiver.js +18 -6
- package/dist/telegram/receiver.js.map +1 -1
- package/mcp/tools/discord/access.ts +136 -20
- package/mcp/tools/discord/client.ts +3 -1
- package/mcp/tools/discord/module.ts +75 -4
- package/mcp/tools/discord/skills/access/SKILL.md +66 -8
- package/mcp/tools/discord/skills/configure/SKILL.md +11 -1
- package/mcp/tools/discord/types.ts +21 -2
- package/mcp/tools/telegram/dedup.ts +4 -1
- package/mcp/tools/telegram/module.ts +5 -14
- package/mcp/tools/telegram/pure.ts +169 -31
- package/mcp/tools/telegram/receiver-server.ts +108 -78
- package/mcp/tools/telegram/skills/access/SKILL.md +93 -23
- package/mcp/tools/telegram/skills/configure/SKILL.md +31 -26
- package/package.json +1 -1
package/dist/api/router.js
CHANGED
|
@@ -150,35 +150,6 @@ async function verifyTelegramToken(token) {
|
|
|
150
150
|
return null;
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Non-blocking Telegram getUpdates check.
|
|
155
|
-
* Returns match details + nextOffset on code match, { nextOffset } on no match, null on error.
|
|
156
|
-
* Always advancing the offset ensures we never re-process seen messages across poll calls.
|
|
157
|
-
*/
|
|
158
|
-
async function checkTelegramCode(token, expectedCode, offset) {
|
|
159
|
-
try {
|
|
160
|
-
const url = `${TELEGRAM_API_BASE}/bot${token}/getUpdates?offset=${offset}&timeout=0&limit=100`;
|
|
161
|
-
const res = await fetch(url);
|
|
162
|
-
const data = await res.json();
|
|
163
|
-
if (!data.ok)
|
|
164
|
-
return null;
|
|
165
|
-
let nextOffset = offset;
|
|
166
|
-
for (const upd of data.result) {
|
|
167
|
-
nextOffset = upd.update_id + 1;
|
|
168
|
-
if (upd.message?.chat.type === 'private' &&
|
|
169
|
-
upd.message.text?.trim().toUpperCase() === expectedCode.toUpperCase()) {
|
|
170
|
-
const chatId = String(upd.message.chat.id);
|
|
171
|
-
const senderId = upd.message.from ? String(upd.message.from.id) : chatId;
|
|
172
|
-
return { found: true, chatId, senderId, nextOffset };
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return { found: false, nextOffset };
|
|
176
|
-
}
|
|
177
|
-
catch (err) {
|
|
178
|
-
console.error('[wizard/verify] getUpdates failed:', err.message);
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
153
|
// ---------------------------------------------------------------------------
|
|
183
154
|
// In-memory rate limiter for media uploads (per API key)
|
|
184
155
|
// ---------------------------------------------------------------------------
|
|
@@ -506,6 +477,18 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
506
477
|
telegram_token_preview: cfg.telegram?.botToken ? maskToken(cfg.telegram.botToken) : null,
|
|
507
478
|
discord_token_preview: cfg.discord?.botToken ? maskToken(cfg.discord.botToken) : null,
|
|
508
479
|
telegram_dm_policy: cfg.telegram?.botToken ? readTelegramAccess(id).dmPolicy : null,
|
|
480
|
+
// Orthogonal pairing toggle (mirrors line_pairing below). Absent ⇒ on.
|
|
481
|
+
telegram_pairing: cfg.telegram?.botToken ? readTelegramAccess(id).pairing : null,
|
|
482
|
+
// Group tier (mirrors line_group_* below). Null when Telegram not connected.
|
|
483
|
+
telegram_group_policy: cfg.telegram?.botToken ? readTelegramAccess(id).groupPolicy : null,
|
|
484
|
+
telegram_group_allowlist: cfg.telegram?.botToken ? readTelegramAccess(id).groupAllowlist : null,
|
|
485
|
+
telegram_require_mention: cfg.telegram?.botToken ? readTelegramAccess(id).requireMention : null,
|
|
486
|
+
discord_dm_policy: cfg.discord?.botToken ? readDiscordAccess(id).dmPolicy : null,
|
|
487
|
+
discord_pairing: cfg.discord?.botToken ? readDiscordAccess(id).pairing : null,
|
|
488
|
+
// Discord approves at guild level — guildAllowlist IS the group allowlist.
|
|
489
|
+
discord_group_policy: cfg.discord?.botToken ? readDiscordAccess(id).groupPolicy : null,
|
|
490
|
+
discord_guild_allowlist: cfg.discord?.botToken ? readDiscordAccess(id).guildAllowlist : null,
|
|
491
|
+
discord_require_mention: cfg.discord?.botToken ? readDiscordAccess(id).requireMention : null,
|
|
509
492
|
line_connected: !!cfg.line?.channelSecret,
|
|
510
493
|
line_token_preview: cfg.line?.channelAccessToken ? maskToken(cfg.line.channelAccessToken) : null,
|
|
511
494
|
line_webhook_path: cfg.line?.channelSecret ? `/webhooks/line/${id}` : null,
|
|
@@ -668,15 +651,54 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
668
651
|
try {
|
|
669
652
|
const raw = fs.readFileSync(accessFile, 'utf8');
|
|
670
653
|
const parsed = JSON.parse(raw);
|
|
654
|
+
// Migrate the legacy 4-value dmPolicy (pairing folded in) to the split
|
|
655
|
+
// model. SECURITY: a legacy 'allowlist' file was locked down → pairing:false
|
|
656
|
+
// (an absent pairing on an existing file means pre-split); 'pairing' → mint on.
|
|
657
|
+
// Mirrors migrateAccess() in mcp/tools/telegram/pure.ts — keep in sync.
|
|
658
|
+
const legacy = parsed.dmPolicy;
|
|
659
|
+
let dmPolicy;
|
|
660
|
+
let pairing;
|
|
661
|
+
if (legacy === 'pairing') {
|
|
662
|
+
dmPolicy = 'allowlist';
|
|
663
|
+
pairing = true;
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
dmPolicy = legacy ?? 'allowlist';
|
|
667
|
+
pairing = parsed.pairing ?? false;
|
|
668
|
+
}
|
|
669
|
+
// Group tier: flatten legacy per-group `groups` map to a flat allowlist,
|
|
670
|
+
// behavior-preserving (groups were closed-by-default). Mirrors pure.ts.
|
|
671
|
+
// A group's per-user `allowFrom` override has no flat-model equivalent,
|
|
672
|
+
// but it's a real restriction — preserve it (mirrors
|
|
673
|
+
// deriveLegacyGroupAllowFrom in pure.ts, keep in sync) rather than
|
|
674
|
+
// silently dropping it and widening the group to every member.
|
|
675
|
+
const groupAllowlist = parsed.groupAllowlist ?? Object.keys(parsed.groups ?? {});
|
|
676
|
+
const groupPolicy = parsed.groupPolicy ?? 'allowlist';
|
|
677
|
+
const requireMention = parsed.requireMention ?? true;
|
|
678
|
+
let legacyGroupAllowFrom = parsed.legacyGroupAllowFrom;
|
|
679
|
+
if (!legacyGroupAllowFrom && parsed.groups) {
|
|
680
|
+
const derived = {};
|
|
681
|
+
for (const [groupId, g] of Object.entries(parsed.groups)) {
|
|
682
|
+
if (g.allowFrom && g.allowFrom.length > 0)
|
|
683
|
+
derived[groupId] = [...g.allowFrom];
|
|
684
|
+
}
|
|
685
|
+
if (Object.keys(derived).length > 0)
|
|
686
|
+
legacyGroupAllowFrom = derived;
|
|
687
|
+
}
|
|
671
688
|
return {
|
|
672
|
-
dmPolicy
|
|
689
|
+
dmPolicy,
|
|
690
|
+
pairing,
|
|
673
691
|
allowFrom: parsed.allowFrom ?? [],
|
|
674
|
-
|
|
692
|
+
groupPolicy,
|
|
693
|
+
groupAllowlist,
|
|
694
|
+
requireMention,
|
|
695
|
+
legacyGroupAllowFrom,
|
|
675
696
|
pending: parsed.pending ?? {},
|
|
676
697
|
};
|
|
677
698
|
}
|
|
678
699
|
catch {
|
|
679
|
-
|
|
700
|
+
// Brand-new agent (no file): closed base + pairing on (capture owner id).
|
|
701
|
+
return { dmPolicy: 'allowlist', pairing: true, allowFrom: [], groupPolicy: 'allowlist', groupAllowlist: [], requireMention: true, pending: {} };
|
|
680
702
|
}
|
|
681
703
|
}
|
|
682
704
|
function writeTelegramAccess(agentId, access) {
|
|
@@ -689,6 +711,70 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
689
711
|
throw new Error(`Failed to write Telegram access config: ${err.message}`);
|
|
690
712
|
}
|
|
691
713
|
}
|
|
714
|
+
function getDiscordStateDir(agentId) {
|
|
715
|
+
const agentsBase = getAgentsBaseDir();
|
|
716
|
+
const cfg = agentConfigs.get(agentId);
|
|
717
|
+
const workspace = cfg?.workspace
|
|
718
|
+
? (cfg.workspace.startsWith('~') ? path.join(os.homedir(), cfg.workspace.slice(1)) : cfg.workspace)
|
|
719
|
+
: path.join(agentsBase, agentId, 'workspace');
|
|
720
|
+
return path.join(workspace, '.discord-state');
|
|
721
|
+
}
|
|
722
|
+
function readDiscordAccess(agentId) {
|
|
723
|
+
const accessFile = path.join(getDiscordStateDir(agentId), 'access.json');
|
|
724
|
+
try {
|
|
725
|
+
const raw = fs.readFileSync(accessFile, 'utf8');
|
|
726
|
+
const parsed = JSON.parse(raw);
|
|
727
|
+
// Migrate the legacy fused dmPolicy ('pairing' folded pairing in) to the
|
|
728
|
+
// split model. SECURITY: a legacy 'allowlist' file was locked down →
|
|
729
|
+
// pairing:false (an absent pairing on an existing file means pre-split);
|
|
730
|
+
// 'pairing' → mint on. Mirrors migrateAccess() in
|
|
731
|
+
// mcp/tools/discord/access.ts — keep in sync.
|
|
732
|
+
const legacy = parsed.dmPolicy;
|
|
733
|
+
let dmPolicy;
|
|
734
|
+
let pairing;
|
|
735
|
+
if (legacy === 'pairing') {
|
|
736
|
+
dmPolicy = 'allowlist';
|
|
737
|
+
pairing = true;
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
dmPolicy = legacy ?? 'allowlist';
|
|
741
|
+
pairing = parsed.pairing ?? false;
|
|
742
|
+
}
|
|
743
|
+
// Guild tier migration is behavior-preserving: today an empty
|
|
744
|
+
// guildAllowlist means "deliver to all guilds" → derive 'open' when empty,
|
|
745
|
+
// 'allowlist' when non-empty; requireMention defaults false for existing
|
|
746
|
+
// files (no prior mention gate). Mirrors discord/access.ts migrateAccess.
|
|
747
|
+
const guildAllowlist = parsed.guildAllowlist ?? [];
|
|
748
|
+
const groupPolicy = parsed.groupPolicy
|
|
749
|
+
?? (guildAllowlist.length > 0 ? 'allowlist' : 'open');
|
|
750
|
+
const requireMention = parsed.requireMention ?? false;
|
|
751
|
+
return {
|
|
752
|
+
dmPolicy,
|
|
753
|
+
pairing,
|
|
754
|
+
allowFrom: parsed.allowFrom ?? [],
|
|
755
|
+
groupPolicy,
|
|
756
|
+
requireMention,
|
|
757
|
+
guildAllowlist,
|
|
758
|
+
channelAllowlist: parsed.channelAllowlist ?? [],
|
|
759
|
+
roleAllowlist: parsed.roleAllowlist ?? [],
|
|
760
|
+
pending: parsed.pending ?? {},
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
// Brand-new agent (no file): secure defaults (mirrors defaultAccess()).
|
|
765
|
+
return { dmPolicy: 'allowlist', pairing: true, allowFrom: [], groupPolicy: 'allowlist', requireMention: true, guildAllowlist: [], channelAllowlist: [], roleAllowlist: [], pending: {} };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function writeDiscordAccess(agentId, access) {
|
|
769
|
+
const stateDir = getDiscordStateDir(agentId);
|
|
770
|
+
try {
|
|
771
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
772
|
+
fs.writeFileSync(path.join(stateDir, 'access.json'), JSON.stringify(access, null, 2));
|
|
773
|
+
}
|
|
774
|
+
catch (err) {
|
|
775
|
+
throw new Error(`Failed to write Discord access config: ${err.message}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
692
778
|
/**
|
|
693
779
|
* POST /api/v1/agents/wizard/start
|
|
694
780
|
* Start wizard: call Claude to generate workspace files, return wizardId + preview.
|
|
@@ -988,101 +1074,68 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
988
1074
|
return;
|
|
989
1075
|
}
|
|
990
1076
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
botToken,
|
|
996
|
-
pairingCode,
|
|
997
|
-
updateOffset: 0,
|
|
998
|
-
});
|
|
999
|
-
res.json({
|
|
1000
|
-
channel,
|
|
1001
|
-
botName,
|
|
1002
|
-
pairingCode,
|
|
1003
|
-
instruction: `Send this code as a DM to ${botName} to complete pairing`,
|
|
1004
|
-
});
|
|
1005
|
-
});
|
|
1006
|
-
/**
|
|
1007
|
-
* POST /api/v1/agents/wizard/:wizardId/channel/verify
|
|
1008
|
-
* Poll for pairing code. Client polls this endpoint until { success: true }.
|
|
1009
|
-
*/
|
|
1010
|
-
router.post('/v1/agents/wizard/:wizardId/channel/verify', auth, async (req, res) => {
|
|
1011
|
-
const apiKey = req.apiKey;
|
|
1012
|
-
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1013
|
-
res.status(403).json({ error: 'Admin key required' });
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1077
|
+
// Token-only connect: persist the bot token to the agent config now. The
|
|
1078
|
+
// wizard no longer mints/relays a pairing code — pairing is incoming-first
|
|
1079
|
+
// and happens later on the edit-page Channels card (user DMs the bot → code
|
|
1080
|
+
// lands in Pending → admin approves). This mirrors the LINE flow.
|
|
1016
1081
|
if (!configPath) {
|
|
1017
1082
|
res.status(501).json({ error: 'Agent management not available (no configPath)' });
|
|
1018
1083
|
return;
|
|
1019
1084
|
}
|
|
1020
|
-
const { wizardId } = req.params;
|
|
1021
|
-
const wizard = wizard_state_1.wizardStore.get(wizardId);
|
|
1022
|
-
if (!wizard) {
|
|
1023
|
-
res.status(404).json({ error: 'Wizard not found or expired' });
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
if (wizard.step !== 'pairing') {
|
|
1027
|
-
res.status(409).json({ error: `Expected step 'pairing', got '${wizard.step}'` });
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
if (wizard.channel !== 'telegram') {
|
|
1031
|
-
res.status(501).json({ error: 'Discord pairing verification via API is not yet supported' });
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
const result = await checkTelegramCode(wizard.botToken, wizard.pairingCode, wizard.updateOffset ?? 0);
|
|
1035
|
-
// Always advance offset on non-error responses to avoid re-processing seen messages
|
|
1036
|
-
if (!result) {
|
|
1037
|
-
// Network/API error — keep current offset; client may retry
|
|
1038
|
-
res.json({ success: false, pending: true });
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
if (!result.found) {
|
|
1042
|
-
wizard_state_1.wizardStore.update(wizardId, { updateOffset: result.nextOffset });
|
|
1043
|
-
res.json({ success: false, pending: true });
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
// Code matched — commit config first, then advance offset so a retry can still succeed
|
|
1047
|
-
// if the config write failed mid-way
|
|
1048
1085
|
try {
|
|
1049
1086
|
await writeAgentsToConfig(configPath, (agents) => {
|
|
1050
1087
|
const agent = agents.find((a) => a.id === wizard.agentId);
|
|
1051
|
-
if (agent)
|
|
1052
|
-
|
|
1088
|
+
if (agent) {
|
|
1089
|
+
if (channel === 'telegram')
|
|
1090
|
+
agent.telegram = { botToken };
|
|
1091
|
+
else
|
|
1092
|
+
agent.discord = { botToken };
|
|
1093
|
+
}
|
|
1053
1094
|
});
|
|
1054
1095
|
}
|
|
1055
1096
|
catch (err) {
|
|
1056
1097
|
res.status(500).json({ error: `Failed to update config: ${err.message}` });
|
|
1057
1098
|
return;
|
|
1058
1099
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1100
|
+
// Seed a secure access.json immediately for a brand-new Discord connection.
|
|
1101
|
+
// Without this, the receiver's env-derived fallback (DISCORD_GUILD_ALLOWLIST
|
|
1102
|
+
// empty ⇒ groupPolicy 'open') answers in any server with no pairing/approval —
|
|
1103
|
+
// only DMs were ever gated. Skip if a file already exists (don't clobber a
|
|
1104
|
+
// prior connect/reconnect).
|
|
1105
|
+
if (channel === 'discord') {
|
|
1106
|
+
const accessFile = path.join(getDiscordStateDir(wizard.agentId), 'access.json');
|
|
1107
|
+
if (!fs.existsSync(accessFile)) {
|
|
1108
|
+
try {
|
|
1109
|
+
writeDiscordAccess(wizard.agentId, {
|
|
1110
|
+
dmPolicy: 'allowlist', pairing: true, allowFrom: [],
|
|
1111
|
+
groupPolicy: 'allowlist', requireMention: true,
|
|
1112
|
+
guildAllowlist: [], channelAllowlist: [], roleAllowlist: [], pending: {},
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
catch { /* non-fatal — receiver still has a (less safe) env fallback */ }
|
|
1116
|
+
}
|
|
1076
1117
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1118
|
+
wizard_state_1.wizardStore.update(wizardId, {
|
|
1119
|
+
step: 'complete',
|
|
1120
|
+
channel: channel,
|
|
1121
|
+
botToken,
|
|
1122
|
+
});
|
|
1123
|
+
// Hot-start the receiver so the bot comes online immediately without a
|
|
1124
|
+
// gateway restart. The first DM from the owner then produces a pairing code
|
|
1125
|
+
// they approve on the edit-page Channels card.
|
|
1079
1126
|
const runner = agentRunners.get(wizard.agentId);
|
|
1080
1127
|
if (runner) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1128
|
+
const base = runner.getAgentConfig();
|
|
1129
|
+
if (channel === 'telegram') {
|
|
1130
|
+
runner.updateAgentConfig({ ...base, telegram: { botToken } });
|
|
1131
|
+
runner.startTelegramReceiver();
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
runner.updateAgentConfig({ ...base, discord: { botToken } });
|
|
1135
|
+
runner.startDiscordReceiver();
|
|
1136
|
+
}
|
|
1083
1137
|
}
|
|
1084
|
-
|
|
1085
|
-
res.json({ success: true, agentId: wizard.agentId });
|
|
1138
|
+
res.json({ channel, botName, connected: true });
|
|
1086
1139
|
});
|
|
1087
1140
|
/**
|
|
1088
1141
|
* POST /api/v1/agents/wizard/:wizardId/complete
|
|
@@ -1342,7 +1395,24 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1342
1395
|
if (discord_bot_token !== undefined) {
|
|
1343
1396
|
const token = typeof discord_bot_token === 'string' ? discord_bot_token.trim() : null;
|
|
1344
1397
|
if (token) {
|
|
1398
|
+
const isNewConnection = !cfg.discord?.botToken;
|
|
1345
1399
|
cfg.discord = { ...(cfg.discord ?? {}), botToken: token };
|
|
1400
|
+
// Seed a secure access.json for a brand-new connection — see matching
|
|
1401
|
+
// comment on the wizard /channel handler for why this can't be left to
|
|
1402
|
+
// the receiver's env-derived fallback.
|
|
1403
|
+
if (isNewConnection) {
|
|
1404
|
+
const accessFile = path.join(getDiscordStateDir(agentId), 'access.json');
|
|
1405
|
+
if (!fs.existsSync(accessFile)) {
|
|
1406
|
+
try {
|
|
1407
|
+
writeDiscordAccess(agentId, {
|
|
1408
|
+
dmPolicy: 'allowlist', pairing: true, allowFrom: [],
|
|
1409
|
+
groupPolicy: 'allowlist', requireMention: true,
|
|
1410
|
+
guildAllowlist: [], channelAllowlist: [], roleAllowlist: [], pending: {},
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
catch { /* non-fatal — receiver still has a (less safe) env fallback */ }
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1346
1416
|
// Hot-start receiver if not already running
|
|
1347
1417
|
const runner = agentRunners.get(agentId);
|
|
1348
1418
|
if (runner) {
|
|
@@ -1429,6 +1499,15 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1429
1499
|
telegram_token_preview: cfg.telegram?.botToken ? maskToken(cfg.telegram.botToken) : null,
|
|
1430
1500
|
discord_token_preview: cfg.discord?.botToken ? maskToken(cfg.discord.botToken) : null,
|
|
1431
1501
|
telegram_dm_policy: cfg.telegram?.botToken ? readTelegramAccess(agentId).dmPolicy : null,
|
|
1502
|
+
telegram_pairing: cfg.telegram?.botToken ? readTelegramAccess(agentId).pairing : null,
|
|
1503
|
+
telegram_group_policy: cfg.telegram?.botToken ? readTelegramAccess(agentId).groupPolicy : null,
|
|
1504
|
+
telegram_group_allowlist: cfg.telegram?.botToken ? readTelegramAccess(agentId).groupAllowlist : null,
|
|
1505
|
+
telegram_require_mention: cfg.telegram?.botToken ? readTelegramAccess(agentId).requireMention : null,
|
|
1506
|
+
discord_dm_policy: cfg.discord?.botToken ? readDiscordAccess(agentId).dmPolicy : null,
|
|
1507
|
+
discord_pairing: cfg.discord?.botToken ? readDiscordAccess(agentId).pairing : null,
|
|
1508
|
+
discord_group_policy: cfg.discord?.botToken ? readDiscordAccess(agentId).groupPolicy : null,
|
|
1509
|
+
discord_guild_allowlist: cfg.discord?.botToken ? readDiscordAccess(agentId).guildAllowlist : null,
|
|
1510
|
+
discord_require_mention: cfg.discord?.botToken ? readDiscordAccess(agentId).requireMention : null,
|
|
1432
1511
|
line_connected: !!cfg.line?.channelSecret,
|
|
1433
1512
|
line_token_preview: cfg.line?.channelAccessToken ? maskToken(cfg.line.channelAccessToken) : null,
|
|
1434
1513
|
line_webhook_path: cfg.line?.channelSecret ? `/webhooks/line/${agentId}` : null,
|
|
@@ -1461,12 +1540,15 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1461
1540
|
catch { /* non-fatal cleanup */ }
|
|
1462
1541
|
}
|
|
1463
1542
|
const pending = Object.entries(access.pending)
|
|
1464
|
-
.map(([code, p]) => ({ code, senderId: p.senderId, chatId: p.chatId, createdAt: p.createdAt, expiresAt: p.expiresAt }));
|
|
1543
|
+
.map(([code, p]) => ({ code, senderId: p.senderId, chatId: p.chatId, createdAt: p.createdAt, expiresAt: p.expiresAt, kind: p.kind ?? 'dm' }));
|
|
1465
1544
|
res.json({ pending });
|
|
1466
1545
|
});
|
|
1467
1546
|
/**
|
|
1468
1547
|
* POST /api/v1/agents/:agentId/telegram/approve
|
|
1469
|
-
* Approve a pending Telegram pairing by code.
|
|
1548
|
+
* Approve a pending Telegram pairing by code. Kind-aware (mirrors LINE): a
|
|
1549
|
+
* 'group' knock moves its chatId into groupAllowlist (no approved/ handshake —
|
|
1550
|
+
* a group has no single recipient); a 'dm' knock allowlists the sender and
|
|
1551
|
+
* drops the approved/<senderId> file so the receiver sends a confirmation.
|
|
1470
1552
|
*/
|
|
1471
1553
|
router.post('/v1/agents/:agentId/telegram/approve', auth, (req, res) => {
|
|
1472
1554
|
const { agentId } = req.params;
|
|
@@ -1490,20 +1572,29 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1490
1572
|
res.status(404).json({ error: 'Pairing code not found or expired' });
|
|
1491
1573
|
return;
|
|
1492
1574
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1575
|
+
const isGroup = entry.kind === 'group';
|
|
1576
|
+
if (isGroup) {
|
|
1577
|
+
if (!access.groupAllowlist.includes(entry.chatId))
|
|
1578
|
+
access.groupAllowlist.push(entry.chatId);
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
if (!access.allowFrom.includes(entry.senderId))
|
|
1582
|
+
access.allowFrom.push(entry.senderId);
|
|
1583
|
+
}
|
|
1495
1584
|
delete access.pending[code];
|
|
1496
1585
|
try {
|
|
1497
1586
|
writeTelegramAccess(agentId, access);
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1587
|
+
if (!isGroup) {
|
|
1588
|
+
const approvedDir = path.join(getTelegramStateDir(agentId), 'approved');
|
|
1589
|
+
fs.mkdirSync(approvedDir, { recursive: true });
|
|
1590
|
+
fs.writeFileSync(path.join(approvedDir, entry.senderId), entry.chatId);
|
|
1591
|
+
}
|
|
1501
1592
|
}
|
|
1502
1593
|
catch (err) {
|
|
1503
1594
|
res.status(500).json({ error: `Failed to approve pairing: ${err.message}` });
|
|
1504
1595
|
return;
|
|
1505
1596
|
}
|
|
1506
|
-
res.json({ ok: true, senderId: entry.senderId });
|
|
1597
|
+
res.json({ ok: true, senderId: entry.senderId, groupId: isGroup ? entry.chatId : undefined });
|
|
1507
1598
|
});
|
|
1508
1599
|
/**
|
|
1509
1600
|
* POST /api/v1/agents/:agentId/telegram/deny
|
|
@@ -1541,10 +1632,13 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1541
1632
|
res.json({ ok: true });
|
|
1542
1633
|
});
|
|
1543
1634
|
/**
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1635
|
+
* PATCH /api/v1/agents/:agentId/telegram/policy
|
|
1636
|
+
* Update the Telegram DM policy, the orthogonal pairing toggle, the group
|
|
1637
|
+
* policy, and/or the group mention gate.
|
|
1638
|
+
* Body: { dmPolicy?, pairing?, groupPolicy?: 'open'|'allowlist'|'disabled', requireMention?: boolean }.
|
|
1639
|
+
* At least one field must be present; each is applied only if provided.
|
|
1546
1640
|
*/
|
|
1547
|
-
router.
|
|
1641
|
+
router.patch('/v1/agents/:agentId/telegram/policy', auth, (req, res) => {
|
|
1548
1642
|
const { agentId } = req.params;
|
|
1549
1643
|
const apiKey = req.apiKey;
|
|
1550
1644
|
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
@@ -1555,67 +1649,37 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1555
1649
|
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
1556
1650
|
return;
|
|
1557
1651
|
}
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
catch (err) {
|
|
1564
|
-
res.status(500).json({ error: `Failed to write sentinel: ${err.message}` });
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
|
-
res.json({ ok: true });
|
|
1568
|
-
});
|
|
1569
|
-
/**
|
|
1570
|
-
* GET /api/v1/agents/:agentId/telegram/pairing-status
|
|
1571
|
-
* Returns whether init-pairing sentinel is still active.
|
|
1572
|
-
*/
|
|
1573
|
-
router.get('/v1/agents/:agentId/telegram/pairing-status', auth, (req, res) => {
|
|
1574
|
-
const { agentId } = req.params;
|
|
1575
|
-
const apiKey = req.apiKey;
|
|
1576
|
-
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1577
|
-
res.status(403).json({ error: 'Admin key required' });
|
|
1652
|
+
const { dmPolicy, pairing, groupPolicy, requireMention } = req.body;
|
|
1653
|
+
const valid = ['open', 'allowlist', 'disabled'];
|
|
1654
|
+
if (dmPolicy !== undefined && !valid.includes(dmPolicy)) {
|
|
1655
|
+
res.status(400).json({ error: `dmPolicy must be one of: ${valid.join(', ')}` });
|
|
1578
1656
|
return;
|
|
1579
1657
|
}
|
|
1580
|
-
if (
|
|
1581
|
-
res.status(
|
|
1658
|
+
if (pairing !== undefined && typeof pairing !== 'boolean') {
|
|
1659
|
+
res.status(400).json({ error: 'pairing must be a boolean' });
|
|
1582
1660
|
return;
|
|
1583
1661
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
try {
|
|
1587
|
-
const stat = fs.statSync(sentinelPath);
|
|
1588
|
-
waiting = Date.now() - stat.mtimeMs < 10 * 60 * 1000;
|
|
1589
|
-
if (!waiting)
|
|
1590
|
-
fs.rmSync(sentinelPath, { force: true });
|
|
1591
|
-
}
|
|
1592
|
-
catch { /* ENOENT — not waiting */ }
|
|
1593
|
-
const access = readTelegramAccess(agentId);
|
|
1594
|
-
res.json({ waiting, allowFrom: access.allowFrom });
|
|
1595
|
-
});
|
|
1596
|
-
/**
|
|
1597
|
-
* PATCH /api/v1/agents/:agentId/telegram/policy
|
|
1598
|
-
* Update the Telegram DM policy for an agent.
|
|
1599
|
-
*/
|
|
1600
|
-
router.patch('/v1/agents/:agentId/telegram/policy', auth, (req, res) => {
|
|
1601
|
-
const { agentId } = req.params;
|
|
1602
|
-
const apiKey = req.apiKey;
|
|
1603
|
-
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1604
|
-
res.status(403).json({ error: 'Admin key required' });
|
|
1662
|
+
if (groupPolicy !== undefined && !valid.includes(groupPolicy)) {
|
|
1663
|
+
res.status(400).json({ error: `groupPolicy must be one of: ${valid.join(', ')}` });
|
|
1605
1664
|
return;
|
|
1606
1665
|
}
|
|
1607
|
-
if (
|
|
1608
|
-
res.status(
|
|
1666
|
+
if (requireMention !== undefined && typeof requireMention !== 'boolean') {
|
|
1667
|
+
res.status(400).json({ error: 'requireMention must be a boolean' });
|
|
1609
1668
|
return;
|
|
1610
1669
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
if (!dmPolicy || !valid.includes(dmPolicy)) {
|
|
1614
|
-
res.status(400).json({ error: `dmPolicy must be one of: ${valid.join(', ')}` });
|
|
1670
|
+
if (dmPolicy === undefined && pairing === undefined && groupPolicy === undefined && requireMention === undefined) {
|
|
1671
|
+
res.status(400).json({ error: 'provide dmPolicy, pairing, groupPolicy and/or requireMention' });
|
|
1615
1672
|
return;
|
|
1616
1673
|
}
|
|
1617
1674
|
const access = readTelegramAccess(agentId);
|
|
1618
|
-
|
|
1675
|
+
if (dmPolicy !== undefined)
|
|
1676
|
+
access.dmPolicy = dmPolicy;
|
|
1677
|
+
if (pairing !== undefined)
|
|
1678
|
+
access.pairing = pairing;
|
|
1679
|
+
if (groupPolicy !== undefined)
|
|
1680
|
+
access.groupPolicy = groupPolicy;
|
|
1681
|
+
if (requireMention !== undefined)
|
|
1682
|
+
access.requireMention = requireMention;
|
|
1619
1683
|
try {
|
|
1620
1684
|
writeTelegramAccess(agentId, access);
|
|
1621
1685
|
}
|
|
@@ -1623,7 +1687,7 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1623
1687
|
res.status(500).json({ error: `Failed to update policy: ${err.message}` });
|
|
1624
1688
|
return;
|
|
1625
1689
|
}
|
|
1626
|
-
res.json({ ok: true, dmPolicy });
|
|
1690
|
+
res.json({ ok: true, dmPolicy: access.dmPolicy, pairing: access.pairing, groupPolicy: access.groupPolicy, requireMention: access.requireMention });
|
|
1627
1691
|
});
|
|
1628
1692
|
/**
|
|
1629
1693
|
* GET /api/v1/agents/:agentId/telegram/allowlist
|
|
@@ -1713,6 +1777,333 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1713
1777
|
}
|
|
1714
1778
|
res.json({ ok: true });
|
|
1715
1779
|
});
|
|
1780
|
+
/**
|
|
1781
|
+
* GET /api/v1/agents/:agentId/telegram/group/allowlist
|
|
1782
|
+
* Return the allowlisted group ids for an agent's Telegram channel. Admin only.
|
|
1783
|
+
*/
|
|
1784
|
+
router.get('/v1/agents/:agentId/telegram/group/allowlist', auth, (req, res) => {
|
|
1785
|
+
const { agentId } = req.params;
|
|
1786
|
+
const apiKey = req.apiKey;
|
|
1787
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1788
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (!agentConfigs.has(agentId)) {
|
|
1792
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const access = readTelegramAccess(agentId);
|
|
1796
|
+
res.json({ groupAllowlist: access.groupAllowlist });
|
|
1797
|
+
});
|
|
1798
|
+
/**
|
|
1799
|
+
* DELETE /api/v1/agents/:agentId/telegram/group/allow/:groupId
|
|
1800
|
+
* Remove a group from the group allowlist. Admin only. Telegram group ids are
|
|
1801
|
+
* negative (e.g. -1001234567890) so the validation allows a leading minus.
|
|
1802
|
+
*/
|
|
1803
|
+
router.delete('/v1/agents/:agentId/telegram/group/allow/:groupId', auth, (req, res) => {
|
|
1804
|
+
const apiKey = req.apiKey;
|
|
1805
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1806
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const { agentId, groupId } = req.params;
|
|
1810
|
+
if (!/^-?\d+$/.test(groupId)) {
|
|
1811
|
+
res.status(400).json({ error: 'Invalid groupId: must be a numeric Telegram chat ID' });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (!agentConfigs.has(agentId)) {
|
|
1815
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const access = readTelegramAccess(agentId);
|
|
1819
|
+
access.groupAllowlist = access.groupAllowlist.filter((id) => id !== groupId);
|
|
1820
|
+
// Also drop any legacy per-sender restriction for this group so a later
|
|
1821
|
+
// re-add (via a fresh pairing knock) doesn't resurrect a stale allowlist.
|
|
1822
|
+
if (access.legacyGroupAllowFrom)
|
|
1823
|
+
delete access.legacyGroupAllowFrom[groupId];
|
|
1824
|
+
try {
|
|
1825
|
+
writeTelegramAccess(agentId, access);
|
|
1826
|
+
}
|
|
1827
|
+
catch (err) {
|
|
1828
|
+
res.status(500).json({ error: `Failed to update group allowlist: ${err.message}` });
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
res.json({ ok: true });
|
|
1832
|
+
});
|
|
1833
|
+
/**
|
|
1834
|
+
* GET /api/v1/agents/:agentId/discord/pending
|
|
1835
|
+
* List pending Discord pairing requests (non-expired).
|
|
1836
|
+
*/
|
|
1837
|
+
router.get('/v1/agents/:agentId/discord/pending', auth, (req, res) => {
|
|
1838
|
+
const { agentId } = req.params;
|
|
1839
|
+
const apiKey = req.apiKey;
|
|
1840
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1841
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (!agentConfigs.has(agentId)) {
|
|
1845
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const access = readDiscordAccess(agentId);
|
|
1849
|
+
const now = Date.now();
|
|
1850
|
+
const expired = Object.keys(access.pending).filter((code) => access.pending[code].expiresAt <= now);
|
|
1851
|
+
if (expired.length > 0) {
|
|
1852
|
+
expired.forEach((code) => { delete access.pending[code]; });
|
|
1853
|
+
try {
|
|
1854
|
+
writeDiscordAccess(agentId, access);
|
|
1855
|
+
}
|
|
1856
|
+
catch { /* non-fatal cleanup */ }
|
|
1857
|
+
}
|
|
1858
|
+
const pending = Object.entries(access.pending)
|
|
1859
|
+
.map(([code, p]) => ({ code, senderId: p.senderId, channelId: p.channelId, createdAt: p.createdAt, expiresAt: p.expiresAt, kind: p.kind ?? 'dm', guildId: p.guildId }));
|
|
1860
|
+
res.json({ pending });
|
|
1861
|
+
});
|
|
1862
|
+
/**
|
|
1863
|
+
* POST /api/v1/agents/:agentId/discord/approve
|
|
1864
|
+
* Approve a pending Discord pairing by code. Kind-aware (mirrors LINE): a
|
|
1865
|
+
* 'guild' knock moves its guildId into guildAllowlist (no approved/ handshake
|
|
1866
|
+
* — a guild has no single recipient); a 'dm' knock allowlists the sender and
|
|
1867
|
+
* drops the approved/<senderId> file for the "You're connected!" reply.
|
|
1868
|
+
*/
|
|
1869
|
+
router.post('/v1/agents/:agentId/discord/approve', auth, (req, res) => {
|
|
1870
|
+
const { agentId } = req.params;
|
|
1871
|
+
const apiKey = req.apiKey;
|
|
1872
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1873
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (!agentConfigs.has(agentId)) {
|
|
1877
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const { code } = req.body;
|
|
1881
|
+
if (!code) {
|
|
1882
|
+
res.status(400).json({ error: 'code required' });
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
const access = readDiscordAccess(agentId);
|
|
1886
|
+
const entry = access.pending[code];
|
|
1887
|
+
if (!entry || entry.expiresAt < Date.now()) {
|
|
1888
|
+
res.status(404).json({ error: 'Pairing code not found or expired' });
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
const isGuild = entry.kind === 'guild';
|
|
1892
|
+
if (isGuild) {
|
|
1893
|
+
if (entry.guildId && !access.guildAllowlist.includes(entry.guildId))
|
|
1894
|
+
access.guildAllowlist.push(entry.guildId);
|
|
1895
|
+
}
|
|
1896
|
+
else {
|
|
1897
|
+
if (!access.allowFrom.includes(entry.senderId))
|
|
1898
|
+
access.allowFrom.push(entry.senderId);
|
|
1899
|
+
}
|
|
1900
|
+
delete access.pending[code];
|
|
1901
|
+
try {
|
|
1902
|
+
writeDiscordAccess(agentId, access);
|
|
1903
|
+
if (!isGuild) {
|
|
1904
|
+
// Handshake consumed by module.ts:checkApprovals() — file name is the
|
|
1905
|
+
// senderId, content is the channelId to DM "You're connected!".
|
|
1906
|
+
const approvedDir = path.join(getDiscordStateDir(agentId), 'approved');
|
|
1907
|
+
fs.mkdirSync(approvedDir, { recursive: true });
|
|
1908
|
+
fs.writeFileSync(path.join(approvedDir, entry.senderId), entry.channelId);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
catch (err) {
|
|
1912
|
+
res.status(500).json({ error: `Failed to approve pairing: ${err.message}` });
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
res.json({ ok: true, senderId: entry.senderId, guildId: isGuild ? entry.guildId : undefined });
|
|
1916
|
+
});
|
|
1917
|
+
/**
|
|
1918
|
+
* POST /api/v1/agents/:agentId/discord/deny
|
|
1919
|
+
* Deny and remove a pending Discord pairing by code.
|
|
1920
|
+
*/
|
|
1921
|
+
router.post('/v1/agents/:agentId/discord/deny', auth, (req, res) => {
|
|
1922
|
+
const { agentId } = req.params;
|
|
1923
|
+
const apiKey = req.apiKey;
|
|
1924
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1925
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
if (!agentConfigs.has(agentId)) {
|
|
1929
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const { code } = req.body;
|
|
1933
|
+
if (!code) {
|
|
1934
|
+
res.status(400).json({ error: 'code required' });
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
const access = readDiscordAccess(agentId);
|
|
1938
|
+
if (!access.pending[code]) {
|
|
1939
|
+
res.status(404).json({ error: 'Pairing code not found' });
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
delete access.pending[code];
|
|
1943
|
+
try {
|
|
1944
|
+
writeDiscordAccess(agentId, access);
|
|
1945
|
+
}
|
|
1946
|
+
catch (err) {
|
|
1947
|
+
res.status(500).json({ error: `Failed to deny pairing: ${err.message}` });
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
res.json({ ok: true });
|
|
1951
|
+
});
|
|
1952
|
+
/**
|
|
1953
|
+
* PATCH /api/v1/agents/:agentId/discord/policy
|
|
1954
|
+
* Update the Discord DM policy, the orthogonal pairing toggle, the guild
|
|
1955
|
+
* policy, and/or the guild mention gate.
|
|
1956
|
+
* Body: { dmPolicy?, pairing?, groupPolicy?: 'open'|'allowlist'|'disabled', requireMention?: boolean }.
|
|
1957
|
+
* At least one field must be present; each is applied only if provided.
|
|
1958
|
+
*/
|
|
1959
|
+
router.patch('/v1/agents/:agentId/discord/policy', auth, (req, res) => {
|
|
1960
|
+
const { agentId } = req.params;
|
|
1961
|
+
const apiKey = req.apiKey;
|
|
1962
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
1963
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
if (!agentConfigs.has(agentId)) {
|
|
1967
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const { dmPolicy, pairing, groupPolicy, requireMention } = req.body;
|
|
1971
|
+
const valid = ['open', 'allowlist', 'disabled'];
|
|
1972
|
+
if (dmPolicy !== undefined && !valid.includes(dmPolicy)) {
|
|
1973
|
+
res.status(400).json({ error: `dmPolicy must be one of: ${valid.join(', ')}` });
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (pairing !== undefined && typeof pairing !== 'boolean') {
|
|
1977
|
+
res.status(400).json({ error: 'pairing must be a boolean' });
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
if (groupPolicy !== undefined && !valid.includes(groupPolicy)) {
|
|
1981
|
+
res.status(400).json({ error: `groupPolicy must be one of: ${valid.join(', ')}` });
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (requireMention !== undefined && typeof requireMention !== 'boolean') {
|
|
1985
|
+
res.status(400).json({ error: 'requireMention must be a boolean' });
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
if (dmPolicy === undefined && pairing === undefined && groupPolicy === undefined && requireMention === undefined) {
|
|
1989
|
+
res.status(400).json({ error: 'provide dmPolicy, pairing, groupPolicy and/or requireMention' });
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const access = readDiscordAccess(agentId);
|
|
1993
|
+
if (dmPolicy !== undefined)
|
|
1994
|
+
access.dmPolicy = dmPolicy;
|
|
1995
|
+
if (pairing !== undefined)
|
|
1996
|
+
access.pairing = pairing;
|
|
1997
|
+
if (groupPolicy !== undefined)
|
|
1998
|
+
access.groupPolicy = groupPolicy;
|
|
1999
|
+
if (requireMention !== undefined)
|
|
2000
|
+
access.requireMention = requireMention;
|
|
2001
|
+
try {
|
|
2002
|
+
writeDiscordAccess(agentId, access);
|
|
2003
|
+
}
|
|
2004
|
+
catch (err) {
|
|
2005
|
+
res.status(500).json({ error: `Failed to update policy: ${err.message}` });
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
res.json({ ok: true, dmPolicy: access.dmPolicy, pairing: access.pairing, groupPolicy: access.groupPolicy, requireMention: access.requireMention });
|
|
2009
|
+
});
|
|
2010
|
+
/**
|
|
2011
|
+
* GET /api/v1/agents/:agentId/discord/allowlist
|
|
2012
|
+
* Return all users in allowFrom for an agent's Discord channel.
|
|
2013
|
+
*/
|
|
2014
|
+
router.get('/v1/agents/:agentId/discord/allowlist', auth, (req, res) => {
|
|
2015
|
+
const { agentId } = req.params;
|
|
2016
|
+
const apiKey = req.apiKey;
|
|
2017
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
2018
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
if (!agentConfigs.has(agentId)) {
|
|
2022
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
const access = readDiscordAccess(agentId);
|
|
2026
|
+
res.json({ allowFrom: access.allowFrom });
|
|
2027
|
+
});
|
|
2028
|
+
/**
|
|
2029
|
+
* DELETE /api/v1/agents/:agentId/discord/allow/:userId
|
|
2030
|
+
* Remove a user from the allowFrom list. Admin only.
|
|
2031
|
+
*/
|
|
2032
|
+
router.delete('/v1/agents/:agentId/discord/allow/:userId', auth, (req, res) => {
|
|
2033
|
+
const apiKey = req.apiKey;
|
|
2034
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
2035
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const { agentId, userId } = req.params;
|
|
2039
|
+
if (!/^\d+$/.test(userId)) {
|
|
2040
|
+
res.status(400).json({ error: 'Invalid userId: must be a numeric Discord user ID' });
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (!agentConfigs.has(agentId)) {
|
|
2044
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const access = readDiscordAccess(agentId);
|
|
2048
|
+
access.allowFrom = access.allowFrom.filter((id) => id !== userId);
|
|
2049
|
+
try {
|
|
2050
|
+
writeDiscordAccess(agentId, access);
|
|
2051
|
+
}
|
|
2052
|
+
catch (err) {
|
|
2053
|
+
res.status(500).json({ error: `Failed to update allowlist: ${err.message}` });
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
res.json({ ok: true });
|
|
2057
|
+
});
|
|
2058
|
+
/**
|
|
2059
|
+
* GET /api/v1/agents/:agentId/discord/guild/allowlist
|
|
2060
|
+
* Return the allowlisted guild ids for an agent's Discord channel. Admin only.
|
|
2061
|
+
*/
|
|
2062
|
+
router.get('/v1/agents/:agentId/discord/guild/allowlist', auth, (req, res) => {
|
|
2063
|
+
const { agentId } = req.params;
|
|
2064
|
+
const apiKey = req.apiKey;
|
|
2065
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
2066
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
if (!agentConfigs.has(agentId)) {
|
|
2070
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
const access = readDiscordAccess(agentId);
|
|
2074
|
+
res.json({ guildAllowlist: access.guildAllowlist });
|
|
2075
|
+
});
|
|
2076
|
+
/**
|
|
2077
|
+
* DELETE /api/v1/agents/:agentId/discord/guild/allow/:guildId
|
|
2078
|
+
* Remove a guild from the guild allowlist. Admin only. Discord guild ids are
|
|
2079
|
+
* numeric snowflakes (no leading minus).
|
|
2080
|
+
*/
|
|
2081
|
+
router.delete('/v1/agents/:agentId/discord/guild/allow/:guildId', auth, (req, res) => {
|
|
2082
|
+
const apiKey = req.apiKey;
|
|
2083
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
2084
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
const { agentId, guildId } = req.params;
|
|
2088
|
+
if (!/^\d+$/.test(guildId)) {
|
|
2089
|
+
res.status(400).json({ error: 'Invalid guildId: must be a numeric Discord guild ID' });
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
if (!agentConfigs.has(agentId)) {
|
|
2093
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
const access = readDiscordAccess(agentId);
|
|
2097
|
+
access.guildAllowlist = access.guildAllowlist.filter((id) => id !== guildId);
|
|
2098
|
+
try {
|
|
2099
|
+
writeDiscordAccess(agentId, access);
|
|
2100
|
+
}
|
|
2101
|
+
catch (err) {
|
|
2102
|
+
res.status(500).json({ error: `Failed to update guild allowlist: ${err.message}` });
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
res.json({ ok: true });
|
|
2106
|
+
});
|
|
1716
2107
|
/**
|
|
1717
2108
|
* DELETE /api/v1/agents/:agentId
|
|
1718
2109
|
*
|