@provos/ironcurtain 0.3.1 → 0.4.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.
Files changed (101) hide show
  1. package/README.md +20 -1
  2. package/dist/cli.js +15 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.js +12 -1
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/config/mcp-servers.json +3 -0
  7. package/dist/config/types.d.ts +2 -0
  8. package/dist/config/user-config.d.ts +11 -0
  9. package/dist/config/user-config.js +56 -2
  10. package/dist/config/user-config.js.map +1 -1
  11. package/dist/docker/adapters/claude-code.d.ts +4 -0
  12. package/dist/docker/adapters/claude-code.js +35 -21
  13. package/dist/docker/adapters/claude-code.js.map +1 -1
  14. package/dist/docker/agent-adapter.d.ts +6 -12
  15. package/dist/docker/audit-log-tailer.d.ts +4 -3
  16. package/dist/docker/audit-log-tailer.js +9 -7
  17. package/dist/docker/audit-log-tailer.js.map +1 -1
  18. package/dist/docker/code-mode-proxy.d.ts +34 -0
  19. package/dist/docker/code-mode-proxy.js +137 -0
  20. package/dist/docker/code-mode-proxy.js.map +1 -0
  21. package/dist/docker/docker-agent-session.d.ts +17 -5
  22. package/dist/docker/docker-agent-session.js +171 -36
  23. package/dist/docker/docker-agent-session.js.map +1 -1
  24. package/dist/docker/docker-manager.js +106 -15
  25. package/dist/docker/docker-manager.js.map +1 -1
  26. package/dist/docker/mitm-proxy.d.ts +7 -4
  27. package/dist/docker/mitm-proxy.js +43 -13
  28. package/dist/docker/mitm-proxy.js.map +1 -1
  29. package/dist/docker/orientation.d.ts +3 -2
  30. package/dist/docker/orientation.js +6 -3
  31. package/dist/docker/orientation.js.map +1 -1
  32. package/dist/docker/platform.d.ts +20 -0
  33. package/dist/docker/platform.js +24 -0
  34. package/dist/docker/platform.js.map +1 -0
  35. package/dist/docker/types.d.ts +59 -7
  36. package/dist/sandbox/index.d.ts +25 -1
  37. package/dist/sandbox/index.js +78 -9
  38. package/dist/sandbox/index.js.map +1 -1
  39. package/dist/servers/fetch-server.d.ts +6 -1
  40. package/dist/servers/fetch-server.js +24 -12
  41. package/dist/servers/fetch-server.js.map +1 -1
  42. package/dist/session/agent-session.js +9 -5
  43. package/dist/session/agent-session.js.map +1 -1
  44. package/dist/session/index.js +18 -36
  45. package/dist/session/index.js.map +1 -1
  46. package/dist/session/preflight.js +6 -0
  47. package/dist/session/preflight.js.map +1 -1
  48. package/dist/session/prompts.d.ts +9 -3
  49. package/dist/session/prompts.js +28 -10
  50. package/dist/session/prompts.js.map +1 -1
  51. package/dist/signal/bot-command.d.ts +13 -0
  52. package/dist/signal/bot-command.js +54 -0
  53. package/dist/signal/bot-command.js.map +1 -0
  54. package/dist/signal/format.d.ts +31 -0
  55. package/dist/signal/format.js +101 -0
  56. package/dist/signal/format.js.map +1 -0
  57. package/dist/signal/index.d.ts +16 -0
  58. package/dist/signal/index.js +13 -0
  59. package/dist/signal/index.js.map +1 -0
  60. package/dist/signal/markdown-to-signal.d.ts +29 -0
  61. package/dist/signal/markdown-to-signal.js +154 -0
  62. package/dist/signal/markdown-to-signal.js.map +1 -0
  63. package/dist/signal/setup-signal.d.ts +48 -0
  64. package/dist/signal/setup-signal.js +443 -0
  65. package/dist/signal/setup-signal.js.map +1 -0
  66. package/dist/signal/signal-bot-daemon.d.ts +142 -0
  67. package/dist/signal/signal-bot-daemon.js +504 -0
  68. package/dist/signal/signal-bot-daemon.js.map +1 -0
  69. package/dist/signal/signal-config.d.ts +64 -0
  70. package/dist/signal/signal-config.js +53 -0
  71. package/dist/signal/signal-config.js.map +1 -0
  72. package/dist/signal/signal-container.d.ts +37 -0
  73. package/dist/signal/signal-container.js +112 -0
  74. package/dist/signal/signal-container.js.map +1 -0
  75. package/dist/signal/signal-transport.d.ts +34 -0
  76. package/dist/signal/signal-transport.js +70 -0
  77. package/dist/signal/signal-transport.js.map +1 -0
  78. package/dist/trusted-process/mcp-proxy-server.d.ts +111 -1
  79. package/dist/trusted-process/mcp-proxy-server.js +319 -247
  80. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  81. package/dist/trusted-process/path-utils.d.ts +5 -1
  82. package/dist/trusted-process/path-utils.js +47 -4
  83. package/dist/trusted-process/path-utils.js.map +1 -1
  84. package/dist/trusted-process/policy-engine.d.ts +3 -1
  85. package/dist/trusted-process/policy-engine.js +17 -6
  86. package/dist/trusted-process/policy-engine.js.map +1 -1
  87. package/dist/trusted-process/sandbox-integration.d.ts +11 -4
  88. package/dist/trusted-process/sandbox-integration.js +13 -7
  89. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  90. package/dist/trusted-process/tcp-server-transport.d.ts +43 -0
  91. package/dist/trusted-process/tcp-server-transport.js +119 -0
  92. package/dist/trusted-process/tcp-server-transport.js.map +1 -0
  93. package/dist/types/argument-roles.d.ts +7 -0
  94. package/dist/types/argument-roles.js +30 -5
  95. package/dist/types/argument-roles.js.map +1 -1
  96. package/docker/Dockerfile.base.arm64 +23 -0
  97. package/package.json +9 -4
  98. package/src/config/mcp-servers.json +3 -0
  99. package/dist/docker/managed-proxy.d.ts +0 -27
  100. package/dist/docker/managed-proxy.js +0 -175
  101. package/dist/docker/managed-proxy.js.map +0 -1
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Interactive Signal onboarding wizard.
3
+ *
4
+ * Implements `ironcurtain setup-signal` with two modes:
5
+ * - Full setup: Docker check, container start, registration,
6
+ * recipient identity verification, and config save.
7
+ * - Re-trust (`--re-trust`): Re-verify a changed identity key without
8
+ * repeating the full registration flow.
9
+ *
10
+ * Uses @clack/prompts for consistent terminal UI with other IronCurtain
11
+ * interactive commands (config, first-start).
12
+ */
13
+ /** Validates a phone number in E.164 format. Returns error string or undefined. */
14
+ export declare function validatePhoneNumber(value: string | undefined): string | undefined;
15
+ /**
16
+ * Registers a new phone number with Signal via the signal-cli REST API.
17
+ * Handles the captcha and SMS verification code flow.
18
+ */
19
+ export declare function registerNewNumber(baseUrl: string): Promise<string>;
20
+ /**
21
+ * Sends a random challenge code to the recipient via Signal,
22
+ * waits for the user to type it into the terminal, then
23
+ * captures and returns the recipient's identity key.
24
+ *
25
+ * The identity key comes from signal-cli's identity endpoint
26
+ * after a successful message exchange has occurred.
27
+ */
28
+ export declare function verifyRecipientIdentity(baseUrl: string, botNumber: string, recipientNumber: string): Promise<string>;
29
+ /**
30
+ * Re-verifies a changed identity key without repeating full registration.
31
+ * Used when the transport detects an identity key change at runtime.
32
+ */
33
+ export declare function runReTrust(): Promise<void>;
34
+ /**
35
+ * Runs the interactive Signal setup wizard.
36
+ *
37
+ * Steps:
38
+ * 1. Validate Docker availability
39
+ * 2. Pull and start signal-cli container
40
+ * 3. Register new number (or reuse existing account)
41
+ * 4. Configure recipient number
42
+ * 5. Challenge-response identity verification
43
+ * 6. Capture and store identity key
44
+ * 7. Save config
45
+ */
46
+ export declare function runSignalSetup(options?: {
47
+ reTrust?: boolean;
48
+ }): Promise<void>;
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Interactive Signal onboarding wizard.
3
+ *
4
+ * Implements `ironcurtain setup-signal` with two modes:
5
+ * - Full setup: Docker check, container start, registration,
6
+ * recipient identity verification, and config save.
7
+ * - Re-trust (`--re-trust`): Re-verify a changed identity key without
8
+ * repeating the full registration flow.
9
+ *
10
+ * Uses @clack/prompts for consistent terminal UI with other IronCurtain
11
+ * interactive commands (config, first-start).
12
+ */
13
+ import { randomInt } from 'node:crypto';
14
+ import { exec as execCb } from 'node:child_process';
15
+ import { readFileSync } from 'node:fs';
16
+ import { resolve } from 'node:path';
17
+ import { promisify } from 'node:util';
18
+ import * as p from '@clack/prompts';
19
+ import { createDockerManager } from '../docker/docker-manager.js';
20
+ import { saveUserConfig, loadUserConfig } from '../config/user-config.js';
21
+ import { createSignalContainerManager, } from './signal-container.js';
22
+ import { SIGNAL_DEFAULTS, resolveSignalConfig, getSignalDataDir } from './signal-config.js';
23
+ const execAsync = promisify(execCb);
24
+ // ---- Cancel handling ------------------------------------------------
25
+ /** Checks if a prompt result was cancelled and exits cleanly. */
26
+ function handleCancel(value) {
27
+ if (p.isCancel(value)) {
28
+ p.cancel('Setup cancelled.');
29
+ process.exit(0);
30
+ }
31
+ }
32
+ // ---- Phone number validation ----------------------------------------
33
+ const E164_REGEX = /^\+\d{7,15}$/;
34
+ /** Validates a phone number in E.164 format. Returns error string or undefined. */
35
+ export function validatePhoneNumber(value) {
36
+ if (!value)
37
+ return 'Phone number is required';
38
+ if (!E164_REGEX.test(value)) {
39
+ return 'Must be E.164 format: +<country code><number> (e.g., +15551234567)';
40
+ }
41
+ return undefined;
42
+ }
43
+ // ---- Docker validation ----------------------------------------------
44
+ async function validateDocker(docker) {
45
+ const spinner = p.spinner();
46
+ spinner.start('Checking Docker...');
47
+ try {
48
+ await docker.preflight(SIGNAL_DEFAULTS.image);
49
+ spinner.stop('Docker is available');
50
+ }
51
+ catch {
52
+ // preflight checks both docker info and image -- we only need docker info here
53
+ try {
54
+ // Check if Docker daemon is reachable at all
55
+ await execAsync('docker info', { timeout: 10_000 });
56
+ spinner.stop('Docker is available');
57
+ }
58
+ catch {
59
+ spinner.stop('Docker is not available');
60
+ p.log.error('Docker is not available. Start Docker and try again.\n\n' +
61
+ 'Install Docker: https://docs.docker.com/get-docker/');
62
+ process.exit(1);
63
+ }
64
+ }
65
+ }
66
+ // ---- Container setup ------------------------------------------------
67
+ function resolveContainerConfig() {
68
+ return {
69
+ image: SIGNAL_DEFAULTS.image,
70
+ port: SIGNAL_DEFAULTS.port,
71
+ dataDir: getSignalDataDir(),
72
+ containerName: SIGNAL_DEFAULTS.containerName,
73
+ };
74
+ }
75
+ async function startContainer(manager, containerConfig) {
76
+ const startSpinner = p.spinner();
77
+ startSpinner.start('Starting signal-cli container (pulling image if needed)...');
78
+ try {
79
+ const baseUrl = await manager.ensureRunning();
80
+ startSpinner.stop(`Container started on port ${containerConfig.port}`);
81
+ const healthSpinner = p.spinner();
82
+ healthSpinner.start('Waiting for signal-cli to become healthy...');
83
+ try {
84
+ await manager.waitForHealthy(baseUrl);
85
+ healthSpinner.stop('Health check passed');
86
+ }
87
+ catch (err) {
88
+ healthSpinner.stop('Health check failed');
89
+ p.log.error(`signal-cli container did not become healthy: ${err instanceof Error ? err.message : String(err)}`);
90
+ process.exit(1);
91
+ }
92
+ return baseUrl;
93
+ }
94
+ catch (err) {
95
+ startSpinner.stop('Container start failed');
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ if (message.includes('port') && message.includes('already')) {
98
+ p.log.error(`Port ${containerConfig.port} is already in use.\n` +
99
+ 'Either stop the conflicting service or configure a different port\n' +
100
+ 'in ~/.ironcurtain/config.json under signal.container.port.');
101
+ }
102
+ else {
103
+ p.log.error(`Failed to start container: ${message}`);
104
+ }
105
+ process.exit(1);
106
+ }
107
+ }
108
+ // ---- Account discovery -----------------------------------------------
109
+ async function fetchAccounts(baseUrl) {
110
+ try {
111
+ const resp = await fetch(`${baseUrl}/v1/accounts`);
112
+ if (!resp.ok)
113
+ return [];
114
+ const accounts = (await resp.json());
115
+ if (accounts.length > 0)
116
+ return accounts;
117
+ }
118
+ catch {
119
+ // Fall through to local check
120
+ }
121
+ // Fallback: in json-rpc mode, signal-cli loads accounts at startup.
122
+ // If registration happened after the container started, the REST API
123
+ // won't know about the account until a restart. Check the host data
124
+ // directory directly.
125
+ return readAccountsFromDataDir();
126
+ }
127
+ /**
128
+ * Reads accounts directly from signal-cli's accounts.json on the host.
129
+ * This catches accounts the REST API doesn't see (json-rpc mode startup race).
130
+ */
131
+ function readAccountsFromDataDir() {
132
+ try {
133
+ const accountsPath = resolve(getSignalDataDir(), 'data', 'accounts.json');
134
+ const data = JSON.parse(readFileSync(accountsPath, 'utf-8'));
135
+ return (data.accounts ?? []).map((a) => a.number).filter((n) => !!n);
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
141
+ // ---- Registration flow ----------------------------------------------
142
+ /**
143
+ * Registers a new phone number with Signal via the signal-cli REST API.
144
+ * Handles the captcha and SMS verification code flow.
145
+ */
146
+ export async function registerNewNumber(baseUrl) {
147
+ const phoneNumber = await p.text({
148
+ message: 'Enter the phone number to register (with country code):',
149
+ placeholder: '+15551234567',
150
+ validate: validatePhoneNumber,
151
+ });
152
+ handleCancel(phoneNumber);
153
+ // Direct the user to the captcha page
154
+ const captchaUrl = 'https://signalcaptchas.org/registration/generate.html';
155
+ p.log.info('To complete registration, Signal requires a captcha.\n' +
156
+ 'Open this URL in your browser:\n\n' +
157
+ ` ${captchaUrl}\n\n` +
158
+ 'After completing the captcha, right-click the "Open Signal"\n' +
159
+ 'button and copy the link. It will start with signalcaptcha://\n' +
160
+ 'Paste the full link below.');
161
+ const captchaRaw = await p.text({
162
+ message: 'Paste the captcha link:',
163
+ validate: (v) => (v?.includes('signalcaptcha://') ? undefined : 'Must contain signalcaptcha://'),
164
+ });
165
+ handleCancel(captchaRaw);
166
+ // Strip the signalcaptcha:// prefix - signal-cli expects just the token
167
+ const captcha = captchaRaw.replace(/^signalcaptcha:\/\//, '');
168
+ // POST /v1/register/{number}
169
+ const registerSpinner = p.spinner();
170
+ registerSpinner.start(`Registering ${phoneNumber}...`);
171
+ const registerResp = await fetch(`${baseUrl}/v1/register/${encodeURIComponent(phoneNumber)}`, {
172
+ method: 'POST',
173
+ headers: { 'Content-Type': 'application/json' },
174
+ body: JSON.stringify({ captcha }),
175
+ });
176
+ if (!registerResp.ok) {
177
+ registerSpinner.stop('Registration failed');
178
+ const body = await registerResp.text();
179
+ if (registerResp.status === 400) {
180
+ p.log.error(`Captcha rejected by Signal. Please try again with a new captcha.\n` + `Server response: ${body}`);
181
+ }
182
+ else {
183
+ p.log.error(`Registration failed: ${registerResp.status} ${body}`);
184
+ }
185
+ throw new Error(`Registration failed: ${registerResp.status} ${body}`);
186
+ }
187
+ registerSpinner.stop('Registration request sent');
188
+ // SMS verification code
189
+ const verifyCode = await p.text({
190
+ message: 'Enter the verification code from SMS:',
191
+ validate: (v) => (v && /^\d{3}-?\d{3}$/.test(v) ? undefined : 'Enter 6-digit code (e.g., 123-456)'),
192
+ });
193
+ handleCancel(verifyCode);
194
+ const normalizedCode = verifyCode.replace('-', '');
195
+ // POST /v1/register/{number}/verify/{code}
196
+ const verifySpinner = p.spinner();
197
+ verifySpinner.start('Verifying...');
198
+ const verifyResp = await fetch(`${baseUrl}/v1/register/${encodeURIComponent(phoneNumber)}/verify/${normalizedCode}`, { method: 'POST' });
199
+ if (!verifyResp.ok) {
200
+ verifySpinner.stop('Verification failed');
201
+ const body = await verifyResp.text();
202
+ p.log.error(`Verification failed: ${verifyResp.status} ${body}`);
203
+ throw new Error(`Verification failed: ${verifyResp.status} ${body}`);
204
+ }
205
+ verifySpinner.stop('Phone number verified!');
206
+ return phoneNumber;
207
+ }
208
+ /**
209
+ * Sends a random challenge code to the recipient via Signal,
210
+ * waits for the user to type it into the terminal, then
211
+ * captures and returns the recipient's identity key.
212
+ *
213
+ * The identity key comes from signal-cli's identity endpoint
214
+ * after a successful message exchange has occurred.
215
+ */
216
+ export async function verifyRecipientIdentity(baseUrl, botNumber, recipientNumber) {
217
+ const challengeCode = String(randomInt(100000, 999999));
218
+ // Send the challenge via Signal
219
+ p.log.info('A 6-digit challenge code has been sent to your Signal app.');
220
+ const sendResp = await fetch(`${baseUrl}/v2/send`, {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({
224
+ message: `IronCurtain identity verification.\nYour challenge code is: ${challengeCode}`,
225
+ number: botNumber,
226
+ recipients: [recipientNumber],
227
+ }),
228
+ });
229
+ if (!sendResp.ok) {
230
+ throw new Error(`Failed to send challenge message: ${sendResp.status}`);
231
+ }
232
+ // Prompt user to enter the code they received
233
+ let attempts = 0;
234
+ const maxAttempts = 3;
235
+ while (attempts < maxAttempts) {
236
+ const entered = await p.text({
237
+ message: 'Enter the challenge code from your Signal app:',
238
+ validate: (v) => (v && /^\d{6}$/.test(v) ? undefined : 'Enter the 6-digit code'),
239
+ });
240
+ handleCancel(entered);
241
+ if (entered === challengeCode) {
242
+ break;
243
+ }
244
+ attempts++;
245
+ if (attempts >= maxAttempts) {
246
+ throw new Error('Challenge verification failed after 3 attempts. ' +
247
+ 'Ensure you are reading the code from the correct Signal conversation.');
248
+ }
249
+ p.log.warn(`Incorrect code. ${maxAttempts - attempts} attempt(s) remaining.`);
250
+ }
251
+ // Capture the recipient's identity key from signal-cli
252
+ return fetchIdentityKey(baseUrl, botNumber, recipientNumber);
253
+ }
254
+ /**
255
+ * Fetches the recipient's identity key from signal-cli's trust store.
256
+ * Retries once after a short delay if the key is not yet available
257
+ * (signal-cli may need time to process the message exchange).
258
+ */
259
+ async function fetchIdentityKey(baseUrl, botNumber, recipientNumber) {
260
+ const fetchKey = async () => {
261
+ // The per-recipient endpoint (/v1/identities/{bot}/{recipient}) is not
262
+ // available in all versions. Use the all-identities endpoint and filter.
263
+ const resp = await fetch(`${baseUrl}/v1/identities/${encodeURIComponent(botNumber)}`);
264
+ if (!resp.ok)
265
+ return null;
266
+ const identities = (await resp.json());
267
+ const match = identities.find((id) => id.number === recipientNumber);
268
+ if (!match)
269
+ return null;
270
+ // Fingerprint may contain spaces (e.g., "05 a1 b2 ...") - normalize to compact hex
271
+ return match.fingerprint.replace(/\s+/g, '');
272
+ };
273
+ let fingerprint = await fetchKey();
274
+ if (!fingerprint) {
275
+ // Retry after a short delay - signal-cli may still be processing
276
+ await new Promise((resolve) => setTimeout(resolve, 2000));
277
+ fingerprint = await fetchKey();
278
+ }
279
+ if (!fingerprint) {
280
+ throw new Error('Identity key could not be retrieved. ' +
281
+ 'This may indicate signal-cli has not yet exchanged keys with the recipient.');
282
+ }
283
+ p.log.success(`Identity verified! Signal identity key recorded.\n` + ` Fingerprint: ${fingerprint.substring(0, 20)}...`);
284
+ return fingerprint;
285
+ }
286
+ // ---- Re-trust flow --------------------------------------------------
287
+ /**
288
+ * Re-verifies a changed identity key without repeating full registration.
289
+ * Used when the transport detects an identity key change at runtime.
290
+ */
291
+ export async function runReTrust() {
292
+ p.intro('Signal Identity Re-verification');
293
+ const userConfig = loadUserConfig();
294
+ const signalConfig = resolveSignalConfig(userConfig);
295
+ if (!signalConfig) {
296
+ p.log.error('Signal is not configured. Run: ironcurtain setup-signal');
297
+ process.exit(1);
298
+ }
299
+ const { botNumber, recipientNumber, recipientIdentityKey: previousKey } = signalConfig;
300
+ const baseUrl = `http://127.0.0.1:${signalConfig.container.port}`;
301
+ p.note(`The Signal identity key for ${recipientNumber} has changed.\n` +
302
+ 'This can happen when:\n' +
303
+ ' - You re-installed Signal\n' +
304
+ ' - You switched to a new phone\n' +
305
+ ' - Someone else has taken over the phone number\n\n' +
306
+ 'To re-establish trust, a new challenge code will be sent.', 'Identity changed');
307
+ // Verify the container is running before attempting to send
308
+ const docker = createDockerManager();
309
+ const containerManager = createSignalContainerManager(docker, signalConfig.container);
310
+ if (!(await containerManager.isRunning())) {
311
+ const startSpinner = p.spinner();
312
+ startSpinner.start('Starting signal-cli container...');
313
+ try {
314
+ await containerManager.ensureRunning();
315
+ await containerManager.waitForHealthy(baseUrl);
316
+ startSpinner.stop('Container started');
317
+ }
318
+ catch (err) {
319
+ startSpinner.stop('Failed to start container');
320
+ p.log.error(`Could not start signal-cli container: ${err instanceof Error ? err.message : String(err)}`);
321
+ process.exit(1);
322
+ }
323
+ }
324
+ const newKey = await verifyRecipientIdentity(baseUrl, botNumber, recipientNumber);
325
+ // Save the new identity key
326
+ saveUserConfig({
327
+ signal: {
328
+ botNumber,
329
+ recipientNumber,
330
+ recipientIdentityKey: newKey,
331
+ },
332
+ });
333
+ p.note(`Previous key: ${previousKey.substring(0, 20)}...\n` + `New key: ${newKey.substring(0, 20)}...`, 'Key updated');
334
+ p.outro('Transport unlocked. Sessions will now accept messages.');
335
+ }
336
+ // ---- Main setup wizard ----------------------------------------------
337
+ /**
338
+ * Runs the interactive Signal setup wizard.
339
+ *
340
+ * Steps:
341
+ * 1. Validate Docker availability
342
+ * 2. Pull and start signal-cli container
343
+ * 3. Register new number (or reuse existing account)
344
+ * 4. Configure recipient number
345
+ * 5. Challenge-response identity verification
346
+ * 6. Capture and store identity key
347
+ * 7. Save config
348
+ */
349
+ export async function runSignalSetup(options) {
350
+ if (options?.reTrust) {
351
+ await runReTrust();
352
+ return;
353
+ }
354
+ p.intro('Signal Transport Setup');
355
+ p.note('Signal lets you interact with IronCurtain sessions from your\n' +
356
+ 'phone. The communication channel is end-to-end encrypted and\n' +
357
+ 'securely paired between the bot and your phone.\n\n' +
358
+ "You'll need:\n" +
359
+ ' - Docker running on this machine\n' +
360
+ ' - A dedicated phone number for the bot\n' +
361
+ ' - Your own Signal phone number (to receive messages)', 'What is this?');
362
+ const cont = await p.confirm({ message: 'Continue with setup?', initialValue: true });
363
+ handleCancel(cont);
364
+ if (!cont) {
365
+ p.cancel('Setup cancelled.');
366
+ process.exit(0);
367
+ }
368
+ // Step 1: Docker check
369
+ const docker = createDockerManager();
370
+ await validateDocker(docker);
371
+ // Step 2: Pull image and start container
372
+ const containerConfig = resolveContainerConfig();
373
+ const manager = createSignalContainerManager(docker, containerConfig);
374
+ const baseUrl = await startContainer(manager, containerConfig);
375
+ // Step 3: Check for existing accounts, then register
376
+ let botNumber;
377
+ const existingAccounts = await fetchAccounts(baseUrl);
378
+ if (existingAccounts.length > 0) {
379
+ const useExisting = await p.select({
380
+ message: `Found existing account: ${existingAccounts.join(', ')}. What would you like to do?`,
381
+ options: [
382
+ { value: 'use', label: `Use ${existingAccounts[0]}`, hint: 'skip registration' },
383
+ { value: 'register', label: 'Register a new phone number' },
384
+ ],
385
+ });
386
+ handleCancel(useExisting);
387
+ if (useExisting === 'use') {
388
+ botNumber = existingAccounts[0];
389
+ p.log.success(`Using existing account: ${botNumber}`);
390
+ }
391
+ else {
392
+ botNumber = await registerNewNumber(baseUrl);
393
+ }
394
+ }
395
+ else {
396
+ botNumber = await registerNewNumber(baseUrl);
397
+ }
398
+ // In json-rpc mode, signal-cli loads accounts at startup. After registration
399
+ // or when the account was found only in the data directory, restart the
400
+ // container so the daemon picks up the account for subsequent API calls.
401
+ const restartSpinner = p.spinner();
402
+ restartSpinner.start('Restarting signal-cli to load account...');
403
+ try {
404
+ await docker.stop(containerConfig.containerName);
405
+ await docker.start(containerConfig.containerName);
406
+ await manager.waitForHealthy(baseUrl);
407
+ restartSpinner.stop('signal-cli restarted');
408
+ }
409
+ catch (err) {
410
+ restartSpinner.stop('Restart failed');
411
+ p.log.error(`Failed to restart container: ${err instanceof Error ? err.message : String(err)}`);
412
+ process.exit(1);
413
+ }
414
+ // Step 4: Recipient number
415
+ const recipientNumber = await p.text({
416
+ message: 'Enter YOUR Signal phone number (to receive agent messages):',
417
+ placeholder: '+15559876543',
418
+ validate: validatePhoneNumber,
419
+ });
420
+ handleCancel(recipientNumber);
421
+ // Step 5: Challenge-response identity verification
422
+ const identityKey = await verifyRecipientIdentity(baseUrl, botNumber, recipientNumber);
423
+ // Step 6: Save config
424
+ saveUserConfig({
425
+ signal: {
426
+ botNumber,
427
+ recipientNumber: recipientNumber,
428
+ recipientIdentityKey: identityKey,
429
+ container: {
430
+ image: containerConfig.image,
431
+ port: containerConfig.port,
432
+ },
433
+ },
434
+ });
435
+ // Summary
436
+ p.note(`Bot number: ${botNumber}\n` +
437
+ `Your number: ${recipientNumber}\n` +
438
+ `Identity key: ${identityKey.substring(0, 20)}...\n` +
439
+ `Container: ${containerConfig.containerName}\n` +
440
+ `API port: ${containerConfig.port}`, 'Configuration saved');
441
+ p.outro('Setup complete. Run: ironcurtain bot');
442
+ }
443
+ //# sourceMappingURL=setup-signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-signal.js","sourceRoot":"","sources":["../../src/signal/setup-signal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,142 @@
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 { type ResolvedSignalConfig } from './signal-config.js';
14
+ import type { SessionMode } from '../session/types.js';
15
+ import type { SignalContainerManager } from './signal-container.js';
16
+ /**
17
+ * Signal message envelope received via WebSocket.
18
+ * Only the fields relevant to IronCurtain are typed here;
19
+ * the full envelope has many more fields (receipts, typing
20
+ * indicators, etc.) that we ignore.
21
+ */
22
+ export interface SignalEnvelope {
23
+ /** Sender's phone number or UUID. */
24
+ source?: string;
25
+ /** Sender's phone number (may differ from source in group messages). */
26
+ sourceNumber?: string;
27
+ /** Whether the sender's identity key has changed since last seen by signal-cli. */
28
+ untrustedIdentity?: boolean;
29
+ /** The actual message content, present for data messages. */
30
+ dataMessage?: {
31
+ /** Message text. */
32
+ message?: string;
33
+ /** Timestamp of the message. */
34
+ timestamp?: number;
35
+ /** Group context, if this is a group message. */
36
+ groupInfo?: {
37
+ groupId: string;
38
+ };
39
+ };
40
+ /** Typing indicator events - ignored. */
41
+ typingMessage?: unknown;
42
+ /** Receipt events - ignored. */
43
+ receiptMessage?: unknown;
44
+ }
45
+ export interface SignalBotDaemonOptions {
46
+ readonly config: ResolvedSignalConfig;
47
+ readonly containerManager: SignalContainerManager;
48
+ readonly mode: SessionMode;
49
+ }
50
+ export declare class SignalBotDaemon {
51
+ private config;
52
+ private readonly containerManager;
53
+ private readonly mode;
54
+ private ws;
55
+ private baseUrl;
56
+ private closed;
57
+ private reconnectAttempts;
58
+ private static readonly MAX_RECONNECT_DELAY_MS;
59
+ private static readonly BASE_RECONNECT_DELAY_MS;
60
+ private activeSession;
61
+ private activeTransport;
62
+ private messageInFlight;
63
+ private pendingEscalationId;
64
+ private escalationResolving;
65
+ private identityLocked;
66
+ private lastIdentityCheckMs;
67
+ private recentTimestamps;
68
+ private exitResolve;
69
+ constructor(options: SignalBotDaemonOptions);
70
+ /**
71
+ * Starts the daemon. Returns a promise that resolves when
72
+ * shutdown() is called (e.g., SIGTERM/SIGINT).
73
+ */
74
+ start(): Promise<void>;
75
+ /**
76
+ * Initiates graceful shutdown. Ends the active session,
77
+ * closes the WebSocket, and unblocks start().
78
+ */
79
+ shutdown(): Promise<void>;
80
+ private connectWebSocket;
81
+ /**
82
+ * Polls GET /v1/receive/{number} to drain messages missed during
83
+ * a WebSocket disconnect. Uses timestamp-based deduplication to
84
+ * avoid processing messages that the WebSocket already delivered.
85
+ */
86
+ private drainMissedMessages;
87
+ private scheduleReconnect;
88
+ private handleIncomingMessage;
89
+ /**
90
+ * Routes a user message to the active session. Creates a new
91
+ * session if none exists. Handles BudgetExhaustedError by ending
92
+ * the exhausted session and notifying the user.
93
+ */
94
+ private routeToSession;
95
+ /**
96
+ * Creates a new session with a fresh SignalSessionTransport.
97
+ * Follows the same pattern as index.ts: create transport,
98
+ * wire callbacks, create session, start transport.
99
+ */
100
+ private startNewSession;
101
+ /**
102
+ * Ends the current session and cleans up. Awaits session.close()
103
+ * to ensure resources are released (sandbox, MCP connections, etc.).
104
+ * Guarded against re-entrance.
105
+ */
106
+ endCurrentSession(): Promise<void>;
107
+ /**
108
+ * Checks if the message is an escalation reply.
109
+ * Accepts: approve, deny, /approve, /deny (case-insensitive).
110
+ *
111
+ * Race condition prevention: escalationResolving flag prevents
112
+ * concurrent replies. pendingEscalationId is cleared in .finally()
113
+ * after async resolution completes, not before.
114
+ */
115
+ private handleEscalationReply;
116
+ /** Called by SignalSessionTransport when the session surfaces an escalation. */
117
+ setPendingEscalation(escalationId: string): void;
118
+ /** Called by SignalSessionTransport when an escalation expires. */
119
+ clearPendingEscalation(): void;
120
+ private handleControlCommand;
121
+ /**
122
+ * Checks whether the sender's Signal identity key has changed.
123
+ *
124
+ * Detection path 1: signal-cli flags the envelope with `untrustedIdentity`.
125
+ * Detection path 2: periodic proactive check via GET /v1/identities.
126
+ * Cached with IDENTITY_CHECK_INTERVAL_MS TTL to avoid HTTP overhead
127
+ * on every message.
128
+ *
129
+ * When locked, attempts self-healing by re-reading config from disk
130
+ * (handles the case where the user ran `--re-trust` externally).
131
+ *
132
+ * Returns true if the message should be rejected.
133
+ */
134
+ checkIdentityChanged(envelope: SignalEnvelope): Promise<boolean>;
135
+ private lockTransport;
136
+ sendSignalMessage(text: string): Promise<void>;
137
+ private postMessage;
138
+ }
139
+ export declare function parseSignalEnvelope(data: string): SignalEnvelope | null;
140
+ /** Normalize a phone number by stripping spaces. */
141
+ export declare function normalizePhoneNumber(number: string): string;
142
+ export declare function isAuthorizedSender(envelope: SignalEnvelope, recipientNumber: string): boolean;