/** * iMessage attachment model. * * File transfer states (raw `transfer_state` values): * * -1 archiving metadata exists, no local file yet * 0 waitingForAccept queued, transfer not started * 1 accepted transfer request accepted * 2 preparing encoding / compressing * 3 transferring active data transmission * 4 finalizing writing to disk / post-processing * 5 finished file available on disk * 6 error non-recoverable failure * 7 recoverableError transient failure, may retry * * Normalized into four consumer-facing states plus an `unknown` sentinel: * * pending ← -1, 0 * transferring ← 1, 2, 3, 4 * complete ← 5 * failed ← 6, 7 * unknown ← null / any unrecognised code (forward-compat with * future macOS `transfer_state` values) */ /** Normalized attachment transfer status. */ type TransferStatus = 'pending' | 'transferring' | 'complete' | 'failed' | 'unknown'; /** Attachment linked to a message. */ interface Attachment { /** Stable identifier derived from `attachment.guid`. */ readonly id: string; /** Preferred file name: `transfer_name` if available, otherwise local path basename. */ readonly fileName: string | null; /** Absolute local path when the file exists on disk. */ readonly localPath: string | null; /** MIME type such as `image/jpeg`. */ readonly mimeType: string; /** Uniform Type Identifier such as `public.jpeg`. */ readonly uti: string | null; /** File size in bytes. */ readonly sizeBytes: number; /** Typed transfer status resolved from `attachment.transfer_state`. */ readonly transferStatus: TransferStatus; /** Whether the attachment was sent by the local user. */ readonly isFromMe: boolean; /** Whether the attachment is a sticker. */ readonly isSticker: boolean; /** Flagged by Apple Communication Safety (child-safety content scanning). */ readonly isSensitiveContent: boolean; /** Accessibility alt text or short description when available. */ readonly altText: string | null; /** Attachment creation timestamp. */ readonly createdAt: Date; } /** * iMessage transport service identification. * * Known service strings from chat.db: * - "iMessage" * - "SMS" * - "RCS" */ /** Transport protocol used by a conversation or message. */ type Service = 'iMessage' | 'SMS' | 'RCS'; /** * iMessage chat (conversation) model. * * Chat style values (`chat.style`): * * 43 (ASCII '+') group chat * 45 (ASCII '-') DM (1-on-1) chat */ /** Normalized chat kind derived from `chat.style`. */ type ChatKind = 'dm' | 'group' | 'unknown'; /** Chat summary. */ interface Chat { /** Normalized chat id suitable for routing and matching. */ readonly chatId: string; /** User-visible display name. */ readonly name: string | null; /** Transport used by the chat when known. */ readonly service: Service | null; /** Normalized chat kind. */ readonly kind: ChatKind; /** Account login associated with the chat when known. */ readonly account: string | null; /** Chat is archived. */ readonly isArchived: boolean; /** Chat is in the filtered / unknown-senders bucket. */ readonly isFiltered: boolean; /** Incoming messages are silently dropped for this chat. */ readonly dropsIncomingMessages: boolean; /** Incoming messages are automatically deleted. */ readonly autoDeletesIncomingMessages: boolean; /** Last read timestamp for the chat. */ readonly lastReadAt: Date | null; /** Number of unread incoming messages. */ readonly unreadCount: number; /** Timestamp of the most recent message in the chat. */ readonly lastMessageAt: Date | null; } /** * iMessage reaction model (Tapback, sticker, poll). * * Reaction codes (`associated_message_type`): * * 0, 2, 3 app / balloon marker (Polls, msgine, etc.) — NOT a reaction; * `resolveReactionMeta` intentionally returns `{kind: null}`. * 1000 sticker placement * 2000–2007 add reaction (love, like, dislike, laugh, emphasize, question, emoji, sticker) * 3000–3007 remove reaction (base = code − 1000) * 4000 poll vote */ /** Normalized Tapback / sticker / poll reaction kind. */ type ReactionKind = 'love' | 'like' | 'dislike' | 'laugh' | 'emphasize' | 'question' | 'emoji' | 'sticker' | 'pollVote'; /** UTF-16 range inside the target message text. */ interface ReactionTextRange { /** UTF-16 start index. */ readonly location: number; /** UTF-16 length from `location`. */ readonly length: number; } /** Tapback or sticker reaction attached to another message. */ interface Reaction { /** Normalized reaction kind. */ readonly kind: ReactionKind; /** Public message id that this reaction targets. */ readonly targetMessageId: string | null; /** Emoji payload for emoji reactions; `null` for classic Tapbacks. */ readonly emoji: string | null; /** Substring range inside the target message text. */ readonly textRange: ReactionTextRange; /** `true` when this row removes a previous reaction. */ readonly isRemoved: boolean; } /** * iMessage message model. * * Message item types (`message.item_type`, Apple `IMItemType`): * * 0 Message text / media message * 1 ParticipantChange add / remove (sub-typed by `group_action_type`) * 2 GroupTitleChange display name changed * 3 GroupAction photo, background, or other group action * 4 LocationShareStatusChange location sharing status change * 5 MessageAction message-level action (kept audio, etc.) * 6 TUConversation FaceTime / TelephonyUtilities conversation event */ /** * Normalized message kind derived from `message.item_type`. * * Orthogonal to reactions — a row with a non-null `reaction` payload still * carries its wire-level kind here (typically `'text'` for tapbacks). */ type MessageKind = 'text' | 'memberAdded' | 'memberRemoved' | 'nameChanged' | 'groupAction' | 'unknown'; /** Message expiration state derived from `message.expire_state`. */ type ExpireStatus = 'active' | 'willExpire' | 'expired'; /** Sharing activity state derived from `message.share_status`. */ type ShareActivity = 'none' | 'pending' | 'active' | 'unknown'; /** Sharing direction derived from `message.share_direction`. */ type ShareDirection = 'none' | 'incoming' | 'outgoing' | 'unknown'; /** Scheduled message kind derived from `message.schedule_type`. */ type ScheduleKind = 'none' | 'sendLater' | 'unknown'; /** Scheduled message status derived from `message.schedule_state`. */ type ScheduleStatus = 'none' | 'pending' | 'sent' | 'failed' | 'unknown'; /** Message model. */ interface Message { /** Local store row id. Useful for cursors and watcher checkpoints. */ readonly rowId: number; /** Stable message id derived from `message.guid`. */ readonly id: string; /** * Normalized chat id suitable for routing and matching. * * `null` only when all authoritative sources are missing from the row * (no `chat_guid` / `chat_id` via join, no `ck_chat_id`, and the message * is in-bound so no `destination_caller_id`). Observable in rare WAL * races before `chat_message_join` flushes — treat as "chat unknown, * skip routing". */ readonly chatId: string | null; /** Chat kind derived from the owning chat. */ readonly chatKind: ChatKind; /** Database-associated remote participant handle. */ readonly participant: string | null; /** Transport used for this message. `null` when the raw column is missing or unrecognized. */ readonly service: Service | null; /** Best-effort decoded text body. */ readonly text: string | null; /** Normalized message kind. */ readonly kind: MessageKind; /** Whether this row was sent by the local user. */ readonly isFromMe: boolean; /** Local read state. */ readonly isRead: boolean; /** Send completed on the local device. */ readonly isSent: boolean; /** Delivery was confirmed by the recipient device. */ readonly isDelivered: boolean; /** The message downgraded from iMessage to SMS. */ readonly isDowngraded: boolean; /** Recipient device actually displayed a notification. */ readonly didNotifyRecipient: boolean; /** Focus / DND auto-reply message. */ readonly isAutoReply: boolean; /** System-generated message row. */ readonly isSystem: boolean; /** Forwarded message. */ readonly isForwarded: boolean; /** Audio message. */ readonly isAudioMessage: boolean; /** Audio message has been played. */ readonly isPlayed: boolean; /** Message content can expire. */ readonly isExpirable: boolean; /** `true` when `errorCode !== 0`. */ readonly hasError: boolean; /** Raw error code for diagnostics. */ readonly errorCode: number; /** Marked as spam. */ readonly isSpam: boolean; /** Contact Key Verification passed. */ readonly isContactKeyVerified: boolean; /** Group chat has an unseen mention. */ readonly hasUnseenMention: boolean; /** Delivered quietly without notification. */ readonly wasDeliveredQuietly: boolean; /** Emergency SOS message. */ readonly isEmergencySos: boolean; /** Critical alert message. */ readonly isCriticalAlert: boolean; /** Message was sent or received off-grid via satellite. */ readonly isOffGrid: boolean; /** Sent or received timestamp. */ readonly createdAt: Date; /** Delivery confirmation timestamp. */ readonly deliveredAt: Date | null; /** Read timestamp. */ readonly readAt: Date | null; /** Audio playback timestamp. */ readonly playedAt: Date | null; /** Edit timestamp. */ readonly editedAt: Date | null; /** Unsend / retract timestamp. */ readonly retractedAt: Date | null; /** Recovery-from-trash timestamp. */ readonly recoveredAt: Date | null; /** Direct reply target message id. */ readonly replyToMessageId: string | null; /** Thread root message id. */ readonly threadRootMessageId: string | null; /** Participant affected by a membership change event. */ readonly affectedParticipant: string | null; /** New group name for rename events. */ readonly newGroupName: string | null; /** Send effect identifier. */ readonly sendEffect: string | null; /** Rich-message / app extension bundle identifier. */ readonly appBundleId: string | null; /** Invisible Ink content has been revealed. */ readonly isInvisibleInkRevealed: boolean; /** Expiration state derived from `message.expire_state`. */ readonly expireStatus: ExpireStatus; /** Sharing activity state derived from `message.share_status`. */ readonly shareActivity: ShareActivity; /** Sharing direction derived from `message.share_direction`. */ readonly shareDirection: ShareDirection; /** Scheduled-message kind derived from `message.schedule_type`. */ readonly scheduleKind: ScheduleKind; /** Scheduled-message status derived from `message.schedule_state`. */ readonly scheduleStatus: ScheduleStatus; /** Number of parts / segments in the message. */ readonly segmentCount: number; /** * Whether `message.cache_has_attachments` is set on the underlying row. * * Use this to detect a WAL race where a message row is already visible * but its `message_attachment_join` entries have not yet been flushed — * `hasAttachments === true && attachments.length === 0` means the * attachments will arrive on a subsequent batch. Treat such messages * as provisional and re-query (or wait for the next watcher tick). */ readonly hasAttachments: boolean; /** Reaction payload when this row is a reaction. */ readonly reaction: Reaction | null; /** Attachments linked to the message. */ readonly attachments: readonly Attachment[]; } /** * Incoming message dispatcher. * * Routes batches of messages from the watch source to user callbacks * and plugin sinks. */ /** Callback for handling a single message. */ type MessageCallback = (message: Message) => void | Promise; /** User-provided event handlers for watcher-observed messages. */ interface DispatchEvents { /** Called for every incoming (non-from-me) message. */ readonly onIncomingMessage?: MessageCallback; /** Called for incoming direct (1-on-1) messages. */ readonly onDirectMessage?: MessageCallback; /** Called for incoming group messages. */ readonly onGroupMessage?: MessageCallback; /** * Called for every from-me message observed in the database, regardless * of origin (this SDK, other Apple clients, Messages.app UI). * * This is the authoritative source of "my send landed in chat.db" — * `sdk.send()` itself resolves on AppleScript dispatch and does not * wait for a chat.db row. */ readonly onFromMeMessage?: MessageCallback; /** Called when dispatch encounters an error. */ readonly onError?: (error: Error) => void; } /** * Send request type. */ /** Arguments for `sdk.send()`. */ interface SendRequest { /** * Recipient. One of: * - Phone number (`+1234567890`) or email (`user@example.com`) * for a DM. * - A chatId shape returned by the SDK (e.g. `message.chatId` or * `chat.chatId`) to reply to or continue an existing conversation. * * Group chatIds must come from the SDK — they encode Messages.app * internal GUIDs that cannot be reconstructed from user data. */ readonly to: string; /** Message body. Optional when `attachments` is non-empty. */ readonly text?: string; /** * Local file paths only. Download remote URLs yourself and pass * the resulting local path. * * Paths outside TCC-safe directories (`~/Pictures`, `~/Downloads`, * `~/Documents`) are copied into `~/Pictures/imsg_temp_*` before * AppleScript dispatch — the Messages.app sandbox rejects direct * attachment from arbitrary locations. The copies are removed by a * background cleanup pass (default: every 5 min, files older than * 10 min), NOT synchronously after send. */ readonly attachments?: readonly string[]; } /** * Send port definition. * * The application-layer contract for sending messages. * Infra implements SendPort; application orchestrators depend on it. * * The data shape (`SendRequest`) lives in `types/send.ts` as the canonical * source; this module defines only the port contract. */ /** * Application-facing send capability. Implemented by infra. * * Resolves when Messages.app accepts the AppleScript dispatch — * acceptance, not delivery. For the chat.db row or later `isDelivered` * transitions observe the watcher (`onFromMeMessage` callback on * `startWatching`, or plugin hook `onFromMe`). * * Throws `IMessageError` on validation, AppleScript dispatch, or * cancellation. */ interface SendPort { send(request: SendRequest): Promise; } /** * Filter shapes consumed by `sdk.getMessages()` and `sdk.listChats()`. * * Both shapes flow through the query adapter (`infra/db/contract.ts`) down * to the active schema builder (`macos26.ts`) and back as typed domain models. */ interface MessageQuery { /** * Chat identifier to scope results to a single conversation. * * Accepts `service;+;guid`, `service;-;address`, bare `chat`, or a * bare recipient address — matched against `chat.chat_identifier` and * `chat.guid` after normalization via `ChatId.fromUserInput`. */ readonly chatId?: string; /** Remote handle (phone/email) to match against `handle.id`. */ readonly participant?: string; /** Transport filter. */ readonly service?: Service; /** `true` → only outgoing rows; `false` → only incoming; omitted → both. */ readonly isFromMe?: boolean; /** `true` → only read rows; `false` → only unread; omitted → both. */ readonly isRead?: boolean; /** `true` → only rows with ≥1 attachment; `false` → only rows with none; omitted → both. */ readonly hasAttachments?: boolean; /** Skip Tapback / sticker reactions (rows with non-null `associated_message_type`). */ readonly excludeReactions?: boolean; /** Inclusive lower bound on `message.date` (`message.date >= since`). */ readonly since?: Date; /** Exclusive upper bound on `message.date` (`message.date < before`). */ readonly before?: Date; /** * Case-insensitive substring search over decoded message text. * * Runs as an application-layer scan: rows are fetched and their * `text` (decoded from the `attributedBody` BLOB when needed) is * matched against the query. This is NOT a SQL `LIKE` and does not * use an index — narrow the result set with `since` / `chatId` / * `participant` / `limit` for large databases. */ readonly search?: string; /** Page size. Omit for unbounded (watch your memory on large DBs). */ readonly limit?: number; /** Row offset. Requires or implies `limit`. */ readonly offset?: number; } interface ChatQuery { /** * Chat identifier to scope results to a single conversation. * * Accepts `service;+;guid`, `service;-;address`, bare `chat`, or a * bare recipient address — matched against `chat.chat_identifier` and * `chat.guid` after normalization via `ChatId.fromUserInput`. */ readonly chatId?: string; /** Restrict to a single chat kind. Omit to include both. */ readonly kind?: 'group' | 'dm'; /** Transport filter. */ readonly service?: Service; /** `true` → only archived chats; `false` → only non-archived; omitted → both. */ readonly isArchived?: boolean; /** `true` → only chats with ≥1 unread incoming message; `false` → only chats with none; omitted → both. */ readonly hasUnread?: boolean; /** * Ordering. * - `'recent'` → by most recent message timestamp (newest first) * - `'name'` → by display name (ASC, nulls last) */ readonly sortBy?: 'recent' | 'name'; /** Case-insensitive substring search against `display_name` and `chat_identifier`. */ readonly search?: string; /** Page size. Omit for unbounded. */ readonly limit?: number; /** Row offset. Requires or implies `limit`. */ readonly offset?: number; } /** * Plugin type definitions. * * Defines the contract that user-authored plugins implement. */ interface BeforeMessageQueryContext { readonly query: MessageQuery; } interface AfterMessageQueryContext { readonly query: MessageQuery; readonly messages: readonly Message[]; } interface BeforeChatQueryContext { readonly query: ChatQuery; } interface AfterChatQueryContext { readonly query: ChatQuery; readonly chats: readonly Chat[]; } interface BeforeSendContext { readonly request: SendRequest; } interface AfterSendContext { readonly request: SendRequest; } interface MessageContext { readonly message: Message; } interface PluginErrorContext { readonly error: Error; readonly context?: string; } /** * Plugin lifecycle and event hooks. * * **Dispatch modes** (see `infra/plugin.ts`): * * - **Interrupting (sequential, fail-fast)** — `onBeforeMessageQuery`, * `onBeforeChatQuery`, `onBeforeSend`. Plugins run in pre / normal / * post order. The FIRST plugin that throws aborts the surrounding * SDK operation (`getMessages` / `listChats` / `send`) — the remaining * plugins are not invoked, and the caller receives an `IMessageError` * whose `code` matches the operation (`DATABASE` for queries, `SEND` * for sends) and whose `cause` is the plugin's thrown error. These * hooks are therefore a legitimate gate: throw to reject auth, rate * limits, or content policy. * * - **Sequential (observing)** — `onInit`, `onError`, `onDestroy`. Run * one at a time. A throw is captured, logged, and reported to `onError` * (except for `onError` itself, which is logged once to avoid * recursion). The surrounding operation continues so that a single * plugin failure cannot take down the SDK lifecycle. * * - **Parallel (observing)** — `onAfterMessageQuery`, `onAfterChatQuery`, * `onIncomingMessage`, `onFromMe`. All matching plugins run concurrently; * their promises are awaited as a group. Individual failures are * reported to `onError`; the query result / incoming message still * propagates to the caller or other plugins. * * Within each mode, plugins with `order: 'pre'` run before unset, and * `order: 'post'` run last. * * Hook return values are ignored — plugins cannot rewrite the request or * the result. Interception is all-or-nothing via throw from an * interrupting hook. */ interface PluginHooks { /** Sequential (observing). Called once per plugin when the SDK initialises. Throws are routed to `onError`. */ onInit?: () => void | Promise; /** Interrupting. Before each `sdk.getMessages()`. Throw to abort the query with `IMessageError` (code `DATABASE`). */ onBeforeMessageQuery?: (ctx: BeforeMessageQueryContext) => void | Promise; /** Parallel (observing). After `sdk.getMessages()` resolves. Throws are routed to `onError`. */ onAfterMessageQuery?: (ctx: AfterMessageQueryContext) => void | Promise; /** Interrupting. Before each `sdk.listChats()`. Throw to abort the query with `IMessageError` (code `DATABASE`). */ onBeforeChatQuery?: (ctx: BeforeChatQueryContext) => void | Promise; /** Parallel (observing). After `sdk.listChats()` resolves. Throws are routed to `onError`. */ onAfterChatQuery?: (ctx: AfterChatQueryContext) => void | Promise; /** * Interrupting. Before each `sdk.send()` dispatches via AppleScript. * Throw to reject the send with `IMessageError` (code `SEND`); the * plugin's error is attached as `cause`. Use as an auth / policy gate. */ onBeforeSend?: (ctx: BeforeSendContext) => void | Promise; /** * Parallel (observing). Fires only after AppleScript dispatch * succeeded; failed sends propagate to the caller and do NOT fire * this hook. Reports "accepted by Messages.app" — for * "landed in chat.db / delivery state" use `onFromMe` via the * watcher. `onError` is not invoked on send failure: it covers * plugin hook failures, not SDK operation failures. */ onAfterSend?: (ctx: AfterSendContext) => void | Promise; /** Parallel (observing). Fired for each incoming (non-from-me) message the watcher observes. */ onIncomingMessage?: (ctx: MessageContext) => void | Promise; /** * Parallel (observing). Fired for each from-me message the watcher observes, regardless * of origin (this SDK, other Apple clients, Messages.app UI). This is the * authoritative source of "my send landed in chat.db" — `sdk.send()` * itself only reports AppleScript dispatch, not DB arrival. */ onFromMe?: (ctx: MessageContext) => void | Promise; /** Sequential (observing). Receives errors from any other hook; own throws are logged, not re-dispatched. */ onError?: (ctx: PluginErrorContext) => void | Promise; /** Sequential (observing). Called once per plugin on `sdk.close()`. Throws are routed to `onError`. */ onDestroy?: () => void | Promise; } interface Plugin extends PluginHooks { readonly name: string; readonly version?: string; readonly description?: string; readonly order?: 'pre' | 'post'; } /** * Public SDK configuration types. */ interface IMessageConfig { /** Path to the Messages SQLite database. @default ~/Library/Messages/chat.db */ readonly databasePath?: string; /** Maximum concurrent send operations. @default 10 */ readonly maxConcurrentSends?: number; /** * Ceiling (ms) for a single osascript dispatch. On timeout the * child is killed and the attempt counts as a failure; `retry` * decides whether to try again (3 attempts total by default), and * `sdk.send()` throws `SendError` only after attempts are * exhausted. Large attachments need headroom. Governs per-script * execution only — `sdk.send()` never waits for chat.db arrival. * @default 30_000 */ readonly sendTimeout?: number; /** Enable verbose debug logging. @default false */ readonly debug?: boolean; /** Plugin list. */ readonly plugins?: readonly Plugin[]; } /** * IMessageSDK — composition root. * * Wires domain, application, and infrastructure layers into a * single public API. All external dependencies are resolved here. */ declare class IMessageSDK implements SendPort { private readonly databasePath; private readonly debug; private readonly database; private readonly tempFiles; private readonly sender; private readonly plugins; private readonly abortController; private watchSource; private destroyed; private closePromise; constructor(config?: IMessageConfig); /** Register a plugin. Late registrations are auto-initialized. */ use(plugin: Plugin): this; /** Query messages with optional filters. */ getMessages(query?: MessageQuery): Promise; /** List chats with optional filters and sorting. */ listChats(query?: ChatQuery): Promise; /** * Resolves when Messages.app accepts the AppleScript dispatch — acceptance, * not delivery. For the chat.db row or later `isDelivered` transitions * observe the watcher (`onFromMeMessage` callback on `startWatching`, or * plugin hook `onFromMe`); for "accepted" only, `onAfterSend` is lighter. * * The first `onBeforeSend` to throw aborts dispatch with `IMessageError` * (code `SEND`, thrown error as `cause`); `onAfterSend` does not fire. * Also throws `IMessageError` on validation, AppleScript dispatch, or * shutdown cancellation. */ send(request: SendRequest): Promise; /** * Start watching for new messages in real time. * * Independent of `sdk.send()` — sends do not require a watcher. * A running watcher gives you: * - `onIncomingMessage` / `onDirectMessage` / `onGroupMessage` for peers' messages * - `onFromMeMessage` for your own sends (including reads of * `isDelivered`, edits, retractions as they land in chat.db) * * Throws `IMessageError` (code `CONFIG`) if a watcher is already * running on this SDK instance — stop the existing one first. */ startWatching(events?: DispatchEvents): Promise; /** * Stop watching for new messages. * * Resolves only after any in-flight batch dispatch has finished, so * callers chaining `await sdk.stopWatching()` observe a truly quiet SDK. */ stopWatching(): Promise; /** Gracefully shut down the SDK and release all resources. */ close(): Promise; private doClose; [Symbol.asyncDispose](): Promise; private assertNotDestroyed; /** * Single path for tearing down the watcher. * * Awaits `watchSource.stop()` so that any in-flight `onBatch` dispatch * has completed before we return to the caller — this is what * prevents plugin `onIncomingMessage` from racing with `onDestroy` during * SDK shutdown. */ private teardownWatcher; } /** * Canonical SDK configuration bounds. * * Standalone module with zero dependencies — safe to import from anywhere. */ declare const BOUNDS: { readonly maxConcurrentSends: { readonly default: 10; readonly min: 1; readonly max: 50; }; readonly sendTimeout: { readonly default: 30000; readonly min: 1000; readonly max: 300000; }; }; /** * ChatId value object. * * Parses, normalizes, validates, and matches iMessage chat identifiers. * * Supported formats: * * service;+;guid group chat (iMessage;+;chat613…, any;+;chat687…) * service;-;address direct message (iMessage;-;+1234567890) * chat bare group GUID (chat61321855167474084) * bare address DM recipient (+1234567890, user@example.com, …) */ /** * Known chat service prefixes. * * Accepts any well-formed token for forward compatibility. */ type ChatServicePrefix = 'iMessage' | 'SMS' | 'RCS' | 'any' | (string & {}); declare class ChatId { /** The raw chat identifier string. */ readonly raw: string; /** Whether this identifies a group conversation. */ readonly isGroup: boolean; private constructor(); /** * Construct from a raw identifier string. Whitespace is trimmed — * the name signals the input may be untrusted. */ static fromUserInput(raw: string): ChatId; /** Construct a DM chat id from a recipient address and optional service prefix. */ static fromDMRecipient(recipient: string, prefix?: ChatServicePrefix): ChatId; /** * Core identifier with all service prefixes stripped. * * iMessage;-;pilot@photon.codes → pilot@photon.codes * iMessage;+;chat613ABF → chat613ABF * chat613ABF → chat613ABF */ get coreIdentifier(): string; /** * Extract the recipient address from a `service;-;address` DM chat id. * * Returns `null` for any other form (bare address, group, etc.). */ extractRecipient(): string | null; /** * Validate the chat id format. * * Throws `IMessageError` (code `CONFIG`) for malformed inputs. Accepts the * three documented formats: `service;+;guid`, `service;-;address`, and any * non-empty bare identifier (no semicolons). */ validate(): void; /** * Build a full Messages.app guid with a service prefix. * * chat613ABF + `iMessage` → iMessage;+;chat613ABF * * Group-only: throws `IMessageError` (code `CONFIG`) on DM instances — * calling on a DM would produce a malformed id like `any;+;+1234567890`. */ buildGroupGuid(prefix: ChatServicePrefix): string; toString(): string; /** * Extract everything after the first occurrence of separator, or `null` * if the separator is absent OR the suffix is empty (e.g. `iMessage;-;`). * Empty-suffix inputs are malformed; treating them as "not found" keeps * callers from receiving `""` as if it were a valid identifier. */ private extractAfter; private static detectGroup; } /** * Message target resolution. * * Maps a user-provided target string to either a DM (buddy method) or * group (chat method) send target. Format validity is delegated to * `ChatId.validate()`; this function only decides the routing kind. */ /** Discriminated union for resolved send targets. */ type MessageTarget = { readonly kind: 'dm'; readonly recipient: string; } | { readonly kind: 'group'; readonly chatId: ChatId; }; /** * Resolve a target string. * * Exposed publicly so callers can pre-validate a `to` value (and branch * on DM vs group) before calling `sdk.send()` — the SDK itself invokes * this internally, so you only need it for up-front validation or * routing-aware UI. * * Accepted formats (enforced by `ChatId.validate`): * * service;+;guid / chat → group * service;-;addr / bare address → DM * * Throws `IMessageError` (code `CONFIG`) for empty or malformed input. */ declare function resolveTarget(input: string): MessageTarget; /** * Unified error type, factory functions, and normalization utilities. * * Prefer factory functions over `new IMessageError` so the error code * always matches the intent. */ type ErrorCode = 'PLATFORM' | 'DATABASE' | 'SEND' | 'CONFIG'; declare class IMessageError extends Error { readonly code: ErrorCode; constructor(code: ErrorCode, message: string, options?: ErrorOptions); } declare function PlatformError(message?: string, cause?: unknown): IMessageError; declare function DatabaseError(message: string, cause?: unknown): IMessageError; declare function SendError(message: string, cause?: unknown): IMessageError; declare function ConfigError(message: string, cause?: unknown): IMessageError; /** * Platform detection. * * macOS requirement, default paths, Darwin version, and service prefix. */ /** Assert running on macOS. Throws PlatformError otherwise. */ declare function requireMacOS(): void; /** Default path to the iMessage database. */ declare function getDefaultDatabasePath(): string; /** * Plugin lifecycle management and hook dispatch. * * Three dispatch modes (aligned with Vite/Rollup conventions): * - Interrupting (fail-fast): onBefore* — first throw aborts the SDK operation. * Go through `callInterruptingHook`. * - Sequential (observing): onInit, onError, onDestroy — run one at a time, * failures collected and reported to `onError`. Go through `callHook`. * - Parallel (observing): onAfter*, onIncomingMessage, onFromMe — concurrent, * failures collected and reported to `onError`. Go through `callHook`. * * Plugin ordering via `order` property within each mode: * - 'pre' plugins run first * - 'post' plugins run last * - Unset (normal) plugins run in registration order between pre and post */ /** Identity function that provides type inference for plugin definitions. */ declare const definePlugin: (plugin: Plugin) => Plugin; /** * Read-only helpers for existing message attachments. * * Keeps only platform-relevant helpers: existence check with * null-safe localPath, extension normalization, and type classification. * Use node:fs directly for copy/read/stat — they are trivial wrappers. */ /** Check whether the attachment file exists on disk. */ declare function attachmentExists(attachment: Attachment): Promise; /** Extract the lowercase file extension (without dot) from an attachment. */ declare function getAttachmentExtension(attachment: Attachment): string; /** Check whether the attachment is an image based on file extension. */ declare function isImageAttachment(attachment: Attachment): boolean; /** Check whether the attachment is a video based on file extension. */ declare function isVideoAttachment(attachment: Attachment): boolean; /** Check whether the attachment is audio based on file extension. */ declare function isAudioAttachment(attachment: Attachment): boolean; export { type AfterChatQueryContext, type AfterMessageQueryContext, type AfterSendContext, type Attachment, BOUNDS, type BeforeChatQueryContext, type BeforeMessageQueryContext, type BeforeSendContext, type Chat, ChatId, type ChatKind, type ChatQuery, type ChatServicePrefix, ConfigError, DatabaseError, type DispatchEvents, type ErrorCode, type ExpireStatus, type IMessageConfig, IMessageError, IMessageSDK, type Message, type MessageCallback, type MessageContext, type MessageKind, type MessageQuery, type MessageTarget, PlatformError, type Plugin, type PluginErrorContext, type PluginHooks, type Reaction, type ReactionKind, type ReactionTextRange, type ScheduleKind, type ScheduleStatus, SendError, type SendPort, type SendRequest, type Service, type ShareActivity, type ShareDirection, type TransferStatus, attachmentExists, definePlugin, getAttachmentExtension, getDefaultDatabasePath, isAudioAttachment, isImageAttachment, isVideoAttachment, requireMacOS, resolveTarget };