@mapier/imsg-sdk 0.6.0 → 0.7.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.
Potentially problematic release.
This version of @mapier/imsg-sdk might be problematic. Click here for more details.
- package/dist/gateway/fake.d.ts +6 -1
- package/dist/gateway/fake.js +57 -1
- package/dist/gateway/fake.js.map +1 -1
- package/dist/gateway/imsg.d.ts +6 -1
- package/dist/gateway/imsg.js +22 -0
- package/dist/gateway/imsg.js.map +1 -1
- package/dist/gateway/multipart.d.ts +38 -0
- package/dist/gateway/multipart.js +216 -0
- package/dist/gateway/multipart.js.map +1 -0
- package/dist/gateway/types.d.ts +22 -0
- package/dist/gateway/types.js +4 -0
- package/dist/gateway/types.js.map +1 -1
- package/dist/imsg/rpc.d.ts +2 -0
- package/dist/imsg/rpc.js +8 -0
- package/dist/imsg/rpc.js.map +1 -1
- package/dist/imsg/status.js +10 -0
- package/dist/imsg/status.js.map +1 -1
- package/docs/api.md +1 -1
- package/docs/capability-matrix.md +1 -0
- package/docs/gateway-contract.md +153 -0
- package/package.json +1 -1
package/docs/gateway-contract.md
CHANGED
|
@@ -66,6 +66,7 @@ interface Gateway {
|
|
|
66
66
|
locationRequest: boolean;
|
|
67
67
|
extensionCardSend: boolean;
|
|
68
68
|
extensionCardUpdate: boolean;
|
|
69
|
+
multipartSend: boolean;
|
|
69
70
|
};
|
|
70
71
|
subscribe(sinceId?: number): AsyncIterable<GatewayEvent>;
|
|
71
72
|
history(chatId: number, limit?: number): Promise<ImsgMessage[]>;
|
|
@@ -117,6 +118,11 @@ interface Gateway {
|
|
|
117
118
|
card: ExtensionCard,
|
|
118
119
|
opts?: { update?: ExtensionCardHandle },
|
|
119
120
|
): Promise<ExtensionCardResult>;
|
|
121
|
+
sendMultipart(
|
|
122
|
+
chatId: number,
|
|
123
|
+
parts: readonly MultipartPart[],
|
|
124
|
+
opts?: { replyToGuid?: string; effect?: string; subject?: string },
|
|
125
|
+
): Promise<MultipartResult>;
|
|
120
126
|
editMessage(chatId: number, targetGuid: string, text: string): Promise<{
|
|
121
127
|
ok: boolean;
|
|
122
128
|
skipped?: 'unchanged';
|
|
@@ -266,6 +272,22 @@ interface ExtensionCardResult {
|
|
|
266
272
|
effectStarted?: boolean;
|
|
267
273
|
reason?: GroupMutationFailure; // only unsupported / refused / fire-failed are reachable
|
|
268
274
|
}
|
|
275
|
+
|
|
276
|
+
// sendMultipart (M2B-55). One message made of several parts, in the order
|
|
277
|
+
// given — that order is each part's NUMBER, the same number tapback's
|
|
278
|
+
// `partIndex` names. Audio is not a part: the voice flag is message-wide, so a
|
|
279
|
+
// voice note still goes alone through sendAttachment.
|
|
280
|
+
type MultipartPart =
|
|
281
|
+
| { kind: 'text'; text: string; textFormatting?: TextFormatRange[] } // the same ranges sendRich takes
|
|
282
|
+
| { kind: 'file'; filePath: string }; // a path on the HOST Mac
|
|
283
|
+
|
|
284
|
+
interface MultipartResult {
|
|
285
|
+
ok: boolean;
|
|
286
|
+
guid?: string; // absent when Messages had not exposed the row yet; never synthesized
|
|
287
|
+
partCount?: number; // the host's own count of the parts it built
|
|
288
|
+
effectStarted?: boolean;
|
|
289
|
+
reason?: GroupMutationFailure; // only unsupported / refused / fire-failed are reachable
|
|
290
|
+
}
|
|
269
291
|
```
|
|
270
292
|
|
|
271
293
|
### Tier 2 (bridge-backed)
|
|
@@ -305,6 +327,11 @@ Part-level tapbacks require `tapbackPart`, which the helper computes from the
|
|
|
305
327
|
part's own chat item plus an associated-message initializer taking a range —
|
|
306
328
|
and which no other tapback marker may stand in for, because a helper without it
|
|
307
329
|
does not reject `part_index`, it writes a corrupt row (M2B-45, above).
|
|
330
|
+
Multipart sends require `multipartSend`, composed by the helper from the
|
|
331
|
+
file-transfer centre plus the attachment IMMessage initializer, and equally
|
|
332
|
+
un-substitutable: a helper predating file parts reads only each part's `text`,
|
|
333
|
+
so it DROPS every file part without a word and answers ok with a short
|
|
334
|
+
`parts_count` (M2B-55, below).
|
|
308
335
|
Missing status, malformed JSON, or absent markers fail closed.
|
|
309
336
|
`FakeGateway.capabilities` mirrors this gate and can disable the same surface
|
|
310
337
|
for parity tests. A remote adapter (e.g. imsg-agent's connector) must gate on
|
|
@@ -592,6 +619,67 @@ prevent.
|
|
|
592
619
|
builder's returns above the IMChat call; `send-extension-card failed:` is
|
|
593
620
|
deliberately **not** among them, because that `@try` wraps the whole
|
|
594
621
|
build-and-send block. FakeGateway reaches only the proven half.
|
|
622
|
+
- **`sendMultipart(chatId, parts, opts?)`** — `send.multipart`
|
|
623
|
+
(`RPCServer+MultipartHandler.swift` → `handleSendMultipart` in
|
|
624
|
+
`IMsgInjected.m`). Several parts as **one message**, targeting an **existing**
|
|
625
|
+
chat only. Gated on `multipartSend`, and the gate covers the whole verb
|
|
626
|
+
including a text-only list: the marker guards a silent LOSS rather than a
|
|
627
|
+
rejection (a helper predating file parts drops them and answers ok), so a
|
|
628
|
+
caller must never be left to discover mid-list that this host eats photos.
|
|
629
|
+
|
|
630
|
+
**What "one message" means on a phone** — proven on a real iPhone from the
|
|
631
|
+
lab Mac (macOS 26.6.2) on 2026-09-21, through the native CLI:
|
|
632
|
+
one notification and one `chat.db` row, **not** one bubble. Each part draws as
|
|
633
|
+
its own bubble, in the order given, and consecutive photos are gathered by the
|
|
634
|
+
receiving phone into a stack (five photos arrived as one stack labelled
|
|
635
|
+
"5 photos"). Two text parts in a row are fine — two bubbles — so, unlike
|
|
636
|
+
Linq, this verb allows them. A text part with a **trailing newline draws an
|
|
637
|
+
empty last line** in its bubble, so callers must not add one (the
|
|
638
|
+
single-attachment path's caption does, which is a native wart, not a model to
|
|
639
|
+
copy). **Part number = index in `parts`**, and that is exactly the
|
|
640
|
+
`partIndex` a later `tapback` names. Audio is not a part: the voice flag is
|
|
641
|
+
message-wide, so a voice note still goes alone through `sendAttachment`.
|
|
642
|
+
|
|
643
|
+
**`effect`, `subject` and `replyToGuid` are message-wide**, read by the
|
|
644
|
+
native handler exactly as `send.rich` reads them — IMCore has one of each per
|
|
645
|
+
message, never one per part. Per-part `textFormatting` is the `sendRich`
|
|
646
|
+
range vocabulary scoped to that part, offsets counted in that part's own
|
|
647
|
+
text; a mention still needs `mentionFormatting` and is `unsupported` without
|
|
648
|
+
it. Formatting on a **file** part is refused rather than dropped.
|
|
649
|
+
|
|
650
|
+
**Validation happens before the RPC**, mirroring the native pass that runs
|
|
651
|
+
before a single transfer is staged: a non-empty array, every entry an object
|
|
652
|
+
of a known `kind`, non-empty `text`, non-empty `filePath` carrying no newline
|
|
653
|
+
or NUL, per-part formatting through the same `textFormattingRefusal` sendRich
|
|
654
|
+
uses, and the Mac-side caps **100 parts / 40 file parts**
|
|
655
|
+
— plus one rule that exists only because of how this verb fails: a part
|
|
656
|
+
carrying **both** a `text` and a `filePath` is refused *before* its `kind` is
|
|
657
|
+
consulted, never resolved by the label. Whichever branch the label chose would
|
|
658
|
+
drop the other key without a word, and losing part content in silence is the
|
|
659
|
+
failure the whole verb exists to remove; it must not come back in through a
|
|
660
|
+
caller that names a part wrong. The native handler refuses it by index for the
|
|
661
|
+
same reason (an empty string counts as absent on both keys, on both sides)
|
|
662
|
+
(`MULTIPART_MAX_PARTS` / `MULTIPART_MAX_FILE_PARTS`, mirroring
|
|
663
|
+
`MultipartLimits` and `kMaxMultipartParts`). Each is a proven no-op
|
|
664
|
+
(`effectStarted:false`, `reason:'refused'`). Two native checks are
|
|
665
|
+
deliberately **not** mirrored: the 100 MiB total-byte cap and whether each
|
|
666
|
+
file exists. This SDK never touches the filesystem — the path belongs to the
|
|
667
|
+
Mac — so a local check would make the fake and the real path disagree in both
|
|
668
|
+
directions; a missing file is the helper's refusal, classified through
|
|
669
|
+
`MULTIPART_PROVEN_REJECTIONS`.
|
|
670
|
+
|
|
671
|
+
**No verification pass exists**, for the reason `sendAttachment` has none:
|
|
672
|
+
the synchronous response carries the message's guid, which is the proof to
|
|
673
|
+
read `sendStatus()` on, while the row Messages writes at dispatch says
|
|
674
|
+
nothing about the upload that follows (`is_sent` stays 0 until the bytes are
|
|
675
|
+
out). So only `unsupported` / `refused` / `fire-failed` are reachable.
|
|
676
|
+
`send-multipart failed:` is deliberately **not** a proven rejection: that
|
|
677
|
+
`@try` wraps the whole build-and-dispatch block. `partCount` is the **host's**
|
|
678
|
+
count of the parts it built and is never replaced with the caller's list
|
|
679
|
+
length — a count shorter than the list is precisely how a helper that dropped
|
|
680
|
+
file parts would show up. There is deliberately no numeric `id`: the native
|
|
681
|
+
result sets `message_id` to the message GUID, so no rowid exists on the real
|
|
682
|
+
path and FakeGateway must not hand back one either.
|
|
595
683
|
- **`editMessage(chatId, targetGuid, text)`** — `message.edit`
|
|
596
684
|
(`handleMessageEdit`; imsg `docs/edit.md`). Capability-gated on the
|
|
597
685
|
`editMessage` marker (see above): on macOS 27 the helper calls the
|
|
@@ -1209,6 +1297,49 @@ both markers read `true` on the current build — a host with `extensionCardSend
|
|
|
1209
1297
|
and not `extensionCardUpdate` is possible by construction but has never been
|
|
1210
1298
|
seen. There is no smoke leg for this verb yet; the host proof is driven by hand.
|
|
1211
1299
|
|
|
1300
|
+
**HOST-VERIFIED 2026-09-21 (lab Mac, macOS 26.6.2) — `sendMultipart` (M2B-55),
|
|
1301
|
+
through this SDK verb.** The native layer was already proven on a real iPhone
|
|
1302
|
+
the same day through the native CLI (Mapier-Labs/imsg#28): ten shapes — caption
|
|
1303
|
+
+ photo, photo + caption, caption + 3 photos, alternating text and photos, two
|
|
1304
|
+
text parts, five photos, a 93 MiB ten-photo message — each arriving as a single
|
|
1305
|
+
notification with the parts in the order given and the photos stacked, which is
|
|
1306
|
+
where the "no trailing newline" and "consecutive text parts are fine" rules come
|
|
1307
|
+
from, and where the phone RENDERING of these shapes is settled. This run drove
|
|
1308
|
+
the gateway verb itself: the packed tarball of SDK `43b6454` installed into a
|
|
1309
|
+
scratch dir, `IMSG_BIN` pointed at the native build of imsg#28 head `f2a8bc6`
|
|
1310
|
+
(helper sha256 `3229d9dd…`). The phone rendering was not re-judged per SDK
|
|
1311
|
+
send — these legs prove the SDK's half, and `chat.db` is the oracle for it:
|
|
1312
|
+
|
|
1313
|
+
- `capabilities.multipartSend` and `capabilities.mentionFormatting` both read
|
|
1314
|
+
`true` off that build.
|
|
1315
|
+
- `[text, file, file, text]` → `{ok:true, guid, partCount:4}`, and that guid is
|
|
1316
|
+
**ONE** `chat.db` row with `part_count` 4, **two attachment joins**,
|
|
1317
|
+
`is_sent` 1, `is_delivered` 1, `error` 0. That pair is the load-bearing
|
|
1318
|
+
observation of the whole verb: a `partCount` of 4 arriving with both photos
|
|
1319
|
+
actually joined is the direct evidence that the file parts were not silently
|
|
1320
|
+
dropped — the failure the `multipartSend` marker exists to prevent, until now
|
|
1321
|
+
reasoned from the helper's source rather than seen.
|
|
1322
|
+
- `[text with a bold range, file]` with `effect` and `subject` →
|
|
1323
|
+
`{ok:true, guid, partCount:2}`; ONE row, `part_count` 2, one attachment,
|
|
1324
|
+
the subject set, and `expressive_send_style_id`
|
|
1325
|
+
`com.apple.messages.effect.CKConfettiEffect`, sent and delivered, `error` 0.
|
|
1326
|
+
So the per-part `text_formatting` and the message-wide `effect` / `subject`
|
|
1327
|
+
survive this SDK's wire shape into the fields the native handler's
|
|
1328
|
+
`send.rich` parsers write.
|
|
1329
|
+
- An empty list, a 41-file list, and `textFormatting` on a file part each
|
|
1330
|
+
answered `{ok:false, effectStarted:false, reason:'refused'}` — and the run
|
|
1331
|
+
wrote **exactly two rows in total**, so all three refusals fired nothing.
|
|
1332
|
+
That row count is the only way to prove `effectStarted:false`, which is a
|
|
1333
|
+
claim about what did NOT happen.
|
|
1334
|
+
|
|
1335
|
+
**Still NOT host-verified for M2B-55:** a confirmed mention inside a multipart
|
|
1336
|
+
text part (it needs a group thread, and the marker reading `true` is not the
|
|
1337
|
+
same as a mention landing); `replyToGuid` as an inline-reply target; and every
|
|
1338
|
+
string in `MULTIPART_PROVEN_REJECTIONS` — no send failed on the host, so the
|
|
1339
|
+
fire-failure classifier stays read-from-source and unit-tested only, unlike
|
|
1340
|
+
M2B-45's `'has no part '`, which its run happened to provoke. There is still no
|
|
1341
|
+
smoke leg for this verb; the host proof is driven by hand.
|
|
1342
|
+
|
|
1212
1343
|
- **subscribe(sinceId?)** — long-lived event stream of new messages/reactions across all
|
|
1213
1344
|
chats the host Mac's Messages account can see. `sinceId` is an **exclusive** cursor: only
|
|
1214
1345
|
events with `id > sinceId` are delivered (catch-up semantics, not "starting at"). Today
|
|
@@ -1576,6 +1707,22 @@ a different product.
|
|
|
1576
1707
|
implementations also refuse to invent a guid: when Messages exposes none, the
|
|
1577
1708
|
success carries `updatable:false` and no handle rather than a synthesized one.
|
|
1578
1709
|
|
|
1710
|
+
- **A multipart send is ONE row, and the fake may under-count its parts —
|
|
1711
|
+
never over-count** (M2B-55). `sendMultipart` emits a single outbound row on
|
|
1712
|
+
both implementations, whatever the part list holds: one notification, one
|
|
1713
|
+
`chat.db` row, several bubbles. What the fake does not do is synthesize that
|
|
1714
|
+
row's inner shape. The real row's `text` is Apple's own concatenation of the
|
|
1715
|
+
parts with object-replacement placeholders where the files sit, and its
|
|
1716
|
+
`attachments[]` only appears on a later history/watch row — the send response
|
|
1717
|
+
reports neither, and inventing them is exactly what `sendAttachment` already
|
|
1718
|
+
refuses to do. The consequence is deliberate: `FakeGateway.partCount()` still
|
|
1719
|
+
counts attachments, so a fake-sent multipart row reports 1 part however many
|
|
1720
|
+
it really had, and a part-level tapback on it is refused where a host would
|
|
1721
|
+
accept it. That is the allowed direction (the fake must not succeed at what
|
|
1722
|
+
the real path refuses), and counting `(text ? 1 : 0) + attachments.length`
|
|
1723
|
+
instead is a contract change that needs the host run to prove the real part
|
|
1724
|
+
layout first.
|
|
1725
|
+
|
|
1579
1726
|
- **Inbound media.** Real messages can carry `attachments[]` with empty `text`. FakeGateway must
|
|
1580
1727
|
be able to emit the exact attachment metadata with `text: undefined`/empty so
|
|
1581
1728
|
this path gets exercised — the agent currently ignores attachments gracefully
|
|
@@ -1737,6 +1884,12 @@ harness (`tsx --test`, see `package.json`). Required cases:
|
|
|
1737
1884
|
`extensionCardSend` but not `extensionCardUpdate` sends but refuses the update. One case
|
|
1738
1885
|
asserts a NEGATIVE: an update aimed at a previous update's guid is **not** caught — the
|
|
1739
1886
|
fake must not be safer than the Mac, which cannot detect it either
|
|
1887
|
+
- `sendMultipart` lands text and file parts as ONE outbound row (not one per part), reports
|
|
1888
|
+
the host's `partCount` and a guid, and preserves an inline-reply guid alongside the
|
|
1889
|
+
message-wide effect and subject; an empty list, an empty text part, formatting on a file
|
|
1890
|
+
part, 41 file parts and an unknown chat are each `refused` with no row written, and a
|
|
1891
|
+
missing `multipartSend` marker is `unsupported` — including for a text-only list, which
|
|
1892
|
+
such a host could actually have sent
|
|
1740
1893
|
- Fake-only restart coverage snapshots and restores a world, then proves
|
|
1741
1894
|
`sendStatus`, `checkHandle`, Name & Photo idempotency, chat backgrounds (and their
|
|
1742
1895
|
guid sequence), Find My shares, current group metadata, fixture state, and monotonic IDs
|
package/package.json
CHANGED