@mitumba/sdk 1.31.0 → 1.32.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/index.js CHANGED
@@ -1,5 +1,55 @@
1
1
  'use strict';
2
2
 
3
+ // src/types/messages-v2.ts
4
+ var MESSAGING_ERROR_CODES = [
5
+ "invalid_input",
6
+ "unauthorized",
7
+ "session_expired",
8
+ "forbidden",
9
+ "conversation_not_found",
10
+ "thread_not_found",
11
+ "message_not_found",
12
+ "timeline_item_not_found",
13
+ "context_mismatch",
14
+ "idempotency_conflict",
15
+ "cursor_invalid",
16
+ "cursor_expired",
17
+ "attachment_not_ready",
18
+ "blocked",
19
+ "legacy_context_required",
20
+ "client_upgrade_required",
21
+ "rate_limited",
22
+ "temporarily_unavailable",
23
+ "internal_error",
24
+ "invalid_response",
25
+ "network_error"
26
+ ];
27
+ var MESSAGING_ERROR_CODE_SET = new Set(MESSAGING_ERROR_CODES);
28
+ function isMessagingAPIError(value, code) {
29
+ if (!(value instanceof Error) || value.name !== "APIError") {
30
+ return false;
31
+ }
32
+ const candidate = value;
33
+ if (typeof candidate.code !== "string" || typeof candidate.status !== "number" || !MESSAGING_ERROR_CODE_SET.has(candidate.code)) {
34
+ return false;
35
+ }
36
+ return code === void 0 || candidate.code === code;
37
+ }
38
+ var RealtimeProtocolError = class extends Error {
39
+ /** Construct a stable protocol failure without echoing an untrusted frame. */
40
+ constructor() {
41
+ super("Messaging realtime protocol is invalid.");
42
+ this.name = "RealtimeProtocolError";
43
+ }
44
+ };
45
+ var RealtimeUnavailableError = class extends Error {
46
+ /** Construct a stable runtime-unavailable failure. */
47
+ constructor() {
48
+ super("Messaging realtime is unavailable in this runtime.");
49
+ this.name = "RealtimeUnavailableError";
50
+ }
51
+ };
52
+
3
53
  // src/token-store.ts
4
54
  var MemoryTokenStore = class {
5
55
  accessToken = null;
@@ -101,6 +151,37 @@ function isExpiringSoon(token, thresholdSeconds) {
101
151
  return false;
102
152
  }
103
153
  }
154
+ function isErrorRecord(value) {
155
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
156
+ }
157
+ function normalizeAPIErrorResponse(value, fallbackMessage) {
158
+ if (isErrorRecord(value) && typeof value.error === "string") {
159
+ return {
160
+ error: value.error,
161
+ message: typeof value.message === "string" ? value.message : fallbackMessage,
162
+ details: value.details
163
+ };
164
+ }
165
+ if (isErrorRecord(value) && isErrorRecord(value.error) && typeof value.error.code === "string") {
166
+ return {
167
+ error: value.error.code,
168
+ message: typeof value.error.message === "string" ? value.error.message : fallbackMessage,
169
+ details: value.error.details
170
+ };
171
+ }
172
+ return { error: "unknown_error", message: fallbackMessage };
173
+ }
174
+ var InvalidRefreshCredentialsError = class extends Error {
175
+ constructor() {
176
+ super("Refresh credentials are invalid.");
177
+ this.name = "InvalidRefreshCredentialsError";
178
+ }
179
+ };
180
+ function isTokenResponse(value) {
181
+ if (!value || typeof value !== "object") return false;
182
+ const candidate = value;
183
+ return typeof candidate.access_token === "string" && candidate.access_token.length > 0 && typeof candidate.refresh_token === "string" && candidate.refresh_token.length > 0;
184
+ }
104
185
  var APIClient = class {
105
186
  config;
106
187
  tokenStore;
@@ -136,6 +217,13 @@ var APIClient = class {
136
217
  getBaseUrl() {
137
218
  return this.config.baseUrl;
138
219
  }
220
+ /** Create an isomorphic WebSocket using the configured or global transport. */
221
+ createWebSocket(url, protocols) {
222
+ if (this.config.webSocketFactory) return this.config.webSocketFactory(url, protocols);
223
+ const Constructor = globalThis.WebSocket;
224
+ if (typeof Constructor !== "function") throw new RealtimeUnavailableError();
225
+ return new Constructor(url, protocols);
226
+ }
139
227
  async clearToken() {
140
228
  this.config.token = void 0;
141
229
  this.config.refreshToken = void 0;
@@ -159,7 +247,7 @@ var APIClient = class {
159
247
  if (access) this.config.token = access;
160
248
  if (refresh) this.config.refreshToken = refresh;
161
249
  }
162
- async ensureFreshToken() {
250
+ async ensureFreshToken(options) {
163
251
  if (!this.config.token) {
164
252
  const stored = await this.tokenStore.getAccessToken();
165
253
  if (stored) this.config.token = stored;
@@ -167,12 +255,12 @@ var APIClient = class {
167
255
  if (refresh) this.config.refreshToken = refresh;
168
256
  }
169
257
  if (this.config.token && isExpiringSoon(this.config.token, 60) && this.config.refreshToken && this.config.refreshToken.length > 0) {
170
- await this.handleTokenRefresh();
258
+ await this.handleTokenRefresh(options?.signal);
171
259
  }
172
260
  }
173
261
  async request(method, path, body, params, options) {
174
262
  if (!path.includes("/auth/refresh") && !path.includes("/auth/login") && !path.includes("/auth/register")) {
175
- await this.ensureFreshToken();
263
+ await this.ensureFreshToken(options);
176
264
  }
177
265
  const url = new URL(path, this.config.baseUrl);
178
266
  if (params) {
@@ -236,15 +324,14 @@ var APIClient = class {
236
324
  const hasRefreshToken = this.config.refreshToken && this.config.refreshToken.length > 0;
237
325
  if (hasRefreshToken) {
238
326
  try {
239
- await this.handleTokenRefresh();
240
- headers.set("Authorization", `Bearer ${this.config.token}`);
241
- response = await fetch(url.toString(), { ...init, headers });
242
- } catch {
243
- if (this.config.onAuthExpired) {
244
- this.config.onAuthExpired();
245
- }
327
+ await this.handleTokenRefresh(options?.signal);
328
+ } catch (error) {
329
+ if (!(error instanceof InvalidRefreshCredentialsError)) throw error;
330
+ this.config.onAuthExpired?.();
246
331
  throw new APIError(401, { error: "session_expired", message: "Session expired. Please log in again." });
247
332
  }
333
+ headers.set("Authorization", `Bearer ${this.config.token}`);
334
+ response = await fetch(url.toString(), { ...init, headers });
248
335
  } else {
249
336
  if (this.config.onAuthExpired) {
250
337
  this.config.onAuthExpired();
@@ -253,20 +340,20 @@ var APIClient = class {
253
340
  }
254
341
  }
255
342
  if (!response.ok) {
256
- let errorData;
343
+ let decoded;
257
344
  try {
258
- errorData = await response.json();
345
+ decoded = await response.json();
259
346
  } catch {
260
- errorData = { error: "unknown_error", message: response.statusText };
347
+ decoded = null;
261
348
  }
262
- throw new APIError(response.status, errorData);
349
+ throw new APIError(response.status, normalizeAPIErrorResponse(decoded, response.statusText));
263
350
  }
264
351
  if (response.status === 204) {
265
352
  return void 0;
266
353
  }
267
354
  return response.json();
268
355
  }
269
- async handleTokenRefresh() {
356
+ async handleTokenRefresh(signal) {
270
357
  if (this.isRefreshing && this.refreshPromise) {
271
358
  return this.refreshPromise;
272
359
  }
@@ -277,21 +364,35 @@ var APIClient = class {
277
364
  const response = await fetch(url.toString(), {
278
365
  method: "POST",
279
366
  headers: { "Content-Type": "application/json" },
280
- body: JSON.stringify({ refresh_token: this.config.refreshToken })
367
+ body: JSON.stringify({ refresh_token: this.config.refreshToken }),
368
+ signal
281
369
  });
370
+ if (response.status === 400 || response.status === 401) {
371
+ await this.clearToken();
372
+ throw new InvalidRefreshCredentialsError();
373
+ }
282
374
  if (!response.ok) {
283
- throw new Error("Refresh failed");
375
+ let decoded2;
376
+ try {
377
+ decoded2 = await response.json();
378
+ } catch {
379
+ decoded2 = null;
380
+ }
381
+ throw new APIError(response.status, normalizeAPIErrorResponse(decoded2, response.statusText || "Token refresh failed."));
382
+ }
383
+ let decoded;
384
+ try {
385
+ decoded = await response.json();
386
+ } catch {
387
+ throw new APIError(response.status, { error: "refresh_response_invalid", message: "Token refresh returned an invalid response." });
284
388
  }
285
- const data = await response.json();
286
- this.config.token = data.access_token;
287
- this.config.refreshToken = data.refresh_token;
288
- await this.tokenStore.setTokens(data.access_token, data.refresh_token);
289
- if (this.config.onTokenRefresh) {
290
- this.config.onTokenRefresh({ token: data.access_token, refreshToken: data.refresh_token });
389
+ if (!isTokenResponse(decoded)) {
390
+ throw new APIError(response.status, { error: "refresh_response_invalid", message: "Token refresh returned an invalid response." });
291
391
  }
292
- } catch (err) {
293
- await this.clearToken();
294
- throw err;
392
+ this.config.token = decoded.access_token;
393
+ this.config.refreshToken = decoded.refresh_token;
394
+ await this.tokenStore.setTokens(decoded.access_token, decoded.refresh_token);
395
+ this.config.onTokenRefresh?.({ token: decoded.access_token, refreshToken: decoded.refresh_token });
295
396
  } finally {
296
397
  this.isRefreshing = false;
297
398
  this.refreshPromise = null;
@@ -861,15 +962,447 @@ var VaziModule = class {
861
962
  }
862
963
  };
863
964
 
965
+ // src/realtime/messages-realtime.ts
966
+ var PROTOCOL = "mitumba-messaging-v1";
967
+ var TYPING_THROTTLE_MS = 750;
968
+ var TYPING_CLEAR_MS = 3e3;
969
+ var MAX_DEDUPLICATION_IDS = 2e3;
970
+ function isRecord(value) {
971
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
972
+ }
973
+ function isAuthor(value) {
974
+ if (!isRecord(value)) return false;
975
+ if (value.side === "buyer") return isRecord(value.buyer) && typeof value.buyer.id === "string";
976
+ return value.side === "store" && isRecord(value.store) && typeof value.store.id === "string";
977
+ }
978
+ function isThreadMessage(value) {
979
+ return isRecord(value) && typeof value.id === "string" && typeof value.thread_id === "string" && Number.isSafeInteger(value.sequence) && typeof value.body === "string" && isAuthor(value.author) && Array.isArray(value.attachments) && typeof value.created_at === "string";
980
+ }
981
+ function isThreadCard(value) {
982
+ return isRecord(value) && typeof value.id === "string" && typeof value.conversation_id === "string" && ["general", "listing", "order"].includes(String(value.kind)) && typeof value.unread_count === "number";
983
+ }
984
+ function isConversation(value) {
985
+ return isRecord(value) && typeof value.id === "string" && isRecord(value.buyer) && isRecord(value.store) && typeof value.unread_count === "number";
986
+ }
987
+ function isSystemEvent(value) {
988
+ return isRecord(value) && typeof value.id === "string" && typeof value.thread_id === "string" && Number.isSafeInteger(value.sequence) && value.kind === "thread.linked";
989
+ }
990
+ function isReadCursor(value) {
991
+ return isRecord(value) && typeof value.thread_id === "string" && typeof value.last_read_item_id === "string" && Number.isSafeInteger(value.last_read_sequence);
992
+ }
993
+ function isUpload(value) {
994
+ return isRecord(value) && typeof value.id === "string" && typeof value.thread_id === "string" && ["authorized", "uploaded", "pending_scan", "safe", "rejected", "expired", "claimed"].includes(String(value.state));
995
+ }
996
+ function validDurablePayload(type, payload) {
997
+ if (!isRecord(payload)) return false;
998
+ if (type === "message.created") return isThreadMessage(payload.message);
999
+ if (type === "system_event.created") return isSystemEvent(payload.event);
1000
+ if (type === "conversation.updated") return isConversation(payload.conversation);
1001
+ if (type === "thread.created" || type === "thread.updated") return isThreadCard(payload.thread);
1002
+ if (type === "read_cursor.updated") return isReadCursor(payload.cursor);
1003
+ if (type === "attachment.updated") return isUpload(payload.upload);
1004
+ return type === "message.policy_applied" && typeof payload.message_id === "string" && Array.isArray(payload.notices) && payload.notices.every((notice) => typeof notice === "string");
1005
+ }
1006
+ function parseEvent(value) {
1007
+ if (!isRecord(value) || value.api_version !== 2 || value.protocol_version !== 1 || typeof value.type !== "string") {
1008
+ throw new RealtimeProtocolError();
1009
+ }
1010
+ if (value.type === "ready") {
1011
+ if (typeof value.connection_id !== "string" || value.resume_cursor !== null && typeof value.resume_cursor !== "string") {
1012
+ throw new RealtimeProtocolError();
1013
+ }
1014
+ return { type: "ready", resume_cursor: value.resume_cursor };
1015
+ }
1016
+ if (value.type === "typing.updated") {
1017
+ if (typeof value.thread_id !== "string" || !isAuthor(value.author) || typeof value.is_typing !== "boolean" || typeof value.occurred_at !== "string") {
1018
+ throw new RealtimeProtocolError();
1019
+ }
1020
+ return value;
1021
+ }
1022
+ if (value.type === "resync.required") {
1023
+ if (!["cursor_expired", "gap_detected", "authorization_changed"].includes(String(value.reason)) || typeof value.resume_cursor !== "string") {
1024
+ throw new RealtimeProtocolError();
1025
+ }
1026
+ return value;
1027
+ }
1028
+ const durableTypes = [
1029
+ "message.created",
1030
+ "system_event.created",
1031
+ "conversation.updated",
1032
+ "thread.created",
1033
+ "thread.updated",
1034
+ "read_cursor.updated",
1035
+ "attachment.updated",
1036
+ "message.policy_applied"
1037
+ ];
1038
+ if (!durableTypes.includes(value.type) || typeof value.event_id !== "string" || typeof value.resume_cursor !== "string" || typeof value.occurred_at !== "string" || !validDurablePayload(value.type, value.payload)) {
1039
+ throw new RealtimeProtocolError();
1040
+ }
1041
+ return value;
1042
+ }
1043
+ function ticketPath(options) {
1044
+ return options.perspective.kind === "buyer" ? "/notify/v2/inboxes/buyer/realtime/tickets" : `/notify/v2/inboxes/stores/${encodeURIComponent(options.perspective.store_id)}/realtime/tickets`;
1045
+ }
1046
+ function parseTicket(value) {
1047
+ if (!isRecord(value) || value.api_version !== 2 || !isRecord(value.data)) throw new RealtimeProtocolError();
1048
+ const ticket = value.data;
1049
+ if (typeof ticket.ticket !== "string" || typeof ticket.expires_at !== "string" || typeof ticket.websocket_url !== "string" || ticket.websocket_protocol !== PROTOCOL || ticket.protocol_version !== 1 || ticket.resume_cursor !== null && typeof ticket.resume_cursor !== "string") {
1050
+ throw new RealtimeProtocolError();
1051
+ }
1052
+ let url;
1053
+ try {
1054
+ url = new URL(ticket.websocket_url);
1055
+ } catch {
1056
+ throw new RealtimeProtocolError();
1057
+ }
1058
+ if (!["ws:", "wss:"].includes(url.protocol) || url.searchParams.has("token") || !url.searchParams.has("ticket")) {
1059
+ throw new RealtimeProtocolError();
1060
+ }
1061
+ return ticket;
1062
+ }
1063
+ var ManagedMessagesRealtimeConnection = class {
1064
+ constructor(client, options) {
1065
+ this.client = client;
1066
+ this.options = options;
1067
+ this.currentCursor = options.resumeCursor ?? null;
1068
+ }
1069
+ client;
1070
+ options;
1071
+ currentState = "closed";
1072
+ currentCursor;
1073
+ socket = null;
1074
+ reconnectTimer = null;
1075
+ typingTimers = /* @__PURE__ */ new Map();
1076
+ typingSentAt = /* @__PURE__ */ new Map();
1077
+ eventListeners = /* @__PURE__ */ new Set();
1078
+ stateListeners = /* @__PURE__ */ new Set();
1079
+ subscribedThreads = /* @__PURE__ */ new Set();
1080
+ seenEventIds = /* @__PURE__ */ new Set();
1081
+ lastMessageSequence = /* @__PURE__ */ new Map();
1082
+ explicitClose = false;
1083
+ reconnectAttempt = 0;
1084
+ startPromise = null;
1085
+ resolveStart = null;
1086
+ rejectStart = null;
1087
+ onOpen = () => void 0;
1088
+ onMessage = (event) => {
1089
+ void this.handleMessage(event);
1090
+ };
1091
+ onClose = (event) => {
1092
+ this.handleClose(event);
1093
+ };
1094
+ onError = () => void 0;
1095
+ get state() {
1096
+ return this.currentState;
1097
+ }
1098
+ get resumeCursor() {
1099
+ return this.currentCursor;
1100
+ }
1101
+ onEvent(listener) {
1102
+ this.eventListeners.add(listener);
1103
+ return () => {
1104
+ this.eventListeners.delete(listener);
1105
+ };
1106
+ }
1107
+ onStateChange(listener) {
1108
+ this.stateListeners.add(listener);
1109
+ return () => {
1110
+ this.stateListeners.delete(listener);
1111
+ };
1112
+ }
1113
+ subscribeThread(threadId) {
1114
+ this.subscribedThreads.add(threadId);
1115
+ }
1116
+ unsubscribeThread(threadId) {
1117
+ this.clearTyping(threadId, true);
1118
+ this.subscribedThreads.delete(threadId);
1119
+ }
1120
+ setTyping(threadId, isTyping) {
1121
+ if (!this.subscribedThreads.has(threadId) || this.currentState !== "open" || !this.socket) return;
1122
+ const now = Date.now();
1123
+ const last = this.typingSentAt.get(threadId) ?? 0;
1124
+ if (now - last < TYPING_THROTTLE_MS) return;
1125
+ this.send({ protocol_version: 1, type: "typing.set", thread_id: threadId, is_typing: isTyping });
1126
+ this.typingSentAt.set(threadId, now);
1127
+ const existing = this.typingTimers.get(threadId);
1128
+ if (existing) clearTimeout(existing);
1129
+ if (isTyping) {
1130
+ this.typingTimers.set(threadId, setTimeout(() => {
1131
+ this.typingSentAt.delete(threadId);
1132
+ this.setTyping(threadId, false);
1133
+ this.typingTimers.delete(threadId);
1134
+ }, TYPING_CLEAR_MS));
1135
+ } else {
1136
+ this.typingTimers.delete(threadId);
1137
+ }
1138
+ }
1139
+ async start() {
1140
+ if (this.explicitClose) throw new RealtimeUnavailableError();
1141
+ if (this.currentState === "open") return;
1142
+ if (this.startPromise) return this.startPromise;
1143
+ const pending = new Promise((resolve, reject) => {
1144
+ this.resolveStart = resolve;
1145
+ this.rejectStart = reject;
1146
+ });
1147
+ this.startPromise = pending;
1148
+ await this.open(false).catch((error) => {
1149
+ if (error instanceof RealtimeProtocolError || error instanceof RealtimeUnavailableError || error instanceof APIError && [401, 403, 409].includes(error.status)) {
1150
+ this.fail(error);
1151
+ } else {
1152
+ this.scheduleReconnect();
1153
+ }
1154
+ });
1155
+ return pending;
1156
+ }
1157
+ close() {
1158
+ this.explicitClose = true;
1159
+ this.clearReconnect();
1160
+ this.clearAllTyping();
1161
+ this.detachSocket(true);
1162
+ this.transition("closed");
1163
+ this.resolveStart?.();
1164
+ this.finishStart();
1165
+ this.eventListeners.clear();
1166
+ this.stateListeners.clear();
1167
+ }
1168
+ async open(reconnecting) {
1169
+ this.transition(reconnecting ? "reconnecting" : "connecting");
1170
+ const raw = await this.client.post(ticketPath(this.options), {
1171
+ resume_cursor: this.currentCursor,
1172
+ supported_protocol_versions: [1]
1173
+ }, { signal: this.options.signal });
1174
+ if (this.explicitClose) return;
1175
+ const ticket = parseTicket(raw);
1176
+ const socket = this.client.createWebSocket(ticket.websocket_url, ticket.websocket_protocol);
1177
+ this.socket = socket;
1178
+ socket.addEventListener("open", this.onOpen);
1179
+ socket.addEventListener("message", this.onMessage);
1180
+ socket.addEventListener("close", this.onClose);
1181
+ socket.addEventListener("error", this.onError);
1182
+ }
1183
+ async handleMessage(event) {
1184
+ try {
1185
+ if (typeof event.data !== "string") throw new RealtimeProtocolError();
1186
+ let decoded;
1187
+ try {
1188
+ decoded = JSON.parse(event.data);
1189
+ } catch {
1190
+ throw new RealtimeProtocolError();
1191
+ }
1192
+ const parsed = parseEvent(decoded);
1193
+ if (parsed.type === "ready") {
1194
+ this.currentCursor = parsed.resume_cursor ?? this.currentCursor;
1195
+ this.reconnectAttempt = 0;
1196
+ this.transition("open");
1197
+ this.resolveStart?.();
1198
+ this.finishStart();
1199
+ return;
1200
+ }
1201
+ if ("event_id" in parsed) {
1202
+ if (this.seenEventIds.has(parsed.event_id)) {
1203
+ this.ack(parsed.event_id, parsed.resume_cursor);
1204
+ return;
1205
+ }
1206
+ if (parsed.type === "message.created") {
1207
+ const message = parsed.payload.message;
1208
+ const previous = this.lastMessageSequence.get(message.thread_id) ?? 0;
1209
+ if (message.sequence <= previous) {
1210
+ this.remember(parsed.event_id);
1211
+ this.ack(parsed.event_id, parsed.resume_cursor);
1212
+ return;
1213
+ }
1214
+ this.lastMessageSequence.set(message.thread_id, message.sequence);
1215
+ }
1216
+ if (parsed.thread_id && this.subscribedThreads.size > 0 && !this.subscribedThreads.has(parsed.thread_id)) {
1217
+ this.remember(parsed.event_id);
1218
+ this.ack(parsed.event_id, parsed.resume_cursor);
1219
+ return;
1220
+ }
1221
+ } else if (parsed.type === "typing.updated" && !this.subscribedThreads.has(parsed.thread_id)) {
1222
+ return;
1223
+ }
1224
+ for (const listener of [...this.eventListeners]) await listener(parsed);
1225
+ if ("event_id" in parsed) {
1226
+ this.remember(parsed.event_id);
1227
+ this.currentCursor = parsed.resume_cursor;
1228
+ this.ack(parsed.event_id, parsed.resume_cursor);
1229
+ } else if (parsed.type === "resync.required") {
1230
+ this.currentCursor = parsed.resume_cursor;
1231
+ }
1232
+ } catch (error) {
1233
+ this.fail(error instanceof RealtimeProtocolError ? error : new RealtimeProtocolError());
1234
+ this.socket?.close(4004, "protocol_error");
1235
+ }
1236
+ }
1237
+ handleClose(event) {
1238
+ this.detachSocket(false);
1239
+ this.clearAllTyping();
1240
+ if (this.explicitClose) return;
1241
+ if ([4002, 4003, 4004].includes(event.code)) {
1242
+ this.fail(new RealtimeProtocolError());
1243
+ return;
1244
+ }
1245
+ this.scheduleReconnect();
1246
+ }
1247
+ scheduleReconnect() {
1248
+ if (this.reconnectTimer || this.explicitClose) return;
1249
+ this.transition("reconnecting");
1250
+ const base = Math.max(10, this.options.reconnectBaseDelayMs ?? 500);
1251
+ const maximum = Math.max(base, this.options.reconnectMaxDelayMs ?? 3e4);
1252
+ const cap = Math.min(maximum, base * 2 ** this.reconnectAttempt);
1253
+ const random = this.options.random ?? Math.random;
1254
+ const delay = Math.floor(random() * cap);
1255
+ this.reconnectAttempt += 1;
1256
+ this.reconnectTimer = setTimeout(() => {
1257
+ this.reconnectTimer = null;
1258
+ void this.open(true).catch((error) => {
1259
+ if (error instanceof APIError && [401, 403].includes(error.status)) this.fail(error);
1260
+ else this.scheduleReconnect();
1261
+ });
1262
+ }, delay);
1263
+ }
1264
+ ack(eventId, cursor) {
1265
+ this.send({ protocol_version: 1, type: "ack", event_id: eventId, resume_cursor: cursor });
1266
+ }
1267
+ send(frame) {
1268
+ if (!this.socket || this.currentState !== "open") return;
1269
+ try {
1270
+ this.socket.send(JSON.stringify(frame));
1271
+ } catch {
1272
+ }
1273
+ }
1274
+ remember(eventId) {
1275
+ this.seenEventIds.add(eventId);
1276
+ if (this.seenEventIds.size <= MAX_DEDUPLICATION_IDS) return;
1277
+ const oldest = this.seenEventIds.values().next().value;
1278
+ if (typeof oldest === "string") this.seenEventIds.delete(oldest);
1279
+ }
1280
+ clearTyping(threadId, sendFalse) {
1281
+ const timer = this.typingTimers.get(threadId);
1282
+ if (timer) clearTimeout(timer);
1283
+ this.typingTimers.delete(threadId);
1284
+ this.typingSentAt.delete(threadId);
1285
+ if (sendFalse && this.subscribedThreads.has(threadId)) {
1286
+ this.send({ protocol_version: 1, type: "typing.set", thread_id: threadId, is_typing: false });
1287
+ }
1288
+ }
1289
+ clearAllTyping() {
1290
+ for (const timer of this.typingTimers.values()) clearTimeout(timer);
1291
+ this.typingTimers.clear();
1292
+ this.typingSentAt.clear();
1293
+ }
1294
+ clearReconnect() {
1295
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
1296
+ this.reconnectTimer = null;
1297
+ }
1298
+ detachSocket(close) {
1299
+ const socket = this.socket;
1300
+ if (!socket) return;
1301
+ socket.removeEventListener("open", this.onOpen);
1302
+ socket.removeEventListener("message", this.onMessage);
1303
+ socket.removeEventListener("close", this.onClose);
1304
+ socket.removeEventListener("error", this.onError);
1305
+ if (close) {
1306
+ try {
1307
+ socket.close(1e3, "client_close");
1308
+ } catch {
1309
+ }
1310
+ }
1311
+ this.socket = null;
1312
+ }
1313
+ transition(state) {
1314
+ if (this.currentState === state) return;
1315
+ this.currentState = state;
1316
+ for (const listener of [...this.stateListeners]) listener(state);
1317
+ }
1318
+ fail(error) {
1319
+ this.clearReconnect();
1320
+ this.transition("failed");
1321
+ this.rejectStart?.(error);
1322
+ this.finishStart();
1323
+ }
1324
+ finishStart() {
1325
+ this.resolveStart = null;
1326
+ this.rejectStart = null;
1327
+ this.startPromise = null;
1328
+ }
1329
+ };
1330
+
864
1331
  // src/modules/messages.ts
1332
+ function encode(value) {
1333
+ return encodeURIComponent(value);
1334
+ }
1335
+ function isRecord2(value) {
1336
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1337
+ }
1338
+ function unwrap(value, valid = () => true) {
1339
+ if (!isRecord2(value) || value.api_version !== 2 || !("data" in value) || !valid(value.data)) {
1340
+ throw new APIError(502, { error: "invalid_response", message: "Messaging returned an invalid response." });
1341
+ }
1342
+ return value.data;
1343
+ }
1344
+ function isPage(value) {
1345
+ return isRecord2(value) && Array.isArray(value.items);
1346
+ }
1347
+ function isIdentified(value) {
1348
+ return isRecord2(value) && typeof value.id === "string";
1349
+ }
1350
+ function perspectiveBase(perspective) {
1351
+ return perspective.kind === "buyer" ? "/notify/v2/inboxes/buyer" : `/notify/v2/inboxes/stores/${encode(perspective.store_id)}`;
1352
+ }
1353
+ function query(params) {
1354
+ const entries = Object.entries(params);
1355
+ return entries.some(([, value]) => value !== void 0) ? Object.fromEntries(entries) : void 0;
1356
+ }
1357
+ function idempotentOptions(options) {
1358
+ if (!options.idempotencyKey.trim()) throw new TypeError("idempotencyKey is required.");
1359
+ return {
1360
+ signal: options.signal,
1361
+ headers: { ...options.headers, "Idempotency-Key": options.idempotencyKey }
1362
+ };
1363
+ }
1364
+ function validateLimit(params) {
1365
+ if (params.limit !== void 0 && (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 100)) {
1366
+ throw new RangeError("Messaging limit must be an integer from 1 through 100.");
1367
+ }
1368
+ }
1369
+ function validateSendInput(input) {
1370
+ if (!input.client_message_id.trim()) throw new TypeError("client_message_id is required.");
1371
+ const length = input.body.trim().length;
1372
+ if (length < 1 || length > 2e3) throw new RangeError("Message body must contain 1 to 2,000 characters after trimming.");
1373
+ }
1374
+ function validateAttachmentInput(input) {
1375
+ if (!input.file_name.trim() || !input.content_type.trim()) throw new TypeError("Attachment file name and content type are required.");
1376
+ if (!Number.isSafeInteger(input.size_bytes) || input.size_bytes < 1) throw new RangeError("Attachment size must be a positive integer.");
1377
+ if (!/^[a-f0-9]{64}$/u.test(input.sha256)) throw new TypeError("Attachment SHA-256 must be lowercase hexadecimal.");
1378
+ }
1379
+ function uploadBodySize(body) {
1380
+ if ("size" in body) return body.size;
1381
+ return body.byteLength;
1382
+ }
1383
+ async function parseDirectResponse(response) {
1384
+ let decoded;
1385
+ try {
1386
+ decoded = await response.json();
1387
+ } catch {
1388
+ throw new APIError(response.status || 502, { error: "invalid_response", message: "Messaging returned an invalid response." });
1389
+ }
1390
+ if (!response.ok) {
1391
+ const error = isRecord2(decoded) && isRecord2(decoded.error) ? decoded.error : decoded;
1392
+ throw new APIError(response.status, {
1393
+ error: isRecord2(error) && typeof error.code === "string" ? error.code : isRecord2(error) && typeof error.error === "string" ? error.error : "unknown_error",
1394
+ message: isRecord2(error) && typeof error.message === "string" ? error.message : response.statusText,
1395
+ details: isRecord2(error) ? error.details : void 0
1396
+ });
1397
+ }
1398
+ return unwrap(decoded);
1399
+ }
865
1400
  var MessagesModule = class {
866
1401
  constructor(client) {
867
1402
  this.client = client;
868
1403
  }
869
1404
  client;
870
- /**
871
- * List all conversations for the authenticated user.
872
- */
1405
+ /** @deprecated Use listConversations or listStoreConversations. Context fields are non-authoritative server-validated hints. */
873
1406
  async list(storeId, options) {
874
1407
  return this.client.get(
875
1408
  "/notify/messages",
@@ -877,9 +1410,7 @@ var MessagesModule = class {
877
1410
  options
878
1411
  );
879
1412
  }
880
- /**
881
- * Get the message thread with a specific partner.
882
- */
1413
+ /** @deprecated Resolve a v2 thread and call listThreadMessages. */
883
1414
  async getThread(partnerId, storeId, options) {
884
1415
  return this.client.get(
885
1416
  `/notify/messages/${partnerId}`,
@@ -887,37 +1418,206 @@ var MessagesModule = class {
887
1418
  options
888
1419
  );
889
1420
  }
890
- /**
891
- * Send a message to another user.
892
- */
1421
+ /** @deprecated Use sendThreadMessage. Receiver and context fields are hints validated by the server. */
893
1422
  async send(input, options) {
894
1423
  return this.client.post("/notify/messages", input, options);
895
1424
  }
896
- /**
897
- * Mark all messages from a partner as read.
898
- */
1425
+ /** @deprecated Use advanceThreadReadCursor for precise monotonic state. */
899
1426
  async markRead(partnerId, storeId, options) {
900
1427
  const path = storeId ? `/notify/messages/${partnerId}/read?store_id=${storeId}` : `/notify/messages/${partnerId}/read`;
901
1428
  return this.client.post(path, void 0, options);
902
1429
  }
903
- /**
904
- * Connect to the typing indicator WebSocket for a conversation.
905
- * Token is passed via query param since WebSocket can't set headers.
906
- */
1430
+ /** @deprecated Use managed messaging realtime. */
907
1431
  connectTyping(partnerId) {
908
1432
  const token = this.client.getToken();
909
1433
  const wsUrl = this.client.getBaseUrl().replace(/^http/, "ws") + `/notify/messages/${partnerId}/ws?token=${token}`;
910
1434
  return new WebSocket(wsUrl);
911
1435
  }
912
- /**
913
- * Connect to the user-scoped inbox WebSocket.
914
- * Emits InboxRealtimeEvent for every inbound message across all conversations.
915
- */
1436
+ /** @deprecated Use managed messaging realtime. */
916
1437
  connectInbox() {
917
1438
  const token = this.client.getToken();
918
1439
  const wsUrl = this.client.getBaseUrl().replace(/^http/, "ws") + `/notify/messages/inbox?token=${token}`;
919
1440
  return new WebSocket(wsUrl);
920
1441
  }
1442
+ /** Create a legacy inbox socket only after hydrating/refreshing the access token. */
1443
+ async connectInboxFresh(options) {
1444
+ await this.client.ensureFreshToken(options);
1445
+ const token = this.client.getToken() ?? "";
1446
+ const wsUrl = this.client.getBaseUrl().replace(/^http/, "ws") + `/notify/messages/inbox?token=${encode(token)}`;
1447
+ return this.client.createWebSocket(wsUrl, []);
1448
+ }
1449
+ /** Create a report for a message or conversation. */
1450
+ async createReport(input, options) {
1451
+ return this.client.post("/notify/messages/reports", input, options);
1452
+ }
1453
+ /** Create an idempotent bilateral buyer/store relationship block. */
1454
+ async createBlock(input, options) {
1455
+ return this.client.post("/notify/messages/blocks", input, options);
1456
+ }
1457
+ /** Remove a block created by the current perspective. */
1458
+ async removeBlock(blockId, options) {
1459
+ return this.client.delete(`/notify/messages/blocks/${encode(blockId)}`, void 0, options);
1460
+ }
1461
+ /** Discover server messaging capabilities. */
1462
+ async getMessagingCapabilities(options) {
1463
+ const value = await this.client.get("/notify/v2/capabilities", void 0, options);
1464
+ return unwrap(value, isRecord2);
1465
+ }
1466
+ /** List buyer and represented-store inbox perspectives. */
1467
+ async listMessagingInboxes(options) {
1468
+ const value = await this.client.get("/notify/v2/inboxes", void 0, options);
1469
+ return unwrap(value, isRecord2);
1470
+ }
1471
+ /** List personal buyer conversations. */
1472
+ async listConversations(params = {}, options) {
1473
+ validateLimit(params);
1474
+ const value = await this.client.get("/notify/v2/inboxes/buyer/conversations", query(params), options);
1475
+ return unwrap(value, isPage);
1476
+ }
1477
+ /** List conversations for one represented store. */
1478
+ async listStoreConversations(storeId, params = {}, options) {
1479
+ validateLimit(params);
1480
+ const value = await this.client.get(`/notify/v2/inboxes/stores/${encode(storeId)}/conversations`, query(params), options);
1481
+ return unwrap(value, isPage);
1482
+ }
1483
+ /** Get one authorized buyer/store conversation. */
1484
+ async getConversation(perspective, conversationId, options) {
1485
+ const value = await this.client.get(`${perspectiveBase(perspective)}/conversations/${encode(conversationId)}`, void 0, options);
1486
+ const data = unwrap(value, isRecord2);
1487
+ if (!isIdentified(data.conversation)) throw new APIError(502, { error: "invalid_response" });
1488
+ return data.conversation;
1489
+ }
1490
+ /** Resolve a General, Listing, or Order thread idempotently. */
1491
+ async resolveThread(perspective, input, options) {
1492
+ const value = await this.client.post(`${perspectiveBase(perspective)}/threads/resolve`, input, idempotentOptions(options));
1493
+ return unwrap(value, (data) => isRecord2(data) && isIdentified(data.thread) && isIdentified(data.conversation));
1494
+ }
1495
+ /** List contextual thread cards for one conversation. */
1496
+ async listThreads(perspective, conversationId, params = {}, options) {
1497
+ validateLimit(params);
1498
+ const value = await this.client.get(
1499
+ `${perspectiveBase(perspective)}/conversations/${encode(conversationId)}/threads`,
1500
+ query(params),
1501
+ options
1502
+ );
1503
+ return unwrap(value, isPage);
1504
+ }
1505
+ /** Get one contextual thread card and its conversation. */
1506
+ async getConversationThread(perspective, threadId, options) {
1507
+ const value = await this.client.get(`${perspectiveBase(perspective)}/threads/${encode(threadId)}`, void 0, options);
1508
+ return unwrap(value, (data) => isRecord2(data) && isIdentified(data.thread) && isIdentified(data.conversation));
1509
+ }
1510
+ /** Update shared support status for a Listing or Order thread. */
1511
+ async updateThreadStatus(perspective, threadId, status, options) {
1512
+ const value = await this.client.patch(`${perspectiveBase(perspective)}/threads/${encode(threadId)}/status`, { status }, options);
1513
+ const data = unwrap(value, isRecord2);
1514
+ if (!isIdentified(data.thread)) throw new APIError(502, { error: "invalid_response" });
1515
+ return data.thread;
1516
+ }
1517
+ /** Update perspective-private thread preferences. */
1518
+ async updateThreadPreferences(perspective, threadId, preferences, options) {
1519
+ const value = await this.client.patch(`${perspectiveBase(perspective)}/threads/${encode(threadId)}/preferences`, preferences, options);
1520
+ const data = unwrap(value, isRecord2);
1521
+ if (!isIdentified(data.thread)) throw new APIError(502, { error: "invalid_response" });
1522
+ return data.thread;
1523
+ }
1524
+ /** Update perspective-private conversation preferences. */
1525
+ async updateConversationPreferences(perspective, conversationId, preferences, options) {
1526
+ const value = await this.client.patch(`${perspectiveBase(perspective)}/conversations/${encode(conversationId)}/preferences`, preferences, options);
1527
+ const data = unwrap(value, isRecord2);
1528
+ if (!isIdentified(data.conversation)) throw new APIError(502, { error: "invalid_response" });
1529
+ return data.conversation;
1530
+ }
1531
+ /** List the newest, older, or catch-up timeline window for one thread. */
1532
+ async listThreadMessages(perspective, threadId, params = {}, options) {
1533
+ validateLimit(params);
1534
+ const value = await this.client.get(`${perspectiveBase(perspective)}/threads/${encode(threadId)}/messages`, query(params), options);
1535
+ return unwrap(value, isPage);
1536
+ }
1537
+ /** Send one idempotent message without ever replacing the caller-owned ID. */
1538
+ async sendThreadMessage(perspective, threadId, input, options) {
1539
+ validateSendInput(input);
1540
+ const { client_message_id, ...body } = input;
1541
+ const value = await this.client.post(
1542
+ `${perspectiveBase(perspective)}/threads/${encode(threadId)}/messages`,
1543
+ body,
1544
+ { ...options, headers: { ...options?.headers, "Idempotency-Key": client_message_id } }
1545
+ );
1546
+ return unwrap(value, (data) => isRecord2(data) && isIdentified(data.message));
1547
+ }
1548
+ /** Advance the selected perspective read cursor to one visible timeline item. */
1549
+ async advanceThreadReadCursor(perspective, threadId, timelineItemId, options) {
1550
+ const value = await this.client.put(
1551
+ `${perspectiveBase(perspective)}/threads/${encode(threadId)}/read-cursor`,
1552
+ { timeline_item_id: timelineItemId },
1553
+ options
1554
+ );
1555
+ return unwrap(value, (data) => isRecord2(data) && typeof data.last_read_sequence === "number");
1556
+ }
1557
+ /** List read-only exact-participant legacy archives. */
1558
+ async listLegacyMessageArchives(params = {}, options) {
1559
+ validateLimit(params);
1560
+ const value = await this.client.get("/notify/v2/legacy-messages", query(params), options);
1561
+ return unwrap(value, isPage);
1562
+ }
1563
+ /** List messages in one read-only legacy archive. */
1564
+ async listLegacyMessages(legacyThreadId, params = {}, options) {
1565
+ validateLimit(params);
1566
+ const value = await this.client.get(`/notify/v2/legacy-messages/${encode(legacyThreadId)}`, query(params), options);
1567
+ return unwrap(value, isPage);
1568
+ }
1569
+ /** Resolve a temporary partner/order link without guessing between stores. */
1570
+ async resolveLegacyLink(input, options) {
1571
+ const value = await this.client.post("/notify/v2/inboxes/buyer/legacy-links/resolve", input, options);
1572
+ return unwrap(value, (data) => isRecord2(data) && typeof data.result === "string");
1573
+ }
1574
+ /** Authorize a private attachment upload for one thread. */
1575
+ async createThreadAttachmentUpload(perspective, threadId, input, options) {
1576
+ validateAttachmentInput(input);
1577
+ const value = await this.client.post(`${perspectiveBase(perspective)}/threads/${encode(threadId)}/attachment-uploads`, input, options);
1578
+ return unwrap(value, (data) => isRecord2(data) && data.state === "authorized" && typeof data.upload_url === "string");
1579
+ }
1580
+ /** Upload exact bytes through a one-use application ticket. */
1581
+ async uploadThreadAttachment(authorization, body, options) {
1582
+ if (uploadBodySize(body) !== authorization.accepted.size_bytes) {
1583
+ throw new RangeError("Attachment body size does not match its authorization.");
1584
+ }
1585
+ const response = await fetch(authorization.upload_url, {
1586
+ method: "PUT",
1587
+ headers: { ...options?.headers, "Content-Type": authorization.accepted.content_type },
1588
+ body,
1589
+ signal: options?.signal
1590
+ });
1591
+ return parseDirectResponse(response);
1592
+ }
1593
+ /** Get current status for an upload created by the same perspective. */
1594
+ async getThreadAttachmentUpload(perspective, threadId, uploadId, options) {
1595
+ const value = await this.client.get(
1596
+ `${perspectiveBase(perspective)}/threads/${encode(threadId)}/attachment-uploads/${encode(uploadId)}`,
1597
+ void 0,
1598
+ options
1599
+ );
1600
+ return unwrap(value, (data) => isRecord2(data) && typeof data.state === "string");
1601
+ }
1602
+ /** Create a short-lived mediated download for one committed attachment. */
1603
+ async createThreadAttachmentDownload(perspective, threadId, attachmentId, options) {
1604
+ const value = await this.client.post(
1605
+ `${perspectiveBase(perspective)}/threads/${encode(threadId)}/attachments/${encode(attachmentId)}/download`,
1606
+ void 0,
1607
+ options
1608
+ );
1609
+ return unwrap(value, (data) => isRecord2(data) && typeof data.url === "string");
1610
+ }
1611
+ /** Create an inert managed realtime connection. Register listeners before start(). */
1612
+ createMessagesRealtimeConnection(options) {
1613
+ return new ManagedMessagesRealtimeConnection(this.client, options);
1614
+ }
1615
+ /** Create and start a managed realtime connection. */
1616
+ async connectMessagesRealtime(options) {
1617
+ const connection = this.createMessagesRealtimeConnection(options);
1618
+ await connection.start();
1619
+ return connection;
1620
+ }
921
1621
  };
922
1622
 
923
1623
  // src/modules/notifications.ts
@@ -1826,6 +2526,8 @@ exports.ORDER_STATUSES = ORDER_STATUSES;
1826
2526
  exports.OrdersModule = OrdersModule;
1827
2527
  exports.PAYMENT_STATUSES = PAYMENT_STATUSES;
1828
2528
  exports.PayModule = PayModule;
2529
+ exports.RealtimeProtocolError = RealtimeProtocolError;
2530
+ exports.RealtimeUnavailableError = RealtimeUnavailableError;
1829
2531
  exports.ReportsModule = ReportsModule;
1830
2532
  exports.ReviewsModule = ReviewsModule;
1831
2533
  exports.SearchModule = SearchModule;
@@ -1838,6 +2540,7 @@ exports.WholesaleModule = WholesaleModule;
1838
2540
  exports.WishlistsModule = WishlistsModule;
1839
2541
  exports.baleShareUrl = baleShareUrl;
1840
2542
  exports.createTokenStore = createTokenStore;
2543
+ exports.isMessagingAPIError = isMessagingAPIError;
1841
2544
  exports.listingShareUrl = listingShareUrl;
1842
2545
  exports.storeShareUrl = storeShareUrl;
1843
2546
  //# sourceMappingURL=index.js.map