@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.
@@ -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
- resolveWebhookTargets,
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
- const resolved = resolveWebhookTargets(req, webhookTargets);
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
- inFlightKey: `${path}:${req.socket.remoteAddress ?? "unknown"}`,
137
- });
138
- if (!requestLifecycle.ok) {
139
- return true;
140
- }
141
-
142
- try {
143
- const guidParam = url.searchParams.get("guid") ?? url.searchParams.get("password");
144
- const headerToken =
145
- req.headers["x-guid"] ??
146
- req.headers["x-password"] ??
147
- req.headers["x-bluebubbles-guid"] ??
148
- req.headers["authorization"];
149
- const guid = (Array.isArray(headerToken) ? headerToken[0] : headerToken) ?? guidParam ?? "";
150
- const target = resolveWebhookTargetWithAuthOrRejectSync({
151
- targets,
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
- } else if (message) {
242
- // Route messages through debouncer to coalesce rapid-fire events
243
- // (e.g., text message + URL balloon arriving as separate webhooks)
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
- res.statusCode = 200;
253
- res.end("ok");
254
- if (reaction) {
255
- if (firstTarget) {
256
- logVerbose(
257
- firstTarget.core,
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
- } else if (message) {
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 accepted sender=${message.senderId} group=${message.isGroup} chatGuid=${message.chatGuid ?? ""} chatId=${message.chatId ?? ""}`,
178
+ `webhook received path=${path} keys=${Object.keys(payload).join(",") || "none"}`,
268
179
  );
269
180
  }
270
- }
271
- return true;
272
- } finally {
273
- requestLifecycle.release();
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
- const account = createMockAccount();
290
- const config: OpenClawConfig = {};
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
- const account = createMockAccount();
349
- const config: OpenClawConfig = {};
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("POST", "/bluebubbles-webhook?password=secret-token", {
467
- type: "new-message",
468
- data: {
469
- text: "hello",
470
- handle: { address: "+15551234567" },
471
- isGroup: false,
472
- isFromMe: false,
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
- type: "new-message",
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 as unknown as { socket: { remoteAddress: string } }).socket = {
517
- remoteAddress: "192.168.1.100",
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 config: OpenClawConfig = {};
538
- const core = createMockRuntime();
539
- setBlueBubblesRuntime(core);
540
-
541
- const req = createMockRequest("POST", "/bluebubbles-webhook?password=wrong-token", {
542
- type: "new-message",
543
- data: {
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
- const account = createMockAccount({ groupPolicy: "open" });
774
- const config: OpenClawConfig = {};
775
- const core = createMockRuntime();
776
- setBlueBubblesRuntime(core);
777
-
778
- unregister = registerBlueBubblesWebhookTarget({
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
- const account = createMockAccount({ groupPolicy: "open" });
823
- const config: OpenClawConfig = {};
824
- const core = createMockRuntime();
825
- setBlueBubblesRuntime(core);
826
-
827
- unregister = registerBlueBubblesWebhookTarget({
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", () => {