@mapier/imsg-sdk 0.2.2 → 0.3.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.
@@ -0,0 +1,634 @@
1
+ # IMCore Selectors
2
+
3
+ Every bridge handler in `Sources/IMsgHelper/IMsgInjected.m` ultimately calls one
4
+ or more **private Apple selectors**. This maps each verb → handler → the selector
5
+ it fires → the class and framework that selector lives in on the Mac. Use it when
6
+ a macOS release breaks a verb (a selector renamed/removed, like `leaveChat` in
7
+ [#5]) and you need to find what to re-point to.
8
+
9
+ ## Where the selectors live
10
+
11
+ | Framework | Classes used here | Path (pre-26) |
12
+ |---|---|---|
13
+ | **IMCore** | `IMChat`, `IMMessage`, `IMMessageItem`, `IMMessagePartChatItem`, `IMChatRegistry`, `IMHandle`, `IMHandleRegistrar`, `IMAccount`, `IMAccountController`, `IMChatHistoryController`, `IMNicknameController`, `IMFileTransfer`, `IMFileTransferCenter`, `IMDaemonController`, `IMEmojiTapback`, `IMTapbackSender`, `IMMutableChatContext` | `/System/Library/PrivateFrameworks/IMCore.framework` |
14
+ | **IDS** (Apple Identity Services) | `IDSIDQueryController` | `/System/Library/PrivateFrameworks/IDS.framework` |
15
+ | **IMDPersistence / daemon** | `IMDPersistentAttachmentController` | daemon-side (IMSharedUtilities/IMDPersistence) |
16
+
17
+ On **macOS 26** the framework binaries moved, and that changes how you inspect them:
18
+
19
+ - **The binary moved off disk.** Pre-26, a framework's code was a real file inside its
20
+ bundle (`IMCore.framework/Versions/A/IMCore`) that you could open, link, or dump. On
21
+ macOS 26 that file is gone — the code exists **only inside the dyld shared cache**, one
22
+ giant pre-linked blob that's mapped into every running process. The `.framework` folder
23
+ may still be there, but it's an empty shell (Info.plist, no executable).
24
+ - **`class-dump` must target the cache, not a raw binary.** `class-dump` lists a binary's
25
+ Objective-C classes and selectors — this is how you'd find what `leaveChat` became. Since
26
+ there's no on-disk `IMCore` binary to point it at, use a cache-aware tool
27
+ (`ipsw class-dump`, `dsdump`) or first extract the image out with `ipsw dyld extract`.
28
+ - **Some C functions must be found at runtime (`dlsym`), not linked at build time.** To call
29
+ a function the computer needs its memory address. Normally the *linker* fills that in when
30
+ you build — but it needs an IMCore file/stub at build time, and there isn't one, so the
31
+ build would fail ("undefined symbol"). Instead the dylib looks the address up **while
32
+ running** and populates the map from function to address dynamically with function `dlsym` — the code is already in memory (via the shared cache),
33
+ so `dlsym("IMCreate…")` hands back a pointer to call through.
34
+
35
+ ## Consequences of linking at runtime instead of compile-time
36
+
37
+ Because there are no framework files to link against, the dylib resolves every
38
+ private class, selector, and C symbol **by name at runtime** (`NSClassFromString`
39
+ + `performSelector:` + `dlsym`). That trades the compiler's build-time guarantees
40
+ for per-host fragility, and it's the reason for nearly every defensive choice in
41
+ `IMsgInjected.m`:
42
+
43
+ 1. **No compiler safety net — this *is* why a selector like `leaveChat` fails the
44
+ way it does.** A renamed/removed name still **compiles fine**; it only fails when
45
+ it runs on a host that lacks it. So `#5` is a runtime `respondsToSelector: NO`,
46
+ not a build error. The whole "a selector died on the new OS" bug class exists
47
+ *because* resolution is deferred to runtime.
48
+ 2. **Fragile to OS version upgrades.** The same shipped dylib
49
+ can work on one macOS and silently no-op on another. That's why the SDK model is
50
+ **capability-based, not version-based**: `imsg status --json` probes each selector
51
+ live (`respondsToSelector:` / `dlsym != NULL`) and reports what actually resolved.
52
+ 3. **Everything is hand-declared, and nothing checks you.** No headers to `#import`,
53
+ so the dylib forward-declares the private classes/selectors itself and writes
54
+ function-pointer typedefs for C funcs. If Apple changes a method's *signature*
55
+ (arg types, not just its name), the declaration is now wrong with **no compiler to
56
+ flag it** — you get corruption or a crash at runtime. Correctness is on you + tests.
57
+ * Particularly dangerous for long arg calls. `performSelector:` only handles
58
+ simple signatures (object args, ≤2). For the 4-arg `editMessageItem:…` or the 13-arg `initWithSender:…` drop to `objc_msgSend` cast to a hand-written function-pointer type, and a wrong function declaration can cause memory corruption (usually compiler is there to guard).
59
+ 4. **Every call is defensive by necessity.** `respondsToSelector:` guard →
60
+ `performSelector:` → `@try/@catch`, degrading to `{ok:false}` rather than crashing
61
+ Messages.app. Compiler would have caught this at compile-time.
62
+ 5. **Two runtime gates stack.** The dylib both injects (`DYLD_INSERT_LIBRARIES`, SIP off) and resolves privately at runtime. macOS 26 library-validation/entitlement limits can block the injection entirely; even injected, the names must still resolve against the mapped shared cache. Neither is knowable at build time.
63
+ 6. **Testing is the only verification.** Since neither compiler nor linker validates any
64
+ of this, the only way to know a build works is to run it on a specific macOS and smoke
65
+ it — which is why the release record pins `imsg status --json` + `live-smoke` per OS
66
+ version, and why "wired ≠ host-verified."
67
+
68
+ **The upside:** runtime resolution also buys adaptivity. One dylib can carry fallbacks
69
+ and pick per host — which is exactly why `handleEditMessage` tries the macOS 27
70
+ five-argument `editMessageItem:…newPartTranslation:…` first, then `editMessageItem:…`,
71
+ then `editMessage:…`, and why a fixed `leaveChat` would probe candidates
72
+ and choose whichever the host has. A statically-linked binary couldn't branch like that;
73
+ a runtime-resolved one straddles multiple macOS versions from a single build.
74
+
75
+ ## The map
76
+
77
+ Target class in **bold**; `sel` is the selector the handler invokes on it.
78
+
79
+ **Note: In Objective C, the arguments are part of the function name itself, and are separated by colons. Square brackets denote a function call.**
80
+
81
+ Ex.
82
+ ```
83
+ [chat sendMessage:aMessage reason:aReason]; // call
84
+ selector: sendMessage:reason:
85
+ ```
86
+
87
+ ### Send
88
+ | Verb | Handler | Call | Host |
89
+ |---|---|---|---|
90
+ | `send-message` / `send-rich-link` | `handleSendMessage` | **IMChat** `sendMessage:reason:` (msg built on **IMMessage**, threaded via `setThreadOriginator:` / `setThreadIdentifier:`) | ✓ |
91
+ | `send-poll` | `handleSendPoll` | **IMMessage** `initWithSender:time:text:…associatedMessageType:…` + `setBalloonBundleID:` + `setPayloadData:` → **IMChat** send | ✓ |
92
+ | `send-poll-vote` / `-unvote` | `handleSendPollVoteMutation` | **IMChat** `sendMessage:` (summary-info mutation) | ✓ selector live, not wired above |
93
+ | `send-multipart` / `send-attachment` | `handleSendMultipart` | **IMFileTransferCenter** `guidForNewOutgoingTransferWithLocalURL:` · `transferForGUID:` · `registerTransferWithDaemon:` · **IMDPersistentAttachmentController** `_persistentPathForTransfer:filename:highQuality:chatGUID:storeAtExternalPath:` · **IMFileTransfer** `setLocalURL:`/`setMimeType:`/`setTransferredFilename:` | ✓ |
94
+ | `send-sticker` | `handleSendSticker` | **IMChat** `sendMessage:` targeting **IMMessagePartChatItem** (`guid`/`index`/`messagePartRange`) | ✓ |
95
+ | `send-reaction` | `handleSendReaction` | **IMTapbackSender** `initWithTapback:chat:messagePartChatItem:` / `initWithTapback:chat:messageGUID:messagePartRange:messageSummaryInfo:threadIdentifier:` + `send`; emoji via **IMEmojiTapback** `initWithEmoji:isRemoved:`; legacy fallback **IMChat** `sendMessage:` | ✓ |
96
+ | `notify-anyways` | `handleNotifyAnyways` | **IMChat** `markChatItemAsNotifyRecipient:` | ✗ not wired above |
97
+
98
+ ### Mutate a message
99
+ | Verb | Handler | Call | Host |
100
+ |---|---|---|---|
101
+ | `edit-message` | `handleEditMessage` | **IMChat** `editMessageItem:atPartIndex:withNewPartText:newPartTranslation:backwardCompatabilityText:` (macOS 27; fallbacks `editMessageItem:…` / `editMessage:…`) | ✓ wired on macOS 27 (imsg#3), native CLI/RPC E2E'd (imsg#11); SDK verb capability-gated + preflighted, host `tier2-smoke --only-edit-message` pending |
102
+ | `unsend-message` | `handleUnsendMessage` | **IMChat** `retractMessagePart:` | ✓ |
103
+ | `delete-message` | `handleDeleteMessage` | **IMChat** `deleteChatItems:` (device-local only) | ✓ |
104
+
105
+ ### Group / chat management
106
+ | Verb | Handler | Call | Host |
107
+ |---|---|---|---|
108
+ | `add-participant` | `handleAddParticipant` | **IMChat** `inviteParticipants:reason:` / `inviteParticipantsToiMessageChat:reason:` | ✓ |
109
+ | `remove-participant` | `handleRemoveParticipant` | **IMChat** `removeParticipants:reason:` / `removeParticipantsFromiMessageChat:reason:` | ✓ |
110
+ | `set-display-name` | `handleSetDisplayName` | **IMChat** `_setDisplayName:` (+ `sendGroupPhotoUpdate:`) | ✓ |
111
+ | `update-group-photo` | `handleUpdateGroupPhoto` | **IMChat** `sendGroupPhotoUpdate:` + **IMFileTransferCenter** `createNewOutgoingGroupPhotoTransferWithLocalFileURL:` | ✓ |
112
+ | `create-chat` | `handleCreateChat` | **IMChatRegistry** `chatForIMHandle:` / `chatForIMHandles:` (+ `_setDisplayName:`) | ✓ |
113
+ | **`leave-chat`** | `handleLeaveChat` | **IMChat** `leave` (macOS 27; `leaveChat` fallback for older hosts) | **⚠ call site updated to `leave`; not yet tested on relay e2e** |
114
+ | `delete-chat` | `handleDeleteChat` | **IMChatRegistry** `sharedInstance` (delete path selector-gated) | ✗ not wired above |
115
+
116
+ ### Read / presence
117
+ | Verb | Handler | Call | Host |
118
+ |---|---|---|---|
119
+ | `mark-chat-read` | `handleMarkChatRead` | **IMChat** `markAllMessagesAsRead` | ✓ internal only |
120
+ | `mark-chat-unread` | `handleMarkChatUnread` | **IMChat** `markLastMessageAsUnread` | ✗ not wired above |
121
+ | `start-/stop-/check-typing` | `handleStartTyping` … | **IMChat** `isCurrentlyTyping` (+ typing setter) | ✓ internal only |
122
+
123
+ ### Identity / introspection
124
+ | Verb | Handler | Call | Host |
125
+ |---|---|---|---|
126
+ | `check-imessage-availability` | `handleCheckIMessageAvailability` | **IDSIDQueryController** `_currentIDStatusForDestination:service:listenerID:` / `currentIDStatusForDestination:service:` | ✓ selector live, not wired above |
127
+ | `get-account-info` | `handleGetAccountInfo` | **IMAccountController** `sharedInstance`/`activeIMessageAccount` → **IMAccount** `vettedAliases`/`loginIMHandle` | ✓ |
128
+ | `get-nickname-info` / `share-nickname` / `should-offer-nickname-sharing` | `handleGetNicknameInfo` … | **IMNicknameController** `nicknameForHandle:` · `personalNickname` · `shouldOfferNicknameSharingForChat:` | ✓ (Name & Photo) |
129
+ | `download-purged-attachment` | `handleDownloadPurgedAttachment` | **IMFileTransferCenter** `acceptTransfer:` · **IMDaemonController** `connectToDaemon` | ✓ |
130
+
131
+ The selector **markers** reported by `imsg status --json` are probed in
132
+ `IMsgInjected.m` near the top of the file: it checks `IMChat` for
133
+ `editMessageItem:…newPartTranslation:…` (marker `editMessageItemTranslation`),
134
+ `editMessageItem:…`, `editMessage:…`, `retractMessagePart:`, `sendMessage:reason:`.
135
+ Add a probe there when adding a verb so the SDK can capability-gate it; the SDK's
136
+ `editMessage` capability is any one of the three edit markers.
137
+
138
+ ## macOS 27 operation callsites — MAP-175
139
+
140
+ **Research attribution:** These replacement entrypoints and downstream
141
+ operation callsites were found and verified by **Neo Shangguan** for MAP-175.
142
+
143
+ | Operation | Current bridge selector | macOS 27 entrypoint found | Downstream operation dispatch | Integration status | Found by |
144
+ |---|---|---|---|---|---|
145
+ | `leave-chat` | `-[IMChat leaveChat]` — absent | `-[IMChat leave]` | `[provider _chat_leave:chat]` | Call site updated to `leave`; not yet tested on relay e2e | **Neo Shangguan** |
146
+ | `edit-message` | Four-argument `-[IMChat editMessageItem:…]` / `-[IMChat editMessage:…]` — absent | Five-argument `-[IMChat editMessageItem:…newPartTranslation:…]` | `[provider _chat:chat sendEditedMessageItem:editedItem previousMessageItem:originalItem partIndex:index editType:1 backwardCompatabilityText:text]` | Wired (imsg#3), native CLI/RPC E2E'd (imsg#11) and SDK verb host-verified 2026-09-03 on macOS 27 | **Neo Shangguan** |
147
+ | `mentions` | `send-rich` / `send.rich` `text_formatting[].mention` (imsg#13) | Confirmed-mention `NSAttributedString` attribute (`__kIMMentionConfirmedMention`) resolved at runtime; no dedicated send selector | Existing `IMMessage` construction → `IMChat` send path | Wired + native E2E'd 2026-09-03 (attribute keys match a genuine iPhone mention row; highlight + notification on the recipient); SDK verb host-verified 2026-09-03 incl. recipient highlight | **Neo Shangguan** |
148
+ | `text-styling` / `text-effects` | No bridge operation | IM text-style/effect attributes on `NSAttributedString`; no dedicated send selector | Existing `IMMessage` construction → `IMChat` send path | Wired + tested e2e relayside | **Neo Shangguan** |
149
+ | `contact-card` | No contact-card-specific bridge operation | `.vcf` attachment identified by `kUTTypeVCard` / `public.vcard` | Existing `IMFileTransfer` attachment send path | Attachment representation found; not live-smoked | **Neo Shangguan** |
150
+ | `chat-background` | `chat.background.set` / `chat.background.remove` / `chat.background.status` (Mapier-Labs/imsg#12, merged) | `-[IMChat setTranscriptBackgroundAndSendToChat:transferID:]` | `[provider _chat:chat setTranscriptBackgroundAndSendToChat:background transferID:transferID]` | Native bridge merged after matched-build CLI/RPC E2E; SDK verbs `setChatBackground` / `removeChatBackground` / `chatBackgroundStatus` in this repo; relay command + recipient-side validation pending | **Neo Shangguan** |
151
+ | `location-request` | No bridge operation | `+[MSMessage findMyLocationRequestMessage]` | `+[CKComposition compositionWithMSMessage:appExtensionIdentifier:]` using `IMBalloonBundleIdentifierWaldo` → `-[CKChatController setComposition:]` | Factory/staging callsites found; payload/bridge route pending; not wired/live-smoked | **Neo Shangguan** |
152
+
153
+ The summary records stable selector names for implementation. Numeric callsite
154
+ addresses remain in the detailed evidence below because they are specific to a
155
+ single macOS build and must not become bridge constants.
156
+
157
+ The research ran against the development `mapidev` Messages profile, not the
158
+ production profile, on macOS 27.0 build `26A5421a` (`arm64e`) with IMCore
159
+ `1491.100.1.1.9`. `ipsw 3.1.712` read the native IMCore image directly from the
160
+ dyld shared cache. Runtime availability was checked by loading IMCore in an
161
+ independent process and querying the Objective-C method table; the probe did
162
+ not attach to or mutate Messages.
163
+
164
+ All addresses below are unslid and specific to this OS build. They are audit
165
+ evidence only and must never be hard-coded by the bridge; selectors still need
166
+ to be resolved by name at runtime.
167
+
168
+ ### `leave-chat` — found by Neo Shangguan
169
+
170
+ Runtime availability:
171
+
172
+ ```text
173
+ -[IMChat leaveChat] false
174
+ -[IMChat leave] true
175
+ -[IMChat leaveConversation] true
176
+ ```
177
+
178
+ `-[IMChat leave]` begins at `0x1bf212c88`. Its operation path is:
179
+
180
+ ```text
181
+ -[IMChat leave]
182
+ [self daemonServiceProvider] callsite 0x1bf212da4
183
+ [provider _chat_leave:self] callsite 0x1bf212db4
184
+ ```
185
+
186
+ The selector stubs resolve to `daemonServiceProvider` and `_chat_leave:`. At
187
+ the downstream callsite, the provider is the receiver and the current `IMChat`
188
+ is the operation argument. This establishes `-[IMChat leave]` as the high-level
189
+ replacement entrypoint; bridge code should call it rather than reaching into
190
+ `_chat_leave:` directly.
191
+
192
+ `-[IMChat leaveConversation]` was also disassembled and resolves to
193
+ `[self _closeSession:NO]`. It closes the session rather than dispatching the
194
+ group-leave operation, so it is not a `leaveChat` replacement.
195
+
196
+ The existing `leaveChat` selector can remain as the older-system path, with
197
+ `leave` as a runtime-gated fallback. **Update:** the bridge call site has since
198
+ been updated to probe `leave`; it has not yet been tested end-to-end through
199
+ relay (no relay e2e group-leave smoke performed).
200
+
201
+ ### `edit-message` — found by Neo Shangguan
202
+
203
+ Runtime availability:
204
+
205
+ ```text
206
+ editMessageItem:atPartIndex:withNewPartText:
207
+ backwardCompatabilityText: false
208
+ editMessage:atPartIndex:withNewPartText:
209
+ backwardCompatabilityText: false
210
+ editMessageItem:atPartIndex:withNewPartText:
211
+ newPartTranslation:backwardCompatabilityText: true
212
+ ```
213
+
214
+ The new method inserts a `newPartTranslation:` argument. It begins at
215
+ `0x1bf209994`, with the arm64 business arguments laid out as:
216
+
217
+ ```text
218
+ x2 = original message item
219
+ x3 = part index
220
+ x4 = new part text
221
+ x5 = new part translation
222
+ x6 = backward-compatibility text
223
+ ```
224
+
225
+ The ordinary-message operation path is:
226
+
227
+ ```text
228
+ -[IMChat editMessageItem:...newPartTranslation:...]
229
+ [factory editedMessageItemWithOriginalMessageItem:
230
+ editedPartIndex:newPartText:newPartTranslation:] callsite 0x1bf209b18
231
+ [self daemonServiceProvider] callsite 0x1bf209b28
232
+ [provider _chat:self
233
+ sendEditedMessageItem:editedItem
234
+ previousMessageItem:originalItem
235
+ partIndex:index
236
+ editType:1
237
+ backwardCompatabilityText:backwardText] callsite 0x1bf209b4c
238
+ ```
239
+
240
+ The final selector is
241
+ `_chat:sendEditedMessageItem:previousMessageItem:partIndex:editType:backwardCompatabilityText:`.
242
+ The method constructs the new edited item, resolves the daemon service
243
+ provider, and dispatches the edit with constant `editType` value `1`. The
244
+ alternate branch calls
245
+ `editScheduledMessageItem:atPartIndex:withNewPartText:newPartTranslation:` and
246
+ is for scheduled messages, not the ordinary edit path.
247
+
248
+ `newPartTranslation` is retained and forwarded without a non-null branch, which
249
+ is consistent with passing `nil` for an ordinary untranslated edit. A live
250
+ smoke must verify that assumption. The high-level five-argument `IMChat` method
251
+ is the replacement target; it requires an accurately cast `objc_msgSend` call
252
+ rather than `performSelector:` and should be runtime-gated alongside the older
253
+ selectors.
254
+
255
+ ### `mentions` — found by Neo Shangguan
256
+
257
+ Mentions do not introduce a separate daemon operation selector. `IMMessage`
258
+ continues to carry an `NSAttributedString`, and the existing `IMChat` send path
259
+ dispatches the message. The missing bridge work is therefore attributed-text
260
+ payload construction rather than discovery of another `IMChat` send method.
261
+
262
+ The native macOS 27 `IMSharedUtilities` image exports the transport constants
263
+ needed to construct and inspect that payload:
264
+
265
+ ```text
266
+ IMMentionAttributeName "__kIMMentionAttributeName"
267
+ IMMentionAttributeNameNeedsAnimation "__kIMMentionAttributeNameNeedsAnimation"
268
+ IMMentionConfirmedMention "__kIMMentionConfirmedMention"
269
+ IMMentionAutomaticConfirmedMention "__kIMMentionAutomaticConfirmedMention"
270
+ IMMentionUnconfirmedDirectMention "__kIMMentionUnconfirmedDirectMention"
271
+ ```
272
+
273
+ They were resolved from
274
+ `/System/Library/PrivateFrameworks/IMSharedUtilities.framework` in an
275
+ independent runtime probe. Bridge code should likewise resolve symbols by name
276
+ and must not embed their build-specific addresses or private string values.
277
+
278
+ The construction pattern is visible in the iOSSupport ChatKit image at
279
+ `+[CKMentionsUtilities configureAttributedString:inTextView:
280
+ forConfirmedMentionInRange:needingAnimation:]`, beginning at `0x1dbecbe38`:
281
+
282
+ ```text
283
+ add IMMentionAttributeName to the confirmed mention range 0x1dbecbea0
284
+ remove IMMentionUnconfirmedDirectMention from that range 0x1dbecbebc
285
+ remove IMMentionAutomaticConfirmedMention from that range 0x1dbecbed8
286
+ conditionally add IMMentionAttributeNameNeedsAnimation 0x1dbecbf54
287
+ ```
288
+
289
+ The mutation callsites resolve to
290
+ `-[NSMutableAttributedString addAttribute:value:range:]` and
291
+ `-[NSMutableAttributedString removeAttribute:range:]`. The related
292
+ `configureAttributedString:automaticMentionAttributeWithOriginalText:
293
+ entityNode:nodeId:forRange:` method builds composer/autocomplete metadata and
294
+ stores it under `IMMentionAutomaticConfirmedMention`; the confirmed path
295
+ removes that temporary marker before sending.
296
+
297
+ ChatKit is present in this cache under `/System/iOSSupport` rather than as the
298
+ native transport framework, so its helper itself is evidence for the payload
299
+ shape, not a selector the bridge should invoke. The native
300
+ `IMSharedUtilities` constants plus the existing attributed `IMMessage` send
301
+ path are the candidate integration boundary. Exact attribute values and range
302
+ handling still require a development-profile live send smoke before the
303
+ feature can be declared available.
304
+
305
+ ### `text-styling` and `text-effects` — found by Neo Shangguan
306
+
307
+ Inline bold, italic, underline, strikethrough, and animated text effects also
308
+ reuse the attributed `IMMessage` and existing `IMChat` send operation. There is
309
+ no additional daemon send selector. The missing bridge work is to construct
310
+ the transport attributes over the requested text ranges.
311
+
312
+ The native macOS 27 `IMSharedUtilities` image exports the relevant integration
313
+ symbols:
314
+
315
+ ```text
316
+ IMTextBoldAttributeName
317
+ IMTextItalicAttributeName
318
+ IMTextUnderlineAttributeName
319
+ IMTextStrikethroughAttributeName
320
+ IMTextEffectAttributeName
321
+ IMTextStyleAll
322
+ IMTextEffectNameFromType
323
+ IMTextEffectTypeFromName
324
+ IMTextEffectOrderedSupportedNames
325
+ IMTextEffectNameBig / Bloom / Bounce / Explode / Jitter / Nod /
326
+ ScaleRipple / Shake / Small / Somersault / Squish / Stretch
327
+ IMServiceCapabilityTextEffects
328
+ ```
329
+
330
+ The iOSSupport ChatKit construction reference maps the inline-style bit values
331
+ to transport and local display attributes as follows:
332
+
333
+ | Style value | IM transport marker | Local display attribute |
334
+ |---|---|---|
335
+ | `1` | `IMTextBoldAttributeName` | `NSFontAttributeName` with the bold trait |
336
+ | `2` | `IMTextItalicAttributeName` | `NSFontAttributeName` with the italic trait |
337
+ | `4` | `IMTextUnderlineAttributeName` | `NSUnderlineStyleAttributeName` |
338
+ | `8` | `IMTextStrikethroughAttributeName` | `NSStrikethroughStyleAttributeName` |
339
+
340
+ `-[NSMutableAttributedString(TextEffects)
341
+ ck_applyTextStyle:options:range:]` begins at `0x1dbcc19fc`. It asks
342
+ `ck_actionForIMTextStyle:range:` whether the requested style should be added,
343
+ removed, or left unchanged, then tail-dispatches to
344
+ `ck_addTextStyle:options:range:` at `0x1dbcc1a74` or
345
+ `ck_removeTextStyle:options:range:` at `0x1dbcc1aac`. The add implementation
346
+ begins at `0x1dbcc1ac4`.
347
+
348
+ Within `ck_addTextStyle:options:range:`, option bit zero controls the private IM
349
+ transport marker and option bit one controls the corresponding local display
350
+ attribute. The callsites that add the four IM markers are:
351
+
352
+ ```text
353
+ IMTextBoldAttributeName 0x1dbcc1d04
354
+ IMTextItalicAttributeName 0x1dbcc1c2c
355
+ IMTextUnderlineAttributeName 0x1dbcc1ccc
356
+ IMTextStrikethroughAttributeName 0x1dbcc1da8
357
+ ```
358
+
359
+ Each transport marker receives a constant `NSNumber` value. ChatKit separately
360
+ updates font traits or the standard underline/strikethrough attributes for
361
+ local display. Those visual attributes must not be confused with the private
362
+ IM markers that describe the wire payload.
363
+
364
+ Animated effects are constructed by
365
+ `-[NSMutableAttributedString(TextEffects)
366
+ ck_applyTextEffectType:range:]`, beginning at `0x1dbcc1340`. Its primary path
367
+ is:
368
+
369
+ ```text
370
+ remove IMTextEffectAttributeName and NSTextAnimationAttributeName
371
+ remove IMTextStyleAll with both transport and display options
372
+ effectName = IMTextEffectNameFromType(effectType) 0x1dbcc14e4
373
+ animation = [UITextAnimation animationWithName:effectName] 0x1dbcc1670
374
+ wireValue = [NSNumber numberWithInteger:effectType] 0x1dbcc16a0
375
+ add NSTextAnimationAttributeName = animation 0x1dbcc1694
376
+ add IMTextEffectAttributeName = wireValue 0x1dbcc16c0
377
+ ```
378
+
379
+ The method also checks confirmed-mention and attachment ranges before applying
380
+ an effect, and may repeat the two add operations over adjusted subranges. The
381
+ `NSTextAnimationAttributeName`/`UITextAnimation` pair is the local preview;
382
+ the `IMTextEffectAttributeName`/numeric effect type pair is the transport
383
+ representation.
384
+
385
+ As with mentions, these mutation helpers live in the `/System/iOSSupport`
386
+ ChatKit image and are evidence for payload shape rather than bridge call
387
+ targets. A bridge implementation should resolve the native
388
+ `IMSharedUtilities` symbols by name, construct the attributed payload itself,
389
+ and reuse the existing `IMMessage`/`IMChat` send path. **Update:** this is now
390
+ wired and tested end-to-end through relay — the attributed payload is
391
+ constructed over the requested ranges and sent via the existing path, with
392
+ effects animating on device on the `mapidev` development profile. Inbound
393
+ styled-text parsing remains open if that direction is needed.
394
+
395
+ ### `contact-card` — found by Neo Shangguan
396
+
397
+ A real contact card is a specialized vCard attachment, not a distinct IMCore
398
+ send operation. The recipient-side ChatKit type is `CKContactMediaObject`,
399
+ which inherits through `CKCardMediaObject` from the generic `CKMediaObject`
400
+ attachment model:
401
+
402
+ ```text
403
+ CKContactMediaObject : CKCardMediaObject : CKMediaObject
404
+ ```
405
+
406
+ `CKMediaObject` directly stores a `CKFileTransfer`, file URL, transfer GUID,
407
+ data, filename, MIME type, and UTI. `CKContactMediaObject` adds vCard parsing,
408
+ summary, image, preview, and contact-balloon behavior; its methods include
409
+ `contactCardPayloadFileURL:`, `vCardSummary`, and `vCardImageOfSize:`.
410
+
411
+ `+[CKContactMediaObject UTITypes]` begins at `0x1dbcf40b4`. It loads
412
+ `kUTTypeVCard` and returns it in a one-element array via
413
+ `+[NSArray arrayWithObjects:count:]` at `0x1dbcf40f4`:
414
+
415
+ ```text
416
+ +[CKContactMediaObject UTITypes] → @[ kUTTypeVCard ]
417
+ ```
418
+
419
+ The shared-cache wire identifiers corroborate the representation:
420
+
421
+ ```text
422
+ public.vcard native IMSharedUtilities
423
+ text/vcard IMDaemonCore
424
+ text/x-vcard IMDaemonCore and IMFoundation (legacy-compatible MIME)
425
+ .vcf Contacts filename generation
426
+ ```
427
+
428
+ The outbound integration path is therefore:
429
+
430
+ ```text
431
+ CNContact or caller-supplied vCard data
432
+ → serialize/write a .vcf file
433
+ → filename + vCard MIME/UTI metadata
434
+ → existing IMFileTransfer attachment send
435
+ → recipient CKContactMediaObject
436
+ → CKContactBalloonView
437
+ ```
438
+
439
+ The existing bridge attachment handler already stages a local file through
440
+ `IMFileTransferCenter`, sets the transfer filename and MIME type, registers the
441
+ transfer with the daemon, and reuses the ordinary message send operation. A
442
+ contact-card integration should build on that path rather than call ChatKit.
443
+ It may either accept a valid `.vcf` or serialize a contact using the Contacts
444
+ framework; the exact accepted MIME choice, transfer metadata, recipient
445
+ rendering, and inbound parsing still require a development-profile live smoke.
446
+
447
+ One apparent UI lead was explicitly ruled out. Despite its name,
448
+ `-[CKChatController(Contacts) contactPicker:didSelectContact:]` at
449
+ `0x1dc070830` eventually dispatches
450
+ `updateContact:withNicknameUpdate:updateType:addHandleToContact:presentationMode:`
451
+ at `0x1dc070a00` / `0x1dc070a40`. It updates the local address book after a
452
+ contact/nickname prompt; it is not a contact-card send callsite.
453
+
454
+ ### `chat-background` — found by Neo Shangguan
455
+
456
+ The macOS 27 `IMChat` entrypoint is:
457
+
458
+ ```objc
459
+ -[IMChat setTranscriptBackgroundAndSendToChat:transferID:]
460
+ ```
461
+
462
+ It begins at `0x1bf20b080`. The arm64 argument flow shows the first business
463
+ argument retained as the pending background object and the second retained as
464
+ the transfer identifier. The operation path is:
465
+
466
+ ```text
467
+ -[IMChat setTranscriptBackgroundAndSendToChat:background transferID:transferID]
468
+ [self daemonServiceProvider] callsite 0x1bf20b0b8
469
+ [provider _chat:self
470
+ setTranscriptBackgroundAndSendToChat:background
471
+ transferID:transferID] callsite 0x1bf20b0d0
472
+ ```
473
+
474
+ The final selector stub resolves to
475
+ `_chat:setTranscriptBackgroundAndSendToChat:transferID:`. ChatKit also exposes
476
+ `setPendingTranscriptBackground:transferID:`, corroborating that the outbound
477
+ UI flow stages a background and its associated transfer before invoking the
478
+ `IMChat` entrypoint.
479
+
480
+ The ChatKit set/remove control flow was also recovered. The UI entrypoint
481
+ `-[CKCoreChatController setNewTranscriptBackground:]` begins at
482
+ `0x1dbc7d1b8` and generates the transfer identifier with
483
+ `+[NSString stringGUID]`. It stores the pending state on the conversation and
484
+ then branches on the background argument:
485
+
486
+ ```text
487
+ transferID = [NSString stringGUID]
488
+ [self.conversation setPendingTranscriptBackground:background
489
+ transferID:transferID]
490
+
491
+ if background != nil:
492
+ [background createBackgroundWithWatchDataWithCompletion:^(preparedBackground) {
493
+ [self updateTranscriptBackground:preparedBackground
494
+ transferID:transferID]
495
+ }]
496
+ else:
497
+ [self updateTranscriptBackground:nil
498
+ transferID:transferID]
499
+ ```
500
+
501
+ `-[CKCoreChatController updateTranscriptBackground:transferID:]` begins at
502
+ `0x1dbc7c538`. Its non-null branch stages the prepared poster and watch payloads
503
+ on disk, obtains the poster URL's `absoluteString`, and then converges with the
504
+ null removal branch on the same `IMChat` selector:
505
+
506
+ ```text
507
+ set: [chat setTranscriptBackgroundAndSendToChat:posterURL.absoluteString
508
+ transferID:transferID] callsite 0x1dbc7cc38
509
+ remove: [chat setTranscriptBackgroundAndSendToChat:nil
510
+ transferID:transferID] callsite 0x1dbc7c710
511
+ ```
512
+
513
+ The `absoluteString` selector stub immediately before the non-null callsite is
514
+ at `0x1e0016430` and resolves to selector address `0x1f5907443`. A live smoke
515
+ that passed the `CKTranscriptBackground` object itself produced
516
+ `-[ChatKit.CKTranscriptBackground length]: unrecognized selector`, further
517
+ confirming that the daemon-facing first argument is the staged URL string, not
518
+ the ChatKit wrapper object.
519
+
520
+ This establishes that removal is represented by a null background sent through
521
+ the same operation, not by a separate remove selector. The complete native
522
+ recipe archives the poster configuration, prepares its watch snapshot, stages
523
+ the resulting `posterData` and `watchData` at the poster URL and its
524
+ `im_associatedWatchBackgroundURL`, and passes the poster URL string to IMChat.
525
+ Recipient behavior outside the development profile still requires validation.
526
+
527
+ A read-only runtime probe on the macOS 27 development host (build `26A5421a`)
528
+ confirmed that `IMChat` responds to
529
+ `setTranscriptBackgroundAndSendToChat:transferID:`. Its live Objective-C method
530
+ signature returns `void`, has four total arguments (`self`, `_cmd`, and two
531
+ object arguments), and `+[NSString stringGUID]` is available for generating the
532
+ transfer identifier. A development-profile semantic smoke then generated and
533
+ staged a Gradient background, invoked IMChat, and matched the live background
534
+ GUID to the persisted `set` event and cache assets. Passing a null first
535
+ argument with a fresh transfer identifier subsequently cleared the same live
536
+ GUID and persisted a matching `clear` event. This verified the direct native
537
+ set/remove operations. The production bridge (Gradient builder, null-background
538
+ removal handler, capability probes, standalone smoke scripts, and
539
+ persistence-confirming CLI/RPC surfaces) then passed its matched CLI/dylib
540
+ E2E on the development profile and merged as Mapier-Labs/imsg#12 into
541
+ `mapier/deploy`. This repo's `ImsgGateway` exposes it as `setChatBackground`,
542
+ `removeChatBackground`, and `chatBackgroundStatus` (see
543
+ `docs/gateway-contract.md` §2 "Chat backgrounds"). Relay command wiring and
544
+ recipient-side end-to-end coverage remain pending.
545
+
546
+ ### `location-request` — found by Neo Shangguan
547
+
548
+ Location Request is a Find My Messages-app payload, not an
549
+ `IMFMFSession` tracking operation. The ChatKit entrypoint
550
+ `-[CKChatController _stageFindMyLocationRequest]` begins at `0x1dbc13240`.
551
+ Its relevant operation path is:
552
+
553
+ ```text
554
+ [IMFeatureFlags sharedFeatureFlags]
555
+ -isWaldoEnabled
556
+
557
+ soft-link the MSMessage class
558
+ +respondsToSelector:@selector(findMyLocationRequestMessage)
559
+ +findMyLocationRequestMessage
560
+
561
+ [CKComposition compositionWithMSMessage:requestMessage
562
+ appExtensionIdentifier:IMBalloonBundleIdentifierWaldo]
563
+ → [CKChatController setComposition:composition]
564
+ ```
565
+
566
+ The feature-gate selectors resolve to `sharedFeatureFlags` and
567
+ `isWaldoEnabled`. ChatKit soft-links the `MSMessage` class, verifies the class
568
+ factory at runtime, and then invokes `+[MSMessage findMyLocationRequestMessage]`.
569
+ It wraps the returned request in a `CKComposition` using the private
570
+ `IMBalloonBundleIdentifierWaldo` constant before staging the composition.
571
+ `Waldo` is the Find My Messages-balloon/app identity used for this request.
572
+
573
+ This rules out two misleading IMCore paths. `-[IMFMFSession
574
+ startTrackingLocationForChat:]` refreshes an already-shared location, while
575
+ `-[IMFMFSession startSharingWithChat:withDuration:]` starts sharing the local
576
+ user's location. Neither is the operation that constructs a request asking the
577
+ other participant to share.
578
+
579
+ The callable factory and staging callsites are now known, but a bridge recipe
580
+ is still incomplete. The implementation of
581
+ `+[MSMessage findMyLocationRequestMessage]`, the serialized payload it returns,
582
+ and the safest route from that payload into the bridge's existing `IMMessage`
583
+ send path remain to be recovered. Calling a `CKChatController` UI instance is
584
+ not assumed to be an appropriate relay integration path.
585
+
586
+ ### Implementation readiness and remaining work
587
+
588
+ Finding a callsite removes the selector-discovery blocker; it does not by
589
+ itself make the operation available. This table is the implementation handoff
590
+ for the MAP-175 findings above:
591
+
592
+ | Operation | Readiness | What is resolved | Remaining implementation work |
593
+ |---|---|---|---|
594
+ | `leave-chat` | **Call site updated** | macOS 27 `-[IMChat leave]` entrypoint and `_chat_leave:` dispatch; call site re-pointed to `leave` with the `leaveChat` fallback retained | Test end-to-end through relay on `mapidev` (bridge call site updated, relay e2e not yet run) |
595
+ | `edit-message` | **Native merged (imsg#3/#11); SDK verb host-verified 2026-09-03** | Five-argument selector, argument order, edited-item builder, daemon dispatch, runtime probe/fallbacks, native CLI/RPC E2E | Relay `message.moderate edit` mapping of `effectStarted`; recipient-side rendering check |
596
+ | `mentions` | **Native merged (imsg#13, `7c31772`); SDK verb host-verified 2026-09-03** | Attribute key + handle value confirmed against a genuine row, recipient highlight + notification, membership fail-closed | Relay `message.send` payload for mention ranges |
597
+ | `text-styling` / `text-effects` | **Done — tested e2e relayside** | Transport attributes, style bit mapping, effect type/name mapping, existing send boundary; attributed payload constructed and sent through the relay path, animating on device | — (inbound styled-text parsing still open if needed) |
598
+ | `contact-card` | **Prototype-ready** | vCard UTI/content representation and reuse of the existing attachment-transfer path | Choose/validate MIME and transfer metadata, accept or serialize `.vcf` input, verify recipient rendering and inbound parsing |
599
+ | `chat-background` remove | **Native merged (imsg#12); SDK verb in this repo** | Null-background semantics, transfer-ID generation, selector ABI, live clear, persisted clear event, race-guard contract, matched-build CLI/RPC E2E | Host-Mac `tier2-smoke.ts --chat-background`; relay `chat.background.remove` command; validate recipient state |
600
+ | `chat-background` set | **Native merged (imsg#12); SDK verb in this repo** | Gradient configuration build, archive, poster/watch snapshot, staging contract, URL-string IMChat argument, live set, persisted event, cache/upload metadata, matched-build CLI/RPC E2E | Host-Mac `tier2-smoke.ts --chat-background`; relay `chat.background.set` command; validate recipient rendering and retry behavior |
601
+ | `location-request` | **Callsite-only** | Waldo feature gate, `MSMessage` factory selector, `CKComposition` wrapper, and app-extension identifier | Recover the factory's serialized Waldo payload and a safe bridge route into the existing send path without depending on a `CKChatController` UI instance |
602
+
603
+ Every new or replaced bridge path still needs defensive runtime capability
604
+ probing, native wiring, a matched CLI/dylib rebuild, injection under the
605
+ development `mapidev` profile, and operation-specific live smoke evidence.
606
+ Build-specific addresses above remain audit evidence and must never become
607
+ runtime constants.
608
+
609
+ These findings do not include an upstream Messages UI-controller trace. MAP-175
610
+ does not require that trace once the callable entrypoint and downstream
611
+ operation dispatch have been established.
612
+
613
+ ## Finding a replacement selector (the archaeology)
614
+
615
+ When a selector goes dead (host `✗`, `respondsToSelector:` NO), on a host of that
616
+ macOS version:
617
+
618
+ 1. **Dump the class.** `class-dump` (or Hopper/IDA) the dyld shared cache and read
619
+ the target class — e.g. all `IMChat` methods — to spot the renamed/new one.
620
+ 2. **Trace the UI.** Attach `lldb` / `frida-trace` to Messages.app, do the action
621
+ by hand, and watch which IMCore selectors fire (`objc` method trace on `IMChat`).
622
+ 3. **Cross-check open source.** BlueBubbles and Barcelona (open-imcore) implement
623
+ leave / edit / etc.; their newer-macOS handling names candidate selectors.
624
+ 4. Swap the `@selector(...)` in the handler, add its status-probe marker, rebuild
625
+ the matched CLI+dylib, inject (SIP off), and smoke.
626
+
627
+ For `leave-chat`: `handleLeaveChat` and everything above it (relay → RPC → file
628
+ IPC → dispatch) is intact. The only dead link was `-[IMChat leaveChat]`;
629
+ `-[IMChat leave]` is its macOS 27 replacement and the call site has since been
630
+ re-pointed to it (with `leaveChat` retained as the older-host path). The
631
+ remaining work is to test it end-to-end through relay on the development
632
+ profile.
633
+
634
+ [#5]: https://github.com/Mapier-Labs/imsg-sdk/issues/5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapier/imsg-sdk",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript SDK for iMessage automation on macOS: the Gateway contract, ImsgGateway (real), and a device-free FakeGateway.",
5
5
  "license": "MIT",
6
6
  "repository": {