agent-messenger 2.25.0 → 2.27.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/.claude-plugin/README.md +16 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +28 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/commands/auth.d.ts +4 -0
- package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/auth.js +61 -1
- package/dist/src/platforms/discord/commands/auth.js.map +1 -1
- package/dist/src/platforms/discord/index.d.ts +2 -0
- package/dist/src/platforms/discord/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/index.js +1 -0
- package/dist/src/platforms/discord/index.js.map +1 -1
- package/dist/src/platforms/discord/listener.d.ts +1 -4
- package/dist/src/platforms/discord/listener.d.ts.map +1 -1
- package/dist/src/platforms/discord/listener.js +33 -15
- package/dist/src/platforms/discord/listener.js.map +1 -1
- package/dist/src/platforms/discord/remote-auth.d.ts +42 -0
- package/dist/src/platforms/discord/remote-auth.d.ts.map +1 -0
- package/dist/src/platforms/discord/remote-auth.js +208 -0
- package/dist/src/platforms/discord/remote-auth.js.map +1 -0
- package/dist/src/platforms/slack/qr-http-login.d.ts.map +1 -1
- package/dist/src/platforms/slack/qr-http-login.js +105 -6
- package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
- package/docs/content/docs/sdk/discord.mdx +23 -9
- package/e2e/README.md +3 -1
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +43 -7
- package/skills/agent-discord/references/authentication.md +46 -8
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +7 -4
- package/skills/agent-slack/references/authentication.md +8 -3
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/commands/auth.ts +81 -1
- package/src/platforms/discord/index.ts +2 -0
- package/src/platforms/discord/listener.test.ts +12 -4
- package/src/platforms/discord/listener.ts +36 -16
- package/src/platforms/discord/remote-auth.test.ts +261 -0
- package/src/platforms/discord/remote-auth.ts +317 -0
- package/src/platforms/slack/qr-http-login.test.ts +47 -0
- package/src/platforms/slack/qr-http-login.ts +112 -7
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { constants as cryptoConstants, createHash, createPrivateKey, createPublicKey, generateKeyPairSync, privateDecrypt, } from 'node:crypto';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { DiscordError } from './client.js';
|
|
4
|
+
import { getDiscordHeaders } from './super-properties.js';
|
|
5
|
+
const REMOTE_AUTH_GATEWAY_URL = 'wss://remote-auth-gateway.discord.gg/?v=2';
|
|
6
|
+
const REMOTE_AUTH_ORIGIN = 'https://discord.com';
|
|
7
|
+
const REMOTE_AUTH_LOGIN_URL = 'https://discord.com/api/v9/users/@me/remote-auth/login';
|
|
8
|
+
const QR_URL_PREFIX = 'https://discord.com/ra/';
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 150_000;
|
|
10
|
+
/**
|
|
11
|
+
* Authenticate a Discord user account via the Remote Auth ("scan QR with the mobile app") flow.
|
|
12
|
+
*
|
|
13
|
+
* The desktop side (this function) opens a WebSocket to Discord's remote-auth gateway, performs an
|
|
14
|
+
* RSA-OAEP key exchange, and renders a QR code. Once the user scans it with an authenticated Discord
|
|
15
|
+
* mobile app and confirms, Discord returns an encrypted ticket which is exchanged for the user token.
|
|
16
|
+
*/
|
|
17
|
+
export function loginWithRemoteAuth(options = {}) {
|
|
18
|
+
const debug = options.debug;
|
|
19
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
20
|
+
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
|
|
21
|
+
modulusLength: 2048,
|
|
22
|
+
publicExponent: 0x10001,
|
|
23
|
+
});
|
|
24
|
+
const ws = options.createWebSocket
|
|
25
|
+
? options.createWebSocket(REMOTE_AUTH_GATEWAY_URL)
|
|
26
|
+
: new WebSocket(REMOTE_AUTH_GATEWAY_URL, { headers: { Origin: REMOTE_AUTH_ORIGIN } });
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let settled = false;
|
|
29
|
+
let heartbeatTimer = null;
|
|
30
|
+
let pendingUser = null;
|
|
31
|
+
let exchangeStarted = false;
|
|
32
|
+
const cleanup = () => {
|
|
33
|
+
if (heartbeatTimer) {
|
|
34
|
+
clearInterval(heartbeatTimer);
|
|
35
|
+
heartbeatTimer = null;
|
|
36
|
+
}
|
|
37
|
+
clearTimeout(timeoutTimer);
|
|
38
|
+
try {
|
|
39
|
+
ws.close();
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
};
|
|
43
|
+
const fail = (error) => {
|
|
44
|
+
if (settled)
|
|
45
|
+
return;
|
|
46
|
+
settled = true;
|
|
47
|
+
cleanup();
|
|
48
|
+
reject(error);
|
|
49
|
+
};
|
|
50
|
+
const succeed = (session) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
settled = true;
|
|
54
|
+
cleanup();
|
|
55
|
+
resolve(session);
|
|
56
|
+
};
|
|
57
|
+
const timeoutTimer = setTimeout(() => {
|
|
58
|
+
fail(new DiscordError('Remote auth timed out before the QR code was scanned.', 'remote_auth_timeout'));
|
|
59
|
+
}, timeoutMs);
|
|
60
|
+
timeoutTimer.unref?.();
|
|
61
|
+
const send = (payload) => {
|
|
62
|
+
try {
|
|
63
|
+
ws.send(JSON.stringify(payload));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
ws.on('open', () => {
|
|
70
|
+
debug?.('Connected to remote auth gateway');
|
|
71
|
+
});
|
|
72
|
+
ws.on('message', (raw) => {
|
|
73
|
+
let message;
|
|
74
|
+
try {
|
|
75
|
+
message = JSON.parse(raw.toString());
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
handleMessage(message);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const handleMessage = (message) => {
|
|
88
|
+
switch (message.op) {
|
|
89
|
+
case 'hello': {
|
|
90
|
+
const interval = message.heartbeat_interval;
|
|
91
|
+
heartbeatTimer = setInterval(() => send({ op: 'heartbeat' }), interval);
|
|
92
|
+
heartbeatTimer.unref?.();
|
|
93
|
+
send({ op: 'init', encoded_public_key: encodePublicKey(publicKey) });
|
|
94
|
+
debug?.('Sent init with public key');
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'nonce_proof': {
|
|
98
|
+
const encrypted = message.encrypted_nonce;
|
|
99
|
+
const proof = computeNonceProof(privateKey, encrypted);
|
|
100
|
+
send({ op: 'nonce_proof', proof });
|
|
101
|
+
debug?.('Sent nonce proof');
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 'pending_remote_init': {
|
|
105
|
+
const { fingerprint } = message;
|
|
106
|
+
const qrUrl = `${QR_URL_PREFIX}${fingerprint}`;
|
|
107
|
+
debug?.('Received fingerprint; QR ready');
|
|
108
|
+
Promise.resolve(options.onQrUrl?.(qrUrl)).catch((error) => {
|
|
109
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
110
|
+
});
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case 'pending_ticket': {
|
|
114
|
+
const payload = decryptPayload(privateKey, message.encrypted_user_payload);
|
|
115
|
+
pendingUser = parseUserPayload(payload.toString('utf8'));
|
|
116
|
+
options.onPendingLogin?.(pendingUser);
|
|
117
|
+
debug?.(`QR scanned by ${pendingUser.username}`);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'pending_login': {
|
|
121
|
+
if (exchangeStarted)
|
|
122
|
+
break;
|
|
123
|
+
exchangeStarted = true;
|
|
124
|
+
const { ticket } = message;
|
|
125
|
+
debug?.('Exchanging ticket for token');
|
|
126
|
+
exchangeTicket(ticket, privateKey, options.fetchImpl ?? fetch)
|
|
127
|
+
.then((token) => succeed({ token, user: pendingUser }))
|
|
128
|
+
.catch(fail);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'cancel': {
|
|
132
|
+
fail(new DiscordError('Remote auth was cancelled from the mobile device.', 'remote_auth_cancelled'));
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
ws.on('close', (code) => {
|
|
138
|
+
if (settled)
|
|
139
|
+
return;
|
|
140
|
+
// The gateway closes the socket right after pending_login; the in-flight
|
|
141
|
+
// ticket exchange settles the result, so this close must not preempt it.
|
|
142
|
+
if (exchangeStarted)
|
|
143
|
+
return;
|
|
144
|
+
// 4003 is the gateway's timeout close code.
|
|
145
|
+
if (code === 4003) {
|
|
146
|
+
fail(new DiscordError('Remote auth session expired. Generate a new QR code and try again.', 'remote_auth_timeout'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
fail(new DiscordError(`Remote auth connection closed before completion (code ${code}).`, 'remote_auth_closed'));
|
|
150
|
+
});
|
|
151
|
+
ws.on('error', (error) => {
|
|
152
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function encodePublicKey(publicKey) {
|
|
157
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
158
|
+
return spki.toString('base64');
|
|
159
|
+
}
|
|
160
|
+
function decryptPayload(privateKey, encryptedBase64) {
|
|
161
|
+
return privateDecrypt({
|
|
162
|
+
key: privateKey,
|
|
163
|
+
padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING,
|
|
164
|
+
oaepHash: 'sha256',
|
|
165
|
+
}, Buffer.from(encryptedBase64, 'base64'));
|
|
166
|
+
}
|
|
167
|
+
function computeNonceProof(privateKey, encryptedNonce) {
|
|
168
|
+
const nonce = decryptPayload(privateKey, encryptedNonce);
|
|
169
|
+
return base64UrlNoPadding(createHash('sha256').update(nonce).digest());
|
|
170
|
+
}
|
|
171
|
+
function parseUserPayload(payload) {
|
|
172
|
+
const [id = '', discriminator = '0', avatar = '', username = ''] = payload.split(':');
|
|
173
|
+
return { id, username, discriminator, avatar: avatar || null };
|
|
174
|
+
}
|
|
175
|
+
async function exchangeTicket(ticket, privateKey, fetchImpl) {
|
|
176
|
+
const { Authorization: _omit, ...headers } = getDiscordHeaders('');
|
|
177
|
+
const response = await fetchImpl(REMOTE_AUTH_LOGIN_URL, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify({ ticket }),
|
|
181
|
+
});
|
|
182
|
+
if (response.status === 400) {
|
|
183
|
+
const body = (await response.json().catch(() => ({})));
|
|
184
|
+
if (body.captcha_sitekey) {
|
|
185
|
+
throw new DiscordError('Discord requires a captcha to complete this login. Captcha solving is not supported; use `auth extract` instead.', 'remote_auth_captcha');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
throw new DiscordError(`Failed to exchange remote auth ticket (HTTP ${response.status}).`, 'remote_auth_login_failed');
|
|
190
|
+
}
|
|
191
|
+
const body = (await response.json());
|
|
192
|
+
if (!body.encrypted_token) {
|
|
193
|
+
throw new DiscordError('Remote auth response did not include a token.', 'remote_auth_login_failed');
|
|
194
|
+
}
|
|
195
|
+
return decryptPayload(privateKey, body.encrypted_token).toString('utf8');
|
|
196
|
+
}
|
|
197
|
+
function base64UrlNoPadding(buffer) {
|
|
198
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
199
|
+
}
|
|
200
|
+
export const __test = {
|
|
201
|
+
encodePublicKey,
|
|
202
|
+
computeNonceProof,
|
|
203
|
+
parseUserPayload,
|
|
204
|
+
base64UrlNoPadding,
|
|
205
|
+
createPublicKey,
|
|
206
|
+
createPrivateKey,
|
|
207
|
+
};
|
|
208
|
+
//# sourceMappingURL=remote-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-auth.js","sourceRoot":"","sources":["../../../../src/platforms/discord/remote-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qr-http-login.d.ts","sourceRoot":"","sources":["../../../../src/platforms/slack/qr-http-login.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC;AAMD,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,SAAS,CAAC,CAwBnG;
|
|
1
|
+
{"version":3,"file":"qr-http-login.d.ts","sourceRoot":"","sources":["../../../../src/platforms/slack/qr-http-login.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC;AAMD,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,SAAS,CAAC,CAwBnG;AA8JD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED,wBAAgB,YAAY,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGnE"}
|
|
@@ -7,9 +7,9 @@ export async function loginWithQr(dataUrl, options = {}) {
|
|
|
7
7
|
const login = decodeSlackQr(dataUrl.trim());
|
|
8
8
|
const debug = options.debug;
|
|
9
9
|
debug?.(`Decoded QR for workspace ${login.workspace}`);
|
|
10
|
-
const cookie = await captureDCookie(login.url, options);
|
|
10
|
+
const { cookie, denialReason, ssoProvider } = await captureDCookie(login.url, options);
|
|
11
11
|
if (!cookie) {
|
|
12
|
-
throw new SlackError(
|
|
12
|
+
throw new SlackError(qrSessionFailureMessage(denialReason, { workspace: login.workspace, ssoProvider }), 'qr_session_failed');
|
|
13
13
|
}
|
|
14
14
|
debug?.('Captured session cookie');
|
|
15
15
|
const token = await refreshTokenFromWeb(login.workspace, cookie, options.fetchImpl ?? fetch);
|
|
@@ -19,12 +19,60 @@ export async function loginWithQr(dataUrl, options = {}) {
|
|
|
19
19
|
debug?.('Retrieved client token');
|
|
20
20
|
return { token, cookie, workspace: login.workspace };
|
|
21
21
|
}
|
|
22
|
+
function qrSessionFailureMessage(denialReason, context = {}) {
|
|
23
|
+
const workspace = context.workspace ? `"${context.workspace}"` : 'this workspace';
|
|
24
|
+
if (denialReason === 'sso_required') {
|
|
25
|
+
const via = context.ssoProvider ? ` (via ${context.ssoProvider})` : '';
|
|
26
|
+
return [
|
|
27
|
+
`Slack workspace ${workspace} enforces SSO${via}, so QR sign-in cannot complete here.`,
|
|
28
|
+
'',
|
|
29
|
+
`Why: scanning the QR redirects the login to your identity provider${via}. agent-slack follows the`,
|
|
30
|
+
'redirect chain over plain HTTP and never sends your session cookie off Slack-owned hosts, so it',
|
|
31
|
+
'stops at the IdP and no Slack session (the "d" cookie) is ever issued. Completing SSO requires a',
|
|
32
|
+
'real browser, which a headless environment does not have.',
|
|
33
|
+
'',
|
|
34
|
+
'What to do instead:',
|
|
35
|
+
' On a computer where you are already signed in to Slack (desktop app or a Chromium browser),',
|
|
36
|
+
' run: agent-slack auth extract',
|
|
37
|
+
' This reads your existing session locally — no SSO re-login, no DevTools.',
|
|
38
|
+
'',
|
|
39
|
+
'Background: https://github.com/agent-messenger/agent-messenger/issues/260',
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
if (denialReason === 'link_expired') {
|
|
43
|
+
return [
|
|
44
|
+
`The Slack QR code for ${workspace} has expired or was already used.`,
|
|
45
|
+
'The z-app sign-in link is single-use and short-lived: it is consumed the first time it is opened',
|
|
46
|
+
'(including previewing or scanning it), and re-using it returns "Link Expired".',
|
|
47
|
+
'',
|
|
48
|
+
'Generate a fresh QR (your name → "Sign in on mobile") and pipe it in immediately, without',
|
|
49
|
+
'previewing or scanning it first.',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
if (denialReason === 'awaiting_device_confirmation') {
|
|
53
|
+
return [
|
|
54
|
+
`Slack requires this sign-in to ${workspace} to be confirmed on an already-authenticated device.`,
|
|
55
|
+
'QR sign-in over HTTP cannot complete that approval step.',
|
|
56
|
+
'',
|
|
57
|
+
'Use agent-slack auth extract on a computer already signed in to Slack instead.',
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
return [
|
|
61
|
+
`Could not establish a Slack session for ${workspace} from the QR code.`,
|
|
62
|
+
'The link may have expired, or the workspace requires a sign-in step that QR-over-HTTP cannot complete.',
|
|
63
|
+
'',
|
|
64
|
+
'Generate a fresh QR and try again, or run agent-slack auth extract on a computer already signed in',
|
|
65
|
+
'to Slack.',
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
22
68
|
async function captureDCookie(startUrl, options) {
|
|
23
69
|
const doFetch = options.fetchImpl ?? fetch;
|
|
24
70
|
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
25
71
|
const debug = options.debug;
|
|
26
72
|
let url = startUrl;
|
|
27
73
|
let dCookie = null;
|
|
74
|
+
let sessionDenialReason = null;
|
|
75
|
+
let ssoProvider = null;
|
|
28
76
|
const cookieJar = [];
|
|
29
77
|
for (let hop = 0; hop < maxRedirects; hop++) {
|
|
30
78
|
// Only ever send captured cookies (including the d session cookie) to Slack
|
|
@@ -43,26 +91,77 @@ async function captureDCookie(startUrl, options) {
|
|
|
43
91
|
...(cookieJar.length ? { Cookie: cookieJar.join('; ') } : {}),
|
|
44
92
|
},
|
|
45
93
|
});
|
|
94
|
+
const cookieNames = [];
|
|
46
95
|
for (const setCookie of getSetCookies(response)) {
|
|
47
96
|
const value = parseDCookie(setCookie);
|
|
48
97
|
if (value)
|
|
49
98
|
dCookie = value;
|
|
99
|
+
const name = setCookie.split('=')[0]?.trim();
|
|
100
|
+
if (name)
|
|
101
|
+
cookieNames.push(name);
|
|
50
102
|
const pair = setCookie.split(';')[0]?.trim();
|
|
51
103
|
if (pair)
|
|
52
104
|
cookieJar.push(pair);
|
|
53
105
|
}
|
|
54
|
-
debug?.(`hop ${hop}: ${response.status}`);
|
|
106
|
+
debug?.(`hop ${hop}: ${response.status} set-cookie=[${cookieNames.join(',') || 'none'}]`);
|
|
55
107
|
const location = response.status >= 300 && response.status < 400 ? response.headers.get('location') : null;
|
|
56
|
-
if (!location)
|
|
108
|
+
if (!location) {
|
|
109
|
+
if (!dCookie)
|
|
110
|
+
sessionDenialReason = await classifySessionDenial(response);
|
|
57
111
|
break;
|
|
112
|
+
}
|
|
58
113
|
const next = new URL(location, url).toString();
|
|
59
114
|
if (!isSlackHost(next)) {
|
|
60
|
-
|
|
115
|
+
// A no-cookie login QR that redirects off Slack is an SSO/IdP hand-off the
|
|
116
|
+
// HTTP flow cannot complete — classify it regardless of whether the IdP
|
|
117
|
+
// host is a recognized provider (custom/vanity SAML domains included).
|
|
118
|
+
if (!dCookie) {
|
|
119
|
+
sessionDenialReason = 'sso_required';
|
|
120
|
+
ssoProvider = ssoProviderFromUrl(next);
|
|
121
|
+
}
|
|
122
|
+
debug?.(`hop ${hop}: redirect target is not a Slack host (${new URL(next).hostname}), stopping`);
|
|
61
123
|
break;
|
|
62
124
|
}
|
|
63
125
|
url = next;
|
|
64
126
|
}
|
|
65
|
-
return dCookie;
|
|
127
|
+
return { cookie: dCookie, denialReason: sessionDenialReason, ssoProvider };
|
|
128
|
+
}
|
|
129
|
+
function ssoProviderFromUrl(rawUrl) {
|
|
130
|
+
let hostname;
|
|
131
|
+
try {
|
|
132
|
+
hostname = new URL(rawUrl).hostname;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (hostname === 'accounts.google.com')
|
|
138
|
+
return 'Google';
|
|
139
|
+
if (hostname.endsWith('.okta.com') || hostname.endsWith('.oktapreview.com'))
|
|
140
|
+
return 'Okta';
|
|
141
|
+
if (hostname === 'login.microsoftonline.com')
|
|
142
|
+
return 'Microsoft';
|
|
143
|
+
if (hostname.endsWith('.onelogin.com'))
|
|
144
|
+
return 'OneLogin';
|
|
145
|
+
if (hostname.endsWith('.pingidentity.com') || hostname.endsWith('.pingone.com'))
|
|
146
|
+
return 'Ping Identity';
|
|
147
|
+
if (hostname.endsWith('.auth0.com'))
|
|
148
|
+
return 'Auth0';
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
async function classifySessionDenial(response) {
|
|
152
|
+
try {
|
|
153
|
+
const body = await response.text();
|
|
154
|
+
if (/Link Expired/i.test(body) || /trouble signing you in/i.test(body)) {
|
|
155
|
+
return 'link_expired';
|
|
156
|
+
}
|
|
157
|
+
if (/sign in on your other device/i.test(body) || /open Slack/i.test(body) || /confirm/i.test(body)) {
|
|
158
|
+
return 'awaiting_device_confirmation';
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
66
165
|
}
|
|
67
166
|
export function isSlackHost(rawUrl) {
|
|
68
167
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qr-http-login.js","sourceRoot":"","sources":["../../../../src/platforms/slack/qr-http-login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAc1C,MAAM,kBAAkB,GACtB,uHAAuH,CAAA;AACzH,MAAM,qBAAqB,GAAG,EAAE,CAAA;AAEhC,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAe,EAAE,UAA0B,EAAE;IAC7E,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IAC3C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;IAC3B,KAAK,EAAE,CAAC,4BAA4B,KAAK,CAAC,SAAS,EAAE,CAAC,CAAA;IAEtD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"qr-http-login.js","sourceRoot":"","sources":["../../../../src/platforms/slack/qr-http-login.ts"],"names":[],"mappings":"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"}
|
|
@@ -206,6 +206,28 @@ await client.archiveThread(threadId, false) // unarchive
|
|
|
206
206
|
// → DiscordChannel
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
+
## QR Code Login
|
|
210
|
+
|
|
211
|
+
Sign in by scanning a QR code with the Discord mobile app — no desktop app or browser token extraction required. `loginWithRemoteAuth` implements Discord's Remote Auth protocol: it opens a WebSocket to Discord's remote-auth gateway, performs an RSA key exchange, and hands you a QR URL to display. Once the user scans it with an authenticated Discord mobile app and confirms, you receive the user token.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { DiscordClient, loginWithRemoteAuth } from 'agent-messenger/discord'
|
|
215
|
+
|
|
216
|
+
const session = await loginWithRemoteAuth({
|
|
217
|
+
onQrUrl: (url) => {
|
|
218
|
+
// Render this URL as a QR code (e.g. with the `qrcode` package)
|
|
219
|
+
console.log('Scan this with the Discord mobile app:', url)
|
|
220
|
+
},
|
|
221
|
+
onPendingLogin: (user) => {
|
|
222
|
+
console.log(`Scanned by ${user.username}. Confirm on your phone...`)
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const client = await new DiscordClient().login({ token: session.token })
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`session` is `{ token, user: RemoteAuthUser | null }`, where `RemoteAuthUser` is `{ id, username, discriminator, avatar }`. `user` is populated from the scan confirmation, but can be `null` if the gateway skips the user payload — if you need the identity, guard for `null` and call `await client.testAuth()` after login. Discord may require an hCaptcha on the final token exchange; when it does, the call rejects with a `DiscordError` of code `remote_auth_captcha` — fall back to `auth extract` in that case. The flow requires a phone with the Discord app, so it cannot run headlessly.
|
|
230
|
+
|
|
209
231
|
## Real-Time Events
|
|
210
232
|
|
|
211
233
|
`DiscordListener` connects to Discord's Gateway WebSocket for instant event streaming.
|
|
@@ -235,15 +257,7 @@ await listener.start() // connects via Gateway WebSocket
|
|
|
235
257
|
// listener.stop() // clean shutdown
|
|
236
258
|
```
|
|
237
259
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
import { DiscordIntent } from 'agent-messenger/discord'
|
|
242
|
-
|
|
243
|
-
const listener = new DiscordListener(client, {
|
|
244
|
-
intents: DiscordIntent.Guilds | DiscordIntent.GuildMessages | DiscordIntent.MessageContent, // MessageContent is privileged
|
|
245
|
-
})
|
|
246
|
-
```
|
|
260
|
+
The user-account listener does not take intents — Gateway intents are a bot concept. It identifies as a user session and automatically receives full message content (including other users' messages), so no configuration is required.
|
|
247
261
|
|
|
248
262
|
### Available Events
|
|
249
263
|
|
package/e2e/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Before running E2E tests, you need:
|
|
|
41
41
|
|
|
42
42
|
## Running E2E Tests Locally
|
|
43
43
|
|
|
44
|
-
### Step 1:
|
|
44
|
+
### Step 1: Authenticate
|
|
45
45
|
|
|
46
46
|
First, make sure the desktop apps (Slack/Discord) are running and logged into your **test** workspaces.
|
|
47
47
|
|
|
@@ -56,6 +56,8 @@ agent-discord auth extract
|
|
|
56
56
|
agent-teams auth extract
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
> For local runs you can also use QR code sign-in for the **test** accounts (`agent-slack auth qr` / `agent-discord auth qr`) instead of extraction — it's the recommended auth method and doesn't require the desktop app. CI uses the environment variables below (extracted token/cookie values), since QR sign-in is interactive.
|
|
60
|
+
|
|
59
61
|
### Step 2: Switch to Test Workspace
|
|
60
62
|
|
|
61
63
|
Ensure you're targeting the correct test workspace:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-messenger",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.27.0",
|
|
4
4
|
"description": "Multi-platform messaging CLI for AI agents (Slack, Discord, Teams, Webex, Telegram, Telegram Bot, WhatsApp, LINE, Instagram, KakaoTalk, Channel Talk)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-channeltalk
|
|
3
3
|
description: Interact with Channel Talk using extracted desktop app or browser credentials - read chats, send messages, search messages, manage groups
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.27.0
|
|
5
5
|
allowed-tools: Bash(agent-channeltalk:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-discord
|
|
3
3
|
description: Interact with Discord servers - send messages, read channels, manage reactions
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.27.0
|
|
5
5
|
allowed-tools: Bash(agent-discord:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -37,7 +37,22 @@ Credentials are extracted automatically from the Discord desktop app (or Chromiu
|
|
|
37
37
|
|
|
38
38
|
On macOS, the system may prompt for your Keychain password the first time (required to decrypt Discord's stored token). This is a one-time prompt.
|
|
39
39
|
|
|
40
|
-
**
|
|
40
|
+
**Obtaining tokens** — two options:
|
|
41
|
+
|
|
42
|
+
- **`agent-discord auth qr` — recommended**: signs in by scanning a QR code with the Discord mobile app (Settings → Scan QR Code). This is the safest and most reliable method — it authenticates through Discord's official Remote Auth flow instead of reading credentials off disk, and needs no desktop app or browser. Requires a phone to scan, so it cannot run headlessly.
|
|
43
|
+
- **`agent-discord auth extract`**: extracts from the Discord desktop app first, falling back to Chromium browsers if the app isn't installed. Best for automated/headless use where no phone is available to scan.
|
|
44
|
+
|
|
45
|
+
### QR Code Sign-In (Recommended)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Generate a QR code and wait for you to scan it with the Discord mobile app
|
|
49
|
+
agent-discord auth qr
|
|
50
|
+
|
|
51
|
+
# Show protocol details while debugging
|
|
52
|
+
agent-discord auth qr --debug
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Open the Discord mobile app → **Settings → Scan QR Code**, scan the printed code, and confirm on your phone. The token (plus all discovered servers) is validated and stored like `auth extract`. The QR code expires after ~150 seconds — re-run the command to generate a fresh one. See [references/authentication.md](references/authentication.md) for the full flow.
|
|
41
56
|
|
|
42
57
|
### Multi-Server Support
|
|
43
58
|
|
|
@@ -142,6 +157,10 @@ agent-discord auth extract --browser-profile "$HOME/work-profile,$HOME/personal-
|
|
|
142
157
|
|
|
143
158
|
# --browser-profile accepts repeatable or comma-separated Chromium profile/user-data dirs
|
|
144
159
|
|
|
160
|
+
# Sign in by scanning a QR code with the Discord mobile app (Settings -> Scan QR Code)
|
|
161
|
+
agent-discord auth qr
|
|
162
|
+
agent-discord auth qr --debug
|
|
163
|
+
|
|
145
164
|
# Check auth status
|
|
146
165
|
agent-discord auth status
|
|
147
166
|
|
|
@@ -411,7 +430,7 @@ All commands return consistent error format:
|
|
|
411
430
|
|
|
412
431
|
Common errors:
|
|
413
432
|
|
|
414
|
-
- `Not authenticated`: No valid token (auto-extraction failed — see Troubleshooting)
|
|
433
|
+
- `Not authenticated`: No valid token (auto-extraction failed — run `auth qr` to sign in, or see Troubleshooting)
|
|
415
434
|
- `No current server set`: Run `server switch <id>` first
|
|
416
435
|
- `Message not found`: Invalid message ID
|
|
417
436
|
- `Unknown Channel`: Invalid channel ID
|
|
@@ -445,6 +464,25 @@ if (!token) {
|
|
|
445
464
|
const client = await new DiscordClient().login({ token })
|
|
446
465
|
```
|
|
447
466
|
|
|
467
|
+
### QR Code Login (Recommended)
|
|
468
|
+
|
|
469
|
+
`loginWithRemoteAuth` signs in by scanning a QR code with the Discord mobile app — no desktop app or token extraction required. It runs Discord's Remote Auth protocol and hands you a QR URL to display; once the user scans and confirms on their phone, you receive the user token.
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
import { DiscordClient, loginWithRemoteAuth } from 'agent-messenger/discord'
|
|
473
|
+
|
|
474
|
+
const session = await loginWithRemoteAuth({
|
|
475
|
+
onQrUrl: (url) => {
|
|
476
|
+
// Render this URL as a QR code (e.g. with the `qrcode` package)
|
|
477
|
+
console.log('Scan this with the Discord mobile app:', url)
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const client = await new DiscordClient().login({ token: session.token })
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
`session` is `{ token, user }`, where `user` may be `null` if the gateway skips the user payload — call `await client.testAuth()` after login if you need the identity. If Discord requires a captcha on the final token exchange, the call rejects with a `DiscordError` of code `remote_auth_captcha`; fall back to `auth extract` in that case. Requires a phone with the Discord app, so it cannot run headlessly.
|
|
485
|
+
|
|
448
486
|
### Example
|
|
449
487
|
|
|
450
488
|
```typescript
|
|
@@ -471,12 +509,10 @@ await client.sendMessage(thread.id, 'First message in thread')
|
|
|
471
509
|
`DiscordListener` connects to Discord's Gateway WebSocket for instant event streaming:
|
|
472
510
|
|
|
473
511
|
```typescript
|
|
474
|
-
import { DiscordClient, DiscordListener
|
|
512
|
+
import { DiscordClient, DiscordListener } from 'agent-messenger/discord'
|
|
475
513
|
|
|
476
514
|
const client = await new DiscordClient().login()
|
|
477
|
-
const listener = new DiscordListener(client
|
|
478
|
-
intents: DiscordIntent.Guilds | DiscordIntent.GuildMessages | DiscordIntent.MessageContent,
|
|
479
|
-
})
|
|
515
|
+
const listener = new DiscordListener(client)
|
|
480
516
|
|
|
481
517
|
listener.on('message_create', (event) => {
|
|
482
518
|
console.log(`${event.author.username}: ${event.content}`)
|