@openclaw/signal 0.0.0 → 2026.6.11

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.
Files changed (49) hide show
  1. package/README.md +12 -2
  2. package/dist/accounts-hOCHbEhX.js +40 -0
  3. package/dist/api.js +16 -0
  4. package/dist/approval-handler.runtime-CeJI1wow.js +158 -0
  5. package/dist/approval-resolver-BR0MioAA.js +15 -0
  6. package/dist/channel-LmY2UpOt.js +619 -0
  7. package/dist/channel-config-api.js +2 -0
  8. package/dist/channel-entry.js +18 -0
  9. package/dist/channel-plugin-api.js +2 -0
  10. package/dist/channel.runtime-B0-YpkC4.js +65 -0
  11. package/dist/config-api-KS-qhQvD.js +2 -0
  12. package/dist/config-schema-BiojLEsX.js +27 -0
  13. package/dist/contract-api.js +3 -0
  14. package/dist/identity-B6O4k8xg.js +130 -0
  15. package/dist/index.js +18 -0
  16. package/dist/install-signal-cli-CXgTF3de.js +243 -0
  17. package/dist/message-actions-Bue0g2Kc.js +548 -0
  18. package/dist/monitor-CUhIKHJo.js +1404 -0
  19. package/dist/probe-BQ_Izoya.js +48 -0
  20. package/dist/reaction-runtime-api-C_PQ45D9.js +1074 -0
  21. package/dist/reaction-runtime-api.js +2 -0
  22. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  23. package/dist/runtime-api.js +18 -0
  24. package/dist/secret-contract-api.js +5 -0
  25. package/dist/send-CBlFUkY_.js +599 -0
  26. package/dist/send.runtime-CfaZd8X4.js +2 -0
  27. package/dist/setup-entry.js +11 -0
  28. package/node_modules/ws/LICENSE +20 -0
  29. package/node_modules/ws/README.md +548 -0
  30. package/node_modules/ws/browser.js +8 -0
  31. package/node_modules/ws/index.js +22 -0
  32. package/node_modules/ws/lib/buffer-util.js +131 -0
  33. package/node_modules/ws/lib/constants.js +19 -0
  34. package/node_modules/ws/lib/event-target.js +292 -0
  35. package/node_modules/ws/lib/extension.js +203 -0
  36. package/node_modules/ws/lib/limiter.js +55 -0
  37. package/node_modules/ws/lib/permessage-deflate.js +528 -0
  38. package/node_modules/ws/lib/receiver.js +760 -0
  39. package/node_modules/ws/lib/sender.js +607 -0
  40. package/node_modules/ws/lib/stream.js +161 -0
  41. package/node_modules/ws/lib/subprotocol.js +62 -0
  42. package/node_modules/ws/lib/validation.js +152 -0
  43. package/node_modules/ws/lib/websocket-server.js +562 -0
  44. package/node_modules/ws/lib/websocket.js +1407 -0
  45. package/node_modules/ws/package.json +70 -0
  46. package/node_modules/ws/wrapper.mjs +21 -0
  47. package/npm-shrinkwrap.json +36 -0
  48. package/openclaw.plugin.json +771 -0
  49. package/package.json +77 -7
@@ -0,0 +1,1074 @@
1
+ import { i as resolveSignalAccount } from "./accounts-hOCHbEhX.js";
2
+ import { detectMime, parseMediaContentLength } from "openclaw/plugin-sdk/media-runtime";
3
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
4
+ import { asDateTimestampMs, parseStrictNonNegativeInteger, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
5
+ import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { resolveFetch } from "openclaw/plugin-sdk/fetch-runtime";
9
+ import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
10
+ import WebSocket from "ws";
11
+ import { Buffer as Buffer$1 } from "node:buffer";
12
+ import http from "node:http";
13
+ import https from "node:https";
14
+ import { generateSecureUuid } from "openclaw/plugin-sdk/core";
15
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
16
+ //#region extensions/signal/src/client-container.ts
17
+ /**
18
+ * Signal client for bbernhard/signal-cli-rest-api container.
19
+ * Uses WebSocket for receiving messages and REST API for sending.
20
+ *
21
+ * This is a separate implementation from client.ts (native signal-cli)
22
+ * to keep the two modes cleanly isolated.
23
+ */
24
+ const DEFAULT_TIMEOUT_MS$2 = 1e4;
25
+ const DEFAULT_ATTACHMENT_RESPONSE_MAX_BYTES = 1048576;
26
+ const CONTAINER_TEXT_STYLE_MARKERS = {
27
+ BOLD: "**",
28
+ ITALIC: "*",
29
+ STRIKETHROUGH: "~",
30
+ MONOSPACE: "`",
31
+ SPOILER: "||"
32
+ };
33
+ function normalizeBaseUrl$1(url) {
34
+ const trimmed = url.trim();
35
+ if (!trimmed) throw new Error("Signal base URL is required");
36
+ const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
37
+ const parsed = new URL(withProtocol);
38
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`Signal base URL unsupported protocol: ${parsed.protocol}`);
39
+ if (parsed.username || parsed.password) throw new Error("Signal base URL must not include credentials");
40
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
41
+ return `${parsed.protocol}//${parsed.host}${pathname}`;
42
+ }
43
+ async function fetchWithTimeout(url, init, timeoutMs) {
44
+ const fetchImpl = resolveFetch();
45
+ if (!fetchImpl) throw new Error("fetch is not available");
46
+ const safeTimeoutMs = resolveTimerTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS$2);
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => controller.abort(), safeTimeoutMs);
49
+ try {
50
+ return await fetchImpl(url, {
51
+ ...init,
52
+ signal: controller.signal
53
+ });
54
+ } finally {
55
+ clearTimeout(timer);
56
+ }
57
+ }
58
+ function normalizeMaxResponseBytes(value) {
59
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return DEFAULT_ATTACHMENT_RESPONSE_MAX_BYTES;
60
+ return Math.floor(value);
61
+ }
62
+ function readContentLength(res) {
63
+ return parseMediaContentLength(res.headers?.get("content-length") ?? null) ?? void 0;
64
+ }
65
+ async function readCappedResponseBuffer(res, maxResponseBytes) {
66
+ const contentLength = readContentLength(res);
67
+ if (contentLength !== void 0 && contentLength > maxResponseBytes) throw new Error("Signal REST attachment exceeded size limit");
68
+ return await readResponseWithLimit(res, maxResponseBytes, { onOverflow: () => /* @__PURE__ */ new Error("Signal REST attachment exceeded size limit") });
69
+ }
70
+ async function releaseUnreadResponseBody(res) {
71
+ if (res?.bodyUsed !== true) await res?.body?.cancel().catch(() => void 0);
72
+ }
73
+ /**
74
+ * Check if bbernhard container REST API is available.
75
+ */
76
+ async function containerCheck(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS$2, account) {
77
+ const normalized = normalizeBaseUrl$1(baseUrl);
78
+ let res;
79
+ try {
80
+ res = await fetchWithTimeout(`${normalized}/v1/about`, { method: "GET" }, timeoutMs);
81
+ if (!res.ok) return {
82
+ ok: false,
83
+ status: res.status,
84
+ error: `HTTP ${res.status}`
85
+ };
86
+ const receiveAccount = account?.trim();
87
+ if (receiveAccount) return await containerReceiveCheck(normalized, receiveAccount, timeoutMs);
88
+ return {
89
+ ok: true,
90
+ status: res.status,
91
+ error: null
92
+ };
93
+ } catch (err) {
94
+ return {
95
+ ok: false,
96
+ status: null,
97
+ error: err instanceof Error ? err.message : String(err)
98
+ };
99
+ } finally {
100
+ await releaseUnreadResponseBody(res);
101
+ }
102
+ }
103
+ function containerReceiveCheck(normalizedBaseUrl, account, timeoutMs) {
104
+ const wsUrl = `${normalizedBaseUrl.replace(/^http/, "ws")}/v1/receive/${encodeURIComponent(account)}`;
105
+ return new Promise((resolve) => {
106
+ const safeTimeoutMs = resolveTimerTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS$2);
107
+ let settled = false;
108
+ let ws;
109
+ const timer = setTimeout(() => {
110
+ settle({
111
+ ok: false,
112
+ status: null,
113
+ error: "Signal container receive WebSocket timed out"
114
+ });
115
+ ws?.terminate();
116
+ }, safeTimeoutMs);
117
+ timer.unref?.();
118
+ const settle = (result) => {
119
+ if (settled) return;
120
+ settled = true;
121
+ clearTimeout(timer);
122
+ resolve(result);
123
+ };
124
+ try {
125
+ ws = new WebSocket(wsUrl);
126
+ } catch (err) {
127
+ settle({
128
+ ok: false,
129
+ status: null,
130
+ error: err instanceof Error ? err.message : String(err)
131
+ });
132
+ return;
133
+ }
134
+ ws.once("open", () => {
135
+ settle({
136
+ ok: true,
137
+ status: 101,
138
+ error: null
139
+ });
140
+ ws?.close();
141
+ });
142
+ ws.once("unexpected-response", (_request, response) => {
143
+ settle({
144
+ ok: false,
145
+ status: response.statusCode ?? null,
146
+ error: `Signal container receive endpoint did not upgrade to WebSocket (HTTP ${response.statusCode ?? "unknown"})`
147
+ });
148
+ ws?.terminate();
149
+ });
150
+ ws.once("error", (err) => {
151
+ settle({
152
+ ok: false,
153
+ status: null,
154
+ error: err instanceof Error ? err.message : String(err)
155
+ });
156
+ });
157
+ ws.once("close", (code, reason) => {
158
+ settle({
159
+ ok: false,
160
+ status: null,
161
+ error: `Signal container receive WebSocket closed before open (${code}${reason.length > 0 ? `: ${reason.toString("utf8")}` : ""})`
162
+ });
163
+ });
164
+ });
165
+ }
166
+ /**
167
+ * Make a REST API request to bbernhard container.
168
+ */
169
+ async function containerRestRequest(endpoint, opts, method = "GET", body) {
170
+ const url = `${normalizeBaseUrl$1(opts.baseUrl)}${endpoint}`;
171
+ const init = {
172
+ method,
173
+ headers: { "Content-Type": "application/json" }
174
+ };
175
+ if (body) init.body = JSON.stringify(body);
176
+ const res = await fetchWithTimeout(url, init, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$2);
177
+ if (res.status === 204) return;
178
+ if (!res.ok) {
179
+ const errorText = await res.text().catch(() => "");
180
+ throw new Error(`Signal REST ${res.status}: ${errorText || res.statusText}`);
181
+ }
182
+ const text = await res.text();
183
+ if (!text) return;
184
+ return JSON.parse(text);
185
+ }
186
+ /**
187
+ * Fetch attachment binary from bbernhard container.
188
+ */
189
+ async function containerFetchAttachment(attachmentId, opts) {
190
+ const url = `${normalizeBaseUrl$1(opts.baseUrl)}/v1/attachments/${encodeURIComponent(attachmentId)}`;
191
+ let res;
192
+ try {
193
+ res = await fetchWithTimeout(url, { method: "GET" }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$2);
194
+ if (!res.ok) return null;
195
+ return await readCappedResponseBuffer(res, normalizeMaxResponseBytes(opts.maxResponseBytes));
196
+ } finally {
197
+ await releaseUnreadResponseBody(res);
198
+ }
199
+ }
200
+ /**
201
+ * Stream messages using WebSocket from bbernhard container.
202
+ * The Promise resolves when the connection closes (for any reason).
203
+ * The caller (runSignalLoopAdapter) is responsible for reconnection.
204
+ */
205
+ async function streamContainerEvents(params) {
206
+ const normalized = normalizeBaseUrl$1(params.baseUrl);
207
+ const wsUrl = `${normalized.replace(/^http/, "ws")}/v1/receive/${encodeURIComponent(params.account ?? "")}`;
208
+ const redactedWsUrl = `${normalized.replace(/^http/, "ws")}/v1/receive/<redacted>`;
209
+ const log = params.logger?.log ?? (() => {});
210
+ const logError = params.logger?.error ?? (() => {});
211
+ log(`[signal-ws] connecting to ${redactedWsUrl}`);
212
+ return new Promise((resolve, reject) => {
213
+ let ws;
214
+ let resolved = false;
215
+ let abortHandler;
216
+ const cleanup = () => {
217
+ if (resolved) return;
218
+ resolved = true;
219
+ if (abortHandler) {
220
+ params.abortSignal?.removeEventListener("abort", abortHandler);
221
+ abortHandler = void 0;
222
+ }
223
+ };
224
+ try {
225
+ ws = new WebSocket(wsUrl);
226
+ } catch (err) {
227
+ logError(`[signal-ws] failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`);
228
+ reject(toLintErrorObject$1(err, "Non-Error rejection"));
229
+ return;
230
+ }
231
+ ws.on("open", () => {
232
+ log("[signal-ws] connected");
233
+ });
234
+ ws.on("message", (data) => {
235
+ try {
236
+ const text = data.toString();
237
+ const envelope = JSON.parse(text);
238
+ if (envelope) params.onEvent(envelope);
239
+ } catch (err) {
240
+ logError(`[signal-ws] parse error: ${err instanceof Error ? err.message : String(err)}`);
241
+ }
242
+ });
243
+ ws.on("error", (err) => {
244
+ logError(`[signal-ws] error: ${err instanceof Error ? err.message : String(err)}`);
245
+ });
246
+ ws.on("close", (code, reason) => {
247
+ log(`[signal-ws] closed (code=${code}, reason=${reason?.toString() || "no reason"})`);
248
+ cleanup();
249
+ resolve();
250
+ });
251
+ ws.on("ping", () => {
252
+ log("[signal-ws] ping received");
253
+ });
254
+ ws.on("pong", () => {
255
+ log("[signal-ws] pong received");
256
+ });
257
+ if (params.abortSignal) {
258
+ abortHandler = () => {
259
+ log("[signal-ws] aborted, closing connection");
260
+ cleanup();
261
+ ws.close();
262
+ resolve();
263
+ };
264
+ params.abortSignal.addEventListener("abort", abortHandler, { once: true });
265
+ }
266
+ });
267
+ }
268
+ /**
269
+ * Convert local file paths to base64 data URIs for the container REST API.
270
+ * The bbernhard container /v2/send only accepts `base64_attachments` (not file paths).
271
+ */
272
+ async function filesToBase64DataUris(filePaths) {
273
+ const results = [];
274
+ for (const filePath of filePaths) {
275
+ const buffer = await fs.readFile(filePath);
276
+ const mime = await detectMime({
277
+ buffer,
278
+ filePath
279
+ }) ?? "application/octet-stream";
280
+ const filename = path.basename(filePath);
281
+ const b64 = buffer.toString("base64");
282
+ results.push(`data:${mime};filename=${filename};base64,${b64}`);
283
+ }
284
+ return results;
285
+ }
286
+ function escapeContainerStyledText(text) {
287
+ return text.replace(/[*~`|]/g, (char) => `\\${char}`);
288
+ }
289
+ function renderContainerStyledText(text, styles) {
290
+ const spans = styles.map((style) => {
291
+ const marker = CONTAINER_TEXT_STYLE_MARKERS[style.style];
292
+ if (!marker) return null;
293
+ const start = Math.max(0, Math.min(style.start, text.length));
294
+ const end = Math.max(start, Math.min(style.start + style.length, text.length));
295
+ if (end <= start) return null;
296
+ return {
297
+ start,
298
+ end,
299
+ marker
300
+ };
301
+ }).filter((span) => span !== null);
302
+ if (spans.length === 0) return text;
303
+ const positions = [...new Set([
304
+ 0,
305
+ text.length,
306
+ ...spans.flatMap((span) => [span.start, span.end])
307
+ ])].toSorted((a, b) => a - b);
308
+ let rendered = "";
309
+ for (let i = 0; i < positions.length; i += 1) {
310
+ const pos = positions[i];
311
+ for (const span of spans.filter((candidate) => candidate.end === pos).toSorted((a, b) => b.start - a.start)) rendered += span.marker;
312
+ for (const span of spans.filter((candidate) => candidate.start === pos).toSorted((a, b) => b.end - a.end)) rendered += span.marker;
313
+ const next = positions[i + 1];
314
+ if (next !== void 0 && next > pos) rendered += escapeContainerStyledText(text.slice(pos, next));
315
+ }
316
+ return rendered;
317
+ }
318
+ function parseContainerSendTimestamp(raw) {
319
+ if (raw == null) return;
320
+ const timestamp = parseStrictNonNegativeInteger(raw);
321
+ if (timestamp === void 0) throw new Error("Signal REST send returned invalid timestamp");
322
+ return timestamp;
323
+ }
324
+ /**
325
+ * Send message via bbernhard container REST API.
326
+ */
327
+ async function containerSendMessage(params) {
328
+ const payload = {
329
+ message: params.message,
330
+ number: params.account,
331
+ recipients: params.recipients
332
+ };
333
+ if (params.textStyles && params.textStyles.length > 0) {
334
+ payload.message = renderContainerStyledText(params.message, params.textStyles);
335
+ payload["text_mode"] = "styled";
336
+ }
337
+ if (params.attachments && params.attachments.length > 0) payload.base64_attachments = await filesToBase64DataUris(params.attachments);
338
+ const timestamp = parseContainerSendTimestamp((await containerRestRequest("/v2/send", {
339
+ baseUrl: params.baseUrl,
340
+ timeoutMs: params.timeoutMs
341
+ }, "POST", payload))?.timestamp);
342
+ return timestamp === void 0 ? {} : { timestamp };
343
+ }
344
+ /**
345
+ * Send typing indicator via bbernhard container REST API.
346
+ */
347
+ async function containerSendTyping(params) {
348
+ const method = params.stop ? "DELETE" : "PUT";
349
+ await containerRestRequest(`/v1/typing-indicator/${encodeURIComponent(params.account)}`, {
350
+ baseUrl: params.baseUrl,
351
+ timeoutMs: params.timeoutMs
352
+ }, method, { recipient: params.recipient });
353
+ return true;
354
+ }
355
+ /**
356
+ * Send read receipt via bbernhard container REST API.
357
+ */
358
+ async function containerSendReceipt(params) {
359
+ await containerRestRequest(`/v1/receipts/${encodeURIComponent(params.account)}`, {
360
+ baseUrl: params.baseUrl,
361
+ timeoutMs: params.timeoutMs
362
+ }, "POST", {
363
+ recipient: params.recipient,
364
+ timestamp: params.timestamp,
365
+ receipt_type: params.type ?? "read"
366
+ });
367
+ return true;
368
+ }
369
+ /**
370
+ * Send a reaction to a message via bbernhard container REST API.
371
+ */
372
+ async function containerSendReaction(params) {
373
+ const payload = {
374
+ recipient: params.recipient,
375
+ reaction: params.emoji,
376
+ target_author: params.targetAuthor,
377
+ timestamp: params.targetTimestamp
378
+ };
379
+ if (params.groupId) payload.group_id = params.groupId;
380
+ return await containerRestRequest(`/v1/reactions/${encodeURIComponent(params.account)}`, {
381
+ baseUrl: params.baseUrl,
382
+ timeoutMs: params.timeoutMs
383
+ }, "POST", payload) ?? {};
384
+ }
385
+ /**
386
+ * Remove a reaction from a message via bbernhard container REST API.
387
+ */
388
+ async function containerRemoveReaction(params) {
389
+ const payload = {
390
+ recipient: params.recipient,
391
+ reaction: params.emoji,
392
+ target_author: params.targetAuthor,
393
+ timestamp: params.targetTimestamp
394
+ };
395
+ if (params.groupId) payload.group_id = params.groupId;
396
+ return await containerRestRequest(`/v1/reactions/${encodeURIComponent(params.account)}`, {
397
+ baseUrl: params.baseUrl,
398
+ timeoutMs: params.timeoutMs
399
+ }, "DELETE", payload) ?? {};
400
+ }
401
+ /**
402
+ * Strip the "uuid:" prefix that native signal-cli accepts but the container API rejects.
403
+ */
404
+ function stripUuidPrefix(id) {
405
+ return id.startsWith("uuid:") ? id.slice(5) : id;
406
+ }
407
+ /**
408
+ * Convert a group internal_id to the container-expected format.
409
+ * The bbernhard container expects groups as "group.{base64(internal_id)}".
410
+ */
411
+ function formatGroupIdForContainer(groupId) {
412
+ if (groupId.startsWith("group.")) return groupId;
413
+ return `group.${Buffer.from(groupId).toString("base64")}`;
414
+ }
415
+ /**
416
+ * Drop-in replacement for native signalRpcRequest that translates
417
+ * JSON-RPC method + params into the equivalent container REST API calls.
418
+ * This keeps all container protocol details (uuid: stripping, group ID
419
+ * formatting, base64 attachments, text-style conversion) isolated here.
420
+ */
421
+ async function containerRpcRequest(method, params, opts) {
422
+ const p = params ?? {};
423
+ switch (method) {
424
+ case "send": {
425
+ const recipients = (p.recipient ?? []).map(stripUuidPrefix);
426
+ const usernames = (p.username ?? []).map(stripUuidPrefix);
427
+ const groupId = p.groupId;
428
+ const formattedGroupId = groupId ? formatGroupIdForContainer(groupId) : void 0;
429
+ const finalRecipients = recipients.length > 0 ? recipients : usernames.length > 0 ? usernames : formattedGroupId ? [formattedGroupId] : [];
430
+ const textStyles = p["text-style"]?.map((s) => {
431
+ const [start, length, style] = s.split(":");
432
+ return {
433
+ start: Number(start),
434
+ length: Number(length),
435
+ style
436
+ };
437
+ });
438
+ return await containerSendMessage({
439
+ baseUrl: opts.baseUrl,
440
+ account: p.account ?? "",
441
+ recipients: finalRecipients,
442
+ message: p.message ?? "",
443
+ textStyles,
444
+ attachments: p.attachments,
445
+ timeoutMs: opts.timeoutMs
446
+ });
447
+ }
448
+ case "sendTyping": {
449
+ const recipient = stripUuidPrefix(p.recipient?.[0] ?? (p.groupId ? formatGroupIdForContainer(p.groupId) : ""));
450
+ await containerSendTyping({
451
+ baseUrl: opts.baseUrl,
452
+ account: p.account ?? "",
453
+ recipient,
454
+ stop: p.stop,
455
+ timeoutMs: opts.timeoutMs
456
+ });
457
+ return;
458
+ }
459
+ case "sendReceipt": {
460
+ const recipient = stripUuidPrefix(p.recipient?.[0] ?? "");
461
+ await containerSendReceipt({
462
+ baseUrl: opts.baseUrl,
463
+ account: p.account ?? "",
464
+ recipient,
465
+ timestamp: p.targetTimestamp,
466
+ type: p.type,
467
+ timeoutMs: opts.timeoutMs
468
+ });
469
+ return;
470
+ }
471
+ case "sendReaction": {
472
+ const recipient = stripUuidPrefix(p.recipients?.[0] ?? "");
473
+ const groupId = p.groupIds?.[0] ?? void 0;
474
+ const formattedGroupId = groupId ? formatGroupIdForContainer(groupId) : void 0;
475
+ const effectiveRecipient = formattedGroupId || recipient || "";
476
+ const reactionParams = {
477
+ baseUrl: opts.baseUrl,
478
+ account: p.account ?? "",
479
+ recipient: effectiveRecipient,
480
+ emoji: p.emoji ?? "",
481
+ targetAuthor: stripUuidPrefix(p.targetAuthor ?? recipient),
482
+ targetTimestamp: p.targetTimestamp,
483
+ groupId: formattedGroupId,
484
+ timeoutMs: opts.timeoutMs
485
+ };
486
+ return await (p.remove ? containerRemoveReaction : containerSendReaction)(reactionParams);
487
+ }
488
+ case "getAttachment": {
489
+ const attachmentId = p.id;
490
+ const buffer = await containerFetchAttachment(attachmentId, {
491
+ baseUrl: opts.baseUrl,
492
+ timeoutMs: opts.timeoutMs,
493
+ maxResponseBytes: opts.maxResponseBytes
494
+ });
495
+ if (!buffer) return { data: void 0 };
496
+ return { data: buffer.toString("base64") };
497
+ }
498
+ case "version": return await containerRestRequest("/v1/about", {
499
+ baseUrl: opts.baseUrl,
500
+ timeoutMs: opts.timeoutMs
501
+ });
502
+ default: throw new Error(`Unsupported container RPC method: ${method}`);
503
+ }
504
+ }
505
+ function toLintErrorObject$1(value, fallbackMessage) {
506
+ if (value instanceof Error) return value;
507
+ if (typeof value === "string") return new Error(value);
508
+ const error = new Error(fallbackMessage, { cause: value });
509
+ if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
510
+ return error;
511
+ }
512
+ //#endregion
513
+ //#region extensions/signal/src/client.ts
514
+ const DEFAULT_TIMEOUT_MS$1 = 1e4;
515
+ const DEFAULT_SIGNAL_HTTP_RESPONSE_MAX_BYTES = 1048576;
516
+ const MAX_SIGNAL_SSE_BUFFER_BYTES = 1048576;
517
+ const MAX_SIGNAL_SSE_EVENT_DATA_BYTES = 1048576;
518
+ function createSignalSseAbortError() {
519
+ const error = /* @__PURE__ */ new Error("Signal SSE aborted");
520
+ error.name = "AbortError";
521
+ return error;
522
+ }
523
+ function normalizeBaseUrl(url) {
524
+ const trimmed = url.trim();
525
+ if (!trimmed) throw new Error("Signal base URL is required");
526
+ if (/^https?:\/\//i.test(trimmed)) return trimmed.replace(/\/+$/, "");
527
+ return `http://${trimmed}`.replace(/\/+$/, "");
528
+ }
529
+ function parseSignalBaseUrl(url) {
530
+ const parsed = new URL(normalizeBaseUrl(url));
531
+ if (parsed.username || parsed.password) throw new Error("Signal base URL must not include credentials");
532
+ return parsed;
533
+ }
534
+ function resolveSignalEndpointUrl(baseUrl, pathname) {
535
+ return new URL(pathname, parseSignalBaseUrl(baseUrl));
536
+ }
537
+ function parseSignalRpcResponse(text, status) {
538
+ let parsed;
539
+ try {
540
+ parsed = JSON.parse(text);
541
+ } catch (err) {
542
+ throw new Error(`Signal RPC returned malformed JSON (status ${status})`, { cause: err });
543
+ }
544
+ if (!parsed || typeof parsed !== "object") throw new Error(`Signal RPC returned invalid response envelope (status ${status})`);
545
+ const rpc = parsed;
546
+ const hasResult = Object.hasOwn(rpc, "result");
547
+ if (!rpc.error && !hasResult) throw new Error(`Signal RPC returned invalid response envelope (status ${status})`);
548
+ return rpc;
549
+ }
550
+ function assertSignalHttpProtocol(url, label) {
551
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`Signal ${label} unsupported protocol: ${url.protocol}`);
552
+ }
553
+ function normalizeSignalHttpResponseMaxBytes(value) {
554
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return DEFAULT_SIGNAL_HTTP_RESPONSE_MAX_BYTES;
555
+ return Math.floor(value);
556
+ }
557
+ function normalizeSignalSseTimeoutMs(timeoutMs) {
558
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return null;
559
+ return resolveTimerTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS$1);
560
+ }
561
+ function requestSignalHttpText(url, options) {
562
+ assertSignalHttpProtocol(url, "HTTP");
563
+ const timeoutMs = resolveTimerTimeoutMs(options.timeoutMs, DEFAULT_TIMEOUT_MS$1);
564
+ const client = url.protocol === "https:" ? https : http;
565
+ return new Promise((resolve, reject) => {
566
+ let settled = false;
567
+ const deadline = setTimeout(() => {
568
+ request?.destroy(/* @__PURE__ */ new Error(`Signal HTTP exceeded deadline after ${timeoutMs}ms`));
569
+ }, timeoutMs);
570
+ deadline.unref?.();
571
+ const cleanup = () => {
572
+ clearTimeout(deadline);
573
+ request?.setTimeout(0);
574
+ };
575
+ const rejectOnce = (error) => {
576
+ if (settled) return;
577
+ settled = true;
578
+ cleanup();
579
+ reject(toLintErrorObject(error, "Non-Error rejection"));
580
+ };
581
+ const resolveOnce = (response) => {
582
+ if (settled) return;
583
+ settled = true;
584
+ cleanup();
585
+ resolve(response);
586
+ };
587
+ const maxResponseBytes = normalizeSignalHttpResponseMaxBytes(options.maxResponseBytes);
588
+ const request = client.request(url, {
589
+ method: options.method,
590
+ headers: options.headers
591
+ }, (res) => {
592
+ const chunks = [];
593
+ let totalBytes = 0;
594
+ res.on("data", (chunk) => {
595
+ const next = typeof chunk === "string" ? Buffer$1.from(chunk) : chunk;
596
+ totalBytes += next.byteLength;
597
+ if (totalBytes > maxResponseBytes) {
598
+ const error = /* @__PURE__ */ new Error("Signal HTTP response exceeded size limit");
599
+ request?.destroy(error);
600
+ res.destroy(error);
601
+ rejectOnce(error);
602
+ return;
603
+ }
604
+ chunks.push(next);
605
+ });
606
+ res.on("error", rejectOnce);
607
+ res.on("end", () => {
608
+ resolveOnce({
609
+ status: res.statusCode ?? 0,
610
+ statusText: res.statusMessage || "error",
611
+ text: Buffer$1.concat(chunks).toString("utf8")
612
+ });
613
+ });
614
+ });
615
+ request.setTimeout(timeoutMs, () => {
616
+ request?.destroy(/* @__PURE__ */ new Error(`Signal HTTP timed out after ${timeoutMs}ms`));
617
+ });
618
+ request.on("error", rejectOnce);
619
+ if (options.body !== void 0) request.write(options.body);
620
+ request.end();
621
+ });
622
+ }
623
+ async function signalRpcRequest$1(method, params, opts) {
624
+ const id = generateSecureUuid();
625
+ const body = JSON.stringify({
626
+ jsonrpc: "2.0",
627
+ method,
628
+ params,
629
+ id
630
+ });
631
+ const res = await requestSignalHttpText(resolveSignalEndpointUrl(opts.baseUrl, "/api/v1/rpc"), {
632
+ method: "POST",
633
+ headers: {
634
+ "Content-Type": "application/json",
635
+ "Content-Length": String(Buffer$1.byteLength(body))
636
+ },
637
+ body,
638
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$1,
639
+ maxResponseBytes: opts.maxResponseBytes
640
+ });
641
+ if (res.status === 201) return;
642
+ if (!res.text) throw new Error(`Signal RPC empty response (status ${res.status})`);
643
+ const parsed = parseSignalRpcResponse(res.text, res.status);
644
+ if (parsed.error) {
645
+ const code = parsed.error.code ?? "unknown";
646
+ const msg = parsed.error.message ?? "Signal RPC error";
647
+ throw new Error(`Signal RPC ${code}: ${msg}`);
648
+ }
649
+ return parsed.result;
650
+ }
651
+ async function signalCheck$1(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
652
+ try {
653
+ const res = await requestSignalHttpText(resolveSignalEndpointUrl(baseUrl, "/api/v1/check"), {
654
+ method: "GET",
655
+ timeoutMs
656
+ });
657
+ if (res.status < 200 || res.status >= 300) return {
658
+ ok: false,
659
+ status: res.status,
660
+ error: `HTTP ${res.status}`
661
+ };
662
+ return {
663
+ ok: true,
664
+ status: res.status,
665
+ error: null
666
+ };
667
+ } catch (err) {
668
+ return {
669
+ ok: false,
670
+ status: null,
671
+ error: formatErrorMessage(err)
672
+ };
673
+ }
674
+ }
675
+ function openSignalEventStream(url, abortSignal, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
676
+ assertSignalHttpProtocol(url, "SSE");
677
+ if (abortSignal?.aborted) throw createSignalSseAbortError();
678
+ const client = url.protocol === "https:" ? https : http;
679
+ return new Promise((resolve, reject) => {
680
+ let settled = false;
681
+ let response;
682
+ let onAbort = () => {};
683
+ const effectiveTimeoutMs = normalizeSignalSseTimeoutMs(timeoutMs);
684
+ const headerDeadline = effectiveTimeoutMs === null ? void 0 : setTimeout(() => {
685
+ const error = /* @__PURE__ */ new Error(`Signal SSE connection timed out after ${effectiveTimeoutMs}ms`);
686
+ response?.destroy(error);
687
+ request.destroy(error);
688
+ rejectOnce(error);
689
+ }, effectiveTimeoutMs);
690
+ headerDeadline?.unref?.();
691
+ const cleanup = () => {
692
+ if (headerDeadline) clearTimeout(headerDeadline);
693
+ abortSignal?.removeEventListener("abort", onAbort);
694
+ };
695
+ const rejectOnce = (error) => {
696
+ if (settled) return;
697
+ settled = true;
698
+ cleanup();
699
+ reject(toLintErrorObject(error, "Non-Error rejection"));
700
+ };
701
+ const request = client.request(url, {
702
+ method: "GET",
703
+ headers: { Accept: "text/event-stream" }
704
+ }, (res) => {
705
+ const status = res.statusCode ?? 0;
706
+ if (status < 200 || status >= 300) {
707
+ res.resume();
708
+ rejectOnce(/* @__PURE__ */ new Error(`Signal SSE failed (${status} ${res.statusMessage || "error"})`));
709
+ return;
710
+ }
711
+ if (settled) {
712
+ res.destroy();
713
+ return;
714
+ }
715
+ if (headerDeadline) clearTimeout(headerDeadline);
716
+ settled = true;
717
+ response = res;
718
+ resolve({
719
+ response: res,
720
+ cleanup
721
+ });
722
+ });
723
+ onAbort = () => {
724
+ const error = createSignalSseAbortError();
725
+ response?.destroy(error);
726
+ request.destroy(error);
727
+ rejectOnce(error);
728
+ };
729
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
730
+ request.on("error", rejectOnce);
731
+ request.end();
732
+ });
733
+ }
734
+ async function streamSignalEvents$1(params) {
735
+ const url = resolveSignalEndpointUrl(params.baseUrl, "/api/v1/events");
736
+ if (params.account) url.searchParams.set("account", params.account);
737
+ const { response, cleanup } = await openSignalEventStream(url, params.abortSignal, params.timeoutMs ?? DEFAULT_TIMEOUT_MS$1);
738
+ const decoder = new TextDecoder();
739
+ let buffer = "";
740
+ let bufferedBytes = 0;
741
+ let currentEvent = {};
742
+ let currentEventDataBytes = 0;
743
+ const flushEvent = () => {
744
+ if (!currentEvent.data && !currentEvent.event && !currentEvent.id) return;
745
+ params.onEvent({
746
+ event: currentEvent.event,
747
+ data: currentEvent.data,
748
+ id: currentEvent.id
749
+ });
750
+ currentEvent = {};
751
+ currentEventDataBytes = 0;
752
+ };
753
+ const processLine = (line) => {
754
+ if (line === "") {
755
+ flushEvent();
756
+ return;
757
+ }
758
+ if (line.startsWith(":")) return;
759
+ const [rawField, ...rest] = line.split(":");
760
+ const field = rawField.trim();
761
+ const rawValue = rest.join(":");
762
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
763
+ if (field === "event") currentEvent.event = value;
764
+ else if (field === "data") {
765
+ const segment = currentEvent.data ? `\n${value}` : value;
766
+ currentEventDataBytes += Buffer$1.byteLength(segment, "utf8");
767
+ if (currentEventDataBytes > MAX_SIGNAL_SSE_EVENT_DATA_BYTES) throw new Error("Signal SSE event data exceeded size limit");
768
+ currentEvent.data = currentEvent.data ? `${currentEvent.data}${segment}` : segment;
769
+ } else if (field === "id") currentEvent.id = value;
770
+ };
771
+ const drainCompleteLines = () => {
772
+ let lineEnd = buffer.indexOf("\n");
773
+ while (lineEnd !== -1) {
774
+ let line = buffer.slice(0, lineEnd);
775
+ buffer = buffer.slice(lineEnd + 1);
776
+ if (line.endsWith("\r")) line = line.slice(0, -1);
777
+ processLine(line);
778
+ lineEnd = buffer.indexOf("\n");
779
+ }
780
+ bufferedBytes = Buffer$1.byteLength(buffer, "utf8");
781
+ };
782
+ try {
783
+ for await (const chunk of response) {
784
+ const value = typeof chunk === "string" ? Buffer$1.from(chunk) : chunk;
785
+ bufferedBytes += value.byteLength;
786
+ if (bufferedBytes > MAX_SIGNAL_SSE_BUFFER_BYTES) throw new Error("Signal SSE buffer exceeded size limit");
787
+ buffer += decoder.decode(value, { stream: true });
788
+ drainCompleteLines();
789
+ }
790
+ const tail = decoder.decode();
791
+ if (tail) {
792
+ buffer += tail;
793
+ bufferedBytes = Buffer$1.byteLength(buffer, "utf8");
794
+ }
795
+ if (bufferedBytes > MAX_SIGNAL_SSE_BUFFER_BYTES) throw new Error("Signal SSE buffer exceeded size limit");
796
+ drainCompleteLines();
797
+ } finally {
798
+ cleanup();
799
+ }
800
+ flushEvent();
801
+ }
802
+ function toLintErrorObject(value, fallbackMessage) {
803
+ if (value instanceof Error) return value;
804
+ if (typeof value === "string") return new Error(value);
805
+ const error = new Error(fallbackMessage, { cause: value });
806
+ if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
807
+ return error;
808
+ }
809
+ //#endregion
810
+ //#region extensions/signal/src/client-adapter.ts
811
+ /**
812
+ * Signal client adapter - unified interface for both native signal-cli and bbernhard container.
813
+ *
814
+ * This adapter provides a single API that routes to the appropriate implementation
815
+ * based on the configured API mode. Exports mirror client.ts names so consumers
816
+ * only need to change their import path.
817
+ */
818
+ const DEFAULT_TIMEOUT_MS = 1e4;
819
+ const MODE_CACHE_TTL_MS = 3e4;
820
+ const NATIVE_PREFERENCE_GRACE_MS = 50;
821
+ const detectedModeCache = /* @__PURE__ */ new Map();
822
+ function resolveConfiguredApiMode(configured) {
823
+ if (configured === "native" || configured === "container") return configured;
824
+ return "auto";
825
+ }
826
+ function formatErrorMessage$1(error) {
827
+ return error instanceof Error ? error.message : String(error);
828
+ }
829
+ function resolveAutoProbeTimeoutMs(timeoutMs) {
830
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
831
+ }
832
+ function waitForNativePreferenceGrace(nativeResultPromise) {
833
+ return new Promise((resolve) => {
834
+ const timer = setTimeout(() => resolve({ ok: false }), NATIVE_PREFERENCE_GRACE_MS);
835
+ timer.unref?.();
836
+ nativeResultPromise.then((result) => {
837
+ clearTimeout(timer);
838
+ resolve(result);
839
+ });
840
+ });
841
+ }
842
+ async function resolveAutoApiMode(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
843
+ const rawNow = Date.now();
844
+ const now = asDateTimestampMs(rawNow);
845
+ const cached = detectedModeCache.get(baseUrl);
846
+ if (cached) if (now !== void 0 && cached.expiresAt > now) {
847
+ if (cached.mode !== "container" || !options.requireContainerReceive || Boolean(options.account?.trim()) && cached.receiveAccount === options.account?.trim()) return cached.mode;
848
+ } else detectedModeCache.delete(baseUrl);
849
+ const detected = await detectSignalApiMode(baseUrl, timeoutMs, options);
850
+ const expiresAt = resolveExpiresAtMsFromDurationMs(MODE_CACHE_TTL_MS, { nowMs: rawNow });
851
+ if (expiresAt !== void 0) detectedModeCache.set(baseUrl, {
852
+ mode: detected,
853
+ expiresAt,
854
+ ...detected === "container" && options.requireContainerReceive && options.account ? { receiveAccount: options.account } : {}
855
+ });
856
+ return detected;
857
+ }
858
+ async function resolveApiModeForOperation(params) {
859
+ const configured = resolveConfiguredApiMode(params.apiMode);
860
+ if (configured === "native" || configured === "container") return configured;
861
+ return resolveAutoApiMode(params.baseUrl, params.timeoutMs ?? DEFAULT_TIMEOUT_MS, {
862
+ account: params.account,
863
+ requireContainerReceive: params.requireContainerReceive
864
+ });
865
+ }
866
+ /**
867
+ * Detect which Signal API mode is available by probing endpoints.
868
+ * Native wins when both APIs are healthy because it preserves the richer JSON-RPC contract.
869
+ */
870
+ async function detectSignalApiMode(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
871
+ const containerAccount = options.requireContainerReceive ? options.account?.trim() : void 0;
872
+ const nativeResultPromise = signalCheck$1(baseUrl, timeoutMs).catch(() => ({ ok: false }));
873
+ const containerResultPromise = containerAccount ? containerCheck(baseUrl, timeoutMs, containerAccount).catch(() => ({ ok: false })) : options.requireContainerReceive ? Promise.resolve({ ok: false }) : containerCheck(baseUrl, timeoutMs).catch(() => ({ ok: false }));
874
+ const nativeHealthyPromise = nativeResultPromise.then((result) => {
875
+ if (result.ok) return "native";
876
+ throw new Error("native not ok");
877
+ });
878
+ const containerHealthyPromise = containerResultPromise.then((result) => {
879
+ if (result.ok) return "container";
880
+ throw new Error("container not ok");
881
+ });
882
+ try {
883
+ if (await Promise.any([nativeHealthyPromise, containerHealthyPromise]) === "native") return "native";
884
+ return (await waitForNativePreferenceGrace(nativeResultPromise)).ok ? "native" : "container";
885
+ } catch {
886
+ throw new Error(`Signal API not reachable at ${baseUrl}`);
887
+ }
888
+ }
889
+ /**
890
+ * Drop-in replacement for native signalRpcRequest.
891
+ * Routes to native JSON-RPC or container REST based on config.
892
+ */
893
+ async function signalRpcRequest(method, params, opts) {
894
+ if (await resolveApiModeForOperation({
895
+ baseUrl: opts.baseUrl,
896
+ accountId: opts.accountId,
897
+ account: typeof params?.account === "string" ? params.account : void 0,
898
+ timeoutMs: opts.timeoutMs,
899
+ apiMode: opts.apiMode
900
+ }) === "native") return signalRpcRequest$1(method, params, opts);
901
+ return containerRpcRequest(method, params, opts);
902
+ }
903
+ /**
904
+ * Drop-in replacement for native signalCheck.
905
+ */
906
+ async function signalCheck(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
907
+ const configured = resolveConfiguredApiMode(options.apiMode);
908
+ const mode = configured === "auto" ? await resolveAutoApiMode(baseUrl, timeoutMs).catch((error) => {
909
+ return {
910
+ ok: false,
911
+ status: null,
912
+ error: formatErrorMessage$1(error)
913
+ };
914
+ }) : configured;
915
+ if (typeof mode !== "string") return mode;
916
+ if (mode === "container") return containerCheck(baseUrl, timeoutMs);
917
+ return signalCheck$1(baseUrl, timeoutMs);
918
+ }
919
+ /**
920
+ * Drop-in replacement for native streamSignalEvents.
921
+ * Container mode uses WebSocket; native uses SSE.
922
+ */
923
+ async function streamSignalEvents(params) {
924
+ if (await resolveApiModeForOperation({
925
+ baseUrl: params.baseUrl,
926
+ accountId: params.accountId,
927
+ account: params.account,
928
+ requireContainerReceive: true,
929
+ timeoutMs: resolveAutoProbeTimeoutMs(params.timeoutMs),
930
+ apiMode: params.apiMode
931
+ }) === "container") return streamContainerEvents({
932
+ baseUrl: params.baseUrl,
933
+ account: params.account,
934
+ abortSignal: params.abortSignal,
935
+ timeoutMs: params.timeoutMs,
936
+ onEvent: (event) => params.onEvent({
937
+ event: "receive",
938
+ data: JSON.stringify(event)
939
+ }),
940
+ logger: params.logger
941
+ });
942
+ return streamSignalEvents$1({
943
+ baseUrl: params.baseUrl,
944
+ account: params.account,
945
+ abortSignal: params.abortSignal,
946
+ timeoutMs: params.timeoutMs,
947
+ onEvent: (event) => params.onEvent(event)
948
+ });
949
+ }
950
+ //#endregion
951
+ //#region extensions/signal/src/rpc-context.ts
952
+ function resolveSignalRpcContext(opts, accountInfo) {
953
+ const hasBaseUrl = Boolean(normalizeOptionalString(opts.baseUrl));
954
+ const hasAccount = Boolean(normalizeOptionalString(opts.account));
955
+ if ((!hasBaseUrl || !hasAccount) && !accountInfo) throw new Error("Signal account config is required when baseUrl or account is missing");
956
+ const resolvedAccount = accountInfo;
957
+ const baseUrl = normalizeOptionalString(opts.baseUrl) ?? resolvedAccount?.baseUrl;
958
+ if (!baseUrl) throw new Error("Signal base URL is required");
959
+ return {
960
+ baseUrl,
961
+ account: normalizeOptionalString(opts.account) ?? normalizeOptionalString(resolvedAccount?.config.account)
962
+ };
963
+ }
964
+ //#endregion
965
+ //#region extensions/signal/src/send-reactions.ts
966
+ function normalizeSignalId(raw) {
967
+ const trimmed = raw.trim();
968
+ if (!trimmed) return "";
969
+ return trimmed.replace(/^signal:/i, "").trim();
970
+ }
971
+ function normalizeSignalUuid(raw) {
972
+ const trimmed = normalizeSignalId(raw);
973
+ if (!trimmed) return "";
974
+ if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("uuid:")) return trimmed.slice(5).trim();
975
+ return trimmed;
976
+ }
977
+ function resolveTargetAuthorParams(params) {
978
+ const candidates = [
979
+ params.targetAuthor,
980
+ params.targetAuthorUuid,
981
+ params.fallback
982
+ ];
983
+ for (const candidate of candidates) {
984
+ const raw = candidate?.trim();
985
+ if (!raw) continue;
986
+ const normalized = normalizeSignalUuid(raw);
987
+ if (normalized) return { targetAuthor: normalized };
988
+ }
989
+ return {};
990
+ }
991
+ async function sendReactionSignalCore(params) {
992
+ const cfg = requireRuntimeConfig(params.opts.cfg, "Signal reactions");
993
+ const apiMode = cfg.channels?.signal?.apiMode;
994
+ const accountInfo = resolveSignalAccount({
995
+ cfg,
996
+ accountId: params.opts.accountId
997
+ });
998
+ const { baseUrl, account } = resolveSignalRpcContext(params.opts, accountInfo);
999
+ const normalizedRecipient = normalizeSignalUuid(params.recipient);
1000
+ const groupId = params.opts.groupId?.trim();
1001
+ if (!normalizedRecipient && !groupId) throw new Error(params.errors.missingRecipient);
1002
+ if (!Number.isFinite(params.targetTimestamp) || params.targetTimestamp <= 0) throw new Error(params.errors.invalidTargetTimestamp);
1003
+ const normalizedEmoji = params.emoji?.trim();
1004
+ if (!normalizedEmoji) throw new Error(params.errors.missingEmoji);
1005
+ const targetAuthorParams = resolveTargetAuthorParams({
1006
+ targetAuthor: params.opts.targetAuthor,
1007
+ targetAuthorUuid: params.opts.targetAuthorUuid,
1008
+ fallback: normalizedRecipient
1009
+ });
1010
+ if (groupId && !targetAuthorParams.targetAuthor) throw new Error(params.errors.missingTargetAuthor);
1011
+ const requestParams = {
1012
+ emoji: normalizedEmoji,
1013
+ targetTimestamp: params.targetTimestamp,
1014
+ ...params.remove ? { remove: true } : {},
1015
+ ...targetAuthorParams
1016
+ };
1017
+ if (normalizedRecipient) requestParams.recipients = [normalizedRecipient];
1018
+ if (groupId) requestParams.groupIds = [groupId];
1019
+ if (account) requestParams.account = account;
1020
+ return {
1021
+ ok: true,
1022
+ timestamp: (await signalRpcRequest("sendReaction", requestParams, {
1023
+ baseUrl,
1024
+ timeoutMs: params.opts.timeoutMs,
1025
+ apiMode
1026
+ }))?.timestamp
1027
+ };
1028
+ }
1029
+ /**
1030
+ * Send a Signal reaction to a message
1031
+ * @param recipient - UUID or E.164 phone number of the message author
1032
+ * @param targetTimestamp - Message ID (timestamp) to react to
1033
+ * @param emoji - Emoji to react with
1034
+ * @param opts - Optional account/connection overrides
1035
+ */
1036
+ async function sendReactionSignal(recipient, targetTimestamp, emoji, opts) {
1037
+ return await sendReactionSignalCore({
1038
+ recipient,
1039
+ targetTimestamp,
1040
+ emoji,
1041
+ remove: false,
1042
+ opts,
1043
+ errors: {
1044
+ missingRecipient: "Recipient or groupId is required for Signal reaction",
1045
+ invalidTargetTimestamp: "Valid targetTimestamp is required for Signal reaction",
1046
+ missingEmoji: "Emoji is required for Signal reaction",
1047
+ missingTargetAuthor: "targetAuthor is required for group reactions"
1048
+ }
1049
+ });
1050
+ }
1051
+ /**
1052
+ * Remove a Signal reaction from a message
1053
+ * @param recipient - UUID or E.164 phone number of the message author
1054
+ * @param targetTimestamp - Message ID (timestamp) to remove reaction from
1055
+ * @param emoji - Emoji to remove
1056
+ * @param opts - Optional account/connection overrides
1057
+ */
1058
+ async function removeReactionSignal(recipient, targetTimestamp, emoji, opts) {
1059
+ return await sendReactionSignalCore({
1060
+ recipient,
1061
+ targetTimestamp,
1062
+ emoji,
1063
+ remove: true,
1064
+ opts,
1065
+ errors: {
1066
+ missingRecipient: "Recipient or groupId is required for Signal reaction removal",
1067
+ invalidTargetTimestamp: "Valid targetTimestamp is required for Signal reaction removal",
1068
+ missingEmoji: "Emoji is required for Signal reaction removal",
1069
+ missingTargetAuthor: "targetAuthor is required for group reaction removal"
1070
+ }
1071
+ });
1072
+ }
1073
+ //#endregion
1074
+ export { signalRpcRequest as a, signalCheck as i, sendReactionSignal as n, streamSignalEvents as o, resolveSignalRpcContext as r, removeReactionSignal as t };