@provos/ironcurtain 0.3.0 → 0.4.0

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 (108) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +20 -1
  3. package/dist/cli.js +15 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config/config-command.d.ts +1 -0
  6. package/dist/config/config-command.js +138 -34
  7. package/dist/config/config-command.js.map +1 -1
  8. package/dist/config/constitution-user-base.md +2 -0
  9. package/dist/config/first-start.js +31 -3
  10. package/dist/config/first-start.js.map +1 -1
  11. package/dist/config/generated/compiled-policy.json +158 -64
  12. package/dist/config/generated/test-scenarios.json +144 -293
  13. package/dist/config/generated/tool-annotations.json +17 -3
  14. package/dist/config/index.js +49 -17
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/paths.js +9 -4
  17. package/dist/config/paths.js.map +1 -1
  18. package/dist/config/user-config.d.ts +47 -0
  19. package/dist/config/user-config.js +97 -4
  20. package/dist/config/user-config.js.map +1 -1
  21. package/dist/docker/adapters/claude-code.js +14 -3
  22. package/dist/docker/adapters/claude-code.js.map +1 -1
  23. package/dist/docker/agent-adapter.d.ts +2 -0
  24. package/dist/docker/audit-log-tailer.d.ts +4 -3
  25. package/dist/docker/audit-log-tailer.js +9 -7
  26. package/dist/docker/audit-log-tailer.js.map +1 -1
  27. package/dist/docker/docker-agent-session.d.ts +11 -0
  28. package/dist/docker/docker-agent-session.js +103 -22
  29. package/dist/docker/docker-agent-session.js.map +1 -1
  30. package/dist/docker/docker-manager.js +82 -15
  31. package/dist/docker/docker-manager.js.map +1 -1
  32. package/dist/docker/managed-proxy.d.ts +7 -3
  33. package/dist/docker/managed-proxy.js +98 -8
  34. package/dist/docker/managed-proxy.js.map +1 -1
  35. package/dist/docker/mitm-proxy.d.ts +7 -4
  36. package/dist/docker/mitm-proxy.js +152 -62
  37. package/dist/docker/mitm-proxy.js.map +1 -1
  38. package/dist/docker/orientation.d.ts +1 -1
  39. package/dist/docker/orientation.js +5 -2
  40. package/dist/docker/orientation.js.map +1 -1
  41. package/dist/docker/platform.d.ts +20 -0
  42. package/dist/docker/platform.js +24 -0
  43. package/dist/docker/platform.js.map +1 -0
  44. package/dist/docker/provider-config.d.ts +38 -2
  45. package/dist/docker/provider-config.js +61 -0
  46. package/dist/docker/provider-config.js.map +1 -1
  47. package/dist/docker/types.d.ts +48 -6
  48. package/dist/pipeline/compile.js +2 -2
  49. package/dist/pipeline/compile.js.map +1 -1
  50. package/dist/pipeline/constitution-compiler.js +5 -2
  51. package/dist/pipeline/constitution-compiler.js.map +1 -1
  52. package/dist/pipeline/handwritten-scenarios.js +9 -10
  53. package/dist/pipeline/handwritten-scenarios.js.map +1 -1
  54. package/dist/pipeline/policy-verifier.d.ts +1 -1
  55. package/dist/pipeline/policy-verifier.js +7 -4
  56. package/dist/pipeline/policy-verifier.js.map +1 -1
  57. package/dist/pipeline/types.d.ts +7 -1
  58. package/dist/servers/fetch-server.d.ts +4 -3
  59. package/dist/servers/fetch-server.js +72 -10
  60. package/dist/servers/fetch-server.js.map +1 -1
  61. package/dist/servers/search-providers.d.ts +29 -0
  62. package/dist/servers/search-providers.js +156 -0
  63. package/dist/servers/search-providers.js.map +1 -0
  64. package/dist/session/index.js +16 -6
  65. package/dist/session/index.js.map +1 -1
  66. package/dist/signal/bot-command.d.ts +13 -0
  67. package/dist/signal/bot-command.js +54 -0
  68. package/dist/signal/bot-command.js.map +1 -0
  69. package/dist/signal/format.d.ts +31 -0
  70. package/dist/signal/format.js +101 -0
  71. package/dist/signal/format.js.map +1 -0
  72. package/dist/signal/index.d.ts +16 -0
  73. package/dist/signal/index.js +13 -0
  74. package/dist/signal/index.js.map +1 -0
  75. package/dist/signal/markdown-to-signal.d.ts +29 -0
  76. package/dist/signal/markdown-to-signal.js +154 -0
  77. package/dist/signal/markdown-to-signal.js.map +1 -0
  78. package/dist/signal/setup-signal.d.ts +48 -0
  79. package/dist/signal/setup-signal.js +443 -0
  80. package/dist/signal/setup-signal.js.map +1 -0
  81. package/dist/signal/signal-bot-daemon.d.ts +142 -0
  82. package/dist/signal/signal-bot-daemon.js +504 -0
  83. package/dist/signal/signal-bot-daemon.js.map +1 -0
  84. package/dist/signal/signal-config.d.ts +64 -0
  85. package/dist/signal/signal-config.js +53 -0
  86. package/dist/signal/signal-config.js.map +1 -0
  87. package/dist/signal/signal-container.d.ts +37 -0
  88. package/dist/signal/signal-container.js +112 -0
  89. package/dist/signal/signal-container.js.map +1 -0
  90. package/dist/signal/signal-transport.d.ts +34 -0
  91. package/dist/signal/signal-transport.js +70 -0
  92. package/dist/signal/signal-transport.js.map +1 -0
  93. package/dist/trusted-process/mcp-proxy-server.js +34 -3
  94. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  95. package/dist/trusted-process/path-utils.d.ts +5 -1
  96. package/dist/trusted-process/path-utils.js +47 -4
  97. package/dist/trusted-process/path-utils.js.map +1 -1
  98. package/dist/trusted-process/tcp-server-transport.d.ts +43 -0
  99. package/dist/trusted-process/tcp-server-transport.js +119 -0
  100. package/dist/trusted-process/tcp-server-transport.js.map +1 -0
  101. package/dist/types/argument-roles.js +20 -5
  102. package/dist/types/argument-roles.js.map +1 -1
  103. package/docker/Dockerfile.base.arm64 +23 -0
  104. package/package.json +6 -3
  105. package/src/config/constitution-user-base.md +2 -0
  106. package/src/config/generated/compiled-policy.json +158 -64
  107. package/src/config/generated/test-scenarios.json +144 -293
  108. package/src/config/generated/tool-annotations.json +17 -3
@@ -0,0 +1,504 @@
1
+ /**
2
+ * SignalBotDaemon - long-lived singleton managing the Signal transport.
3
+ *
4
+ * Responsibilities:
5
+ * - WebSocket connection to signal-cli-rest-api with reconnect + exponential backoff
6
+ * - Message routing: authorization, identity verification, escalation replies,
7
+ * control commands, and session forwarding
8
+ * - Session lifecycle: creates transport+session on demand, cleans up on end
9
+ * - Identity verification with TTL cache (5 min), fail-closed
10
+ * - Escalation state with race prevention
11
+ * - Message sending via POST /v2/send with text_mode: "styled"
12
+ */
13
+ import { createSession } from '../session/index.js';
14
+ import { loadConfig } from '../config/index.js';
15
+ import { loadUserConfig } from '../config/user-config.js';
16
+ import { resolveSignalConfig } from './signal-config.js';
17
+ import { SignalSessionTransport } from './signal-transport.js';
18
+ import { markdownToSignal } from './markdown-to-signal.js';
19
+ import { formatBudgetMessage, formatBudgetSummary, splitMessage, SIGNAL_MAX_MESSAGE_LENGTH } from './format.js';
20
+ import { BudgetExhaustedError } from '../session/errors.js';
21
+ import * as logger from '../logger.js';
22
+ /** How often to proactively verify the recipient's identity key (ms). */
23
+ const IDENTITY_CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
24
+ export class SignalBotDaemon {
25
+ config;
26
+ containerManager;
27
+ mode;
28
+ // WebSocket state
29
+ ws = null;
30
+ baseUrl = '';
31
+ closed = false;
32
+ reconnectAttempts = 0;
33
+ static MAX_RECONNECT_DELAY_MS = 30_000;
34
+ static BASE_RECONNECT_DELAY_MS = 1_000;
35
+ // Session state - at most one active session at a time
36
+ activeSession = null;
37
+ activeTransport = null;
38
+ messageInFlight = false;
39
+ // Escalation state - owned by the daemon because the daemon routes
40
+ // "approve"/"deny" messages before they reach the transport.
41
+ pendingEscalationId = null;
42
+ escalationResolving = false;
43
+ // Identity verification state
44
+ identityLocked = false;
45
+ lastIdentityCheckMs = 0;
46
+ // Message deduplication for drainMissedMessages()
47
+ recentTimestamps = new Set();
48
+ // Resolves when the daemon should exit (shutdown() called)
49
+ exitResolve = null;
50
+ constructor(options) {
51
+ this.config = options.config;
52
+ this.containerManager = options.containerManager;
53
+ this.mode = options.mode;
54
+ }
55
+ /**
56
+ * Starts the daemon. Returns a promise that resolves when
57
+ * shutdown() is called (e.g., SIGTERM/SIGINT).
58
+ */
59
+ async start() {
60
+ this.baseUrl = await this.containerManager.ensureRunning();
61
+ await this.containerManager.waitForHealthy(this.baseUrl);
62
+ await this.connectWebSocket();
63
+ await this.sendSignalMessage('IronCurtain bot is online. Send a message to begin.');
64
+ // Block until shutdown
65
+ await new Promise((resolve) => {
66
+ this.exitResolve = resolve;
67
+ });
68
+ }
69
+ /**
70
+ * Initiates graceful shutdown. Ends the active session,
71
+ * closes the WebSocket, and unblocks start().
72
+ */
73
+ async shutdown() {
74
+ logger.info('[Signal Daemon] Shutting down...');
75
+ this.closed = true;
76
+ await this.sendSignalMessage('IronCurtain bot is shutting down. Goodbye.').catch(() => { });
77
+ await this.endCurrentSession();
78
+ if (this.ws) {
79
+ this.ws.close();
80
+ this.ws = null;
81
+ }
82
+ this.exitResolve?.();
83
+ }
84
+ // --- WebSocket connection ---
85
+ async connectWebSocket() {
86
+ const wsUrl = `ws://127.0.0.1:${this.config.container.port}/v1/receive/${encodeURIComponent(this.config.botNumber)}`;
87
+ const isReconnect = this.reconnectAttempts > 0;
88
+ return new Promise((resolve, reject) => {
89
+ const ws = new WebSocket(wsUrl);
90
+ ws.addEventListener('open', () => {
91
+ this.reconnectAttempts = 0;
92
+ if (isReconnect) {
93
+ this.drainMissedMessages().catch((err) => {
94
+ logger.info(`[Signal Daemon] Failed to drain missed messages: ${String(err)}`);
95
+ });
96
+ }
97
+ resolve();
98
+ });
99
+ ws.addEventListener('message', (event) => {
100
+ this.handleIncomingMessage(String(event.data)).catch((err) => {
101
+ logger.error(`[Signal Daemon] Error handling message: ${String(err)}`);
102
+ });
103
+ });
104
+ ws.addEventListener('close', () => {
105
+ if (!this.closed) {
106
+ this.scheduleReconnect();
107
+ }
108
+ });
109
+ ws.addEventListener('error', () => {
110
+ if (this.reconnectAttempts === 0 && !this.ws) {
111
+ reject(new Error('WebSocket connection failed'));
112
+ }
113
+ });
114
+ this.ws = ws;
115
+ });
116
+ }
117
+ /**
118
+ * Polls GET /v1/receive/{number} to drain messages missed during
119
+ * a WebSocket disconnect. Uses timestamp-based deduplication to
120
+ * avoid processing messages that the WebSocket already delivered.
121
+ */
122
+ async drainMissedMessages() {
123
+ const url = `${this.baseUrl}/v1/receive/${encodeURIComponent(this.config.botNumber)}`;
124
+ try {
125
+ const resp = await fetch(url);
126
+ if (!resp.ok)
127
+ return;
128
+ const messages = (await resp.json());
129
+ for (const msg of messages) {
130
+ const data = JSON.stringify(msg);
131
+ await this.handleIncomingMessage(data);
132
+ }
133
+ }
134
+ catch {
135
+ // Best-effort - new messages will arrive via WS
136
+ }
137
+ }
138
+ scheduleReconnect() {
139
+ if (this.closed)
140
+ return;
141
+ this.reconnectAttempts++;
142
+ const delay = Math.min(SignalBotDaemon.BASE_RECONNECT_DELAY_MS * Math.pow(1.5, this.reconnectAttempts), SignalBotDaemon.MAX_RECONNECT_DELAY_MS);
143
+ setTimeout(() => {
144
+ if (!this.closed) {
145
+ this.connectWebSocket().catch(() => {
146
+ // Retry will be scheduled by the close handler
147
+ });
148
+ }
149
+ }, delay);
150
+ }
151
+ // --- Message routing ---
152
+ async handleIncomingMessage(data) {
153
+ const envelope = parseSignalEnvelope(data);
154
+ if (!envelope)
155
+ return;
156
+ // Authorization: only accept messages from the configured user
157
+ if (!isAuthorizedSender(envelope, this.config.recipientNumber))
158
+ return;
159
+ // Deduplication: skip messages we've already processed
160
+ const ts = envelope.dataMessage?.timestamp;
161
+ if (ts) {
162
+ if (this.recentTimestamps.has(ts))
163
+ return;
164
+ this.recentTimestamps.add(ts);
165
+ // Prune old timestamps (keep last 5 minutes)
166
+ if (this.recentTimestamps.size > 500) {
167
+ const cutoff = Date.now() - 5 * 60 * 1000;
168
+ for (const t of this.recentTimestamps) {
169
+ if (t < cutoff)
170
+ this.recentTimestamps.delete(t);
171
+ }
172
+ }
173
+ }
174
+ // Identity verification
175
+ if (await this.checkIdentityChanged(envelope))
176
+ return;
177
+ const text = envelope.dataMessage?.message;
178
+ if (!text)
179
+ return;
180
+ // Escalation replies: approve/deny (also accept /approve, /deny)
181
+ if (this.handleEscalationReply(text))
182
+ return;
183
+ // Control commands: /quit, /new, /budget, /help
184
+ if (this.handleControlCommand(text))
185
+ return;
186
+ // Regular message -> route to session
187
+ await this.routeToSession(text);
188
+ }
189
+ /**
190
+ * Routes a user message to the active session. Creates a new
191
+ * session if none exists. Handles BudgetExhaustedError by ending
192
+ * the exhausted session and notifying the user.
193
+ */
194
+ async routeToSession(text) {
195
+ if (this.messageInFlight) {
196
+ await this.sendSignalMessage('Still processing previous message, please wait...');
197
+ return;
198
+ }
199
+ // Create session on demand
200
+ if (!this.activeSession) {
201
+ try {
202
+ await this.startNewSession();
203
+ }
204
+ catch (err) {
205
+ const msg = err instanceof Error ? err.message : String(err);
206
+ await this.sendSignalMessage(`Failed to create session: ${msg}`);
207
+ return;
208
+ }
209
+ }
210
+ const session = this.activeSession;
211
+ if (!session)
212
+ return;
213
+ this.messageInFlight = true;
214
+ try {
215
+ const response = await session.sendMessage(text);
216
+ const styledText = markdownToSignal(response);
217
+ await this.sendSignalMessage(styledText);
218
+ }
219
+ catch (error) {
220
+ if (error instanceof BudgetExhaustedError) {
221
+ const status = session.getBudgetStatus();
222
+ await this.sendSignalMessage(`Session budget exhausted: ${error.message}\n` +
223
+ formatBudgetSummary(status) +
224
+ '\n' +
225
+ 'Send a new message to start a fresh session.');
226
+ await this.endCurrentSession();
227
+ }
228
+ else {
229
+ const message = error instanceof Error ? error.message : String(error);
230
+ await this.sendSignalMessage(`Error: ${message}`);
231
+ }
232
+ }
233
+ finally {
234
+ this.messageInFlight = false;
235
+ }
236
+ }
237
+ // --- Session lifecycle ---
238
+ /**
239
+ * Creates a new session with a fresh SignalSessionTransport.
240
+ * Follows the same pattern as index.ts: create transport,
241
+ * wire callbacks, create session, start transport.
242
+ */
243
+ async startNewSession() {
244
+ const transport = new SignalSessionTransport(this);
245
+ this.activeTransport = transport;
246
+ const config = loadConfig();
247
+ const session = await createSession({
248
+ config,
249
+ mode: this.mode,
250
+ onEscalation: transport.createEscalationHandler(),
251
+ onEscalationExpired: transport.createEscalationExpiredHandler(),
252
+ onDiagnostic: transport.createDiagnosticHandler(),
253
+ });
254
+ this.activeSession = session;
255
+ // Start the transport in the background. When it resolves
256
+ // (session closed/budget exhausted), clean up.
257
+ transport
258
+ .run(session)
259
+ .then(() => {
260
+ // Transport exited - session is done
261
+ if (this.activeTransport === transport) {
262
+ this.activeSession = null;
263
+ this.activeTransport = null;
264
+ }
265
+ })
266
+ .catch((err) => {
267
+ logger.error(`[Signal Daemon] Transport error: ${String(err)}`);
268
+ });
269
+ await this.sendSignalMessage('Started a new session.');
270
+ }
271
+ /**
272
+ * Ends the current session and cleans up. Awaits session.close()
273
+ * to ensure resources are released (sandbox, MCP connections, etc.).
274
+ * Guarded against re-entrance.
275
+ */
276
+ async endCurrentSession() {
277
+ if (!this.activeSession)
278
+ return;
279
+ const session = this.activeSession;
280
+ const transport = this.activeTransport;
281
+ this.activeSession = null;
282
+ this.activeTransport = null;
283
+ this.pendingEscalationId = null;
284
+ this.escalationResolving = false;
285
+ // Close transport first (resolves run() promise), then session
286
+ transport?.close();
287
+ await session.close();
288
+ }
289
+ // --- Escalation handling ---
290
+ /**
291
+ * Checks if the message is an escalation reply.
292
+ * Accepts: approve, deny, /approve, /deny (case-insensitive).
293
+ *
294
+ * Race condition prevention: escalationResolving flag prevents
295
+ * concurrent replies. pendingEscalationId is cleared in .finally()
296
+ * after async resolution completes, not before.
297
+ */
298
+ handleEscalationReply(text) {
299
+ if (!this.pendingEscalationId || !this.activeSession)
300
+ return false;
301
+ const normalized = text.trim().toLowerCase();
302
+ const isApprove = normalized === 'approve' || normalized === '/approve';
303
+ const isDeny = normalized === 'deny' || normalized === '/deny';
304
+ if (!isApprove && !isDeny)
305
+ return false;
306
+ if (this.escalationResolving) {
307
+ this.sendSignalMessage('Escalation is being resolved, please wait...').catch(() => { });
308
+ return true;
309
+ }
310
+ const decision = isApprove ? 'approved' : 'denied';
311
+ const escalationId = this.pendingEscalationId;
312
+ this.escalationResolving = true;
313
+ this.activeSession
314
+ .resolveEscalation(escalationId, decision)
315
+ .then(() => {
316
+ return this.sendSignalMessage(`Escalation ${decision}.`);
317
+ })
318
+ .catch((err) => {
319
+ const msg = err instanceof Error ? err.message : String(err);
320
+ this.sendSignalMessage(`Escalation error: ${msg}`).catch(() => { });
321
+ })
322
+ .finally(() => {
323
+ this.escalationResolving = false;
324
+ if (this.pendingEscalationId === escalationId) {
325
+ this.pendingEscalationId = null;
326
+ }
327
+ });
328
+ return true;
329
+ }
330
+ /** Called by SignalSessionTransport when the session surfaces an escalation. */
331
+ setPendingEscalation(escalationId) {
332
+ this.pendingEscalationId = escalationId;
333
+ }
334
+ /** Called by SignalSessionTransport when an escalation expires. */
335
+ clearPendingEscalation() {
336
+ this.pendingEscalationId = null;
337
+ }
338
+ // --- Control commands ---
339
+ handleControlCommand(text) {
340
+ const trimmed = text.trim().toLowerCase();
341
+ switch (trimmed) {
342
+ case '/quit':
343
+ case '/exit':
344
+ case '/new':
345
+ this.endCurrentSession()
346
+ .then(() => {
347
+ this.sendSignalMessage('Session ended. Send a message to start a new one.').catch(() => { });
348
+ })
349
+ .catch(() => { });
350
+ return true;
351
+ case '/budget': {
352
+ if (!this.activeSession) {
353
+ this.sendSignalMessage('No active session.').catch(() => { });
354
+ return true;
355
+ }
356
+ const status = this.activeSession.getBudgetStatus();
357
+ this.sendSignalMessage(formatBudgetMessage(status)).catch(() => { });
358
+ return true;
359
+ }
360
+ case '/help':
361
+ this.sendSignalMessage('Commands:\n' +
362
+ ' /quit or /new - end current session\n' +
363
+ ' /budget - show resource usage\n' +
364
+ ' /help - show this message\n' +
365
+ ' approve or /approve - approve pending escalation\n' +
366
+ ' deny or /deny - deny pending escalation').catch(() => { });
367
+ return true;
368
+ default:
369
+ return false;
370
+ }
371
+ }
372
+ // --- Identity verification ---
373
+ /**
374
+ * Checks whether the sender's Signal identity key has changed.
375
+ *
376
+ * Detection path 1: signal-cli flags the envelope with `untrustedIdentity`.
377
+ * Detection path 2: periodic proactive check via GET /v1/identities.
378
+ * Cached with IDENTITY_CHECK_INTERVAL_MS TTL to avoid HTTP overhead
379
+ * on every message.
380
+ *
381
+ * When locked, attempts self-healing by re-reading config from disk
382
+ * (handles the case where the user ran `--re-trust` externally).
383
+ *
384
+ * Returns true if the message should be rejected.
385
+ */
386
+ async checkIdentityChanged(envelope) {
387
+ // If locked, attempt self-healing via config reload
388
+ if (this.identityLocked) {
389
+ try {
390
+ const freshUserConfig = loadUserConfig();
391
+ const freshSignal = resolveSignalConfig(freshUserConfig);
392
+ if (freshSignal && freshSignal.recipientIdentityKey !== this.config.recipientIdentityKey) {
393
+ logger.info('[Signal Daemon] Detected new identity key on disk. Unlocking.');
394
+ this.config = freshSignal;
395
+ this.identityLocked = false;
396
+ }
397
+ }
398
+ catch {
399
+ logger.error('[Signal Daemon] Failed to reload config during lock check.');
400
+ }
401
+ if (this.identityLocked)
402
+ return true;
403
+ }
404
+ // Detection path 1: envelope flag (real-time)
405
+ if (envelope.untrustedIdentity) {
406
+ this.lockTransport('Identity key change detected via envelope flag.');
407
+ return true;
408
+ }
409
+ // Detection path 2: proactive API check (periodic, TTL-cached)
410
+ const now = Date.now();
411
+ if (now - this.lastIdentityCheckMs < IDENTITY_CHECK_INTERVAL_MS) {
412
+ return false; // Within TTL, skip API call
413
+ }
414
+ try {
415
+ // Use the all-identities endpoint and filter by recipient number.
416
+ // The per-recipient endpoint is not available in all versions.
417
+ const resp = await fetch(`${this.baseUrl}/v1/identities/${encodeURIComponent(this.config.botNumber)}`);
418
+ if (!resp.ok) {
419
+ // Fail closed: treat API errors as unverifiable.
420
+ // Don't update lastIdentityCheckMs so the next message retries.
421
+ logger.error(`[Signal Daemon] Identity check API returned ${resp.status}. ` +
422
+ 'Rejecting message (fail-closed). Check signal-cli container health.');
423
+ return true;
424
+ }
425
+ const identities = (await resp.json());
426
+ const current = identities.find((id) => id.number === this.config.recipientNumber);
427
+ // Normalize fingerprint (signal-cli may return space-separated hex)
428
+ const currentFp = current?.fingerprint.replace(/\s+/g, '');
429
+ if (currentFp && currentFp !== this.config.recipientIdentityKey) {
430
+ this.lockTransport(`Identity key mismatch.\n` +
431
+ ` Expected: ${this.config.recipientIdentityKey.substring(0, 20)}...\n` +
432
+ ` Received: ${currentFp.substring(0, 20)}...`);
433
+ return true;
434
+ }
435
+ // Only cache success - failures will retry on the next message
436
+ this.lastIdentityCheckMs = now;
437
+ }
438
+ catch {
439
+ // Fail closed: reject message when we cannot verify identity.
440
+ // Don't update lastIdentityCheckMs so the next message retries.
441
+ logger.error('[Signal Daemon] Identity check API unavailable. ' +
442
+ 'Rejecting message (fail-closed). Check signal-cli container health.');
443
+ return true;
444
+ }
445
+ return false;
446
+ }
447
+ lockTransport(reason) {
448
+ this.identityLocked = true;
449
+ logger.error(`[Signal Daemon] LOCKED: ${reason}\n` +
450
+ `The Signal identity key for ${this.config.recipientNumber} has changed.\n` +
451
+ `All messages are being rejected until re-trust is completed.\n` +
452
+ `Run: ironcurtain setup-signal --re-trust`);
453
+ }
454
+ // --- Message sending ---
455
+ async sendSignalMessage(text) {
456
+ const chunks = splitMessage(text, SIGNAL_MAX_MESSAGE_LENGTH);
457
+ for (const chunk of chunks) {
458
+ await this.postMessage(chunk);
459
+ }
460
+ }
461
+ async postMessage(text) {
462
+ const body = {
463
+ message: text,
464
+ number: this.config.botNumber,
465
+ recipients: [this.config.recipientNumber],
466
+ text_mode: 'styled',
467
+ };
468
+ const resp = await fetch(`${this.baseUrl}/v2/send`, {
469
+ method: 'POST',
470
+ headers: { 'Content-Type': 'application/json' },
471
+ body: JSON.stringify(body),
472
+ });
473
+ if (!resp.ok) {
474
+ const errBody = await resp.text();
475
+ throw new Error(`Signal send failed: ${resp.status} ${errBody}`);
476
+ }
477
+ }
478
+ }
479
+ // --- Envelope parsing and authorization ---
480
+ export function parseSignalEnvelope(data) {
481
+ try {
482
+ const parsed = JSON.parse(data);
483
+ if (typeof parsed !== 'object' || parsed === null)
484
+ return null;
485
+ // json-rpc mode wraps the envelope
486
+ const record = parsed;
487
+ const envelope = (record.envelope ?? parsed);
488
+ return envelope;
489
+ }
490
+ catch {
491
+ return null;
492
+ }
493
+ }
494
+ /** Normalize a phone number by stripping spaces. */
495
+ export function normalizePhoneNumber(number) {
496
+ return number.replace(/\s+/g, '');
497
+ }
498
+ export function isAuthorizedSender(envelope, recipientNumber) {
499
+ const sender = envelope.sourceNumber ?? envelope.source;
500
+ if (!sender)
501
+ return false;
502
+ return normalizePhoneNumber(sender) === normalizePhoneNumber(recipientNumber);
503
+ }
504
+ //# sourceMappingURL=signal-bot-daemon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal-bot-daemon.js","sourceRoot":"","sources":["../../src/signal/signal-bot-daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAA6B,MAAM,oBAAoB,CAAC;AAGpF,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,YAAY,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAChH,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AAEvC,yEAAyE;AACzE,MAAM,0BAA0B,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,YAAY;AAsC9D,MAAM,OAAO,eAAe;IAClB,MAAM,CAAuB;IACpB,gBAAgB,CAAyB;IACzC,IAAI,CAAc;IAEnC,kBAAkB;IACV,EAAE,GAAqB,IAAI,CAAC;IAC5B,OAAO,GAAW,EAAE,CAAC;IACrB,MAAM,GAAG,KAAK,CAAC;IACf,iBAAiB,GAAG,CAAC,CAAC;IACtB,MAAM,CAAU,sBAAsB,GAAG,MAAM,CAAC;IAChD,MAAM,CAAU,uBAAuB,GAAG,KAAK,CAAC;IAExD,uDAAuD;IAC/C,aAAa,GAAmB,IAAI,CAAC;IACrC,eAAe,GAAkC,IAAI,CAAC;IACtD,eAAe,GAAG,KAAK,CAAC;IAEhC,mEAAmE;IACnE,6DAA6D;IACrD,mBAAmB,GAAkB,IAAI,CAAC;IAC1C,mBAAmB,GAAG,KAAK,CAAC;IAEpC,8BAA8B;IACtB,cAAc,GAAG,KAAK,CAAC;IACvB,mBAAmB,GAAG,CAAC,CAAC;IAEhC,kDAAkD;IAC1C,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;IAE7C,2DAA2D;IACnD,WAAW,GAAwB,IAAI,CAAC;IAEhD,YAAY,OAA+B;QACzC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;QACjD,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,OAAO,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,CAAC;QAC3D,MAAM,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzD,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC9B,MAAM,IAAI,CAAC,iBAAiB,CAAC,qDAAqD,CAAC,CAAC;QAEpF,uBAAuB;QACvB,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC;QAC7B,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAQ;QACZ,MAAM,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QAChD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,MAAM,IAAI,CAAC,iBAAiB,CAAC,4CAA4C,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC3F,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;YACZ,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;IACvB,CAAC;IAED,+BAA+B;IAEvB,KAAK,CAAC,gBAAgB;QAC5B,MAAM,KAAK,GAAG,kBAAkB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,eAAe,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QACrH,MAAM,WAAW,GAAG,IAAI,CAAC,iBAAiB,GAAG,CAAC,CAAC;QAE/C,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3C,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC;YAEhC,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE;gBAC/B,IAAI,CAAC,iBAAiB,GAAG,CAAC,CAAC;gBAE3B,IAAI,WAAW,EAAE,CAAC;oBAChB,IAAI,CAAC,mBAAmB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;wBAChD,MAAM,CAAC,IAAI,CAAC,oDAAoD,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBACjF,CAAC,CAAC,CAAC;gBACL,CAAC;gBAED,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;gBACvC,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;oBACpE,MAAM,CAAC,KAAK,CAAC,2CAA2C,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACzE,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;gBAChC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;oBACjB,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBAC3B,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;gBAChC,IAAI,IAAI,CAAC,iBAAiB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;oBAC7C,MAAM,CAAC,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC,CAAC;gBACnD,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,mBAAmB;QAC/B,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,eAAe,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QACtF,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,EAAE;gBAAE,OAAO;YACrB,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAc,CAAC;YAClD,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;gBAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACjC,MAAM,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,gDAAgD;QAClD,CAAC;IACH,CAAC;IAEO,iBAAiB;QACvB,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAEzB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACpB,eAAe,CAAC,uBAAuB,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,iBAAiB,CAAC,EAC/E,eAAe,CAAC,sBAAsB,CACvC,CAAC;QAEF,UAAU,CAAC,GAAG,EAAE;YACd,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACjB,IAAI,CAAC,gBAAgB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;oBACjC,+CAA+C;gBACjD,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC;IAED,0BAA0B;IAElB,KAAK,CAAC,qBAAqB,CAAC,IAAY;QAC9C,MAAM,QAAQ,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,CAAC,QAAQ;YAAE,OAAO;QAEtB,+DAA+D;QAC/D,IAAI,CAAC,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;YAAE,OAAO;QAEvE,uDAAuD;QACvD,MAAM,EAAE,GAAG,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC;QAC3C,IAAI,EAAE,EAAE,CAAC;YACP,IAAI,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,OAAO;YAC1C,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC9B,6CAA6C;YAC7C,IAAI,IAAI,CAAC,gBAAgB,CAAC,IAAI,GAAG,GAAG,EAAE,CAAC;gBACrC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;gBAC1C,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBACtC,IAAI,CAAC,GAAG,MAAM;wBAAE,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAClD,CAAC;YACH,CAAC;QACH,CAAC;QAED,wBAAwB;QACxB,IAAI,MAAM,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC;YAAE,OAAO;QAEtD,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;QAC3C,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,iEAAiE;QACjE,IAAI,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC;YAAE,OAAO;QAE7C,gDAAgD;QAChD,IAAI,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC;YAAE,OAAO;QAE5C,sCAAsC;QACtC,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,cAAc,CAAC,IAAY;QACvC,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,MAAM,IAAI,CAAC,iBAAiB,CAAC,mDAAmD,CAAC,CAAC;YAClF,OAAO;QACT,CAAC;QAED,2BAA2B;QAC3B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;YAC/B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC7D,MAAM,IAAI,CAAC,iBAAiB,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAC;gBACjE,OAAO;YACT,CAAC;QACH,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC;QACnC,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACjD,MAAM,UAAU,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAC9C,MAAM,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,oBAAoB,EAAE,CAAC;gBAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;gBACzC,MAAM,IAAI,CAAC,iBAAiB,CAC1B,6BAA6B,KAAK,CAAC,OAAO,IAAI;oBAC5C,mBAAmB,CAAC,MAAM,CAAC;oBAC3B,IAAI;oBACJ,8CAA8C,CACjD,CAAC;gBACF,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACjC,CAAC;iBAAM,CAAC;gBACN,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACvE,MAAM,IAAI,CAAC,iBAAiB,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;YACpD,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,4BAA4B;IAE5B;;;;OAIG;IACK,KAAK,CAAC,eAAe;QAC3B,MAAM,SAAS,GAAG,IAAI,sBAAsB,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QAEjC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAE5B,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC;YAClC,MAAM;YACN,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,YAAY,EAAE,SAAS,CAAC,uBAAuB,EAAE;YACjD,mBAAmB,EAAE,SAAS,CAAC,8BAA8B,EAAE;YAC/D,YAAY,EAAE,SAAS,CAAC,uBAAuB,EAAE;SAClD,CAAC,CAAC;QAEH,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC;QAE7B,0DAA0D;QAC1D,+CAA+C;QAC/C,SAAS;aACN,GAAG,CAAC,OAAO,CAAC;aACZ,IAAI,CAAC,GAAG,EAAE;YACT,qCAAqC;YACrC,IAAI,IAAI,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;gBACvC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;gBAC1B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;YAC9B,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACtB,MAAM,CAAC,KAAK,CAAC,oCAAoC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC,CAAC,CAAC;QAEL,MAAM,IAAI,CAAC,iBAAiB,CAAC,wBAAwB,CAAC,CAAC;IACzD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,iBAAiB;QACrB,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,OAAO;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC;QACvC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC;QAChC,IAAI,CAAC,mBAAmB,GAAG,KAAK,CAAC;QAEjC,+DAA+D;QAC/D,SAAS,EAAE,KAAK,EAAE,CAAC;QACnB,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,8BAA8B;IAE9B;;;;;;;OAOG;IACK,qBAAqB,CAAC,IAAY;QACxC,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,OAAO,KAAK,CAAC;QAEnE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC7C,MAAM,SAAS,GAAG,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,UAAU,CAAC;QACxE,MAAM,MAAM,GAAG,UAAU,KAAK,MAAM,IAAI,UAAU,KAAK,OAAO,CAAC;QAC/D,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAExC,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC7B,IAAI,CAAC,iBAAiB,CAAC,8CAA8C,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACvF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAE,UAAoB,CAAC,CAAC,CAAE,QAAkB,CAAC;QACzE,MAAM,YAAY,GAAG,IAAI,CAAC,mBAAmB,CAAC;QAC9C,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC;QAEhC,IAAI,CAAC,aAAa;aACf,iBAAiB,CAAC,YAAY,EAAE,QAAQ,CAAC;aACzC,IAAI,CAAC,GAAG,EAAE;YACT,OAAO,IAAI,CAAC,iBAAiB,CAAC,cAAc,QAAQ,GAAG,CAAC,CAAC;QAC3D,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACtB,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,IAAI,CAAC,iBAAiB,CAAC,qBAAqB,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACrE,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,mBAAmB,GAAG,KAAK,CAAC;YACjC,IAAI,IAAI,CAAC,mBAAmB,KAAK,YAAY,EAAE,CAAC;gBAC9C,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC;YAClC,CAAC;QACH,CAAC,CAAC,CAAC;QAEL,OAAO,IAAI,CAAC;IACd,CAAC;IAED,gFAAgF;IAChF,oBAAoB,CAAC,YAAoB;QACvC,IAAI,CAAC,mBAAmB,GAAG,YAAY,CAAC;IAC1C,CAAC;IAED,mEAAmE;IACnE,sBAAsB;QACpB,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAClC,CAAC;IAED,2BAA2B;IAEnB,oBAAoB,CAAC,IAAY;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAE1C,QAAQ,OAAO,EAAE,CAAC;YAChB,KAAK,OAAO,CAAC;YACb,KAAK,OAAO,CAAC;YACb,KAAK,MAAM;gBACT,IAAI,CAAC,iBAAiB,EAAE;qBACrB,IAAI,CAAC,GAAG,EAAE;oBACT,IAAI,CAAC,iBAAiB,CAAC,mDAAmD,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBAC9F,CAAC,CAAC;qBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBACnB,OAAO,IAAI,CAAC;YAEd,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;oBACxB,IAAI,CAAC,iBAAiB,CAAC,oBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;oBAC7D,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,eAAe,EAAE,CAAC;gBACpD,IAAI,CAAC,iBAAiB,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBACpE,OAAO,IAAI,CAAC;YACd,CAAC;YAED,KAAK,OAAO;gBACV,IAAI,CAAC,iBAAiB,CACpB,aAAa;oBACX,yCAAyC;oBACzC,mCAAmC;oBACnC,+BAA+B;oBAC/B,sDAAsD;oBACtD,2CAA2C,CAC9C,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBAClB,OAAO,IAAI,CAAC;YAEd;gBACE,OAAO,KAAK,CAAC;QACjB,CAAC;IACH,CAAC;IAED,gCAAgC;IAEhC;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,oBAAoB,CAAC,QAAwB;QACjD,oDAAoD;QACpD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,MAAM,eAAe,GAAG,cAAc,EAAE,CAAC;gBACzC,MAAM,WAAW,GAAG,mBAAmB,CAAC,eAAe,CAAC,CAAC;gBACzD,IAAI,WAAW,IAAI,WAAW,CAAC,oBAAoB,KAAK,IAAI,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC;oBACzF,MAAM,CAAC,IAAI,CAAC,+DAA+D,CAAC,CAAC;oBAC7E,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC;oBAC1B,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;gBAC9B,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;YAC7E,CAAC;YACD,IAAI,IAAI,CAAC,cAAc;gBAAE,OAAO,IAAI,CAAC;QACvC,CAAC;QAED,8CAA8C;QAC9C,IAAI,QAAQ,CAAC,iBAAiB,EAAE,CAAC;YAC/B,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC,CAAC;YACtE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,+DAA+D;QAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,GAAG,GAAG,IAAI,CAAC,mBAAmB,GAAG,0BAA0B,EAAE,CAAC;YAChE,OAAO,KAAK,CAAC,CAAC,4BAA4B;QAC5C,CAAC;QAED,IAAI,CAAC;YACH,kEAAkE;YAClE,+DAA+D;YAC/D,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,kBAAkB,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACvG,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;gBACb,iDAAiD;gBACjD,gEAAgE;gBAChE,MAAM,CAAC,KAAK,CACV,+CAA+C,IAAI,CAAC,MAAM,IAAI;oBAC5D,qEAAqE,CACxE,CAAC;gBACF,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,UAAU,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAmD,CAAC;YACzF,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;YACnF,oEAAoE;YACpE,MAAM,SAAS,GAAG,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAC3D,IAAI,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC;gBAChE,IAAI,CAAC,aAAa,CAChB,0BAA0B;oBACxB,eAAe,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO;oBACvE,eAAe,SAAS,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CACjD,CAAC;gBACF,OAAO,IAAI,CAAC;YACd,CAAC;YACD,+DAA+D;YAC/D,IAAI,CAAC,mBAAmB,GAAG,GAAG,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,8DAA8D;YAC9D,gEAAgE;YAChE,MAAM,CAAC,KAAK,CACV,kDAAkD;gBAChD,qEAAqE,CACxE,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,aAAa,CAAC,MAAc;QAClC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,MAAM,CAAC,KAAK,CACV,2BAA2B,MAAM,IAAI;YACnC,+BAA+B,IAAI,CAAC,MAAM,CAAC,eAAe,iBAAiB;YAC3E,gEAAgE;YAChE,0CAA0C,CAC7C,CAAC;IACJ,CAAC;IAED,0BAA0B;IAE1B,KAAK,CAAC,iBAAiB,CAAC,IAAY;QAClC,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,yBAAyB,CAAC,CAAC;QAC7D,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,IAAY;QACpC,MAAM,IAAI,GAA4B;YACpC,OAAO,EAAE,IAAI;YACb,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;YAC7B,UAAU,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;YACzC,SAAS,EAAE,QAAQ;SACpB,CAAC;QAEF,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,UAAU,EAAE;YAClD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;;AAGH,6CAA6C;AAE7C,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC/D,mCAAmC;QACnC,MAAM,MAAM,GAAG,MAAiC,CAAC;QACjD,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAmB,CAAC;QAC/D,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAwB,EAAE,eAAuB;IAClF,MAAM,MAAM,GAAG,QAAQ,CAAC,YAAY,IAAI,QAAQ,CAAC,MAAM,CAAC;IACxD,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,OAAO,oBAAoB,CAAC,MAAM,CAAC,KAAK,oBAAoB,CAAC,eAAe,CAAC,CAAC;AAChF,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Signal transport configuration types and resolution.
3
+ *
4
+ * SignalConfig is the raw shape stored in config.json (all fields optional).
5
+ * ResolvedSignalConfig has all fields present with defaults applied.
6
+ * resolveSignalConfig() bridges the two, returning null when Signal
7
+ * is not configured (missing required fields).
8
+ */
9
+ /** Signal transport configuration as stored in the config file. */
10
+ export interface SignalConfig {
11
+ /** The bot's registered Signal phone number (e.g., '+15551234567'). */
12
+ botNumber?: string;
13
+ /** The user's Signal phone number to send messages to. */
14
+ recipientNumber?: string;
15
+ /**
16
+ * The user's Signal identity key fingerprint, captured during onboarding.
17
+ * Used to detect identity changes (SIM swap, number reassignment).
18
+ */
19
+ recipientIdentityKey?: string;
20
+ /** Container configuration overrides. */
21
+ container?: {
22
+ /** Docker image. Default: 'bbernhard/signal-cli-rest-api:latest' */
23
+ image?: string;
24
+ /** Host port for REST API. Default: 18080 */
25
+ port?: number;
26
+ };
27
+ }
28
+ /** Resolved Signal config with all fields present. */
29
+ export interface ResolvedSignalConfig {
30
+ readonly botNumber: string;
31
+ readonly recipientNumber: string;
32
+ readonly recipientIdentityKey: string;
33
+ readonly container: {
34
+ readonly image: string;
35
+ readonly port: number;
36
+ readonly dataDir: string;
37
+ readonly containerName: string;
38
+ };
39
+ }
40
+ export declare const SIGNAL_DEFAULTS: {
41
+ readonly image: "bbernhard/signal-cli-rest-api:latest";
42
+ readonly port: 18080;
43
+ readonly containerName: "ironcurtain-signal";
44
+ };
45
+ /** Returns the host directory for signal-cli persistent data. */
46
+ export declare function getSignalDataDir(): string;
47
+ /**
48
+ * Minimal shape needed to resolve Signal config.
49
+ * Accepts both UserConfig (partial signal) and ResolvedUserConfig
50
+ * (where signal is ResolvedSignalConfig | null).
51
+ */
52
+ interface HasSignalConfig {
53
+ signal?: SignalConfig | ResolvedSignalConfig | null | undefined;
54
+ }
55
+ /**
56
+ * Resolves Signal config from user config, applying defaults.
57
+ * Returns null when Signal is not configured (missing required fields).
58
+ *
59
+ * Accepts both raw UserConfig (from Zod parse) and ResolvedUserConfig
60
+ * (from loadUserConfig). When the input is already resolved, returns
61
+ * it directly.
62
+ */
63
+ export declare function resolveSignalConfig(config: HasSignalConfig): ResolvedSignalConfig | null;
64
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Signal transport configuration types and resolution.
3
+ *
4
+ * SignalConfig is the raw shape stored in config.json (all fields optional).
5
+ * ResolvedSignalConfig has all fields present with defaults applied.
6
+ * resolveSignalConfig() bridges the two, returning null when Signal
7
+ * is not configured (missing required fields).
8
+ */
9
+ import { resolve } from 'node:path';
10
+ import { getIronCurtainHome } from '../config/paths.js';
11
+ export const SIGNAL_DEFAULTS = {
12
+ image: 'bbernhard/signal-cli-rest-api:latest',
13
+ port: 18080,
14
+ containerName: 'ironcurtain-signal',
15
+ };
16
+ /** Returns the host directory for signal-cli persistent data. */
17
+ export function getSignalDataDir() {
18
+ return resolve(getIronCurtainHome(), 'signal-data');
19
+ }
20
+ /**
21
+ * Resolves Signal config from user config, applying defaults.
22
+ * Returns null when Signal is not configured (missing required fields).
23
+ *
24
+ * Accepts both raw UserConfig (from Zod parse) and ResolvedUserConfig
25
+ * (from loadUserConfig). When the input is already resolved, returns
26
+ * it directly.
27
+ */
28
+ export function resolveSignalConfig(config) {
29
+ const signal = config.signal;
30
+ if (!signal)
31
+ return null;
32
+ // If already resolved (has container.dataDir), return as-is
33
+ if ('container' in signal && signal.container && 'dataDir' in signal.container) {
34
+ return signal;
35
+ }
36
+ // Raw config - resolve with defaults
37
+ const raw = signal;
38
+ if (!raw.botNumber || !raw.recipientNumber || !raw.recipientIdentityKey) {
39
+ return null;
40
+ }
41
+ return {
42
+ botNumber: raw.botNumber,
43
+ recipientNumber: raw.recipientNumber,
44
+ recipientIdentityKey: raw.recipientIdentityKey,
45
+ container: {
46
+ image: raw.container?.image ?? SIGNAL_DEFAULTS.image,
47
+ port: raw.container?.port ?? SIGNAL_DEFAULTS.port,
48
+ dataDir: getSignalDataDir(),
49
+ containerName: SIGNAL_DEFAULTS.containerName,
50
+ },
51
+ };
52
+ }
53
+ //# sourceMappingURL=signal-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal-config.js","sourceRoot":"","sources":["../../src/signal/signal-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAkCxD,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,KAAK,EAAE,sCAAsC;IAC7C,IAAI,EAAE,KAAK;IACX,aAAa,EAAE,oBAAoB;CAC3B,CAAC;AAEX,iEAAiE;AACjE,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,CAAC,kBAAkB,EAAE,EAAE,aAAa,CAAC,CAAC;AACtD,CAAC;AAWD;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAuB;IACzD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,4DAA4D;IAC5D,IAAI,WAAW,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,IAAI,SAAS,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QAC/E,OAAO,MAA8B,CAAC;IACxC,CAAC;IAED,qCAAqC;IACrC,MAAM,GAAG,GAAG,MAAsB,CAAC;IACnC,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,GAAG,CAAC,oBAAoB,EAAE,CAAC;QACxE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,oBAAoB,EAAE,GAAG,CAAC,oBAAoB;QAC9C,SAAS,EAAE;YACT,KAAK,EAAE,GAAG,CAAC,SAAS,EAAE,KAAK,IAAI,eAAe,CAAC,KAAK;YACpD,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,IAAI,IAAI,eAAe,CAAC,IAAI;YACjD,OAAO,EAAE,gBAAgB,EAAE;YAC3B,aAAa,EAAE,eAAe,CAAC,aAAa;SAC7C;KACF,CAAC;AACJ,CAAC"}