@xmtp/node-sdk
Version:
XMTP Node client SDK for interacting with XMTP networks
1,391 lines (1,376 loc) • 102 kB
JavaScript
import { BackendBuilder, generateInboxId as generateInboxId$1, getInboxIdByIdentity, contentTypeIntent, contentTypeActions, contentTypeWalletSendCalls, contentTypeLeaveRequest, contentTypeReadReceipt, contentTypeGroupUpdated, contentTypeTransactionReference, contentTypeMultiRemoteAttachment, contentTypeRemoteAttachment, contentTypeAttachment, contentTypeReaction, contentTypeReply, contentTypeMarkdown, contentTypeText, createClientWithBackend, revokeInstallationsSignatureRequest, applySignatureRequest, fetchInboxStatesByInboxIds, verifySignedWithPublicKey, isAddressAuthorized, isInstallationAuthorized, Backend } from '@xmtp/node-bindings';
export { ActionStyle, BackupElementSelectionOption, ConsentEntityType, ConsentState, ContentType, ConversationType, DeliveryStatus, GroupMembershipState, GroupMessageKind, GroupPermissionsOptions, IdentifierKind, ListConversationsOrderBy, LogLevel, MessageSortBy, MetadataField, PermissionLevel, PermissionPolicy, PermissionUpdateType, ReactionAction, ReactionSchema, SortDirection, WorkerKind, contentTypeActions, contentTypeAttachment, contentTypeGroupUpdated, contentTypeIntent, contentTypeLeaveRequest, contentTypeMarkdown, contentTypeMultiRemoteAttachment, contentTypeReaction, contentTypeReadReceipt, contentTypeRemoteAttachment, contentTypeReply, contentTypeText, contentTypeTransactionReference, contentTypeWalletSendCalls, decryptAttachment, encodeActions, encodeAttachment, encodeIntent, encodeMarkdown, encodeMultiRemoteAttachment, encodeReaction, encodeReadReceipt, encodeRemoteAttachment, encodeText, encodeTransactionReference, encodeWalletSendCalls, encryptAttachment, flushTelemetry, initLogging } from '@xmtp/node-bindings';
import { randomBytes } from 'node:crypto';
import { contentTypeToString } from '@xmtp/content-type-primitives';
import { isPromise } from 'node:util/types';
import { join } from 'node:path';
import process from 'node:process';
/**
* Pre-configured URLs for the XMTP network based on the environment
*
* @deprecated Use `createBackend()` instead.
* @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",
"testnet-staging": "https://message-history.dev.ephemera.network",
"testnet-dev": "https://message-history.dev.ephemera.network",
testnet: "https://message-history.dev.ephemera.network",
mainnet: "https://message-history.production.ephemera.network",
};
const envMap = {
local: "Local" /* BindingsEnv.Local */,
dev: "Dev" /* BindingsEnv.Dev */,
production: "Production" /* BindingsEnv.Production */,
"testnet-staging": "TestnetStaging" /* BindingsEnv.TestnetStaging */,
"testnet-dev": "TestnetDev" /* BindingsEnv.TestnetDev */,
testnet: "Testnet" /* BindingsEnv.Testnet */,
mainnet: "Mainnet" /* BindingsEnv.Mainnet */,
};
const reverseEnvMap = {
["Local" /* BindingsEnv.Local */]: "local",
["Dev" /* BindingsEnv.Dev */]: "dev",
["Production" /* BindingsEnv.Production */]: "production",
["TestnetStaging" /* BindingsEnv.TestnetStaging */]: "testnet-staging",
["TestnetDev" /* BindingsEnv.TestnetDev */]: "testnet-dev",
["Testnet" /* BindingsEnv.Testnet */]: "testnet",
["Mainnet" /* BindingsEnv.Mainnet */]: "mainnet",
};
const envToString = (env) => {
return reverseEnvMap[env];
};
const createBackend = async (options) => {
const env = options?.env ?? "dev";
const builder = new BackendBuilder(envMap[env]);
if (options?.apiUrl)
builder.setApiUrl(options.apiUrl);
if (options?.gatewayHost)
builder.setGatewayHost(options.gatewayHost);
if (options?.appVersion)
builder.setAppVersion(options.appVersion);
return builder.build();
};
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 MissingContentTypeError extends Error {
constructor() {
super("Content type is required when sending encoded content");
}
}
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");
}
}
const generateInboxId = (identifier, nonce) => {
return generateInboxId$1(identifier, nonce);
};
const getInboxIdForIdentifier = async (backend, identifier) => {
return getInboxIdByIdentity(backend, identifier);
};
function isHexString(value) {
return typeof value === "string" && /^0x(?:[0-9a-fA-F]{2})+$/.test(value);
}
function validHex(value) {
if (!isHexString(value)) {
throw new TypeError(`Value is not a hexadecimal string.`);
}
return value;
}
const isReaction = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "reaction";
const isReply = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "reply";
const isTextReply = (m) => isReply(m) && typeof m.content?.content === "string";
const isText = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "text";
const isRemoteAttachment = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "remoteStaticAttachment";
const isAttachment = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "attachment";
const isMultiRemoteAttachment = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "multiRemoteStaticAttachment";
const isTransactionReference = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "transactionReference";
const isGroupUpdated = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "group_updated";
const isReadReceipt = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "readReceipt";
const isLeaveRequest = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "leave_request";
const isWalletSendCalls = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "walletSendCalls";
const isIntent = (m) => m.contentType.authorityId === "coinbase.com" &&
m.contentType.typeId === "intent";
const isActions = (m) => m.contentType.authorityId === "coinbase.com" &&
m.contentType.typeId === "actions";
const isMarkdown = (m) => m.contentType.authorityId === "xmtp.org" &&
m.contentType.typeId === "markdown";
class CodecRegistry {
#codecs;
constructor(codecs) {
this.#codecs = new Map(codecs.map((codec) => [contentTypeToString(codec.contentType), codec]));
}
/**
* Gets the codec for a given content type
*
* @param contentType - The content type to get the codec for
* @returns The codec, if found
*/
getCodec(contentType) {
return this.#codecs.get(contentTypeToString(contentType));
}
}
function nsToDate(ns) {
return new Date(Number(ns / 1000000n));
}
const getContentFromDecodedMessageContent = (content) => {
switch (content.type) {
case "Text" /* DecodedMessageContentType.Text */: {
return content.text;
}
case "Markdown" /* DecodedMessageContentType.Markdown */: {
return content.markdown;
}
case "Reply" /* DecodedMessageContentType.Reply */: {
return content.reply;
}
case "Reaction" /* DecodedMessageContentType.Reaction */: {
return content.reaction;
}
case "Attachment" /* DecodedMessageContentType.Attachment */: {
return content.attachment;
}
case "RemoteAttachment" /* DecodedMessageContentType.RemoteAttachment */: {
return content.remoteAttachment;
}
case "MultiRemoteAttachment" /* DecodedMessageContentType.MultiRemoteAttachment */: {
return content.multiRemoteAttachment;
}
case "TransactionReference" /* DecodedMessageContentType.TransactionReference */: {
return content.transactionReference;
}
case "GroupUpdated" /* DecodedMessageContentType.GroupUpdated */: {
return content.groupUpdated;
}
case "ReadReceipt" /* DecodedMessageContentType.ReadReceipt */: {
return content.readReceipt;
}
case "LeaveRequest" /* DecodedMessageContentType.LeaveRequest */: {
return content.leaveRequest;
}
case "WalletSendCalls" /* DecodedMessageContentType.WalletSendCalls */: {
return content.walletSendCalls;
}
case "Actions" /* DecodedMessageContentType.Actions */: {
return content.actions;
}
case "Intent" /* DecodedMessageContentType.Intent */: {
return content.intent;
}
case "DeletedMessage" /* DecodedMessageContentType.DeletedMessage */: {
return content.deletedMessage;
}
case "Custom" /* DecodedMessageContentType.Custom */: {
return content.custom;
}
default:
content.type;
return null;
}
};
const getContentTypeFromDecodedMessageContent = (content) => {
switch (content.type) {
case "Text" /* DecodedMessageContentType.Text */: {
return contentTypeText();
}
case "Markdown" /* DecodedMessageContentType.Markdown */: {
return contentTypeMarkdown();
}
case "Reply" /* DecodedMessageContentType.Reply */: {
return contentTypeReply();
}
case "Reaction" /* DecodedMessageContentType.Reaction */: {
return contentTypeReaction();
}
case "Attachment" /* DecodedMessageContentType.Attachment */: {
return contentTypeAttachment();
}
case "RemoteAttachment" /* DecodedMessageContentType.RemoteAttachment */: {
return contentTypeRemoteAttachment();
}
case "MultiRemoteAttachment" /* DecodedMessageContentType.MultiRemoteAttachment */: {
return contentTypeMultiRemoteAttachment();
}
case "TransactionReference" /* DecodedMessageContentType.TransactionReference */: {
return contentTypeTransactionReference();
}
case "GroupUpdated" /* DecodedMessageContentType.GroupUpdated */: {
return contentTypeGroupUpdated();
}
case "ReadReceipt" /* DecodedMessageContentType.ReadReceipt */: {
return contentTypeReadReceipt();
}
case "LeaveRequest" /* DecodedMessageContentType.LeaveRequest */: {
return contentTypeLeaveRequest();
}
case "WalletSendCalls" /* DecodedMessageContentType.WalletSendCalls */: {
return contentTypeWalletSendCalls();
}
case "Actions" /* DecodedMessageContentType.Actions */: {
return contentTypeActions();
}
case "Intent" /* DecodedMessageContentType.Intent */: {
return contentTypeIntent();
}
case "DeletedMessage" /* DecodedMessageContentType.DeletedMessage */: {
return undefined;
}
case "Custom" /* DecodedMessageContentType.Custom */: {
return content.custom?.type;
}
default:
content.type;
return undefined;
}
};
/**
* Represents a decoded XMTP message
*
* @class
* @property {unknown} 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 {bigint} expiresAtNs - Timestamp when the message will expire (in nanoseconds)
* @property {Date} expiresAt - Timestamp when the message will expire
* @property {string} [fallback] - Optional fallback text for the message
* @property {string} id - Unique identifier for the message
* @property {MessageKind} kind - Type of message ("application" | "membership_change")
* @property {number} numReplies - Number of replies to the message
* @property {DecodedMessage<Reaction>[]} reactions - Reactions to the message
* @property {string} senderInboxId - Identifier for the sender's inbox
* @property {Date} sentAt - Timestamp when the message was sent
* @property {bigint} sentAtNs - Timestamp when the message was sent (in nanoseconds)
*/
class DecodedMessage {
content;
contentType;
conversationId;
deliveryStatus;
expiresAtNs;
expiresAt;
fallback;
id;
kind;
numReplies;
reactions;
senderInboxId;
sentAt;
sentAtNs;
constructor(codecRegistry, message) {
this.id = message.id;
this.expiresAtNs = message.expiresAtNs ?? undefined;
this.expiresAt = message.expiresAtNs
? nsToDate(message.expiresAtNs)
: undefined;
this.sentAtNs = message.sentAtNs;
this.sentAt = nsToDate(message.sentAtNs);
this.conversationId = message.conversationId;
this.senderInboxId = message.senderInboxId;
this.contentType = message.contentType;
this.fallback = message.fallback ?? undefined;
this.kind = message.kind;
this.deliveryStatus = message.deliveryStatus;
this.numReplies = message.numReplies;
this.reactions = message.reactions.map((reaction) => new DecodedMessage(codecRegistry, reaction));
this.content =
getContentFromDecodedMessageContent(message.content) ??
undefined;
switch (message.content.type) {
case "Reply" /* DecodedMessageContentType.Reply */: {
const reply = message.content.reply;
let replyContent = getContentFromDecodedMessageContent(reply.content);
if (reply.content.type === "Custom" /* DecodedMessageContentType.Custom */) {
const codec = codecRegistry.getCodec(reply.content.custom?.type);
if (codec) {
try {
replyContent = codec.decode(replyContent);
}
catch (error) {
if (error instanceof Error) {
console.warn(`Error decoding custom content: ${error.message}`);
}
else {
console.warn(`Error decoding custom content`);
}
}
}
}
this.content = {
referenceId: reply.referenceId,
content: replyContent,
contentType: getContentTypeFromDecodedMessageContent(reply.content),
inReplyTo: reply.inReplyTo
? new DecodedMessage(codecRegistry, reply.inReplyTo)
: null,
};
break;
}
case "Custom" /* DecodedMessageContentType.Custom */: {
const customContent = message.content.custom;
if (customContent !== null) {
const codec = codecRegistry.getCodec(this.contentType);
if (codec) {
try {
this.content = codec.decode(customContent);
}
catch (error) {
if (error instanceof Error) {
console.warn(`Error decoding custom content: ${error.message}`);
}
else {
console.warn(`Error decoding custom content`);
}
this.content = undefined;
}
}
else {
console.warn(`No codec found for content type "${contentTypeToString(this.contentType)}"`);
this.content = undefined;
}
}
break;
}
}
}
}
/**
* 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 = 60_000; // milliseconds
const DEFAULT_RETRY_ATTEMPTS = 10;
/**
* 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) => {
if (mutatedValue !== undefined) {
asyncStream.push(mutatedValue);
onValue?.(mutatedValue);
}
})
.catch((error) => {
onError?.(error);
});
}
else {
if (mutatedValue !== undefined) {
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, call onError
if (retries === 0) {
void asyncStream.end();
onError?.(new StreamFailedError(retryAttempts));
return;
}
// 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
onError?.(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;
#codecRegistry;
#conversation;
/**
* Creates a new conversation instance
*
* @param client - The client instance managing the conversation
* @param codecRegistry - The codec registry instance
* @param conversation - The underlying conversation instance
*/
constructor(client, codecRegistry, conversation) {
this.#client = client;
this.#codecRegistry = codecRegistry;
this.#conversation = conversation;
}
/**
* 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);
}
get topic() {
return `/xmtp/mls/1/g-${this.id}/proto`;
}
pausedForVersion() {
return this.#conversation.pausedForVersion() ?? undefined;
}
/**
* Gets HMAC keys for this conversation
*
* @returns The HMAC keys for this conversation
*/
hmacKeys() {
return this.#conversation.hmacKeys();
}
/**
* 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) => {
if (!options?.disableSync) {
await this.sync();
}
return this.#conversation.stream(callback, onFail);
};
const convertMessage = (value) => {
const enrichedMessage = this.#client.conversations.getMessageById(value.id);
if (enrichedMessage === undefined) {
console.warn(`Streamed message with ID "${value.id}" not found`);
}
return enrichedMessage;
};
return createStream(stream, convertMessage, options);
}
/**
* Decodes, decrypts, and persists a raw envelope from a group message stream.
*
* @param envelopeBytes - Raw protobuf-encoded envelope bytes from the stream
* @returns The processed and stored messages
*/
async processStreamedMessage(envelopeBytes) {
return this.#conversation.processStreamedGroupMessage(envelopeBytes);
}
/**
* Publishes pending messages that were sent optimistically
*
* @returns Promise that resolves when publishing is complete
*/
async publishMessages() {
return this.#conversation.publishMessages();
}
/**
* Sends a message with configurable delivery behavior
*
* @param encodedContent - The encoded content to send
* @param sendOptions - Options for sending the message
* @param sendOptions.shouldPush - Indicates whether this message should be
* included in push notifications
* @param sendOptions.optimistic - Indicates whether this message should be
* sent optimistically and published later via `publishMessages`
* @param sendOptions.idempotencyKey - Optional idempotency key; re-sending
* identical content with the same key produces the same deduplicated message id
* @returns Promise that resolves with the message ID after it has been sent
*/
async send(encodedContent, sendOptions) {
if (!encodedContent.type) {
throw new MissingContentTypeError();
}
return this.#conversation.send(encodedContent, sendOptions ?? { shouldPush: false });
}
/**
* Sends a text message
*
* @param text - The text to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendText(text, opts) {
return this.#conversation.sendText(text, opts);
}
/**
* Sends a markdown message
*
* @param markdown - The markdown to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendMarkdown(markdown, opts) {
return this.#conversation.sendMarkdown(markdown, opts);
}
/**
* Sends a reaction message
*
* @param reaction - The reaction to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendReaction(reaction, opts) {
return this.#conversation.sendReaction(reaction, opts);
}
/**
* Sends a read receipt message
*
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendReadReceipt(opts) {
return this.#conversation.sendReadReceipt(opts);
}
/**
* Sends a reply message
*
* @param reply - The reply to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendReply(reply, opts) {
return this.#conversation.sendReply(reply, opts);
}
/**
* Sends a transaction reference message
*
* @param transactionReference - The transaction reference to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendTransactionReference(transactionReference, opts) {
return this.#conversation.sendTransactionReference(transactionReference, opts);
}
/**
* Sends a wallet send calls message
*
* @param walletSendCalls - The wallet send calls to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendWalletSendCalls(walletSendCalls, opts) {
return this.#conversation.sendWalletSendCalls(walletSendCalls, opts);
}
/**
* Sends a actions message
*
* @param actions - The actions to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendActions(actions, opts) {
return this.#conversation.sendActions(actions, opts);
}
/**
* Sends a intent message
*
* @param intent - The intent to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendIntent(intent, opts) {
return this.#conversation.sendIntent(intent, opts);
}
/**
* Sends an attachment message
*
* @param attachment - The attachment to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendAttachment(attachment, opts) {
return this.#conversation.sendAttachment(attachment, opts);
}
/**
* Sends a multi remote attachment message
*
* @param multiRemoteAttachment - The multi remote attachment to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendMultiRemoteAttachment(multiRemoteAttachment, opts) {
return this.#conversation.sendMultiRemoteAttachment(multiRemoteAttachment, opts);
}
/**
* Sends a remote attachment message
*
* @param remoteAttachment - The remote attachment to send
* @param opts - Send options (optimistic delivery, idempotency key)
* @returns Promise that resolves with the message ID after it has been sent
*/
async sendRemoteAttachment(remoteAttachment, opts) {
return this.#conversation.sendRemoteAttachment(remoteAttachment, opts);
}
/**
* 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.listEnrichedMessages(options);
return messages.map((message) => new DecodedMessage(this.#codecRegistry, message));
}
/**
* Counts messages in this conversation
*
* @param options - Optional filtering options
* @returns Promise that resolves with the count of messages
*/
async countMessages(options) {
const count = await this.#conversation.countMessages(options);
return count;
}
/**
* Gets the last message in this conversation
*
* @returns Promise that resolves with the last message or undefined if none exists
*/
async lastMessage() {
const messages = await this.messages({
limit: 1,
direction: 1 /* SortDirection.Descending */,
});
if (messages.length > 0) {
return messages[0];
}
return undefined;
}
/**
* Gets the consent state for this conversation
*/
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();
}
/**
* Retrieves information for this conversation to help with debugging
*
* @returns The debug information for this conversation
*/
async debugInfo() {
return this.#conversation.debugInfo();
}
/**
* Retrieves the last read times for this conversation
*
* @returns A map keyed by inbox ID with the last read timestamp
* (nanoseconds since epoch)
*/
async lastReadTimes() {
return this.#conversation.lastReadTimes();
}
}
/**
* Represents a direct message conversation between two inboxes
*
* This class is not intended to be initialized directly.
*/
class Dm extends Conversation {
#client;
#codecRegistry;
#conversation;
/**
* Creates a new direct message conversation instance
*
* @param client - The client instance managing this direct message conversation
* @param codecRegistry - The codec registry instance
* @param conversation - The underlying conversation instance
*/
constructor(client, codecRegistry, conversation) {
super(client, codecRegistry, conversation);
this.#client = client;
this.#codecRegistry = codecRegistry;
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 duplicateDms() {
const duplicateDms = await this.#conversation.duplicateDms();
return duplicateDms.map((dm) => new Dm(this.#client, this.#codecRegistry, 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 codecRegistry - The codec registry instance
* @param conversation - The underlying conversation object
*/
constructor(client, codecRegistry, conversation) {
super(client, codecRegistry, conversation);
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 app data of the group
*/
get appData() {
return this.#conversation.appData();
}
/**
* Updates the group's app data (max 8192 bytes)
*
* @param appData The new app data for the group
*/
async updateAppData(appData) {
return this.#conversation.updateAppData({ value: appData });
}
/**
* The permissions of the group
*/
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
*/
listAdmins() {
return this.#conversation.listAdmins();
}
/**
* The list of super admins of the group
*/
listSuperAdmins() {
return this.#conversation.listSuperAdmins();
}
/**
* 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.addMembersByIdentity(identifiers);
}
/**
* Adds members to the group using inbox IDs
*
* @param inboxIds Array of inbox IDs to add
*/
async addMembers(inboxIds) {
return this.#conversation.addMembers(inboxIds);
}
/**
* Removes members from the group using identifiers
*
* @param identifiers Array of member identifiers to remove
*/
async removeMembersByIdentifiers(identifiers) {
return this.#conversation.removeMembersByIdentity(identifiers);
}
/**
* Removes members from the group using inbox IDs
*
* @param inboxIds Array of inbox IDs to remove
*/
async removeMembers(inboxIds) {
return this.#conversation.removeMembers(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);
}
/**
* Request to leave the group
*/
async requestRemoval() {
return this.#conversation.leaveGroup();
}
/**
* Checks if the current user has requested to leave the group
*
* @returns Boolean
*/
isPendingRemoval() {
return (this.#conversation.membershipState() ===
4 /* GroupMembershipState.PendingRemove */);
}
}
/**
* Manages conversations
*
* This class is not intended to be initialized directly.
*/
class Conversations {
#client;
#codecRegistry;
#conversations;
/**
* Creates a new conversations instance
*
* @param client - The client instance managing the conversations
* @param codecRegistry - The codec registry instance
* @param conversations - The underlying conversations instance
*/
constructor(client, codecRegistry, conversations) {
this.#client = client;
this.#codecRegistry = codecRegistry;
this.#conversations = conversations;
}
get topic() {
return `/xmtp/mls/1/w-${this.#client.installationId}/proto`;
}
/**
* Retrieves a conversation by its ID
*
* @param id - The conversation ID to look up
* @returns The conversation if found, undefined otherwise
* @see https://docs.xmtp.org/chat-apps/core-messaging/create-conversations#conversation-helper-methods
*/
async getConversationById(id) {
try {
// getConversationById will throw if group is not found
const group = this.#conversations.getConversationById(id);
const metadata = await group.groupMetadata();
switch (metadata.conversationType()) {
case 1 /* ConversationType.Group */:
return new Group(this.#client, this.#codecRegistry, group);
case 0 /* ConversationType.Dm */:
return new Dm(this.#client, this.#codecRegistry, group);
default:
return undefined;
}
}
catch {
return undefined;
}
}
/**
* Retrieves a DM by inbox ID
*
* @param inboxId - The inbox ID to look up
* @returns The DM if found, undefined otherwise
* @see https://docs.xmtp.org/chat-apps/core-messaging/create-conversations#conversation-helper-methods
*/
getDmByInboxId(inboxId) {
try {
// getDmByInboxId will throw if group is not found
const group = this.#conversations.getDmByInboxId(inboxId);
return new Dm(this.#client, this.#codecRegistry, group);
}
catch {
return undefined;
}
}
/**
* Retrieves a DM by identifier
*
* @param identifier - The identifier to look up
* @returns Promise that resolves with the DM, if found
* @see https://docs.xmtp.org/chat-apps/core-messaging/create-conversations#conversation-helper-methods
*/
async fetchDmByIdentifier(identifier) {
const inboxId = await this.#client.fetchInboxIdByIdentifier(identifier);
if (!inboxId) {
return undefined;
}
return this.getDmByInboxId(inboxId);
}
/**
* Retrieves a message by its ID
*
* @param id - The message ID to look up
* @returns The decoded message if found, undefined otherwise
* @see https://docs.xmtp.org/chat-apps/core-messaging/create-conversations#conversation-helper-methods
*/
getMessageById(id) {
try {
// getEnrich