signal-sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/SignalCli.js CHANGED
@@ -88,6 +88,25 @@ class SignalCli extends events_1.EventEmitter {
88
88
  this.account = phoneNumber;
89
89
  }
90
90
  async connect() {
91
+ const daemonMode = this.config.daemonMode || 'json-rpc';
92
+ switch (daemonMode) {
93
+ case 'json-rpc':
94
+ await this.connectJsonRpc();
95
+ break;
96
+ case 'unix-socket':
97
+ await this.connectUnixSocket();
98
+ break;
99
+ case 'tcp':
100
+ await this.connectTcp();
101
+ break;
102
+ case 'http':
103
+ await this.connectHttp();
104
+ break;
105
+ default:
106
+ throw new Error(`Invalid daemon mode: ${daemonMode}`);
107
+ }
108
+ }
109
+ async connectJsonRpc() {
91
110
  const args = this.account ? ['-a', this.account, 'jsonRpc'] : ['jsonRpc'];
92
111
  if (process.platform === 'win32') {
93
112
  // On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
@@ -133,11 +152,120 @@ class SignalCli extends events_1.EventEmitter {
133
152
  });
134
153
  });
135
154
  }
155
+ async connectUnixSocket() {
156
+ const net = await Promise.resolve().then(() => __importStar(require('net')));
157
+ const socketPath = this.config.socketPath || '/tmp/signal-cli.sock';
158
+ return new Promise((resolve, reject) => {
159
+ const socket = net.createConnection(socketPath);
160
+ socket.on('connect', () => {
161
+ this.logger.debug('Connected to Unix socket:', socketPath);
162
+ socket.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
163
+ socket.on('error', (err) => this.emit('error', err));
164
+ socket.on('close', () => {
165
+ this.emit('close', 0);
166
+ });
167
+ this.socket = socket;
168
+ resolve();
169
+ });
170
+ socket.on('error', (err) => {
171
+ reject(new errors_1.ConnectionError(`Failed to connect to Unix socket: ${err.message}`));
172
+ });
173
+ setTimeout(() => reject(new errors_1.ConnectionError('Unix socket connection timeout')), this.config.connectionTimeout);
174
+ });
175
+ }
176
+ async connectTcp() {
177
+ const net = await Promise.resolve().then(() => __importStar(require('net')));
178
+ const host = this.config.tcpHost || 'localhost';
179
+ const port = this.config.tcpPort || 7583;
180
+ return new Promise((resolve, reject) => {
181
+ const socket = net.createConnection(port, host);
182
+ socket.on('connect', () => {
183
+ this.logger.debug(`Connected to TCP: ${host}:${port}`);
184
+ socket.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
185
+ socket.on('error', (err) => this.emit('error', err));
186
+ socket.on('close', () => {
187
+ this.emit('close', 0);
188
+ });
189
+ this.socket = socket;
190
+ resolve();
191
+ });
192
+ socket.on('error', (err) => {
193
+ reject(new errors_1.ConnectionError(`Failed to connect to TCP: ${err.message}`));
194
+ });
195
+ setTimeout(() => reject(new errors_1.ConnectionError('TCP connection timeout')), this.config.connectionTimeout);
196
+ });
197
+ }
198
+ async connectHttp() {
199
+ const baseUrl = this.config.httpBaseUrl || 'http://localhost:8080';
200
+ // For HTTP mode, we don't maintain a persistent connection
201
+ // Instead, we'll use the httpRequest method for each operation
202
+ this.logger.debug('HTTP mode configured:', baseUrl);
203
+ this.httpBaseUrl = baseUrl;
204
+ // Test connection by sending a simple request
205
+ try {
206
+ await this.httpRequest({ jsonrpc: '2.0', method: 'version', params: {}, id: (0, uuid_1.v4)() });
207
+ this.logger.debug('HTTP connection verified');
208
+ }
209
+ catch (error) {
210
+ throw new errors_1.ConnectionError(`Failed to connect to HTTP endpoint: ${error instanceof Error ? error.message : String(error)}`);
211
+ }
212
+ }
213
+ async httpRequest(request) {
214
+ const https = await Promise.resolve(`${this.config.httpBaseUrl?.startsWith('https:') ? 'https' : 'http'}`).then(s => __importStar(require(s)));
215
+ const baseUrl = this.config.httpBaseUrl || 'http://localhost:8080';
216
+ const url = new URL('/api/v1/rpc', baseUrl);
217
+ return new Promise((resolve, reject) => {
218
+ const data = JSON.stringify(request);
219
+ const options = {
220
+ method: 'POST',
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ 'Content-Length': Buffer.byteLength(data)
224
+ }
225
+ };
226
+ const req = https.request(url, options, (res) => {
227
+ let body = '';
228
+ res.on('data', (chunk) => body += chunk);
229
+ res.on('end', () => {
230
+ try {
231
+ const response = JSON.parse(body);
232
+ if (response.error) {
233
+ reject(new Error(`[${response.error.code}] ${response.error.message}`));
234
+ }
235
+ else {
236
+ resolve(response.result);
237
+ }
238
+ }
239
+ catch (error) {
240
+ reject(new Error(`Failed to parse HTTP response: ${body}`));
241
+ }
242
+ });
243
+ });
244
+ req.on('error', (err) => reject(new errors_1.ConnectionError(`HTTP request failed: ${err.message}`)));
245
+ req.setTimeout(this.config.requestTimeout, () => {
246
+ req.destroy();
247
+ reject(new errors_1.ConnectionError('HTTP request timeout'));
248
+ });
249
+ req.write(data);
250
+ req.end();
251
+ });
252
+ }
136
253
  disconnect() {
137
- if (this.cliProcess) {
254
+ const daemonMode = this.config.daemonMode || 'json-rpc';
255
+ // Close socket connections
256
+ if (daemonMode === 'unix-socket' || daemonMode === 'tcp') {
257
+ const socket = this.socket;
258
+ if (socket && !socket.destroyed) {
259
+ socket.destroy();
260
+ this.socket = null;
261
+ }
262
+ }
263
+ // Close process for json-rpc mode
264
+ if (daemonMode === 'json-rpc' && this.cliProcess) {
138
265
  this.cliProcess.kill();
139
266
  this.cliProcess = null;
140
267
  }
268
+ // For HTTP mode, nothing to disconnect (stateless)
141
269
  }
142
270
  async gracefulShutdown() {
143
271
  return new Promise((resolve) => {
@@ -153,13 +281,17 @@ class SignalCli extends events_1.EventEmitter {
153
281
  // Send SIGTERM for graceful shutdown
154
282
  this.cliProcess.kill('SIGTERM');
155
283
  // Force kill after 5 seconds if it doesn't close gracefully
156
- setTimeout(() => {
284
+ const forceKillTimer = setTimeout(() => {
157
285
  if (this.cliProcess) {
158
286
  this.cliProcess.kill('SIGKILL');
159
287
  this.cliProcess = null;
160
288
  resolve();
161
289
  }
162
290
  }, 5000);
291
+ // Use unref() to prevent this timer from keeping the process alive
292
+ if (forceKillTimer.unref) {
293
+ forceKillTimer.unref();
294
+ }
163
295
  });
164
296
  }
165
297
  handleRpcResponse(data) {
@@ -234,8 +366,40 @@ class SignalCli extends events_1.EventEmitter {
234
366
  }
235
367
  }
236
368
  async sendJsonRpcRequest(method, params) {
369
+ const daemonMode = this.config.daemonMode || 'json-rpc';
370
+ // For HTTP mode, use HTTP requests
371
+ if (daemonMode === 'http') {
372
+ const id = (0, uuid_1.v4)();
373
+ const request = {
374
+ jsonrpc: '2.0',
375
+ method,
376
+ params,
377
+ id,
378
+ };
379
+ return await this.httpRequest(request);
380
+ }
381
+ // For socket modes (Unix socket, TCP), write to socket
382
+ if (daemonMode === 'unix-socket' || daemonMode === 'tcp') {
383
+ const socket = this.socket;
384
+ if (!socket || socket.destroyed) {
385
+ throw new errors_1.ConnectionError('Not connected. Call connect() first.');
386
+ }
387
+ const id = (0, uuid_1.v4)();
388
+ const request = {
389
+ jsonrpc: '2.0',
390
+ method,
391
+ params,
392
+ id,
393
+ };
394
+ const promise = new Promise((resolve, reject) => {
395
+ this.requestPromises.set(id, { resolve, reject });
396
+ });
397
+ socket.write(JSON.stringify(request) + '\n');
398
+ return promise;
399
+ }
400
+ // Default JSON-RPC mode with stdin/stdout
237
401
  if (!this.cliProcess || !this.cliProcess.stdin) {
238
- throw new Error('Not connected. Call connect() first.');
402
+ throw new errors_1.ConnectionError('Not connected. Call connect() first.');
239
403
  }
240
404
  const id = (0, uuid_1.v4)();
241
405
  const request = {
@@ -272,7 +436,7 @@ class SignalCli extends events_1.EventEmitter {
272
436
  else {
273
437
  params.recipients = [recipient];
274
438
  }
275
- // Only add safe, well-known options to avoid JSON parsing issues
439
+ // Add well-known options
276
440
  if (options.attachments && options.attachments.length > 0) {
277
441
  params.attachments = options.attachments;
278
442
  }
@@ -280,7 +444,64 @@ class SignalCli extends events_1.EventEmitter {
280
444
  params.expiresInSeconds = options.expiresInSeconds;
281
445
  }
282
446
  if (options.isViewOnce) {
283
- params.isViewOnce = options.isViewOnce;
447
+ params.viewOnce = options.isViewOnce;
448
+ }
449
+ // Add advanced text formatting options
450
+ if (options.mentions && options.mentions.length > 0) {
451
+ params.mentions = options.mentions.map(m => ({
452
+ start: m.start,
453
+ length: m.length,
454
+ number: m.recipient || m.number
455
+ }));
456
+ }
457
+ if (options.textStyles && options.textStyles.length > 0) {
458
+ params.textStyles = options.textStyles.map(ts => ({
459
+ start: ts.start,
460
+ length: ts.length,
461
+ style: ts.style
462
+ }));
463
+ }
464
+ // Add quote/reply information
465
+ if (options.quote) {
466
+ params.quoteTimestamp = options.quote.timestamp;
467
+ params.quoteAuthor = options.quote.author;
468
+ if (options.quote.text) {
469
+ params.quoteMessage = options.quote.text;
470
+ }
471
+ if (options.quote.mentions && options.quote.mentions.length > 0) {
472
+ params.quoteMentions = options.quote.mentions.map(m => ({
473
+ start: m.start,
474
+ length: m.length,
475
+ number: m.recipient || m.number
476
+ }));
477
+ }
478
+ if (options.quote.textStyles && options.quote.textStyles.length > 0) {
479
+ params.quoteTextStyles = options.quote.textStyles.map(ts => ({
480
+ start: ts.start,
481
+ length: ts.length,
482
+ style: ts.style
483
+ }));
484
+ }
485
+ }
486
+ // Add preview URL
487
+ if (options.previewUrl) {
488
+ params.previewUrl = options.previewUrl;
489
+ }
490
+ // Add edit timestamp for editing existing messages
491
+ if (options.editTimestamp) {
492
+ params.editTimestamp = options.editTimestamp;
493
+ }
494
+ // Add story reply information
495
+ if (options.storyTimestamp && options.storyAuthor) {
496
+ params.storyTimestamp = options.storyTimestamp;
497
+ params.storyAuthor = options.storyAuthor;
498
+ }
499
+ // Add special flags
500
+ if (options.noteToSelf) {
501
+ params.noteToSelf = options.noteToSelf;
502
+ }
503
+ if (options.endSession) {
504
+ params.endSession = options.endSession;
284
505
  }
285
506
  return this.sendJsonRpcRequest('send', params);
286
507
  }
@@ -371,6 +592,79 @@ class SignalCli extends events_1.EventEmitter {
371
592
  async trustIdentity(number, safetyNumber, verified = true) {
372
593
  await this.sendJsonRpcRequest('trust', { account: this.account, recipient: number, safetyNumber, verified });
373
594
  }
595
+ /**
596
+ * Get the safety number for a specific contact.
597
+ * This is a helper method that extracts just the safety number from identity information.
598
+ *
599
+ * @param number - The phone number of the contact
600
+ * @returns The safety number string, or null if not found
601
+ *
602
+ * @example
603
+ * ```typescript
604
+ * const safetyNumber = await signal.getSafetyNumber('+33123456789');
605
+ * console.log(`Safety number: ${safetyNumber}`);
606
+ * ```
607
+ */
608
+ async getSafetyNumber(number) {
609
+ const identities = await this.listIdentities(number);
610
+ if (identities.length > 0 && identities[0].safetyNumber) {
611
+ return identities[0].safetyNumber;
612
+ }
613
+ return null;
614
+ }
615
+ /**
616
+ * Verify a safety number for a contact.
617
+ * Checks if the provided safety number matches the stored one and marks it as trusted if it does.
618
+ *
619
+ * @param number - The phone number of the contact
620
+ * @param safetyNumber - The safety number to verify
621
+ * @returns True if the safety number matches and was trusted, false otherwise
622
+ *
623
+ * @example
624
+ * ```typescript
625
+ * const verified = await signal.verifySafetyNumber('+33123456789', '123456 78901...');
626
+ * if (verified) {
627
+ * console.log('Safety number verified and trusted');
628
+ * } else {
629
+ * console.log('Safety number does not match!');
630
+ * }
631
+ * ```
632
+ */
633
+ async verifySafetyNumber(number, safetyNumber) {
634
+ const storedSafetyNumber = await this.getSafetyNumber(number);
635
+ if (!storedSafetyNumber) {
636
+ return false;
637
+ }
638
+ // Compare safety numbers (remove spaces for comparison)
639
+ const normalizedStored = storedSafetyNumber.replace(/\s/g, '');
640
+ const normalizedProvided = safetyNumber.replace(/\s/g, '');
641
+ if (normalizedStored === normalizedProvided) {
642
+ await this.trustIdentity(number, safetyNumber, true);
643
+ return true;
644
+ }
645
+ return false;
646
+ }
647
+ /**
648
+ * List all untrusted identities.
649
+ * Returns identities that have not been explicitly trusted.
650
+ *
651
+ * @returns Array of untrusted identity keys
652
+ *
653
+ * @example
654
+ * ```typescript
655
+ * const untrusted = await signal.listUntrustedIdentities();
656
+ * console.log(`Found ${untrusted.length} untrusted identities`);
657
+ * untrusted.forEach(id => {
658
+ * console.log(`${id.number}: ${id.safetyNumber}`);
659
+ * });
660
+ * ```
661
+ */
662
+ async listUntrustedIdentities() {
663
+ const allIdentities = await this.listIdentities();
664
+ return allIdentities.filter(identity => identity.trustLevel === 'UNTRUSTED' ||
665
+ identity.trustLevel === 'TRUST_ON_FIRST_USE' ||
666
+ !identity.trustLevel);
667
+ }
374
668
  async link(deviceName) {
375
669
  const result = await this.sendJsonRpcRequest('link', { deviceName });
376
670
  return result.uri;
@@ -505,6 +799,10 @@ class SignalCli extends events_1.EventEmitter {
505
799
  params.promoteAdmins = options.promoteAdmins;
506
800
  if (options.demoteAdmins)
507
801
  params.demoteAdmins = options.demoteAdmins;
802
+ if (options.banMembers)
803
+ params.banMembers = options.banMembers;
804
+ if (options.unbanMembers)
805
+ params.unbanMembers = options.unbanMembers;
508
806
  if (options.resetInviteLink)
509
807
  params.resetLink = true;
510
808
  if (options.permissionAddMember)
@@ -520,6 +818,58 @@ class SignalCli extends events_1.EventEmitter {
520
818
  async listGroups() {
521
819
  return this.sendJsonRpcRequest('listGroups', { account: this.account });
522
820
  }
821
+ /**
822
+ * Send group invite link to a recipient.
823
+ * Retrieves and sends the invitation link for a group.
824
+ *
825
+ * @param groupId - The group ID
826
+ * @param recipient - The recipient to send the invite link to
827
+ * @returns Send response
828
+ *
829
+ * @example
830
+ * ```typescript
831
+ * await signal.sendGroupInviteLink('groupId123==', '+33123456789');
832
+ * ```
833
+ */
834
+ async sendGroupInviteLink(groupId, recipient) {
835
+ // Get group info to retrieve invite link
836
+ const groups = await this.listGroups();
837
+ const group = groups.find(g => g.groupId === groupId);
838
+ const inviteLink = group?.groupInviteLink || group?.inviteLink;
839
+ if (!group || !inviteLink) {
840
+ throw new Error('Group not found or does not have an invite link');
841
+ }
842
+ return this.sendMessage(recipient, `Join our group: ${inviteLink}`);
843
+ }
844
+ /**
845
+ * Set banned members for a group.
846
+ * Ban specific members from the group.
847
+ *
848
+ * @param groupId - The group ID
849
+ * @param members - Array of phone numbers to ban
850
+ *
851
+ * @example
852
+ * ```typescript
853
+ * await signal.setBannedMembers('groupId123==', ['+33111111111', '+33222222222']);
854
+ * ```
855
+ */
856
+ async setBannedMembers(groupId, members) {
857
+ await this.updateGroup(groupId, { banMembers: members });
858
+ }
859
+ /**
860
+ * Reset group invite link.
861
+ * Invalidates the current group invite link and generates a new one.
862
+ *
863
+ * @param groupId - The group ID
864
+ *
865
+ * @example
866
+ * ```typescript
867
+ * await signal.resetGroupLink('groupId123==');
868
+ * ```
869
+ */
870
+ async resetGroupLink(groupId) {
871
+ await this.updateGroup(groupId, { resetInviteLink: true });
872
+ }
523
873
  async listContacts() {
524
874
  return this.sendJsonRpcRequest('listContacts', { account: this.account });
525
875
  }
@@ -559,6 +909,101 @@ class SignalCli extends events_1.EventEmitter {
559
909
  console.warn("stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.");
560
910
  this.gracefulShutdown();
561
911
  }
912
+ // ############# MESSAGE RECEIVING #############
913
+ /**
914
+ * Receive messages from Signal with configurable options.
915
+ * This is the modern replacement for the deprecated receiveMessages().
916
+ *
917
+ * @param options - Options for receiving messages
918
+ * @returns Array of received messages
919
+ *
920
+ * @example
921
+ * ```typescript
922
+ * // Receive with default timeout
923
+ * const messages = await signal.receive();
924
+ *
925
+ * // Receive with custom options
926
+ * const messages = await signal.receive({
927
+ * timeout: 10,
928
+ * maxMessages: 5,
929
+ * ignoreAttachments: true,
930
+ * sendReadReceipts: true
931
+ * });
932
+ * ```
933
+ */
934
+ async receive(options = {}) {
935
+ const params = { account: this.account };
936
+ // Set timeout (default: 5 seconds)
937
+ if (options.timeout !== undefined) {
938
+ params.timeout = options.timeout;
939
+ }
940
+ // Set maximum number of messages
941
+ if (options.maxMessages !== undefined) {
942
+ params.maxMessages = options.maxMessages;
943
+ }
944
+ // Skip attachment downloads
945
+ if (options.ignoreAttachments) {
946
+ params.ignoreAttachments = true;
947
+ }
948
+ // Skip stories
949
+ if (options.ignoreStories) {
950
+ params.ignoreStories = true;
951
+ }
952
+ // Send read receipts automatically
953
+ if (options.sendReadReceipts) {
954
+ params.sendReadReceipts = true;
955
+ }
956
+ try {
957
+ const result = await this.sendJsonRpcRequest('receive', params);
958
+ // Parse and return messages
959
+ if (Array.isArray(result)) {
960
+ return result.map(envelope => this.parseEnvelope(envelope));
961
+ }
962
+ return [];
963
+ }
964
+ catch (error) {
965
+ this.logger.error('Failed to receive messages:', error);
966
+ throw error;
967
+ }
968
+ }
969
+ /**
970
+ * Parse a message envelope from signal-cli into a Message object.
971
+ * @private
972
+ */
973
+ parseEnvelope(envelope) {
974
+ const message = {
975
+ timestamp: envelope.timestamp || Date.now(),
976
+ source: envelope.source || envelope.sourceNumber,
977
+ sourceUuid: envelope.sourceUuid,
978
+ sourceDevice: envelope.sourceDevice,
979
+ };
980
+ // Parse data message
981
+ if (envelope.dataMessage) {
982
+ const data = envelope.dataMessage;
983
+ message.text = data.message || data.body;
984
+ message.groupId = data.groupInfo?.groupId;
985
+ message.attachments = data.attachments;
986
+ message.mentions = data.mentions;
987
+ message.quote = data.quote;
988
+ message.reaction = data.reaction;
989
+ message.sticker = data.sticker;
990
+ message.expiresInSeconds = data.expiresInSeconds;
991
+ message.viewOnce = data.viewOnce;
992
+ }
993
+ // Parse sync message
994
+ if (envelope.syncMessage) {
995
+ message.syncMessage = envelope.syncMessage;
996
+ }
997
+ // Parse receipt message
998
+ if (envelope.receiptMessage) {
999
+ message.receipt = envelope.receiptMessage;
1000
+ }
1001
+ // Parse typing message
1002
+ if (envelope.typingMessage) {
1003
+ message.typing = envelope.typingMessage;
1004
+ }
1005
+ return message;
1006
+ }
562
1007
  // ############# NEW FEATURES - Missing signal-cli Commands #############
563
1008
  /**
564
1009
  * Remove a contact from the contact list.
@@ -604,12 +1049,29 @@ class SignalCli extends events_1.EventEmitter {
604
1049
  return statusResults;
605
1050
  }
606
1051
  /**
607
- * Send a payment notification to a recipient.
608
- * @param recipient - Phone number or group ID to send the notification to
609
- * @param paymentData - Payment notification data including receipt
610
- * @returns Send response with timestamp and other details
1052
+ * Send a payment notification to a recipient (MobileCoin).
1053
+ * Sends a notification about a cryptocurrency payment made through Signal's MobileCoin integration.
1054
+ *
1055
+ * @param recipient - The phone number or group ID of the recipient
1056
+ * @param paymentData - Payment notification data including receipt and optional note
1057
+ * @returns Send result with timestamp
1058
+ * @throws {Error} If receipt is invalid or sending fails
1059
+ *
1060
+ * @example
1061
+ * ```typescript
1062
+ * const receiptBlob = 'base64EncodedReceiptData...';
1063
+ * await signal.sendPaymentNotification('+33612345678', {
1064
+ * receipt: receiptBlob,
1065
+ * note: 'Thanks for dinner!'
1066
+ * });
1067
+ * ```
611
1068
  */
612
1069
  async sendPaymentNotification(recipient, paymentData) {
1070
+ this.logger.info(`Sending payment notification to ${recipient}`);
1071
+ (0, validators_1.validateRecipient)(recipient);
1072
+ if (!paymentData.receipt || paymentData.receipt.trim().length === 0) {
1073
+ throw new Error('Payment receipt is required');
1074
+ }
613
1075
  const params = {
614
1076
  receipt: paymentData.receipt,
615
1077
  account: this.account
@@ -662,13 +1124,18 @@ class SignalCli extends events_1.EventEmitter {
662
1124
  };
663
1125
  }
664
1126
  /**
665
- * Start the process of changing phone number.
666
- * @param newNumber - The new phone number to change to
667
- * @param voice - Whether to use voice verification instead of SMS
668
- * @param captcha - Captcha token if required
669
- * @returns Change number session information
1127
+ * Start the phone number change process.
1128
+ * Initiates SMS or voice verification for changing your account to a new phone number.
1129
+ * After calling this, you must verify the new number with finishChangeNumber().
1130
+ *
1131
+ * @param newNumber - The new phone number in E164 format (e.g., "+33612345678")
1132
+ * @param voice - Use voice verification instead of SMS (default: false)
1133
+ * @param captcha - Optional captcha token if required
1134
+ * @throws {Error} If not a primary device or rate limited
670
1135
  */
671
1136
  async startChangeNumber(newNumber, voice = false, captcha) {
1137
+ this.logger.info(`Starting change number to ${newNumber} (voice: ${voice})`);
1138
+ (0, validators_1.validatePhoneNumber)(newNumber);
672
1139
  const params = {
673
1140
  account: this.account,
674
1141
  number: newNumber,
@@ -676,22 +1143,28 @@ class SignalCli extends events_1.EventEmitter {
676
1143
  };
677
1144
  if (captcha)
678
1145
  params.captcha = captcha;
679
- const result = await this.sendJsonRpcRequest('startChangeNumber', params);
680
- return {
681
- session: result.session,
682
- newNumber,
683
- challenge: result.challenge
684
- };
1146
+ await this.sendJsonRpcRequest('startChangeNumber', params);
685
1147
  }
686
1148
  /**
687
- * Finish the phone number change process with verification code.
688
- * @param verificationCode - The verification code received via SMS/voice
689
- * @param pin - Registration lock PIN if enabled
1149
+ * Complete the phone number change process.
1150
+ * Verifies the code received via SMS or voice and changes your account to the new number.
1151
+ * Must be called after startChangeNumber().
1152
+ *
1153
+ * @param newNumber - The new phone number (same as startChangeNumber)
1154
+ * @param verificationCode - The verification code received via SMS or voice
1155
+ * @param pin - Optional registration lock PIN if one was set
1156
+ * @throws {Error} If verification fails or incorrect PIN
690
1157
  */
691
- async finishChangeNumber(verificationCode, pin) {
1158
+ async finishChangeNumber(newNumber, verificationCode, pin) {
1159
+ this.logger.info(`Finishing change number to ${newNumber}`);
1160
+ (0, validators_1.validatePhoneNumber)(newNumber);
1161
+ if (!verificationCode || verificationCode.trim().length === 0) {
1162
+ throw new Error('Verification code is required');
1163
+ }
692
1164
  const params = {
693
1165
  account: this.account,
694
- code: verificationCode
1166
+ number: newNumber,
1167
+ verificationCode
695
1168
  };
696
1169
  if (pin)
697
1170
  params.pin = pin;
@@ -857,6 +1330,40 @@ class SignalCli extends events_1.EventEmitter {
857
1330
  };
858
1331
  }
859
1332
  }
1333
+ /**
1334
+ * Set or update the username for this account.
1335
+ * Helper method that wraps updateAccount() for simpler username management.
1336
+ *
1337
+ * @param username - The username to set (without discriminator)
1338
+ * @returns Account update result with username and link
1339
+ *
1340
+ * @example
1341
+ * ```typescript
1342
+ * const result = await signal.setUsername('myusername');
1343
+ * console.log(`Username: ${result.username}`);
1344
+ * console.log(`Link: ${result.usernameLink}`);
1345
+ * ```
1346
+ */
1347
+ async setUsername(username) {
1348
+ return this.updateAccount({ username });
1349
+ }
1350
+ /**
1351
+ * Delete the current username from this account.
1352
+ * Helper method that wraps updateAccount() for simpler username deletion.
1353
+ *
1354
+ * @returns Account update result
1355
+ *
1356
+ * @example
1357
+ * ```typescript
1358
+ * const result = await signal.deleteUsername();
1359
+ * if (result.success) {
1360
+ * console.log('Username deleted successfully');
1361
+ * }
1362
+ * ```
1363
+ */
1364
+ async deleteUsername() {
1365
+ return this.updateAccount({ deleteUsername: true });
1366
+ }
860
1367
  /**
861
1368
  * Get raw attachment data as base64 string.
862
1369
  * @param options Attachment retrieval options
@@ -963,5 +1470,96 @@ class SignalCli extends events_1.EventEmitter {
963
1470
  const result = await this.sendJsonRpcRequest('listAccounts');
964
1471
  return result.accounts || [];
965
1472
  }
1473
+ /**
1474
+ * Extract profile information from a contact.
1475
+ * Parses givenName, familyName, mobileCoinAddress from profile data.
1476
+ *
1477
+ * @param contact - The contact object to parse
1478
+ * @returns Enhanced contact with extracted profile fields
1479
+ *
1480
+ * @example
1481
+ * ```typescript
1482
+ * const contacts = await signal.listContacts();
1483
+ * const enriched = signal.parseContactProfile(contacts[0]);
1484
+ * console.log(enriched.givenName, enriched.familyName);
1485
+ * ```
1486
+ */
1487
+ parseContactProfile(contact) {
1488
+ // signal-cli already provides these fields if available
1489
+ // This method normalizes and validates the data
1490
+ return {
1491
+ ...contact,
1492
+ givenName: contact.givenName || undefined,
1493
+ familyName: contact.familyName || undefined,
1494
+ mobileCoinAddress: contact.mobileCoinAddress || undefined,
1495
+ profileName: contact.profileName ||
1496
+ (contact.givenName && contact.familyName
1497
+ ? `${contact.givenName} ${contact.familyName}`
1498
+ : contact.givenName || contact.familyName),
1499
+ };
1500
+ }
1501
+ /**
1502
+ * Extract group membership details.
1503
+ * Parses pendingMembers, bannedMembers, inviteLink from group data.
1504
+ *
1505
+ * @param group - The group info to parse
1506
+ * @returns Enhanced group with extracted membership fields
1507
+ *
1508
+ * @example
1509
+ * ```typescript
1510
+ * const groups = await signal.listGroups();
1511
+ * const enriched = signal.parseGroupDetails(groups[0]);
1512
+ * console.log(enriched.pendingMembers, enriched.bannedMembers);
1513
+ * ```
1514
+ */
1515
+ parseGroupDetails(group) {
1516
+ return {
1517
+ ...group,
1518
+ // Normalize inviteLink field
1519
+ inviteLink: group.groupInviteLink || group.inviteLink,
1520
+ groupInviteLink: group.groupInviteLink || group.inviteLink,
1521
+ // Ensure arrays exist
1522
+ pendingMembers: group.pendingMembers || [],
1523
+ banned: group.banned || [],
1524
+ requestingMembers: group.requestingMembers || [],
1525
+ admins: group.admins || [],
1526
+ members: group.members || [],
1527
+ };
1528
+ }
1529
+ /**
1530
+ * Get enriched contacts list with parsed profile information.
1531
+ *
1532
+ * @returns Array of contacts with full profile data
1533
+ *
1534
+ * @example
1535
+ * ```typescript
1536
+ * const contacts = await signal.getContactsWithProfiles();
1537
+ * contacts.forEach(c => {
1538
+ * console.log(`${c.givenName} ${c.familyName} - ${c.mobileCoinAddress}`);
1539
+ * });
1540
+ * ```
1541
+ */
1542
+ async getContactsWithProfiles() {
1543
+ const contacts = await this.listContacts();
1544
+ return contacts.map(c => this.parseContactProfile(c));
1545
+ }
1546
+ /**
1547
+ * Get enriched groups list with parsed membership details.
1548
+ *
1549
+ * @param options - List groups options
1550
+ * @returns Array of groups with full membership data
1551
+ *
1552
+ * @example
1553
+ * ```typescript
1554
+ * const groups = await signal.getGroupsWithDetails();
1555
+ * groups.forEach(g => {
1556
+ * console.log(`${g.name}: ${g.members.length} members, ${g.pendingMembers.length} pending`);
1557
+ * });
1558
+ * ```
1559
+ */
1560
+ async getGroupsWithDetails(options = {}) {
1561
+ const groups = await this.listGroupsDetailed(options);
1562
+ return groups.map(g => this.parseGroupDetails(g));
1563
+ }
966
1564
  }
967
1565
  exports.SignalCli = SignalCli;