@xmtp/node-sdk
Version:
XMTP Node client SDK for interacting with XMTP networks
1,497 lines (1,481 loc) • 71.9 kB
JavaScript
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 { isPromise } from 'node:util/types';
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",
};
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");
}
}
class StreamFailedError extends Error {
constructor(retryAttempts) {
const times = `time${retryAttempts !== 1 ? "s" : ""}`;
super(`Stream failed, retried ${retryAttempts} ${times}`);
}
}
class StreamInvalidRetryAttemptsError extends Error {
constructor() {
super("Stream retry attempts must be greater than 0");
}
}
/**
* AsyncStream provides an async iterable interface for streaming data.
*
* This class implements a producer-consumer pattern where:
* - Producers can push values using the `push()` method
* - Consumers can iterate over values asynchronously using `for await` loops or `next()`
* - Values are queued internally when no consumers are waiting
* - Consumers are resolved immediately when values are available
* - The stream can be terminated using `done()`, `return()`, or `end()`
*
* @example
* ```typescript
* const stream = new AsyncStream<string>();
*
* stream.push("hello");
* stream.push("world");
*
* for await (const value of stream) {
* console.log(value); // "hello", "world"
* }
* ```
*/
class AsyncStream {
isDone = false;
#pendingResolves = [];
#queue;
onDone;
onReturn;
constructor() {
this.#queue = [];
this.isDone = false;
}
flush() {
while (this.#pendingResolves.length > 0) {
const nextResolve = this.#pendingResolves.shift();
if (nextResolve) {
nextResolve({ done: true, value: undefined });
}
}
}
done() {
this.flush();
this.#queue = [];
this.#pendingResolves = [];
this.isDone = true;
this.onDone?.();
}
push = (value) => {
if (this.isDone) {
return;
}
const nextResolve = this.#pendingResolves.shift();
if (nextResolve) {
nextResolve({
done: false,
value,
});
}
else {
this.#queue.push(value);
}
};
next = () => {
if (this.isDone) {
return Promise.resolve({ done: true, value: undefined });
}
if (this.#queue.length > 0) {
return Promise.resolve({
done: false,
value: this.#queue.shift(),
});
}
return new Promise((resolve) => {
this.#pendingResolves.push(resolve);
});
};
return = () => {
this.onReturn?.();
this.done();
return Promise.resolve({
done: true,
value: undefined,
});
};
end = () => this.return();
[Symbol.asyncIterator]() {
return this;
}
}
const usableProperties = [
"end",
"isDone",
"next",
"return",
Symbol.asyncIterator,
];
const isUsableProperty = (prop) => {
return usableProperties.includes(prop);
};
/**
* Creates a read-only proxy for AsyncStream instances that restricts access to consumer-only methods.
*
* This proxy only exposes the following properties and methods:
* - `next()`: Get the next value from the stream
* - `end()`: Terminate the stream and stop iteration
* - `return()`: Same as end(), terminates the stream
* - `isDone`: Boolean indicating if the stream has been terminated
* - `Symbol.asyncIterator`: Enables `for await` loop iteration
*
* Producer methods like `push()`, `done()`, and `flush()` are hidden to prevent
* consumers from accidentally modifying the stream state.
*
* @param stream - The AsyncStream instance to create a proxy for
* @returns A read-only proxy that implements AsyncStreamProxy<T>
*
* @example
* ```typescript
* const stream = new AsyncStream<string>();
* const proxy = createAsyncStreamProxy(stream);
*
* stream.push("hello");
* stream.push("world");
*
* for await (const value of proxy) {
* console.log(value); // "hello", "world"
* }
* ```
*/
function createAsyncStreamProxy(stream) {
return new Proxy(stream, {
get(target, prop, receiver) {
if (isUsableProperty(prop)) {
return Reflect.get(target, prop, receiver);
}
},
set() {
return true;
},
has(_target, prop) {
return isUsableProperty(prop);
},
ownKeys() {
return usableProperties;
},
getOwnPropertyDescriptor(target, prop) {
if (isUsableProperty(prop)) {
return {
enumerable: true,
configurable: true,
value: Reflect.get(target, prop),
};
}
return undefined;
},
});
}
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const DEFAULT_RETRY_DELAY = 10000; // milliseconds
const DEFAULT_RETRY_ATTEMPTS = 6;
/**
* Creates a stream from a stream function
*
* If the stream fails, an attempt will be made to restart it.
*
* This function is not intended to be used directly.
*
* @param streamFunction - The stream function to create a stream from
* @param streamValueMutator - An optional function to mutate the value emitted from the stream
* @param options - The options for the stream
* @param args - Additional arguments to pass to the stream function
* @returns An async iterable stream proxy
* @throws {StreamInvalidRetryAttemptsError} if the retryAttempts option is less than 0 and retryOnFail is true
* @throws {StreamFailedError} if the stream fails and can't be restarted
*/
const createStream = async (streamFunction, streamValueMutator, options) => {
const { onError, onFail, onRestart, onRetry, onValue, retryAttempts = DEFAULT_RETRY_ATTEMPTS, retryDelay = DEFAULT_RETRY_DELAY, retryOnFail = true, } = options ?? {};
// retry attempts must be greater than 0
if (retryOnFail && retryAttempts < 0) {
throw new StreamInvalidRetryAttemptsError();
}
const asyncStream = new AsyncStream();
const streamCallback = (error, value) => {
// if a stream error occurs, call the onError callback
if (error) {
onError?.(error);
return;
}
// ensure the value is not undefined
if (value !== undefined) {
try {
// if a streamValueMutator is provided, mutate the value
if (streamValueMutator) {
const mutatedValue = streamValueMutator(value);
if (isPromise(mutatedValue)) {
void mutatedValue
.then((mutatedValue) => {
asyncStream.push(mutatedValue);
onValue?.(mutatedValue);
})
.catch((error) => {
onError?.(error);
});
}
else {
asyncStream.push(mutatedValue);
onValue?.(mutatedValue);
}
}
else {
asyncStream.push(value);
onValue?.(value);
}
}
catch (error) {
onError?.(error);
}
}
};
const retry = async (retries = retryAttempts) => {
// if the stream has been retried the maximum number of times without
// success, throw an error
if (retries === 0) {
void asyncStream.end();
throw new StreamFailedError(retryAttempts);
}
// wait for the retry delay before attempting to restart the stream
await wait(retryDelay);
// call the onRetry callback
onRetry?.(retryAttempts - retries + 1, retryAttempts);
try {
// attempt to restart the stream
const streamCloser = await streamFunction(streamCallback, () => {
// call the onFail callback
onFail?.();
void retry();
});
await streamCloser.waitForReady();
// when the async stream is done, end the stream
asyncStream.onDone = () => {
streamCloser.end();
};
// stream restarted, call the onRestart callback
onRestart?.();
}
catch (error) {
onError?.(error);
// retry
void retry(retries - 1);
}
};
const startRetry = () => {
// if the stream should be retried, start the process
if (retryOnFail) {
void retry();
}
else {
void asyncStream.end();
// stream failed and should not be retried, throw an error
throw new StreamFailedError(0);
}
};
try {
// create the stream
const streamCloser = await streamFunction(streamCallback, () => {
// call the onFail callback
onFail?.();
startRetry();
});
await streamCloser.waitForReady();
// when the async stream is done, end the stream
asyncStream.onDone = () => {
streamCloser.end();
};
}
catch (error) {
onError?.(error);
startRetry();
}
// return a proxy for the async stream
return createAsyncStreamProxy(asyncStream);
};
/**
* 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 options - Optional stream options
* @returns Stream instance for new messages
*/
async stream(options) {
const stream = async (callback, onFail) => {
await this.sync();
return this.#conversation.stream(callback, onFail);
};
const convertMessage = (value) => {
return new DecodedMessage(this.#client, value);
};
return createStream(stream, convertMessage, options);
}
/**
* 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 options - Optional stream options
* @param options.conversationType - Optional conversation type to filter by
* @returns Stream instance for new conversations
*/
async stream(options) {
const stream = async (callback, onFail) => {
await this.sync();
return this.#conversations.stream(callback, onFail, options?.conversationType);
};
const convertConversation = async (value) => {
const metadata = await value.groupMetadata();
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;
}
return conversation;
};
return createStream(stream, convertConversation, options);
}
/**
* Creates a stream for new group conversations
*
* @param options - Optional stream options
* @returns Stream instance for new group conversations
*/
async streamGroups(options) {
const stream = async (callback, onFail) => {
await this.sync();
return this.#conversations.stream(callback, onFail, 1 /* ConversationType.Group */);
};
const convertConversation = (value) => {
return new Group(this.#client, value);
};
return createStream(stream, convertConversation, options);
}
/**
* Creates a stream for new DM conversations
*
* @param options - Optional stream options
* @returns Stream instance for new DM conversations
*/
async streamDms(options) {
const stream = async (callback, onFail) => {
await this.sync();
return this.#conversations.stream(callback, onFail, 0 /* ConversationType.Dm */);
};
const convertConversation = (value) => {
return new Dm(this.#client, value);
};
return createStream(stream, convertConversation, options);
}
/**
* Creates a stream for all new messages
*
* @param options - Optional stream options
* @param options.conversationType - Optional conversation type to filter by
* @param options.consentStates - Optional array of consent states to filter by
* @returns Stream instance for new messages
*/
async streamAllMessages(options) {
const streamAllMessages = async (callback, onFail) => {
await this.sync();
return this.#conversations.streamAllMessages(callback, onFail, options?.conversationType, options?.consentStates);
};
const convertMessage = (value) => {
return new DecodedMessage(this.#client, value);
};
return createStream(streamAllMessages, convertMessage, options);
}
/**
* Creates a stream for all new group messages
*
* @param options - Optional stream options
* @param options.consentStates - Optional array of consent states to filter by
* @returns Stream instance for new group messages
*/
async streamAllGroupMessages(options) {
return this.streamAllMessages({
...(options ?? {}),
conversationType: 1 /* ConversationType.Group */,
consentStates: options?.consentStates,
});
}
/**
* Creates a stream for all new DM messages
*
* @param options - Optional stream options
* @param options.consentStates - Optional array of consent states to filter by
* @returns Stream instance for new DM messages
*/
async streamAllDmMessages(options) {
return this.streamAllMessages({
...(options ?? {}),
conversationType: 0 /* ConversationType.Dm */,
consentStates: options?.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 options - Optional stream options
* @returns Stream instance for consent updates
*/
streamConsent(options) {
const streamConsent = async (callback, onFail) => {
await this.sync();
return this.#conversations.streamConsent(callback, onFail);
};
return createStream(streamConsent, undefined, options);
}
/**
* Creates a stream of user preference updates
*
* @param options - Optional stream options
* @returns Stream instance for preference updates
*/
streamPreferences(options) {
const streamPreferences = async (callback, onFail) => {
await this.sync();
return this.#conversations.streamPreferences(callback, onFail);
};
return createStream(streamPreferences, undefined, options);
}
}
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, undefined, options?.debugEventsEnabled, options?.appVersion);
};
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