@mapier/imsg-sdk 0.1.5 → 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.
@@ -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.1.5",
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": {