UNPKG

@xmtp/node-sdk

Version:

XMTP Node client SDK for interacting with XMTP networks

1,493 lines (1,478 loc) 66 kB
import { GroupUpdatedCodec, ContentTypeGroupUpdated } from '@xmtp/content-type-group-updated'; import { ContentTypeText, TextCodec } from '@xmtp/content-type-text'; import { generateInboxId as generateInboxId$1, getInboxIdForIdentifier as getInboxIdForIdentifier$1, createClient as createClient$1, revokeInstallationsSignatureRequest, applySignatureRequest, inboxStateFromInboxIds, verifySignedWithPublicKey, isAddressAuthorized, isInstallationAuthorized } from '@xmtp/node-bindings'; export { ConsentEntityType, ConsentState, ConversationType, DeliveryStatus, GroupMember, GroupMembershipState, GroupMessageKind, GroupMetadata, GroupPermissions, GroupPermissionsOptions, IdentifierKind, LogLevel, MetadataField, PermissionLevel, PermissionPolicy, PermissionUpdateType, SignatureRequestHandle, SortDirection } from '@xmtp/node-bindings'; import { ContentTypeId } from '@xmtp/content-type-primitives'; import { join } from 'node:path'; import process from 'node:process'; import bindingsVersion from '@xmtp/node-bindings/version.json' with { type: 'json' }; /** * Pre-configured URLs for the XMTP network based on the environment * * @constant * @property {string} local - The local URL for the XMTP network * @property {string} dev - The development URL for the XMTP network * @property {string} production - The production URL for the XMTP network */ const ApiUrls = { local: "http://localhost:5556", dev: "https://grpc.dev.xmtp.network:443", production: "https://grpc.production.xmtp.network:443", }; /** * Pre-configured URLs for the XMTP history sync service based on the environment * * @constant * @property {string} local - The local URL for the XMTP history sync service * @property {string} dev - The development URL for the XMTP history sync service * @property {string} production - The production URL for the XMTP history sync service */ const HistorySyncUrls = { local: "http://localhost:5558", dev: "https://message-history.dev.ephemera.network", production: "https://message-history.production.ephemera.network", }; class AsyncStream { #isDone = false; #resolveNext; #rejectNext; #queue; #error; onReturn; onError; constructor() { this.#queue = []; this.#isDone = false; } #done() { this.#queue = []; this.#resolveNext = undefined; this.#rejectNext = undefined; this.#isDone = true; } get error() { return this.#error; } get isDone() { return this.#isDone; } callback = (error, value) => { if (this.#isDone) { return; } if (error) { this.#error = error; if (this.#rejectNext) { this.#rejectNext(error); this.#done(); this.onError?.(error); } return; } if (this.#resolveNext) { this.#resolveNext({ done: false, value, }); this.#resolveNext = undefined; this.#rejectNext = undefined; } else { this.#queue.push(value); } }; next = () => { if (this.#error) { this.#done(); this.onError?.(this.#error); return Promise.reject(this.#error); } if (this.#queue.length > 0) { return Promise.resolve({ done: false, value: this.#queue.shift(), }); } return new Promise((resolve, reject) => { this.#resolveNext = resolve; this.#rejectNext = reject; }); }; return = (value) => { this.#resolveNext?.({ done: true, value, }); this.onReturn?.(); this.#done(); return { done: true, value, }; }; end = () => this.return(); [Symbol.asyncIterator]() { return this; } } function nsToDate(ns) { return new Date(ns / 1_000_000); } /** * Represents a decoded XMTP message * * This class transforms network messages into a structured format with * content decoding. * * @class * @property {any} content - The decoded content of the message * @property {ContentTypeId} contentType - The content type of the message content * @property {string} conversationId - Unique identifier for the conversation * @property {MessageDeliveryStatus} deliveryStatus - Current delivery status of the message ("unpublished" | "published" | "failed") * @property {string} [fallback] - Optional fallback text for the message * @property {number} [compression] - Optional compression level applied to the message * @property {string} id - Unique identifier for the message * @property {MessageKind} kind - Type of message ("application" | "membership_change") * @property {Record<string, string>} parameters - Additional parameters associated with the message * @property {string} senderInboxId - Identifier for the sender's inbox * @property {Date} sentAt - Timestamp when the message was sent * @property {number} sentAtNs - Timestamp when the message was sent (in nanoseconds) */ class DecodedMessage { #client; content; contentType; conversationId; deliveryStatus; fallback; compression; id; kind; parameters; senderInboxId; sentAt; sentAtNs; constructor(client, message) { this.#client = client; this.id = message.id; this.sentAtNs = message.sentAtNs; this.sentAt = nsToDate(message.sentAtNs); this.conversationId = message.convoId; this.senderInboxId = message.senderInboxId; switch (message.kind) { case 0 /* GroupMessageKind.Application */: this.kind = "application"; break; case 1 /* GroupMessageKind.MembershipChange */: this.kind = "membership_change"; break; // no default } switch (message.deliveryStatus) { case 0 /* DeliveryStatus.Unpublished */: this.deliveryStatus = "unpublished"; break; case 1 /* DeliveryStatus.Published */: this.deliveryStatus = "published"; break; case 2 /* DeliveryStatus.Failed */: this.deliveryStatus = "failed"; break; // no default } this.contentType = message.content.type ? new ContentTypeId(message.content.type) : undefined; this.parameters = message.content.parameters; this.fallback = message.content.fallback; this.compression = message.content.compression; this.content = undefined; if (this.contentType) { try { this.content = this.#client.decodeContent(message, this.contentType); } catch { this.content = undefined; } } } } class CodecNotFoundError extends Error { constructor(contentType) { super(`Codec not found for "${contentType.toString()}" content type`); } } class InboxReassignError extends Error { constructor() { super("Unable to create add account signature text, `allowInboxReassign` must be true"); } } class AccountAlreadyAssociatedError extends Error { constructor(inboxId) { super(`Account already associated with inbox ${inboxId}`); } } class InvalidGroupMembershipChangeError extends Error { constructor(messageId) { super(`Invalid group membership change for message ${messageId}`); } } class MissingContentTypeError extends Error { constructor() { super("Content type is required when sending content other than text"); } } class SignerUnavailableError extends Error { constructor() { super("Signer unavailable, use Client.create to create a client with a signer"); } } class ClientNotInitializedError extends Error { constructor() { super("Client not initialized, use Client.create or Client.build to create a client"); } } /** * Represents a conversation * * This class is not intended to be initialized directly. */ class Conversation { #client; #conversation; #lastMessage; /** * Creates a new conversation instance * * @param client - The client instance managing the conversation * @param conversation - The underlying conversation instance * @param lastMessage - Optional last message in the conversation */ constructor(client, conversation, lastMessage) { this.#client = client; this.#conversation = conversation; this.#lastMessage = lastMessage ? new DecodedMessage(client, lastMessage) : undefined; } /** * Gets the unique identifier for this conversation */ get id() { return this.#conversation.id(); } /** * Gets whether this conversation is currently active */ get isActive() { return this.#conversation.isActive(); } /** * Gets the inbox ID that added this client's inbox to the conversation */ get addedByInboxId() { return this.#conversation.addedByInboxId(); } /** * Gets the timestamp when the conversation was created in nanoseconds */ get createdAtNs() { return this.#conversation.createdAtNs(); } /** * Gets the date when the conversation was created */ get createdAt() { return nsToDate(this.createdAtNs); } /** * Gets the metadata for this conversation * * @returns Promise that resolves with the conversation metadata */ async metadata() { const metadata = await this.#conversation.groupMetadata(); return { creatorInboxId: metadata.creatorInboxId(), conversationType: metadata.conversationType(), }; } /** * Gets the members of this conversation * * @returns Promise that resolves with the conversation members */ async members() { return this.#conversation.listMembers(); } /** * Synchronizes conversation data from the network * * @returns Promise that resolves when synchronization is complete */ async sync() { return this.#conversation.sync(); } /** * Creates a stream for new messages in this conversation * * @param callback - Optional callback function for handling new stream values * @returns Stream instance for new messages */ stream(callback) { const asyncStream = new AsyncStream(); const stream = this.#conversation.stream((error, value) => { let err = error; let message; if (value) { try { message = new DecodedMessage(this.#client, value); } catch (error) { err = error; } } asyncStream.callback(err, message); callback?.(err, message); }); asyncStream.onReturn = stream.end.bind(stream); return asyncStream; } /** * Publishes pending messages that were sent optimistically * * @returns Promise that resolves when publishing is complete */ async publishMessages() { return this.#conversation.publishMessages(); } /** * Prepares a message to be published * * @param content - The content to send * @param contentType - Optional content type of the message content * @returns Promise that resolves with the message ID * @throws {MissingContentTypeError} if content type is required but not provided */ sendOptimistic(content, contentType) { if (typeof content !== "string" && !contentType) { throw new MissingContentTypeError(); } const encodedContent = typeof content === "string" ? this.#client.encodeContent(content, contentType ?? ContentTypeText) : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.#client.encodeContent(content, contentType); return this.#conversation.sendOptimistic(encodedContent); } /** * Publishes a new message * * @param content - The content to send * @param contentType - Optional content type of the message content * @returns Promise that resolves with the message ID after it has been sent * @throws {MissingContentTypeError} if content type is required but not provided */ async send(content, contentType) { if (typeof content !== "string" && !contentType) { throw new MissingContentTypeError(); } const encodedContent = typeof content === "string" ? this.#client.encodeContent(content, contentType ?? ContentTypeText) : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.#client.encodeContent(content, contentType); return this.#conversation.send(encodedContent); } /** * Lists messages in this conversation * * @param options - Optional filtering and pagination options * @returns Promise that resolves with an array of decoded messages */ async messages(options) { const messages = await this.#conversation.findMessages(options); return messages.map((message) => new DecodedMessage(this.#client, message)); } /** * Gets the last message in this conversation * * @returns Promise that resolves with the last message or undefined if none exists */ async lastMessage() { return this.#lastMessage ?? (await this.messages({ limit: 1 }))[0]; } /** * Gets the consent state for this conversation */ get consentState() { return this.#conversation.consentState(); } /** * Updates the consent state for this conversation * * @param consentState - The new consent state to set */ updateConsentState(consentState) { this.#conversation.updateConsentState(consentState); } /** * Gets the message disappearing settings for this conversation * * @returns The current message disappearing settings or undefined if not set */ messageDisappearingSettings() { return this.#conversation.messageDisappearingSettings() ?? undefined; } /** * Updates message disappearing settings for this conversation * * @param fromNs - The timestamp from which messages should start disappearing * @param inNs - The duration after which messages should disappear * @returns Promise that resolves when the update is complete */ async updateMessageDisappearingSettings(fromNs, inNs) { return this.#conversation.updateMessageDisappearingSettings({ fromNs, inNs, }); } /** * Removes message disappearing settings from this conversation * * @returns Promise that resolves when the settings are removed */ async removeMessageDisappearingSettings() { return this.#conversation.removeMessageDisappearingSettings(); } /** * Checks if message disappearing is enabled for this conversation * * @returns Whether message disappearing is enabled */ isMessageDisappearingEnabled() { return this.#conversation.isMessageDisappearingEnabled(); } pausedForVersion() { return this.#conversation.pausedForVersion() ?? undefined; } /** * Retrieves HMAC keys for this conversation * * @returns The HMAC keys for this conversation */ getHmacKeys() { return this.#conversation.getHmacKeys(); } /** * Retrieves information for this conversation to help with debugging * * @returns The debug information for this conversation */ async debugInfo() { return this.#conversation.debugInfo(); } } /** * Represents a direct message conversation between two inboxes * * This class is not intended to be initialized directly. */ class Dm extends Conversation { #client; #conversation; /** * Creates a new direct message conversation instance * * @param client - The client instance managing this direct message conversation * @param conversation - The underlying conversation instance * @param lastMessage - Optional last message in the conversation */ constructor(client, conversation, lastMessage) { super(client, conversation, lastMessage); this.#client = client; this.#conversation = conversation; } /** * Retrieves the inbox ID of the other participant in the DM * * @returns Promise that resolves with the peer's inbox ID */ get peerInboxId() { return this.#conversation.dmPeerInboxId(); } async getDuplicateDms() { const duplicateDms = await this.#conversation.findDuplicateDms(); return duplicateDms.map((dm) => new Dm(this.#client, dm)); } } /** * Represents a group conversation between multiple inboxes * * This class is not intended to be initialized directly. */ class Group extends Conversation { #conversation; /** * Creates a new group conversation instance * * @param client - The client instance managing this group conversation * @param conversation - The underlying conversation object * @param lastMessage - Optional last message in the conversation */ constructor(client, conversation, lastMessage) { super(client, conversation, lastMessage); this.#conversation = conversation; } /** * The name of the group */ get name() { return this.#conversation.groupName(); } /** * Updates the group's name * * @param name The new name for the group */ async updateName(name) { return this.#conversation.updateGroupName(name); } /** * The image URL of the group */ get imageUrl() { return this.#conversation.groupImageUrlSquare(); } /** * Updates the group's image URL * * @param imageUrl The new image URL for the group */ async updateImageUrl(imageUrl) { return this.#conversation.updateGroupImageUrlSquare(imageUrl); } /** * The description of the group */ get description() { return this.#conversation.groupDescription(); } /** * Updates the group's description * * @param description The new description for the group */ async updateDescription(description) { return this.#conversation.updateGroupDescription(description); } /** * The permissions of the group */ get permissions() { const permissions = this.#conversation.groupPermissions(); return { policyType: permissions.policyType(), policySet: permissions.policySet(), }; } /** * Updates a specific permission policy for the group * * @param permissionType The type of permission to update * @param policy The new permission policy * @param metadataField Optional metadata field for the permission */ async updatePermission(permissionType, policy, metadataField) { return this.#conversation.updatePermissionPolicy(permissionType, policy, metadataField); } /** * The list of admins of the group */ get admins() { return this.#conversation.adminList(); } /** * The list of super admins of the group */ get superAdmins() { return this.#conversation.superAdminList(); } /** * Checks if an inbox is an admin of the group * * @param inboxId The inbox ID to check * @returns Boolean indicating if the inbox is an admin */ isAdmin(inboxId) { return this.#conversation.isAdmin(inboxId); } /** * Checks if an inbox is a super admin of the group * * @param inboxId The inbox ID to check * @returns Boolean indicating if the inbox is a super admin */ isSuperAdmin(inboxId) { return this.#conversation.isSuperAdmin(inboxId); } /** * Adds members to the group using identifiers * * @param identifiers Array of member identifiers to add */ async addMembersByIdentifiers(identifiers) { return this.#conversation.addMembers(identifiers); } /** * Adds members to the group using inbox IDs * * @param inboxIds Array of inbox IDs to add */ async addMembers(inboxIds) { return this.#conversation.addMembersByInboxId(inboxIds); } /** * Removes members from the group using identifiers * * @param identifiers Array of member identifiers to remove */ async removeMembersByIdentifiers(identifiers) { return this.#conversation.removeMembers(identifiers); } /** * Removes members from the group using inbox IDs * * @param inboxIds Array of inbox IDs to remove */ async removeMembers(inboxIds) { return this.#conversation.removeMembersByInboxId(inboxIds); } /** * Promotes a group member to admin status * * @param inboxId The inbox ID of the member to promote */ async addAdmin(inboxId) { return this.#conversation.addAdmin(inboxId); } /** * Removes admin status from a group member * * @param inboxId The inbox ID of the admin to demote */ async removeAdmin(inboxId) { return this.#conversation.removeAdmin(inboxId); } /** * Promotes a group member to super admin status * * @param inboxId The inbox ID of the member to promote */ async addSuperAdmin(inboxId) { return this.#conversation.addSuperAdmin(inboxId); } /** * Removes super admin status from a group member * * @param inboxId The inbox ID of the super admin to demote */ async removeSuperAdmin(inboxId) { return this.#conversation.removeSuperAdmin(inboxId); } } /** * Manages conversations * * This class is not intended to be initialized directly. */ class Conversations { #client; #conversations; /** * Creates a new conversations instance * * @param client - The client instance managing the conversations * @param conversations - The underlying conversations instance */ constructor(client, conversations) { this.#client = client; this.#conversations = conversations; } /** * Retrieves a conversation by its ID * * @param id - The conversation ID to look up * @returns The conversation if found, undefined otherwise */ async getConversationById(id) { try { // findGroupById will throw if group is not found const group = this.#conversations.findGroupById(id); const metadata = await group.groupMetadata(); return metadata.conversationType() === "group" ? new Group(this.#client, group) : new Dm(this.#client, group); } catch { return undefined; } } /** * Retrieves a DM by inbox ID * * @param inboxId - The inbox ID to look up * @returns The DM if found, undefined otherwise */ getDmByInboxId(inboxId) { try { // findDmByTargetInboxId will throw if group is not found const group = this.#conversations.findDmByTargetInboxId(inboxId); return new Dm(this.#client, group); } catch { return undefined; } } /** * Retrieves a message by its ID * * @param id - The message ID to look up * @returns The decoded message if found, undefined otherwise */ getMessageById(id) { try { // findMessageById will throw if message is not found const message = this.#conversations.findMessageById(id); return new DecodedMessage(this.#client, message); } catch { return undefined; } } /** * Creates a new group conversation without syncing to the network * * @param options - Optional group creation options * @returns The new group */ newGroupOptimistic(options) { const group = this.#conversations.createGroupOptimistic(options); return new Group(this.#client, group); } /** * Creates a new group conversation with the specified identifiers * * @param identifiers - Array of identifiers for group members * @param options - Optional group creation options * @returns The new group */ async newGroupWithIdentifiers(identifiers, options) { const group = await this.#conversations.createGroup(identifiers, options); const conversation = new Group(this.#client, group); return conversation; } /** * Creates a new group conversation with the specified inbox IDs * * @param inboxIds - Array of inbox IDs for group members * @param options - Optional group creation options * @returns The new group */ async newGroup(inboxIds, options) { const group = await this.#conversations.createGroupByInboxId(inboxIds, options); const conversation = new Group(this.#client, group); return conversation; } /** * Creates a new DM conversation with the specified identifier * * @param identifier - Identifier for the DM recipient * @param options - Optional DM creation options * @returns The new DM */ async newDmWithIdentifier(identifier, options) { const group = await this.#conversations.createDm(identifier, options); const conversation = new Dm(this.#client, group); return conversation; } /** * Creates a new DM conversation with the specified inbox ID * * @param inboxId - Inbox ID for the DM recipient * @param options - Optional DM creation options * @returns The new DM */ async newDm(inboxId, options) { const group = await this.#conversations.createDmByInboxId(inboxId, options); const conversation = new Dm(this.#client, group); return conversation; } /** * Lists all conversations with optional filtering * * @param options - Optional filtering and pagination options * @returns Array of conversations */ async list(options) { const groups = this.#conversations.list(options); const conversations = await Promise.all(groups.map(async (item) => { const metadata = await item.conversation.groupMetadata(); const conversationType = metadata.conversationType(); switch (conversationType) { case "dm": return new Dm(this.#client, item.conversation, item.lastMessage); case "group": return new Group(this.#client, item.conversation, item.lastMessage); default: return undefined; } })); return conversations.filter((conversation) => conversation !== undefined); } /** * Lists all groups with optional filtering * * @param options - Optional filtering and pagination options * @returns Array of groups */ listGroups(options) { const groups = this.#conversations.list({ ...(options ?? {}), conversationType: 1 /* ConversationType.Group */, }); return groups.map((item) => { const conversation = new Group(this.#client, item.conversation, item.lastMessage); return conversation; }); } /** * Lists all DMs with optional filtering * * @param options - Optional filtering and pagination options * @returns Array of DMs */ listDms(options) { const groups = this.#conversations.list({ ...(options ?? {}), conversationType: 0 /* ConversationType.Dm */, }); return groups.map((item) => { const conversation = new Dm(this.#client, item.conversation, item.lastMessage); return conversation; }); } /** * Synchronizes conversations for the current client from the network * * @returns Promise that resolves when sync is complete */ async sync() { return this.#conversations.sync(); } /** * Synchronizes all conversations and messages from the network with optional * consent state filtering * * @param consentStates - Optional array of consent states to filter by * @returns Promise that resolves when sync is complete */ async syncAll(consentStates) { return this.#conversations.syncAllConversations(consentStates); } /** * Creates a stream for new conversations * * @param callback - Optional callback function for handling new stream value * @returns Stream instance for new conversations */ stream(callback) { const asyncStream = new AsyncStream(); const stream = this.#conversations.stream((err, value) => { if (err) { asyncStream.callback(err, undefined); callback?.(err, undefined); return; } value ?.groupMetadata() .then((metadata) => { const conversationType = metadata.conversationType(); let conversation; switch (conversationType) { case "dm": conversation = new Dm(this.#client, value); break; case "group": conversation = new Group(this.#client, value); break; } if (conversation) { asyncStream.callback(null, conversation); callback?.(null, conversation); } }) .catch((error) => { asyncStream.callback(error, undefined); callback?.(error, undefined); }); }); asyncStream.onReturn = stream.end.bind(stream); return asyncStream; } /** * Creates a stream for new group conversations * * @param callback - Optional callback function for handling new stream value * @returns Stream instance for new group conversations */ streamGroups(callback) { const asyncStream = new AsyncStream(); const stream = this.#conversations.stream((error, value) => { let err = error; let group; if (value) { try { group = new Group(this.#client, value); } catch (error) { err = error; } } asyncStream.callback(err, group); callback?.(err, group); }, 1 /* ConversationType.Group */); asyncStream.onReturn = stream.end.bind(stream); return asyncStream; } /** * Creates a stream for new DM conversations * * @param callback - Optional callback function for handling new stream value * @returns Stream instance for new DM conversations */ streamDms(callback) { const asyncStream = new AsyncStream(); const stream = this.#conversations.stream((error, value) => { let err = error; let dm; if (value) { try { dm = new Dm(this.#client, value); } catch (error) { err = error; } } asyncStream.callback(err, dm); callback?.(err, dm); }, 0 /* ConversationType.Dm */); asyncStream.onReturn = stream.end.bind(stream); return asyncStream; } /** * Creates a stream for all new messages * * @param callback - Optional callback function for handling new stream value * @returns Stream instance for new messages */ async streamAllMessages(callback, conversationType, consentStates) { // sync conversations first await this.sync(); const asyncStream = new AsyncStream(); const stream = this.#conversations.streamAllMessages((error, value) => { let err = error; let message; if (value) { try { message = new DecodedMessage(this.#client, value); } catch (error) { err = error; } } asyncStream.callback(err, message); callback?.(err, message); }, conversationType, consentStates); asyncStream.onReturn = stream.end.bind(stream); return asyncStream; } /** * Creates a stream for all new group messages * * @param callback - Optional callback function for handling new stream value * @returns Stream instance for new group messages */ async streamAllGroupMessages(callback, consentStates) { return this.streamAllMessages(callback, 1 /* ConversationType.Group */, consentStates); } /** * Creates a stream for all new DM messages * * @param callback - Optional callback function for handling new stream value * @returns Stream instance for new DM messages */ async streamAllDmMessages(callback, consentStates) { return this.streamAllMessages(callback, 0 /* ConversationType.Dm */, consentStates); } /** * Retrieves HMAC keys for all conversations * * @returns The HMAC keys for all conversations */ hmacKeys() { return this.#conversations.getHmacKeys(); } } /** * Debug information helpers for the client * * This class is not intended to be initialized directly. */ class DebugInformation { #client; #options; constructor(client, options) { this.#client = client; this.#options = options; } apiStatistics() { return this.#client.apiStatistics(); } apiIdentityStatistics() { return this.#client.apiIdentityStatistics(); } apiAggregateStatistics() { return this.#client.apiAggregateStatistics(); } clearAllStatistics() { this.#client.clearAllStatistics(); } uploadDebugArchive(serverUrl) { const env = this.#options?.env || "dev"; const historySyncUrl = this.#options?.historySyncUrl || HistorySyncUrls[env]; return this.#client.uploadDebugArchive(serverUrl || historySyncUrl); } } /** * Manages user preferences and consent states * * This class is not intended to be initialized directly. */ class Preferences { #client; #conversations; /** * Creates a new preferences instance * * @param client - The client instance managing preferences * @param conversations - The underlying conversations instance */ constructor(client, conversations) { this.#client = client; this.#conversations = conversations; } sync() { return this.#client.syncPreferences(); } /** * Retrieves the current inbox state * * @param refreshFromNetwork - Optional flag to force refresh from network * @returns Promise that resolves with the inbox state */ async inboxState(refreshFromNetwork = false) { return this.#client.inboxState(refreshFromNetwork); } /** * Gets the latest inbox state for a specific inbox * * @param inboxId - The inbox ID to get state for * @returns Promise that resolves with the latest inbox state */ async getLatestInboxState(inboxId) { return this.#client.getLatestInboxState(inboxId); } /** * Retrieves inbox state for specific inbox IDs * * @param inboxIds - Array of inbox IDs to get state for * @param refreshFromNetwork - Optional flag to force refresh from network * @returns Promise that resolves with the inbox state for the inbox IDs */ async inboxStateFromInboxIds(inboxIds, refreshFromNetwork) { return this.#client.addressesFromInboxId(refreshFromNetwork ?? false, inboxIds); } /** * Updates consent states for multiple records * * @param consentStates - Array of consent records to update * @returns Promise that resolves when consent states are updated */ async setConsentStates(consentStates) { return this.#client.setConsentStates(consentStates); } /** * Retrieves consent state for a specific entity * * @param entityType - Type of entity to get consent for * @param entity - Entity identifier * @returns Promise that resolves with the consent state */ async getConsentState(entityType, entity) { return this.#client.getConsentState(entityType, entity); } /** * Creates a stream of consent state updates * * @param callback - Optional callback function for handling stream updates * @returns Stream instance for consent updates */ streamConsent(callback) { const asyncStream = new AsyncStream(); const stream = this.#conversations.streamConsent((err, value) => { if (err) { asyncStream.callback(err, undefined); callback?.(err, undefined); return; } asyncStream.callback(null, value); callback?.(null, value); }); asyncStream.onReturn = stream.end.bind(stream); return asyncStream; } /** * Creates a stream of user preference updates * * @param callback - Optional callback function for handling stream updates * @returns Stream instance for preference updates */ streamPreferences(callback) { const asyncStream = new AsyncStream(); const stream = this.#conversations.streamPreferences((err, value) => { if (err) { asyncStream.callback(err, undefined); callback?.(err, undefined); return; } // TODO: remove this once the node bindings type is updated asyncStream.callback(null, value); callback?.(null, value); }); asyncStream.onReturn = stream.end.bind(stream); return asyncStream; } } const generateInboxId = (identifier) => { return generateInboxId$1(identifier); }; const getInboxIdForIdentifier = async (identifier, env = "dev") => { const host = ApiUrls[env]; const isSecure = host.startsWith("https"); return getInboxIdForIdentifier$1(host, isSecure, identifier); }; const createClient = async (identifier, options) => { const env = options?.env || "dev"; const host = options?.apiUrl || ApiUrls[env]; const isSecure = host.startsWith("https"); const inboxId = (await getInboxIdForIdentifier(identifier, env)) || generateInboxId(identifier); const dbPath = options?.dbPath === undefined ? join(process.cwd(), `xmtp-${env}-${inboxId}.db3`) : options.dbPath; const logOptions = { structured: options?.structuredLogging ?? false, level: options?.loggingLevel ?? "off" /* LogLevel.off */, }; const historySyncUrl = options?.historySyncUrl === undefined ? HistorySyncUrls[env] : options.historySyncUrl; const deviceSyncWorkerMode = options?.disableDeviceSync ? "disabled" /* SyncWorkerMode.disabled */ : "enabled" /* SyncWorkerMode.enabled */; return createClient$1(host, isSecure, dbPath, inboxId, identifier, options?.dbEncryptionKey, historySyncUrl, deviceSyncWorkerMode, logOptions); }; const version = `${bindingsVersion.branch}@${bindingsVersion.version} (${bindingsVersion.date})`; /** * Client for interacting with the XMTP network */ class Client { #client; #conversations; #debugInformation; #preferences; #signer; #codecs; #identifier; #options; /** * Creates a new XMTP client instance * * This class is not intended to be initialized directly. * Use `Client.create` or `Client.build` instead. * * @param options - Optional configuration for the client */ constructor(options) { this.#options = options; const codecs = [ new GroupUpdatedCodec(), new TextCodec(), ...(options?.codecs ?? []), ]; this.#codecs = new Map(codecs.map((codec) => [codec.contentType.toString(), codec])); } /** * Initializes the client with the provided identifier * * This is not meant to be called directly. * Use `Client.create` or `Client.build` instead. * * @param identifier - The identifier to initialize the client with */ async init(identifier) { if (this.#client) { return; } this.#identifier = identifier; this.#client = await createClient(identifier, this.#options); const conversations = this.#client.conversations(); this.#conversations = new Conversations(this, conversations); this.#debugInformation = new DebugInformation(this.#client, this.#options); this.#preferences = new Preferences(this.#client, conversations); } /** * Creates a new client instance with a signer * * @param signer - The signer to use for authentication * @param options - Optional configuration for the client * @returns A new client instance */ static async create(signer, options) { const identifier = await signer.getIdentifier(); const client = new Client(options); client.#signer = signer; await client.init(identifier); if (!options?.disableAutoRegister) { await client.register(); } return client; } /** * Creates a new client instance with an identifier * * Clients created with this method must already be registered. * Any methods called that require a signer will throw an error. * * @param identifier - The identifier to use * @param options - Optional configuration for the client * @returns A new client instance */ static async build(identifier, options) { const client = new Client({ ...options, disableAutoRegister: true, }); await client.init(identifier); return client; } /** * Gets the client options */ get options() { return this.#options; } /** * Gets the signer associated with this client */ get signer() { return this.#signer; } /** * Gets the account identifier for this client */ get accountIdentifier() { return this.#identifier; } /** * Gets the inbox ID associated with this client */ get inboxId() { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.inboxId(); } /** * Gets the installation ID for this client */ get installationId() { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.installationId(); } /** * Gets the installation ID bytes for this client */ get installationIdBytes() { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.installationIdBytes(); } /** * Gets whether the client is registered with the XMTP network * * @throws {ClientNotInitializedError} if the client is not initialized */ get isRegistered() { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.isRegistered(); } /** * Gets the conversations manager for this client * * @throws {ClientNotInitializedError} if the client is not initialized */ get conversations() { if (!this.#conversations) { throw new ClientNotInitializedError(); } return this.#conversations; } /** * Gets the debug information helpersfor this client * * @throws {ClientNotInitializedError} if the client is not initialized */ get debugInformation() { if (!this.#debugInformation) { throw new ClientNotInitializedError(); } return this.#debugInformation; } /** * Gets the preferences manager for this client * * @throws {ClientNotInitializedError} if the client is not initialized */ get preferences() { if (!this.#preferences) { throw new ClientNotInitializedError(); } return this.#preferences; } /** * Adds a signature to a signature request using the client's signer (or the * provided signer) * * WARNING: This function should be used with caution. It is only provided * for use in special cases where the provided workflows do not meet the * requirements of an application. * * It is highly recommended to use the `register`, `unsafe_addAccount`, * `removeAccount`, `revokeAllOtherInstallations`, or `revokeInstallations` * methods instead. * * @param signatureRequest - The signature request to add the signature to * @throws {ClientNotInitializedError} if the client is not initialized * @throws {SignerUnavailableError} if no signer is available */ async unsafe_addSignature(signatureRequest, signer) { if (!this.#client) { throw new ClientNotInitializedError(); } if (!this.#signer) { throw new SignerUnavailableError(); } const finalSigner = signer ?? this.#signer; const signature = await finalSigner.signMessage(await signatureRequest.signatureText()); const identifier = await finalSigner.getIdentifier(); switch (finalSigner.type) { case "SCW": await signatureRequest.addScwSignature(identifier, signature, finalSigner.getChainId(), finalSigner.getBlockNumber?.()); break; case "EOA": await signatureRequest.addEcdsaSignature(signature); break; } } /** * Returns a signature request handler for creating a new inbox * * WARNING: This function should be used with caution. It is only provided * for use in special cases where the provided workflows do not meet the * requirements of an application. * * It is highly recommended to use the `register` method instead. * * @returns The signature text * @throws {ClientNotInitializedError} if the client is not initialized */ async unsafe_createInboxSignatureRequest() { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.createInboxSignatureRequest(); } /** * Returns a signature request handler for adding a new account to the * client's inbox * * WARNING: This function should be used with caution. It is only provided * for use in special cases where the provided workflows do not meet the * requirements of an application. * * It is highly recommended to use the `unsafe_addAccount` method instead. * * The `allowInboxReassign` parameter must be true or this function will * throw an error. * * @param newAccountIdentifier - The identifier of the new account * @param allowInboxReassign - Whether to allow inbox reassignment * @returns The signature text * @throws {ClientNotInitializedError} if the client is not initialized */ async unsafe_addAccountSignatureRequest(newAccountIdentifier, allowInboxReassign = false) { if (!this.#client) { throw new ClientNotInitializedError(); } if (!allowInboxReassign) { throw new InboxReassignError(); } return this.#client.addIdentifierSignatureRequest(newAccountIdentifier); } /** * Returns a signature request handler for removing an account from the * client's inbox * * WARNING: This function should be used with caution. It is only provided * for use in special cases where the provided workflows do not meet the * requirements of an application. * * It is highly recommended to use the `removeAccount` method instead. * * @param identifier - The identifier of the account to remove * @returns The signature text * @throws {ClientNotInitializedError} if the client is not initialized */ async unsafe_removeAccountSignatureRequest(identifier) { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.revokeIdentifierSignatureRequest(identifier); } /** * Returns a signature request handler for revoking all other installations * of the client's inbox * * WARNING: This function should be used with caution. It is only provided * for use in special cases where the provided workflows do not meet the * requirements of an application. * * It is highly recommended to use the `revokeAllOtherInstallations` method instead. * * @returns The signature text * @throws {ClientNotInitializedError} if t