@mapier/imsg-sdk 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor.d.ts +25 -0
- package/dist/doctor.js +92 -0
- package/dist/doctor.js.map +1 -0
- package/dist/imsg/rpc.d.ts +2 -1
- package/dist/imsg/rpc.js +11 -2
- package/dist/imsg/rpc.js.map +1 -1
- package/dist/imsg/watch.d.ts +1 -1
- package/dist/imsg/watch.js +9 -2
- package/dist/imsg/watch.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/docs/api.md +58 -0
- package/docs/capability-matrix.md +23 -0
- package/docs/compatibility.md +48 -0
- package/docs/gateway-contract.md +817 -0
- package/docs/host-mac-control.md +188 -0
- package/docs/imsg-polls.md +473 -0
- package/docs/interactions.md +176 -0
- package/docs/operations.md +47 -0
- package/package.json +9 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Interaction catalog
|
|
2
|
+
|
|
3
|
+
`src/interactions/` is the extensibility seam for iMessage interaction kinds: web-link
|
|
4
|
+
cards, App Clip checkout sheets, native balloons (tapbacks, polls, message effects, URL
|
|
5
|
+
previews), and a future MSMessages host-extension tier. It is a typed catalog
|
|
6
|
+
(`InteractionRegistry`) plus a device-free trial harness (`InteractionHarness`) over
|
|
7
|
+
`FakeGateway`. It does not change `src/gateway/*` — every claim about our own Gateway is a
|
|
8
|
+
citation against `docs/gateway-contract.md` / `docs/imsg-polls.md`, never a new capability.
|
|
9
|
+
Claims about external products (cited below) are outside what this repo's docs can verify.
|
|
10
|
+
|
|
11
|
+
## Four-mechanism model
|
|
12
|
+
|
|
13
|
+
| Tier | Install cost | Reach | Richness | Fallback |
|
|
14
|
+
|---|---|---|---|---|
|
|
15
|
+
| `web-surface` | none | anyone with the link | full web UI, but leaves the chat | none — this **is** the floor |
|
|
16
|
+
| `app-clip` | none (ephemeral, no App Store entry) | anyone with an iPhone | native sheet (checkout/action UI), in-flow | `web-url` |
|
|
17
|
+
| `native-balloon` | none | anyone (stock Messages) | Apple's own balloon types only — tapback, poll, message effect, URL preview | `plain-text` |
|
|
18
|
+
| `extension` | recipient installs our MSMessages host **once** | installed users only | fully custom, live/interactive card, reusable across mini-apps | `web-url` |
|
|
19
|
+
|
|
20
|
+
Every tier richer than `web-surface` degrades to something (`InteractionDescriptor.fallback`) —
|
|
21
|
+
a recipient without the richer capability is never left with nothing, just a less rich version
|
|
22
|
+
of the same interaction.
|
|
23
|
+
|
|
24
|
+
**Two corrections to the naive three-tier read, both citing external primary sources (not
|
|
25
|
+
verifiable from this repo's own docs, cited here as provenance):**
|
|
26
|
+
|
|
27
|
+
- **App Clips are zero-install**, not "extension-lite." Apple's App Clip model launches an
|
|
28
|
+
ephemeral native experience straight from a link — no App Store visit, nothing left installed
|
|
29
|
+
after. Linq's Agent Pay flow uses exactly this: an iMessage link opens a native checkout sheet
|
|
30
|
+
with zero install cost, falling back to a web checkout on non-iPhone. That makes `app-clip`
|
|
31
|
+
tier-distinct from `extension` on the install axis even though both are "native" — **this is
|
|
32
|
+
the highest-value unbuilt lever for zero-install native interaction** in the catalog: richer
|
|
33
|
+
than a balloon, no install tax.
|
|
34
|
+
- **A real MSMessages extension is a *host*, not a one-off card.** Photon's Spectrum model:
|
|
35
|
+
the recipient installs Photon's MSMessages extension once; after that, arbitrary mini-app URLs
|
|
36
|
+
render live in-bubble via `app(url, {live:true})` (`MSMessageLiveLayout` /
|
|
37
|
+
`MSMessagesAppViewController`), no per-mini-app install. Non-installed recipients get a URL
|
|
38
|
+
fallback. The install wall is real (Apple gates live/interactive rendering behind "the
|
|
39
|
+
extension is installed," full stop — no wire flag, no server-render around it) but it's a
|
|
40
|
+
*one-time* cost amortized across every future mini-app, not a per-card cost. See
|
|
41
|
+
`host-extension-card` below.
|
|
42
|
+
|
|
43
|
+
## The contract
|
|
44
|
+
|
|
45
|
+
`InteractionDescriptor` (`src/interactions/types.ts`) is one row per kind. `InteractionRegistry`
|
|
46
|
+
enforces laws at `register()` time — a bad row fails at startup, not at first use:
|
|
47
|
+
|
|
48
|
+
1. No duplicate `id`.
|
|
49
|
+
2. `tier: 'extension'` requires `requiresRecipientInstall: true` — a real MSMessages host card
|
|
50
|
+
cannot exist without an install; a row claiming otherwise is a lie about the tier itself. This
|
|
51
|
+
check keys off `tier === 'extension'` specifically, not off `requiresRecipientInstall` in
|
|
52
|
+
general — `app-clip` is a separate, legitimately install-free tier and must not trip it.
|
|
53
|
+
3. `correlation: 'none'` forbids `inboundReadback: true` — a readback needs a key to correlate
|
|
54
|
+
against.
|
|
55
|
+
|
|
56
|
+
`InteractionRegistry.agentUsable()` — `outbound: 'agent' && status: 'host-smoked'` — is the set a
|
|
57
|
+
product feature can actually build on today: the running agent process can trigger it, and the
|
|
58
|
+
real host Mac has confirmed it does what it claims.
|
|
59
|
+
|
|
60
|
+
### Status is not a success flag
|
|
61
|
+
|
|
62
|
+
`InteractionStatus` (`unbuilt` → `fake-only` → `host-smoked`) is a **verification-maturity
|
|
63
|
+
ladder** — how far up `unit tests → FakeGateway → live smoke on host Mac` (the ladder in this
|
|
64
|
+
repo's `CLAUDE.md`) a kind has climbed. It does **not** mean "this works." `host-smoked` means
|
|
65
|
+
*ground truth is known*, not *ground truth is good*. Usability is carried by `outbound` and
|
|
66
|
+
`completionTransport`, not `status` — see `rich-link-card` below, which is `host-smoked` and
|
|
67
|
+
simultaneously `outbound: 'none'` because the host run proved it does *not* deliver.
|
|
68
|
+
|
|
69
|
+
## Today's catalog (`src/interactions/catalog.ts`)
|
|
70
|
+
|
|
71
|
+
Several rows below are corrected against earlier catalog drafts a teammate proposed from memory;
|
|
72
|
+
each correction was checked against this repo's own contract docs (or, for external-product
|
|
73
|
+
claims, cited as cross-repo/external provenance) before being written into `catalog.ts`. Three
|
|
74
|
+
corrections changed which kinds `agentUsable()` returns.
|
|
75
|
+
|
|
76
|
+
- **`tapback`** — native-balloon, `message-guid` correlation. The real path is
|
|
77
|
+
`Gateway.tapback(chatId, targetGuid, reaction, remove?)`, not `react()` — `react()` takes no
|
|
78
|
+
guid parameter at all (it always targets "the newest incoming bubble", `expectedGuid` is a
|
|
79
|
+
precondition, not targeting; `docs/gateway-contract.md` §1). A row that claims
|
|
80
|
+
`message-guid` correlation only fits the guid-targeted verb. Host-verified: `tier2-smoke.ts`
|
|
81
|
+
fires add/remove and confirms against history in the same run. `fallback: 'plain-text'` — a
|
|
82
|
+
recipient just sees no tapback, only the plain message.
|
|
83
|
+
- **`web-link`** — web-surface, `link-code` correlation, completion out-of-band (a product
|
|
84
|
+
store/webhook, not a Gateway event). This kind's real implementation (`link-surface.ts`, the
|
|
85
|
+
onboarding/round pages) lives in `imsg-agent`, not this repo, so its verification is outside
|
|
86
|
+
what `docs/gateway-contract.md` can attest to — flagged as such in its `notes`.
|
|
87
|
+
`fallback: 'none'` — it's the floor of the model, nothing to degrade to.
|
|
88
|
+
- **`poll`** — native-balloon, `message-guid` correlation. **Corrected from a draft that had
|
|
89
|
+
`outbound: 'manual-host-op'`, citing "contract forbids Gateway poll send."** That citation was
|
|
90
|
+
wrong: the contract's forbidden-surface list says `sendPoll` *creates polls only* — it forbids
|
|
91
|
+
**voting/unvoting** through the Gateway, not creating. `sendPoll` itself is a first-class
|
|
92
|
+
`Gateway` method, live-verified delivering ("renders as a native interactive poll on the
|
|
93
|
+
receiving device", `docs/gateway-contract.md` "Host-verification ledger"). There is no
|
|
94
|
+
principled distinction that makes `tapback` agent-outbound but `poll` manual-only — both are
|
|
95
|
+
Tier-2 bridge verbs, both host-verified, both agent-callable. `outbound: 'agent'`. Voting stays
|
|
96
|
+
inbound-readback-only; the agent still cannot vote on anyone's behalf, only create.
|
|
97
|
+
`fallback: 'plain-text'`.
|
|
98
|
+
- **`effect`** — native-balloon, correlation `none` (a send-time decoration, not a
|
|
99
|
+
request/response interaction). **Corrected from a draft that had `outbound: 'manual-host-op'`,
|
|
100
|
+
`status: 'unbuilt'`, citing "fork/IMCore can drive ~12 message effects via `balloonBundleID`;
|
|
101
|
+
not yet wired to Gateway."** That undercounts what's already shipped: `Gateway.sendRich`'s
|
|
102
|
+
`opts.effect` (`docs/gateway-contract.md` §1) already accepts exactly this set — 4 bubble
|
|
103
|
+
effects (`impact`, `loud`, `gentle`, `invisibleink`) + 8 screen effects (`confetti`, `lasers`,
|
|
104
|
+
`fireworks`, `balloons`, `sparkles`, `spotlight`, `echo`, `love`, `celebration`) — 12 total,
|
|
105
|
+
matching the draft's own count. The host-verification ledger states "the `sendRich`
|
|
106
|
+
text/**effect**/reply path was already live-verified." `outbound: 'agent'`, `status:
|
|
107
|
+
'host-smoked'`. This is the same mislabel `poll` had — a capability already wired through an
|
|
108
|
+
existing Gateway method, not a new unbuilt seam. `fallback: 'plain-text'`.
|
|
109
|
+
- **`rich-link-card`** — native-balloon, correlation `none` (the real `sendRichLink` returns no
|
|
110
|
+
guid — `{ ok, queued }` only). `sendRichLink` **is** wired into `Gateway`
|
|
111
|
+
(`docs/gateway-contract.md` §1, `src/gateway/types.ts`), and on builds carrying the fork's
|
|
112
|
+
`mapier/richlink-sync-fix` patch it **delivers**. The earlier 2026-07-14 host run had
|
|
113
|
+
established it as non-delivering headless (`{ok:true, queued:true}` with no `chat.db` row —
|
|
114
|
+
rich links were the only send type forced down the dylib's *deferred* `dispatch_after` branch,
|
|
115
|
+
where the RPC response was written before the deferred block fired and a throw there was
|
|
116
|
+
silent; polls build the same balloon payload but dispatch synchronously). The same-day fork
|
|
117
|
+
fix moved rich links onto the synchronous dispatch path, and the follow-up host run recorded
|
|
118
|
+
an `is_sent=1` URLBalloonProvider row with `send_status` reporting delivery (`imsg-agent`
|
|
119
|
+
`docs/parking-lot.md` 2026-07-14 and the gateway contract's host-verification ledger). Two
|
|
120
|
+
caveats keep this row conservative: the url submode is iMessage-only (hard-rejects
|
|
121
|
+
SMS-service chats), and **no capability marker gates the patch** — a binary predating
|
|
122
|
+
`richlink-sync-fix` (including the pinned `v0.1.0` release) still returns `{ok:true}` while
|
|
123
|
+
silently dropping the send. `outbound: 'none'` therefore stands until the deployment
|
|
124
|
+
branch/release provably carries the patch: flipping it would put a kind into `agentUsable()`
|
|
125
|
+
that regresses silently on a stale binary, the exact over-claim `docs/gateway-contract.md`
|
|
126
|
+
parity rule 2 exists to prevent. Fallback until then: `send()` a bare URL as plain text — it
|
|
127
|
+
delivers, and the receiver's Messages auto-unfurls it into a preview card (gated behind
|
|
128
|
+
"Tap to Load" until Mapi is a saved contact). `status: 'host-smoked'` — see "Status is not a
|
|
129
|
+
success flag" above. `fallback: 'plain-text'`.
|
|
130
|
+
- **`app-clip`** — **new tier**, `tier: 'app-clip'`, `requiresRecipientInstall: false` (App Clips
|
|
131
|
+
are ephemeral — no App Store install persists), `link-code` correlation, completion
|
|
132
|
+
out-of-band. Modeled on Linq's Agent Pay flow (cited above) — external product, cited as
|
|
133
|
+
provenance, not verified from this repo. `status: 'unbuilt'`: nothing in this SDK or its native
|
|
134
|
+
fork launches an App Clip today. `outbound: 'none'`. `fallback: 'web-url'` — a non-iPhone
|
|
135
|
+
recipient (or a stock flow before this is built) gets the existing `web-link` page instead.
|
|
136
|
+
Flagged as the catalog's highest-value unbuilt seam: richer than any `native-balloon` kind,
|
|
137
|
+
with `web-surface`'s zero install cost.
|
|
138
|
+
- **`host-extension-card`** — renamed from an earlier draft's `extension-card`. `tier:
|
|
139
|
+
'extension'`, `requiresRecipientInstall: true`, `status: 'unbuilt'`. Modeled on Photon's
|
|
140
|
+
Spectrum host-extension pattern (cited above) — external product, cited as provenance. The
|
|
141
|
+
2026-07-14 spike (`imsg-agent` `docs/parking-lot.md`, cross-repo context) already ran and
|
|
142
|
+
returned a **verdict against building a bespoke per-feature extension**: Apple's own
|
|
143
|
+
`MSMessageLiveLayout` docs confirm a live/interactive card only renders on a device with the
|
|
144
|
+
*sending* extension installed, and there is no reverse-engineering precedent anywhere
|
|
145
|
+
(BlueBubbles/mautrix/jesec/Project Zero) for injecting a third-party extension balloon via
|
|
146
|
+
private API. The host model doesn't remove the install wall — it only amortizes it across
|
|
147
|
+
every future mini-app instead of paying it per card, which is a real difference in economics
|
|
148
|
+
but not in Apple's gate. `status` stays `unbuilt`; unlike the `app-clip` seam, this one is not
|
|
149
|
+
currently prioritized. `fallback: 'web-url'`.
|
|
150
|
+
|
|
151
|
+
## Adding a kind
|
|
152
|
+
|
|
153
|
+
Mirrors `docs/gateway-contract.md` §4's contributor discipline:
|
|
154
|
+
|
|
155
|
+
1. **Real-path proof.** A manual host-Mac run (or, for `web-surface`/external-product-modeled
|
|
156
|
+
kinds, the owning repo's own verification, or cited external documentation) establishing
|
|
157
|
+
ground truth — what actually happens, not what the API promises.
|
|
158
|
+
2. **Catalog row.** Register it in `catalog.ts` with `status` reflecting where that proof landed
|
|
159
|
+
(`fake-only` if only exercised against `FakeGateway`, `host-smoked` once real-path proof
|
|
160
|
+
exists) and `outbound`/`completionTransport`/`fallback` reflecting what the proof actually
|
|
161
|
+
showed — not what the API surface merely allows.
|
|
162
|
+
3. **Conformance case.** A `tests/interactions.test.ts` case exercising the row through
|
|
163
|
+
`InteractionRegistry`/`InteractionHarness`.
|
|
164
|
+
4. **This doc.** Update the row's writeup here with the citation.
|
|
165
|
+
|
|
166
|
+
A row may enter the catalog as `status: 'unbuilt'` only as a documented seam — e.g. `app-clip`,
|
|
167
|
+
`host-extension-card` — never as a placeholder for something already claimed to work elsewhere
|
|
168
|
+
(the mistake the original `poll` and `effect` drafts made).
|
|
169
|
+
|
|
170
|
+
## Unbuilt seams: priority note
|
|
171
|
+
|
|
172
|
+
`app-clip` (zero-install, native, richer than any balloon) is the highest-value unbuilt lever in
|
|
173
|
+
this catalog. `host-extension-card` (real MSMessages host extension) stays cataloged as a
|
|
174
|
+
documented, deliberately `unbuilt` seam — the 2026-07-14 spike found the install wall real and
|
|
175
|
+
not currently worth paying for a bespoke extension — but is not prioritized the way `app-clip`
|
|
176
|
+
is.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Operating a host gateway
|
|
2
|
+
|
|
3
|
+
## Start-up
|
|
4
|
+
|
|
5
|
+
1. Build and inject the matching Mapier `imsg` CLI and bridge.
|
|
6
|
+
2. Set `IMSG_BIN` to that CLI.
|
|
7
|
+
3. Run `imsg-sdk doctor --json` and retain its output with the deployment.
|
|
8
|
+
4. Refuse bridge-backed workflows unless doctor reports the needed capability.
|
|
9
|
+
|
|
10
|
+
## Durable event consumption
|
|
11
|
+
|
|
12
|
+
Gateway row IDs are global and monotonic. Persist the highest fully handled
|
|
13
|
+
event ID only after downstream work succeeds, then pass it back to
|
|
14
|
+
`subscribe(cursor)` after a process restart. The cursor is exclusive: an event
|
|
15
|
+
with `id === cursor` is not delivered again.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
let cursor = await loadLastHandledRowId();
|
|
19
|
+
const gateway = new ImsgGateway(onUnexpectedRpcExit);
|
|
20
|
+
|
|
21
|
+
for await (const event of gateway.subscribe(cursor)) {
|
|
22
|
+
await handle(event);
|
|
23
|
+
cursor = event.id;
|
|
24
|
+
await saveLastHandledRowId(cursor);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`ImsgGateway` supervises a silent native watch session and resumes it from the
|
|
29
|
+
last observed ID. That protects a running process. A process restart still
|
|
30
|
+
requires the application's durable cursor; no SDK can infer whether the caller
|
|
31
|
+
finished handling its last event.
|
|
32
|
+
|
|
33
|
+
## Failure handling
|
|
34
|
+
|
|
35
|
+
Treat the constructor's unexpected-RPC-exit callback as a supervisor signal:
|
|
36
|
+
record the closed error context, construct a fresh gateway, and resume from the
|
|
37
|
+
persisted cursor. Do not reuse an instance whose RPC child exited.
|
|
38
|
+
|
|
39
|
+
For support reports, collect only:
|
|
40
|
+
|
|
41
|
+
- SDK version and commit;
|
|
42
|
+
- native fork commit and `imsg-sdk doctor --json` output;
|
|
43
|
+
- macOS version and architecture;
|
|
44
|
+
- the action name, result, and opaque message/chat IDs if available.
|
|
45
|
+
|
|
46
|
+
Do not put message text, attachment paths, contact data, or raw private-API
|
|
47
|
+
errors into shared logs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapier/imsg-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
},
|
|
34
34
|
"main": "./dist/index.js",
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
36
|
+
"bin": {
|
|
37
|
+
"imsg-sdk": "./dist/cli.js"
|
|
38
|
+
},
|
|
36
39
|
"exports": {
|
|
37
40
|
".": {
|
|
38
41
|
"types": "./dist/index.d.ts",
|
|
@@ -46,6 +49,10 @@
|
|
|
46
49
|
"types": "./dist/polls.d.ts",
|
|
47
50
|
"import": "./dist/polls.js"
|
|
48
51
|
},
|
|
52
|
+
"./doctor": {
|
|
53
|
+
"types": "./dist/doctor.d.ts",
|
|
54
|
+
"import": "./dist/doctor.js"
|
|
55
|
+
},
|
|
49
56
|
"./gateway/types": {
|
|
50
57
|
"types": "./dist/gateway/types.d.ts",
|
|
51
58
|
"import": "./dist/gateway/types.js"
|
|
@@ -62,6 +69,7 @@
|
|
|
62
69
|
},
|
|
63
70
|
"files": [
|
|
64
71
|
"dist",
|
|
72
|
+
"docs",
|
|
65
73
|
"scripts/*.applescript"
|
|
66
74
|
],
|
|
67
75
|
"scripts": {
|