@openclaw/bluebubbles 2026.3.2 → 2026.3.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/index.ts +2 -2
- package/package.json +4 -1
- package/src/account-resolve.ts +1 -1
- package/src/accounts.ts +7 -36
- package/src/actions.test.ts +1 -1
- package/src/actions.ts +1 -1
- package/src/attachments.test.ts +1 -1
- package/src/attachments.ts +1 -1
- package/src/channel.ts +50 -70
- package/src/chat.ts +46 -39
- package/src/config-apply.ts +77 -0
- package/src/config-schema.test.ts +1 -1
- package/src/config-schema.ts +1 -1
- package/src/history.ts +1 -1
- package/src/media-send.test.ts +1 -1
- package/src/media-send.ts +1 -1
- package/src/monitor-debounce.ts +1 -1
- package/src/monitor-normalize.ts +2 -11
- package/src/monitor-processing.ts +23 -22
- package/src/monitor-shared.ts +1 -1
- package/src/monitor.test.ts +3 -3
- package/src/monitor.ts +126 -138
- package/src/monitor.webhook-auth.test.ts +77 -172
- package/src/monitor.webhook-route.test.ts +1 -1
- package/src/onboarding.secret-input.test.ts +10 -2
- package/src/onboarding.ts +29 -67
- package/src/probe.ts +1 -1
- package/src/reactions.ts +1 -1
- package/src/request-url.ts +1 -12
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +8 -14
- package/src/send.test.ts +1 -1
- package/src/send.ts +17 -22
- package/src/targets.ts +1 -1
- package/src/types.ts +2 -2
package/src/monitor.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
-
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
|
6
6
|
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
|
@@ -2391,11 +2391,11 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2391
2391
|
});
|
|
2392
2392
|
|
|
2393
2393
|
const accountA: ResolvedBlueBubblesAccount = {
|
|
2394
|
-
...createMockAccount({ dmHistoryLimit: 3, password: "password-a" }),
|
|
2394
|
+
...createMockAccount({ dmHistoryLimit: 3, password: "password-a" }), // pragma: allowlist secret
|
|
2395
2395
|
accountId: "acc-a",
|
|
2396
2396
|
};
|
|
2397
2397
|
const accountB: ResolvedBlueBubblesAccount = {
|
|
2398
|
-
...createMockAccount({ dmHistoryLimit: 3, password: "password-b" }),
|
|
2398
|
+
...createMockAccount({ dmHistoryLimit: 3, password: "password-b" }), // pragma: allowlist secret
|
|
2399
2399
|
accountId: "acc-b",
|
|
2400
2400
|
};
|
|
2401
2401
|
const config: OpenClawConfig = {};
|
package/src/monitor.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
3
|
import {
|
|
4
|
-
beginWebhookRequestPipelineOrReject,
|
|
5
4
|
createWebhookInFlightLimiter,
|
|
6
5
|
registerWebhookTargetWithPluginRoute,
|
|
7
6
|
readWebhookBodyOrReject,
|
|
8
7
|
resolveWebhookTargetWithAuthOrRejectSync,
|
|
9
|
-
|
|
10
|
-
} from "openclaw/plugin-sdk";
|
|
8
|
+
withResolvedWebhookRequestPipeline,
|
|
9
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
11
10
|
import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js";
|
|
12
11
|
import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js";
|
|
13
12
|
import { logVerbose, processMessage, processReaction } from "./monitor-processing.js";
|
|
@@ -122,156 +121,145 @@ export async function handleBlueBubblesWebhookRequest(
|
|
|
122
121
|
req: IncomingMessage,
|
|
123
122
|
res: ServerResponse,
|
|
124
123
|
): Promise<boolean> {
|
|
125
|
-
|
|
126
|
-
if (!resolved) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
const { path, targets } = resolved;
|
|
130
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
131
|
-
const requestLifecycle = beginWebhookRequestPipelineOrReject({
|
|
124
|
+
return await withResolvedWebhookRequestPipeline({
|
|
132
125
|
req,
|
|
133
126
|
res,
|
|
127
|
+
targetsByPath: webhookTargets,
|
|
134
128
|
allowMethods: ["POST"],
|
|
135
129
|
inFlightLimiter: webhookInFlightLimiter,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
res,
|
|
153
|
-
isMatch: (target) => {
|
|
154
|
-
const token = target.account.config.password?.trim() ?? "";
|
|
155
|
-
return safeEqualSecret(guid, token);
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
if (!target) {
|
|
159
|
-
console.warn(
|
|
160
|
-
`[bluebubbles] webhook rejected: status=${res.statusCode} path=${path} guid=${maskSecret(url.searchParams.get("guid") ?? url.searchParams.get("password") ?? "")}`,
|
|
161
|
-
);
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
const body = await readWebhookBodyOrReject({
|
|
165
|
-
req,
|
|
166
|
-
res,
|
|
167
|
-
profile: "post-auth",
|
|
168
|
-
invalidBodyMessage: "invalid payload",
|
|
169
|
-
});
|
|
170
|
-
if (!body.ok) {
|
|
171
|
-
console.warn(`[bluebubbles] webhook rejected: status=${res.statusCode}`);
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const parsed = parseBlueBubblesWebhookPayload(body.value);
|
|
176
|
-
if (!parsed.ok) {
|
|
177
|
-
res.statusCode = 400;
|
|
178
|
-
res.end(parsed.error);
|
|
179
|
-
console.warn(`[bluebubbles] webhook rejected: ${parsed.error}`);
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const payload = asRecord(parsed.value) ?? {};
|
|
184
|
-
const firstTarget = targets[0];
|
|
185
|
-
if (firstTarget) {
|
|
186
|
-
logVerbose(
|
|
187
|
-
firstTarget.core,
|
|
188
|
-
firstTarget.runtime,
|
|
189
|
-
`webhook received path=${path} keys=${Object.keys(payload).join(",") || "none"}`,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
const eventTypeRaw = payload.type;
|
|
193
|
-
const eventType = typeof eventTypeRaw === "string" ? eventTypeRaw.trim() : "";
|
|
194
|
-
const allowedEventTypes = new Set([
|
|
195
|
-
"new-message",
|
|
196
|
-
"updated-message",
|
|
197
|
-
"message-reaction",
|
|
198
|
-
"reaction",
|
|
199
|
-
]);
|
|
200
|
-
if (eventType && !allowedEventTypes.has(eventType)) {
|
|
201
|
-
res.statusCode = 200;
|
|
202
|
-
res.end("ok");
|
|
203
|
-
if (firstTarget) {
|
|
204
|
-
logVerbose(firstTarget.core, firstTarget.runtime, `webhook ignored type=${eventType}`);
|
|
205
|
-
}
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
208
|
-
const reaction = normalizeWebhookReaction(payload);
|
|
209
|
-
if (
|
|
210
|
-
(eventType === "updated-message" ||
|
|
211
|
-
eventType === "message-reaction" ||
|
|
212
|
-
eventType === "reaction") &&
|
|
213
|
-
!reaction
|
|
214
|
-
) {
|
|
215
|
-
res.statusCode = 200;
|
|
216
|
-
res.end("ok");
|
|
217
|
-
if (firstTarget) {
|
|
218
|
-
logVerbose(
|
|
219
|
-
firstTarget.core,
|
|
220
|
-
firstTarget.runtime,
|
|
221
|
-
`webhook ignored ${eventType || "event"} without reaction`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
const message = reaction ? null : normalizeWebhookMessage(payload);
|
|
227
|
-
if (!message && !reaction) {
|
|
228
|
-
res.statusCode = 400;
|
|
229
|
-
res.end("invalid payload");
|
|
230
|
-
console.warn("[bluebubbles] webhook rejected: unable to parse message payload");
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
target.statusSink?.({ lastInboundAt: Date.now() });
|
|
235
|
-
if (reaction) {
|
|
236
|
-
processReaction(reaction, target).catch((err) => {
|
|
237
|
-
target.runtime.error?.(
|
|
238
|
-
`[${target.account.accountId}] BlueBubbles reaction failed: ${String(err)}`,
|
|
239
|
-
);
|
|
130
|
+
handle: async ({ path, targets }) => {
|
|
131
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
132
|
+
const guidParam = url.searchParams.get("guid") ?? url.searchParams.get("password");
|
|
133
|
+
const headerToken =
|
|
134
|
+
req.headers["x-guid"] ??
|
|
135
|
+
req.headers["x-password"] ??
|
|
136
|
+
req.headers["x-bluebubbles-guid"] ??
|
|
137
|
+
req.headers["authorization"];
|
|
138
|
+
const guid = (Array.isArray(headerToken) ? headerToken[0] : headerToken) ?? guidParam ?? "";
|
|
139
|
+
const target = resolveWebhookTargetWithAuthOrRejectSync({
|
|
140
|
+
targets,
|
|
141
|
+
res,
|
|
142
|
+
isMatch: (target) => {
|
|
143
|
+
const token = target.account.config.password?.trim() ?? "";
|
|
144
|
+
return safeEqualSecret(guid, token);
|
|
145
|
+
},
|
|
240
146
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const debouncer = debounceRegistry.getOrCreateDebouncer(target);
|
|
245
|
-
debouncer.enqueue({ message, target }).catch((err) => {
|
|
246
|
-
target.runtime.error?.(
|
|
247
|
-
`[${target.account.accountId}] BlueBubbles webhook failed: ${String(err)}`,
|
|
147
|
+
if (!target) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`[bluebubbles] webhook rejected: status=${res.statusCode} path=${path} guid=${maskSecret(url.searchParams.get("guid") ?? url.searchParams.get("password") ?? "")}`,
|
|
248
150
|
);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const body = await readWebhookBodyOrReject({
|
|
154
|
+
req,
|
|
155
|
+
res,
|
|
156
|
+
profile: "post-auth",
|
|
157
|
+
invalidBodyMessage: "invalid payload",
|
|
249
158
|
});
|
|
250
|
-
|
|
159
|
+
if (!body.ok) {
|
|
160
|
+
console.warn(`[bluebubbles] webhook rejected: status=${res.statusCode}`);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
251
163
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
firstTarget.runtime,
|
|
259
|
-
`webhook accepted reaction sender=${reaction.senderId} msg=${reaction.messageId} action=${reaction.action}`,
|
|
260
|
-
);
|
|
164
|
+
const parsed = parseBlueBubblesWebhookPayload(body.value);
|
|
165
|
+
if (!parsed.ok) {
|
|
166
|
+
res.statusCode = 400;
|
|
167
|
+
res.end(parsed.error);
|
|
168
|
+
console.warn(`[bluebubbles] webhook rejected: ${parsed.error}`);
|
|
169
|
+
return true;
|
|
261
170
|
}
|
|
262
|
-
|
|
171
|
+
|
|
172
|
+
const payload = asRecord(parsed.value) ?? {};
|
|
173
|
+
const firstTarget = targets[0];
|
|
263
174
|
if (firstTarget) {
|
|
264
175
|
logVerbose(
|
|
265
176
|
firstTarget.core,
|
|
266
177
|
firstTarget.runtime,
|
|
267
|
-
`webhook
|
|
178
|
+
`webhook received path=${path} keys=${Object.keys(payload).join(",") || "none"}`,
|
|
268
179
|
);
|
|
269
180
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
181
|
+
const eventTypeRaw = payload.type;
|
|
182
|
+
const eventType = typeof eventTypeRaw === "string" ? eventTypeRaw.trim() : "";
|
|
183
|
+
const allowedEventTypes = new Set([
|
|
184
|
+
"new-message",
|
|
185
|
+
"updated-message",
|
|
186
|
+
"message-reaction",
|
|
187
|
+
"reaction",
|
|
188
|
+
]);
|
|
189
|
+
if (eventType && !allowedEventTypes.has(eventType)) {
|
|
190
|
+
res.statusCode = 200;
|
|
191
|
+
res.end("ok");
|
|
192
|
+
if (firstTarget) {
|
|
193
|
+
logVerbose(firstTarget.core, firstTarget.runtime, `webhook ignored type=${eventType}`);
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
const reaction = normalizeWebhookReaction(payload);
|
|
198
|
+
if (
|
|
199
|
+
(eventType === "updated-message" ||
|
|
200
|
+
eventType === "message-reaction" ||
|
|
201
|
+
eventType === "reaction") &&
|
|
202
|
+
!reaction
|
|
203
|
+
) {
|
|
204
|
+
res.statusCode = 200;
|
|
205
|
+
res.end("ok");
|
|
206
|
+
if (firstTarget) {
|
|
207
|
+
logVerbose(
|
|
208
|
+
firstTarget.core,
|
|
209
|
+
firstTarget.runtime,
|
|
210
|
+
`webhook ignored ${eventType || "event"} without reaction`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
const message = reaction ? null : normalizeWebhookMessage(payload);
|
|
216
|
+
if (!message && !reaction) {
|
|
217
|
+
res.statusCode = 400;
|
|
218
|
+
res.end("invalid payload");
|
|
219
|
+
console.warn("[bluebubbles] webhook rejected: unable to parse message payload");
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
target.statusSink?.({ lastInboundAt: Date.now() });
|
|
224
|
+
if (reaction) {
|
|
225
|
+
processReaction(reaction, target).catch((err) => {
|
|
226
|
+
target.runtime.error?.(
|
|
227
|
+
`[${target.account.accountId}] BlueBubbles reaction failed: ${String(err)}`,
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
} else if (message) {
|
|
231
|
+
// Route messages through debouncer to coalesce rapid-fire events
|
|
232
|
+
// (e.g., text message + URL balloon arriving as separate webhooks)
|
|
233
|
+
const debouncer = debounceRegistry.getOrCreateDebouncer(target);
|
|
234
|
+
debouncer.enqueue({ message, target }).catch((err) => {
|
|
235
|
+
target.runtime.error?.(
|
|
236
|
+
`[${target.account.accountId}] BlueBubbles webhook failed: ${String(err)}`,
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
res.statusCode = 200;
|
|
242
|
+
res.end("ok");
|
|
243
|
+
if (reaction) {
|
|
244
|
+
if (firstTarget) {
|
|
245
|
+
logVerbose(
|
|
246
|
+
firstTarget.core,
|
|
247
|
+
firstTarget.runtime,
|
|
248
|
+
`webhook accepted reaction sender=${reaction.senderId} msg=${reaction.messageId} action=${reaction.action}`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
} else if (message) {
|
|
252
|
+
if (firstTarget) {
|
|
253
|
+
logVerbose(
|
|
254
|
+
firstTarget.core,
|
|
255
|
+
firstTarget.runtime,
|
|
256
|
+
`webhook accepted sender=${message.senderId} group=${message.isGroup} chatGuid=${message.chatGuid ?? ""} chatId=${message.chatId ?? ""}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
275
263
|
}
|
|
276
264
|
|
|
277
265
|
export async function monitorBlueBubblesProvider(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
-
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
|
6
6
|
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
|
@@ -166,7 +166,7 @@ function createMockAccount(
|
|
|
166
166
|
configured: true,
|
|
167
167
|
config: {
|
|
168
168
|
serverUrl: "http://localhost:1234",
|
|
169
|
-
password: "test-password",
|
|
169
|
+
password: "test-password", // pragma: allowlist secret
|
|
170
170
|
dmPolicy: "open",
|
|
171
171
|
groupPolicy: "open",
|
|
172
172
|
allowFrom: [],
|
|
@@ -261,6 +261,47 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
261
261
|
unregister?.();
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
function setupWebhookTarget(params?: {
|
|
265
|
+
account?: ResolvedBlueBubblesAccount;
|
|
266
|
+
config?: OpenClawConfig;
|
|
267
|
+
core?: PluginRuntime;
|
|
268
|
+
statusSink?: (event: unknown) => void;
|
|
269
|
+
}) {
|
|
270
|
+
const account = params?.account ?? createMockAccount();
|
|
271
|
+
const config = params?.config ?? {};
|
|
272
|
+
const core = params?.core ?? createMockRuntime();
|
|
273
|
+
setBlueBubblesRuntime(core);
|
|
274
|
+
unregister = registerBlueBubblesWebhookTarget({
|
|
275
|
+
account,
|
|
276
|
+
config,
|
|
277
|
+
runtime: { log: vi.fn(), error: vi.fn() },
|
|
278
|
+
core,
|
|
279
|
+
path: "/bluebubbles-webhook",
|
|
280
|
+
statusSink: params?.statusSink,
|
|
281
|
+
});
|
|
282
|
+
return { account, config, core };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function createNewMessagePayload(dataOverrides: Record<string, unknown> = {}) {
|
|
286
|
+
return {
|
|
287
|
+
type: "new-message",
|
|
288
|
+
data: {
|
|
289
|
+
text: "hello",
|
|
290
|
+
handle: { address: "+15551234567" },
|
|
291
|
+
isGroup: false,
|
|
292
|
+
isFromMe: false,
|
|
293
|
+
guid: "msg-1",
|
|
294
|
+
...dataOverrides,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function setRequestRemoteAddress(req: IncomingMessage, remoteAddress: string) {
|
|
300
|
+
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
301
|
+
remoteAddress,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
264
305
|
describe("webhook parsing + auth handling", () => {
|
|
265
306
|
it("rejects non-POST requests", async () => {
|
|
266
307
|
const account = createMockAccount();
|
|
@@ -286,30 +327,8 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
286
327
|
});
|
|
287
328
|
|
|
288
329
|
it("accepts POST requests with valid JSON payload", async () => {
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
const core = createMockRuntime();
|
|
292
|
-
setBlueBubblesRuntime(core);
|
|
293
|
-
|
|
294
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
295
|
-
account,
|
|
296
|
-
config,
|
|
297
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
298
|
-
core,
|
|
299
|
-
path: "/bluebubbles-webhook",
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
const payload = {
|
|
303
|
-
type: "new-message",
|
|
304
|
-
data: {
|
|
305
|
-
text: "hello",
|
|
306
|
-
handle: { address: "+15551234567" },
|
|
307
|
-
isGroup: false,
|
|
308
|
-
isFromMe: false,
|
|
309
|
-
guid: "msg-1",
|
|
310
|
-
date: Date.now(),
|
|
311
|
-
},
|
|
312
|
-
};
|
|
330
|
+
setupWebhookTarget();
|
|
331
|
+
const payload = createNewMessagePayload({ date: Date.now() });
|
|
313
332
|
|
|
314
333
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
315
334
|
const res = createMockResponse();
|
|
@@ -345,30 +364,8 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
345
364
|
});
|
|
346
365
|
|
|
347
366
|
it("accepts URL-encoded payload wrappers", async () => {
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const core = createMockRuntime();
|
|
351
|
-
setBlueBubblesRuntime(core);
|
|
352
|
-
|
|
353
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
354
|
-
account,
|
|
355
|
-
config,
|
|
356
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
357
|
-
core,
|
|
358
|
-
path: "/bluebubbles-webhook",
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
const payload = {
|
|
362
|
-
type: "new-message",
|
|
363
|
-
data: {
|
|
364
|
-
text: "hello",
|
|
365
|
-
handle: { address: "+15551234567" },
|
|
366
|
-
isGroup: false,
|
|
367
|
-
isFromMe: false,
|
|
368
|
-
guid: "msg-1",
|
|
369
|
-
date: Date.now(),
|
|
370
|
-
},
|
|
371
|
-
};
|
|
367
|
+
setupWebhookTarget();
|
|
368
|
+
const payload = createNewMessagePayload({ date: Date.now() });
|
|
372
369
|
const encodedBody = new URLSearchParams({
|
|
373
370
|
payload: JSON.stringify(payload),
|
|
374
371
|
}).toString();
|
|
@@ -458,32 +455,15 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
458
455
|
|
|
459
456
|
it("authenticates via password query parameter", async () => {
|
|
460
457
|
const account = createMockAccount({ password: "secret-token" });
|
|
461
|
-
const config: OpenClawConfig = {};
|
|
462
|
-
const core = createMockRuntime();
|
|
463
|
-
setBlueBubblesRuntime(core);
|
|
464
458
|
|
|
465
459
|
// Mock non-localhost request
|
|
466
|
-
const req = createMockRequest(
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
guid: "msg-1",
|
|
474
|
-
},
|
|
475
|
-
});
|
|
476
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
477
|
-
remoteAddress: "192.168.1.100",
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
481
|
-
account,
|
|
482
|
-
config,
|
|
483
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
484
|
-
core,
|
|
485
|
-
path: "/bluebubbles-webhook",
|
|
486
|
-
});
|
|
460
|
+
const req = createMockRequest(
|
|
461
|
+
"POST",
|
|
462
|
+
"/bluebubbles-webhook?password=secret-token",
|
|
463
|
+
createNewMessagePayload(),
|
|
464
|
+
);
|
|
465
|
+
setRequestRemoteAddress(req, "192.168.1.100");
|
|
466
|
+
setupWebhookTarget({ account });
|
|
487
467
|
|
|
488
468
|
const res = createMockResponse();
|
|
489
469
|
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
@@ -494,36 +474,15 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
494
474
|
|
|
495
475
|
it("authenticates via x-password header", async () => {
|
|
496
476
|
const account = createMockAccount({ password: "secret-token" });
|
|
497
|
-
const config: OpenClawConfig = {};
|
|
498
|
-
const core = createMockRuntime();
|
|
499
|
-
setBlueBubblesRuntime(core);
|
|
500
477
|
|
|
501
478
|
const req = createMockRequest(
|
|
502
479
|
"POST",
|
|
503
480
|
"/bluebubbles-webhook",
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
data: {
|
|
507
|
-
text: "hello",
|
|
508
|
-
handle: { address: "+15551234567" },
|
|
509
|
-
isGroup: false,
|
|
510
|
-
isFromMe: false,
|
|
511
|
-
guid: "msg-1",
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
{ "x-password": "secret-token" },
|
|
481
|
+
createNewMessagePayload(),
|
|
482
|
+
{ "x-password": "secret-token" }, // pragma: allowlist secret
|
|
515
483
|
);
|
|
516
|
-
(req
|
|
517
|
-
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
521
|
-
account,
|
|
522
|
-
config,
|
|
523
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
524
|
-
core,
|
|
525
|
-
path: "/bluebubbles-webhook",
|
|
526
|
-
});
|
|
484
|
+
setRequestRemoteAddress(req, "192.168.1.100");
|
|
485
|
+
setupWebhookTarget({ account });
|
|
527
486
|
|
|
528
487
|
const res = createMockResponse();
|
|
529
488
|
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
@@ -534,31 +493,13 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
534
493
|
|
|
535
494
|
it("rejects unauthorized requests with wrong password", async () => {
|
|
536
495
|
const account = createMockAccount({ password: "secret-token" });
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
text: "hello",
|
|
545
|
-
handle: { address: "+15551234567" },
|
|
546
|
-
isGroup: false,
|
|
547
|
-
isFromMe: false,
|
|
548
|
-
guid: "msg-1",
|
|
549
|
-
},
|
|
550
|
-
});
|
|
551
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
552
|
-
remoteAddress: "192.168.1.100",
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
556
|
-
account,
|
|
557
|
-
config,
|
|
558
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
559
|
-
core,
|
|
560
|
-
path: "/bluebubbles-webhook",
|
|
561
|
-
});
|
|
496
|
+
const req = createMockRequest(
|
|
497
|
+
"POST",
|
|
498
|
+
"/bluebubbles-webhook?password=wrong-token",
|
|
499
|
+
createNewMessagePayload(),
|
|
500
|
+
);
|
|
501
|
+
setRequestRemoteAddress(req, "192.168.1.100");
|
|
502
|
+
setupWebhookTarget({ account });
|
|
562
503
|
|
|
563
504
|
const res = createMockResponse();
|
|
564
505
|
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
@@ -770,32 +711,14 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
770
711
|
const { resolveChatGuidForTarget } = await import("./send.js");
|
|
771
712
|
vi.mocked(resolveChatGuidForTarget).mockClear();
|
|
772
713
|
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
account,
|
|
780
|
-
config,
|
|
781
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
782
|
-
core,
|
|
783
|
-
path: "/bluebubbles-webhook",
|
|
714
|
+
setupWebhookTarget({ account: createMockAccount({ groupPolicy: "open" }) });
|
|
715
|
+
const payload = createNewMessagePayload({
|
|
716
|
+
text: "hello from group",
|
|
717
|
+
isGroup: true,
|
|
718
|
+
chatId: "123",
|
|
719
|
+
date: Date.now(),
|
|
784
720
|
});
|
|
785
721
|
|
|
786
|
-
const payload = {
|
|
787
|
-
type: "new-message",
|
|
788
|
-
data: {
|
|
789
|
-
text: "hello from group",
|
|
790
|
-
handle: { address: "+15551234567" },
|
|
791
|
-
isGroup: true,
|
|
792
|
-
isFromMe: false,
|
|
793
|
-
guid: "msg-1",
|
|
794
|
-
chatId: "123",
|
|
795
|
-
date: Date.now(),
|
|
796
|
-
},
|
|
797
|
-
};
|
|
798
|
-
|
|
799
722
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
800
723
|
const res = createMockResponse();
|
|
801
724
|
|
|
@@ -819,32 +742,14 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
819
742
|
return EMPTY_DISPATCH_RESULT;
|
|
820
743
|
});
|
|
821
744
|
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
account,
|
|
829
|
-
config,
|
|
830
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
831
|
-
core,
|
|
832
|
-
path: "/bluebubbles-webhook",
|
|
745
|
+
setupWebhookTarget({ account: createMockAccount({ groupPolicy: "open" }) });
|
|
746
|
+
const payload = createNewMessagePayload({
|
|
747
|
+
text: "hello from group",
|
|
748
|
+
isGroup: true,
|
|
749
|
+
chat: { chatGuid: "iMessage;+;chat123456" },
|
|
750
|
+
date: Date.now(),
|
|
833
751
|
});
|
|
834
752
|
|
|
835
|
-
const payload = {
|
|
836
|
-
type: "new-message",
|
|
837
|
-
data: {
|
|
838
|
-
text: "hello from group",
|
|
839
|
-
handle: { address: "+15551234567" },
|
|
840
|
-
isGroup: true,
|
|
841
|
-
isFromMe: false,
|
|
842
|
-
guid: "msg-1",
|
|
843
|
-
chat: { chatGuid: "iMessage;+;chat123456" },
|
|
844
|
-
date: Date.now(),
|
|
845
|
-
},
|
|
846
|
-
};
|
|
847
|
-
|
|
848
753
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
849
754
|
const res = createMockResponse();
|
|
850
755
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import { afterEach, describe, expect, it } from "vitest";
|
|
3
3
|
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
|
|
4
4
|
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { WizardPrompter } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { WizardPrompter } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
|
-
vi.mock("openclaw/plugin-sdk", () => ({
|
|
4
|
+
vi.mock("openclaw/plugin-sdk/bluebubbles", () => ({
|
|
5
5
|
DEFAULT_ACCOUNT_ID: "default",
|
|
6
6
|
addWildcardAllowFrom: vi.fn(),
|
|
7
7
|
formatDocsLink: (_url: string, fallback: string) => fallback,
|
|
@@ -23,6 +23,10 @@ vi.mock("openclaw/plugin-sdk", () => ({
|
|
|
23
23
|
);
|
|
24
24
|
},
|
|
25
25
|
mergeAllowFromEntries: (_existing: unknown, entries: string[]) => entries,
|
|
26
|
+
createAccountListHelpers: () => ({
|
|
27
|
+
listAccountIds: () => ["default"],
|
|
28
|
+
resolveDefaultAccountId: () => "default",
|
|
29
|
+
}),
|
|
26
30
|
normalizeSecretInputString: (value: unknown) => {
|
|
27
31
|
if (typeof value !== "string") {
|
|
28
32
|
return undefined;
|
|
@@ -33,6 +37,10 @@ vi.mock("openclaw/plugin-sdk", () => ({
|
|
|
33
37
|
normalizeAccountId: (value?: string | null) =>
|
|
34
38
|
value && value.trim().length > 0 ? value : "default",
|
|
35
39
|
promptAccountId: vi.fn(),
|
|
40
|
+
resolveAccountIdForConfigure: async (params: {
|
|
41
|
+
accountOverride?: string;
|
|
42
|
+
defaultAccountId: string;
|
|
43
|
+
}) => params.accountOverride?.trim() || params.defaultAccountId,
|
|
36
44
|
}));
|
|
37
45
|
|
|
38
46
|
describe("bluebubbles onboarding SecretInput", () => {
|