@openclaw/bluebubbles 2026.2.24 → 2026.3.1
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/package.json +1 -1
- package/src/accounts.ts +14 -2
- package/src/attachments.test.ts +20 -2
- package/src/attachments.ts +15 -1
- package/src/config-schema.ts +1 -0
- package/src/monitor-processing.ts +52 -41
- package/src/monitor.test.ts +63 -0
- package/src/types.ts +2 -0
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
normalizeAccountId,
|
|
5
|
+
normalizeOptionalAccountId,
|
|
6
|
+
} from "openclaw/plugin-sdk/account-id";
|
|
3
7
|
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
|
|
4
8
|
|
|
5
9
|
export type ResolvedBlueBubblesAccount = {
|
|
@@ -28,6 +32,13 @@ export function listBlueBubblesAccountIds(cfg: OpenClawConfig): string[] {
|
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export function resolveDefaultBlueBubblesAccountId(cfg: OpenClawConfig): string {
|
|
35
|
+
const preferred = normalizeOptionalAccountId(cfg.channels?.bluebubbles?.defaultAccount);
|
|
36
|
+
if (
|
|
37
|
+
preferred &&
|
|
38
|
+
listBlueBubblesAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
|
39
|
+
) {
|
|
40
|
+
return preferred;
|
|
41
|
+
}
|
|
31
42
|
const ids = listBlueBubblesAccountIds(cfg);
|
|
32
43
|
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
33
44
|
return DEFAULT_ACCOUNT_ID;
|
|
@@ -52,8 +63,9 @@ function mergeBlueBubblesAccountConfig(
|
|
|
52
63
|
): BlueBubblesAccountConfig {
|
|
53
64
|
const base = (cfg.channels?.bluebubbles ?? {}) as BlueBubblesAccountConfig & {
|
|
54
65
|
accounts?: unknown;
|
|
66
|
+
defaultAccount?: unknown;
|
|
55
67
|
};
|
|
56
|
-
const { accounts: _ignored, ...rest } = base;
|
|
68
|
+
const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...rest } = base;
|
|
57
69
|
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
58
70
|
const chunkMode = account.chunkMode ?? rest.chunkMode ?? "length";
|
|
59
71
|
return { ...rest, ...account, chunkMode };
|
package/src/attachments.test.ts
CHANGED
|
@@ -294,7 +294,7 @@ describe("downloadBlueBubblesAttachment", () => {
|
|
|
294
294
|
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowPrivateNetwork: true });
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
-
it("
|
|
297
|
+
it("auto-allowlists serverUrl hostname when allowPrivateNetwork is not set", async () => {
|
|
298
298
|
const mockBuffer = new Uint8Array([1]);
|
|
299
299
|
mockFetch.mockResolvedValueOnce({
|
|
300
300
|
ok: true,
|
|
@@ -309,7 +309,25 @@ describe("downloadBlueBubblesAttachment", () => {
|
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
const fetchMediaArgs = fetchRemoteMediaMock.mock.calls[0][0] as Record<string, unknown>;
|
|
312
|
-
expect(fetchMediaArgs.ssrfPolicy).
|
|
312
|
+
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowedHostnames: ["localhost"] });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("auto-allowlists private IP serverUrl hostname when allowPrivateNetwork is not set", async () => {
|
|
316
|
+
const mockBuffer = new Uint8Array([1]);
|
|
317
|
+
mockFetch.mockResolvedValueOnce({
|
|
318
|
+
ok: true,
|
|
319
|
+
headers: new Headers(),
|
|
320
|
+
arrayBuffer: () => Promise.resolve(mockBuffer.buffer),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const attachment: BlueBubblesAttachment = { guid: "att-private-ip" };
|
|
324
|
+
await downloadBlueBubblesAttachment(attachment, {
|
|
325
|
+
serverUrl: "http://192.168.1.5:1234",
|
|
326
|
+
password: "test",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const fetchMediaArgs = fetchRemoteMediaMock.mock.calls[0][0] as Record<string, unknown>;
|
|
330
|
+
expect(fetchMediaArgs.ssrfPolicy).toEqual({ allowedHostnames: ["192.168.1.5"] });
|
|
313
331
|
});
|
|
314
332
|
});
|
|
315
333
|
|
package/src/attachments.ts
CHANGED
|
@@ -62,6 +62,15 @@ function resolveAccount(params: BlueBubblesAttachmentOpts) {
|
|
|
62
62
|
return resolveBlueBubblesServerAccount(params);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function safeExtractHostname(url: string): string | undefined {
|
|
66
|
+
try {
|
|
67
|
+
const hostname = new URL(url).hostname.trim();
|
|
68
|
+
return hostname || undefined;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
type MediaFetchErrorCode = "max_bytes" | "http_error" | "fetch_failed";
|
|
66
75
|
|
|
67
76
|
function readMediaFetchErrorCode(error: unknown): MediaFetchErrorCode | undefined {
|
|
@@ -89,12 +98,17 @@ export async function downloadBlueBubblesAttachment(
|
|
|
89
98
|
password,
|
|
90
99
|
});
|
|
91
100
|
const maxBytes = typeof opts.maxBytes === "number" ? opts.maxBytes : DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
101
|
+
const trustedHostname = safeExtractHostname(baseUrl);
|
|
92
102
|
try {
|
|
93
103
|
const fetched = await getBlueBubblesRuntime().channel.media.fetchRemoteMedia({
|
|
94
104
|
url,
|
|
95
105
|
filePathHint: attachment.transferName ?? attachment.guid ?? "attachment",
|
|
96
106
|
maxBytes,
|
|
97
|
-
ssrfPolicy: allowPrivateNetwork
|
|
107
|
+
ssrfPolicy: allowPrivateNetwork
|
|
108
|
+
? { allowPrivateNetwork: true }
|
|
109
|
+
: trustedHostname
|
|
110
|
+
? { allowedHostnames: [trustedHostname] }
|
|
111
|
+
: undefined,
|
|
98
112
|
fetchImpl: async (input, init) =>
|
|
99
113
|
await blueBubblesFetchWithTimeout(
|
|
100
114
|
resolveRequestUrl(input),
|
package/src/config-schema.ts
CHANGED
|
@@ -61,5 +61,6 @@ const bluebubblesAccountSchema = z
|
|
|
61
61
|
|
|
62
62
|
export const BlueBubblesConfigSchema = bluebubblesAccountSchema.extend({
|
|
63
63
|
accounts: z.object({}).catchall(bluebubblesAccountSchema).optional(),
|
|
64
|
+
defaultAccount: z.string().optional(),
|
|
64
65
|
actions: bluebubblesActionSchema,
|
|
65
66
|
});
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import {
|
|
3
|
+
DM_GROUP_ACCESS_REASON,
|
|
4
|
+
createScopedPairingAccess,
|
|
3
5
|
createReplyPrefixOptions,
|
|
4
6
|
evictOldHistoryKeys,
|
|
5
7
|
logAckFailure,
|
|
6
8
|
logInboundDrop,
|
|
7
9
|
logTypingFailure,
|
|
10
|
+
readStoreAllowFromForDmPolicy,
|
|
8
11
|
recordPendingHistoryEntryIfEnabled,
|
|
9
12
|
resolveAckReaction,
|
|
10
|
-
|
|
11
|
-
resolveEffectiveAllowFromLists,
|
|
13
|
+
resolveDmGroupAccessWithLists,
|
|
12
14
|
resolveControlCommandGate,
|
|
13
15
|
stripMarkdown,
|
|
14
16
|
type HistoryEntry,
|
|
@@ -420,6 +422,11 @@ export async function processMessage(
|
|
|
420
422
|
target: WebhookTarget,
|
|
421
423
|
): Promise<void> {
|
|
422
424
|
const { account, config, runtime, core, statusSink } = target;
|
|
425
|
+
const pairing = createScopedPairingAccess({
|
|
426
|
+
core,
|
|
427
|
+
channel: "bluebubbles",
|
|
428
|
+
accountId: account.accountId,
|
|
429
|
+
});
|
|
423
430
|
const privateApiEnabled = isBlueBubblesPrivateApiEnabled(account.accountId);
|
|
424
431
|
|
|
425
432
|
const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
|
|
@@ -501,27 +508,20 @@ export async function processMessage(
|
|
|
501
508
|
|
|
502
509
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
503
510
|
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
allowFrom: account.config.allowFrom,
|
|
509
|
-
groupAllowFrom: account.config.groupAllowFrom,
|
|
510
|
-
storeAllowFrom,
|
|
511
|
+
const configuredAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
|
|
512
|
+
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
|
513
|
+
provider: "bluebubbles",
|
|
514
|
+
accountId: account.accountId,
|
|
511
515
|
dmPolicy,
|
|
516
|
+
readStore: pairing.readStoreForDmPolicy,
|
|
512
517
|
});
|
|
513
|
-
const
|
|
514
|
-
chatGuid: message.chatGuid,
|
|
515
|
-
chatId: message.chatId ?? undefined,
|
|
516
|
-
chatIdentifier: message.chatIdentifier ?? undefined,
|
|
517
|
-
});
|
|
518
|
-
const groupName = message.chatName?.trim() || undefined;
|
|
519
|
-
const accessDecision = resolveDmGroupAccessDecision({
|
|
518
|
+
const accessDecision = resolveDmGroupAccessWithLists({
|
|
520
519
|
isGroup,
|
|
521
520
|
dmPolicy,
|
|
522
521
|
groupPolicy,
|
|
523
|
-
|
|
524
|
-
|
|
522
|
+
allowFrom: configuredAllowFrom,
|
|
523
|
+
groupAllowFrom: account.config.groupAllowFrom,
|
|
524
|
+
storeAllowFrom,
|
|
525
525
|
isSenderAllowed: (allowFrom) =>
|
|
526
526
|
isAllowedBlueBubblesSender({
|
|
527
527
|
allowFrom,
|
|
@@ -531,10 +531,18 @@ export async function processMessage(
|
|
|
531
531
|
chatIdentifier: message.chatIdentifier ?? undefined,
|
|
532
532
|
}),
|
|
533
533
|
});
|
|
534
|
+
const effectiveAllowFrom = accessDecision.effectiveAllowFrom;
|
|
535
|
+
const effectiveGroupAllowFrom = accessDecision.effectiveGroupAllowFrom;
|
|
536
|
+
const groupAllowEntry = formatGroupAllowlistEntry({
|
|
537
|
+
chatGuid: message.chatGuid,
|
|
538
|
+
chatId: message.chatId ?? undefined,
|
|
539
|
+
chatIdentifier: message.chatIdentifier ?? undefined,
|
|
540
|
+
});
|
|
541
|
+
const groupName = message.chatName?.trim() || undefined;
|
|
534
542
|
|
|
535
543
|
if (accessDecision.decision !== "allow") {
|
|
536
544
|
if (isGroup) {
|
|
537
|
-
if (accessDecision.
|
|
545
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_DISABLED) {
|
|
538
546
|
logVerbose(core, runtime, "Blocked BlueBubbles group message (groupPolicy=disabled)");
|
|
539
547
|
logGroupAllowlistHint({
|
|
540
548
|
runtime,
|
|
@@ -545,7 +553,7 @@ export async function processMessage(
|
|
|
545
553
|
});
|
|
546
554
|
return;
|
|
547
555
|
}
|
|
548
|
-
if (accessDecision.
|
|
556
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST) {
|
|
549
557
|
logVerbose(core, runtime, "Blocked BlueBubbles group message (no allowlist)");
|
|
550
558
|
logGroupAllowlistHint({
|
|
551
559
|
runtime,
|
|
@@ -556,7 +564,7 @@ export async function processMessage(
|
|
|
556
564
|
});
|
|
557
565
|
return;
|
|
558
566
|
}
|
|
559
|
-
if (accessDecision.
|
|
567
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED) {
|
|
560
568
|
logVerbose(
|
|
561
569
|
core,
|
|
562
570
|
runtime,
|
|
@@ -579,15 +587,14 @@ export async function processMessage(
|
|
|
579
587
|
return;
|
|
580
588
|
}
|
|
581
589
|
|
|
582
|
-
if (accessDecision.
|
|
590
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED) {
|
|
583
591
|
logVerbose(core, runtime, `Blocked BlueBubbles DM from ${message.senderId}`);
|
|
584
592
|
logVerbose(core, runtime, `drop: dmPolicy disabled sender=${message.senderId}`);
|
|
585
593
|
return;
|
|
586
594
|
}
|
|
587
595
|
|
|
588
596
|
if (accessDecision.decision === "pairing") {
|
|
589
|
-
const { code, created } = await
|
|
590
|
-
channel: "bluebubbles",
|
|
597
|
+
const { code, created } = await pairing.upsertPairingRequest({
|
|
591
598
|
id: message.senderId,
|
|
592
599
|
meta: { name: message.senderName },
|
|
593
600
|
});
|
|
@@ -666,10 +673,11 @@ export async function processMessage(
|
|
|
666
673
|
// Command gating (parity with iMessage/WhatsApp)
|
|
667
674
|
const useAccessGroups = config.commands?.useAccessGroups !== false;
|
|
668
675
|
const hasControlCmd = core.channel.text.hasControlCommand(messageText, config);
|
|
676
|
+
const commandDmAllowFrom = isGroup ? configuredAllowFrom : effectiveAllowFrom;
|
|
669
677
|
const ownerAllowedForCommands =
|
|
670
|
-
|
|
678
|
+
commandDmAllowFrom.length > 0
|
|
671
679
|
? isAllowedBlueBubblesSender({
|
|
672
|
-
allowFrom:
|
|
680
|
+
allowFrom: commandDmAllowFrom,
|
|
673
681
|
sender: message.senderId,
|
|
674
682
|
chatId: message.chatId ?? undefined,
|
|
675
683
|
chatGuid: message.chatGuid ?? undefined,
|
|
@@ -686,17 +694,16 @@ export async function processMessage(
|
|
|
686
694
|
chatIdentifier: message.chatIdentifier ?? undefined,
|
|
687
695
|
})
|
|
688
696
|
: false;
|
|
689
|
-
const dmAuthorized = dmPolicy === "open" || ownerAllowedForCommands;
|
|
690
697
|
const commandGate = resolveControlCommandGate({
|
|
691
698
|
useAccessGroups,
|
|
692
699
|
authorizers: [
|
|
693
|
-
{ configured:
|
|
700
|
+
{ configured: commandDmAllowFrom.length > 0, allowed: ownerAllowedForCommands },
|
|
694
701
|
{ configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
|
|
695
702
|
],
|
|
696
703
|
allowTextCommands: true,
|
|
697
704
|
hasControlCommand: hasControlCmd,
|
|
698
705
|
});
|
|
699
|
-
const commandAuthorized =
|
|
706
|
+
const commandAuthorized = commandGate.commandAuthorized;
|
|
700
707
|
|
|
701
708
|
// Block control commands from unauthorized senders in groups
|
|
702
709
|
if (isGroup && commandGate.shouldBlock) {
|
|
@@ -1091,14 +1098,15 @@ export async function processMessage(
|
|
|
1091
1098
|
});
|
|
1092
1099
|
}
|
|
1093
1100
|
}
|
|
1101
|
+
const commandBody = messageText.trim();
|
|
1094
1102
|
|
|
1095
1103
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
1096
1104
|
Body: body,
|
|
1097
1105
|
BodyForAgent: rawBody,
|
|
1098
1106
|
InboundHistory: inboundHistory,
|
|
1099
1107
|
RawBody: rawBody,
|
|
1100
|
-
CommandBody:
|
|
1101
|
-
BodyForCommands:
|
|
1108
|
+
CommandBody: commandBody,
|
|
1109
|
+
BodyForCommands: commandBody,
|
|
1102
1110
|
MediaUrl: mediaUrls[0],
|
|
1103
1111
|
MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
|
|
1104
1112
|
MediaPath: mediaPaths[0],
|
|
@@ -1380,27 +1388,30 @@ export async function processReaction(
|
|
|
1380
1388
|
target: WebhookTarget,
|
|
1381
1389
|
): Promise<void> {
|
|
1382
1390
|
const { account, config, runtime, core } = target;
|
|
1391
|
+
const pairing = createScopedPairingAccess({
|
|
1392
|
+
core,
|
|
1393
|
+
channel: "bluebubbles",
|
|
1394
|
+
accountId: account.accountId,
|
|
1395
|
+
});
|
|
1383
1396
|
if (reaction.fromMe) {
|
|
1384
1397
|
return;
|
|
1385
1398
|
}
|
|
1386
1399
|
|
|
1387
1400
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
1388
1401
|
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
|
1389
|
-
const storeAllowFrom = await
|
|
1390
|
-
|
|
1391
|
-
.
|
|
1392
|
-
const { effectiveAllowFrom, effectiveGroupAllowFrom } = resolveEffectiveAllowFromLists({
|
|
1393
|
-
allowFrom: account.config.allowFrom,
|
|
1394
|
-
groupAllowFrom: account.config.groupAllowFrom,
|
|
1395
|
-
storeAllowFrom,
|
|
1402
|
+
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
|
1403
|
+
provider: "bluebubbles",
|
|
1404
|
+
accountId: account.accountId,
|
|
1396
1405
|
dmPolicy,
|
|
1406
|
+
readStore: pairing.readStoreForDmPolicy,
|
|
1397
1407
|
});
|
|
1398
|
-
const accessDecision =
|
|
1408
|
+
const accessDecision = resolveDmGroupAccessWithLists({
|
|
1399
1409
|
isGroup: reaction.isGroup,
|
|
1400
1410
|
dmPolicy,
|
|
1401
1411
|
groupPolicy,
|
|
1402
|
-
|
|
1403
|
-
|
|
1412
|
+
allowFrom: account.config.allowFrom,
|
|
1413
|
+
groupAllowFrom: account.config.groupAllowFrom,
|
|
1414
|
+
storeAllowFrom,
|
|
1404
1415
|
isSenderAllowed: (allowFrom) =>
|
|
1405
1416
|
isAllowedBlueBubblesSender({
|
|
1406
1417
|
allowFrom,
|
package/src/monitor.test.ts
CHANGED
|
@@ -162,6 +162,24 @@ function createMockRuntime(): PluginRuntime {
|
|
|
162
162
|
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
|
|
163
163
|
dispatchReplyFromConfig:
|
|
164
164
|
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
|
|
165
|
+
withReplyDispatcher: vi.fn(
|
|
166
|
+
async ({
|
|
167
|
+
dispatcher,
|
|
168
|
+
run,
|
|
169
|
+
onSettled,
|
|
170
|
+
}: Parameters<PluginRuntime["channel"]["reply"]["withReplyDispatcher"]>[0]) => {
|
|
171
|
+
try {
|
|
172
|
+
return await run();
|
|
173
|
+
} finally {
|
|
174
|
+
dispatcher.markComplete();
|
|
175
|
+
try {
|
|
176
|
+
await dispatcher.waitForIdle();
|
|
177
|
+
} finally {
|
|
178
|
+
await onSettled?.();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
) as unknown as PluginRuntime["channel"]["reply"]["withReplyDispatcher"],
|
|
165
183
|
finalizeInboundContext: vi.fn(
|
|
166
184
|
(ctx: Record<string, unknown>) => ctx,
|
|
167
185
|
) as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
|
@@ -2287,6 +2305,51 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2287
2305
|
|
|
2288
2306
|
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
2289
2307
|
});
|
|
2308
|
+
|
|
2309
|
+
it("does not auto-authorize DM control commands in open mode without allowlists", async () => {
|
|
2310
|
+
mockHasControlCommand.mockReturnValue(true);
|
|
2311
|
+
|
|
2312
|
+
const account = createMockAccount({
|
|
2313
|
+
dmPolicy: "open",
|
|
2314
|
+
allowFrom: [],
|
|
2315
|
+
});
|
|
2316
|
+
const config: OpenClawConfig = {};
|
|
2317
|
+
const core = createMockRuntime();
|
|
2318
|
+
setBlueBubblesRuntime(core);
|
|
2319
|
+
|
|
2320
|
+
unregister = registerBlueBubblesWebhookTarget({
|
|
2321
|
+
account,
|
|
2322
|
+
config,
|
|
2323
|
+
runtime: { log: vi.fn(), error: vi.fn() },
|
|
2324
|
+
core,
|
|
2325
|
+
path: "/bluebubbles-webhook",
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
const payload = {
|
|
2329
|
+
type: "new-message",
|
|
2330
|
+
data: {
|
|
2331
|
+
text: "/status",
|
|
2332
|
+
handle: { address: "+15559999999" },
|
|
2333
|
+
isGroup: false,
|
|
2334
|
+
isFromMe: false,
|
|
2335
|
+
guid: "msg-dm-open-unauthorized",
|
|
2336
|
+
date: Date.now(),
|
|
2337
|
+
},
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
2341
|
+
const res = createMockResponse();
|
|
2342
|
+
|
|
2343
|
+
await handleBlueBubblesWebhookRequest(req, res);
|
|
2344
|
+
await flushAsync();
|
|
2345
|
+
|
|
2346
|
+
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
|
2347
|
+
const latestDispatch =
|
|
2348
|
+
mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[
|
|
2349
|
+
mockDispatchReplyWithBufferedBlockDispatcher.mock.calls.length - 1
|
|
2350
|
+
]?.[0];
|
|
2351
|
+
expect(latestDispatch?.ctx?.CommandAuthorized).toBe(false);
|
|
2352
|
+
});
|
|
2290
2353
|
});
|
|
2291
2354
|
|
|
2292
2355
|
describe("typing/read receipt toggles", () => {
|
package/src/types.ts
CHANGED
|
@@ -75,6 +75,8 @@ export type BlueBubblesActionConfig = {
|
|
|
75
75
|
export type BlueBubblesConfig = {
|
|
76
76
|
/** Optional per-account BlueBubbles configuration (multi-account). */
|
|
77
77
|
accounts?: Record<string, BlueBubblesAccountConfig>;
|
|
78
|
+
/** Optional default account id when multiple accounts are configured. */
|
|
79
|
+
defaultAccount?: string;
|
|
78
80
|
/** Per-action tool gating (default: true for all). */
|
|
79
81
|
actions?: BlueBubblesActionConfig;
|
|
80
82
|
} & BlueBubblesAccountConfig;
|