@mapier/imsg-sdk 0.2.1 → 0.2.3

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,600 @@
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 `editMessageItem:…`
70
+ then falls back to `editMessage:…`, and why a fixed `leaveChat` would probe candidates
71
+ and choose whichever the host has. A statically-linked binary couldn't branch like that;
72
+ a runtime-resolved one straddles multiple macOS versions from a single build.
73
+
74
+ ## The map
75
+
76
+ Target class in **bold**; `sel` is the selector the handler invokes on it.
77
+
78
+ **Note: In Objective C, the arguments are part of the function name itself, and are separated by colons. Square brackets denote a function call.**
79
+
80
+ Ex.
81
+ ```
82
+ [chat sendMessage:aMessage reason:aReason]; // call
83
+ selector: sendMessage:reason:
84
+ ```
85
+
86
+ ### Send
87
+ | Verb | Handler | Call | Host |
88
+ |---|---|---|---|
89
+ | `send-message` / `send-rich-link` | `handleSendMessage` | **IMChat** `sendMessage:reason:` (msg built on **IMMessage**, threaded via `setThreadOriginator:` / `setThreadIdentifier:`) | ✓ |
90
+ | `send-poll` | `handleSendPoll` | **IMMessage** `initWithSender:time:text:…associatedMessageType:…` + `setBalloonBundleID:` + `setPayloadData:` → **IMChat** send | ✓ |
91
+ | `send-poll-vote` / `-unvote` | `handleSendPollVoteMutation` | **IMChat** `sendMessage:` (summary-info mutation) | ✓ selector live, not wired above |
92
+ | `send-multipart` / `send-attachment` | `handleSendMultipart` | **IMFileTransferCenter** `guidForNewOutgoingTransferWithLocalURL:` · `transferForGUID:` · `registerTransferWithDaemon:` · **IMDPersistentAttachmentController** `_persistentPathForTransfer:filename:highQuality:chatGUID:storeAtExternalPath:` · **IMFileTransfer** `setLocalURL:`/`setMimeType:`/`setTransferredFilename:` | ✓ |
93
+ | `send-sticker` | `handleSendSticker` | **IMChat** `sendMessage:` targeting **IMMessagePartChatItem** (`guid`/`index`/`messagePartRange`) | ✓ |
94
+ | `send-reaction` | `handleSendReaction` | **IMTapbackSender** `initWithTapback:chat:messagePartChatItem:` / `initWithTapback:chat:messageGUID:messagePartRange:messageSummaryInfo:threadIdentifier:` + `send`; emoji via **IMEmojiTapback** `initWithEmoji:isRemoved:`; legacy fallback **IMChat** `sendMessage:` | ✓ |
95
+ | `notify-anyways` | `handleNotifyAnyways` | **IMChat** `markChatItemAsNotifyRecipient:` | ✗ not wired above |
96
+
97
+ ### Mutate a message
98
+ | Verb | Handler | Call | Host |
99
+ |---|---|---|---|
100
+ | `edit-message` | `handleEditMessage` | **IMChat** `editMessageItem:atPartIndex:withNewPartText:backwardCompatabilityText:` (fallback `editMessage:atPartIndex:…`) | **⚠ old selectors false; replacement identified, not wired/smoked — see MAP-175 findings** |
101
+ | `unsend-message` | `handleUnsendMessage` | **IMChat** `retractMessagePart:` | ✓ |
102
+ | `delete-message` | `handleDeleteMessage` | **IMChat** `deleteChatItems:` (device-local only) | ✓ |
103
+
104
+ ### Group / chat management
105
+ | Verb | Handler | Call | Host |
106
+ |---|---|---|---|
107
+ | `add-participant` | `handleAddParticipant` | **IMChat** `inviteParticipants:reason:` / `inviteParticipantsToiMessageChat:reason:` | ✓ |
108
+ | `remove-participant` | `handleRemoveParticipant` | **IMChat** `removeParticipants:reason:` / `removeParticipantsFromiMessageChat:reason:` | ✓ |
109
+ | `set-display-name` | `handleSetDisplayName` | **IMChat** `_setDisplayName:` (+ `sendGroupPhotoUpdate:`) | ✓ |
110
+ | `update-group-photo` | `handleUpdateGroupPhoto` | **IMChat** `sendGroupPhotoUpdate:` + **IMFileTransferCenter** `createNewOutgoingGroupPhotoTransferWithLocalFileURL:` | ✓ |
111
+ | `create-chat` | `handleCreateChat` | **IMChatRegistry** `chatForIMHandle:` / `chatForIMHandles:` (+ `_setDisplayName:`) | ✓ |
112
+ | **`leave-chat`** | `handleLeaveChat` | **IMChat** `leaveChat` | **✗ selector gone ([#5]); replacement identified, not wired/smoked — see MAP-175 findings** |
113
+ | `delete-chat` | `handleDeleteChat` | **IMChatRegistry** `sharedInstance` (delete path selector-gated) | ✗ not wired above |
114
+
115
+ ### Read / presence
116
+ | Verb | Handler | Call | Host |
117
+ |---|---|---|---|
118
+ | `mark-chat-read` | `handleMarkChatRead` | **IMChat** `markAllMessagesAsRead` | ✓ internal only |
119
+ | `mark-chat-unread` | `handleMarkChatUnread` | **IMChat** `markLastMessageAsUnread` | ✗ not wired above |
120
+ | `start-/stop-/check-typing` | `handleStartTyping` … | **IMChat** `isCurrentlyTyping` (+ typing setter) | ✓ internal only |
121
+
122
+ ### Identity / introspection
123
+ | Verb | Handler | Call | Host |
124
+ |---|---|---|---|
125
+ | `check-imessage-availability` | `handleCheckIMessageAvailability` | **IDSIDQueryController** `_currentIDStatusForDestination:service:listenerID:` / `currentIDStatusForDestination:service:` | ✓ selector live, not wired above |
126
+ | `get-account-info` | `handleGetAccountInfo` | **IMAccountController** `sharedInstance`/`activeIMessageAccount` → **IMAccount** `vettedAliases`/`loginIMHandle` | ✓ |
127
+ | `get-nickname-info` / `share-nickname` / `should-offer-nickname-sharing` | `handleGetNicknameInfo` … | **IMNicknameController** `nicknameForHandle:` · `personalNickname` · `shouldOfferNicknameSharingForChat:` | ✓ (Name & Photo) |
128
+ | `download-purged-attachment` | `handleDownloadPurgedAttachment` | **IMFileTransferCenter** `acceptTransfer:` · **IMDaemonController** `connectToDaemon` | ✓ |
129
+
130
+ The selector **markers** reported by `imsg status --json` are probed in
131
+ `IMsgInjected.m` near the top of the file: it checks `IMChat` for
132
+ `editMessageItem:…`, `editMessage:…`, `retractMessagePart:`, `sendMessage:reason:`.
133
+ Add a probe there when adding a verb so the SDK can capability-gate it.
134
+
135
+ ## macOS 27 operation callsites — MAP-175
136
+
137
+ **Research attribution:** These replacement entrypoints and downstream
138
+ operation callsites were found and verified by **Neo Shangguan** for MAP-175.
139
+
140
+ | Operation | Current bridge selector | macOS 27 entrypoint found | Downstream operation dispatch | Integration status | Found by |
141
+ |---|---|---|---|---|---|
142
+ | `leave-chat` | `-[IMChat leaveChat]` — absent | `-[IMChat leave]` | `[provider _chat_leave:chat]` | Found; not wired/live-smoked | **Neo Shangguan** |
143
+ | `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]` | Found; not wired/live-smoked | **Neo Shangguan** |
144
+ | `mentions` | No bridge operation | Confirmed-mention `NSAttributedString` attributes; no dedicated send selector | Existing `IMMessage` construction → `IMChat` send path | Payload construction found; not wired/live-smoked | **Neo Shangguan** |
145
+ | `text-styling` / `text-effects` | No bridge operation | IM text-style/effect attributes on `NSAttributedString`; no dedicated send selector | Existing `IMMessage` construction → `IMChat` send path | Payload construction found; not wired/live-smoked | **Neo Shangguan** |
146
+ | `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** |
147
+ | `chat-background` | No bridge operation | `-[IMChat setTranscriptBackgroundAndSendToChat:transferID:]` | `[provider _chat:chat setTranscriptBackgroundAndSendToChat:background transferID:transferID]` | Set/remove control flow found; asset/transfer contract not wired/live-smoked | **Neo Shangguan** |
148
+ | `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** |
149
+
150
+ The summary records stable selector names for implementation. Numeric callsite
151
+ addresses remain in the detailed evidence below because they are specific to a
152
+ single macOS build and must not become bridge constants.
153
+
154
+ The research ran against the development `mapidev` Messages profile, not the
155
+ production profile, on macOS 27.0 build `26A5421a` (`arm64e`) with IMCore
156
+ `1491.100.1.1.9`. `ipsw 3.1.712` read the native IMCore image directly from the
157
+ dyld shared cache. Runtime availability was checked by loading IMCore in an
158
+ independent process and querying the Objective-C method table; the probe did
159
+ not attach to or mutate Messages.
160
+
161
+ All addresses below are unslid and specific to this OS build. They are audit
162
+ evidence only and must never be hard-coded by the bridge; selectors still need
163
+ to be resolved by name at runtime.
164
+
165
+ ### `leave-chat` — found by Neo Shangguan
166
+
167
+ Runtime availability:
168
+
169
+ ```text
170
+ -[IMChat leaveChat] false
171
+ -[IMChat leave] true
172
+ -[IMChat leaveConversation] true
173
+ ```
174
+
175
+ `-[IMChat leave]` begins at `0x1bf212c88`. Its operation path is:
176
+
177
+ ```text
178
+ -[IMChat leave]
179
+ [self daemonServiceProvider] callsite 0x1bf212da4
180
+ [provider _chat_leave:self] callsite 0x1bf212db4
181
+ ```
182
+
183
+ The selector stubs resolve to `daemonServiceProvider` and `_chat_leave:`. At
184
+ the downstream callsite, the provider is the receiver and the current `IMChat`
185
+ is the operation argument. This establishes `-[IMChat leave]` as the high-level
186
+ replacement entrypoint; bridge code should call it rather than reaching into
187
+ `_chat_leave:` directly.
188
+
189
+ `-[IMChat leaveConversation]` was also disassembled and resolves to
190
+ `[self _closeSession:NO]`. It closes the session rather than dispatching the
191
+ group-leave operation, so it is not a `leaveChat` replacement.
192
+
193
+ The existing `leaveChat` selector can remain as the older-system path, with
194
+ `leave` as a runtime-gated fallback. This is a research result, not a release
195
+ claim: the native fork has not been rebuilt/injected with the fallback and no
196
+ live group-leave smoke has been performed.
197
+
198
+ ### `edit-message` — found by Neo Shangguan
199
+
200
+ Runtime availability:
201
+
202
+ ```text
203
+ editMessageItem:atPartIndex:withNewPartText:
204
+ backwardCompatabilityText: false
205
+ editMessage:atPartIndex:withNewPartText:
206
+ backwardCompatabilityText: false
207
+ editMessageItem:atPartIndex:withNewPartText:
208
+ newPartTranslation:backwardCompatabilityText: true
209
+ ```
210
+
211
+ The new method inserts a `newPartTranslation:` argument. It begins at
212
+ `0x1bf209994`, with the arm64 business arguments laid out as:
213
+
214
+ ```text
215
+ x2 = original message item
216
+ x3 = part index
217
+ x4 = new part text
218
+ x5 = new part translation
219
+ x6 = backward-compatibility text
220
+ ```
221
+
222
+ The ordinary-message operation path is:
223
+
224
+ ```text
225
+ -[IMChat editMessageItem:...newPartTranslation:...]
226
+ [factory editedMessageItemWithOriginalMessageItem:
227
+ editedPartIndex:newPartText:newPartTranslation:] callsite 0x1bf209b18
228
+ [self daemonServiceProvider] callsite 0x1bf209b28
229
+ [provider _chat:self
230
+ sendEditedMessageItem:editedItem
231
+ previousMessageItem:originalItem
232
+ partIndex:index
233
+ editType:1
234
+ backwardCompatabilityText:backwardText] callsite 0x1bf209b4c
235
+ ```
236
+
237
+ The final selector is
238
+ `_chat:sendEditedMessageItem:previousMessageItem:partIndex:editType:backwardCompatabilityText:`.
239
+ The method constructs the new edited item, resolves the daemon service
240
+ provider, and dispatches the edit with constant `editType` value `1`. The
241
+ alternate branch calls
242
+ `editScheduledMessageItem:atPartIndex:withNewPartText:newPartTranslation:` and
243
+ is for scheduled messages, not the ordinary edit path.
244
+
245
+ `newPartTranslation` is retained and forwarded without a non-null branch, which
246
+ is consistent with passing `nil` for an ordinary untranslated edit. A live
247
+ smoke must verify that assumption. The high-level five-argument `IMChat` method
248
+ is the replacement target; it requires an accurately cast `objc_msgSend` call
249
+ rather than `performSelector:` and should be runtime-gated alongside the older
250
+ selectors.
251
+
252
+ ### `mentions` — found by Neo Shangguan
253
+
254
+ Mentions do not introduce a separate daemon operation selector. `IMMessage`
255
+ continues to carry an `NSAttributedString`, and the existing `IMChat` send path
256
+ dispatches the message. The missing bridge work is therefore attributed-text
257
+ payload construction rather than discovery of another `IMChat` send method.
258
+
259
+ The native macOS 27 `IMSharedUtilities` image exports the transport constants
260
+ needed to construct and inspect that payload:
261
+
262
+ ```text
263
+ IMMentionAttributeName "__kIMMentionAttributeName"
264
+ IMMentionAttributeNameNeedsAnimation "__kIMMentionAttributeNameNeedsAnimation"
265
+ IMMentionConfirmedMention "__kIMMentionConfirmedMention"
266
+ IMMentionAutomaticConfirmedMention "__kIMMentionAutomaticConfirmedMention"
267
+ IMMentionUnconfirmedDirectMention "__kIMMentionUnconfirmedDirectMention"
268
+ ```
269
+
270
+ They were resolved from
271
+ `/System/Library/PrivateFrameworks/IMSharedUtilities.framework` in an
272
+ independent runtime probe. Bridge code should likewise resolve symbols by name
273
+ and must not embed their build-specific addresses or private string values.
274
+
275
+ The construction pattern is visible in the iOSSupport ChatKit image at
276
+ `+[CKMentionsUtilities configureAttributedString:inTextView:
277
+ forConfirmedMentionInRange:needingAnimation:]`, beginning at `0x1dbecbe38`:
278
+
279
+ ```text
280
+ add IMMentionAttributeName to the confirmed mention range 0x1dbecbea0
281
+ remove IMMentionUnconfirmedDirectMention from that range 0x1dbecbebc
282
+ remove IMMentionAutomaticConfirmedMention from that range 0x1dbecbed8
283
+ conditionally add IMMentionAttributeNameNeedsAnimation 0x1dbecbf54
284
+ ```
285
+
286
+ The mutation callsites resolve to
287
+ `-[NSMutableAttributedString addAttribute:value:range:]` and
288
+ `-[NSMutableAttributedString removeAttribute:range:]`. The related
289
+ `configureAttributedString:automaticMentionAttributeWithOriginalText:
290
+ entityNode:nodeId:forRange:` method builds composer/autocomplete metadata and
291
+ stores it under `IMMentionAutomaticConfirmedMention`; the confirmed path
292
+ removes that temporary marker before sending.
293
+
294
+ ChatKit is present in this cache under `/System/iOSSupport` rather than as the
295
+ native transport framework, so its helper itself is evidence for the payload
296
+ shape, not a selector the bridge should invoke. The native
297
+ `IMSharedUtilities` constants plus the existing attributed `IMMessage` send
298
+ path are the candidate integration boundary. Exact attribute values and range
299
+ handling still require a development-profile live send smoke before the
300
+ feature can be declared available.
301
+
302
+ ### `text-styling` and `text-effects` — found by Neo Shangguan
303
+
304
+ Inline bold, italic, underline, strikethrough, and animated text effects also
305
+ reuse the attributed `IMMessage` and existing `IMChat` send operation. There is
306
+ no additional daemon send selector. The missing bridge work is to construct
307
+ the transport attributes over the requested text ranges.
308
+
309
+ The native macOS 27 `IMSharedUtilities` image exports the relevant integration
310
+ symbols:
311
+
312
+ ```text
313
+ IMTextBoldAttributeName
314
+ IMTextItalicAttributeName
315
+ IMTextUnderlineAttributeName
316
+ IMTextStrikethroughAttributeName
317
+ IMTextEffectAttributeName
318
+ IMTextStyleAll
319
+ IMTextEffectNameFromType
320
+ IMTextEffectTypeFromName
321
+ IMTextEffectOrderedSupportedNames
322
+ IMTextEffectNameBig / Bloom / Bounce / Explode / Jitter / Nod /
323
+ ScaleRipple / Shake / Small / Somersault / Squish / Stretch
324
+ IMServiceCapabilityTextEffects
325
+ ```
326
+
327
+ The iOSSupport ChatKit construction reference maps the inline-style bit values
328
+ to transport and local display attributes as follows:
329
+
330
+ | Style value | IM transport marker | Local display attribute |
331
+ |---|---|---|
332
+ | `1` | `IMTextBoldAttributeName` | `NSFontAttributeName` with the bold trait |
333
+ | `2` | `IMTextItalicAttributeName` | `NSFontAttributeName` with the italic trait |
334
+ | `4` | `IMTextUnderlineAttributeName` | `NSUnderlineStyleAttributeName` |
335
+ | `8` | `IMTextStrikethroughAttributeName` | `NSStrikethroughStyleAttributeName` |
336
+
337
+ `-[NSMutableAttributedString(TextEffects)
338
+ ck_applyTextStyle:options:range:]` begins at `0x1dbcc19fc`. It asks
339
+ `ck_actionForIMTextStyle:range:` whether the requested style should be added,
340
+ removed, or left unchanged, then tail-dispatches to
341
+ `ck_addTextStyle:options:range:` at `0x1dbcc1a74` or
342
+ `ck_removeTextStyle:options:range:` at `0x1dbcc1aac`. The add implementation
343
+ begins at `0x1dbcc1ac4`.
344
+
345
+ Within `ck_addTextStyle:options:range:`, option bit zero controls the private IM
346
+ transport marker and option bit one controls the corresponding local display
347
+ attribute. The callsites that add the four IM markers are:
348
+
349
+ ```text
350
+ IMTextBoldAttributeName 0x1dbcc1d04
351
+ IMTextItalicAttributeName 0x1dbcc1c2c
352
+ IMTextUnderlineAttributeName 0x1dbcc1ccc
353
+ IMTextStrikethroughAttributeName 0x1dbcc1da8
354
+ ```
355
+
356
+ Each transport marker receives a constant `NSNumber` value. ChatKit separately
357
+ updates font traits or the standard underline/strikethrough attributes for
358
+ local display. Those visual attributes must not be confused with the private
359
+ IM markers that describe the wire payload.
360
+
361
+ Animated effects are constructed by
362
+ `-[NSMutableAttributedString(TextEffects)
363
+ ck_applyTextEffectType:range:]`, beginning at `0x1dbcc1340`. Its primary path
364
+ is:
365
+
366
+ ```text
367
+ remove IMTextEffectAttributeName and NSTextAnimationAttributeName
368
+ remove IMTextStyleAll with both transport and display options
369
+ effectName = IMTextEffectNameFromType(effectType) 0x1dbcc14e4
370
+ animation = [UITextAnimation animationWithName:effectName] 0x1dbcc1670
371
+ wireValue = [NSNumber numberWithInteger:effectType] 0x1dbcc16a0
372
+ add NSTextAnimationAttributeName = animation 0x1dbcc1694
373
+ add IMTextEffectAttributeName = wireValue 0x1dbcc16c0
374
+ ```
375
+
376
+ The method also checks confirmed-mention and attachment ranges before applying
377
+ an effect, and may repeat the two add operations over adjusted subranges. The
378
+ `NSTextAnimationAttributeName`/`UITextAnimation` pair is the local preview;
379
+ the `IMTextEffectAttributeName`/numeric effect type pair is the transport
380
+ representation.
381
+
382
+ As with mentions, these mutation helpers live in the `/System/iOSSupport`
383
+ ChatKit image and are evidence for payload shape rather than bridge call
384
+ targets. A bridge implementation should resolve the native
385
+ `IMSharedUtilities` symbols by name, construct the attributed payload itself,
386
+ and reuse the existing `IMMessage`/`IMChat` send path. The marker values,
387
+ serialization behavior, recipient rendering, and range handling still require
388
+ a development-profile live send smoke.
389
+
390
+ ### `contact-card` — found by Neo Shangguan
391
+
392
+ A real contact card is a specialized vCard attachment, not a distinct IMCore
393
+ send operation. The recipient-side ChatKit type is `CKContactMediaObject`,
394
+ which inherits through `CKCardMediaObject` from the generic `CKMediaObject`
395
+ attachment model:
396
+
397
+ ```text
398
+ CKContactMediaObject : CKCardMediaObject : CKMediaObject
399
+ ```
400
+
401
+ `CKMediaObject` directly stores a `CKFileTransfer`, file URL, transfer GUID,
402
+ data, filename, MIME type, and UTI. `CKContactMediaObject` adds vCard parsing,
403
+ summary, image, preview, and contact-balloon behavior; its methods include
404
+ `contactCardPayloadFileURL:`, `vCardSummary`, and `vCardImageOfSize:`.
405
+
406
+ `+[CKContactMediaObject UTITypes]` begins at `0x1dbcf40b4`. It loads
407
+ `kUTTypeVCard` and returns it in a one-element array via
408
+ `+[NSArray arrayWithObjects:count:]` at `0x1dbcf40f4`:
409
+
410
+ ```text
411
+ +[CKContactMediaObject UTITypes] → @[ kUTTypeVCard ]
412
+ ```
413
+
414
+ The shared-cache wire identifiers corroborate the representation:
415
+
416
+ ```text
417
+ public.vcard native IMSharedUtilities
418
+ text/vcard IMDaemonCore
419
+ text/x-vcard IMDaemonCore and IMFoundation (legacy-compatible MIME)
420
+ .vcf Contacts filename generation
421
+ ```
422
+
423
+ The outbound integration path is therefore:
424
+
425
+ ```text
426
+ CNContact or caller-supplied vCard data
427
+ → serialize/write a .vcf file
428
+ → filename + vCard MIME/UTI metadata
429
+ → existing IMFileTransfer attachment send
430
+ → recipient CKContactMediaObject
431
+ → CKContactBalloonView
432
+ ```
433
+
434
+ The existing bridge attachment handler already stages a local file through
435
+ `IMFileTransferCenter`, sets the transfer filename and MIME type, registers the
436
+ transfer with the daemon, and reuses the ordinary message send operation. A
437
+ contact-card integration should build on that path rather than call ChatKit.
438
+ It may either accept a valid `.vcf` or serialize a contact using the Contacts
439
+ framework; the exact accepted MIME choice, transfer metadata, recipient
440
+ rendering, and inbound parsing still require a development-profile live smoke.
441
+
442
+ One apparent UI lead was explicitly ruled out. Despite its name,
443
+ `-[CKChatController(Contacts) contactPicker:didSelectContact:]` at
444
+ `0x1dc070830` eventually dispatches
445
+ `updateContact:withNicknameUpdate:updateType:addHandleToContact:presentationMode:`
446
+ at `0x1dc070a00` / `0x1dc070a40`. It updates the local address book after a
447
+ contact/nickname prompt; it is not a contact-card send callsite.
448
+
449
+ ### `chat-background` — found by Neo Shangguan
450
+
451
+ The macOS 27 `IMChat` entrypoint is:
452
+
453
+ ```objc
454
+ -[IMChat setTranscriptBackgroundAndSendToChat:transferID:]
455
+ ```
456
+
457
+ It begins at `0x1bf20b080`. The arm64 argument flow shows the first business
458
+ argument retained as the pending background object and the second retained as
459
+ the transfer identifier. The operation path is:
460
+
461
+ ```text
462
+ -[IMChat setTranscriptBackgroundAndSendToChat:background transferID:transferID]
463
+ [self daemonServiceProvider] callsite 0x1bf20b0b8
464
+ [provider _chat:self
465
+ setTranscriptBackgroundAndSendToChat:background
466
+ transferID:transferID] callsite 0x1bf20b0d0
467
+ ```
468
+
469
+ The final selector stub resolves to
470
+ `_chat:setTranscriptBackgroundAndSendToChat:transferID:`. ChatKit also exposes
471
+ `setPendingTranscriptBackground:transferID:`, corroborating that the outbound
472
+ UI flow stages a background and its associated transfer before invoking the
473
+ `IMChat` entrypoint.
474
+
475
+ The ChatKit set/remove control flow was also recovered. The UI entrypoint
476
+ `-[CKCoreChatController setNewTranscriptBackground:]` begins at
477
+ `0x1dbc7d1b8` and generates the transfer identifier with
478
+ `+[NSString stringGUID]`. It stores the pending state on the conversation and
479
+ then branches on the background argument:
480
+
481
+ ```text
482
+ transferID = [NSString stringGUID]
483
+ [self.conversation setPendingTranscriptBackground:background
484
+ transferID:transferID]
485
+
486
+ if background != nil:
487
+ [background createBackgroundWithWatchDataWithCompletion:^(preparedBackground) {
488
+ [self updateTranscriptBackground:preparedBackground
489
+ transferID:transferID]
490
+ }]
491
+ else:
492
+ [self updateTranscriptBackground:nil
493
+ transferID:transferID]
494
+ ```
495
+
496
+ `-[CKCoreChatController updateTranscriptBackground:transferID:]` begins at
497
+ `0x1dbc7c538`. Its non-null asynchronous completion and null removal branch
498
+ converge on the same `IMChat` selector:
499
+
500
+ ```text
501
+ set: [chat setTranscriptBackgroundAndSendToChat:preparedBackground
502
+ transferID:transferID] callsite 0x1dbc7cc38
503
+ remove: [chat setTranscriptBackgroundAndSendToChat:nil
504
+ transferID:transferID] callsite 0x1dbc7c710
505
+ ```
506
+
507
+ This establishes that removal is represented by a null background sent through
508
+ the same operation, not by a separate remove selector. A complete bridge recipe
509
+ still needs the concrete background object/archive construction and channel
510
+ transfer contract. Those details, recipient behavior, and both set/remove
511
+ operations require a development-profile live smoke before release claims.
512
+
513
+ ### `location-request` — found by Neo Shangguan
514
+
515
+ Location Request is a Find My Messages-app payload, not an
516
+ `IMFMFSession` tracking operation. The ChatKit entrypoint
517
+ `-[CKChatController _stageFindMyLocationRequest]` begins at `0x1dbc13240`.
518
+ Its relevant operation path is:
519
+
520
+ ```text
521
+ [IMFeatureFlags sharedFeatureFlags]
522
+ -isWaldoEnabled
523
+
524
+ soft-link the MSMessage class
525
+ +respondsToSelector:@selector(findMyLocationRequestMessage)
526
+ +findMyLocationRequestMessage
527
+
528
+ [CKComposition compositionWithMSMessage:requestMessage
529
+ appExtensionIdentifier:IMBalloonBundleIdentifierWaldo]
530
+ → [CKChatController setComposition:composition]
531
+ ```
532
+
533
+ The feature-gate selectors resolve to `sharedFeatureFlags` and
534
+ `isWaldoEnabled`. ChatKit soft-links the `MSMessage` class, verifies the class
535
+ factory at runtime, and then invokes `+[MSMessage findMyLocationRequestMessage]`.
536
+ It wraps the returned request in a `CKComposition` using the private
537
+ `IMBalloonBundleIdentifierWaldo` constant before staging the composition.
538
+ `Waldo` is the Find My Messages-balloon/app identity used for this request.
539
+
540
+ This rules out two misleading IMCore paths. `-[IMFMFSession
541
+ startTrackingLocationForChat:]` refreshes an already-shared location, while
542
+ `-[IMFMFSession startSharingWithChat:withDuration:]` starts sharing the local
543
+ user's location. Neither is the operation that constructs a request asking the
544
+ other participant to share.
545
+
546
+ The callable factory and staging callsites are now known, but a bridge recipe
547
+ is still incomplete. The implementation of
548
+ `+[MSMessage findMyLocationRequestMessage]`, the serialized payload it returns,
549
+ and the safest route from that payload into the bridge's existing `IMMessage`
550
+ send path remain to be recovered. Calling a `CKChatController` UI instance is
551
+ not assumed to be an appropriate relay integration path.
552
+
553
+ ### Implementation readiness and remaining work
554
+
555
+ Finding a callsite removes the selector-discovery blocker; it does not by
556
+ itself make the operation available. This table is the implementation handoff
557
+ for the MAP-175 findings above:
558
+
559
+ | Operation | Readiness | What is resolved | Remaining implementation work |
560
+ |---|---|---|---|
561
+ | `leave-chat` | **Ready to wire** | macOS 27 `-[IMChat leave]` entrypoint and `_chat_leave:` dispatch | Add the runtime-gated `leave` fallback while retaining the older `leaveChat` path; rebuild/inject and live-smoke on `mapidev` |
562
+ | `edit-message` | **Ready to wire** | Five-argument selector, argument order, edited-item builder, and daemon dispatch | Add an accurately cast `objc_msgSend`, runtime probe/fallbacks, and ordinary-edit `nil` translation handling; rebuild/inject and live-smoke |
563
+ | `mentions` | **Partially blocked** | Native mention attribute symbols and confirmed-mention mutation path | Confirm the target identity/handle attribute value, serialization, and range behavior with a development-profile send/receive smoke |
564
+ | `text-styling` / `text-effects` | **Prototype-ready** | Transport attributes, style bit mapping, effect type/name mapping, and existing send boundary | Resolve symbols dynamically, construct/serialize the attributed payload, validate mixed/attachment ranges and recipient rendering, then live-smoke |
565
+ | `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 |
566
+ | `chat-background` remove | **Ready to prototype** | Null-background remove semantics, transfer-ID generation, `IMChat` entrypoint, and daemon dispatch | Wire the operation and validate removal plus recipient state on the development profile |
567
+ | `chat-background` set | **Partially blocked** | Set control flow, Watch-data preparation selector, `IMChat` entrypoint, and daemon dispatch | Recover/reproduce the concrete background archive and channel-transfer contract; then validate upload, recipient rendering, and retry behavior |
568
+ | `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 |
569
+
570
+ Every new or replaced bridge path still needs defensive runtime capability
571
+ probing, native wiring, a matched CLI/dylib rebuild, injection under the
572
+ development `mapidev` profile, and operation-specific live smoke evidence.
573
+ Build-specific addresses above remain audit evidence and must never become
574
+ runtime constants.
575
+
576
+ These findings do not include an upstream Messages UI-controller trace. MAP-175
577
+ does not require that trace once the callable entrypoint and downstream
578
+ operation dispatch have been established.
579
+
580
+ ## Finding a replacement selector (the archaeology)
581
+
582
+ When a selector goes dead (host `✗`, `respondsToSelector:` NO), on a host of that
583
+ macOS version:
584
+
585
+ 1. **Dump the class.** `class-dump` (or Hopper/IDA) the dyld shared cache and read
586
+ the target class — e.g. all `IMChat` methods — to spot the renamed/new one.
587
+ 2. **Trace the UI.** Attach `lldb` / `frida-trace` to Messages.app, do the action
588
+ by hand, and watch which IMCore selectors fire (`objc` method trace on `IMChat`).
589
+ 3. **Cross-check open source.** BlueBubbles and Barcelona (open-imcore) implement
590
+ leave / edit / etc.; their newer-macOS handling names candidate selectors.
591
+ 4. Swap the `@selector(...)` in the handler, add its status-probe marker, rebuild
592
+ the matched CLI+dylib, inject (SIP off), and smoke.
593
+
594
+ For `leave-chat`: `handleLeaveChat` and everything above it (relay → RPC → file
595
+ IPC → dispatch) is intact. The only dead link is `-[IMChat leaveChat]`. MAP-175
596
+ identified `-[IMChat leave]` as its macOS 27 replacement; the remaining work is
597
+ to re-point that call, add a status marker, rebuild/inject the matched native
598
+ fork, and live-smoke it on the development profile.
599
+
600
+ [#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.1",
3
+ "version": "0.2.3",
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": {