cclawd 1.0.6 → 1.0.7
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/dist/{active-listener-Dkhmfuwx.js → active-listener-DYmI7imH.js} +2 -2
- package/dist/{api-key-rotation-BOfI3cG3.js → api-key-rotation-DLU4jvSu.js} +1 -1
- package/dist/{audio-preflight-CtkZ5SAs.js → audio-preflight-C9TMbRb4.js} +15 -15
- package/dist/{audio-transcription-runner-CbPqoiHX.js → audio-transcription-runner-Q5zG_hYd.js} +10 -10
- package/dist/{audit-membership-runtime-hXUuer4x.js → audit-membership-runtime-DhxSwFnF.js} +6 -6
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +35 -35
- package/dist/bundled/bootstrap-extra-files/handler.js +5 -5
- package/dist/bundled/command-logger/handler.js +2 -2
- package/dist/bundled/session-memory/handler.js +35 -35
- package/dist/{channel-activity-DWAER4wd.js → channel-activity-Bx08UTAg.js} +2 -2
- package/dist/{commands-registry-BUyiA7nE.js → commands-registry-llLVCTH9.js} +2 -2
- package/dist/compact.runtime-BEn3giMt.js +39 -0
- package/dist/{deliver-aHOaRbkt.js → deliver-DJf2ZBpe.js} +19 -19
- package/dist/deliver-runtime-DkQ3XzGv.js +19 -0
- package/dist/deps-send-discord.runtime-BLpqSj6s.js +19 -0
- package/dist/deps-send-imessage.runtime-BFzyYqvR.js +18 -0
- package/dist/deps-send-signal.runtime-DT0TYCy1.js +17 -0
- package/dist/deps-send-slack.runtime-BhaGFfMX.js +17 -0
- package/dist/deps-send-telegram.runtime-B6Cic9NX.js +20 -0
- package/dist/deps-send-whatsapp.runtime-WtEhIq2S.js +43 -0
- package/dist/{diagnostic-BdcXX9iJ.js → diagnostic-BCCMF3O_.js} +2 -2
- package/dist/{env-lw2hsIUY.js → env-aYXLHjfZ.js} +1 -1
- package/dist/{fetch-D9NUULbj.js → fetch-DCTUdr1U.js} +5 -5
- package/dist/{fetch-C0iyt-Iz.js → fetch-bvgIiupu.js} +3 -3
- package/dist/{fetch-guard-B5ZMnGaN.js → fetch-guard-CqpEmMQ2.js} +2 -2
- package/dist/{frontmatter-C_obXuTp.js → frontmatter-DjZuS525.js} +3 -3
- package/dist/{github-copilot-token-8N63GdbE.js → github-copilot-token-CQmATy5E.js} +7 -7
- package/dist/{image-4x07m4Jl.js → image-Q8E1-lZn.js} +4 -4
- package/dist/image-runtime-B1LFYfQ2.js +12 -0
- package/dist/{ir-CsgNUpOU.js → ir-CzM3SxId.js} +6 -6
- package/dist/llm-slug-generator.js +35 -35
- package/dist/{logger-CbUVl62f.js → logger-ChbX1G7s.js} +7 -7
- package/dist/{login-p_O59TVQ.js → login-B0mtU11X.js} +4 -4
- package/dist/{login-qr-BCJpDsAy.js → login-qr-DY_i60f5.js} +10 -10
- package/dist/{manager-CwYv8O3T.js → manager-FAQPC0uO.js} +12 -12
- package/dist/manager-runtime-Da7ME9vS.js +15 -0
- package/dist/{model-selection-Cv2Puf5z.js → model-selection-wf3OY5DX.js} +122 -122
- package/dist/{outbound-Chpiwybe.js → outbound-Bw0dOVS7.js} +6 -6
- package/dist/{outbound-attachment-BnAVJDLe.js → outbound-attachment-1R6r9Pg_.js} +2 -2
- package/dist/{paths-CehYKFsO.js → paths-C0HLtPu0.js} +7 -7
- package/dist/{paths-DkxwiA8g.js → paths-hfkBoC7i.js} +5 -5
- package/dist/{pi-embedded-CJVNBk0y.js → pi-embedded-BAHaY-Oh.js} +150 -150
- package/dist/{pi-model-discovery-7IzK0Uc3.js → pi-model-discovery-ItS07aJB.js} +7 -7
- package/dist/pi-model-discovery-runtime-DjM7Z1fx.js +12 -0
- package/dist/{pi-tools.before-tool-call.runtime-BP2UvGJb.js → pi-tools.before-tool-call.runtime-D_mthvtC.js} +10 -10
- package/dist/plugin-sdk/signal.js +2 -2
- package/dist/{proxy-fetch-BOh1PLOW.js → proxy-fetch-c1ZUFFcO.js} +1 -1
- package/dist/{pw-ai-DwH5GpEO.js → pw-ai-Ok6KGelf.js} +9 -9
- package/dist/{qmd-manager-DEscZz5_.js → qmd-manager-DhfEz4Ar.js} +6 -6
- package/dist/{query-expansion-BErUY8P2.js → query-expansion-GqNV2iIE.js} +4 -4
- package/dist/runtime-whatsapp-login.runtime-D4BRhQkK.js +13 -0
- package/dist/runtime-whatsapp-outbound.runtime-DJPpS6g-.js +17 -0
- package/dist/{send-DUibfNQD.js → send-6R8b9zsj.js} +5 -5
- package/dist/{send-ORtn50qg.js → send-CEg4P96c.js} +5 -5
- package/dist/{send-C-Q_WPMf.js → send-CS0ocZHl.js} +3 -3
- package/dist/{send-DtBvCnPQ.js → send-DPflcjM5.js} +6 -6
- package/dist/{send-BDnOgWIp.js → send-DwAoiT2p.js} +25 -25
- package/dist/{session-B7imi6T5.js → session-BoIID5UR.js} +7 -7
- package/dist/{skill-commands-B9brPuiL.js → skill-commands-DhdiziMs.js} +9 -9
- package/dist/slash-commands.runtime-Cu1lTjV9.js +12 -0
- package/dist/slash-dispatch.runtime-DRVJEF4l.js +39 -0
- package/dist/slash-skill-commands.runtime-C373PJjv.js +13 -0
- package/dist/subagent-registry-runtime-D7hWBo1G.js +39 -0
- package/dist/{subsystem-DfXy5gUB.js → subsystem-C8z6w6xC.js} +14 -14
- package/dist/{tables-CjQqTOdD.js → tables-DQusRhkD.js} +1 -1
- package/dist/{target-errors-BZE1mc-W.js → target-errors-CfavnC9U.js} +1 -1
- package/dist/{tokens-6ul2IrzG.js → tokens-BWDIKewp.js} +1 -1
- package/dist/{web-Cd8yK1Zq.js → web-CrcrTQ2c.js} +39 -39
- package/dist/{whatsapp-actions-CYEzUMBI.js → whatsapp-actions-B0u0ZAme.js} +15 -15
- package/dist/{workspace-DGIcKCCW.js → workspace-CWDYHR27.js} +20 -20
- package/extensions/mfa-auth/index.ts +1028 -675
- package/extensions/mfa-auth/node_modules/.package-lock.json +21 -0
- package/extensions/mfa-auth/node_modules/ws/LICENSE +20 -0
- package/extensions/mfa-auth/node_modules/ws/README.md +548 -0
- package/extensions/mfa-auth/node_modules/ws/browser.js +8 -0
- package/extensions/mfa-auth/node_modules/ws/index.js +13 -0
- package/extensions/mfa-auth/node_modules/ws/lib/buffer-util.js +131 -0
- package/extensions/mfa-auth/node_modules/ws/lib/constants.js +19 -0
- package/extensions/mfa-auth/node_modules/ws/lib/event-target.js +292 -0
- package/extensions/mfa-auth/node_modules/ws/lib/extension.js +203 -0
- package/extensions/mfa-auth/node_modules/ws/lib/limiter.js +55 -0
- package/extensions/mfa-auth/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/extensions/mfa-auth/node_modules/ws/lib/receiver.js +706 -0
- package/extensions/mfa-auth/node_modules/ws/lib/sender.js +602 -0
- package/extensions/mfa-auth/node_modules/ws/lib/stream.js +161 -0
- package/extensions/mfa-auth/node_modules/ws/lib/subprotocol.js +62 -0
- package/extensions/mfa-auth/node_modules/ws/lib/validation.js +152 -0
- package/extensions/mfa-auth/node_modules/ws/lib/websocket-server.js +554 -0
- package/extensions/mfa-auth/node_modules/ws/lib/websocket.js +1393 -0
- package/extensions/mfa-auth/node_modules/ws/package.json +69 -0
- package/extensions/mfa-auth/node_modules/ws/wrapper.mjs +8 -0
- package/extensions/mfa-auth/openclaw.plugin.json +1 -1
- package/extensions/mfa-auth/package-lock.json +23 -1
- package/extensions/mfa-auth/src/notification-service.ts +8 -2
- package/package.json +1 -1
- package/dist/compact.runtime-DpcZpcTl.js +0 -39
- package/dist/deliver-runtime-D4bCsr6d.js +0 -19
- package/dist/deps-send-discord.runtime-BziKU-pE.js +0 -19
- package/dist/deps-send-imessage.runtime-CFRnDTqp.js +0 -18
- package/dist/deps-send-signal.runtime-BuOtABJm.js +0 -17
- package/dist/deps-send-slack.runtime-BOLqvMxW.js +0 -17
- package/dist/deps-send-telegram.runtime-DeEoFLv5.js +0 -20
- package/dist/deps-send-whatsapp.runtime-CG1uXYLY.js +0 -43
- package/dist/image-runtime-smkMrIol.js +0 -12
- package/dist/manager-runtime-D_jEoBr9.js +0 -15
- package/dist/pi-model-discovery-runtime-DABef3qy.js +0 -12
- package/dist/runtime-whatsapp-login.runtime-BI3U306v.js +0 -13
- package/dist/runtime-whatsapp-outbound.runtime-Bsc2uD09.js +0 -17
- package/dist/slash-commands.runtime-Cf6ygfBp.js +0 -12
- package/dist/slash-dispatch.runtime-CsmvhO5K.js +0 -39
- package/dist/slash-skill-commands.runtime-CX7stIEP.js +0 -13
- package/dist/subagent-registry-runtime-B_S1nf7y.js +0 -39
|
@@ -1,712 +1,1065 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { authManager } from "./src/auth-manager.js";
|
|
3
|
-
import { config } from "./src/config.js";
|
|
4
|
-
import { dabbyClient } from "./src/dabby-client.js";
|
|
5
|
-
import { NotificationService } from "./src/notification-service.js";
|
|
6
|
-
import { qrCodeAuthProvider } from "./src/providers/qr-code.js";
|
|
7
|
-
import { setNotifyCallback } from "./src/server.js";
|
|
8
|
-
import type { AuthSession } from "./src/types.js";
|
|
9
|
-
|
|
10
|
-
const notificationService = NotificationService.getInstance();
|
|
11
|
-
const pendingAuthUsers = new Set<string>();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
channel
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { authManager } from "./src/auth-manager.js";
|
|
3
|
+
import { config } from "./src/config.js";
|
|
4
|
+
import { dabbyClient } from "./src/dabby-client.js";
|
|
5
|
+
import { NotificationService } from "./src/notification-service.js";
|
|
6
|
+
import { qrCodeAuthProvider } from "./src/providers/qr-code.js";
|
|
7
|
+
import { setNotifyCallback } from "./src/server.js";
|
|
8
|
+
import type { AuthSession } from "./src/types.js";
|
|
9
|
+
|
|
10
|
+
const notificationService = NotificationService.getInstance();
|
|
11
|
+
const pendingAuthUsers = new Set<string>();
|
|
12
|
+
|
|
13
|
+
function isWebchatChannel(channel: string | undefined): boolean {
|
|
14
|
+
if (!channel) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const normalized = channel.trim().toLowerCase();
|
|
18
|
+
return normalized === "webchat" || normalized === "web";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isPeerKindToken(value: string | undefined): boolean {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const normalized = value.trim().toLowerCase();
|
|
26
|
+
return normalized === "direct" || normalized === "group" || normalized === "dm";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseSessionContextFromKey(sessionKey: string | undefined): {
|
|
30
|
+
channel?: string;
|
|
31
|
+
accountId?: string;
|
|
32
|
+
to?: string;
|
|
33
|
+
} {
|
|
34
|
+
const parts = String(sessionKey || "")
|
|
35
|
+
.split(":")
|
|
36
|
+
.map((part) => part.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
if (parts.length === 0) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const channel = parts[2];
|
|
43
|
+
let accountId = parts[3];
|
|
44
|
+
const to = parts[parts.length - 1];
|
|
45
|
+
|
|
46
|
+
// In common session keys, index 3 is peer kind (direct/group/dm), not account id.
|
|
47
|
+
if (isPeerKindToken(accountId)) {
|
|
48
|
+
accountId = undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
channel: channel || undefined,
|
|
53
|
+
accountId: accountId || undefined,
|
|
54
|
+
to: to || undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function addPendingAuthCandidates(...ids: Array<string | undefined>): void {
|
|
59
|
+
for (const id of expandAuthIdCandidates(ids)) {
|
|
60
|
+
pendingAuthUsers.add(id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findPendingAuthCandidate(eventTo: string, conversationId?: string): string | undefined {
|
|
65
|
+
for (const candidate of expandAuthIdCandidates([eventTo, conversationId])) {
|
|
66
|
+
if (pendingAuthUsers.has(candidate)) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeAuthId(value: string | undefined): string {
|
|
74
|
+
const raw = String(value ?? "").trim();
|
|
75
|
+
if (!raw) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
const lower = raw.toLowerCase();
|
|
79
|
+
if (lower.startsWith("agent:main:")) {
|
|
80
|
+
return raw.slice("agent:main:".length);
|
|
81
|
+
}
|
|
82
|
+
if (lower.startsWith("webchat:")) {
|
|
83
|
+
return raw.slice("webchat:".length);
|
|
84
|
+
}
|
|
85
|
+
if (lower.startsWith("web:")) {
|
|
86
|
+
return raw.slice("web:".length);
|
|
87
|
+
}
|
|
88
|
+
return raw;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function expandAuthIdCandidates(values: Array<string | undefined>): string[] {
|
|
92
|
+
const result = new Set<string>();
|
|
93
|
+
for (const value of values) {
|
|
94
|
+
const raw = String(value ?? "").trim();
|
|
95
|
+
if (!raw) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
result.add(raw);
|
|
99
|
+
const normalized = normalizeAuthId(raw);
|
|
100
|
+
if (normalized) {
|
|
101
|
+
result.add(normalized);
|
|
102
|
+
result.add(`agent:main:${normalized}`);
|
|
103
|
+
result.add(`webchat:${normalized}`);
|
|
104
|
+
result.add(`web:${normalized}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Array.from(result);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function authIdEquals(a: string | undefined, b: string | undefined): boolean {
|
|
111
|
+
const aa = String(a ?? "").trim();
|
|
112
|
+
const bb = String(b ?? "").trim();
|
|
113
|
+
if (!aa || !bb) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return aa === bb || normalizeAuthId(aa) === normalizeAuthId(bb);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolvePendingAuthSession(candidateIds: string[]): {
|
|
120
|
+
matchedId: string;
|
|
121
|
+
session: AuthSession;
|
|
122
|
+
} | null {
|
|
123
|
+
const uniqueCandidates = expandAuthIdCandidates(candidateIds);
|
|
124
|
+
|
|
125
|
+
for (const id of uniqueCandidates) {
|
|
126
|
+
const hasPendingCandidate = Array.from(pendingAuthUsers).some((pending) => authIdEquals(pending, id));
|
|
127
|
+
if (!hasPendingCandidate) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const session =
|
|
131
|
+
authManager.getLatestSessionByUserId(id) ||
|
|
132
|
+
authManager.getLatestSessionByUserId(normalizeAuthId(id));
|
|
133
|
+
if (session?.metadata && typeof (session.metadata as Record<string, unknown>).qrCodeUrl === "string") {
|
|
134
|
+
return { matchedId: id, session };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let latest: { matchedId: string; session: AuthSession } | null = null;
|
|
139
|
+
for (const sessionId of authManager.getSessionIds()) {
|
|
140
|
+
const session = authManager.getSession(sessionId);
|
|
141
|
+
if (!session?.metadata) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const metadata = session.metadata as Record<string, unknown>;
|
|
145
|
+
if (typeof metadata.qrCodeUrl !== "string") {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const matches = uniqueCandidates.some((candidate) => {
|
|
149
|
+
return (
|
|
150
|
+
authIdEquals(candidate, session.userId) ||
|
|
151
|
+
authIdEquals(candidate, String(session.originalContext.sessionKey ?? "")) ||
|
|
152
|
+
authIdEquals(candidate, String(session.originalContext.to ?? ""))
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
if (!matches) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!latest || session.timestamp > latest.session.timestamp) {
|
|
159
|
+
latest = {
|
|
160
|
+
matchedId:
|
|
161
|
+
uniqueCandidates.find((candidate) => candidate === session.userId) || session.userId,
|
|
162
|
+
session,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return latest;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveGlobalPendingFirstMessageSession(): AuthSession | null {
|
|
171
|
+
let latestPending: AuthSession | null = null;
|
|
172
|
+
|
|
173
|
+
for (const sessionId of authManager.getSessionIds()) {
|
|
174
|
+
const session = authManager.getSession(sessionId);
|
|
175
|
+
if (!session?.metadata) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const metadata = session.metadata as Record<string, unknown>;
|
|
180
|
+
if (metadata.triggerType !== "first_message") {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (typeof metadata.qrCodeUrl !== "string" || !metadata.qrCodeUrl.trim()) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (authManager.isUserVerifiedForFirstMessage(session.userId)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!latestPending || session.timestamp > latestPending.timestamp) {
|
|
191
|
+
latestPending = session;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return latestPending;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildFirstMessageChallengeText(metadata: Record<string, unknown>): string {
|
|
199
|
+
const qrCodeUrl = typeof metadata.qrCodeUrl === "string" ? metadata.qrCodeUrl : "";
|
|
200
|
+
const isReauth = metadata.isReauth === true;
|
|
201
|
+
if (isReauth) {
|
|
202
|
+
return `\u9700\u8981\u91cd\u65b0\u8ba4\u8bc1\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002`;
|
|
203
|
+
}
|
|
204
|
+
return `\u9996\u6b21\u5bf9\u8bdd\u9700\u8981\u8ba4\u8bc1\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function uniqueNonEmpty(values: Array<string | undefined>): string[] {
|
|
208
|
+
return Array.from(
|
|
209
|
+
new Set(
|
|
210
|
+
values
|
|
211
|
+
.map((value) => String(value ?? "").trim())
|
|
212
|
+
.filter(Boolean),
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isAnyFirstMessageAliasVerified(userIds: string[]): string | undefined {
|
|
218
|
+
for (const userId of userIds) {
|
|
219
|
+
if (authManager.isUserVerifiedForFirstMessage(userId)) {
|
|
220
|
+
return userId;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function consumeNotificationByAliases(userIds: string[]): {
|
|
227
|
+
triggerType: "first_message" | "sensitive_operation";
|
|
228
|
+
isReauth: boolean;
|
|
229
|
+
} | null {
|
|
230
|
+
for (const userId of userIds) {
|
|
231
|
+
const notification = authManager.checkAndConsumeNotification(userId);
|
|
232
|
+
if (notification) {
|
|
233
|
+
return notification;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveInboundAuthUserId(params: {
|
|
240
|
+
channelId?: string;
|
|
241
|
+
from?: string;
|
|
242
|
+
senderId?: string;
|
|
243
|
+
conversationId?: string;
|
|
244
|
+
metadataTo?: string;
|
|
245
|
+
metadataOriginatingTo?: string;
|
|
246
|
+
}): string {
|
|
247
|
+
const candidates = [
|
|
248
|
+
params.from,
|
|
249
|
+
params.senderId,
|
|
250
|
+
params.conversationId,
|
|
251
|
+
params.metadataOriginatingTo,
|
|
252
|
+
params.metadataTo,
|
|
253
|
+
];
|
|
254
|
+
const firstNonEmpty = candidates
|
|
255
|
+
.map((value) => String(value ?? "").trim())
|
|
256
|
+
.find((value) => value.length > 0);
|
|
257
|
+
|
|
258
|
+
if (isWebchatChannel(params.channelId)) {
|
|
259
|
+
return firstNonEmpty || "unknown";
|
|
260
|
+
}
|
|
261
|
+
return firstNonEmpty || "unknown";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function sendAuthMessage(
|
|
265
|
+
channel: string | undefined,
|
|
266
|
+
accountId: string | undefined,
|
|
267
|
+
to: string,
|
|
268
|
+
message: string,
|
|
269
|
+
userId: string,
|
|
270
|
+
overrideSessionKey?: string,
|
|
271
|
+
): Promise<void> {
|
|
272
|
+
const session: AuthSession = {
|
|
273
|
+
userId,
|
|
274
|
+
sessionId: "notification",
|
|
275
|
+
authMethod: "qr-code",
|
|
276
|
+
timestamp: Date.now(),
|
|
277
|
+
originalContext: {
|
|
278
|
+
sessionKey: overrideSessionKey || `${channel || "web"}:${accountId || ""}:${userId}`,
|
|
279
|
+
senderId: userId,
|
|
280
|
+
commandBody: "",
|
|
281
|
+
channel: channel || "web",
|
|
282
|
+
accountId: accountId || "",
|
|
283
|
+
to,
|
|
284
|
+
toolName: "notification",
|
|
285
|
+
toolParams: {},
|
|
286
|
+
timestamp: Date.now(),
|
|
287
|
+
triggerType: "sensitive_operation",
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
await notificationService.sendAuthNotification(session, message);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default function register(api: OpenClawPluginApi) {
|
|
295
|
+
console.log("[mfa-auth] Plugin registration started");
|
|
296
|
+
authManager.registerProvider(qrCodeAuthProvider);
|
|
297
|
+
|
|
298
|
+
notificationService.setConfig(api.config);
|
|
299
|
+
|
|
300
|
+
setNotifyCallback(async (session: AuthSession) => {
|
|
301
|
+
api.logger.info(`[mfa-auth] User ${session.userId} verified`);
|
|
302
|
+
|
|
303
|
+
if (!config.enableAuthNotification) {
|
|
304
|
+
api.logger.info(`[mfa-auth] Auth notification disabled, skipping message send.`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const commandBody = session.originalContext.commandBody;
|
|
310
|
+
const triggerType = session.originalContext.triggerType || "sensitive_operation";
|
|
311
|
+
|
|
312
|
+
const isFirstMessageAuth = triggerType === "first_message";
|
|
313
|
+
const isReauth = commandBody.trim() === "/reauth";
|
|
314
|
+
|
|
315
|
+
let messageText = "";
|
|
316
|
+
if (isFirstMessageAuth) {
|
|
317
|
+
messageText = isReauth
|
|
318
|
+
? "\u2705 \u91cd\u65b0\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u91cd\u65b0\u53d1\u9001\u6d88\u606f\u7ee7\u7eed\u5bf9\u8bdd\u3002"
|
|
319
|
+
: "\u2705 \u9996\u6b21\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u91cd\u65b0\u53d1\u9001\u6d88\u606f\u7ee7\u7eed\u5bf9\u8bdd\u3002";
|
|
320
|
+
} else {
|
|
321
|
+
messageText = "\u2705 \u4e8c\u6b21\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u91cd\u65b0\u53d1\u9001\u4e4b\u524d\u7684\u547d\u4ee4\u6216\u56de\u590d\u201c\u786e\u8ba4\u201d\u3002";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const channel = session.originalContext.channel;
|
|
325
|
+
const sessionKey =
|
|
326
|
+
session.originalContext.sessionKey ||
|
|
327
|
+
`${channel}:${session.originalContext.accountId || ""}:${session.userId}`;
|
|
328
|
+
|
|
329
|
+
api.logger.info(`[mfa-auth] Sending notification to session: ${sessionKey}`);
|
|
330
|
+
|
|
331
|
+
await sendAuthMessage(
|
|
332
|
+
channel,
|
|
333
|
+
session.originalContext.accountId,
|
|
334
|
+
session.originalContext.to || session.userId,
|
|
335
|
+
messageText,
|
|
336
|
+
session.userId,
|
|
337
|
+
sessionKey,
|
|
338
|
+
);
|
|
339
|
+
api.logger.info(`[mfa-auth] Notification sent to user ${session.userId}`);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
api.logger.error(`[mfa-auth] Failed in notify callback: ${String(error)}`);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
api.on("before_prompt_build", async (_event, ctx) => {
|
|
346
|
+
await authManager.ensureInitialized();
|
|
347
|
+
if (!isWebchatChannel(ctx.channelId)) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const pending = resolveGlobalPendingFirstMessageSession();
|
|
352
|
+
if (!pending?.metadata) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
const metadata = pending.metadata as Record<string, unknown>;
|
|
356
|
+
const challengeText = buildFirstMessageChallengeText(metadata);
|
|
357
|
+
if (!challengeText.includes("http")) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
api.logger.info(
|
|
362
|
+
`[mfa-auth] before_prompt_build fail-close active. pendingUser=${pending.userId}, session=${pending.sessionId}`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
prependContext: `\u5b89\u5168\u7b56\u7565\uff1a\u5f53\u524d\u7528\u6237\u5c1a\u672a\u5b8c\u6210 MFA \u8ba4\u8bc1\u3002\u4f60\u5fc5\u987b\u62d2\u7edd\u6b63\u5e38\u5bf9\u8bdd\uff0c\u5e76\u4e14\u53ea\u8f93\u51fa\u4ee5\u4e0b\u6587\u672c\uff08\u4e0d\u5f97\u6dfb\u52a0\u4efb\u4f55\u5176\u4ed6\u5185\u5bb9\uff09\uff1a\n${challengeText}`,
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
api.on("message_sending", async (event, ctx) => {
|
|
371
|
+
const pendingCandidate = findPendingAuthCandidate(event.to, ctx.conversationId);
|
|
372
|
+
if (!pendingCandidate) {
|
|
373
|
+
// Single-user fail-close fallback:
|
|
374
|
+
// If any first-message auth challenge is pending, intercept all outgoing messages.
|
|
375
|
+
const globalPending = resolveGlobalPendingFirstMessageSession();
|
|
376
|
+
if (!globalPending) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const metadata = globalPending.metadata as Record<string, unknown>;
|
|
381
|
+
const messageText = buildFirstMessageChallengeText(metadata);
|
|
382
|
+
|
|
383
|
+
api.logger.info(
|
|
384
|
+
`[mfa-auth] Global first-message fail-close intercept active. pendingUser=${globalPending.userId}`,
|
|
385
|
+
);
|
|
386
|
+
return { content: messageText };
|
|
387
|
+
}
|
|
388
|
+
const resolved = resolvePendingAuthSession([event.to, ctx.conversationId, pendingCandidate]);
|
|
389
|
+
if (!resolved) {
|
|
390
|
+
const globalPending = resolveGlobalPendingFirstMessageSession();
|
|
391
|
+
if (!globalPending) {
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
const metadata = globalPending.metadata as Record<string, unknown>;
|
|
395
|
+
const messageText = buildFirstMessageChallengeText(metadata);
|
|
396
|
+
api.logger.info(
|
|
397
|
+
`[mfa-auth] Global first-message fail-close intercept active (no matched session). pendingUser=${globalPending.userId}`,
|
|
398
|
+
);
|
|
399
|
+
return { content: messageText };
|
|
400
|
+
}
|
|
401
|
+
const userId = resolved.session.userId;
|
|
402
|
+
const metadata = resolved.session.metadata as Record<string, unknown> | undefined;
|
|
403
|
+
|
|
404
|
+
if (metadata?.qrCodeUrl) {
|
|
405
|
+
pendingAuthUsers.delete(resolved.matchedId);
|
|
406
|
+
pendingAuthUsers.delete(userId);
|
|
407
|
+
|
|
408
|
+
let messageText = "";
|
|
409
|
+
if (metadata.triggerType === "first_message") {
|
|
410
|
+
const isReauth = metadata.isReauth === true;
|
|
411
|
+
messageText = isReauth
|
|
412
|
+
? `\u9700\u8981\u91cd\u65b0\u8ba4\u8bc1\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${metadata.qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002`
|
|
413
|
+
: `\u9996\u6b21\u5bf9\u8bdd\u9700\u8981\u8ba4\u8bc1\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${metadata.qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002`;
|
|
414
|
+
} else if (metadata.triggerType === "sensitive_operation") {
|
|
415
|
+
messageText = `\u68c0\u6d4b\u5230\u654f\u611f\u64cd\u4f5c\uff0c\u9700\u4e8c\u6b21\u8ba4\u8bc1\u3002\n\n\u64cd\u4f5c\u5185\u5bb9\uff1a${metadata.commandPreview}\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${metadata.qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002\n\n\u8ba4\u8bc1\u5b8c\u6210\u540e\uff0c\u8bf7\u91cd\u65b0\u53d1\u9001\u4e0a\u4e00\u6761\u547d\u4ee4\u6216\u56de\u590d\u201c\u786e\u8ba4\u201d\u3002`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { content: messageText };
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
api.on("before_tool_call", async (event, ctx) => {
|
|
423
|
+
await authManager.ensureInitialized();
|
|
424
|
+
if (!config.requireAuthOnSensitiveOperation) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const { toolName, params } = event;
|
|
429
|
+
|
|
430
|
+
api.logger.info(`[mfa-auth] Tool call detected: ${toolName}`);
|
|
431
|
+
|
|
432
|
+
const sensitiveTools = ["bash", "exec", "runCommand", "command", "process"];
|
|
433
|
+
if (!sensitiveTools.includes(toolName)) {
|
|
434
|
+
api.logger.info(`[mfa-auth] Tool ${toolName} is not in sensitive list, allowing`);
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const command =
|
|
439
|
+
typeof params?.command === "string"
|
|
440
|
+
? params.command
|
|
441
|
+
: typeof params?.cmd === "string"
|
|
442
|
+
? params.cmd
|
|
443
|
+
: typeof params?.input === "string"
|
|
444
|
+
? params.input
|
|
445
|
+
: typeof params?.args === "string"
|
|
446
|
+
? params.args
|
|
447
|
+
: "";
|
|
448
|
+
|
|
449
|
+
api.logger.info(`[mfa-auth] Extracted command from ${toolName}: ${command}`);
|
|
450
|
+
|
|
451
|
+
if (!command) {
|
|
452
|
+
api.logger.info(`[mfa-auth] No command found in params, allowing`);
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const { isSensitive, preview } = checkSensitiveOperation(command);
|
|
457
|
+
if (!isSensitive) {
|
|
458
|
+
api.logger.info(`[mfa-auth] Command is not sensitive, allowing`);
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const sessionKey = ctx.sessionKey || "";
|
|
463
|
+
const parsed = parseSessionContextFromKey(sessionKey);
|
|
464
|
+
const userId = isWebchatChannel(parsed.channel)
|
|
465
|
+
? parsed.to || sessionKey || "unknown"
|
|
466
|
+
: sessionKey || "unknown";
|
|
467
|
+
|
|
468
|
+
if (authManager.isUserVerifiedForSensitiveOps(userId)) {
|
|
469
|
+
api.logger.info(`[mfa-auth] User ${userId} is verified for sensitive ops, allowing`);
|
|
470
|
+
|
|
471
|
+
const notificationInfo = authManager.checkAndConsumeNotification(userId);
|
|
472
|
+
if (notificationInfo) {
|
|
473
|
+
const parsedChannel = parsed.channel;
|
|
474
|
+
const parsedAccountId = parsed.accountId;
|
|
475
|
+
const parsedTo = parsed.to;
|
|
476
|
+
|
|
477
|
+
const targetSessionKey =
|
|
478
|
+
isWebchatChannel(parsedChannel) ? sessionKey || userId : sessionKey;
|
|
479
|
+
|
|
480
|
+
sendAuthMessage(
|
|
481
|
+
parsedChannel,
|
|
482
|
+
parsedAccountId,
|
|
483
|
+
parsedTo || userId,
|
|
484
|
+
"\u2705 \u4e8c\u6b21\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u91cd\u65b0\u53d1\u9001\u4e4b\u524d\u7684\u547d\u4ee4\u6216\u56de\u590d\u201c\u786e\u8ba4\u201d\u3002",
|
|
485
|
+
userId,
|
|
486
|
+
targetSessionKey,
|
|
487
|
+
).catch((err) =>
|
|
488
|
+
api.logger.error(`[mfa-auth] Failed to send success notification: ${err}`),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
api.logger.info(`[mfa-auth] User ${userId} is NOT verified for sensitive ops.`);
|
|
496
|
+
|
|
497
|
+
const parsedChannel = parsed.channel;
|
|
498
|
+
const parsedAccountId = parsed.accountId;
|
|
499
|
+
const parsedTo = parsed.to;
|
|
500
|
+
|
|
501
|
+
api.logger.info(
|
|
502
|
+
`[mfa-auth] Parsed from sessionKey: channel=${parsedChannel}, accountId=${parsedAccountId}, to=${parsedTo}`,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const session = await authManager.generateSession(userId, {
|
|
506
|
+
sessionKey,
|
|
507
|
+
senderId: userId,
|
|
508
|
+
commandBody: command,
|
|
509
|
+
channel: parsedChannel,
|
|
510
|
+
to: parsedTo,
|
|
511
|
+
accountId: parsedAccountId,
|
|
512
|
+
toolName,
|
|
513
|
+
toolParams: params,
|
|
514
|
+
timestamp: Date.now(),
|
|
515
|
+
triggerType: "sensitive_operation",
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (!session) {
|
|
519
|
+
api.logger.error(`[mfa-auth] Failed to generate session for user ${userId}`);
|
|
191
520
|
return undefined;
|
|
192
521
|
}
|
|
193
522
|
|
|
194
|
-
api.logger.info(`[mfa-auth] User ${userId} is NOT verified for sensitive ops.`);
|
|
195
|
-
|
|
196
|
-
const sessionKey = ctx.sessionKey || "";
|
|
197
|
-
const sessionKeyParts = sessionKey.split(":").filter(Boolean);
|
|
198
|
-
|
|
199
|
-
const parsedChannel = sessionKeyParts[2] || undefined;
|
|
200
|
-
let parsedAccountId = sessionKeyParts[3] || undefined;
|
|
201
|
-
const parsedTo = sessionKeyParts[sessionKeyParts.length - 1] || undefined;
|
|
202
|
-
|
|
203
|
-
// Fix: If accountId is "direct" or "group", it's actually the peerKind, not an accountId.
|
|
204
|
-
// This happens when the sessionKey omits the accountId (using default account).
|
|
205
|
-
if (parsedAccountId === "direct" || parsedAccountId === "group") {
|
|
206
|
-
parsedAccountId = undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
523
|
api.logger.info(
|
|
210
|
-
`[mfa-auth]
|
|
524
|
+
`[mfa-auth] Sensitive auth session created: sessionId=${session.sessionId}, hasQrCode=${Boolean(session.qrCodeUrl)}, certToken=${session.certToken ?? "n/a"}`,
|
|
211
525
|
);
|
|
212
526
|
|
|
213
|
-
const session = await authManager.generateSession(userId, {
|
|
214
|
-
sessionKey,
|
|
215
|
-
senderId: userId,
|
|
216
|
-
commandBody: command,
|
|
217
|
-
channel: parsedChannel,
|
|
218
|
-
to: parsedTo,
|
|
219
|
-
accountId: parsedAccountId,
|
|
220
|
-
toolName,
|
|
221
|
-
toolParams: params,
|
|
222
|
-
timestamp: Date.now(),
|
|
223
|
-
triggerType: "sensitive_operation",
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
if (!session) {
|
|
227
|
-
api.logger.error(`[mfa-auth] Failed to generate session for user ${userId}`);
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
527
|
api.logger.info(`[mfa-auth] Blocking sensitive tool call: ${toolName} from ${userId}`);
|
|
232
|
-
|
|
233
|
-
// For webchat, use userId as sessionKey instead of agent:main:<userId>
|
|
234
|
-
if (parsedChannel
|
|
235
|
-
const sessionKeyForWebchat = userId;
|
|
528
|
+
|
|
529
|
+
// For webchat, use userId as sessionKey instead of agent:main:<userId>
|
|
530
|
+
if (isWebchatChannel(parsedChannel)) {
|
|
531
|
+
const sessionKeyForWebchat = sessionKey || userId;
|
|
532
|
+
const authChallengeText = `检测到敏感操作,需二次认证。\n\n操作内容:${preview}\n\n请点击下方链接完成扫码认证:\n${session.qrCodeUrl}\n\n认证有效期:${Math.floor(config.timeout / 60000)} 分钟。\n\n认证完成后,请重新发送上一条命令或回复“确认”。`;
|
|
236
533
|
|
|
237
534
|
try {
|
|
238
535
|
await sendAuthMessage(
|
|
239
536
|
parsedChannel,
|
|
240
537
|
parsedAccountId,
|
|
241
538
|
parsedTo || userId,
|
|
242
|
-
|
|
539
|
+
authChallengeText,
|
|
243
540
|
userId,
|
|
244
541
|
sessionKeyForWebchat,
|
|
245
542
|
);
|
|
246
|
-
api.logger.info(
|
|
247
|
-
`[mfa-auth] Sent sensitive operation auth notification to webchat: sessionKey=${sessionKeyForWebchat}`,
|
|
248
|
-
);
|
|
249
|
-
} catch (error) {
|
|
250
|
-
api.logger.error(
|
|
251
|
-
`[mfa-auth] Failed to send webchat sensitive auth notification: ${String(error)}`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
// Also add to pending users as fallback
|
|
255
|
-
|
|
256
|
-
authManager.setSessionMetadata(session.sessionId, {
|
|
257
|
-
qrCodeUrl: session.qrCodeUrl,
|
|
258
|
-
triggerType: "sensitive_operation",
|
|
259
|
-
commandPreview: preview,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
startPollingForAuth(api, userId, session.sessionId, {
|
|
263
|
-
triggerType: "sensitive_operation",
|
|
264
|
-
isReauth: false,
|
|
265
|
-
channel: parsedChannel,
|
|
266
|
-
accountId: parsedAccountId,
|
|
267
|
-
to: parsedTo,
|
|
268
|
-
sessionKey: sessionKeyForWebchat,
|
|
269
|
-
});
|
|
270
|
-
|
|
543
|
+
api.logger.info(
|
|
544
|
+
`[mfa-auth] Sent sensitive operation auth notification to webchat: sessionKey=${sessionKeyForWebchat}`,
|
|
545
|
+
);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
api.logger.error(
|
|
548
|
+
`[mfa-auth] Failed to send webchat sensitive auth notification: ${String(error)}`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
// Also add to pending users as fallback
|
|
552
|
+
addPendingAuthCandidates(userId, sessionKeyForWebchat, parsedTo);
|
|
553
|
+
authManager.setSessionMetadata(session.sessionId, {
|
|
554
|
+
qrCodeUrl: session.qrCodeUrl,
|
|
555
|
+
triggerType: "sensitive_operation",
|
|
556
|
+
commandPreview: preview,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
startPollingForAuth(api, userId, session.sessionId, {
|
|
560
|
+
triggerType: "sensitive_operation",
|
|
561
|
+
isReauth: false,
|
|
562
|
+
channel: parsedChannel,
|
|
563
|
+
accountId: parsedAccountId,
|
|
564
|
+
to: parsedTo,
|
|
565
|
+
sessionKey: sessionKeyForWebchat,
|
|
566
|
+
});
|
|
567
|
+
|
|
271
568
|
return {
|
|
272
569
|
block: true,
|
|
273
|
-
blockReason:
|
|
570
|
+
blockReason: authChallengeText,
|
|
274
571
|
};
|
|
275
572
|
}
|
|
276
573
|
|
|
574
|
+
const authChallengeText = `检测到敏感操作,需二次认证。\n\n操作内容:${preview}\n\n请点击下方链接完成扫码认证:\n${session.qrCodeUrl}\n\n认证有效期:${Math.floor(config.timeout / 60000)} 分钟。\n\n认证完成后,请重新发送上一条命令或回复“确认”。`;
|
|
575
|
+
|
|
277
576
|
if (parsedChannel && parsedChannel !== "web") {
|
|
278
577
|
if (parsedChannel !== "feishu") {
|
|
279
578
|
api.logger.warn(
|
|
280
579
|
`[mfa-auth] Channel ${parsedChannel} not supported, skipping auth notification`,
|
|
281
580
|
);
|
|
282
581
|
} else {
|
|
283
|
-
const messageText = `🔐 该操作需要二次认证\n\n检测到敏感操作: ${preview}\n\n📱 请点击以下链接完成扫码认证:\n${session.qrCodeUrl}\n\n验证有效期: ${Math.floor(config.timeout / 60000)} 分钟\n\n验证成功后,请回复"确认"或者重新发送之前的命令以继续执行。`;
|
|
284
|
-
|
|
285
582
|
await sendAuthMessage(
|
|
286
583
|
parsedChannel,
|
|
287
584
|
parsedAccountId,
|
|
288
585
|
parsedTo || userId,
|
|
289
|
-
|
|
586
|
+
authChallengeText,
|
|
290
587
|
userId,
|
|
291
588
|
);
|
|
292
|
-
|
|
293
|
-
startPollingForAuth(api, userId, session.sessionId, {
|
|
294
|
-
triggerType: "sensitive_operation",
|
|
295
|
-
isReauth: false,
|
|
296
|
-
channel: parsedChannel,
|
|
297
|
-
accountId: parsedAccountId,
|
|
298
|
-
to: parsedTo,
|
|
299
|
-
sessionKey: ctx.sessionKey || "",
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
authManager.registerPendingExecution(userId, session.sessionId);
|
|
305
|
-
|
|
589
|
+
|
|
590
|
+
startPollingForAuth(api, userId, session.sessionId, {
|
|
591
|
+
triggerType: "sensitive_operation",
|
|
592
|
+
isReauth: false,
|
|
593
|
+
channel: parsedChannel,
|
|
594
|
+
accountId: parsedAccountId,
|
|
595
|
+
to: parsedTo,
|
|
596
|
+
sessionKey: ctx.sessionKey || "",
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
authManager.registerPendingExecution(userId, session.sessionId);
|
|
602
|
+
|
|
306
603
|
return {
|
|
307
604
|
block: true,
|
|
308
|
-
blockReason:
|
|
605
|
+
blockReason: authChallengeText,
|
|
309
606
|
};
|
|
310
607
|
});
|
|
311
|
-
|
|
312
|
-
api.on("message_received", async (event, ctx) => {
|
|
313
|
-
await authManager.ensureInitialized();
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
608
|
+
|
|
609
|
+
api.on("message_received", async (event, ctx) => {
|
|
610
|
+
await authManager.ensureInitialized();
|
|
611
|
+
const metadata =
|
|
612
|
+
event.metadata && typeof event.metadata === "object"
|
|
613
|
+
? (event.metadata as Record<string, unknown>)
|
|
614
|
+
: undefined;
|
|
615
|
+
const metadataSenderId =
|
|
616
|
+
typeof metadata?.senderId === "string" ? metadata.senderId : undefined;
|
|
617
|
+
const metadataTo = typeof metadata?.to === "string" ? metadata.to : undefined;
|
|
618
|
+
const metadataOriginatingTo =
|
|
619
|
+
typeof metadata?.originatingTo === "string" ? metadata.originatingTo : undefined;
|
|
620
|
+
const userId = resolveInboundAuthUserId({
|
|
621
|
+
channelId: ctx.channelId,
|
|
622
|
+
from: event.from,
|
|
623
|
+
senderId: metadataSenderId,
|
|
624
|
+
conversationId: ctx.conversationId,
|
|
625
|
+
metadataTo,
|
|
626
|
+
metadataOriginatingTo,
|
|
627
|
+
});
|
|
628
|
+
const authAliases = expandAuthIdCandidates([
|
|
629
|
+
event.from,
|
|
630
|
+
metadataSenderId,
|
|
631
|
+
ctx.conversationId,
|
|
632
|
+
metadataOriginatingTo,
|
|
633
|
+
metadataTo,
|
|
634
|
+
userId,
|
|
635
|
+
]);
|
|
636
|
+
|
|
637
|
+
api.logger.info(
|
|
638
|
+
`[mfa-auth] First message auth check: config.requireAuthOnFirstMessage=${config.requireAuthOnFirstMessage}`,
|
|
639
|
+
);
|
|
640
|
+
if (config.debug) {
|
|
641
|
+
api.logger.info(
|
|
642
|
+
`[mfa-auth] Inbound auth aliases: userId=${userId}, from=${event.from}, senderId=${metadataSenderId}, conversationId=${ctx.conversationId}, metadata.to=${metadataTo}, metadata.originatingTo=${metadataOriginatingTo}, aliases=${authAliases.join(",")}`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!config.requireAuthOnFirstMessage) {
|
|
647
|
+
api.logger.warn(`[mfa-auth] First message auth is disabled in config, skipping.`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const content = event.content || "";
|
|
652
|
+
const isReauthCommand = content.trim() === "/reauth";
|
|
653
|
+
|
|
654
|
+
if (isReauthCommand) {
|
|
655
|
+
api.logger.info(`[mfa-auth] /reauth command detected, skipping first message auth check`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const verifiedAlias = isAnyFirstMessageAliasVerified(authAliases);
|
|
660
|
+
if (verifiedAlias) {
|
|
661
|
+
api.logger.info(
|
|
662
|
+
`[mfa-auth] User ${userId} already verified for first message (matchedAlias=${verifiedAlias})`,
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
const notificationInfo = consumeNotificationByAliases(authAliases);
|
|
666
|
+
if (notificationInfo) {
|
|
667
|
+
const parsedChannel = ctx.channelId;
|
|
668
|
+
const parsedAccountId = ctx.accountId || "";
|
|
669
|
+
const parsedTo = isWebchatChannel(parsedChannel) ? userId : event.from;
|
|
670
|
+
|
|
671
|
+
let sessionKey = ctx.conversationId;
|
|
672
|
+
if (!sessionKey) {
|
|
673
|
+
if (parsedChannel === "webchat" || parsedChannel === "web") {
|
|
674
|
+
sessionKey = userId;
|
|
675
|
+
} else {
|
|
676
|
+
sessionKey = `${parsedChannel}:${parsedAccountId}:${userId}`;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const messageText = notificationInfo.isReauth
|
|
681
|
+
? "\u2705 \u91cd\u65b0\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u7ee7\u7eed\u5bf9\u8bdd\u3002"
|
|
682
|
+
: "\u2705 \u9996\u6b21\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u7ee7\u7eed\u5bf9\u8bdd\u3002";
|
|
683
|
+
|
|
684
|
+
sendAuthMessage(
|
|
685
|
+
parsedChannel,
|
|
686
|
+
parsedAccountId,
|
|
687
|
+
parsedTo || userId,
|
|
688
|
+
messageText,
|
|
689
|
+
userId,
|
|
690
|
+
sessionKey,
|
|
691
|
+
).catch((err) =>
|
|
692
|
+
api.logger.error(`[mfa-auth] Failed to send success notification: ${err}`),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
api.logger.info(`[mfa-auth] First message from unauthenticated user ${userId}, requiring auth`);
|
|
700
|
+
api.logger.info(
|
|
701
|
+
`[mfa-auth] Debug Context: channelId=${ctx.channelId}, conversationId=${ctx.conversationId}, accountId=${ctx.accountId}, from=${event.from}`,
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const parsedChannel = ctx.channelId;
|
|
705
|
+
const parsedAccountId = ctx.accountId || "";
|
|
706
|
+
const parsedTo = isWebchatChannel(parsedChannel) ? userId : event.from;
|
|
707
|
+
|
|
708
|
+
// Use conversationId as sessionKey if available
|
|
709
|
+
// For webchat, try to use userId directly as sessionKey (common pattern)
|
|
710
|
+
let sessionKey = ctx.conversationId;
|
|
711
|
+
if (!sessionKey) {
|
|
712
|
+
if (isWebchatChannel(parsedChannel)) {
|
|
713
|
+
// For webchat, use userId as sessionKey (this is the most common pattern)
|
|
714
|
+
sessionKey = userId;
|
|
715
|
+
api.logger.info(`[mfa-auth] Using webchat sessionKey (userId): ${sessionKey}`);
|
|
716
|
+
} else {
|
|
717
|
+
// Fallback to channel:accountId:from format
|
|
718
|
+
sessionKey = `${parsedChannel}:${parsedAccountId}:${userId}`;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const session = await authManager.generateSession(userId, {
|
|
723
|
+
sessionKey,
|
|
724
|
+
senderId: userId,
|
|
725
|
+
commandBody: event.content || "",
|
|
726
|
+
channel: parsedChannel,
|
|
727
|
+
to: parsedTo,
|
|
728
|
+
accountId: parsedAccountId,
|
|
729
|
+
toolName: "",
|
|
730
|
+
toolParams: {},
|
|
731
|
+
timestamp: Date.now(),
|
|
732
|
+
triggerType: "first_message",
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
if (!session) {
|
|
736
|
+
api.logger.error(
|
|
737
|
+
`[mfa-auth] Failed to generate first message auth session for user ${userId}`,
|
|
738
|
+
);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
api.logger.info(`[mfa-auth] Blocking first message from ${userId}`);
|
|
743
|
+
|
|
744
|
+
if (isWebchatChannel(parsedChannel)) {
|
|
745
|
+
addPendingAuthCandidates(userId, sessionKey, parsedTo);
|
|
746
|
+
authManager.setSessionMetadata(session.sessionId, {
|
|
747
|
+
qrCodeUrl: session.qrCodeUrl,
|
|
748
|
+
triggerType: "first_message",
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
await sendAuthMessage(
|
|
753
|
+
parsedChannel,
|
|
754
|
+
parsedAccountId,
|
|
755
|
+
parsedTo || userId,
|
|
756
|
+
`\u9996\u6b21\u5bf9\u8bdd\u9700\u8981\u8ba4\u8bc1\n\n\u4e3a\u4e86\u8d26\u53f7\u5b89\u5168\uff0c\u9996\u6b21\u5bf9\u8bdd\u524d\u9700\u8981\u5b8c\u6210\u8eab\u4efd\u9a8c\u8bc1\u3002\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${session.qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002`,
|
|
757
|
+
userId,
|
|
758
|
+
sessionKey,
|
|
759
|
+
);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
api.logger.error(
|
|
762
|
+
`[mfa-auth] Failed to send first-message auth link to webchat: ${String(error)}`,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
startPollingForAuth(api, userId, session.sessionId, {
|
|
767
|
+
triggerType: "first_message",
|
|
768
|
+
isReauth: false,
|
|
769
|
+
channel: parsedChannel,
|
|
770
|
+
accountId: parsedAccountId,
|
|
771
|
+
to: parsedTo,
|
|
772
|
+
sessionKey: userId,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (parsedChannel && parsedChannel !== "web") {
|
|
779
|
+
if (parsedChannel !== "feishu") {
|
|
780
|
+
api.logger.warn(
|
|
781
|
+
`[mfa-auth] Channel ${parsedChannel} not supported, skipping auth notification`,
|
|
782
|
+
);
|
|
783
|
+
} else {
|
|
784
|
+
const messageText = `\u9996\u6b21\u5bf9\u8bdd\u9700\u8981\u8ba4\u8bc1\n\n\u4e3a\u4e86\u8d26\u53f7\u5b89\u5168\uff0c\u9996\u6b21\u5bf9\u8bdd\u524d\u9700\u8981\u5b8c\u6210\u8eab\u4efd\u9a8c\u8bc1\u3002\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${session.qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002`;
|
|
785
|
+
|
|
786
|
+
await sendAuthMessage(
|
|
787
|
+
parsedChannel,
|
|
788
|
+
parsedAccountId,
|
|
789
|
+
parsedTo || userId,
|
|
790
|
+
messageText,
|
|
791
|
+
userId,
|
|
792
|
+
sessionKey,
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
startPollingForAuth(api, userId, session.sessionId, {
|
|
796
|
+
triggerType: "first_message",
|
|
797
|
+
isReauth: false,
|
|
798
|
+
channel: parsedChannel,
|
|
799
|
+
accountId: parsedAccountId,
|
|
800
|
+
to: parsedTo,
|
|
801
|
+
sessionKey: sessionKey,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
api.registerCommand({
|
|
808
|
+
name: "reauth",
|
|
809
|
+
description: "\u91cd\u65b0\u8fdb\u884c\u9996\u6b21\u5bf9\u8bdd\u8ba4\u8bc1",
|
|
810
|
+
acceptsArgs: false,
|
|
811
|
+
requireAuth: false,
|
|
812
|
+
handler: async (ctx) => {
|
|
813
|
+
const rawCtx = ctx as unknown as Record<string, unknown>;
|
|
814
|
+
const ctxSessionKey = typeof rawCtx.sessionKey === "string" ? rawCtx.sessionKey : undefined;
|
|
815
|
+
const userId =
|
|
816
|
+
normalizeAuthId(ctx.from || ctx.senderId || ctx.to || ctxSessionKey || "unknown") ||
|
|
817
|
+
"unknown";
|
|
818
|
+
const authUserIds = expandAuthIdCandidates(
|
|
819
|
+
uniqueNonEmpty([ctx.from, ctx.senderId, ctx.to, ctxSessionKey, userId]),
|
|
820
|
+
);
|
|
821
|
+
api.logger.info(
|
|
822
|
+
`[mfa-auth] /reauth command received. userId=${userId}, aliases=${authUserIds.join(",")}, ctx.channel=${ctx.channel}, ctx.accountId=${ctx.accountId}, ctx.to=${ctx.to}`,
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
for (const authUserId of authUserIds) {
|
|
826
|
+
authManager.clearFirstMessageAuth(authUserId);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const parsedChannel = ctx.channel;
|
|
830
|
+
const parsedAccountId = ctx.accountId || "";
|
|
831
|
+
const parsedTo = ctx.to;
|
|
832
|
+
|
|
833
|
+
api.logger.info(
|
|
834
|
+
`[mfa-auth] Parsed: channel=${parsedChannel}, accountId=${parsedAccountId}, to=${parsedTo}`,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
// For webchat, use userId as sessionKey
|
|
838
|
+
const sessionKey =
|
|
839
|
+
isWebchatChannel(parsedChannel)
|
|
840
|
+
? ctxSessionKey || userId
|
|
841
|
+
: `${parsedChannel}:${parsedAccountId}:${userId}`;
|
|
842
|
+
|
|
843
|
+
api.logger.info(`[mfa-auth] Using sessionKey for reauth: ${sessionKey}`);
|
|
844
|
+
|
|
845
|
+
const session = await authManager.generateSession(userId, {
|
|
846
|
+
sessionKey,
|
|
847
|
+
senderId: userId,
|
|
848
|
+
commandBody: "/reauth",
|
|
849
|
+
channel: parsedChannel,
|
|
850
|
+
to: parsedTo,
|
|
851
|
+
accountId: parsedAccountId,
|
|
852
|
+
toolName: "",
|
|
853
|
+
toolParams: {},
|
|
854
|
+
timestamp: Date.now(),
|
|
855
|
+
triggerType: "first_message",
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
if (!session) {
|
|
859
|
+
api.logger.error(`[mfa-auth] Failed to generate reauth session for user ${userId}`);
|
|
860
|
+
return { text: "\u8ba4\u8bc1\u4f1a\u8bdd\u521b\u5efa\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002" };
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
api.logger.info(
|
|
864
|
+
`[mfa-auth] Reauth requested by user ${userId}, session=${session.sessionId}`,
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
const messageText = `\u9700\u8981\u91cd\u65b0\u8ba4\u8bc1\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${session.qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002`;
|
|
868
|
+
|
|
869
|
+
// Use sendAuthMessage to ensure consistent delivery via WebSocket for WebChat
|
|
870
|
+
// This will use the new robust session resolution logic
|
|
871
|
+
if (isWebchatChannel(parsedChannel)) {
|
|
872
|
+
try {
|
|
873
|
+
addPendingAuthCandidates(userId, sessionKey, parsedTo);
|
|
874
|
+
authManager.setSessionMetadata(session.sessionId, {
|
|
875
|
+
qrCodeUrl: session.qrCodeUrl,
|
|
876
|
+
triggerType: "first_message",
|
|
877
|
+
isReauth: true,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
await sendAuthMessage(
|
|
881
|
+
parsedChannel,
|
|
882
|
+
parsedAccountId,
|
|
883
|
+
parsedTo || userId,
|
|
884
|
+
messageText,
|
|
885
|
+
userId,
|
|
886
|
+
sessionKey,
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
startPollingForAuth(api, userId, session.sessionId, {
|
|
890
|
+
triggerType: "first_message",
|
|
891
|
+
isReauth: true,
|
|
892
|
+
channel: parsedChannel,
|
|
893
|
+
accountId: parsedAccountId,
|
|
894
|
+
to: parsedTo,
|
|
895
|
+
sessionKey: sessionKey,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
return { text: messageText };
|
|
899
|
+
} catch (error) {
|
|
900
|
+
api.logger.error(`[mfa-auth] Failed to send reauth link to webchat: ${String(error)}`);
|
|
901
|
+
// Fallback to returning text directly if push fails, though this might be less reliable if session context is lost
|
|
902
|
+
return { text: messageText };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (!parsedChannel || parsedChannel !== "feishu") {
|
|
907
|
+
api.logger.warn(`[mfa-auth] Channel ${parsedChannel} not supported`);
|
|
908
|
+
return { text: "\u5f53\u524d\u6e20\u9053\u6682\u4e0d\u652f\u6301\u6b64\u8ba4\u8bc1\u6d41\u7a0b\u3002" };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
api.logger.info(
|
|
913
|
+
`[mfa-auth] Sending reauth notification: channel=${parsedChannel}, to=${parsedTo}, accountId=${parsedAccountId}`,
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
await sendAuthMessage(
|
|
917
|
+
parsedChannel,
|
|
918
|
+
parsedAccountId,
|
|
919
|
+
parsedTo || userId,
|
|
920
|
+
`\u9700\u8981\u91cd\u65b0\u8ba4\u8bc1\n\n\u8bf7\u70b9\u51fb\u4e0b\u65b9\u94fe\u63a5\u5b8c\u6210\u626b\u7801\u8ba4\u8bc1\uff1a\n${session.qrCodeUrl}\n\n\u8ba4\u8bc1\u6709\u6548\u671f\uff1a${Math.floor(config.timeout / 60000)} \u5206\u949f\u3002`,
|
|
921
|
+
userId,
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
startPollingForAuth(api, userId, session.sessionId, {
|
|
925
|
+
triggerType: "first_message",
|
|
926
|
+
isReauth: true,
|
|
927
|
+
channel: parsedChannel,
|
|
928
|
+
accountId: parsedAccountId,
|
|
929
|
+
to: parsedTo,
|
|
930
|
+
sessionKey: sessionKey,
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
api.logger.info(`[mfa-auth] Reauth notification sent successfully`);
|
|
934
|
+
return { text: "\u8ba4\u8bc1\u94fe\u63a5\u5df2\u53d1\u9001\uff0c\u8bf7\u67e5\u770b\u6700\u65b0\u6d88\u606f\u3002" };
|
|
935
|
+
} catch (error) {
|
|
936
|
+
api.logger.error(`[mfa-auth] Failed to send reauth notification: ${String(error)}`);
|
|
937
|
+
return { text: "\u8ba4\u8bc1\u94fe\u63a5\u53d1\u9001\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002" };
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
api.logger.info("mfa-auth plugin loaded");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function startPollingForAuth(
|
|
946
|
+
api: OpenClawPluginApi,
|
|
947
|
+
userId: string,
|
|
948
|
+
sessionId: string,
|
|
949
|
+
context: {
|
|
950
|
+
triggerType: string;
|
|
951
|
+
isReauth: boolean;
|
|
952
|
+
channel?: string;
|
|
953
|
+
accountId?: string;
|
|
954
|
+
to?: string;
|
|
955
|
+
sessionKey?: string;
|
|
956
|
+
},
|
|
957
|
+
) {
|
|
958
|
+
api.logger.info(
|
|
959
|
+
`[mfa-auth] Starting polling for auth status: userId=${userId}, sessionId=${sessionId}`,
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
const pollInterval = setInterval(async () => {
|
|
963
|
+
let isVerified = false;
|
|
964
|
+
if (context.triggerType === "first_message") {
|
|
965
|
+
isVerified = authManager.isUserVerifiedForFirstMessage(userId);
|
|
966
|
+
} else {
|
|
967
|
+
isVerified = authManager.isUserVerifiedForSensitiveOps(userId);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (!isVerified) {
|
|
971
|
+
const session = authManager.getSession(sessionId);
|
|
972
|
+
if (session && session.certToken) {
|
|
973
|
+
try {
|
|
974
|
+
const authResult = await dabbyClient.getAuthResult(session.certToken);
|
|
975
|
+
if (authResult.status === "verified") {
|
|
976
|
+
api.logger.info(`[mfa-auth] Auth verification successful for session ${sessionId}`);
|
|
977
|
+
authManager.markUserVerified(
|
|
978
|
+
userId,
|
|
979
|
+
context.triggerType === "first_message" ? "first_message" : "sensitive_operation",
|
|
980
|
+
context.isReauth,
|
|
981
|
+
);
|
|
982
|
+
isVerified = true;
|
|
983
|
+
} else if (authResult.status === "failed" || authResult.status === "expired") {
|
|
984
|
+
api.logger.warn(
|
|
985
|
+
`[mfa-auth] Auth failed or expired for session ${sessionId}: ${authResult.error}`,
|
|
986
|
+
);
|
|
987
|
+
clearInterval(pollInterval);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
} catch (error) {
|
|
991
|
+
api.logger.error(
|
|
992
|
+
`[mfa-auth] Failed to check auth status for session ${sessionId}: ${String(error)}`,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (isVerified) {
|
|
999
|
+
clearInterval(pollInterval);
|
|
1000
|
+
|
|
1001
|
+
const notificationInfo = authManager.checkAndConsumeNotification(userId);
|
|
1002
|
+
if (notificationInfo) {
|
|
1003
|
+
api.logger.info(`[mfa-auth] Polling detected verification for user ${userId}`);
|
|
1004
|
+
|
|
1005
|
+
const messageText = notificationInfo.isReauth
|
|
1006
|
+
? "\u2705 \u91cd\u65b0\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u7ee7\u7eed\u5bf9\u8bdd\u3002"
|
|
1007
|
+
: notificationInfo.triggerType === "first_message"
|
|
1008
|
+
? "\u2705 \u9996\u6b21\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u7ee7\u7eed\u5bf9\u8bdd\u3002"
|
|
1009
|
+
: "\u2705 \u4e8c\u6b21\u8ba4\u8bc1\u6210\u529f\uff0c\u8bf7\u91cd\u65b0\u53d1\u9001\u4e4b\u524d\u7684\u547d\u4ee4\u6216\u56de\u590d\u201c\u786e\u8ba4\u201d\u3002";
|
|
1010
|
+
|
|
1011
|
+
sendAuthMessage(
|
|
1012
|
+
context.channel,
|
|
1013
|
+
context.accountId,
|
|
1014
|
+
context.to || userId,
|
|
1015
|
+
messageText,
|
|
1016
|
+
userId,
|
|
1017
|
+
context.sessionKey,
|
|
1018
|
+
).catch((err) =>
|
|
1019
|
+
api.logger.error(`[mfa-auth] Failed to send success notification from polling: ${err}`),
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const session = authManager.getSession(sessionId);
|
|
1026
|
+
if (!session) {
|
|
1027
|
+
clearInterval(pollInterval);
|
|
1028
|
+
api.logger.info(
|
|
1029
|
+
`[mfa-auth] Polling stopped: session ${sessionId} not found and user not verified`,
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}, 2000);
|
|
1033
|
+
|
|
1034
|
+
setTimeout(() => {
|
|
1035
|
+
clearInterval(pollInterval);
|
|
1036
|
+
}, config.timeout + 10000);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function checkSensitiveOperation(text: string): { isSensitive: boolean; preview: string } {
|
|
1040
|
+
const lowerText = text.toLowerCase();
|
|
1041
|
+
|
|
1042
|
+
if (config.debug) {
|
|
1043
|
+
console.log(`[mfa-auth] Checking sensitive keywords for: ${text}`);
|
|
1044
|
+
console.log(
|
|
1045
|
+
`[mfa-auth] Sensitive keywords configured: ${JSON.stringify(config.sensitiveKeywords)}`,
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
for (const keyword of config.sensitiveKeywords) {
|
|
1050
|
+
if (lowerText.includes(keyword.toLowerCase())) {
|
|
1051
|
+
const preview = text;
|
|
1052
|
+
if (config.debug) {
|
|
1053
|
+
console.log(`[mfa-auth] Sensitive keyword matched: ${keyword}`);
|
|
1054
|
+
}
|
|
1055
|
+
return { isSensitive: true, preview };
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (config.debug) {
|
|
1060
|
+
console.log(`[mfa-auth] No sensitive keyword matched`);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return { isSensitive: false, preview: "" };
|
|
1064
|
+
}
|
|
1065
|
+
|