@mapier/imsg-sdk 0.5.0 → 0.6.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/extension-card.d.ts +34 -0
- package/dist/gateway/extension-card.js +182 -0
- package/dist/gateway/extension-card.js.map +1 -0
- package/dist/gateway/fake.d.ts +10 -9
- package/dist/gateway/fake.js +110 -17
- package/dist/gateway/fake.js.map +1 -1
- package/dist/gateway/imsg.d.ts +9 -9
- package/dist/gateway/imsg.js +108 -56
- package/dist/gateway/imsg.js.map +1 -1
- package/dist/gateway/message-removal.d.ts +16 -0
- package/dist/gateway/message-removal.js +94 -0
- package/dist/gateway/message-removal.js.map +1 -0
- package/dist/gateway/tapback.d.ts +16 -0
- package/dist/gateway/tapback.js +103 -0
- package/dist/gateway/tapback.js.map +1 -0
- package/dist/gateway/types.d.ts +48 -9
- package/dist/gateway/types.js +16 -0
- package/dist/gateway/types.js.map +1 -1
- package/dist/imsg/rpc.d.ts +4 -2
- package/dist/imsg/rpc.js +35 -4
- package/dist/imsg/rpc.js.map +1 -1
- package/dist/imsg/status.js +15 -0
- package/dist/imsg/status.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/interactions/catalog.js +7 -3
- package/dist/interactions/catalog.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/docs/api.md +1 -1
- package/docs/capability-matrix.md +2 -0
- package/docs/gateway-contract.md +397 -16
- package/docs/interactions.md +21 -19
- package/package.json +1 -1
package/docs/gateway-contract.md
CHANGED
|
@@ -53,6 +53,7 @@ claim about send, reaction, or group mutation behavior.
|
|
|
53
53
|
interface Gateway {
|
|
54
54
|
readonly capabilities: {
|
|
55
55
|
emojiTapback: boolean;
|
|
56
|
+
tapbackPart: boolean;
|
|
56
57
|
stickerSend: boolean;
|
|
57
58
|
groupPhoto: boolean;
|
|
58
59
|
groupParticipants: boolean;
|
|
@@ -63,6 +64,8 @@ interface Gateway {
|
|
|
63
64
|
mentionFormatting: boolean;
|
|
64
65
|
sharedLocations: boolean;
|
|
65
66
|
locationRequest: boolean;
|
|
67
|
+
extensionCardSend: boolean;
|
|
68
|
+
extensionCardUpdate: boolean;
|
|
66
69
|
};
|
|
67
70
|
subscribe(sinceId?: number): AsyncIterable<GatewayEvent>;
|
|
68
71
|
history(chatId: number, limit?: number): Promise<ImsgMessage[]>;
|
|
@@ -78,8 +81,20 @@ interface Gateway {
|
|
|
78
81
|
checkHandle(address: string, opts?: { aliasType?: 'phone' | 'email' }): Promise<HandleCheck>;
|
|
79
82
|
|
|
80
83
|
// Tier 2 (bridge-backed — imsg launch required). See "Tier 2" below.
|
|
81
|
-
tapback(
|
|
82
|
-
|
|
84
|
+
tapback(
|
|
85
|
+
chatId: number,
|
|
86
|
+
targetGuid: string,
|
|
87
|
+
reaction: Reaction,
|
|
88
|
+
remove?: boolean,
|
|
89
|
+
partIndex?: number,
|
|
90
|
+
): Promise<ReactResult>;
|
|
91
|
+
emojiTapback(
|
|
92
|
+
chatId: number,
|
|
93
|
+
targetGuid: string,
|
|
94
|
+
emoji: string,
|
|
95
|
+
remove?: boolean,
|
|
96
|
+
partIndex?: number,
|
|
97
|
+
): Promise<ReactResult>;
|
|
83
98
|
sendRich(
|
|
84
99
|
chatId: number,
|
|
85
100
|
text: string,
|
|
@@ -97,13 +112,18 @@ interface Gateway {
|
|
|
97
112
|
): Promise<SendResult>;
|
|
98
113
|
sendPoll(chatId: number, question: string, options: string[]): Promise<SendResult>;
|
|
99
114
|
sendRichLink(chatId: number, url: string): Promise<SendResult>;
|
|
115
|
+
sendExtensionCard(
|
|
116
|
+
chatId: number,
|
|
117
|
+
card: ExtensionCard,
|
|
118
|
+
opts?: { update?: ExtensionCardHandle },
|
|
119
|
+
): Promise<ExtensionCardResult>;
|
|
100
120
|
editMessage(chatId: number, targetGuid: string, text: string): Promise<{
|
|
101
121
|
ok: boolean;
|
|
102
122
|
skipped?: 'unchanged';
|
|
103
123
|
effectStarted?: boolean;
|
|
104
124
|
}>;
|
|
105
|
-
unsendMessage(chatId: number, targetGuid: string): Promise<
|
|
106
|
-
deleteMessage(chatId: number, targetGuid: string): Promise<
|
|
125
|
+
unsendMessage(chatId: number, targetGuid: string): Promise<MessageRemovalResult>;
|
|
126
|
+
deleteMessage(chatId: number, targetGuid: string): Promise<MessageRemovalResult>;
|
|
107
127
|
setTyping(chatId: number, on: boolean): Promise<{ ok: boolean }>;
|
|
108
128
|
markRead(chatId: number): Promise<{ ok: boolean }>;
|
|
109
129
|
renameGroup(chatId: number, name: string): Promise<GroupMutationResult>;
|
|
@@ -201,7 +221,50 @@ interface GroupMutationResult {
|
|
|
201
221
|
| 'refused' // proven rejection before IMChat — effectStarted:false
|
|
202
222
|
| 'fire-failed' // the call errored, may have applied — effectStarted:true
|
|
203
223
|
| 'unverified' // fired, never confirmed in the window — effectStarted:true
|
|
204
|
-
| 'verify-read-failed'
|
|
224
|
+
| 'verify-read-failed' // fired, the verification read broke — effectStarted:true
|
|
225
|
+
| 'preflight-read-failed'; // a PREflight read broke, nothing fired — effectStarted:false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// unsendMessage / deleteMessage (M2B-43). Same shape, same shared reason
|
|
229
|
+
// table: a success is bare, a failure always names where certainty was lost.
|
|
230
|
+
interface MessageRemovalResult {
|
|
231
|
+
ok: boolean;
|
|
232
|
+
effectStarted?: boolean;
|
|
233
|
+
reason?: GroupMutationFailure;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// sendExtensionCard (M2B-46). A card belonging to a third-party iMessage app
|
|
237
|
+
// extension; the balloon identifier is composed from teamId + extensionBundleId
|
|
238
|
+
// on the native side and never accepted whole.
|
|
239
|
+
interface ExtensionCard {
|
|
240
|
+
teamId: string; // exactly 10 characters of A-Z0-9
|
|
241
|
+
extensionBundleId: string; // the MESSAGES EXTENSION's id, reverse-DNS, no ':'
|
|
242
|
+
appName: string;
|
|
243
|
+
appStoreId?: number; // positive integer; without it an unmatched card leads nowhere
|
|
244
|
+
caption: string;
|
|
245
|
+
subcaption?: string;
|
|
246
|
+
summaryText?: string; // the line left behind once an update replaces this card
|
|
247
|
+
url: string; // the app's own state; any scheme, real cards use data:
|
|
248
|
+
liveLayout?: boolean; // default true — see the verb entry for what each value shows
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// The ONLY update target this SDK accepts, and the reason it is a pair rather
|
|
252
|
+
// than a guid: every update must name the session's FIRST card.
|
|
253
|
+
interface ExtensionCardHandle {
|
|
254
|
+
sessionId: string;
|
|
255
|
+
firstCardMessageGuid: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface ExtensionCardResult {
|
|
259
|
+
ok: boolean;
|
|
260
|
+
sessionId?: string;
|
|
261
|
+
messageGuid?: string; // THIS row (the card, or the update row) — not the update target
|
|
262
|
+
handle?: ExtensionCardHandle; // what a later update passes back as opts.update; present exactly when `updatable`
|
|
263
|
+
updatable?: boolean; // always present on ok:true
|
|
264
|
+
balloonBundleId?: string;
|
|
265
|
+
liveLayout?: boolean;
|
|
266
|
+
effectStarted?: boolean;
|
|
267
|
+
reason?: GroupMutationFailure; // only unsupported / refused / fire-failed are reachable
|
|
205
268
|
}
|
|
206
269
|
```
|
|
207
270
|
|
|
@@ -238,6 +301,10 @@ confirmed-mention attribute key resolves at runtime (imsg#13); emoji
|
|
|
238
301
|
additionally requires the selected RPC binary
|
|
239
302
|
to report `rpc_features:["tapback.emoji"]`. That second half prevents a
|
|
240
303
|
patched dylib behind a stock CLI from false-advertising custom emoji support.
|
|
304
|
+
Part-level tapbacks require `tapbackPart`, which the helper computes from the
|
|
305
|
+
part's own chat item plus an associated-message initializer taking a range —
|
|
306
|
+
and which no other tapback marker may stand in for, because a helper without it
|
|
307
|
+
does not reject `part_index`, it writes a corrupt row (M2B-45, above).
|
|
241
308
|
Missing status, malformed JSON, or absent markers fail closed.
|
|
242
309
|
`FakeGateway.capabilities` mirrors this gate and can disable the same surface
|
|
243
310
|
for parity tests. A remote adapter (e.g. imsg-agent's connector) must gate on
|
|
@@ -250,11 +317,17 @@ guid by scanning `history(chatId, 30)` — the same 30-message ceiling
|
|
|
250
317
|
`recentReactions` already documents above — so a guid older than that is as
|
|
251
318
|
invisible to these methods as it is to `recentReactions`. `tapback` and
|
|
252
319
|
`deleteMessage` pre-check against that scan before firing, so they return
|
|
253
|
-
`
|
|
254
|
-
|
|
320
|
+
`ok: false` without touching anything (`tapback`/`emojiTapback`/`deleteMessage`
|
|
321
|
+
say so explicitly: `effectStarted:false, reason:'refused'`), and so does
|
|
322
|
+
`editMessage`
|
|
323
|
+
(it also refuses a message that is not `is_from_me`); `unsendMessage` fires the RPC
|
|
255
324
|
first and only finds the ceiling on the VERIFY pass, so a guid outside the
|
|
256
|
-
window
|
|
257
|
-
way there — "no confirmable effect," not a
|
|
325
|
+
window reports `ok: false, effectStarted: true, reason: 'unverified'` and may
|
|
326
|
+
have mutated the real row on the way there — "no confirmable effect," not a
|
|
327
|
+
guaranteed no-op. Until M2B-43 that case returned `{ ok: true }`: the verify
|
|
328
|
+
predicate read an ABSENT row as a cleared one, so an out-of-window (or
|
|
329
|
+
merely lagging) guid reported a retraction nobody had observed. Unsend leaves
|
|
330
|
+
a tombstone row, so absence is missing evidence, never success. FakeGateway
|
|
258
331
|
must enforce the identical window regardless: finding the target guid by
|
|
259
332
|
scanning ALL of `this.messages` unconditionally would let the fake succeed
|
|
260
333
|
at targeting a message the real path can't reach (parity rule 2) — a
|
|
@@ -262,7 +335,7 @@ guid-targeted verb "working" in the fake against a message 40 rows back but
|
|
|
262
335
|
refusing on the real path is exactly the trap this contract exists to
|
|
263
336
|
prevent.
|
|
264
337
|
|
|
265
|
-
- **`tapback(chatId, targetGuid, reaction, remove?)`** — a second, bridge-based
|
|
338
|
+
- **`tapback(chatId, targetGuid, reaction, remove?, partIndex?)`** — a second, bridge-based
|
|
266
339
|
tapback path alongside `react()`. What it adds: targets **any** message guid
|
|
267
340
|
in the chat, not just the newest incoming bubble; works in **group** chats
|
|
268
341
|
(the bridge has no focused-window check to defeat, unlike the AppleScript
|
|
@@ -276,13 +349,88 @@ prevent.
|
|
|
276
349
|
deliberate: "fix: reject unsupported custom emoji reaction sends instead of
|
|
277
350
|
taking a no-op AppleScript path" (#55). So `reaction` stays the closed
|
|
278
351
|
`Reaction` type here too.
|
|
279
|
-
- **`emojiTapback(chatId, targetGuid, emoji, remove?)`** — Mapier-fork extension
|
|
352
|
+
- **`emojiTapback(chatId, targetGuid, emoji, remove?, partIndex?)`** — Mapier-fork extension
|
|
280
353
|
backed by `IMEmojiTapback` + `IMTapbackSender`. It targets any guid in the
|
|
281
354
|
same 30-message scan window as `tapback`, uses explicit add/remove guards,
|
|
282
355
|
and verifies history for `type:"custom"` plus the exact emoji before
|
|
283
356
|
returning success. The RPC sends `emoji` instead of `kind`; sending both
|
|
284
357
|
would fall back into stock imsg's classic-6 normalizer. Live-verified on the
|
|
285
358
|
host Mac with 👻 and 💀; chat.db records `associated_message_type=2006`.
|
|
359
|
+
|
|
360
|
+
**A tapback failure says which failure it is** (M2B-40). `tapback` and
|
|
361
|
+
`emojiTapback` are the only reaction path a consumer has left once
|
|
362
|
+
`reaction.apply` lost its minters (M2B-37), and they used to answer a bare
|
|
363
|
+
`ok:false` from four different positions. On `ok:false` they now carry the
|
|
364
|
+
same `reason` / `effectStarted` pair the group verbs do (`GroupMutationFailure`,
|
|
365
|
+
one shared table, so a reason cannot mean two things):
|
|
366
|
+
|
|
367
|
+
- **`unsupported`** (`effectStarted:false`) — `emojiTapback` without the
|
|
368
|
+
`emojiTapback` capability marker.
|
|
369
|
+
- **`refused`** (`effectStarted:false`) — the guid is not in the 30-message
|
|
370
|
+
scan window, the emoji is empty, or the helper rejected the call before its
|
|
371
|
+
send: a `-32602`, or a `-32603` whose `error.data` carries one of
|
|
372
|
+
`TAPBACK_PROVEN_REJECTIONS` (`gateway/tapback.ts`, read off
|
|
373
|
+
`handleSendReaction`'s returns above `[sender send]` / `[chat sendMessage:]`).
|
|
374
|
+
- **`fire-failed`** (`effectStarted:true`) — the call errored without proving
|
|
375
|
+
the helper stopped short. That deliberately includes `send-reaction failed:
|
|
376
|
+
…`: it wraps an NSException caught around the whole build-and-send block, so
|
|
377
|
+
the text cannot say which side of the send raised it.
|
|
378
|
+
- **`unverified`** (`effectStarted:true`) — fired cleanly, and six 1 s history
|
|
379
|
+
reads never showed the wanted state.
|
|
380
|
+
- **`verify-read-failed`** (`effectStarted:true`) — fired cleanly, then a
|
|
381
|
+
verification read threw. This used to escape as an exception.
|
|
382
|
+
|
|
383
|
+
`react()` is unchanged and still answers a bare `ok:false`; an absent
|
|
384
|
+
`effectStarted` must keep being treated as ambiguous.
|
|
385
|
+
|
|
386
|
+
**A tapback lands on ONE part of a message** (M2B-45). A message carrying two
|
|
387
|
+
photos has parts 0 and 1, so `partIndex: 1` reacts to the second photo alone.
|
|
388
|
+
Omitted or `0` is what both verbs always did, and the RPC for it is
|
|
389
|
+
unchanged — `part_index` is not sent at all, so a host that has never heard
|
|
390
|
+
of parts sees the same request it always did. Both verbs, add and remove,
|
|
391
|
+
classic and emoji, behave identically here.
|
|
392
|
+
|
|
393
|
+
- **Capability-gated on `tapbackPart`, and a part >= 1 without it is
|
|
394
|
+
`unsupported` (`effectStarted:false`), never a quiet part 0.** This marker
|
|
395
|
+
guards a SILENT corruption rather than a rejection, which is why nothing
|
|
396
|
+
else may stand in for it: an older helper *accepts* `part_index` and writes
|
|
397
|
+
a row whose `p:N/<guid>` prefix disagrees with its range, so the recipient's
|
|
398
|
+
phone draws the reaction on **every** photo while Messages discards the
|
|
399
|
+
sender's own row — unverifiable and unremovable. Production Macs run such a
|
|
400
|
+
helper. Downgrading to part 0 instead would react to the wrong photo and
|
|
401
|
+
report success, so the SDK refuses before building the RPC.
|
|
402
|
+
- **`partIndex` must be a non-negative integer**, checked client-side
|
|
403
|
+
(`tapbackPartRefusal`, `gateway/tapback.ts`): `1.5`, `-1` and `NaN` are
|
|
404
|
+
`refused` with nothing sent, matching the native's `invalid params`. A part
|
|
405
|
+
that cannot be read is never taken as 0.
|
|
406
|
+
- **A part the message does not have is refused before anything is sent** —
|
|
407
|
+
the helper counts the parts and returns `Message <guid> has no part <n>`,
|
|
408
|
+
which `TAPBACK_PROVEN_REJECTIONS` matches as `refused`
|
|
409
|
+
(`effectStarted:false`). FakeGateway enforces the same rule from its own
|
|
410
|
+
part count: the number of attachments, minimum 1. That model is
|
|
411
|
+
deliberately conservative — a text-plus-photos message really has more
|
|
412
|
+
parts than the fake counts, so the fake refuses parts the host would accept
|
|
413
|
+
and never the reverse (parity rule 2).
|
|
414
|
+
- **The guards and the verify compare the PART, not just the reaction.** One
|
|
415
|
+
sender can hold the same reaction type on part 0 and part 1 as two separate
|
|
416
|
+
reactions; they used to collapse into one. So reacting to photo 2 while
|
|
417
|
+
photo 1 already carries that reaction fires normally instead of answering
|
|
418
|
+
`already-reacted`, a remove on part 1 leaves part 0's alone, and the
|
|
419
|
+
verify-after-fire pass looks for the reaction on the part that was asked
|
|
420
|
+
for.
|
|
421
|
+
- **`react()` is deliberately left part-BLIND.** It drives AppleScript, which
|
|
422
|
+
has no part-level form, and its tapback is a toggle whose guard exists to
|
|
423
|
+
stop a second react from switching the first one off. Whether that toggle
|
|
424
|
+
would reach a reaction sitting on part 1 is Messages.app behaviour nobody
|
|
425
|
+
has verified on a host, so its guard keeps matching any part exactly as it
|
|
426
|
+
always did and declines instead. Only the bridge verbs compare parts.
|
|
427
|
+
- **Reading back:** a reaction that named a part carries `target_part` on the
|
|
428
|
+
`reactions[]` aggregate and `reaction_target_part` on a reaction event row;
|
|
429
|
+
`recentReactions` surfaces it as `ReactionNote.targetPart`. All three are
|
|
430
|
+
**absent** when the reaction named the whole message, so every comparison
|
|
431
|
+
goes through `reactionPart()` / `reactionOnPart()` (exported), which read
|
|
432
|
+
absent as 0. `reacted_to_guid` stays the bare message guid — the part never
|
|
433
|
+
appears in it.
|
|
286
434
|
- **`sendRich(chatId, text, opts?)`** — real and confirmed present on the
|
|
287
435
|
host's selected patched imsg 0.13.x (`send.rich`, `RPCServer+BridgeMessageHandlers.swift`
|
|
288
436
|
`handleSendRich`; shipped well before 0.12.3 per CHANGELOG). Targets an
|
|
@@ -380,6 +528,70 @@ prevent.
|
|
|
380
528
|
guid** (unlike text/poll/attachment, which return a guid) — the URL-preview
|
|
381
529
|
balloon lands as its own row later. `SendResult.guid` is therefore always
|
|
382
530
|
undefined here; FakeGateway must not return one either.
|
|
531
|
+
- **`sendExtensionCard(chatId, card, opts?)`** — `extension_card.send`
|
|
532
|
+
(`RPCServer+ExtensionCardHandler.swift`, `handleSendExtensionCard`), the kind
|
|
533
|
+
of balloon a third-party iMessage app extension sends. Targets an
|
|
534
|
+
**existing** chat only. The balloon identifier is composed on the native side
|
|
535
|
+
from `teamId` + `extensionBundleId` and is never accepted whole, so a caller
|
|
536
|
+
cannot claim an arbitrary app's balloon. Two capability markers gate it, read
|
|
537
|
+
from `imsg status --json` like every other patched verb: `extensionCardSend`
|
|
538
|
+
(the payload initializer polls also use) and `extensionCardUpdate` (that plus
|
|
539
|
+
the associated-message initializer). **Neither implies the other** — a host
|
|
540
|
+
can send cards and be unable to replace one — so a `sendExtensionCard` with
|
|
541
|
+
an `update` needs both and is `unsupported` without the second.
|
|
542
|
+
|
|
543
|
+
**What the recipient sees** (all three observed on a real phone, 2026-09-20):
|
|
544
|
+
with `liveLayout` true (the default) and the app installed, the installed
|
|
545
|
+
extension draws the card itself and the `caption` is never seen; with
|
|
546
|
+
`liveLayout:false` and the app installed, the app's icon beside *this card's*
|
|
547
|
+
caption and subcaption — the mode to use when the text is the point; without
|
|
548
|
+
the app, a small static card that links to the App Store page only when
|
|
549
|
+
`appStoreId` is given. **Cards never carry a picture.**
|
|
550
|
+
|
|
551
|
+
**The first-card rule, which is what shapes this API.** An update must always
|
|
552
|
+
name the session's **FIRST** card — the same `updates_message_guid` for every
|
|
553
|
+
update in the session, including the fifth, plus that session's id. It is not
|
|
554
|
+
a chain. Aiming an update at a *previous update's* guid does not replace
|
|
555
|
+
anything: that card arrives as a separate new card and stays in the thread as
|
|
556
|
+
an orphan, and it looks exactly like success (`ok:true`, a real guid,
|
|
557
|
+
delivered). **Nothing on the Mac can catch this**, because it cannot know
|
|
558
|
+
which guid began a session. So this verb does not take a guid at all: a first
|
|
559
|
+
send returns an `ExtensionCardHandle` as `result.handle` (`{sessionId,
|
|
560
|
+
firstCardMessageGuid}`); every update passes it back **unchanged** as
|
|
561
|
+
`opts.update` and gets the *same* handle out, never one built from its own
|
|
562
|
+
row. A caller doing `handle = result.handle` after each update therefore
|
|
563
|
+
stays correct by construction. A
|
|
564
|
+
handle assembled by hand out of an update's `messageGuid` is still wrong and
|
|
565
|
+
still cannot be detected — by the SDK or by the host — which is why
|
|
566
|
+
`ExtensionCardHandle` is documented as opaque.
|
|
567
|
+
|
|
568
|
+
**`updatable` answers the only question on a success.** `guid` is omitted by
|
|
569
|
+
the native handler when Messages has not exposed the row's guid yet, and
|
|
570
|
+
`ok:true` either way. Rather than invent one, `updatable:false` (with no
|
|
571
|
+
`update`) says plainly that this card can never be updated — send a fresh one
|
|
572
|
+
instead. On `ok:true`, `messageGuid` is *this* row: the card on a first send,
|
|
573
|
+
the associated update row on an update.
|
|
574
|
+
|
|
575
|
+
**Validation happens before the RPC**, mirroring
|
|
576
|
+
`ExtensionCardRequest.swift`: team id shape, reverse-DNS extension bundle id
|
|
577
|
+
without `':'`, required `appName`/`caption`/`url`, positive integer
|
|
578
|
+
`appStoreId`, strict boolean `liveLayout`, a 4096 **UTF-16 unit** cap on
|
|
579
|
+
every string, and both halves of an update handle present and UUID-shaped.
|
|
580
|
+
One deliberate divergence: `url` must be **absolute** here, while the helper
|
|
581
|
+
accepts anything `URL(string:)` parses — a card's URL is the extension's own
|
|
582
|
+
state (`data:` or `https:` in practice), never a relative path. Every one of
|
|
583
|
+
these is a proven no-op (`effectStarted:false`, `reason:'refused'`).
|
|
584
|
+
|
|
585
|
+
**No verification pass exists.** `imsg history` selects `payload_data` as
|
|
586
|
+
NULL for bundle ids it does not decode, so a card reads back with an empty
|
|
587
|
+
payload and there is nothing to confirm against; this verb therefore never
|
|
588
|
+
mints `unverified`, `verify-read-failed` or `preflight-read-failed`, and only
|
|
589
|
+
`unsupported` / `refused` / `fire-failed` are reachable. A caller that needs
|
|
590
|
+
delivery proof reads `sendStatus(messageGuid)`. `EXTENSION_CARD_PROVEN_REJECTIONS`
|
|
591
|
+
(`gateway/extension-card.ts`) are `handleSendExtensionCard`'s and the card
|
|
592
|
+
builder's returns above the IMChat call; `send-extension-card failed:` is
|
|
593
|
+
deliberately **not** among them, because that `@try` wraps the whole
|
|
594
|
+
build-and-send block. FakeGateway reaches only the proven half.
|
|
383
595
|
- **`editMessage(chatId, targetGuid, text)`** — `message.edit`
|
|
384
596
|
(`handleMessageEdit`; imsg `docs/edit.md`). Capability-gated on the
|
|
385
597
|
`editMessage` marker (see above): on macOS 27 the helper calls the
|
|
@@ -432,6 +644,48 @@ prevent.
|
|
|
432
644
|
and was absent in another). imsg exposes no `is_unsent`/`is_deleted` marker,
|
|
433
645
|
so after a guid pre-check, delete `{ok:true}` means only that the RPC
|
|
434
646
|
dispatched. The fake conservatively leaves its history row unchanged.
|
|
647
|
+
|
|
648
|
+
**A failure says which failure it is** (`MessageRemovalResult`, M2B-43).
|
|
649
|
+
Both verbs used to answer a bare `ok:false` whether nothing had fired or a
|
|
650
|
+
retraction had gone out unconfirmed, so a consumer had to settle every
|
|
651
|
+
failure as possibly-applied — the un-retirable rows of M2B-14. They now
|
|
652
|
+
carry the same `reason`/`effectStarted` pair through the same shared table
|
|
653
|
+
as the group verbs and tapback. The native layer contributes nothing here:
|
|
654
|
+
`message.unsend` and `message.delete` discard the bridge payload and answer
|
|
655
|
+
a hardcoded `{"ok": true}` on invocation, and neither verb verifies
|
|
656
|
+
anything, so every distinction below is either a helper refusal string or
|
|
657
|
+
one of the SDK's own read-backs.
|
|
658
|
+
|
|
659
|
+
- **`refused`** (`effectStarted:false`) — nothing fired. For `deleteMessage`,
|
|
660
|
+
the guid was not in the 30-message scan window after the pre-check poll.
|
|
661
|
+
For either verb, a `-32602`, or a `-32603` whose `error.data` carries one
|
|
662
|
+
of `UNSEND_PROVEN_REJECTIONS` / `DELETE_PROVEN_REJECTIONS`
|
|
663
|
+
(`gateway/message-removal.ts`, read off `handleUnsendMessage` /
|
|
664
|
+
`handleDeleteMessage`'s returns above the IMChat call). Note
|
|
665
|
+
`deleteChatItems:` is checked per call and advertises no `imsg status`
|
|
666
|
+
marker, so an unsupported host arrives as this refusal rather than as an
|
|
667
|
+
`unsupported` capability gate.
|
|
668
|
+
- **`preflight-read-failed`** (`effectStarted:false`) — `deleteMessage`'s
|
|
669
|
+
pre-check read threw, so the target was never looked for and nothing
|
|
670
|
+
fired. Kept apart from `refused` because it names the host's history read
|
|
671
|
+
as broken, not the caller's target. This used to escape as an exception.
|
|
672
|
+
- **`fire-failed`** (`effectStarted:true`) — the call errored without
|
|
673
|
+
proving the helper stopped short. Deliberately includes `unsend-message
|
|
674
|
+
failed` / `delete-message failed` and any bare NSException reason: the
|
|
675
|
+
helper's `@try` wraps only the selector invocation, so the text cannot say
|
|
676
|
+
which side of it raised. A bridge timeout lands here too.
|
|
677
|
+
- **`unverified`** (`effectStarted:true`) — `unsendMessage` only. It fired
|
|
678
|
+
cleanly and six 1 s history reads never showed the row with its text
|
|
679
|
+
cleared, including the out-of-window case (see the guid-targeting window
|
|
680
|
+
above, which this change also corrected).
|
|
681
|
+
- **`verify-read-failed`** (`effectStarted:true`) — `unsendMessage` only.
|
|
682
|
+
Fired cleanly, then a verification read threw. This used to escape as an
|
|
683
|
+
exception.
|
|
684
|
+
|
|
685
|
+
`deleteMessage` never mints `unverified`/`verify-read-failed`: it has no
|
|
686
|
+
postcondition to read, so once its RPC returns there is nothing left to
|
|
687
|
+
confirm. FakeGateway reaches only the proven half (`refused`) — it has no
|
|
688
|
+
bridge to time out on and no host read to lose.
|
|
435
689
|
- **`setTyping(chatId, on)`/`markRead(chatId)`** — `typing`/`read`
|
|
436
690
|
(`handleTyping`/`handleRead`). **Fire-and-forget: no observable effect
|
|
437
691
|
through anything this Gateway exposes.** `is_read`/`date_read` are real
|
|
@@ -849,6 +1103,59 @@ the phone, created that day) and group chat 12 (three phone handles):
|
|
|
849
1103
|
backgrounds) THEN failure`). That is per-endpoint pruning, not a failure
|
|
850
1104
|
of the send, and the phone's endpoints are unaffected.
|
|
851
1105
|
|
|
1106
|
+
**HOST-VERIFIED 2026-09-20 (dev login, macOS 26, imsg 0.13.0) — tapback
|
|
1107
|
+
failure reasons (M2B-40), gateway-side half.** Run from the packed branch build
|
|
1108
|
+
against a live DM: add / add-again / remove / remove-again still answer
|
|
1109
|
+
`{ok:true}`, `already-reacted`, `{ok:true}`, `not-reacted` for both verbs
|
|
1110
|
+
(1.2–1.7 s per fired call), so the success and skip shapes are unchanged by the
|
|
1111
|
+
new verify wiring; a guid absent from the 30-row window answers
|
|
1112
|
+
`{ok:false, effectStarted:false, reason:'refused'}` for add, remove and the
|
|
1113
|
+
emoji arm, and chat.db gained **no row**; an empty emoji answers the same; and a
|
|
1114
|
+
real native refusal (`rpc.tapback` on an unknown `chat_id` → `-32602`, `data:
|
|
1115
|
+
"unknown chat_id 999999"`) classifies `refused`, which also confirms
|
|
1116
|
+
`error.data` survives the rpc plumbing the classifier reads.
|
|
1117
|
+
|
|
1118
|
+
**NOT YET HOST-VERIFIED — the helper half.** The `TAPBACK_PROVEN_REJECTIONS`
|
|
1119
|
+
texts are read from `IMsgInjected.m` `handleSendReaction`, not observed: none
|
|
1120
|
+
can be provoked without a broken host. `fire-failed`, `unverified` and
|
|
1121
|
+
`verify-read-failed` are unit-tested only. The one failure seen live (dev,
|
|
1122
|
+
2026-09-18, a tapback aimed at a group-action row: `send-reaction failed:
|
|
1123
|
+
-[IMGroupActionItem expressiveSendStyleID]: unrecognized selector`) classifies
|
|
1124
|
+
`fire-failed`, which is the conservative side.
|
|
1125
|
+
|
|
1126
|
+
**HOST-VERIFIED 2026-09-20 (lab Mac, macOS 26.6.2, marker build, a real iPhone
|
|
1127
|
+
watching the thread) — tapbacks on one part (M2B-45), through these SDK verbs.**
|
|
1128
|
+
The native layer was already proven on real iPhones on macOS 26.6.2 and 27.0
|
|
1129
|
+
(Mapier-Labs/imsg `mapier/deploy` @ `b9c435f`); this run drove the gateway verbs
|
|
1130
|
+
themselves, against SDK `8743b22`:
|
|
1131
|
+
|
|
1132
|
+
- `capabilities.tapbackPart` read `true` off the marker build.
|
|
1133
|
+
- `tapback(…, 'love', false, 1)` → `{ok:true}`, and the SAME call again →
|
|
1134
|
+
`{ok:true, skipped:'already-reacted'}`. That second answer is also the
|
|
1135
|
+
readback proof: the guard can only skip if the history aggregate handed it
|
|
1136
|
+
`target_part: 1`, so `reactions[].target_part` is confirmed live, not just
|
|
1137
|
+
in the fake.
|
|
1138
|
+
- `tapback(…, 'love', false, 0)` then SENT rather than skipping, although part 1
|
|
1139
|
+
already carried `love` — the two do not collapse into one reaction.
|
|
1140
|
+
- `emojiTapback(…, '🔥', false, 1)` → `{ok:true}`: the emoji arm carries a part
|
|
1141
|
+
the same way.
|
|
1142
|
+
- `tapback(…, 'love', true, 0)` → `{ok:true}`, removing part 0's and leaving
|
|
1143
|
+
part 1's alone.
|
|
1144
|
+
- `partIndex: 5` on a message without it → `{ok:false, effectStarted:false,
|
|
1145
|
+
reason:'refused'}`. So `'has no part '` in `TAPBACK_PROVEN_REJECTIONS` is an
|
|
1146
|
+
OBSERVED classification, unlike its neighbours, which remain read-from-source.
|
|
1147
|
+
- `partIndex: 1.5` → refused client-side, nothing sent.
|
|
1148
|
+
- The chat.db rows carry a `p:N/` prefix and a range that AGREE — the shape an
|
|
1149
|
+
iPhone's own per-photo tapback writes, and the one whose disagreement is what
|
|
1150
|
+
the `tapbackPart` marker exists to prevent.
|
|
1151
|
+
|
|
1152
|
+
**Still NOT host-verified for M2B-45:** the capability-OFF arm (`partIndex >= 1`
|
|
1153
|
+
answering `unsupported`), which cannot be provoked without running a
|
|
1154
|
+
pre-marker helper; removing an EMOJI tapback from a part; and the ambiguous
|
|
1155
|
+
failure arms (`fire-failed`, `unverified`, `verify-read-failed`) reached with a
|
|
1156
|
+
part, which are unit-tested only for the same reason the M2B-40 note below
|
|
1157
|
+
gives.
|
|
1158
|
+
|
|
852
1159
|
**NOT YET HOST-VERIFIED — `GroupMutationResult` (M2B-40).** The reason
|
|
853
1160
|
vocabulary, the three-member floor and the DM preflight are derived from the
|
|
854
1161
|
native sources (`IMsgInjected.m` `handleAddParticipant` /
|
|
@@ -865,6 +1172,43 @@ removal; (3) do the add/remove/rename round trips still return the bare
|
|
|
865
1172
|
prints the measured membership and asserts the floor refusal, so a single run
|
|
866
1173
|
settles all three.
|
|
867
1174
|
|
|
1175
|
+
**NOT YET HOST-VERIFIED — `MessageRemovalResult` (M2B-43).** The
|
|
1176
|
+
`UNSEND_PROVEN_REJECTIONS` / `DELETE_PROVEN_REJECTIONS` texts are read from
|
|
1177
|
+
`IMsgInjected.m` `handleUnsendMessage` / `handleDeleteMessage`, not observed;
|
|
1178
|
+
none can be provoked without a broken host. `fire-failed`, `unverified`,
|
|
1179
|
+
`verify-read-failed` and `preflight-read-failed` are unit-tested only
|
|
1180
|
+
(`tests/message-removal-failure.test.ts`), which under hard rule 1 is **not**
|
|
1181
|
+
proof. One run of `scripts/tier2-smoke.ts` on the host settles the two
|
|
1182
|
+
questions that matter: (1) do the unsend/delete round trips still answer a bare
|
|
1183
|
+
`{ok:true}` with the classification wiring in front of them — the smoke already
|
|
1184
|
+
prints the whole result object on failure, so a stray `reason` on a success
|
|
1185
|
+
shows up; and (2) does an out-of-window unsend now answer `unverified` where it
|
|
1186
|
+
used to answer `{ok:true}`. That second one is a **behavior change, not just a
|
|
1187
|
+
new field**: the old verify predicate (`!found?.text`) read an absent row as a
|
|
1188
|
+
cleared one, so an unsend whose guid was outside the 30-row window — or merely
|
|
1189
|
+
lagging history — reported a success nobody had observed. FakeGateway already
|
|
1190
|
+
refused that input, so the fake and the real gateway disagreed on it until now.
|
|
1191
|
+
|
|
1192
|
+
**NOT YET HOST-VERIFIED — `sendExtensionCard` (M2B-46).** The native half is
|
|
1193
|
+
proven: on 2026-09-20 a Mac-injected card for a non-Apple iMessage extension
|
|
1194
|
+
sent, delivered, drew, opened the installed app in-thread, and updated in place
|
|
1195
|
+
on real phones (macOS 26.6.2 and 27.0) — that run is where the first-card rule
|
|
1196
|
+
comes from. **The SDK verb has not run against a host Mac.** Everything in it
|
|
1197
|
+
is derived from the native sources (`RPCServer+ExtensionCardHandler.swift`,
|
|
1198
|
+
`ExtensionCardRequest.swift`, `IMsgExtensionCard.m`, `IMsgInjected.m`
|
|
1199
|
+
`handleSendExtensionCard`) and covered by unit tests and the fake
|
|
1200
|
+
(`tests/extension-card.test.ts`), which under hard rule 1 is **not** proof, and
|
|
1201
|
+
under rule 2's §4 contributor rule this stays forbidden surface until the host
|
|
1202
|
+
run lands. One run settles the three questions that matter: (1) does a first
|
|
1203
|
+
send actually come back carrying a `guid` — the whole `updatable` distinction
|
|
1204
|
+
exists because the handler omits it when Messages has not exposed one, and how
|
|
1205
|
+
often that happens is unmeasured; (2) does an update driven by the *returned*
|
|
1206
|
+
handle restamp the first card rather than adding a row, i.e. does the handle
|
|
1207
|
+
round trip survive the SDK's UUID validation of real Messages guids; and (3) do
|
|
1208
|
+
both markers read `true` on the current build — a host with `extensionCardSend`
|
|
1209
|
+
and not `extensionCardUpdate` is possible by construction but has never been
|
|
1210
|
+
seen. There is no smoke leg for this verb yet; the host proof is driven by hand.
|
|
1211
|
+
|
|
868
1212
|
- **subscribe(sinceId?)** — long-lived event stream of new messages/reactions across all
|
|
869
1213
|
chats the host Mac's Messages account can see. `sinceId` is an **exclusive** cursor: only
|
|
870
1214
|
events with `id > sinceId` are delivered (catch-up semantics, not "starting at"). Today
|
|
@@ -946,7 +1290,9 @@ fail closed (covered by `tests/imsg-rpc.test.ts`).
|
|
|
946
1290
|
recent 30 messages are scanned (the real path reads `history(chatId, 30)`) — a tapback on
|
|
947
1291
|
an older message is invisible, and FakeGateway enforces the same window so features can't
|
|
948
1292
|
be built against reach the real gateway doesn't have. Feeds "[reacted X to your message]"
|
|
949
|
-
acknowledgment notes into agent context.
|
|
1293
|
+
acknowledgment notes into agent context. A note carries `targetPart` when they reacted to one
|
|
1294
|
+
part of our message (M2B-45) — 1 is the second photo of two — and carries no such key at all
|
|
1295
|
+
when they reacted to the whole message.
|
|
950
1296
|
- **resolveDmChat(handle)** — the numeric `chat_id` of the existing DM thread with `handle`, or
|
|
951
1297
|
`null` if none exists. **Read-only** — it must never create a thread (the real path reads
|
|
952
1298
|
`chat.db` via `chats.list` and cannot mint one; FakeGateway may not exceed that). The runtime
|
|
@@ -1010,7 +1356,11 @@ a different product.
|
|
|
1010
1356
|
(2026-07-02) and imsg 0.12.3 (2026-07-07): `messages.history` returns no `is_reaction` rows
|
|
1011
1357
|
(they'd duplicate the reacted
|
|
1012
1358
|
message), but every message carries a `reactions[]` aggregate — the current tapback state on
|
|
1013
|
-
that bubble: `{ id, type, emoji?, is_from_me, sender?, created_at? }` per reaction.
|
|
1359
|
+
that bubble: `{ id, type, emoji?, is_from_me, sender?, created_at?, target_part? }` per reaction.
|
|
1360
|
+
`target_part` (M2B-45) is which PART of the message the reaction named, and is absent when it
|
|
1361
|
+
named the whole message — read it through `reactionPart()`, never directly, or "loved the
|
|
1362
|
+
message" and "loved photo 1" look like different targets. One sender can hold the same
|
|
1363
|
+
reaction type on two parts, and that is two entries, not one.
|
|
1014
1364
|
Consequence: reaction-state logic (toggle guard, `recentReactions`) reads history — it is
|
|
1015
1365
|
authoritative and restart-safe. The subscribe stream (`include_reactions: true`) still
|
|
1016
1366
|
delivers reaction EVENTS for realtime wake-ups, but is never the source of state: a process
|
|
@@ -1213,6 +1563,19 @@ a different product.
|
|
|
1213
1563
|
messages, reacting to an arbitrary (non-most-recent) message, and participant management
|
|
1214
1564
|
on an existing group (add/remove/rename/photo/leave).
|
|
1215
1565
|
|
|
1566
|
+
- **An extension-card update aimed at the wrong card cannot be detected, and
|
|
1567
|
+
FakeGateway must not detect it either** (M2B-46). Every update in a session
|
|
1568
|
+
has to name that session's FIRST card; pointing one at a previous update's
|
|
1569
|
+
guid produces a separate card that is never replaced, with `ok:true`, a real
|
|
1570
|
+
guid and a delivery. The Mac has no way to catch it — it does not know which
|
|
1571
|
+
guid began a session — so a fake that rejected a hand-built handle would test
|
|
1572
|
+
consumers against a safety net that does not exist on the host. The
|
|
1573
|
+
protection lives in the API shape instead: `sendExtensionCard` takes only an
|
|
1574
|
+
`ExtensionCardHandle` it minted, and returns that same handle unchanged from
|
|
1575
|
+
every update, so carrying `result.handle` forward stays correct. Both
|
|
1576
|
+
implementations also refuse to invent a guid: when Messages exposes none, the
|
|
1577
|
+
success carries `updatable:false` and no handle rather than a synthesized one.
|
|
1578
|
+
|
|
1216
1579
|
- **Inbound media.** Real messages can carry `attachments[]` with empty `text`. FakeGateway must
|
|
1217
1580
|
be able to emit the exact attachment metadata with `text: undefined`/empty so
|
|
1218
1581
|
this path gets exercised — the agent currently ignores attachments gracefully
|
|
@@ -1295,7 +1658,9 @@ harness (`tsx --test`, see `package.json`). Required cases:
|
|
|
1295
1658
|
`ok:false` without mutation
|
|
1296
1659
|
- `tapback`/`editMessage`/`unsendMessage`/`deleteMessage` all return `ok: false` — without
|
|
1297
1660
|
mutating anything — for a guid outside the 30-message scan window (the guid-targeting
|
|
1298
|
-
window above), even though the message still exists in the fake's full store
|
|
1661
|
+
window above), even though the message still exists in the fake's full store, and all
|
|
1662
|
+
four say `effectStarted:false` because the fake never fires (the real
|
|
1663
|
+
`unsendMessage` answers `unverified` for that input: it fires first)
|
|
1299
1664
|
- `sendRich` sends to an existing chat with an inline reply target, and returns `ok: false`
|
|
1300
1665
|
for a chat that doesn't exist (no find-or-create, unlike `send`)
|
|
1301
1666
|
- `sendRich` accepts a subject while preserving the observable text echo (subject is
|
|
@@ -1321,8 +1686,16 @@ harness (`tsx --test`, see `package.json`). Required cases:
|
|
|
1321
1686
|
mutation when the `editMessage` capability marker is absent
|
|
1322
1687
|
- `unsendMessage` retracts the target row (text clears, row persists);
|
|
1323
1688
|
`deleteMessage` is dispatch-only with no stable history postcondition (the
|
|
1324
|
-
fake conservatively preserves the row); both
|
|
1325
|
-
|
|
1689
|
+
fake conservatively preserves the row); both name an unknown guid as
|
|
1690
|
+
`ok:false, effectStarted:false, reason:'refused'`
|
|
1691
|
+
- (real-path only, `tests/message-removal-failure.test.ts` over the pure
|
|
1692
|
+
module) for both removal verbs a bridge rejection raised before IMChat is
|
|
1693
|
+
`refused` and a timeout, an unmodelled `-32603` or a non-rpc error is
|
|
1694
|
+
`fire-failed`, with one verb's proven-rejection text not excusing another's;
|
|
1695
|
+
`deleteMessage`'s pre-check names an exhausted poll `refused` and a thrown
|
|
1696
|
+
read `preflight-read-failed`; `unsendMessage`'s read-back names an exhausted
|
|
1697
|
+
verify window `unverified` — including a guid it never sees, which must not
|
|
1698
|
+
be mistaken for a cleared row — and a thrown read `verify-read-failed`
|
|
1326
1699
|
- `setTyping`/`markRead` dispatch `{ ok: true }` against an existing chat and `{ ok: false }`
|
|
1327
1700
|
against an unknown one, with no queryable state either way
|
|
1328
1701
|
- `renameGroup`/`addParticipant`/`removeParticipant` are reflected in a message sent
|
|
@@ -1356,6 +1729,14 @@ harness (`tsx --test`, see `package.json`). Required cases:
|
|
|
1356
1729
|
- `sendLocationRequest` posts the card as its own outbound row to an existing chat, returns
|
|
1357
1730
|
the balloon guid, refuses an unknown chat (no find-or-create), and fails closed without
|
|
1358
1731
|
the `locationRequest` marker (no row written)
|
|
1732
|
+
- `sendExtensionCard` posts the card as its own outbound row to an existing chat, composes
|
|
1733
|
+
the balloon id from the team and extension ids, hands back a `handle` whose
|
|
1734
|
+
`firstCardMessageGuid` is that row, and keeps returning that SAME handle through three
|
|
1735
|
+
chained updates (each its own row); bad input and an unknown chat are `refused` with no
|
|
1736
|
+
row written, a missing `extensionCardSend` marker is `unsupported`, and a host with
|
|
1737
|
+
`extensionCardSend` but not `extensionCardUpdate` sends but refuses the update. One case
|
|
1738
|
+
asserts a NEGATIVE: an update aimed at a previous update's guid is **not** caught — the
|
|
1739
|
+
fake must not be safer than the Mac, which cannot detect it either
|
|
1359
1740
|
- Fake-only restart coverage snapshots and restores a world, then proves
|
|
1360
1741
|
`sendStatus`, `checkHandle`, Name & Photo idempotency, chat backgrounds (and their
|
|
1361
1742
|
guid sequence), Find My shares, current group metadata, fixture state, and monotonic IDs
|
package/docs/interactions.md
CHANGED
|
@@ -136,17 +136,20 @@ corrections changed which kinds `agentUsable()` returns.
|
|
|
136
136
|
Flagged as the catalog's highest-value unbuilt seam: richer than any `native-balloon` kind,
|
|
137
137
|
with `web-surface`'s zero install cost.
|
|
138
138
|
- **`host-extension-card`** — renamed from an earlier draft's `extension-card`. `tier:
|
|
139
|
-
'extension'`, `requiresRecipientInstall: true`, `
|
|
140
|
-
Spectrum host-extension pattern (cited above) — external product, cited as
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
'extension'`, `requiresRecipientInstall: true`, `outbound: 'agent'`, `status: 'fake-only'`.
|
|
140
|
+
Modeled on Photon's Spectrum host-extension pattern (cited above) — external product, cited as
|
|
141
|
+
provenance. **No longer an unbuilt seam** (M2B-46): `Gateway.sendExtensionCard` sends a card
|
|
142
|
+
for a third-party iMessage app extension and updates it in place, which is exactly the
|
|
143
|
+
injection the 2026-07-14 spike found no precedent for. That spike's verdict is **overturned on
|
|
144
|
+
the mechanism, upheld on the wall**: a Mac-injected card for a non-Apple extension was observed
|
|
145
|
+
on real phones on 2026-09-20 sending, delivering, drawing, and opening the installed app
|
|
146
|
+
in-thread — but Apple's gate is exactly where `MSMessageLiveLayout` said it was. With the app
|
|
147
|
+
installed the extension draws the card itself; with `liveLayout:false` the recipient sees the
|
|
148
|
+
app's icon beside *our* caption; without the app they get a small static card (App Store link
|
|
149
|
+
only when `appStoreId` is supplied). So the host model still amortizes the install across every
|
|
150
|
+
future mini-app rather than removing it. `status` is `fake-only`, not `host-smoked`: the SDK
|
|
151
|
+
verb has only been exercised against `FakeGateway` and its unit tests — the host run is
|
|
152
|
+
pending, which is why this row stays out of `agentUsable()`. `fallback: 'web-url'`.
|
|
150
153
|
|
|
151
154
|
## Adding a kind
|
|
152
155
|
|
|
@@ -163,14 +166,13 @@ Mirrors `docs/gateway-contract.md` §4's contributor discipline:
|
|
|
163
166
|
`InteractionRegistry`/`InteractionHarness`.
|
|
164
167
|
4. **This doc.** Update the row's writeup here with the citation.
|
|
165
168
|
|
|
166
|
-
A row may enter the catalog as `status: 'unbuilt'` only as a documented seam — e.g. `app-clip
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
A row may enter the catalog as `status: 'unbuilt'` only as a documented seam — e.g. `app-clip` —
|
|
170
|
+
never as a placeholder for something already claimed to work elsewhere (the mistake the original
|
|
171
|
+
`poll` and `effect` drafts made).
|
|
169
172
|
|
|
170
173
|
## Unbuilt seams: priority note
|
|
171
174
|
|
|
172
|
-
`app-clip` (zero-install, native, richer than any balloon) is the
|
|
173
|
-
this catalog. `host-extension-card`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
is.
|
|
175
|
+
`app-clip` (zero-install, native, richer than any balloon) is now the only unbuilt row left, and
|
|
176
|
+
the highest-value lever in this catalog. `host-extension-card` left this list in M2B-46: the
|
|
177
|
+
mechanism is built (`Gateway.sendExtensionCard`) and climbing the ladder — `fake-only` until the
|
|
178
|
+
host run — rather than deliberately unbuilt.
|
package/package.json
CHANGED