@mitumba/sdk 1.31.0 → 1.32.1
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 +911 -22
- package/dist/index.d.ts +911 -22
- package/dist/index.js +749 -46
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +747 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
341
|
+
let decoded;
|
|
255
342
|
try {
|
|
256
|
-
|
|
343
|
+
decoded = await response.json();
|
|
257
344
|
} catch {
|
|
258
|
-
|
|
345
|
+
decoded = null;
|
|
259
346
|
}
|
|
260
|
-
throw new APIError(response.status,
|
|
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
|
-
|
|
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."));
|
|
380
|
+
}
|
|
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." });
|
|
282
386
|
}
|
|
283
|
-
|
|
284
|
-
|
|
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 });
|
|
387
|
+
if (!isTokenResponse(decoded)) {
|
|
388
|
+
throw new APIError(response.status, { error: "refresh_response_invalid", message: "Token refresh returned an invalid response." });
|
|
289
389
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
@@ -1799,6 +2499,6 @@ var MitumbaClient = class {
|
|
|
1799
2499
|
}
|
|
1800
2500
|
};
|
|
1801
2501
|
|
|
1802
|
-
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, ReportsModule, ReviewsModule, SearchModule, SettingsModule, StoresModule, TWO_FACTOR_METHOD_TYPES, UploadsModule, VaziModule, WholesaleModule, WishlistsModule, baleShareUrl, createTokenStore, listingShareUrl, storeShareUrl };
|
|
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 };
|
|
1803
2503
|
//# sourceMappingURL=index.mjs.map
|
|
1804
2504
|
//# sourceMappingURL=index.mjs.map
|