XMTP messages have a maximum size limit. Files that exceed this limit can’t be sent inline and are instead handled as remote attachments. For this, the file is encrypted, uploaded to an external storage provider, and a reference URL is sent in the message. The recipient then downloads and decrypts the file using the metadata from the message.
This approach keeps messages lightweight while supporting files of any size. To send an attachment, you need three things:
A file to attach (image, document, etc.)
A storage provider to host the encrypted file (any service that supports HTTPS GET requests)
An upload callback that tells the SDK how to upload the encrypted bytes and return a download URL
The SDK encrypts the attachment before the upload callback runs. The host stores an encoded Attachment, not the raw file.
Property
Value
Cipher
AES-256-GCM
Key derivation
HKDF-SHA256 with a random 32-byte secret and 32-byte salt
Nonce
Random, 12 bytes
Authentication tag
16 bytes, appended to the ciphertext
Integrity
Hex SHA-256 contentDigest of the encrypted bytes
The attachment.payload contains the already-encrypted bytes, so what gets stored on IPFS is unreadable without the decryption keys, which are only shared within the XMTP message.
Type ID: xmtp.org/attachment:1.0. Its fallback is Can't display <filename>. This app doesn't support attachments.. shouldPush defaults to true on all four platforms.
Type ID: xmtp.org/multiRemoteStaticAttachment:1.0. The payload is a MultiRemoteAttachment protobuf with repeated RemoteAttachmentInfo entries. The fallback says that the app does not support multiple remote attachments. shouldPush defaults to true on all four platforms.
Each RemoteAttachmentInfo contains url, contentDigest, secret, salt, nonce, scheme, optional encrypted contentLength, and optional filename. Each entry has separate encryption material.
Each attachment in the attachments array contains a URL that points to an encrypted EncodedContent object. The content must be accessible by an HTTP GET request to the URL.
Use ctx.sendRemoteAttachment to send a file as an encrypted remote attachment. You provide the file and an upload callback that handles storing the encrypted bytes:
import {
classCommandRouter<ContentTypes=unknown>
Routes slash commands and unmatched text to agent handlers.
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from'node:process';
env.test =null;
console.log(env.test);
// => 'null'
env.test =undefined;
console.log(env.test);
// => 'undefined'
Use delete to delete a property from process.env.
import { env } from'node:process';
env.TEST=1;
delete env.TEST;
console.log(env.TEST);
// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from'node:process';
env.TEST=1;
console.log(env.test);
// => 1
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
@since ― v0.1.27
env.
string |undefined
PINATA_JWT}`,
pinataGateway?: string |undefined
pinataGateway: `${
var process:NodeJS.Process
process.
NodeJS.Process.env: NodeJS.ProcessEnv
The process.env property returns an object containing the user environment.
See environ(7).
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from'node:process';
env.test =null;
console.log(env.test);
// => 'null'
env.test =undefined;
console.log(env.test);
// => 'undefined'
Use delete to delete a property from process.env.
import { env } from'node:process';
env.TEST=1;
delete env.TEST;
console.log(env.TEST);
// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from'node:process';
env.TEST=1;
console.log(env.test);
// => 1
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
@since ― v0.1.27
env.
string |undefined
PINATA_GATEWAY}`,
});
const
constmimeType:"application/octet-stream"
mimeType='application/octet-stream';
const
constencryptedBlob:Blob
encryptedBlob=new
var Blob:new (blobParts?:BlobPart[], options?:BlobPropertyBag) =>Blob
The Blob interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data.
If array is an Array-like object (that is, one with a length property of
type number), it is treated as if it is an array, unless it is a Buffer or
a Uint8Array. This means all other TypedArray variants get treated as an
Array. To create a Buffer from the bytes backing a TypedArray, use
Buffer.copyBytesFrom().
A TypeError will be thrown if array is not an Array or another type
appropriate for Buffer.from() variants.
Buffer.from(array) and Buffer.from(string) may also use the internal
Buffer pool like Buffer.allocUnsafe() does.
Adds the listener function to the end of the listeners array for the event
named eventName. No checks are made to see if the listener has already
been added. Multiple calls passing the same combination of eventName and
listener will result in the listener being added, and called, multiple times.
server.on('connection', (stream) => {
console.log('someone connected!');
});
Returns a reference to the EventEmitter, so that calls can be chained.
By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the
event listener to the beginning of the listeners array.
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and
process.stderr. The global console can be used without importing the node:console module.
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()).
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and
process.stderr. The global console can be used without importing the node:console module.
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()).
// receivedAttachment.content contains the decrypted file bytes
});
The downloadRemoteAttachment utility handles fetching the encrypted bytes from the remote URL and decrypting them using the metadata from the message. You get back the original file with its filename, MIME type, and data.