@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/README.md +21 -1
- package/dist/index.d.mts +906 -22
- package/dist/index.d.ts +906 -22
- package/dist/index.js +791 -46
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +785 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
343
|
+
let decoded;
|
|
257
344
|
try {
|
|
258
|
-
|
|
345
|
+
decoded = await response.json();
|
|
259
346
|
} catch {
|
|
260
|
-
|
|
347
|
+
decoded = null;
|
|
261
348
|
}
|
|
262
|
-
throw new APIError(response.status,
|
|
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
|
-
|
|
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."));
|
|
284
382
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
this.config.onTokenRefresh({ token: data.access_token, refreshToken: data.refresh_token });
|
|
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." });
|
|
291
388
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
389
|
+
if (!isTokenResponse(decoded)) {
|
|
390
|
+
throw new APIError(response.status, { error: "refresh_response_invalid", message: "Token refresh returned an invalid response." });
|
|
391
|
+
}
|
|
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
|
|
@@ -1678,6 +2378,44 @@ var DISPUTE_STATUSES = ["open", "seller_responded", "under_review", "resolved_re
|
|
|
1678
2378
|
// src/types/wholesale.ts
|
|
1679
2379
|
var BALE_ORDER_STATUSES = ["pending_payment", "paid", "confirmed", "in_transit", "delivered", "completed", "cancelled"];
|
|
1680
2380
|
|
|
2381
|
+
// src/share.ts
|
|
2382
|
+
var DEFAULT_SHARE_ORIGIN = "https://go.mitumba.africa";
|
|
2383
|
+
var ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
2384
|
+
var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2385
|
+
function resolveOrigin(origin) {
|
|
2386
|
+
if (origin === void 0) return DEFAULT_SHARE_ORIGIN;
|
|
2387
|
+
let url;
|
|
2388
|
+
try {
|
|
2389
|
+
url = new URL(origin);
|
|
2390
|
+
} catch {
|
|
2391
|
+
throw new TypeError(`Invalid share origin: ${origin}`);
|
|
2392
|
+
}
|
|
2393
|
+
const local = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
2394
|
+
if (url.protocol !== "https:" && !local || url.username || url.password) {
|
|
2395
|
+
throw new TypeError(`Share origin must be https:// (or http://localhost): ${origin}`);
|
|
2396
|
+
}
|
|
2397
|
+
if (url.pathname !== "/" && url.pathname !== "" || url.search || url.hash) {
|
|
2398
|
+
throw new TypeError(`Share origin must not include a path, query or fragment: ${origin}`);
|
|
2399
|
+
}
|
|
2400
|
+
return url.origin;
|
|
2401
|
+
}
|
|
2402
|
+
function identifier(kind, value) {
|
|
2403
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
2404
|
+
if (!ID_PATTERN.test(normalized)) throw new TypeError(`Invalid ${kind} ID: ${String(value)}`);
|
|
2405
|
+
return normalized;
|
|
2406
|
+
}
|
|
2407
|
+
function listingShareUrl(listingId, options = {}) {
|
|
2408
|
+
return `${resolveOrigin(options.origin)}/l/${encodeURIComponent(identifier("listing", listingId))}`;
|
|
2409
|
+
}
|
|
2410
|
+
function storeShareUrl(storeSlug, options = {}) {
|
|
2411
|
+
const slug = typeof storeSlug === "string" ? storeSlug.trim().toLowerCase() : "";
|
|
2412
|
+
if (slug.length > 64 || !SLUG_PATTERN.test(slug)) throw new TypeError(`Invalid store slug: ${String(storeSlug)}`);
|
|
2413
|
+
return `${resolveOrigin(options.origin)}/s/${encodeURIComponent(slug)}`;
|
|
2414
|
+
}
|
|
2415
|
+
function baleShareUrl(baleId, options = {}) {
|
|
2416
|
+
return `${resolveOrigin(options.origin)}/b/${encodeURIComponent(identifier("bale", baleId))}`;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
1681
2419
|
// src/index.ts
|
|
1682
2420
|
var MitumbaClient = class {
|
|
1683
2421
|
api;
|
|
@@ -1771,6 +2509,7 @@ exports.BALE_ORDER_STATUSES = BALE_ORDER_STATUSES;
|
|
|
1771
2509
|
exports.BusinessesModule = BusinessesModule;
|
|
1772
2510
|
exports.CONDITIONS = CONDITIONS;
|
|
1773
2511
|
exports.CartModule = CartModule;
|
|
2512
|
+
exports.DEFAULT_SHARE_ORIGIN = DEFAULT_SHARE_ORIGIN;
|
|
1774
2513
|
exports.DESIRED_RESOLUTIONS = DESIRED_RESOLUTIONS;
|
|
1775
2514
|
exports.DISPUTE_REASONS = DISPUTE_REASONS;
|
|
1776
2515
|
exports.DISPUTE_STATUSES = DISPUTE_STATUSES;
|
|
@@ -1787,6 +2526,8 @@ exports.ORDER_STATUSES = ORDER_STATUSES;
|
|
|
1787
2526
|
exports.OrdersModule = OrdersModule;
|
|
1788
2527
|
exports.PAYMENT_STATUSES = PAYMENT_STATUSES;
|
|
1789
2528
|
exports.PayModule = PayModule;
|
|
2529
|
+
exports.RealtimeProtocolError = RealtimeProtocolError;
|
|
2530
|
+
exports.RealtimeUnavailableError = RealtimeUnavailableError;
|
|
1790
2531
|
exports.ReportsModule = ReportsModule;
|
|
1791
2532
|
exports.ReviewsModule = ReviewsModule;
|
|
1792
2533
|
exports.SearchModule = SearchModule;
|
|
@@ -1797,6 +2538,10 @@ exports.UploadsModule = UploadsModule;
|
|
|
1797
2538
|
exports.VaziModule = VaziModule;
|
|
1798
2539
|
exports.WholesaleModule = WholesaleModule;
|
|
1799
2540
|
exports.WishlistsModule = WishlistsModule;
|
|
2541
|
+
exports.baleShareUrl = baleShareUrl;
|
|
1800
2542
|
exports.createTokenStore = createTokenStore;
|
|
2543
|
+
exports.isMessagingAPIError = isMessagingAPIError;
|
|
2544
|
+
exports.listingShareUrl = listingShareUrl;
|
|
2545
|
+
exports.storeShareUrl = storeShareUrl;
|
|
1801
2546
|
//# sourceMappingURL=index.js.map
|
|
1802
2547
|
//# sourceMappingURL=index.js.map
|