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