@massalabs/gossip-sdk 0.0.2-dev.20260410095334 → 0.0.2-dev.20260410113627

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 CHANGED
@@ -293,9 +293,6 @@ const utils = sdk.utils;
293
293
  const result = utils.validateUserId(userId);
294
294
  if (!result.valid) console.error(result.error);
295
295
 
296
- // Validate username format
297
- const result = utils.validateUsername(username);
298
-
299
296
  // Encode/decode user IDs
300
297
  const encoded = utils.encodeUserId(rawBytes);
301
298
  const decoded = utils.decodeUserId(encodedString);
package/dist/gossip.d.ts CHANGED
@@ -155,8 +155,6 @@ declare class GossipSdk {
155
155
  interface SdkUtils {
156
156
  /** Validate a user ID format */
157
157
  validateUserId(userId: string): ValidationResult;
158
- /** Validate a username format */
159
- validateUsername(username: string): ValidationResult;
160
158
  /** Encode raw bytes to user ID string */
161
159
  encodeUserId(rawId: Uint8Array): string;
162
160
  /** Decode user ID string to raw bytes */
package/dist/gossip.js CHANGED
@@ -48,7 +48,7 @@ import { AuthService } from './services/auth.js';
48
48
  import { ProfileService } from './services/profile.js';
49
49
  import { ContactService } from './services/contact.js';
50
50
  import { SelfMessageService } from './services/selfMessage.js';
51
- import { validateUserIdFormat, validateUsernameFormat, } from './utils/validation.js';
51
+ import { validateUserIdFormat, } from './utils/validation.js';
52
52
  import { QueueManager } from './utils/queue.js';
53
53
  import { encodeUserId, decodeUserId } from './utils/userId.js';
54
54
  import { MessageStatus } from './db/index.js';
@@ -463,7 +463,6 @@ class GossipSdk {
463
463
  get utils() {
464
464
  return {
465
465
  validateUserId: validateUserIdFormat,
466
- validateUsername: validateUsernameFormat,
467
466
  encodeUserId,
468
467
  decodeUserId,
469
468
  };
@@ -45,6 +45,14 @@ export declare class MessageService {
45
45
  private processingContacts;
46
46
  private isFetchingMessages;
47
47
  private queries;
48
+ /**
49
+ * Message ids currently being sent via `sendMessageFastPath`. The row is
50
+ * WAITING_SESSION in the DB while the fast path runs INSERT + encrypt +
51
+ * POST in parallel. Without this guard, a concurrent `stateUpdate` could
52
+ * pick up the same WAITING_SESSION row via `processSendQueueForContact`
53
+ * and send a duplicate.
54
+ */
55
+ private inFlightFastPath;
48
56
  /** Emit MESSAGE_RECEIVED with a Message that may not have a DB id yet */
49
57
  private emitMessageReceived;
50
58
  constructor(messageProtocol: IMessageProtocol, session: SessionModule, eventEmitter: SdkEventEmitter, config: SdkConfig | undefined, queries: Queries);
@@ -66,6 +74,22 @@ export declare class MessageService {
66
74
  findMessageBySeeker(seeker: Uint8Array, ownerUserId: string): Promise<Message | undefined>;
67
75
  private acknowledgeMessages;
68
76
  sendMessage(message: Message): Promise<SendMessageResult>;
77
+ /**
78
+ * Happy-path send: peer session is Active, so we can encrypt locally,
79
+ * fire the network POST, and INSERT the row in parallel. The three
80
+ * pieces are independent up until the final `UPDATE → SENT`, which
81
+ * needs both the row id (from INSERT) and a successful network ack.
82
+ *
83
+ * ┌── INSERT (~440 ms) ───────────────┐
84
+ * │ ├── UPDATE → SENT (background)
85
+ * └── encrypt (~150 ms) → POST (~620 ms)
86
+ * │
87
+ * └── emit MESSAGE_SENT
88
+ *
89
+ * Returns `null` if the fast path can't run (encrypt declined, network
90
+ * threw, etc.) so the caller can fall back to the slow path.
91
+ */
92
+ private sendMessageFastPath;
69
93
  private serializeMessage;
70
94
  resendMessages(messages: Map<string, Message[]>): Promise<void>;
71
95
  /**
@@ -202,6 +202,19 @@ export class MessageService {
202
202
  writable: true,
203
203
  value: void 0
204
204
  });
205
+ /**
206
+ * Message ids currently being sent via `sendMessageFastPath`. The row is
207
+ * WAITING_SESSION in the DB while the fast path runs INSERT + encrypt +
208
+ * POST in parallel. Without this guard, a concurrent `stateUpdate` could
209
+ * pick up the same WAITING_SESSION row via `processSendQueueForContact`
210
+ * and send a duplicate.
211
+ */
212
+ Object.defineProperty(this, "inFlightFastPath", {
213
+ enumerable: true,
214
+ configurable: true,
215
+ writable: true,
216
+ value: new Set()
217
+ });
205
218
  this.messageProtocol = messageProtocol;
206
219
  this.session = session;
207
220
  this.eventEmitter = eventEmitter;
@@ -297,42 +310,44 @@ export class MessageService {
297
310
  * Add a message to SQLite and update the corresponding discussion.
298
311
  */
299
312
  async addMessageAndUpdateDiscussion(message) {
300
- const messageId = await this.queries.messages.insert({
301
- messageId: message.messageId,
302
- ownerUserId: message.ownerUserId,
303
- contactUserId: message.contactUserId,
304
- content: message.content,
305
- serializedContent: message.serializedContent,
306
- type: message.type,
307
- direction: message.direction,
308
- status: message.status,
309
- timestamp: message.timestamp,
310
- metadata: serializeMetadata(message.metadata),
311
- seeker: message.seeker,
312
- replyTo: serializeReplyTo(message.replyTo),
313
- forwardOf: serializeForwardOf(message.forwardOf),
314
- deleteOf: serializeDeleteOf(message.deleteOf),
315
- editOf: serializeEditOf(message.editOf),
316
- reactionOf: serializeReactionOf(message.reactionOf),
317
- encryptedMessage: message.encryptedMessage,
318
- whenToSend: message.whenToSend,
319
- });
320
- const discussion = await this.queries.discussions.getByOwnerAndContact(message.ownerUserId, message.contactUserId);
321
- if (discussion &&
322
- message.type !== MessageType.KEEP_ALIVE &&
323
- message.type !== MessageType.REACTION &&
324
- message.type !== MessageType.RETENTION_POLICY) {
325
- await this.queries.discussions.updateById(discussion.id, {
326
- lastMessageId: messageId,
327
- lastMessageContent: message.content,
328
- lastMessageTimestamp: message.timestamp,
329
- updatedAt: new Date(),
313
+ return this.queries.conn.withTransaction(async () => {
314
+ const messageId = await this.queries.messages.insert({
315
+ messageId: message.messageId,
316
+ ownerUserId: message.ownerUserId,
317
+ contactUserId: message.contactUserId,
318
+ content: message.content,
319
+ serializedContent: message.serializedContent,
320
+ type: message.type,
321
+ direction: message.direction,
322
+ status: message.status,
323
+ timestamp: message.timestamp,
324
+ metadata: serializeMetadata(message.metadata),
325
+ seeker: message.seeker,
326
+ replyTo: serializeReplyTo(message.replyTo),
327
+ forwardOf: serializeForwardOf(message.forwardOf),
328
+ deleteOf: serializeDeleteOf(message.deleteOf),
329
+ editOf: serializeEditOf(message.editOf),
330
+ reactionOf: serializeReactionOf(message.reactionOf),
331
+ encryptedMessage: message.encryptedMessage,
332
+ whenToSend: message.whenToSend,
330
333
  });
331
- if (message.direction === MessageDirection.INCOMING) {
332
- await this.queries.discussions.incrementUnreadCount(discussion.id);
334
+ const discussion = await this.queries.discussions.getByOwnerAndContact(message.ownerUserId, message.contactUserId);
335
+ if (discussion &&
336
+ message.type !== MessageType.KEEP_ALIVE &&
337
+ message.type !== MessageType.REACTION &&
338
+ message.type !== MessageType.RETENTION_POLICY) {
339
+ await this.queries.discussions.updateById(discussion.id, {
340
+ lastMessageId: messageId,
341
+ lastMessageContent: message.content,
342
+ lastMessageTimestamp: message.timestamp,
343
+ updatedAt: new Date(),
344
+ });
345
+ if (message.direction === MessageDirection.INCOMING) {
346
+ await this.queries.discussions.incrementUnreadCount(discussion.id);
347
+ }
333
348
  }
334
- }
335
- return messageId;
349
+ return messageId;
350
+ });
336
351
  }
337
352
  async decryptMessages(encrypted) {
338
353
  const log = logger.forMethod('decryptMessages');
@@ -647,7 +662,23 @@ export class MessageService {
647
662
  : undefined;
648
663
  message.messageId = randomMessageId;
649
664
  }
650
- // Add message as WAITING_SESSION
665
+ // Fast path: if the peer's session is already Active we can encrypt
666
+ // and ship the message in parallel with the local INSERT instead of
667
+ // waiting for `addMessageAndUpdateDiscussion` to commit before
668
+ // `processSendQueueForContact` even starts. The user-perceived
669
+ // latency drops from `INSERT + encrypt + POST` (sequential) to
670
+ // `max(INSERT, encrypt + POST)` — ~440 ms saved on prod where the
671
+ // network round-trip dominates.
672
+ const sessionStatus = this.session.peerSessionStatus(peerId);
673
+ if (sessionStatus === SessionStatus.Active) {
674
+ const fastPathResult = await this.sendMessageFastPath(message, peerId);
675
+ if (fastPathResult) {
676
+ return fastPathResult;
677
+ }
678
+ // Fast path bailed (encrypt returned null, etc.) — fall back to
679
+ // the slow path below.
680
+ }
681
+ // Slow path: INSERT first, let stateUpdate handle the encrypt + send.
651
682
  let messageId;
652
683
  try {
653
684
  messageId = await this.addMessageAndUpdateDiscussion({
@@ -676,6 +707,173 @@ export class MessageService {
676
707
  message: queuedMessage,
677
708
  };
678
709
  }
710
+ /**
711
+ * Happy-path send: peer session is Active, so we can encrypt locally,
712
+ * fire the network POST, and INSERT the row in parallel. The three
713
+ * pieces are independent up until the final `UPDATE → SENT`, which
714
+ * needs both the row id (from INSERT) and a successful network ack.
715
+ *
716
+ * ┌── INSERT (~440 ms) ───────────────┐
717
+ * │ ├── UPDATE → SENT (background)
718
+ * └── encrypt (~150 ms) → POST (~620 ms)
719
+ * │
720
+ * └── emit MESSAGE_SENT
721
+ *
722
+ * Returns `null` if the fast path can't run (encrypt declined, network
723
+ * threw, etc.) so the caller can fall back to the slow path.
724
+ */
725
+ async sendMessageFastPath(message, peerId) {
726
+ const log = logger.forMethod('sendMessageFastPath');
727
+ // 1. Serialize synchronously — needed before encrypt.
728
+ const serializeResult = await this.serializeMessage(message);
729
+ if (!serializeResult.success) {
730
+ log.error('failed to serialize message for fast path', {
731
+ error: serializeResult.error,
732
+ });
733
+ return null;
734
+ }
735
+ const serializedContent = serializeResult.data;
736
+ // 2. Kick off the local INSERT and the encrypt in parallel.
737
+ // encrypt is fast (~150 ms with rayon) and doesn't depend on
738
+ // the row id; INSERT is slow (~440 ms) and only the row id is
739
+ // later needed for the SENT update.
740
+ const insertPromise = this.addMessageAndUpdateDiscussion({
741
+ ...message,
742
+ status: MessageStatus.WAITING_SESSION,
743
+ })
744
+ .then(id => {
745
+ this.inFlightFastPath.add(id);
746
+ return id;
747
+ })
748
+ .catch(error => {
749
+ log.error('addMessageAndUpdateDiscussion failed in fast path', {
750
+ error,
751
+ });
752
+ return null;
753
+ });
754
+ let sendOutput;
755
+ try {
756
+ sendOutput = await this.session.sendMessage(peerId, serializedContent);
757
+ }
758
+ catch (error) {
759
+ log.error('session.sendMessage threw in fast path, falling back', {
760
+ error,
761
+ });
762
+ sendOutput = undefined;
763
+ }
764
+ if (!sendOutput) {
765
+ // Session became inactive between status check and encrypt.
766
+ // Wait for the INSERT to land then bail out so the slow path
767
+ // can take over.
768
+ const messageId = await insertPromise;
769
+ if (messageId === null) {
770
+ return {
771
+ success: false,
772
+ error: 'Failed to add message to database',
773
+ };
774
+ }
775
+ log.info('encrypt returned null in fast path, falling back to slow path', { messageId });
776
+ this.inFlightFastPath.delete(messageId);
777
+ void this.refreshService?.stateUpdate();
778
+ return {
779
+ success: true,
780
+ message: {
781
+ ...message,
782
+ id: messageId,
783
+ status: MessageStatus.WAITING_SESSION,
784
+ },
785
+ };
786
+ }
787
+ // 3. Encrypt succeeded. Network POST runs in parallel with the
788
+ // still-in-flight INSERT.
789
+ const networkPromise = this.messageProtocol
790
+ .sendMessage({
791
+ seeker: sendOutput.seeker,
792
+ ciphertext: sendOutput.data,
793
+ })
794
+ .then(() => true)
795
+ .catch(error => {
796
+ log.error('network send failed in fast path', { error });
797
+ return false;
798
+ });
799
+ const [messageId, networkOk] = await Promise.all([
800
+ insertPromise,
801
+ networkPromise,
802
+ ]);
803
+ if (messageId === null) {
804
+ return {
805
+ success: false,
806
+ error: 'Failed to add message to database',
807
+ };
808
+ }
809
+ if (!networkOk) {
810
+ // Network failed: persist READY so the next retry doesn't
811
+ // re-encrypt, and let stateUpdate pick it up.
812
+ this.inFlightFastPath.delete(messageId);
813
+ await this.queries.messages.updateById(messageId, {
814
+ status: MessageStatus.READY,
815
+ encryptedMessage: sendOutput.data,
816
+ seeker: sendOutput.seeker,
817
+ whenToSend: new Date(Date.now() + this.config.messages.retryDelayMs),
818
+ serializedContent,
819
+ });
820
+ void this.refreshService?.stateUpdate();
821
+ return {
822
+ success: true,
823
+ message: {
824
+ ...message,
825
+ id: messageId,
826
+ status: MessageStatus.READY,
827
+ },
828
+ };
829
+ }
830
+ // 4. Both succeeded. Race-check then UPDATE → SENT.
831
+ const latestRow = await this.queries.messages.getById(messageId);
832
+ if (!latestRow || latestRow.status !== MessageStatus.WAITING_SESSION) {
833
+ log.debug('message gone or status changed during fast-path send, skipping SENT update', { messageId, currentStatus: latestRow?.status });
834
+ this.inFlightFastPath.delete(messageId);
835
+ return {
836
+ success: true,
837
+ message: {
838
+ ...message,
839
+ id: messageId,
840
+ status: MessageStatus.SENT,
841
+ },
842
+ };
843
+ }
844
+ await this.queries.messages.updateById(messageId, {
845
+ status: MessageStatus.SENT,
846
+ seeker: sendOutput.seeker,
847
+ encryptedMessage: null,
848
+ serializedContent: null,
849
+ whenToSend: null,
850
+ });
851
+ this.inFlightFastPath.delete(messageId);
852
+ const isControlMessage = !!(message.deleteOf || message.editOf);
853
+ if (!isControlMessage) {
854
+ try {
855
+ this.eventEmitter.emit(SdkEventType.MESSAGE_SENT, {
856
+ ...message,
857
+ id: messageId,
858
+ status: MessageStatus.SENT,
859
+ });
860
+ }
861
+ catch (error) {
862
+ log.error('failed to emit message sent event from fast path', {
863
+ messageId,
864
+ error,
865
+ });
866
+ }
867
+ }
868
+ return {
869
+ success: true,
870
+ message: {
871
+ ...message,
872
+ id: messageId,
873
+ status: MessageStatus.SENT,
874
+ },
875
+ };
876
+ }
679
877
  async serializeMessage(message) {
680
878
  const log = logger.forMethod('serializeMessage');
681
879
  if (!message.messageId &&
@@ -853,11 +1051,17 @@ export class MessageService {
853
1051
  for (const msg of pendingMessages) {
854
1052
  if (!msg.id)
855
1053
  continue;
856
- let currentStatus = msg.status;
1054
+ if (this.inFlightFastPath.has(msg.id))
1055
+ continue;
1056
+ const currentStatus = msg.status;
857
1057
  let encryptedMessage = msg.encryptedMessage;
858
1058
  let seeker = msg.seeker;
859
- let whenToSend = msg.whenToSend;
860
- // If the sessions is saturated it can't send messages on session manager
1059
+ const whenToSend = msg.whenToSend;
1060
+ // Happy path: WAITING_SESSION + Active session.
1061
+ // Encrypt → network → SENT directly, skipping the intermediate
1062
+ // READY SQL write. The READY block below still handles retries
1063
+ // (delayed sends, post-failure retries), where the encrypted
1064
+ // bytes need to survive a restart.
861
1065
  if (currentStatus === MessageStatus.WAITING_SESSION &&
862
1066
  sessionStatus === SessionStatus.Active) {
863
1067
  let serializedContent = msg.serializedContent;
@@ -883,22 +1087,73 @@ export class MessageService {
883
1087
  }
884
1088
  encryptedMessage = sendOutput.data;
885
1089
  seeker = sendOutput.seeker;
886
- whenToSend = new Date();
1090
+ // Try the network synchronously. On success, skip READY entirely
1091
+ // and write SENT in the background. On failure, fall back to
1092
+ // persisting READY so the next retry doesn't have to re-encrypt.
1093
+ try {
1094
+ await this.messageProtocol.sendMessage({
1095
+ seeker,
1096
+ ciphertext: encryptedMessage,
1097
+ });
1098
+ }
1099
+ catch (error) {
1100
+ log.error('network send failed for fresh message', {
1101
+ messageId: msg.id,
1102
+ error,
1103
+ });
1104
+ await this.queries.messages.updateById(msg.id, {
1105
+ status: MessageStatus.READY,
1106
+ encryptedMessage,
1107
+ seeker,
1108
+ whenToSend: new Date(Date.now() + this.config.messages.retryDelayMs),
1109
+ serializedContent,
1110
+ });
1111
+ continue;
1112
+ }
1113
+ // Network success. Race-check: most resets only touch
1114
+ // READY/SENT rows so they leave us alone, but the discussion
1115
+ // could have been deleted while we were on the wire — bail
1116
+ // out if the row is gone or has moved to a non-WAITING state.
1117
+ const latestRow = await this.queries.messages.getById(msg.id);
1118
+ if (!latestRow ||
1119
+ latestRow.status !== MessageStatus.WAITING_SESSION) {
1120
+ log.debug('message gone or status changed during network send, skipping SENT update', {
1121
+ messageId: msg.id,
1122
+ currentStatus: latestRow?.status,
1123
+ });
1124
+ continue;
1125
+ }
887
1126
  await this.queries.messages.updateById(msg.id, {
888
- status: MessageStatus.READY,
889
- encryptedMessage,
1127
+ status: MessageStatus.SENT,
890
1128
  seeker,
891
- whenToSend,
892
- serializedContent,
1129
+ encryptedMessage: null,
1130
+ serializedContent: null,
1131
+ whenToSend: null,
893
1132
  });
894
- currentStatus = MessageStatus.READY;
895
- log.debug('message updated to READY', {
1133
+ sentCount++;
1134
+ log.debug('message sent (skipped READY)', {
896
1135
  messageId: msg.id,
897
- status: currentStatus,
1136
+ status: MessageStatus.SENT,
898
1137
  content: msg.content,
899
1138
  type: msg.type,
900
1139
  direction: msg.direction,
901
1140
  });
1141
+ const isControlMessage = !!(msg.deleteOf || msg.editOf);
1142
+ if (!isControlMessage) {
1143
+ try {
1144
+ this.eventEmitter.emit(SdkEventType.MESSAGE_SENT, {
1145
+ ...msg,
1146
+ status: MessageStatus.SENT,
1147
+ });
1148
+ }
1149
+ catch (error) {
1150
+ log.error('failed to emit message sent event', {
1151
+ messageId: msg.id,
1152
+ error,
1153
+ });
1154
+ }
1155
+ }
1156
+ continue;
902
1157
  }
903
1158
  if (currentStatus === MessageStatus.READY) {
904
1159
  const sendAt = whenToSend ?? new Date();
@@ -6,7 +6,6 @@
6
6
  */
7
7
  import { type UserProfile } from '../db/index.js';
8
8
  import { Queries } from '../db/queries/index.js';
9
- import { type ValidationResult } from '../utils/validation.js';
10
9
  export declare class ProfileService {
11
10
  private queries;
12
11
  constructor(queries: Queries);
@@ -16,7 +15,6 @@ export declare class ProfileService {
16
15
  getCount(): Promise<number>;
17
16
  save(profile: UserProfile): Promise<void>;
18
17
  delete(userId: string): Promise<void>;
19
- validateUsername(username: string): Promise<ValidationResult>;
20
18
  isUsernameTaken(username: string, excludeUserId?: string): Promise<boolean>;
21
19
  createOrUpdate(username: string, userId: string, security: UserProfile['security'], session: Uint8Array): Promise<UserProfile>;
22
20
  }
@@ -5,7 +5,6 @@
5
5
  * Only requires Queries — no session needed, so it can be created at init() time.
6
6
  */
7
7
  import { rowToUserProfile, userProfileToRow, } from '../db/queries/index.js';
8
- import { validateUsernameFormatAndAvailability, } from '../utils/validation.js';
9
8
  export class ProfileService {
10
9
  constructor(queries) {
11
10
  Object.defineProperty(this, "queries", {
@@ -37,9 +36,6 @@ export class ProfileService {
37
36
  delete(userId) {
38
37
  return this.queries.userProfiles.delete(userId);
39
38
  }
40
- validateUsername(username) {
41
- return validateUsernameFormatAndAvailability(username, this.queries);
42
- }
43
39
  async isUsernameTaken(username, excludeUserId) {
44
40
  const match = excludeUserId
45
41
  ? await this.queries.userProfiles.getByUsernameLowerExcluding(username, excludeUserId)
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Functions for validating user input like usernames, passwords, and user IDs.
5
5
  */
6
- import { Queries } from '../db/queries/index.js';
7
6
  export type ValidationResult = {
8
7
  valid: true;
9
8
  error?: never;
@@ -18,28 +17,13 @@ export type ValidationResult = {
18
17
  * @returns Validation result
19
18
  */
20
19
  export declare function validatePassword(value: string): ValidationResult;
21
- /**
22
- * Validate a username format (without checking availability)
23
- *
24
- * @param value - The username to validate
25
- * @returns Validation result
26
- */
27
- export declare function validateUsernameFormat(value: string): ValidationResult;
28
20
  /**
29
21
  * Validate a username is available (not already in use)
30
22
  *
31
- * @param value - The username to check
32
- * @returns Validation result
33
- */
34
- export declare function validateUsernameAvailability(value: string, queries: Queries): Promise<ValidationResult>;
35
- /**
36
- * Validate a username format and availability
37
- *
38
- * @param value - The username to validate
39
- * @param db - Database instance
40
- * @returns Validation result
41
- */
42
- export declare function validateUsernameFormatAndAvailability(value: string, queries: Queries): Promise<ValidationResult>;
23
+
24
+
25
+
26
+
43
27
  /**
44
28
  * Validate a user ID format (Bech32 gossip1... format)
45
29
  *
@@ -22,72 +22,13 @@ export function validatePassword(value) {
22
22
  }
23
23
  return { valid: true };
24
24
  }
25
- /**
26
- * Validate a username format (without checking availability)
27
- *
28
- * @param value - The username to validate
29
- * @returns Validation result
30
- */
31
- export function validateUsernameFormat(value) {
32
- const trimmed = value.trim();
33
- if (!trimmed) {
34
- return { valid: false, error: 'Username is required' };
35
- }
36
- // Disallow any whitespace inside the username (single token only)
37
- if (/\s/.test(trimmed)) {
38
- return {
39
- valid: false,
40
- error: 'Username cannot contain spaces',
41
- };
42
- }
43
- if (trimmed.length < 3) {
44
- return {
45
- valid: false,
46
- error: 'Username must be at least 3 characters long',
47
- };
48
- }
49
- return { valid: true };
50
- }
51
25
  /**
52
26
  * Validate a username is available (not already in use)
53
27
  *
54
- * @param value - The username to check
55
- * @returns Validation result
56
- */
57
- export async function validateUsernameAvailability(value, queries) {
58
- try {
59
- const existingProfile = await queries.userProfiles.getByUsernameLower(value);
60
- if (existingProfile) {
61
- return {
62
- valid: false,
63
- error: 'This username is already in use. Please choose another.',
64
- };
65
- }
66
- return { valid: true };
67
- }
68
- catch (error) {
69
- return {
70
- valid: false,
71
- error: error instanceof Error
72
- ? error.message
73
- : 'Unable to verify username availability. Please try again.',
74
- };
75
- }
76
- }
77
- /**
78
- * Validate a username format and availability
79
- *
80
- * @param value - The username to validate
81
- * @param db - Database instance
82
- * @returns Validation result
83
- */
84
- export async function validateUsernameFormatAndAvailability(value, queries) {
85
- const result = validateUsernameFormat(value);
86
- if (!result.valid) {
87
- return result;
88
- }
89
- return await validateUsernameAvailability(value, queries);
90
- }
28
+
29
+
30
+
31
+
91
32
  /**
92
33
  * Validate a user ID format (Bech32 gossip1... format)
93
34
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massalabs/gossip-sdk",
3
- "version": "0.0.2-dev.20260410095334",
3
+ "version": "0.0.2-dev.20260410113627",
4
4
  "description": "Gossip SDK for automation, chatbot, and integration use cases",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",