npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@photon-ai/advanced-imessage

v2.1.0

Published

TypeScript SDK for Advanced iMessage API

Downloads

422,272

Readme

@photon-ai/advanced-imessage

TypeScript SDK for the Advanced iMessage server. Supports two transports: HTTP and gRPC — pick per project, or mix them.

  • HTTP (default): the SDK talks fetch to the REST middleware (imessage-server-v2-http), which forwards to the iMessage plane — no gRPC in the client, so it runs wherever fetch exists: Cloudflare Workers, edge runtimes, browsers, Node, Bun, Deno. Inbound events ride webhooks — see Inbound events.
  • gRPC: the full v1 surface, including the live subscribeEvents/watch/events.catchUp streams. Node/Bun only.

Install

HTTP

bun add @photon-ai/advanced-imessage
import { createHttpClient } from "@photon-ai/advanced-imessage";

const im = createHttpClient({
  address: "http://localhost:8080", // the HTTP middleware
  token: process.env.IMESSAGE_TOKEN!,
});

const sent = await im.messages.sendText("any;-;[email protected]", "hello");
console.log(sent.guid);

await im.close();

Runs on any runtime with fetch (Cloudflare Workers, Node >=18.17, Bun, Deno, browsers). The package is ESM-only. Workers compatibility is enforced in CI: every build bundles the SDK and boots it in workerd.

gRPC

The gRPC transport needs its peer dependencies.

bun add @photon-ai/advanced-imessage nice-grpc nice-grpc-common @grpc/grpc-js
import { createClient } from "@photon-ai/advanced-imessage/grpc";

const im = createClient({
  address: "127.0.0.1:50051", // the gRPC server
  token: process.env.IMESSAGE_TOKEN!,
  tls: false,
});

const sent = await im.messages.sendText("any;-;[email protected]", "hello");
console.log(sent.guid);

// gRPC keeps client-held live event streams:
for await (const event of im.messages.subscribeEvents()) {
  console.log(event.type, event.sequence);
}

await im.close();

Node >=18.17 or Bun (native gRPC — not available on fetch-only runtimes like Workers).

Entrypoints

One package, three entrypoints:

| Import | What you get | | --- | --- | | @photon-ai/advanced-imessage | createHttpClient, createGrpcClient, and every shared type. HTTP-flavored: ClientOptions etc. are the HTTP client's. | | @photon-ai/advanced-imessage/http | The HTTP transport only. Safe everywhere fetch exists. | | @photon-ai/advanced-imessage/grpc | The gRPC transport: the full v1 surface, including events and the live subscribeEvents/watch streams. Node/Bun only. |

gRPC requires its optional peer dependencies (see Install); HTTP-only installs never download or evaluate them.

Migrating from v1

v1 of this package was gRPC-only, exported from the package root. Your code keeps working with two changes — an import specifier and the peer install above:

-import { createClient, type ClientOptions } from "@photon-ai/advanced-imessage";
+import { createClient, type ClientOptions } from "@photon-ai/advanced-imessage/grpc";

Every v1 export keeps its name on the /grpc subpath (createClient is an alias of createGrpcClient). Live event streams stay gRPC-only — over HTTP, inbound events ride webhooks instead (see Inbound events).

Connect

import { createHttpClient } from "@photon-ai/advanced-imessage";

const im = createHttpClient({
  address: "http://localhost:8080", // the HTTP middleware
  token: process.env.IMESSAGE_TOKEN!,
});

await im.close();

address is the imessage-server-v2-http middleware: a bare host[:port] or a full http(s):// URL. token may also be an async function when credentials rotate — it is resolved fresh for every call:

const im = createHttpClient({
  address: "imessage.example.com",
  token: async () => process.env.IMESSAGE_TOKEN!,
});

Bare addresses default to https; set tls: false only for local development.

Shared and dedicated routing

The HTTP client uses the shared proxy by default. Omit server and the SDK will not send an x-photon-server header:

const im = createHttpClient({
  address: "imessage.example.com",
  token: process.env.IMESSAGE_TOKEN!,
});

To route requests to a dedicated iMessage instance, set server to the instance ID assigned by Photon and use a token that instance accepts:

const im = createHttpClient({
  address: "imessage.example.com",
  token: process.env.IMESSAGE_TOKEN!,
  server: "instance-abc",
});

This sends x-photon-server: instance-abc on every request. The server value is the dedicated instance ID, not the middleware address or bearer token.

Chat GUIDs

Methods that take chat expect a server chat guid:

const direct = "any;-;[email protected]";
const group = "any;+;group-chat-guid";

In normal code, pass chat.guid returned by im.chats.create(...), im.chats.get(...), message results, or event payloads. The SDK does not turn bare phone numbers, emails, or group IDs into chat GUIDs.

Send Messages

import { MessageEffect, TextEffect } from "@photon-ai/advanced-imessage";

const chatGuid = "any;-;[email protected]";

const sent = await im.messages.sendText(chatGuid, "Happy birthday", {
  effect: MessageEffect.confetti,
  formatting: [{ type: "effect", start: 0, length: 5, effect: TextEffect.bloom }],
  enableLinkPreview: true,
});

console.log(sent.guid);

Reply to a whole message:

await im.messages.sendText(chatGuid, "reply", {
  replyTo: sent.guid,
});

Reply to one bubble in a multipart message:

await im.messages.sendText(chatGuid, "reply to part 2", {
  replyTo: { guid: sent.guid, partIndex: 2 },
});

Send Attachments

Attachments are sent by uploaded attachment GUID.

import { readFile } from "node:fs/promises";

const jpegBytes = await readFile("photo.jpg");

const uploaded = await im.attachments.upload({
  fileName: "photo.jpg",
  data: jpegBytes,
});

await im.messages.sendAttachment(chatGuid, uploaded.attachment.guid);

The SDK uploads raw bytes and returns a server-hosted attachment GUID. Use that GUID with messages.sendAttachment(...), attachments.get(...), or attachments.downloadStream(...). The SDK does not expose server-local file paths; this matters when the SDK and server run on different machines.

Upload, metadata lookup, and download have been live-tested with these attachment formats:

  • Images: jpg, png, gif, tiff, bmp, webp, avif, svg
  • Video: mov, mp4, webm
  • Audio: aiff, caf, flac, m4a, mp3, ogg, wav
  • Text and structured text: txt, md, csv, json, html, xml, rtf
  • Documents: pdf, docx, xlsx, pptx
  • Contact and calendar: vcf, ics
  • Archives and compressed payloads: zip, tar, tar.gz, tgz, tar.bz2, tar.xz, gz, bz2, xz

Downloads are streamed by GUID and preserve byte-for-byte content. The first frame is metadata, followed by primary payload chunks:

for await (const frame of im.attachments.downloadStream(uploaded.attachment.guid)) {
  if (frame.type === "header") {
    console.log(frame.info.mimeType, frame.info.uti);
  }
  if (frame.type === "primaryChunk") {
    // append frame.data
  }
}

Live Photo companions transfer over HTTP: attachments.upload(...) with a companion sends multipart/form-data (file + companion parts), and downloadStream(...) yields the v1 frame sequence — a header frame carrying companionInfo, the primary chunks, then the companionChunk frames fetched from the middleware's /v1/attachments/{guid}/companion route.

7z and rar are not currently listed as tested formats because the current server test workspace does not include real encoders for those archive types. Fake files are not treated as supported fixtures.

Chat Backgrounds

Chat backgrounds are not general attachments. They use chat GUIDs and raw image bytes:

await im.chats.setBackground(
  "any;-;[email protected]",
  await readFile("photo.jpg")
);

const present = await im.chats.hasBackground("any;-;[email protected]");

await im.chats.removeBackground("any;-;[email protected]");

Supported and live-tested background image MIME types:

  • image/jpeg
  • image/png
  • image/heic
  • image/heif

Callers do not pass a MIME type. The server infers the format from the bytes and rejects image/gif, image/webp, image/avif, image/tiff, image/bmp, and image/svg+xml for chat backgrounds. Those formats may still be uploaded and sent as normal attachments; the background pipeline is stricter because the server converts the input image into Apple's background package format.

Multipart sends are atomic and can mix text, mentions, and uploaded attachments:

await im.messages.sendMultipart(chatGuid, [
  { text: "look at this " },
  { text: "@Alice", mentionedAddress: "[email protected]" },
  {
    attachmentGuid: uploaded.attachment.guid,
    attachmentName: "photo.jpg",
  },
]);

Mutate Messages

import { readFile } from "node:fs/promises";

await im.messages.edit(chatGuid, sent.guid, "updated text");
await im.messages.unsend(chatGuid, sent.guid);

await im.messages.setReaction(chatGuid, sent.guid, { kind: "love" }, true);
await im.messages.setReaction(chatGuid, sent.guid, { kind: "love" }, false);

const sticker = await im.attachments.upload({
  fileName: "sticker.png",
  data: await readFile("sticker.png"),
});

await im.messages.placeSticker(chatGuid, sent.guid, sticker.attachment.guid, {
  x: 120,
  y: 90,
});

For multipart messages, pass partIndex in mutation options to target one bubble.

Read Messages

const message = await im.messages.get(sent.guid);

const recent = await im.messages.listRecent({ pageSize: 25 });
const inChat = await im.messages.listInChat(chatGuid, {
  pageSize: 25,
  before: new Date(),
});

pageSize, when provided, must be between 1 and 100.

Inbound events

The HTTP client is outbound-only. It has no client-held event streams — long-lived gRPC streams don't exist on fetch-only runtimes, so over HTTP inbound delivery is the platform's job, not the client's. (The subscribeEvents(...) / watch(...) / events.catchUp(...) streaming APIs remain available on the gRPC transport.)

Over HTTP, to receive messages and other events, register a webhook for your project (via Spectrum) and reply from your handler using this SDK. A typical Cloudflare Worker:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const event = await request.json();
    if (event.type === "message.received") {
      const im = createHttpClient({
        address: env.IMESSAGE_HTTP_ADDRESS,
        token: () => mintToken(env),
      });
      await im.messages.sendText(event.chat.guid, "got it!");
    }
    return new Response("ok");
  },
};

Transport adapters that already receive an encoded CatchUpEventsResponse frame can map it through the same public event model without importing generated protobuf internals:

import { decodeCatchUpEvent } from "@photon-ai/advanced-imessage";

const event = decodeCatchUpEvent(frameBytes);
if (event?.type === "message.received") {
  console.log(event.message.guid);
}

Heartbeat and payload-less frames return undefined; errors reported by the generated protobuf decoder propagate to the caller.

Write responses remain authoritative: use the return value of a send/mutate call as the result of that write, not a later event.

downloadStream(...) still returns a TypedEventStream<T> (backed by the HTTP response stream). Streams support for await, .on(...), .filter(...), .map(...), .take(...), .close(), and await using.

Other Resources

await im.addresses.get("[email protected]");
await im.addresses.isIMessageAvailable("[email protected]");
await im.addresses.isFocusSilenced("[email protected]");

const created = await im.chats.create(["[email protected]"], {
  message: "hello",
});

await im.chats.markRead(created.chat.guid);
await im.chats.setTyping(created.chat.guid, true);

const group = await im.chats.create(["[email protected]", "[email protected]"]);

await im.groups.setDisplayName(group.chat.guid, "Weekend");
await im.groups.addParticipants(group.chat.guid, ["[email protected]"]);
await im.groups.getIcon(group.chat.guid);

const poll = await im.polls.create(created.chat.guid, "Lunch?", [
  "Sushi",
  "Pizza",
]);

await im.polls.vote(poll.pollMessageGuid, poll.options[0]!.optionIdentifier);

await im.locations.list();
await im.locations.get("[email protected]");

Errors

Server errors are mapped to SDK error classes:

import {
  AuthenticationError,
  NotFoundError,
  RateLimitError,
  ValidationError,
} from "@photon-ai/advanced-imessage";

try {
  await im.messages.sendText(chatGuid, "hello");
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log(error.retryable, error.context);
  }
  if (error instanceof NotFoundError) {
    console.log(error.code);
  }
  if (error instanceof AuthenticationError) {
    console.log("refresh credentials");
  }
  if (error instanceof ValidationError) {
    console.log(error.context);
  }
}

Client Options

const im = createHttpClient({
  address: "http://localhost:8080",
  token: "api-token",
  timeout: 10_000,
  retry: { maxAttempts: 4, initialDelay: 200, maxDelay: 5_000 },
  autoIdempotency: true,
});

timeout applies per call. retry retries only failures the server explicitly marked retryable, with exponential backoff and jitter; the idempotency key (when enabled) is generated once per logical call and reused across attempts, so retries dedupe server-side. autoIdempotency adds the key only to mutating calls.

For dedupe across client restarts, pass clientMessageId in a send's options — the server rejects a repeated clientMessageId with a duplicateMessage error, which means the original send succeeded.

Development

bun install
bun run check
bun run lint
bun test
bun run build

The repo is a bun-workspaces monorepo; only packages/advanced-imessage publishes to npm — it bundles the private workspace packages:

  • packages/core — shared types, errors, streaming, proto↔public mapper, generated proto codecs
  • packages/http — the fetch transport and the live resources
  • packages/grpc — the v1-compatible gRPC transport behind a lazy façade

bun run build regenerates protobuf output from buf.build/photon-hq/imessage, the canonical source published by the server's CI — pinned to a BSR commit in buf.gen.yaml; CI fails if committed codegen drifts from the pin — and builds packages/advanced-imessage/dist/.

bun run generate:http regenerates the HTTP route client (packages/http/src/generated/http) from the middleware's OpenAPI spec (gen/openapi/imessage.swagger.json). After a build, bun test tests/dist gates the published artifact: single class identity across entrypoints and no static nice-grpc in the index/http graphs. CI also bundles the SDK for workerd and boots it (tests/workerd), so a Workers-hostile dependency cannot land unnoticed.