@mapier/imsg-sdk 0.2.2 → 0.3.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.
- package/dist/gateway/chat-background.d.ts +40 -0
- package/dist/gateway/chat-background.js +219 -0
- package/dist/gateway/chat-background.js.map +1 -0
- package/dist/gateway/edit-message.d.ts +17 -0
- package/dist/gateway/edit-message.js +70 -0
- package/dist/gateway/edit-message.js.map +1 -0
- package/dist/gateway/external-ids.d.ts +1 -0
- package/dist/gateway/external-ids.js +3 -1
- package/dist/gateway/external-ids.js.map +1 -1
- package/dist/gateway/fake.d.ts +13 -4
- package/dist/gateway/fake.js +82 -9
- package/dist/gateway/fake.js.map +1 -1
- package/dist/gateway/imsg.d.ts +6 -4
- package/dist/gateway/imsg.js +98 -33
- package/dist/gateway/imsg.js.map +1 -1
- package/dist/gateway/send-rich.d.ts +17 -0
- package/dist/gateway/send-rich.js +43 -0
- package/dist/gateway/send-rich.js.map +1 -0
- package/dist/gateway/types.d.ts +77 -5
- package/dist/gateway/types.js +165 -0
- package/dist/gateway/types.js.map +1 -1
- package/dist/imsg/rpc.d.ts +33 -2
- package/dist/imsg/rpc.js +31 -3
- package/dist/imsg/rpc.js.map +1 -1
- package/dist/imsg/status.js +17 -0
- package/dist/imsg/status.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/docs/api.md +1 -0
- package/docs/bridge-contract.md +7 -3
- package/docs/capability-matrix.md +4 -1
- package/docs/compatibility.md +4 -1
- package/docs/gateway-contract.md +271 -13
- package/docs/imcore_selectors.md +634 -0
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChatBackgroundStatusResponse } from '../imsg/rpc.js';
|
|
2
|
+
import type { ChatBackgroundRemoveResult, ChatBackgroundSetResult, ChatBackgroundStatus } from './types.js';
|
|
3
|
+
export declare const CHAT_BACKGROUND_SET_DEADLINE_MS = 90000;
|
|
4
|
+
export declare const CHAT_BACKGROUND_REMOVE_DEADLINE_MS = 30000;
|
|
5
|
+
export declare const CHAT_BACKGROUND_POLL_INTERVAL_MS = 1000;
|
|
6
|
+
export declare const CHAT_BACKGROUND_HELPER_INTERVAL_MS = 15000;
|
|
7
|
+
export declare const CHAT_BACKGROUND_HELPER_BACKOFF_CAP_MS = 60000;
|
|
8
|
+
export declare function isBridgeTimeout(err: unknown): boolean;
|
|
9
|
+
export declare function chatBackgroundEffectStarted(err: unknown): boolean;
|
|
10
|
+
export declare function bridgeEffectStarted(err: unknown, provenRejections: readonly string[]): boolean;
|
|
11
|
+
export declare function chatBackgroundEventBaseline(status: ChatBackgroundStatusResponse): number;
|
|
12
|
+
export declare function didPersistRemove(current: ChatBackgroundStatusResponse, initialGuid: string | null, baseline: number): boolean;
|
|
13
|
+
export declare function toChatBackgroundStatus(status: ChatBackgroundStatusResponse): ChatBackgroundStatus;
|
|
14
|
+
export interface ChatBackgroundVerifyClock {
|
|
15
|
+
now(): number;
|
|
16
|
+
sleep(ms: number): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare const MONOTONIC_CLOCK: ChatBackgroundVerifyClock;
|
|
19
|
+
export interface ChatBackgroundSetVerifyInput {
|
|
20
|
+
read(operationId?: string): Promise<ChatBackgroundStatusResponse>;
|
|
21
|
+
initialGuid: string | null;
|
|
22
|
+
operationId: string | undefined;
|
|
23
|
+
clock: ChatBackgroundVerifyClock;
|
|
24
|
+
log: (message: string) => void;
|
|
25
|
+
deadlineMs?: number;
|
|
26
|
+
intervalMs?: number;
|
|
27
|
+
helperIntervalMs?: number;
|
|
28
|
+
helperBackoffCapMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function verifyChatBackgroundSet(input: ChatBackgroundSetVerifyInput): Promise<ChatBackgroundSetResult>;
|
|
31
|
+
export interface ChatBackgroundRemoveVerifyInput {
|
|
32
|
+
read(): Promise<ChatBackgroundStatusResponse>;
|
|
33
|
+
initial: ChatBackgroundStatusResponse;
|
|
34
|
+
accepted: ChatBackgroundStatusResponse;
|
|
35
|
+
clock: ChatBackgroundVerifyClock;
|
|
36
|
+
log: (message: string) => void;
|
|
37
|
+
deadlineMs?: number;
|
|
38
|
+
intervalMs?: number;
|
|
39
|
+
}
|
|
40
|
+
export declare function verifyChatBackgroundRemove(input: ChatBackgroundRemoveVerifyInput): Promise<ChatBackgroundRemoveResult>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Chat-background verify-after-fire logic (Mapier-Labs/imsg#12), kept pure so
|
|
2
|
+
// it is unit-testable with mocked reads and a virtual clock. ImsgGateway wires
|
|
3
|
+
// it to the live `imsg rpc` child; nothing here touches a process.
|
|
4
|
+
//
|
|
5
|
+
// Native facts this encodes (imsg docs/chat-background.md,
|
|
6
|
+
// ChatBackgroundMutation.swift, RPCServer+Support.swift):
|
|
7
|
+
// - `chat.background.set`/`remove` return on helper acceptance; chat.db lags
|
|
8
|
+
// imagent, so persistence is proven by polling `chat.background.status`.
|
|
9
|
+
// - A status read WITHOUT `operation_id` is a local chat.db read. WITH it the
|
|
10
|
+
// native handler also round-trips the helper over the bridge: a Messages
|
|
11
|
+
// main-thread hop that competes with the pipeline itself and, because the
|
|
12
|
+
// native JSON-RPC loop is strictly serial, holds every other gateway verb
|
|
13
|
+
// for up to 10 s on a stall. Helper reads are therefore rare and backed off.
|
|
14
|
+
// - The helper record's `transfer_id` is what IMChat persists as the
|
|
15
|
+
// background guid. It is the ONLY evidence that correlates a chat.db guid
|
|
16
|
+
// with THIS operation; a lagging write from an earlier operation can satisfy
|
|
17
|
+
// every other signal (changed guid, newer set event).
|
|
18
|
+
// - Every failure is JSON-RPC -32603 with the reason only in `error.data`.
|
|
19
|
+
import { ImsgRpcError } from '../imsg/rpc.js';
|
|
20
|
+
// The native CLI's own windows (90 polls / 30 polls at 1 s). Here they are
|
|
21
|
+
// wall-clock deadlines, so a stalled bridge hop cannot stretch the wait.
|
|
22
|
+
export const CHAT_BACKGROUND_SET_DEADLINE_MS = 90_000;
|
|
23
|
+
export const CHAT_BACKGROUND_REMOVE_DEADLINE_MS = 30_000;
|
|
24
|
+
export const CHAT_BACKGROUND_POLL_INTERVAL_MS = 1000;
|
|
25
|
+
// Scheduled helper reads (early failure detection) at most this often; a
|
|
26
|
+
// helper timeout doubles the gap up to the cap.
|
|
27
|
+
export const CHAT_BACKGROUND_HELPER_INTERVAL_MS = 15_000;
|
|
28
|
+
export const CHAT_BACKGROUND_HELPER_BACKOFF_CAP_MS = 60_000;
|
|
29
|
+
// The helper's pre-queue refusals (IMsgChatBackground.m /
|
|
30
|
+
// IMsgInjected.m handleRemoveChatBackground) plus the coordinator's own
|
|
31
|
+
// "did not queue". Anything else under -32603 (bridge timeout, a dylib
|
|
32
|
+
// exception raised after IMChat was invoked, ...) may have acted.
|
|
33
|
+
const CHAT_BACKGROUND_PROVEN_REJECTIONS = [
|
|
34
|
+
'did not queue the chat background operation',
|
|
35
|
+
'Another chat background operation is already running',
|
|
36
|
+
'does not match expected GUID',
|
|
37
|
+
'Chat background selector unavailable',
|
|
38
|
+
'Chat background GUID selector unavailable',
|
|
39
|
+
'Chat background runtime unavailable',
|
|
40
|
+
'Chat is required',
|
|
41
|
+
];
|
|
42
|
+
// IMsgBridgeProtocol.swift `IMsgBridgeError.timeout`.
|
|
43
|
+
const BRIDGE_TIMEOUT_DETAIL = 'Timed out waiting for response';
|
|
44
|
+
export function isBridgeTimeout(err) {
|
|
45
|
+
return err instanceof ImsgRpcError && (err.data ?? '').includes(BRIDGE_TIMEOUT_DETAIL);
|
|
46
|
+
}
|
|
47
|
+
// Did the helper possibly act before the request failed? Proven "no": the
|
|
48
|
+
// server rejected the params (-32602: unknown chat, bad preset) or the helper
|
|
49
|
+
// refused before queueing (a known rejection reason in `data`). Everything
|
|
50
|
+
// else — a bridge timeout, an exception after IMChat was invoked, a
|
|
51
|
+
// non-structured rpc-child/transport failure — is ambiguous.
|
|
52
|
+
export function chatBackgroundEffectStarted(err) {
|
|
53
|
+
return bridgeEffectStarted(err, CHAT_BACKGROUND_PROVEN_REJECTIONS);
|
|
54
|
+
}
|
|
55
|
+
// The same rule for any bridge verb whose server folds every failure into
|
|
56
|
+
// -32603: `provenRejections` are the helper's pre-invoke refusal texts for
|
|
57
|
+
// that verb (matched against `error.data`).
|
|
58
|
+
export function bridgeEffectStarted(err, provenRejections) {
|
|
59
|
+
if (!(err instanceof ImsgRpcError))
|
|
60
|
+
return true;
|
|
61
|
+
if (err.code === -32602)
|
|
62
|
+
return false;
|
|
63
|
+
const detail = err.data ?? '';
|
|
64
|
+
return !provenRejections.some((reason) => detail.includes(reason));
|
|
65
|
+
}
|
|
66
|
+
// Highest known background-event ROWID before a mutation. Both the
|
|
67
|
+
// date-ordered and the ROWID-ordered newest event feed in, so a row imagent
|
|
68
|
+
// writes with an out-of-order date is still recognised as new.
|
|
69
|
+
export function chatBackgroundEventBaseline(status) {
|
|
70
|
+
return Math.max(status.latest_event?.row_id ?? 0, status.newest_event?.row_id ?? 0);
|
|
71
|
+
}
|
|
72
|
+
function hasNewChatBackgroundEvent(status, action, baseline) {
|
|
73
|
+
for (const event of [status.latest_event, status.newest_event]) {
|
|
74
|
+
if (event && event.action === action && event.row_id > baseline)
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// Native didPersistRemove: no guid, plus either a clear event past the
|
|
80
|
+
// baseline, or — only when the pre-mutation snapshot recorded the background
|
|
81
|
+
// that is now gone — an events table that is empty. An already-lagging empty
|
|
82
|
+
// snapshot proves nothing: the clear event has to land first.
|
|
83
|
+
export function didPersistRemove(current, initialGuid, baseline) {
|
|
84
|
+
if (current.background_channel_guid)
|
|
85
|
+
return false;
|
|
86
|
+
if (hasNewChatBackgroundEvent(current, 'clear', baseline))
|
|
87
|
+
return true;
|
|
88
|
+
return initialGuid !== null && !current.latest_event && !current.newest_event;
|
|
89
|
+
}
|
|
90
|
+
export function toChatBackgroundStatus(status) {
|
|
91
|
+
const guid = status.background_channel_guid;
|
|
92
|
+
return {
|
|
93
|
+
chatId: status.chat_id,
|
|
94
|
+
backgroundSet: status.background_set,
|
|
95
|
+
backgroundGuid: guid ? guid : null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// The production clock. `performance.now()` is monotonic: a wall-clock
|
|
99
|
+
// adjustment (NTP step, manual change) cannot move a deadline, whereas a
|
|
100
|
+
// `Date.now()`-based deadline would stretch the wait by the size of a
|
|
101
|
+
// backwards step — a 10-minute rollback turned a 90 s window into ~11 min.
|
|
102
|
+
export const MONOTONIC_CLOCK = {
|
|
103
|
+
now: () => performance.now(),
|
|
104
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
105
|
+
};
|
|
106
|
+
// Polls until chat.db shows a guid that the helper record correlates with
|
|
107
|
+
// THIS operation, the helper reports failure, the record is gone, a
|
|
108
|
+
// non-transient read error occurs, or the deadline passes. Only the
|
|
109
|
+
// `transfer_id` match is success; every other exit is either a proven failure
|
|
110
|
+
// with the effect started or ambiguity — never a guess.
|
|
111
|
+
export async function verifyChatBackgroundSet(input) {
|
|
112
|
+
const { read, initialGuid, operationId, clock, log, deadlineMs = CHAT_BACKGROUND_SET_DEADLINE_MS, intervalMs = CHAT_BACKGROUND_POLL_INTERVAL_MS, helperIntervalMs = CHAT_BACKGROUND_HELPER_INTERVAL_MS, helperBackoffCapMs = CHAT_BACKGROUND_HELPER_BACKOFF_CAP_MS, } = input;
|
|
113
|
+
const startedAt = clock.now();
|
|
114
|
+
const deadline = startedAt + deadlineMs;
|
|
115
|
+
// A candidate is a chat.db guid that differs from the pre-mutation
|
|
116
|
+
// snapshot. Seeing a NEW one pulls the next helper read forward once
|
|
117
|
+
// (`pulled`), so it is confirmed at the next opportunity; after that only
|
|
118
|
+
// the scheduled cadence and the timeout backoff decide when the helper is
|
|
119
|
+
// read again. `rejected` is the last candidate the record disproved.
|
|
120
|
+
let pulled = null;
|
|
121
|
+
let rejected = null;
|
|
122
|
+
let helperGapMs = helperIntervalMs;
|
|
123
|
+
let nextHelperAt = startedAt + helperIntervalMs;
|
|
124
|
+
let loggedTimeout = false;
|
|
125
|
+
for (;;) {
|
|
126
|
+
const consultHelper = operationId !== undefined && clock.now() >= nextHelperAt;
|
|
127
|
+
let current;
|
|
128
|
+
try {
|
|
129
|
+
current = await read(consultHelper ? operationId : undefined);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// A bridge hop timing out while the pipeline holds the main thread is
|
|
133
|
+
// not evidence either way: back the helper off and keep polling chat.db.
|
|
134
|
+
// Anything else (dead rpc child, transport loss) ends the wait.
|
|
135
|
+
if (!isBridgeTimeout(err)) {
|
|
136
|
+
log(`setChatBackground verification failed: ${String(err)}`);
|
|
137
|
+
return { ok: false, effectStarted: true };
|
|
138
|
+
}
|
|
139
|
+
if (!loggedTimeout) {
|
|
140
|
+
loggedTimeout = true;
|
|
141
|
+
log(`setChatBackground verification read timed out: ${String(err)}`);
|
|
142
|
+
}
|
|
143
|
+
if (consultHelper) {
|
|
144
|
+
helperGapMs = Math.min(helperGapMs * 2, helperBackoffCapMs);
|
|
145
|
+
nextHelperAt = clock.now() + helperGapMs;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (current) {
|
|
149
|
+
if (consultHelper)
|
|
150
|
+
nextHelperAt = clock.now() + helperGapMs;
|
|
151
|
+
const guid = current.background_channel_guid;
|
|
152
|
+
const candidate = guid && guid !== initialGuid ? guid : null;
|
|
153
|
+
const operation = current.operation;
|
|
154
|
+
if (operation?.found) {
|
|
155
|
+
if (operation.state === 'failed') {
|
|
156
|
+
log(`setChatBackground operation failed: ${operation.stage ?? 'unknown'}: ${operation.detail ?? 'unknown error'}`);
|
|
157
|
+
return { ok: false, effectStarted: true };
|
|
158
|
+
}
|
|
159
|
+
if (candidate) {
|
|
160
|
+
if (operation.transfer_id && candidate === operation.transfer_id) {
|
|
161
|
+
return { ok: true, backgroundGuid: candidate, effectStarted: true };
|
|
162
|
+
}
|
|
163
|
+
// A different guid is a lagging write of another operation's
|
|
164
|
+
// background, or the helper has not reached `invoked` yet and
|
|
165
|
+
// nothing in chat.db can be ours. Wait for the guid to change.
|
|
166
|
+
rejected = candidate;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (operation) {
|
|
170
|
+
// The helper no longer tracks the operation (bounded record table,
|
|
171
|
+
// Messages relaunch mid-flight). Nothing left can correlate a chat.db
|
|
172
|
+
// guid with THIS operation, so the outcome is unknowable from here.
|
|
173
|
+
log('setChatBackground operation record is gone; persistence cannot be correlated');
|
|
174
|
+
return { ok: false, effectStarted: true };
|
|
175
|
+
}
|
|
176
|
+
else if (candidate && candidate !== rejected && candidate !== pulled) {
|
|
177
|
+
pulled = candidate;
|
|
178
|
+
nextHelperAt = Math.min(nextHelperAt, clock.now() + intervalMs);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (clock.now() + intervalMs >= deadline)
|
|
182
|
+
break;
|
|
183
|
+
await clock.sleep(intervalMs);
|
|
184
|
+
}
|
|
185
|
+
return { ok: false, effectStarted: true };
|
|
186
|
+
}
|
|
187
|
+
export async function verifyChatBackgroundRemove(input) {
|
|
188
|
+
const { read, initial, accepted, clock, log, deadlineMs = CHAT_BACKGROUND_REMOVE_DEADLINE_MS, intervalMs = CHAT_BACKGROUND_POLL_INTERVAL_MS, } = input;
|
|
189
|
+
const initialGuid = initial.background_channel_guid ?? null;
|
|
190
|
+
const baseline = chatBackgroundEventBaseline(initial);
|
|
191
|
+
if (didPersistRemove(accepted, initialGuid, baseline))
|
|
192
|
+
return { ok: true, effectStarted: true };
|
|
193
|
+
const deadline = clock.now() + deadlineMs;
|
|
194
|
+
let loggedTimeout = false;
|
|
195
|
+
for (;;) {
|
|
196
|
+
if (clock.now() + intervalMs > deadline)
|
|
197
|
+
break;
|
|
198
|
+
await clock.sleep(intervalMs);
|
|
199
|
+
let current;
|
|
200
|
+
try {
|
|
201
|
+
current = await read();
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
if (!isBridgeTimeout(err)) {
|
|
205
|
+
log(`removeChatBackground verification failed: ${String(err)}`);
|
|
206
|
+
return { ok: false, effectStarted: true };
|
|
207
|
+
}
|
|
208
|
+
if (!loggedTimeout) {
|
|
209
|
+
loggedTimeout = true;
|
|
210
|
+
log(`removeChatBackground verification read timed out: ${String(err)}`);
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (didPersistRemove(current, initialGuid, baseline))
|
|
215
|
+
return { ok: true, effectStarted: true };
|
|
216
|
+
}
|
|
217
|
+
return { ok: false, effectStarted: true };
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=chat-background.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-background.js","sourceRoot":"","sources":["../../src/gateway/chat-background.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ImsgMessage } from '../types.js';
|
|
2
|
+
import { type EditMessageResult, type GatewayCapabilities } from './types.js';
|
|
3
|
+
export declare const EDIT_PREFLIGHT_LOCATE_ATTEMPTS = 3;
|
|
4
|
+
export declare const EDIT_VERIFY_ATTEMPTS = 6;
|
|
5
|
+
export declare const EDIT_POLL_INTERVAL_MS = 1000;
|
|
6
|
+
export declare const EDIT_MESSAGE_PROVEN_REJECTIONS: readonly string[];
|
|
7
|
+
export interface EditMessagePort {
|
|
8
|
+
capabilities: Pick<GatewayCapabilities, 'editMessage'>;
|
|
9
|
+
history(chatId: number): Promise<ImsgMessage[]>;
|
|
10
|
+
edit(chatId: number, targetGuid: string, text: string): Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
}>;
|
|
13
|
+
sleep(ms: number): Promise<void>;
|
|
14
|
+
log(message: string): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function awaitInHistory(port: Pick<EditMessagePort, 'history' | 'sleep'>, chatId: number, matches: (m: ImsgMessage) => boolean, attempts: number, delayFirst: boolean): Promise<ImsgMessage | undefined>;
|
|
17
|
+
export declare function runEditMessage(port: EditMessagePort, chatId: number, targetGuid: string, text: string): Promise<EditMessageResult>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { bridgeEffectStarted } from './chat-background.js';
|
|
2
|
+
import { decideEditMessagePreflight } from './types.js';
|
|
3
|
+
// How long a preflight waits for a just-sent guid to surface in history
|
|
4
|
+
// before calling it absent: "a beat", not the full post-fire verify budget.
|
|
5
|
+
export const EDIT_PREFLIGHT_LOCATE_ATTEMPTS = 3;
|
|
6
|
+
// Post-fire read-back budget (the Tier-2 verify convention: 6 × 1 s).
|
|
7
|
+
export const EDIT_VERIFY_ATTEMPTS = 6;
|
|
8
|
+
export const EDIT_POLL_INTERVAL_MS = 1000;
|
|
9
|
+
// handleEditMessage's pre-invoke refusals (IMsgInjected.m) — anything else
|
|
10
|
+
// under -32603 (bridge timeout, an exception after IMChat was invoked) may
|
|
11
|
+
// have edited the row.
|
|
12
|
+
export const EDIT_MESSAGE_PROVEN_REJECTIONS = [
|
|
13
|
+
'Missing chatGuid',
|
|
14
|
+
'Missing messageGuid',
|
|
15
|
+
'Missing editedMessage',
|
|
16
|
+
'Chat not found:',
|
|
17
|
+
'No edit-message selector available',
|
|
18
|
+
'Message not found:',
|
|
19
|
+
'Message object not found:',
|
|
20
|
+
'Unexpected signature for',
|
|
21
|
+
];
|
|
22
|
+
// Poll history until a row matches, sleeping only BETWEEN attempts.
|
|
23
|
+
// `delayFirst` is for post-fire verification, where the first read would
|
|
24
|
+
// predate the effect; preflights read immediately.
|
|
25
|
+
export async function awaitInHistory(port, chatId, matches, attempts, delayFirst) {
|
|
26
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
27
|
+
if (delayFirst || attempt > 0)
|
|
28
|
+
await port.sleep(EDIT_POLL_INTERVAL_MS);
|
|
29
|
+
const found = (await port.history(chatId)).find(matches);
|
|
30
|
+
if (found)
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
// Preflight (decideEditMessagePreflight, shared with FakeGateway) → fire →
|
|
36
|
+
// verify by history read-back. Every exit is a proven refusal
|
|
37
|
+
// (effectStarted:false), a confirmed edit, or an honest ambiguity
|
|
38
|
+
// (effectStarted:true) — never a guess.
|
|
39
|
+
export async function runEditMessage(port, chatId, targetGuid, text) {
|
|
40
|
+
if (!port.capabilities.editMessage || !text.trim())
|
|
41
|
+
return { ok: false, effectStarted: false };
|
|
42
|
+
let target;
|
|
43
|
+
try {
|
|
44
|
+
target = await awaitInHistory(port, chatId, (m) => m.guid === targetGuid, EDIT_PREFLIGHT_LOCATE_ATTEMPTS, false);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
port.log(`editMessage preflight failed: ${String(err)}`);
|
|
48
|
+
return { ok: false, effectStarted: false };
|
|
49
|
+
}
|
|
50
|
+
const decided = decideEditMessagePreflight(port.capabilities, target, text);
|
|
51
|
+
if (decided)
|
|
52
|
+
return decided;
|
|
53
|
+
try {
|
|
54
|
+
await port.edit(chatId, targetGuid, text);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
port.log(`editMessage failed: ${String(err)}`);
|
|
58
|
+
return { ok: false, effectStarted: bridgeEffectStarted(err, EDIT_MESSAGE_PROVEN_REJECTIONS) };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const confirmed = await awaitInHistory(port, chatId, (m) => m.guid === targetGuid && m.text === text, EDIT_VERIFY_ATTEMPTS, true);
|
|
62
|
+
if (confirmed)
|
|
63
|
+
return { ok: true };
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
port.log(`editMessage verification failed: ${String(err)}`);
|
|
67
|
+
}
|
|
68
|
+
return { ok: false, effectStarted: true };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=edit-message.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-message.js","sourceRoot":"","sources":["../../src/gateway/edit-message.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function isBoundedId(value: string): boolean;
|
|
1
2
|
export declare function isImessageParticipantExternalId(value: string): boolean;
|
|
2
3
|
export declare function isImessageDmConversationExternalId(value: string): boolean;
|
|
3
4
|
export declare function isImessageGroupConversationExternalId(value: string): boolean;
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// schemas wrap them.
|
|
6
6
|
// Bounded-identifier envelope shared by every portable id: non-empty, capped,
|
|
7
7
|
// no control characters, no surrounding whitespace.
|
|
8
|
-
|
|
8
|
+
// The envelope every iMessage identifier must satisfy before any format
|
|
9
|
+
// check: non-empty, bounded, no surrounding whitespace, no control bytes.
|
|
10
|
+
export function isBoundedId(value) {
|
|
9
11
|
if (value.length < 1 || value.length > 512)
|
|
10
12
|
return false;
|
|
11
13
|
if (value.trim() !== value)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"external-ids.js","sourceRoot":"","sources":["../../src/gateway/external-ids.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,gFAAgF;AAChF,gFAAgF;AAChF,6EAA6E;AAC7E,qBAAqB;AAErB,8EAA8E;AAC9E,oDAAoD;AACpD,
|
|
1
|
+
{"version":3,"file":"external-ids.js","sourceRoot":"","sources":["../../src/gateway/external-ids.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,gFAAgF;AAChF,gFAAgF;AAChF,6EAA6E;AAC7E,qBAAqB;AAErB,8EAA8E;AAC9E,oDAAoD;AACpD,wEAAwE;AACxE,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,KAAK,CAAC;IACzD,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACzC,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,EAAE;QACpC,MAAM,SAAS,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QAC3C,OAAO,SAAS,KAAK,SAAS,IAAI,SAAS,GAAG,EAAE,IAAI,SAAS,KAAK,GAAG,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,+EAA+E;AAC/E,kEAAkE;AAClE,2EAA2E;AAC3E,MAAM,UAAU,+BAA+B,CAAC,KAAa;IAC3D,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,CACL,sBAAsB,CAAC,IAAI,CAAC,KAAK,CAAC;QAClC,CAAC,4BAA4B,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,KAAK,CAAC,WAAW,EAAE,IAAI,KAAK,KAAK,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAChH,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,yBAAyB;AACzB,MAAM,UAAU,kCAAkC,CAAC,KAAa;IAC9D,MAAM,MAAM,GAAG,QAAQ,CAAC;IACxB,OAAO,WAAW,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,+BAA+B,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AACvH,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,qCAAqC,CAAC,KAAa;IACjE,OAAO,WAAW,CAAC,KAAK,CAAC,IAAI,0BAA0B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC"}
|
package/dist/gateway/fake.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MessageAttachment } from '../types.js';
|
|
2
|
-
import type { Gateway, GatewayCapabilities, GatewayEvent, GatewayChat, GatewayHistoryRange, GroupChatResolution, GroupChatResolutionRequest, HandleCheck, ImsgMessage, NamePhotoShareResult, Reaction, ReactionNote, ReactResult, SendResult, SendStatus, SendTarget } from './types.js';
|
|
2
|
+
import type { ChatBackgroundPreset, ChatBackgroundRemoveResult, ChatBackgroundSetResult, ChatBackgroundStatus, EditMessageResult, Gateway, GatewayCapabilities, GatewayEvent, GatewayChat, GatewayHistoryRange, GroupChatResolution, GroupChatResolutionRequest, HandleCheck, ImsgMessage, NamePhotoShareResult, Reaction, ReactionNote, ReactResult, SendResult, SendStatus, SendTarget, TextFormatRange } from './types.js';
|
|
3
3
|
export interface FakeGatewayOptions {
|
|
4
4
|
latencyMs?: number;
|
|
5
5
|
capabilities?: Partial<GatewayCapabilities>;
|
|
@@ -20,6 +20,11 @@ export interface FakeGatewaySnapshotV1 {
|
|
|
20
20
|
readonly namePhotoSharedChatIds: readonly number[];
|
|
21
21
|
readonly groupDirectoryComplete: boolean;
|
|
22
22
|
readonly groupParticipantsUnreportedChatIds: readonly number[];
|
|
23
|
+
readonly chatBackgrounds?: ReadonlyArray<{
|
|
24
|
+
readonly chatId: number;
|
|
25
|
+
readonly backgroundGuid: string;
|
|
26
|
+
}>;
|
|
27
|
+
readonly chatBackgroundSeq?: number;
|
|
23
28
|
};
|
|
24
29
|
}
|
|
25
30
|
export type FakeGatewaySnapshot = FakeGatewaySnapshotV1;
|
|
@@ -30,6 +35,8 @@ export declare class FakeGateway implements Gateway {
|
|
|
30
35
|
private groupDirectoryComplete;
|
|
31
36
|
private groupParticipantsUnreported;
|
|
32
37
|
private namePhotoSharedChats;
|
|
38
|
+
private chatBackgrounds;
|
|
39
|
+
private nextBackgroundSeq;
|
|
33
40
|
private reachableAddresses;
|
|
34
41
|
private sentGuids;
|
|
35
42
|
private nextId;
|
|
@@ -71,6 +78,7 @@ export declare class FakeGateway implements Gateway {
|
|
|
71
78
|
effect?: string;
|
|
72
79
|
replyToGuid?: string;
|
|
73
80
|
subject?: string;
|
|
81
|
+
textFormatting?: TextFormatRange[];
|
|
74
82
|
}): Promise<SendResult>;
|
|
75
83
|
sendAttachment(chatId: number, _filePath: string, opts?: {
|
|
76
84
|
audio?: boolean;
|
|
@@ -83,9 +91,7 @@ export declare class FakeGateway implements Gateway {
|
|
|
83
91
|
sendPoll(chatId: number, question: string, options: string[]): Promise<SendResult>;
|
|
84
92
|
votePoll(chatId: number, pollGuid: string, optionId: string): Promise<ReactResult>;
|
|
85
93
|
sendRichLink(chatId: number, _url: string): Promise<SendResult>;
|
|
86
|
-
editMessage(chatId: number, targetGuid: string, text: string): Promise<
|
|
87
|
-
ok: boolean;
|
|
88
|
-
}>;
|
|
94
|
+
editMessage(chatId: number, targetGuid: string, text: string): Promise<EditMessageResult>;
|
|
89
95
|
unsendMessage(chatId: number, targetGuid: string): Promise<{
|
|
90
96
|
ok: boolean;
|
|
91
97
|
}>;
|
|
@@ -114,6 +120,9 @@ export declare class FakeGateway implements Gateway {
|
|
|
114
120
|
ok: boolean;
|
|
115
121
|
}>;
|
|
116
122
|
shareNamePhoto(chatId: number): Promise<NamePhotoShareResult>;
|
|
123
|
+
chatBackgroundStatus(chatId: number): Promise<ChatBackgroundStatus | null>;
|
|
124
|
+
setChatBackground(chatId: number, preset?: ChatBackgroundPreset): Promise<ChatBackgroundSetResult>;
|
|
125
|
+
removeChatBackground(chatId: number, expectedGuid?: string): Promise<ChatBackgroundRemoveResult>;
|
|
117
126
|
setGroupDirectoryComplete(complete: boolean): void;
|
|
118
127
|
setGroupParticipantsUnreported(chatId: number, unreported: boolean): void;
|
|
119
128
|
injectInbound(params: {
|
package/dist/gateway/fake.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { aggregatePolls } from '../polls.js';
|
|
2
|
-
import { PATCHED_GATEWAY_CAPABILITIES, toReactionNoteType } from './types.js';
|
|
2
|
+
import { CHAT_BACKGROUND_PRESETS, decideEditMessagePreflight, normalizeChatBackgroundGuard, PATCHED_GATEWAY_CAPABILITIES, sendRichPreflight, TIER2_HISTORY_SCAN_WINDOW, toReactionNoteType, } from './types.js';
|
|
3
3
|
import { classifyGroupChatResolution } from './portable-chat.js';
|
|
4
4
|
// How far back recentReactions() scans, mirroring ImsgGateway's
|
|
5
5
|
// history(chatId, 30) read — keep the two in lockstep (contract §2).
|
|
6
|
-
const HISTORY_SCAN_WINDOW =
|
|
6
|
+
const HISTORY_SCAN_WINDOW = TIER2_HISTORY_SCAN_WINDOW;
|
|
7
7
|
const REACTION_EMOJI_BY_TYPE = {
|
|
8
8
|
love: '❤️',
|
|
9
9
|
like: '👍',
|
|
@@ -37,6 +37,9 @@ function reactionEventText(type, emoji, isAdd, targetText) {
|
|
|
37
37
|
// runtime.ts because is_reaction is handled first.
|
|
38
38
|
return `Removed a ${type} from ${quotedTarget}`;
|
|
39
39
|
}
|
|
40
|
+
// Fake background GUIDs are minted from a per-gateway sequence so a re-set
|
|
41
|
+
// always yields a NEW guid (the real path's persistence proof).
|
|
42
|
+
const FAKE_BACKGROUND_GUID_PREFIX = 'fake-chat-background-';
|
|
40
43
|
// In-memory Gateway for unit tests and the simulator. No DB, no imsg binary.
|
|
41
44
|
// Must reproduce every invariant in docs/gateway-contract.md §2 exactly —
|
|
42
45
|
// see that doc before changing behavior here.
|
|
@@ -49,6 +52,8 @@ export class FakeGateway {
|
|
|
49
52
|
groupDirectoryComplete = true;
|
|
50
53
|
groupParticipantsUnreported = new Set();
|
|
51
54
|
namePhotoSharedChats = new Set();
|
|
55
|
+
chatBackgrounds = new Map();
|
|
56
|
+
nextBackgroundSeq = 1;
|
|
52
57
|
reachableAddresses = new Set();
|
|
53
58
|
sentGuids = new Set();
|
|
54
59
|
nextId = 1;
|
|
@@ -468,9 +473,22 @@ export class FakeGateway {
|
|
|
468
473
|
const chat = this.chats.get(chatId);
|
|
469
474
|
if (!chat)
|
|
470
475
|
return { ok: false };
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
476
|
+
// The real path's preflight (shared) plus the helper's membership rule:
|
|
477
|
+
// a mention must name a participant of the chat, matched exactly. A
|
|
478
|
+
// refused range sends nothing.
|
|
479
|
+
const preflight = sendRichPreflight(this.capabilities, text, opts.textFormatting);
|
|
480
|
+
if (preflight)
|
|
481
|
+
return preflight.result;
|
|
482
|
+
// The helper resolves the allowed set from the LIVE participant list and
|
|
483
|
+
// refuses every mention when that list is unreadable (empty set); the
|
|
484
|
+
// fake's unreported-directory state models exactly that.
|
|
485
|
+
const members = this.groupParticipantsUnreported.has(chatId) ? [] : chat.participants;
|
|
486
|
+
if (opts.textFormatting?.some((r) => r.mention !== undefined && !members.includes(r.mention))) {
|
|
487
|
+
return { ok: false, refused: 'mention-not-participant' };
|
|
488
|
+
}
|
|
489
|
+
// `effect`, `subject` and formatting have no fields in imsg's JSON message
|
|
490
|
+
// shape (docs/json.md) — they are write-only on the real path too, so the
|
|
491
|
+
// fake has nothing to store.
|
|
474
492
|
const msg = this.buildMessage(chat, { is_from_me: true, text, reply_to_guid: opts.replyToGuid });
|
|
475
493
|
this.emit(msg);
|
|
476
494
|
return { ok: true, id: msg.id, guid: msg.guid };
|
|
@@ -564,12 +582,18 @@ export class FakeGateway {
|
|
|
564
582
|
// Edits mutate the target row's text IN PLACE — no new row, no subscribe()
|
|
565
583
|
// event (contract §2: this is what breaks the "same guid = immutable
|
|
566
584
|
// content" assumption elsewhere in the codebase).
|
|
585
|
+
// The preflight is the real path's, verbatim (decideEditMessagePreflight):
|
|
586
|
+
// capability, non-blank text, scan window, own plain-text message only —
|
|
587
|
+
// so a retracted tombstone or an attachment row is refused here too — and
|
|
588
|
+
// unchanged text is an idempotent skip.
|
|
567
589
|
async editMessage(chatId, targetGuid, text) {
|
|
568
590
|
await this.delay();
|
|
569
591
|
const target = this.findInScanWindow(chatId, targetGuid);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
592
|
+
const decided = decideEditMessagePreflight(this.capabilities, target, text);
|
|
593
|
+
if (decided)
|
|
594
|
+
return decided;
|
|
595
|
+
if (target)
|
|
596
|
+
target.text = text;
|
|
573
597
|
return { ok: true };
|
|
574
598
|
}
|
|
575
599
|
// Live parity: unsend leaves a tombstone row whose text is cleared.
|
|
@@ -656,6 +680,41 @@ export class FakeGateway {
|
|
|
656
680
|
this.namePhotoSharedChats.add(chatId);
|
|
657
681
|
return { ok: true };
|
|
658
682
|
}
|
|
683
|
+
// ---- Chat backgrounds (contract §2 "Chat backgrounds"). Backgrounds are
|
|
684
|
+
// chat-scoped state, not message rows: no subscribe() event, no history
|
|
685
|
+
// row — the real side records them in chat.db's background tables. ----
|
|
686
|
+
async chatBackgroundStatus(chatId) {
|
|
687
|
+
await this.delay();
|
|
688
|
+
if (!this.chats.has(chatId))
|
|
689
|
+
return null;
|
|
690
|
+
const backgroundGuid = this.chatBackgrounds.get(chatId) ?? null;
|
|
691
|
+
return { chatId, backgroundSet: backgroundGuid !== null, backgroundGuid };
|
|
692
|
+
}
|
|
693
|
+
async setChatBackground(chatId, preset = 'gradient') {
|
|
694
|
+
await this.delay();
|
|
695
|
+
if (!this.capabilities.chatBackgroundSet || !this.chats.has(chatId))
|
|
696
|
+
return { ok: false, effectStarted: false };
|
|
697
|
+
if (!CHAT_BACKGROUND_PRESETS.includes(preset))
|
|
698
|
+
return { ok: false, effectStarted: false };
|
|
699
|
+
const backgroundGuid = `${FAKE_BACKGROUND_GUID_PREFIX}${this.nextBackgroundSeq++}`;
|
|
700
|
+
this.chatBackgrounds.set(chatId, backgroundGuid);
|
|
701
|
+
return { ok: true, backgroundGuid, effectStarted: true };
|
|
702
|
+
}
|
|
703
|
+
async removeChatBackground(chatId, expectedGuid) {
|
|
704
|
+
await this.delay();
|
|
705
|
+
if (!this.capabilities.chatBackgroundRemove || !this.chats.has(chatId))
|
|
706
|
+
return { ok: false, effectStarted: false };
|
|
707
|
+
const current = this.chatBackgrounds.get(chatId);
|
|
708
|
+
// Same order as the helper: the guard is compared against the live guid
|
|
709
|
+
// (nothing live counts as a mismatch too) BEFORE the no-background no-op.
|
|
710
|
+
const guard = normalizeChatBackgroundGuard(expectedGuid);
|
|
711
|
+
if (guard !== undefined && guard !== current)
|
|
712
|
+
return { ok: false, effectStarted: false };
|
|
713
|
+
if (current === undefined)
|
|
714
|
+
return { ok: true, skipped: 'no-background' };
|
|
715
|
+
this.chatBackgrounds.delete(chatId);
|
|
716
|
+
return { ok: true, effectStarted: true };
|
|
717
|
+
}
|
|
659
718
|
// ---- fixture/test driver API (not part of Gateway) ----
|
|
660
719
|
// Reproduce the real gateway's bounded-directory failure. ImsgGateway sets
|
|
661
720
|
// directoryComplete=false when the chat scan hits GROUP_CHAT_SCAN_LIMIT or a
|
|
@@ -866,6 +925,10 @@ export class FakeGateway {
|
|
|
866
925
|
namePhotoSharedChatIds: [...this.namePhotoSharedChats].sort((left, right) => left - right),
|
|
867
926
|
groupDirectoryComplete: this.groupDirectoryComplete,
|
|
868
927
|
groupParticipantsUnreportedChatIds: [...this.groupParticipantsUnreported].sort((left, right) => left - right),
|
|
928
|
+
chatBackgrounds: [...this.chatBackgrounds]
|
|
929
|
+
.map(([chatId, backgroundGuid]) => ({ chatId, backgroundGuid }))
|
|
930
|
+
.sort((left, right) => left.chatId - right.chatId),
|
|
931
|
+
chatBackgroundSeq: this.nextBackgroundSeq,
|
|
869
932
|
},
|
|
870
933
|
};
|
|
871
934
|
}
|
|
@@ -912,10 +975,18 @@ export class FakeGateway {
|
|
|
912
975
|
}
|
|
913
976
|
const namePhotoSharedChats = new Set(input.gatewayState.namePhotoSharedChatIds);
|
|
914
977
|
const groupParticipantsUnreported = new Set(input.gatewayState.groupParticipantsUnreportedChatIds);
|
|
978
|
+
const chatBackgroundEntries = input.gatewayState.chatBackgrounds ?? [];
|
|
979
|
+
const chatBackgrounds = new Map(chatBackgroundEntries.map((entry) => [entry.chatId, entry.backgroundGuid]));
|
|
980
|
+
const nextBackgroundSeq = input.gatewayState.chatBackgroundSeq ?? 1;
|
|
915
981
|
if (namePhotoSharedChats.size !== input.gatewayState.namePhotoSharedChatIds.length ||
|
|
916
982
|
[...namePhotoSharedChats].some((chatId) => !chats.has(chatId)) ||
|
|
917
983
|
groupParticipantsUnreported.size !== input.gatewayState.groupParticipantsUnreportedChatIds.length ||
|
|
918
|
-
[...groupParticipantsUnreported].some((chatId) => !chats.get(chatId)?.isGroup)
|
|
984
|
+
[...groupParticipantsUnreported].some((chatId) => !chats.get(chatId)?.isGroup) ||
|
|
985
|
+
chatBackgrounds.size !== chatBackgroundEntries.length ||
|
|
986
|
+
[...chatBackgrounds.keys()].some((chatId) => !chats.has(chatId)) ||
|
|
987
|
+
[...chatBackgrounds.values()].some((guid) => !guid) ||
|
|
988
|
+
!Number.isSafeInteger(nextBackgroundSeq) ||
|
|
989
|
+
nextBackgroundSeq < 1) {
|
|
919
990
|
throw new Error('synthetic snapshot has invalid chat-scoped gateway state');
|
|
920
991
|
}
|
|
921
992
|
this.chats = chats;
|
|
@@ -923,6 +994,8 @@ export class FakeGateway {
|
|
|
923
994
|
this.sentGuids = sentGuids;
|
|
924
995
|
this.reachableAddresses = new Set(input.gatewayState.reachableAddresses);
|
|
925
996
|
this.namePhotoSharedChats = namePhotoSharedChats;
|
|
997
|
+
this.chatBackgrounds = chatBackgrounds;
|
|
998
|
+
this.nextBackgroundSeq = nextBackgroundSeq;
|
|
926
999
|
this.groupDirectoryComplete = input.gatewayState.groupDirectoryComplete;
|
|
927
1000
|
this.groupParticipantsUnreported = groupParticipantsUnreported;
|
|
928
1001
|
this.nextChatId = Math.max(0, ...chats.keys()) + 1;
|