@photon-ai/chat-adapter-imessage 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,15 +138,15 @@ export async function POST(request: Request): Promise<Response> {
138
138
 
139
139
  ### Replying
140
140
 
141
- A webhook delivery carries no live connection, but your bot can still respond: for a **DM**, the adapter rebuilds the thread from its address and sends, reacts, edits, and shows typing over spectrum-ts (gRPC) — no gateway needed.
141
+ A webhook delivery carries no live connection, but your bot can still respond: the adapter rebuilds the thread — **DM or group** — from its chat GUID via spectrum-ts's `space.get` and sends, reacts, edits, and shows typing — no gateway needed.
142
142
 
143
143
  ```typescript
144
144
  bot.onNewMention(async (thread, message) => {
145
- await thread.post("Got it!"); // works directly from a webhook delivery (DM)
145
+ await thread.post("Got it!"); // works directly from a webhook delivery
146
146
  });
147
147
  ```
148
148
 
149
- Replying into a **group** still requires that group to have been received over the [gateway listener](#gateway-setup-for-serverless) in the same session — an unseen group can't be reconstructed from its id (see [Limitations](#limitations)).
149
+ The rebuild needs spectrum-ts to know which iMessage line the chat belongs to; with **multiple lines** configured, an unseen thread can't be resolved and the adapter throws `NotImplementedError` — respond within a thread received over the [gateway listener](#gateway-setup-for-serverless) instead (see [Limitations](#limitations)).
150
150
 
151
151
  ## Gateway setup for serverless
152
152
 
@@ -238,15 +238,23 @@ IMESSAGE_WEBHOOK_SECRET=whsec_... # per-webhook signing secret
238
238
  |---------|-----------|
239
239
  | Mentions | DMs only |
240
240
  | DMs | Yes |
241
+ | Open DM (cold-start) | Yes (`openDM`) |
241
242
  | File uploads | Yes (send) |
242
243
  | Reactions (add) | Remote only |
243
- | Reactions (remove) | No |
244
+ | Reactions (remove) | Remote only (session-added tapbacks) |
244
245
  | Message editing | Remote only |
246
+ | Message delete | Remote only (iMessage unsend window) |
247
+ | Mark read | Remote only (`markRead`) |
245
248
  | Typing indicator | Remote only |
249
+ | Message effects | Remote only (`sendEffect`) |
250
+ | Mini-app cards | Remote only (`sendMiniApp`) |
251
+ | Voice messages | Remote only (`sendVoice`) |
252
+ | Chat background | Remote only (`setBackground`) |
246
253
  | Modals | Limited (Remote only) |
254
+ | Fetch single message | Yes (`fetchMessage`) |
247
255
  | Message history | No |
248
256
  | Thread/chat info | No |
249
- | Cards | No |
257
+ | Cards | Mini-app cards only (`sendMiniApp`) |
250
258
  | Streaming | No |
251
259
  | Ephemeral messages | No |
252
260
  | Webhooks | Yes (remote — Spectrum Cloud delivery) |
@@ -312,14 +320,100 @@ iMessage uses tapbacks instead of emoji reactions. The adapter maps standard emo
312
320
  | `emphasize` / `exclamation` | Emphasize |
313
321
  | `question` | Question |
314
322
 
323
+ ## Message effects
324
+
325
+ iMessage expressive-send effects animate a message when it arrives. The adapter exposes them through `sendEffect(threadId, message, effect)` — an adapter-specific extra (there is no first-class Chat SDK slot for effects). It sends the text with the effect attached and returns the sent message. Remote only; local mode throws `NotImplementedError`.
326
+
327
+ ```typescript
328
+ import { createiMessageAdapter, iMessageEffect } from "@photon-ai/chat-adapter-imessage";
329
+
330
+ const adapter = createiMessageAdapter({ local: false });
331
+
332
+ bot.onNewMention(async (thread) => {
333
+ // Friendly name…
334
+ await adapter.sendEffect(thread.id, "🎉 Task complete!", "confetti");
335
+ // …or the typed constant:
336
+ await adapter.sendEffect(thread.id, "🎉 Task complete!", iMessageEffect.confetti);
337
+ });
338
+ ```
339
+
340
+ The `effect` argument accepts a friendly name or a value from the re-exported `iMessageEffect` map. Full-screen effects: `confetti`, `fireworks`, `balloons`, `heart`, `lasers`, `celebration`, `sparkles`, `spotlight`, `echo`. Bubble effects: `slam`, `loud`, `gentle`, `invisible` (invisible ink). Effects attach to text only, so `sendEffect` requires non-empty text content; an unknown effect throws a `ValidationError`.
341
+
342
+ ## Mini-app cards
343
+
344
+ Mini-app cards are native `MSMessageExtension` balloons — a rich card with a tap-through URL, the closest iMessage gets to a Slack-style rich card rather than a bare link. The adapter exposes them through `sendMiniApp(threadId, card)` — an adapter-specific extra (there is no first-class Chat SDK slot for cards). Remote only; local mode throws `NotImplementedError`.
345
+
346
+ `sendMiniApp` takes **either** a bare URL **or** a fully-specified card.
347
+
348
+ ### Just a URL (`app(url)`)
349
+
350
+ The lightweight form: pass a URL string and iMessage renders it as a mini-app — no extension identifiers required. You can also pass a `Promise<string>` or a thunk (`() => string | Promise<string>`), so the link can be minted at send time (e.g. a signed URL).
351
+
352
+ ```typescript
353
+ import { createiMessageAdapter } from "@photon-ai/chat-adapter-imessage";
354
+
355
+ const adapter = createiMessageAdapter({ local: false });
356
+
357
+ bot.onNewMention(async (thread) => {
358
+ await adapter.sendMiniApp(thread.id, "https://example.com/menu");
359
+
360
+ // …or compute the URL lazily at send time:
361
+ await adapter.sendMiniApp(thread.id, async () => mintSignedLink(thread.id));
362
+ });
363
+ ```
364
+
365
+ ### A full card (`customizedMiniApp`)
366
+
367
+ Pass an object to control the bubble's image, captions, and the exact iMessage extension that opens on tap.
368
+
369
+ ```typescript
370
+ await adapter.sendMiniApp(thread.id, {
371
+ appName: "Poll Kit",
372
+ teamId: "TEAM123",
373
+ extensionBundleId: "com.example.pollkit.MessagesExtension",
374
+ url: "https://example.com/poll/42",
375
+ appStoreId: 1_234_567, // optional — for recipients without the extension
376
+ layout: {
377
+ caption: "Pizza night?",
378
+ subcaption: "Tap to vote",
379
+ imageTitle: "Friday",
380
+ image: pngBytes, // Uint8Array | Buffer | ArrayBuffer | Blob | FileUpload
381
+ summary: "Vote on Friday's dinner",
382
+ },
383
+ });
384
+ ```
385
+
386
+ `appName`, `teamId`, and `extensionBundleId` identify the iMessage extension that opens (receiving `url`) when the recipient taps the card; the server builds the matching `MSMessageExtensionBalloonPlugin` id from `teamId` + `extensionBundleId`. Every `layout` field is optional. The `url` accepts a string or `URL` and is validated; a missing required field or an invalid URL throws a `ValidationError`.
387
+
388
+ ## Chat background
389
+
390
+ iMessage lets a conversation carry its own wallpaper — a touch with no analog on the plain-text competitors. The adapter exposes it through `setBackground(threadId, input, options?)` — an adapter-specific extra (there is no first-class Chat SDK slot for it). It is fire-and-forget: iMessage acknowledges the control signal without returning a message, so the call resolves to `void`. Remote only; local mode throws `NotImplementedError`.
391
+
392
+ ```typescript
393
+ import { readFile } from "node:fs/promises";
394
+
395
+ // From image bytes (Uint8Array | Buffer | ArrayBuffer | Blob | FileUpload).
396
+ await adapter.setBackground(thread.id, await readFile("./wallpaper.jpg"), {
397
+ mimeType: "image/jpeg",
398
+ });
399
+
400
+ // From an http(s) URL, fetched at send time.
401
+ await adapter.setBackground(thread.id, "https://example.com/wallpaper.jpg");
402
+
403
+ // Remove the current background.
404
+ await adapter.setBackground(thread.id, "clear");
405
+ ```
406
+
407
+ Pass the literal `"clear"` to remove the current background, in-memory image bytes, or an `http(s)` URL (a `URL` or a string) that spectrum-ts fetches at send time. Image bytes need an `image/*` MIME type — supply `options.mimeType` (e.g. `"image/jpeg"`) or an `options.name` with an image extension when it can't be inferred. Local file-path strings are rejected — read the file into bytes and pass those instead. A non-image MIME type, an unresolvable MIME type, or a non-`http(s)` string throws a `ValidationError`.
408
+
315
409
  ## Limitations
316
410
 
317
- - **DMs send cold; groups are session-bound.** For a **DM**, the adapter rebuilds the thread from its address via spectrum-ts (gRPC), so it can send, react, edit, and show typing even into a thread it hasn't seen this session including a [webhook](#webhooks) delivery. A **group** chat has no by-id resolver, so addressing one requires it to have been received over the gateway/stream in the current session; cold sends to an unseen group throw `NotImplementedError`. (Local mode cannot create spaces at all — it only replies to received messages.)
318
- - **No message history.** `fetchMessages` is not supported — spectrum-ts exposes no paginated history API.
411
+ - **Cold sends need a resolvable line.** The adapter rebuilds a thread DM or group from its chat GUID via spectrum-ts's `space.get`, so it can send, react, edit, and show typing even into a thread it hasn't seen this session, including a [webhook](#webhooks) delivery. With **multiple iMessage lines** configured, spectrum-ts cannot infer which line an unseen chat belongs to, so cold sends there throw `NotImplementedError` respond within a received message's thread instead.
412
+ - **No message history.** `fetchMessages` is not supported — spectrum-ts exposes no paginated history API. Single messages resolve via `fetchMessage` (from the session cache or spectrum-ts's by-id lookup).
319
413
  - **No thread/chat info.** `fetchThread` is not supported.
320
- - **No reaction removal.** `removeReaction` is not supported.
321
- - **Local mode** supports sending and receiving (reply to an inbound message), but not reactions, typing, editing, modals, history, or thread info.
322
- - **Formatting.** iMessage is plain-text only; Markdown formatting is stripped when sending, preserving the text content.
414
+ - **Session-scoped delete & reaction removal.** `deleteMessage` unsends a message resolved from this session (subject to iMessage's ~2-minute unsend window); `removeReaction` retracts a tapback only if it was added via `addReaction` earlier in this session — spectrum-ts exposes no by-target reaction lookup.
415
+ - **Local mode** supports sending (including cold sends by chat GUID), receiving, and opening DMs, but not reactions, typing, editing, deleting, marking read, effects, modals, history, or thread info.
416
+ - **Formatting.** Markdown-typed content (`{ markdown }` or `{ ast }`) renders as native iMessage styled text on remote bold, italics, links, and lists — via spectrum-ts's `markdown()` builder. Plain strings and `{ raw }` are sent as-is (never reinterpreted as Markdown). Inbound messages always surface as plain text.
323
417
  - **Platform.** Local mode requires macOS. Cloud and self-host run anywhere.
324
418
  - **Cards.** iMessage has no structured card layouts.
325
419
 
@@ -330,7 +424,8 @@ This version re-platforms the adapter onto **spectrum-ts**. If you are upgrading
330
424
  - **Dependency** — replaces `@photon-ai/imessage-kit` + `@photon-ai/advanced-imessage-kit` with `spectrum-ts`.
331
425
  - **`IMESSAGE_SERVER_URL` is now a gRPC `host:port`** (self-host), not an `https://` / Socket.IO URL.
332
426
  - **New cloud path** — set `IMESSAGE_PROJECT_ID` + `IMESSAGE_PROJECT_SECRET` for Spectrum Cloud.
333
- - **Removed capabilities** (now `NotImplementedError`): `fetchMessages`, `fetchThread`, `removeReaction`, and cold `postMessage` to an unseen **group** thread (DMs are rebuilt from their address over gRPC — see [Limitations](#limitations)). Local `fetchMessages` (previously supported) is also removed.
427
+ - **Unsupported capabilities** (throw `NotImplementedError`): `fetchMessages` and `fetchThread` — spectrum-ts exposes no paginated history or chat-info API. Local `fetchMessages` (previously supported) is also removed. Cold `postMessage` works for DMs and groups alike (rebuilt from the chat GUID — see [Limitations](#limitations)).
428
+ - **Newly supported on spectrum-ts v8**: `deleteMessage` (unsend), `removeReaction` (retract a session-added tapback), `openDM` (cold-start a DM from a handle), `fetchMessage` (single message by id), and `markRead` — all remote-only where noted in [Features](#features).
334
429
  - **`adapter.sdk` → `adapter.app`** — the adapter now exposes the underlying `SpectrumInstance` as `adapter.app` (null until `initialize()`).
335
430
 
336
431
  ## Troubleshooting
@@ -349,10 +444,14 @@ This version re-platforms the adapter onto **spectrum-ts**. If you are upgrading
349
444
  - Verify **Full Disk Access** is granted to your terminal or application.
350
445
  - Check that iMessage is signed in and working.
351
446
 
352
- ### `NotImplementedError` from `fetchMessages` / `fetchThread` / `removeReaction`
447
+ ### `NotImplementedError` from `fetchMessages` / `fetchThread`
353
448
 
354
449
  - These are not supported by spectrum-ts. See [Limitations](#limitations).
355
450
 
451
+ ### `NotImplementedError` from `deleteMessage` / `removeReaction` / `markRead`
452
+
453
+ - These are remote-only and session-scoped. `deleteMessage` and `markRead` need the target message to have been seen this session (and delete is bound by iMessage's ~2-minute unsend window); `removeReaction` needs the tapback to have been added via `addReaction` this session. In local mode they throw. See [Limitations](#limitations).
454
+
356
455
  ## License
357
456
 
358
457
  MIT
package/dist/index.d.ts CHANGED
@@ -1,5 +1,44 @@
1
- import { Logger, Adapter, ChatInstance, WebhookOptions, AdapterPostableMessage, RawMessage, Message, FetchOptions, FetchResult, ThreadInfo, EmojiValue, ModalElement, FormattedContent, BaseFormatConverter, Root } from 'chat';
2
- import { SpectrumInstance } from 'spectrum-ts';
1
+ import { FileUpload, Logger, Adapter, ChatInstance, WebhookOptions, AdapterPostableMessage, RawMessage, Message, FetchOptions, FetchResult, ThreadInfo, EmojiValue, ModalElement, FormattedContent, BaseFormatConverter, Root } from 'chat';
2
+ import { ContentBuilder, AppUrl, SpectrumInstance } from 'spectrum-ts';
3
+ export { AppUrl } from 'spectrum-ts';
4
+ import { IMessageMessageEffect, CustomizedMiniAppInput } from 'spectrum-ts/providers/imessage';
5
+ export { CustomizedMiniAppInput, IMessageMessageEffect } from 'spectrum-ts/providers/imessage';
6
+
7
+ /**
8
+ * Image bytes for a chat background. Accepts raw bytes (`Uint8Array` / `Buffer`
9
+ * / `ArrayBuffer`), a `Blob`, or a Chat SDK `FileUpload` — whatever your image
10
+ * pipeline hands back gets normalized to the bytes spectrum-ts expects.
11
+ */
12
+ type BackgroundBytes = Uint8Array | ArrayBuffer | Blob | FileUpload;
13
+ /**
14
+ * A chat-background source. Either:
15
+ *
16
+ * - the literal `"clear"` sentinel, to remove the current background;
17
+ * - in-memory {@link BackgroundBytes};
18
+ * - an `http(s)` URL (a `URL` or a string) that spectrum-ts fetches at send
19
+ * time. Local file paths aren't accepted — read the file into bytes and pass
20
+ * those instead.
21
+ */
22
+ type BackgroundInput = "clear" | BackgroundBytes | URL | string;
23
+ /** Optional chat-background metadata. */
24
+ interface BackgroundOptions {
25
+ /**
26
+ * MIME type of the image (`image/*`). Required for raw bytes when the `name`
27
+ * carries no image extension; inferred from the URL / name otherwise.
28
+ */
29
+ mimeType?: string;
30
+ /** File name used to infer the MIME type from its extension. */
31
+ name?: string;
32
+ }
33
+ /**
34
+ * Validate and normalize a chat-background source into the spectrum-ts
35
+ * `background()` content builder. The `"clear"` sentinel removes the current
36
+ * background; URL sources are fetched by spectrum-ts at send time; byte sources
37
+ * are copied to a detached `Buffer` and tagged with a resolved `image/*` MIME
38
+ * type. The builder is a fire-and-forget control signal — remote and
39
+ * iMessage-only.
40
+ */
41
+ declare function resolveBackground(input: BackgroundInput, options?: BackgroundOptions): Promise<ContentBuilder>;
3
42
 
4
43
  /** Thread ID components for iMessage */
5
44
  interface iMessageThreadId {
@@ -69,6 +108,136 @@ interface CreateiMessageAdapterOptions {
69
108
  */
70
109
  declare function deriveAddress(serverUrl: string): string;
71
110
 
111
+ /**
112
+ * iMessage expressive-send effects, keyed by friendly name. Bubble effects
113
+ * (`slam`, `loud`, `gentle`, `invisible`) animate the message bubble; the rest
114
+ * are full-screen effects (`confetti`, `fireworks`, `balloons`, `heart`,
115
+ * `lasers`, `celebration`, `sparkles`, `spotlight`, `echo`).
116
+ *
117
+ * Re-exported from spectrum-ts so callers can reference effects by name without
118
+ * reaching into the provider package — e.g. `iMessageEffect.confetti`.
119
+ */
120
+ declare const iMessageEffect: {
121
+ readonly slam: "com.apple.MobileSMS.expressivesend.impact";
122
+ readonly loud: "com.apple.MobileSMS.expressivesend.loud";
123
+ readonly gentle: "com.apple.MobileSMS.expressivesend.gentle";
124
+ readonly invisible: "com.apple.MobileSMS.expressivesend.invisibleink";
125
+ readonly confetti: "com.apple.messages.effect.CKConfettiEffect";
126
+ readonly fireworks: "com.apple.messages.effect.CKFireworksEffect";
127
+ readonly balloons: "com.apple.messages.effect.CKBalloonEffect";
128
+ readonly heart: "com.apple.messages.effect.CKHeartEffect";
129
+ readonly lasers: "com.apple.messages.effect.CKLasersEffect";
130
+ readonly celebration: "com.apple.messages.effect.CKHappyBirthdayEffect";
131
+ readonly sparkles: "com.apple.messages.effect.CKSparklesEffect";
132
+ readonly spotlight: "com.apple.messages.effect.CKSpotlightEffect";
133
+ readonly echo: "com.apple.messages.effect.CKEchoEffect";
134
+ };
135
+ /** Accepted effect names (`"confetti"`, `"fireworks"`, …). */
136
+ type iMessageEffectName = keyof typeof iMessageEffect;
137
+ /**
138
+ * Resolve an effect argument to a spectrum-ts effect id. Accepts either a
139
+ * friendly name (`"confetti"`) or the raw effect id
140
+ * (`iMessageEffect.confetti`), so both styles work. Throws a `ValidationError`
141
+ * on an unknown effect.
142
+ */
143
+ declare function resolveEffect(effect: IMessageMessageEffect | iMessageEffectName): IMessageMessageEffect;
144
+
145
+ /**
146
+ * Image bytes for a mini-app card's inline image. Accepts raw bytes
147
+ * (`Uint8Array` / `Buffer` / `ArrayBuffer`), a `Blob`, or a Chat SDK
148
+ * `FileUpload` — whatever you already have on hand gets converted to the
149
+ * `Uint8Array` spectrum-ts expects.
150
+ */
151
+ type MiniAppImage = Uint8Array | ArrayBuffer | Blob | FileUpload;
152
+ /**
153
+ * Layout of a mini-app card — everything the recipient sees in the bubble.
154
+ * Every field is optional; supply the ones that make sense for your card. The
155
+ * `image` renders as the card's artwork, with `imageTitle` / `imageSubtitle`
156
+ * overlaid; the caption fields fill the text rows, and `summary` is the
157
+ * fallback text shown where the mini-app can't render.
158
+ */
159
+ interface MiniAppCardLayout {
160
+ caption?: string;
161
+ image?: MiniAppImage;
162
+ imageSubtitle?: string;
163
+ imageTitle?: string;
164
+ subcaption?: string;
165
+ summary?: string;
166
+ trailingCaption?: string;
167
+ trailingSubcaption?: string;
168
+ }
169
+ /**
170
+ * A native iMessage mini-app card (an `MSMessageExtension` balloon). The
171
+ * `appName`, `teamId`, and `extensionBundleId` identify the iMessage extension
172
+ * that opens when the recipient taps the card, receiving `url`; `appStoreId`
173
+ * optionally points recipients without the extension installed at its App
174
+ * Store entry.
175
+ */
176
+ interface MiniAppCard {
177
+ /** Display name of the iMessage extension. */
178
+ appName: string;
179
+ /** Optional App Store id, for recipients without the extension installed. */
180
+ appStoreId?: number;
181
+ /** Bundle id of the `MSMessageExtension` that receives the tap. */
182
+ extensionBundleId: string;
183
+ /** What the recipient sees in the bubble. */
184
+ layout?: MiniAppCardLayout;
185
+ /** Apple Developer team id that signs the extension. */
186
+ teamId: string;
187
+ /** URL handed to the extension when the card is tapped. */
188
+ url: string | URL;
189
+ }
190
+ /**
191
+ * Validate and normalize a {@link MiniAppCard} into the
192
+ * {@link CustomizedMiniAppInput} spectrum-ts's `customizedMiniApp()` builder
193
+ * expects: required identifiers are checked non-empty, `url` is normalized to a
194
+ * validated string, and the layout image (if any) is decoded to bytes.
195
+ */
196
+ declare function resolveMiniApp(card: MiniAppCard): Promise<CustomizedMiniAppInput>;
197
+ /**
198
+ * Distinguish the lightweight `app(url)` form from a fully-specified
199
+ * {@link MiniAppCard}. An {@link AppUrl} is a string, a `Promise<string>`, or a
200
+ * thunk (`() => string | Promise<string>`) — the mini-app card is a plain
201
+ * object with named fields, so it never matches any of those shapes.
202
+ */
203
+ declare function isAppUrl(input: MiniAppCard | AppUrl): input is AppUrl;
204
+
205
+ /**
206
+ * Audio bytes for a voice message. Accepts raw bytes (`Uint8Array` / `Buffer` /
207
+ * `ArrayBuffer`), a `Blob`, or a Chat SDK `FileUpload` — whatever your TTS
208
+ * pipeline hands back gets normalized to the bytes spectrum-ts expects.
209
+ */
210
+ type VoiceBytes = Uint8Array | ArrayBuffer | Blob | FileUpload;
211
+ /**
212
+ * A voice message source: in-memory {@link VoiceBytes}, or an `http(s)` URL (a
213
+ * `URL` or a string) that spectrum-ts fetches at send time. Local file paths
214
+ * aren't accepted — read the file into bytes and pass those instead.
215
+ */
216
+ type VoiceInput = VoiceBytes | URL | string;
217
+ /** Optional voice-message metadata. */
218
+ interface VoiceOptions {
219
+ /**
220
+ * Playback duration in seconds, surfaced on the waveform bubble. Optional —
221
+ * iMessage derives the waveform from the audio itself when omitted.
222
+ */
223
+ duration?: number;
224
+ /**
225
+ * MIME type of the audio (`audio/*`). Required for raw bytes when the `name`
226
+ * carries no audio extension; inferred from the URL / name otherwise.
227
+ */
228
+ mimeType?: string;
229
+ /** File name handed to spectrum-ts (also used to infer the MIME type). */
230
+ name?: string;
231
+ }
232
+ /**
233
+ * Validate and normalize a voice source into the spectrum-ts `voice()` content
234
+ * builder. URL sources are fetched by spectrum-ts at send time; byte sources
235
+ * are copied to a detached `Buffer` and tagged with a resolved `audio/*` MIME
236
+ * type. The builder is sent as a native iMessage voice note (a waveform
237
+ * bubble), not an audio-file attachment.
238
+ */
239
+ declare function resolveVoice(input: VoiceInput, options?: VoiceOptions): Promise<ContentBuilder>;
240
+
72
241
  declare class iMessageAdapter implements Adapter {
73
242
  readonly name = "imessage";
74
243
  readonly userName: string;
@@ -95,23 +264,112 @@ declare class iMessageAdapter implements Adapter {
95
264
  * Handle a Spectrum Cloud webhook delivery (signed JSON `messages` event).
96
265
  *
97
266
  * Verifies the `X-Spectrum-Signature` HMAC, then routes the message into the
98
- * Chat SDK. Webhook deliveries are receive-only: a delivered thread has no
99
- * live spectrum-ts `Space`, so replying/reacting still requires the gateway
100
- * stream (see `startGatewayListener`).
267
+ * Chat SDK. A delivered thread has no live spectrum-ts `Space`, but the
268
+ * adapter rebuilds one from the chat GUID on demand (see `resolveSpace`), so
269
+ * replying works directly from a webhook delivery.
101
270
  *
102
271
  * @see https://photon.codes/docs/webhooks/overview
103
272
  */
104
273
  handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
274
+ /**
275
+ * Build the spectrum content for an outbound message. Markdown-typed inputs
276
+ * are sent via `markdown()` so remote iMessage renders them as native styled
277
+ * text; raw/string/card inputs stay plain `text()`. Returns the rendered
278
+ * `body` too so callers can skip an empty send.
279
+ */
280
+ private toSpectrumContent;
105
281
  postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage>;
282
+ /**
283
+ * Send a message with an iMessage expressive-send effect — a bubble effect
284
+ * (`slam`, `loud`, `gentle`, `invisible`) or a full-screen effect
285
+ * (`confetti`, `fireworks`, `balloons`, `heart`, `lasers`, `celebration`,
286
+ * `sparkles`, `spotlight`, `echo`). Not part of the Chat SDK `Adapter`
287
+ * interface — exposed as an adapter-specific extra (e.g. celebratory confetti
288
+ * on task completion). Remote only.
289
+ *
290
+ * The `effect` argument accepts a friendly name (`"confetti"`) or a value from
291
+ * the re-exported `iMessageEffect` map. Effects attach to text content only,
292
+ * so this requires non-empty text.
293
+ */
294
+ sendEffect(threadId: string, message: AdapterPostableMessage, effect: IMessageMessageEffect | iMessageEffectName): Promise<RawMessage>;
295
+ /**
296
+ * Send an iMessage mini-app card — an `MSMessageExtension` balloon, the
297
+ * closest iMessage gets to a rich card (à la Slack Block Kit) instead of a
298
+ * bare link. Not part of the Chat SDK `Adapter` interface — exposed as an
299
+ * adapter-specific extra. Remote only.
300
+ *
301
+ * Two forms:
302
+ *
303
+ * - **Just a URL** — pass a string (or a `Promise`/thunk resolving to one, so
304
+ * the link can be minted at send time). This is the lightweight `app(url)`
305
+ * card: the URL is rendered as a mini-app with no extension identifiers
306
+ * required.
307
+ * - **A full {@link MiniAppCard}** — pass an object to control the bubble's
308
+ * image, captions, and the exact iMessage extension that opens on tap. Its
309
+ * `appName`, `teamId`, and `extensionBundleId` identify that extension.
310
+ */
311
+ sendMiniApp(threadId: string, url: AppUrl): Promise<RawMessage>;
312
+ sendMiniApp(threadId: string, card: MiniAppCard): Promise<RawMessage>;
313
+ /**
314
+ * Send a native iMessage voice note — a real, playable waveform bubble (the
315
+ * message renders with `isAudioMessage`), not an audio file dropped in as an
316
+ * attachment. A natural fit for TTS-capable bots that reply with speech. Not
317
+ * part of the Chat SDK `Adapter` interface — exposed as an adapter-specific
318
+ * extra. Remote only.
319
+ *
320
+ * The `input` is either in-memory audio bytes (`Uint8Array` / `Buffer` /
321
+ * `ArrayBuffer`, a `Blob`, or a Chat SDK `FileUpload`) or an `http(s)` URL (a
322
+ * `URL` or a string) that is fetched at send time. Audio bytes need an
323
+ * `audio/*` MIME type — supply `options.mimeType` (e.g. `"audio/mp4"`) or an
324
+ * `options.name` with an audio extension when it can't be inferred.
325
+ */
326
+ sendVoice(threadId: string, input: VoiceInput, options?: VoiceOptions): Promise<RawMessage>;
327
+ /**
328
+ * Set or clear the chat background — the wallpaper behind a conversation, an
329
+ * iMessage-only touch with no analog on the plain-text competitors. Not part
330
+ * of the Chat SDK `Adapter` interface — exposed as an adapter-specific extra.
331
+ * Remote only.
332
+ *
333
+ * The `input` is either the literal `"clear"` (to remove the current
334
+ * background), in-memory image bytes (`Uint8Array` / `Buffer` / `ArrayBuffer`,
335
+ * a `Blob`, or a Chat SDK `FileUpload`), or an `http(s)` URL (a `URL` or a
336
+ * string) that is fetched at send time. Image bytes need an `image/*` MIME
337
+ * type — supply `options.mimeType` (e.g. `"image/jpeg"`) or an `options.name`
338
+ * with an image extension when it can't be inferred.
339
+ *
340
+ * Fire-and-forget: iMessage acknowledges the control signal without returning
341
+ * a message, so this resolves to `void` rather than a {@link RawMessage}.
342
+ */
343
+ setBackground(threadId: string, input: BackgroundInput, options?: BackgroundOptions): Promise<void>;
106
344
  editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise<RawMessage>;
107
- deleteMessage(_threadId: string, _messageId: string): Promise<void>;
345
+ deleteMessage(threadId: string, messageId: string): Promise<void>;
108
346
  parseMessage(raw: unknown): Message;
347
+ /**
348
+ * Fetch a single message by id. spectrum-ts can resolve a message by id
349
+ * (from the inbound cache or the provider's by-id lookup) even though it has
350
+ * no paginated history API, so single-message reads work where
351
+ * `fetchMessages` cannot. Returns `null` when the message can't be resolved.
352
+ */
353
+ fetchMessage(threadId: string, messageId: string): Promise<Message | null>;
109
354
  fetchMessages(_threadId: string, _options?: FetchOptions): Promise<FetchResult>;
110
355
  fetchThread(_threadId: string): Promise<ThreadInfo>;
111
356
  channelIdFromThreadId(threadId: string): string;
112
357
  addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
113
- removeReaction(_threadId: string, _messageId: string, _emoji: EmojiValue | string): Promise<void>;
358
+ removeReaction(_threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
114
359
  startTyping(threadId: string, _status?: string): Promise<void>;
360
+ /**
361
+ * Cold-start a DM with a phone number / handle. spectrum-ts resolves (or
362
+ * creates) the 1:1 conversation from the participant via `space.create`, so
363
+ * the bot can message a user it has never received from. Returns the encoded
364
+ * thread id, ready to pass to `postMessage`.
365
+ */
366
+ openDM(userId: string): Promise<string>;
367
+ /**
368
+ * Mark a received message (and the conversation up to it) as read, surfacing
369
+ * a read receipt where iMessage supports one. Remote only; not part of the
370
+ * Chat SDK `Adapter` interface — exposed as an adapter-specific extra.
371
+ */
372
+ markRead(threadId: string, messageId: string): Promise<void>;
115
373
  openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
116
374
  viewId: string;
117
375
  }>;
@@ -128,13 +386,20 @@ declare class iMessageAdapter implements Adapter {
128
386
  *
129
387
  * Prefers a live `Space` cached from the inbound stream (correct sending
130
388
  * line, no extra round-trip). On a miss — e.g. a webhook-only deployment, or
131
- * a cold send — it reconstructs the Space over gRPC via
132
- * `imessage(app).space([address])`. Only DMs can be rebuilt from a chatGuid:
133
- * the resolver derives them from the peer address, whereas an unseen group
134
- * has no by-id resolver (passing participants would create a *new* chat).
135
- * Returns `undefined` when no Space can be obtained.
389
+ * a cold send — it rebuilds the Space from the chat GUID via
390
+ * `imessage(app).space.get(chatGuid)`, which works for DMs and groups alike.
391
+ * The rebuild can still fail when several iMessage lines are configured and
392
+ * spectrum-ts cannot infer which line the chat belongs to (`space.get`
393
+ * requires `params.phone` there). Returns `undefined` when no Space can be
394
+ * obtained.
136
395
  */
137
396
  private resolveSpace;
397
+ /**
398
+ * The iMessage provider's Space namespace (`get` / `create`). `HasProvider`
399
+ * over the default provider tuple won't narrow to `true`, so `imessage(app)`
400
+ * types as `never` — cast to the slice of the instance we use.
401
+ */
402
+ private platformSpaces;
138
403
  private requireSpace;
139
404
  private resolveMessage;
140
405
  }
@@ -153,11 +418,24 @@ declare function createiMessageAdapter(config?: CreateiMessageAdapterOptions): i
153
418
  /**
154
419
  * iMessage format conversion using AST-based parsing.
155
420
  *
156
- * iMessage supports plain text only -- no rich formatting syntax.
157
- * The converter strips formatting markers and outputs clean plain text,
158
- * preserving structure (lists, blockquotes, code blocks) with whitespace.
421
+ * Remote iMessage (via spectrum-ts) renders CommonMark natively as styled text
422
+ * (bold/italic/links etc. via UTF-16 formatting ranges), so markdown-typed
423
+ * outbound content is sent through spectrum's `markdown()` builder verbatim --
424
+ * see `renderPostableContent`. The plain-text rendering here (`fromAst`) is
425
+ * still used for inbound parsing, `renderFormatted`, and the raw/card fallback
426
+ * paths: it strips formatting markers and preserves structure (lists,
427
+ * blockquotes, code blocks) with whitespace.
159
428
  */
160
429
 
430
+ /**
431
+ * The spectrum content a postable message should be sent as: the body string
432
+ * plus whether spectrum should render it as native markdown (styled text) or
433
+ * pass it through as plain text.
434
+ */
435
+ interface PostableContent {
436
+ body: string;
437
+ markdown: boolean;
438
+ }
161
439
  declare class iMessageFormatConverter extends BaseFormatConverter {
162
440
  /**
163
441
  * Render an AST to iMessage plain text format.
@@ -169,7 +447,21 @@ declare class iMessageFormatConverter extends BaseFormatConverter {
169
447
  * iMessage sends plain text, so we just parse it as markdown.
170
448
  */
171
449
  toAst(text: string): Root;
450
+ /**
451
+ * Decide how a postable message should reach spectrum-ts.
452
+ *
453
+ * Markdown-typed inputs -- `{ markdown }` and `{ ast }` -- carry CommonMark
454
+ * the caller wants styled, so their source is preserved verbatim and flagged
455
+ * `markdown: true`; the adapter sends it via spectrum's `markdown()` builder,
456
+ * which renders bold/italic/links/lists as native iMessage styled text.
457
+ *
458
+ * Everything else is pass-through-as-is by contract -- a plain `string` or
459
+ * `{ raw }` must not have stray `*`/`_` reinterpreted as formatting, and
460
+ * cards fall back to plain text -- so those are rendered to plain text and
461
+ * flagged `markdown: false`.
462
+ */
463
+ renderPostableContent(message: AdapterPostableMessage): PostableContent;
172
464
  private nodeToPlainText;
173
465
  }
174
466
 
175
- export { type CreateiMessageAdapterOptions, type IMessageClientEntry, createiMessageAdapter, deriveAddress, iMessageAdapter, type iMessageAdapterConfig, type iMessageAdapterLocalConfig, type iMessageAdapterRemoteConfig, iMessageFormatConverter, type iMessageThreadId };
467
+ export { type BackgroundBytes, type BackgroundInput, type BackgroundOptions, type CreateiMessageAdapterOptions, type IMessageClientEntry, type MiniAppCard, type MiniAppCardLayout, type MiniAppImage, type VoiceBytes, type VoiceInput, type VoiceOptions, createiMessageAdapter, deriveAddress, iMessageAdapter, type iMessageAdapterConfig, type iMessageAdapterLocalConfig, type iMessageAdapterRemoteConfig, iMessageEffect, type iMessageEffectName, iMessageFormatConverter, type iMessageThreadId, isAppUrl, resolveBackground, resolveEffect, resolveMiniApp, resolveVoice };