@provos/ironcurtain 0.3.1 → 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.
- package/README.md +20 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/index.js +6 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/user-config.d.ts +11 -0
- package/dist/config/user-config.js +56 -2
- package/dist/config/user-config.js.map +1 -1
- package/dist/docker/adapters/claude-code.js +14 -3
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +2 -0
- package/dist/docker/audit-log-tailer.d.ts +4 -3
- package/dist/docker/audit-log-tailer.js +9 -7
- package/dist/docker/audit-log-tailer.js.map +1 -1
- package/dist/docker/docker-agent-session.d.ts +11 -0
- package/dist/docker/docker-agent-session.js +103 -22
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-manager.js +82 -15
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/managed-proxy.d.ts +7 -3
- package/dist/docker/managed-proxy.js +98 -8
- package/dist/docker/managed-proxy.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +7 -4
- package/dist/docker/mitm-proxy.js +43 -13
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/orientation.d.ts +1 -1
- package/dist/docker/orientation.js +5 -2
- package/dist/docker/orientation.js.map +1 -1
- package/dist/docker/platform.d.ts +20 -0
- package/dist/docker/platform.js +24 -0
- package/dist/docker/platform.js.map +1 -0
- package/dist/docker/types.d.ts +48 -6
- package/dist/session/index.js +16 -6
- package/dist/session/index.js.map +1 -1
- package/dist/signal/bot-command.d.ts +13 -0
- package/dist/signal/bot-command.js +54 -0
- package/dist/signal/bot-command.js.map +1 -0
- package/dist/signal/format.d.ts +31 -0
- package/dist/signal/format.js +101 -0
- package/dist/signal/format.js.map +1 -0
- package/dist/signal/index.d.ts +16 -0
- package/dist/signal/index.js +13 -0
- package/dist/signal/index.js.map +1 -0
- package/dist/signal/markdown-to-signal.d.ts +29 -0
- package/dist/signal/markdown-to-signal.js +154 -0
- package/dist/signal/markdown-to-signal.js.map +1 -0
- package/dist/signal/setup-signal.d.ts +48 -0
- package/dist/signal/setup-signal.js +443 -0
- package/dist/signal/setup-signal.js.map +1 -0
- package/dist/signal/signal-bot-daemon.d.ts +142 -0
- package/dist/signal/signal-bot-daemon.js +504 -0
- package/dist/signal/signal-bot-daemon.js.map +1 -0
- package/dist/signal/signal-config.d.ts +64 -0
- package/dist/signal/signal-config.js +53 -0
- package/dist/signal/signal-config.js.map +1 -0
- package/dist/signal/signal-container.d.ts +37 -0
- package/dist/signal/signal-container.js +112 -0
- package/dist/signal/signal-container.js.map +1 -0
- package/dist/signal/signal-transport.d.ts +34 -0
- package/dist/signal/signal-transport.js +70 -0
- package/dist/signal/signal-transport.js.map +1 -0
- package/dist/trusted-process/mcp-proxy-server.js +34 -3
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/path-utils.d.ts +5 -1
- package/dist/trusted-process/path-utils.js +47 -4
- package/dist/trusted-process/path-utils.js.map +1 -1
- package/dist/trusted-process/tcp-server-transport.d.ts +43 -0
- package/dist/trusted-process/tcp-server-transport.js +119 -0
- package/dist/trusted-process/tcp-server-transport.js.map +1 -0
- package/dist/types/argument-roles.js +20 -5
- package/dist/types/argument-roles.js.map +1 -1
- package/docker/Dockerfile.base.arm64 +23 -0
- package/package.json +6 -3
|
@@ -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;
|