@scalemule/sdk 0.0.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/LICENSE +21 -0
- package/README.md +424 -0
- package/dist/chunk-3FTGBRLU.mjs +158 -0
- package/dist/index.d.mts +4014 -0
- package/dist/index.d.ts +4014 -0
- package/dist/index.js +4666 -0
- package/dist/index.mjs +4309 -0
- package/dist/upload-compression-CWKEDQYS.mjs +108 -0
- package/dist/upload-resume-RXLHBH5E.mjs +6 -0
- package/package.json +90 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4666 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
|
|
23
|
+
// src/services/upload-resume.ts
|
|
24
|
+
var upload_resume_exports = {};
|
|
25
|
+
__export(upload_resume_exports, {
|
|
26
|
+
UploadResumeStore: () => UploadResumeStore
|
|
27
|
+
});
|
|
28
|
+
var DB_NAME, STORE_NAME, DB_VERSION, MAX_AGE_MS, UploadResumeStore;
|
|
29
|
+
var init_upload_resume = __esm({
|
|
30
|
+
"src/services/upload-resume.ts"() {
|
|
31
|
+
"use strict";
|
|
32
|
+
DB_NAME = "sm_upload_sessions_v1";
|
|
33
|
+
STORE_NAME = "sessions";
|
|
34
|
+
DB_VERSION = 1;
|
|
35
|
+
MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
36
|
+
UploadResumeStore = class {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.db = null;
|
|
39
|
+
}
|
|
40
|
+
/** Generate a deterministic resume key from upload identity */
|
|
41
|
+
static async generateResumeKey(appId, userId, filename, size, lastModified) {
|
|
42
|
+
const raw = `${appId}:${userId}:${filename}:${size}:${lastModified ?? 0}`;
|
|
43
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
44
|
+
const buffer = new TextEncoder().encode(raw);
|
|
45
|
+
const hash2 = await crypto.subtle.digest("SHA-256", buffer);
|
|
46
|
+
return Array.from(new Uint8Array(hash2)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
47
|
+
}
|
|
48
|
+
let hash = 0;
|
|
49
|
+
for (let i = 0; i < raw.length; i++) {
|
|
50
|
+
const chr = raw.charCodeAt(i);
|
|
51
|
+
hash = (hash << 5) - hash + chr;
|
|
52
|
+
hash |= 0;
|
|
53
|
+
}
|
|
54
|
+
return `fallback_${Math.abs(hash).toString(36)}`;
|
|
55
|
+
}
|
|
56
|
+
/** Open the IndexedDB store. No-ops if IndexedDB is unavailable. */
|
|
57
|
+
async open() {
|
|
58
|
+
if (typeof indexedDB === "undefined") return;
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
61
|
+
request.onupgradeneeded = () => {
|
|
62
|
+
const db = request.result;
|
|
63
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
64
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "key" });
|
|
65
|
+
store.createIndex("updated_at", "updated_at");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
request.onsuccess = () => {
|
|
69
|
+
this.db = request.result;
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
request.onerror = () => {
|
|
73
|
+
reject(request.error);
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Get a resume session by key. Returns null if not found or stale. */
|
|
78
|
+
async get(key) {
|
|
79
|
+
if (!this.db) return null;
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const tx = this.db.transaction(STORE_NAME, "readonly");
|
|
82
|
+
const store = tx.objectStore(STORE_NAME);
|
|
83
|
+
const request = store.get(key);
|
|
84
|
+
request.onsuccess = () => {
|
|
85
|
+
const entry = request.result;
|
|
86
|
+
if (!entry) {
|
|
87
|
+
resolve(null);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (Date.now() - entry.updated_at > MAX_AGE_MS) {
|
|
91
|
+
this.remove(key).catch(() => {
|
|
92
|
+
});
|
|
93
|
+
resolve(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
resolve(entry.session);
|
|
97
|
+
};
|
|
98
|
+
request.onerror = () => resolve(null);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Save a new resume session. */
|
|
102
|
+
async save(key, session) {
|
|
103
|
+
if (!this.db) return;
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
106
|
+
const store = tx.objectStore(STORE_NAME);
|
|
107
|
+
const entry = {
|
|
108
|
+
key,
|
|
109
|
+
session: { ...session, created_at: Date.now() },
|
|
110
|
+
updated_at: Date.now()
|
|
111
|
+
};
|
|
112
|
+
const request = store.put(entry);
|
|
113
|
+
request.onsuccess = () => resolve();
|
|
114
|
+
request.onerror = () => reject(request.error);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Update a single completed part in an existing session. */
|
|
118
|
+
async updatePart(key, partNumber, etag) {
|
|
119
|
+
if (!this.db) return;
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
122
|
+
const store = tx.objectStore(STORE_NAME);
|
|
123
|
+
const getRequest = store.get(key);
|
|
124
|
+
getRequest.onsuccess = () => {
|
|
125
|
+
const entry = getRequest.result;
|
|
126
|
+
if (!entry) {
|
|
127
|
+
resolve();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const existing = entry.session.completed_parts.find((p) => p.part_number === partNumber);
|
|
131
|
+
if (!existing) {
|
|
132
|
+
entry.session.completed_parts.push({ part_number: partNumber, etag });
|
|
133
|
+
}
|
|
134
|
+
entry.updated_at = Date.now();
|
|
135
|
+
const putRequest = store.put(entry);
|
|
136
|
+
putRequest.onsuccess = () => resolve();
|
|
137
|
+
putRequest.onerror = () => resolve();
|
|
138
|
+
};
|
|
139
|
+
getRequest.onerror = () => resolve();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Remove a resume session (e.g., after successful completion). */
|
|
143
|
+
async remove(key) {
|
|
144
|
+
if (!this.db) return;
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
147
|
+
const store = tx.objectStore(STORE_NAME);
|
|
148
|
+
const request = store.delete(key);
|
|
149
|
+
request.onsuccess = () => resolve();
|
|
150
|
+
request.onerror = () => resolve();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** Purge all stale entries (older than MAX_AGE_MS). */
|
|
154
|
+
async purgeStale() {
|
|
155
|
+
if (!this.db) return 0;
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const cutoff = Date.now() - MAX_AGE_MS;
|
|
158
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
159
|
+
const store = tx.objectStore(STORE_NAME);
|
|
160
|
+
const index = store.index("updated_at");
|
|
161
|
+
const range = IDBKeyRange.upperBound(cutoff);
|
|
162
|
+
const request = index.openCursor(range);
|
|
163
|
+
let count = 0;
|
|
164
|
+
request.onsuccess = () => {
|
|
165
|
+
const cursor = request.result;
|
|
166
|
+
if (cursor) {
|
|
167
|
+
cursor.delete();
|
|
168
|
+
count++;
|
|
169
|
+
cursor.continue();
|
|
170
|
+
} else {
|
|
171
|
+
resolve(count);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
request.onerror = () => resolve(0);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/** Close the database connection. */
|
|
178
|
+
close() {
|
|
179
|
+
if (this.db) {
|
|
180
|
+
this.db.close();
|
|
181
|
+
this.db = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// src/services/upload-compression.ts
|
|
189
|
+
var upload_compression_exports = {};
|
|
190
|
+
__export(upload_compression_exports, {
|
|
191
|
+
maybeCompressImage: () => maybeCompressImage
|
|
192
|
+
});
|
|
193
|
+
async function maybeCompressImage(file, userConfig, sessionId, telemetry) {
|
|
194
|
+
const type = file.type?.toLowerCase() || "";
|
|
195
|
+
if (!type.startsWith("image/")) return null;
|
|
196
|
+
if (SKIP_TYPES.has(type)) {
|
|
197
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "format", type });
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (!COMPRESSIBLE_TYPES.has(type)) {
|
|
201
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "unsupported_type", type });
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
if (file.size < MIN_COMPRESS_SIZE) {
|
|
205
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "too_small", size: file.size });
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const networkType = getNetworkEffectiveType();
|
|
209
|
+
const defaultProfile = { maxWidth: 3840, maxHeight: 3840, quality: 0.85, maxSizeMB: 5 };
|
|
210
|
+
const networkProfile = NETWORK_PROFILES[networkType] ?? defaultProfile;
|
|
211
|
+
const config = {
|
|
212
|
+
maxWidth: userConfig?.maxWidth ?? networkProfile.maxWidth,
|
|
213
|
+
maxHeight: userConfig?.maxHeight ?? networkProfile.maxHeight,
|
|
214
|
+
quality: userConfig?.quality ?? networkProfile.quality,
|
|
215
|
+
maxSizeMB: userConfig?.maxSizeMB ?? networkProfile.maxSizeMB
|
|
216
|
+
};
|
|
217
|
+
telemetry?.emit(sessionId, "upload.compression.started", {
|
|
218
|
+
original_size: file.size,
|
|
219
|
+
network: networkType,
|
|
220
|
+
target_quality: config.quality
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
const imageCompression = await loadImageCompression();
|
|
224
|
+
if (!imageCompression) {
|
|
225
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "library_unavailable" });
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const compressed = await imageCompression(file, {
|
|
229
|
+
maxSizeMB: config.maxSizeMB,
|
|
230
|
+
maxWidthOrHeight: Math.max(config.maxWidth, config.maxHeight),
|
|
231
|
+
initialQuality: config.quality,
|
|
232
|
+
useWebWorker: true,
|
|
233
|
+
fileType: type === "image/png" ? "image/webp" : void 0
|
|
234
|
+
});
|
|
235
|
+
if (compressed.size >= file.size * 0.95) {
|
|
236
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", {
|
|
237
|
+
reason: "no_size_reduction",
|
|
238
|
+
original_size: file.size,
|
|
239
|
+
compressed_size: compressed.size
|
|
240
|
+
});
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
telemetry?.emit(sessionId, "upload.compression.completed", {
|
|
244
|
+
original_size: file.size,
|
|
245
|
+
compressed_size: compressed.size,
|
|
246
|
+
ratio: (compressed.size / file.size).toFixed(2)
|
|
247
|
+
});
|
|
248
|
+
return compressed;
|
|
249
|
+
} catch (err) {
|
|
250
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", {
|
|
251
|
+
reason: "error",
|
|
252
|
+
error: err instanceof Error ? err.message : "Unknown compression error"
|
|
253
|
+
});
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function loadImageCompression() {
|
|
258
|
+
if (cachedImport === false) return null;
|
|
259
|
+
if (cachedImport) return cachedImport;
|
|
260
|
+
try {
|
|
261
|
+
const mod = await Function('return import("browser-image-compression")')();
|
|
262
|
+
cachedImport = mod.default || mod;
|
|
263
|
+
return cachedImport;
|
|
264
|
+
} catch {
|
|
265
|
+
cachedImport = false;
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function getNetworkEffectiveType() {
|
|
270
|
+
if (typeof navigator !== "undefined" && "connection" in navigator) {
|
|
271
|
+
const conn = navigator.connection;
|
|
272
|
+
return conn?.effectiveType || "4g";
|
|
273
|
+
}
|
|
274
|
+
return "4g";
|
|
275
|
+
}
|
|
276
|
+
var MIN_COMPRESS_SIZE, COMPRESSIBLE_TYPES, SKIP_TYPES, NETWORK_PROFILES, cachedImport;
|
|
277
|
+
var init_upload_compression = __esm({
|
|
278
|
+
"src/services/upload-compression.ts"() {
|
|
279
|
+
"use strict";
|
|
280
|
+
MIN_COMPRESS_SIZE = 100 * 1024;
|
|
281
|
+
COMPRESSIBLE_TYPES = /* @__PURE__ */ new Set([
|
|
282
|
+
"image/jpeg",
|
|
283
|
+
"image/jpg",
|
|
284
|
+
"image/png",
|
|
285
|
+
"image/bmp",
|
|
286
|
+
"image/tiff"
|
|
287
|
+
]);
|
|
288
|
+
SKIP_TYPES = /* @__PURE__ */ new Set([
|
|
289
|
+
"image/gif",
|
|
290
|
+
"image/svg+xml",
|
|
291
|
+
"image/webp",
|
|
292
|
+
"image/avif"
|
|
293
|
+
]);
|
|
294
|
+
NETWORK_PROFILES = {
|
|
295
|
+
"slow-2g": { maxWidth: 1280, maxHeight: 1280, quality: 0.6, maxSizeMB: 0.5 },
|
|
296
|
+
"2g": { maxWidth: 1600, maxHeight: 1600, quality: 0.65, maxSizeMB: 1 },
|
|
297
|
+
"3g": { maxWidth: 2048, maxHeight: 2048, quality: 0.75, maxSizeMB: 2 },
|
|
298
|
+
"4g": { maxWidth: 3840, maxHeight: 3840, quality: 0.85, maxSizeMB: 5 }
|
|
299
|
+
};
|
|
300
|
+
cachedImport = null;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// src/index.ts
|
|
305
|
+
var index_exports = {};
|
|
306
|
+
__export(index_exports, {
|
|
307
|
+
AccountsService: () => AccountsService,
|
|
308
|
+
AnalyticsService: () => AnalyticsService,
|
|
309
|
+
AuthService: () => AuthService,
|
|
310
|
+
BillingService: () => BillingService,
|
|
311
|
+
CacheService: () => CacheService,
|
|
312
|
+
CatalogService: () => CatalogService,
|
|
313
|
+
ChatService: () => ChatService,
|
|
314
|
+
CommunicationService: () => CommunicationService,
|
|
315
|
+
ComplianceService: () => ComplianceService,
|
|
316
|
+
DataService: () => DataService,
|
|
317
|
+
ErrorCodes: () => ErrorCodes,
|
|
318
|
+
EventsService: () => EventsService,
|
|
319
|
+
FlagContentService: () => FlagContentService,
|
|
320
|
+
FunctionsService: () => FunctionsService,
|
|
321
|
+
GraphService: () => GraphService,
|
|
322
|
+
IdentityService: () => IdentityService,
|
|
323
|
+
LeaderboardService: () => LeaderboardService,
|
|
324
|
+
ListingsService: () => ListingsService,
|
|
325
|
+
LoggerService: () => LoggerService,
|
|
326
|
+
OrchestratorService: () => OrchestratorService,
|
|
327
|
+
PHONE_COUNTRIES: () => import_phone.PHONE_COUNTRIES,
|
|
328
|
+
PHOTO_BREAKPOINTS: () => PHOTO_BREAKPOINTS,
|
|
329
|
+
PermissionsService: () => PermissionsService,
|
|
330
|
+
PhotoService: () => PhotoService,
|
|
331
|
+
QueueService: () => QueueService,
|
|
332
|
+
RealtimeService: () => RealtimeService,
|
|
333
|
+
ScaleMule: () => ScaleMule,
|
|
334
|
+
ScaleMuleClient: () => ScaleMuleClient,
|
|
335
|
+
SchedulerService: () => SchedulerService,
|
|
336
|
+
SearchService: () => SearchService,
|
|
337
|
+
ServiceModule: () => ServiceModule,
|
|
338
|
+
SocialService: () => SocialService,
|
|
339
|
+
StorageService: () => StorageService,
|
|
340
|
+
TeamsService: () => TeamsService,
|
|
341
|
+
UploadResumeStore: () => UploadResumeStore,
|
|
342
|
+
UploadTelemetry: () => UploadTelemetry,
|
|
343
|
+
VideoService: () => VideoService,
|
|
344
|
+
WebhooksService: () => WebhooksService,
|
|
345
|
+
buildClientContextHeaders: () => buildClientContextHeaders,
|
|
346
|
+
calculateTotalParts: () => calculateTotalParts,
|
|
347
|
+
canPerform: () => canPerform,
|
|
348
|
+
composePhoneNumber: () => import_phone.composePhoneNumber,
|
|
349
|
+
countryFlag: () => import_phone.countryFlag,
|
|
350
|
+
createUploadPlan: () => createUploadPlan,
|
|
351
|
+
default: () => index_default,
|
|
352
|
+
detectCountryFromE164: () => import_phone.detectCountryFromE164,
|
|
353
|
+
detectNetworkClass: () => detectNetworkClass,
|
|
354
|
+
extractClientContext: () => extractClientContext,
|
|
355
|
+
findPhoneCountryByCode: () => import_phone.findPhoneCountryByCode,
|
|
356
|
+
findPhoneCountryByDialCode: () => import_phone.findPhoneCountryByDialCode,
|
|
357
|
+
generateUploadSessionId: () => generateUploadSessionId,
|
|
358
|
+
getMeasuredBandwidthMbps: () => getMeasuredBandwidthMbps,
|
|
359
|
+
getPartRange: () => getPartRange,
|
|
360
|
+
hasMinRoleLevel: () => hasMinRoleLevel,
|
|
361
|
+
isValidE164Phone: () => import_phone.isValidE164Phone,
|
|
362
|
+
normalizeAndValidatePhone: () => import_phone.normalizeAndValidatePhone,
|
|
363
|
+
normalizePhoneNumber: () => import_phone.normalizePhoneNumber,
|
|
364
|
+
resolveStrategy: () => resolveStrategy,
|
|
365
|
+
validateIP: () => validateIP
|
|
366
|
+
});
|
|
367
|
+
module.exports = __toCommonJS(index_exports);
|
|
368
|
+
|
|
369
|
+
// src/types.ts
|
|
370
|
+
var ErrorCodes = {
|
|
371
|
+
// Auth & access
|
|
372
|
+
UNAUTHORIZED: "unauthorized",
|
|
373
|
+
FORBIDDEN: "forbidden",
|
|
374
|
+
// Resources
|
|
375
|
+
NOT_FOUND: "not_found",
|
|
376
|
+
CONFLICT: "conflict",
|
|
377
|
+
// Input
|
|
378
|
+
VALIDATION_ERROR: "validation_error",
|
|
379
|
+
// Rate limiting & quotas
|
|
380
|
+
RATE_LIMITED: "rate_limited",
|
|
381
|
+
QUOTA_EXCEEDED: "quota_exceeded",
|
|
382
|
+
// Server
|
|
383
|
+
INTERNAL_ERROR: "internal_error",
|
|
384
|
+
// Network (SDK-generated, not from server)
|
|
385
|
+
NETWORK_ERROR: "network_error",
|
|
386
|
+
TIMEOUT: "timeout",
|
|
387
|
+
ABORTED: "aborted",
|
|
388
|
+
// Storage-specific
|
|
389
|
+
FILE_SCANNING: "file_scanning",
|
|
390
|
+
FILE_THREAT: "file_threat",
|
|
391
|
+
FILE_QUARANTINED: "file_quarantined",
|
|
392
|
+
// Upload
|
|
393
|
+
UPLOAD_ERROR: "upload_error"
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// src/context.ts
|
|
397
|
+
function validateIP(ip) {
|
|
398
|
+
if (!ip) return void 0;
|
|
399
|
+
const trimmed = ip.trim();
|
|
400
|
+
if (!trimmed) return void 0;
|
|
401
|
+
const ipv4 = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
|
|
402
|
+
const ipv6 = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,7}:$|^(?:[0-9a-fA-F]{1,4}:){0,6}::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}$/;
|
|
403
|
+
const mapped = /^::ffff:(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/i;
|
|
404
|
+
if (ipv4.test(trimmed) || ipv6.test(trimmed) || mapped.test(trimmed)) {
|
|
405
|
+
return trimmed;
|
|
406
|
+
}
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
function extractClientContext(request) {
|
|
410
|
+
const h = request.headers;
|
|
411
|
+
const getHeader = (name) => {
|
|
412
|
+
const v = h[name] ?? h[name.toLowerCase()];
|
|
413
|
+
return Array.isArray(v) ? v[0] : v;
|
|
414
|
+
};
|
|
415
|
+
let ip;
|
|
416
|
+
const cfIp = getHeader("cf-connecting-ip");
|
|
417
|
+
if (cfIp) ip = validateIP(cfIp);
|
|
418
|
+
if (!ip) {
|
|
419
|
+
const doIp = getHeader("do-connecting-ip");
|
|
420
|
+
if (doIp) ip = validateIP(doIp);
|
|
421
|
+
}
|
|
422
|
+
if (!ip) {
|
|
423
|
+
const realIp = getHeader("x-real-ip");
|
|
424
|
+
if (realIp) ip = validateIP(realIp);
|
|
425
|
+
}
|
|
426
|
+
if (!ip) {
|
|
427
|
+
const xff = getHeader("x-forwarded-for");
|
|
428
|
+
if (xff) ip = validateIP(xff.split(",")[0]?.trim());
|
|
429
|
+
}
|
|
430
|
+
if (!ip) {
|
|
431
|
+
const vercel = getHeader("x-vercel-forwarded-for");
|
|
432
|
+
if (vercel) ip = validateIP(vercel.split(",")[0]?.trim());
|
|
433
|
+
}
|
|
434
|
+
if (!ip) {
|
|
435
|
+
const akamai = getHeader("true-client-ip");
|
|
436
|
+
if (akamai) ip = validateIP(akamai);
|
|
437
|
+
}
|
|
438
|
+
if (!ip && request.socket?.remoteAddress) {
|
|
439
|
+
ip = validateIP(request.socket.remoteAddress);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
ip,
|
|
443
|
+
userAgent: getHeader("user-agent") || void 0,
|
|
444
|
+
deviceFingerprint: getHeader("x-device-fingerprint") || void 0,
|
|
445
|
+
referrer: getHeader("referer") || void 0
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function buildClientContextHeaders(context) {
|
|
449
|
+
if (!context) return {};
|
|
450
|
+
const headers = {};
|
|
451
|
+
if (context.ip) {
|
|
452
|
+
headers["x-sm-forwarded-client-ip"] = context.ip;
|
|
453
|
+
headers["X-Client-IP"] = context.ip;
|
|
454
|
+
}
|
|
455
|
+
if (context.userAgent) headers["X-Client-User-Agent"] = context.userAgent;
|
|
456
|
+
if (context.deviceFingerprint) headers["X-Client-Device-Fingerprint"] = context.deviceFingerprint;
|
|
457
|
+
if (context.referrer) headers["X-Client-Referrer"] = context.referrer;
|
|
458
|
+
return headers;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/client.ts
|
|
462
|
+
var SDK_VERSION = "0.0.1";
|
|
463
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
464
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
465
|
+
var DEFAULT_BACKOFF_MS = 300;
|
|
466
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
467
|
+
var SESSION_STORAGE_KEY = "scalemule_session";
|
|
468
|
+
var USER_ID_STORAGE_KEY = "scalemule_user_id";
|
|
469
|
+
var OFFLINE_QUEUE_KEY = "scalemule_offline_queue";
|
|
470
|
+
var GATEWAY_URLS = {
|
|
471
|
+
dev: "https://api-dev.scalemule.com",
|
|
472
|
+
prod: "https://api.scalemule.com"
|
|
473
|
+
};
|
|
474
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
475
|
+
function sleep(ms) {
|
|
476
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
477
|
+
}
|
|
478
|
+
function getBackoffDelay(attempt, baseDelay) {
|
|
479
|
+
const exponential = baseDelay * Math.pow(2, attempt);
|
|
480
|
+
const jitter = Math.random() * 0.3 * exponential;
|
|
481
|
+
return Math.min(exponential + jitter, MAX_BACKOFF_MS);
|
|
482
|
+
}
|
|
483
|
+
function generateIdempotencyKey() {
|
|
484
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
485
|
+
}
|
|
486
|
+
function sanitizeFilename(filename) {
|
|
487
|
+
let sanitized = filename.replace(/[\x00-\x1f\x7f]/g, "");
|
|
488
|
+
sanitized = sanitized.replace(/["\\/\n\r]/g, "_").normalize("NFC").replace(/[\u200b-\u200f\ufeff\u2028\u2029]/g, "");
|
|
489
|
+
if (!sanitized || sanitized.trim() === "") {
|
|
490
|
+
sanitized = "unnamed";
|
|
491
|
+
}
|
|
492
|
+
if (sanitized.length > 200) {
|
|
493
|
+
const ext = sanitized.split(".").pop();
|
|
494
|
+
const base = sanitized.substring(0, 190);
|
|
495
|
+
sanitized = ext ? `${base}.${ext}` : base;
|
|
496
|
+
}
|
|
497
|
+
return sanitized.trim();
|
|
498
|
+
}
|
|
499
|
+
function statusToErrorCode(status) {
|
|
500
|
+
switch (status) {
|
|
501
|
+
case 400:
|
|
502
|
+
return "validation_error";
|
|
503
|
+
case 401:
|
|
504
|
+
return "unauthorized";
|
|
505
|
+
case 403:
|
|
506
|
+
return "forbidden";
|
|
507
|
+
case 404:
|
|
508
|
+
return "not_found";
|
|
509
|
+
case 409:
|
|
510
|
+
return "conflict";
|
|
511
|
+
case 422:
|
|
512
|
+
return "validation_error";
|
|
513
|
+
case 429:
|
|
514
|
+
return "rate_limited";
|
|
515
|
+
default:
|
|
516
|
+
return status >= 500 ? "internal_error" : `http_${status}`;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function createDefaultStorage() {
|
|
520
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
521
|
+
return {
|
|
522
|
+
getItem: (key) => window.localStorage.getItem(key),
|
|
523
|
+
setItem: (key, value) => window.localStorage.setItem(key, value),
|
|
524
|
+
removeItem: (key) => window.localStorage.removeItem(key)
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const memory = /* @__PURE__ */ new Map();
|
|
528
|
+
return {
|
|
529
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
530
|
+
setItem: (key, value) => {
|
|
531
|
+
memory.set(key, value);
|
|
532
|
+
},
|
|
533
|
+
removeItem: (key) => {
|
|
534
|
+
memory.delete(key);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
var RateLimitQueue = class {
|
|
539
|
+
constructor() {
|
|
540
|
+
this.queue = [];
|
|
541
|
+
this.processing = false;
|
|
542
|
+
this.rateLimitedUntil = 0;
|
|
543
|
+
}
|
|
544
|
+
enqueue(execute, priority = 0) {
|
|
545
|
+
return new Promise((resolve, reject) => {
|
|
546
|
+
this.queue.push({
|
|
547
|
+
execute,
|
|
548
|
+
resolve,
|
|
549
|
+
reject,
|
|
550
|
+
priority
|
|
551
|
+
});
|
|
552
|
+
this.queue.sort((a, b) => b.priority - a.priority);
|
|
553
|
+
this.processQueue();
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
async processQueue() {
|
|
557
|
+
if (this.processing || this.queue.length === 0) return;
|
|
558
|
+
this.processing = true;
|
|
559
|
+
while (this.queue.length > 0) {
|
|
560
|
+
const now = Date.now();
|
|
561
|
+
if (now < this.rateLimitedUntil) {
|
|
562
|
+
await sleep(this.rateLimitedUntil - now);
|
|
563
|
+
}
|
|
564
|
+
const request = this.queue.shift();
|
|
565
|
+
if (!request) continue;
|
|
566
|
+
try {
|
|
567
|
+
const result = await request.execute();
|
|
568
|
+
if (result.error?.code === "rate_limited") {
|
|
569
|
+
this.queue.unshift(request);
|
|
570
|
+
const retryAfter = result.error.details?.retryAfter || 60;
|
|
571
|
+
this.rateLimitedUntil = Date.now() + retryAfter * 1e3;
|
|
572
|
+
} else {
|
|
573
|
+
request.resolve(result);
|
|
574
|
+
}
|
|
575
|
+
} catch (error) {
|
|
576
|
+
request.reject(error);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
this.processing = false;
|
|
580
|
+
}
|
|
581
|
+
updateFromHeaders(headers) {
|
|
582
|
+
const retryAfter = headers.get("Retry-After");
|
|
583
|
+
if (retryAfter) {
|
|
584
|
+
const seconds = parseInt(retryAfter, 10);
|
|
585
|
+
if (!isNaN(seconds)) {
|
|
586
|
+
this.rateLimitedUntil = Date.now() + seconds * 1e3;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
get length() {
|
|
591
|
+
return this.queue.length;
|
|
592
|
+
}
|
|
593
|
+
get isRateLimited() {
|
|
594
|
+
return Date.now() < this.rateLimitedUntil;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
var OfflineQueue = class {
|
|
598
|
+
constructor(storage) {
|
|
599
|
+
this.queue = [];
|
|
600
|
+
this.onOnlineCallback = null;
|
|
601
|
+
this.storage = storage;
|
|
602
|
+
this.loadFromStorage();
|
|
603
|
+
this.setupListeners();
|
|
604
|
+
}
|
|
605
|
+
setupListeners() {
|
|
606
|
+
if (typeof window === "undefined") return;
|
|
607
|
+
window.addEventListener("online", () => {
|
|
608
|
+
if (this.onOnlineCallback) this.onOnlineCallback();
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async loadFromStorage() {
|
|
612
|
+
try {
|
|
613
|
+
const data = await this.storage.getItem(OFFLINE_QUEUE_KEY);
|
|
614
|
+
if (data) this.queue = JSON.parse(data);
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async save() {
|
|
619
|
+
try {
|
|
620
|
+
await this.storage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(this.queue));
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async add(method, path, body) {
|
|
625
|
+
this.queue.push({
|
|
626
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
627
|
+
method,
|
|
628
|
+
path,
|
|
629
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
630
|
+
timestamp: Date.now()
|
|
631
|
+
});
|
|
632
|
+
await this.save();
|
|
633
|
+
}
|
|
634
|
+
getAll() {
|
|
635
|
+
return [...this.queue];
|
|
636
|
+
}
|
|
637
|
+
async remove(id) {
|
|
638
|
+
this.queue = this.queue.filter((item) => item.id !== id);
|
|
639
|
+
await this.save();
|
|
640
|
+
}
|
|
641
|
+
async clear() {
|
|
642
|
+
this.queue = [];
|
|
643
|
+
await this.save();
|
|
644
|
+
}
|
|
645
|
+
setOnlineCallback(cb) {
|
|
646
|
+
this.onOnlineCallback = cb;
|
|
647
|
+
}
|
|
648
|
+
get length() {
|
|
649
|
+
return this.queue.length;
|
|
650
|
+
}
|
|
651
|
+
get online() {
|
|
652
|
+
return typeof navigator === "undefined" || navigator.onLine;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
var ScaleMuleClient = class {
|
|
656
|
+
constructor(config) {
|
|
657
|
+
this.sessionToken = null;
|
|
658
|
+
this.userId = null;
|
|
659
|
+
this.rateLimitQueue = null;
|
|
660
|
+
this.offlineQueue = null;
|
|
661
|
+
this.apiKey = config.apiKey;
|
|
662
|
+
this.baseUrl = config.baseUrl || GATEWAY_URLS[config.environment || "prod"];
|
|
663
|
+
this.debug = config.debug || false;
|
|
664
|
+
this.storage = config.storage || createDefaultStorage();
|
|
665
|
+
this.defaultTimeout = config.timeout || DEFAULT_TIMEOUT;
|
|
666
|
+
this.maxRetries = config.retry?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
667
|
+
this.backoffMs = config.retry?.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
668
|
+
if (config.enableRateLimitQueue) {
|
|
669
|
+
this.rateLimitQueue = new RateLimitQueue();
|
|
670
|
+
}
|
|
671
|
+
if (config.enableOfflineQueue) {
|
|
672
|
+
this.offlineQueue = new OfflineQueue(this.storage);
|
|
673
|
+
this.offlineQueue.setOnlineCallback(() => this.syncOfflineQueue());
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// --------------------------------------------------------------------------
|
|
677
|
+
// Session Management
|
|
678
|
+
// --------------------------------------------------------------------------
|
|
679
|
+
async initialize() {
|
|
680
|
+
const token = await this.storage.getItem(SESSION_STORAGE_KEY);
|
|
681
|
+
const userId = await this.storage.getItem(USER_ID_STORAGE_KEY);
|
|
682
|
+
if (token) this.sessionToken = token;
|
|
683
|
+
if (userId) this.userId = userId;
|
|
684
|
+
if (this.debug) console.log("[ScaleMule] Initialized, session:", !!token);
|
|
685
|
+
}
|
|
686
|
+
async setSession(token, userId) {
|
|
687
|
+
this.sessionToken = token;
|
|
688
|
+
this.userId = userId;
|
|
689
|
+
await this.storage.setItem(SESSION_STORAGE_KEY, token);
|
|
690
|
+
await this.storage.setItem(USER_ID_STORAGE_KEY, userId);
|
|
691
|
+
}
|
|
692
|
+
async clearSession() {
|
|
693
|
+
this.sessionToken = null;
|
|
694
|
+
this.userId = null;
|
|
695
|
+
await this.storage.removeItem(SESSION_STORAGE_KEY);
|
|
696
|
+
await this.storage.removeItem(USER_ID_STORAGE_KEY);
|
|
697
|
+
}
|
|
698
|
+
setAccessToken(token) {
|
|
699
|
+
this.sessionToken = token;
|
|
700
|
+
}
|
|
701
|
+
clearAccessToken() {
|
|
702
|
+
this.sessionToken = null;
|
|
703
|
+
}
|
|
704
|
+
getSessionToken() {
|
|
705
|
+
return this.sessionToken;
|
|
706
|
+
}
|
|
707
|
+
getUserId() {
|
|
708
|
+
return this.userId;
|
|
709
|
+
}
|
|
710
|
+
isAuthenticated() {
|
|
711
|
+
return this.sessionToken !== null;
|
|
712
|
+
}
|
|
713
|
+
getBaseUrl() {
|
|
714
|
+
return this.baseUrl;
|
|
715
|
+
}
|
|
716
|
+
getApiKey() {
|
|
717
|
+
return this.apiKey;
|
|
718
|
+
}
|
|
719
|
+
isOnline() {
|
|
720
|
+
if (this.offlineQueue) return this.offlineQueue.online;
|
|
721
|
+
return typeof navigator === "undefined" || navigator.onLine;
|
|
722
|
+
}
|
|
723
|
+
getOfflineQueueLength() {
|
|
724
|
+
return this.offlineQueue?.length || 0;
|
|
725
|
+
}
|
|
726
|
+
getRateLimitQueueLength() {
|
|
727
|
+
return this.rateLimitQueue?.length || 0;
|
|
728
|
+
}
|
|
729
|
+
isRateLimited() {
|
|
730
|
+
return this.rateLimitQueue?.isRateLimited || false;
|
|
731
|
+
}
|
|
732
|
+
// --------------------------------------------------------------------------
|
|
733
|
+
// Core Request Method
|
|
734
|
+
// --------------------------------------------------------------------------
|
|
735
|
+
async request(path, init = {}) {
|
|
736
|
+
const url = `${this.baseUrl}${path}`;
|
|
737
|
+
const method = (init.method || "GET").toUpperCase();
|
|
738
|
+
const timeout = init.timeout || this.defaultTimeout;
|
|
739
|
+
const maxRetries = init.skipRetry ? 0 : init.retries ?? this.maxRetries;
|
|
740
|
+
const headers = {
|
|
741
|
+
"x-api-key": this.apiKey,
|
|
742
|
+
"User-Agent": `ScaleMule-SDK-TypeScript/${SDK_VERSION}`,
|
|
743
|
+
...init.headers
|
|
744
|
+
};
|
|
745
|
+
if (!init.skipAuth && this.sessionToken) {
|
|
746
|
+
headers["Authorization"] = `Bearer ${this.sessionToken}`;
|
|
747
|
+
}
|
|
748
|
+
let bodyStr;
|
|
749
|
+
if (init.body !== void 0 && init.body !== null) {
|
|
750
|
+
bodyStr = typeof init.body === "string" ? init.body : JSON.stringify(init.body);
|
|
751
|
+
if (!headers["Content-Type"]) {
|
|
752
|
+
headers["Content-Type"] = "application/json";
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (this.debug) {
|
|
756
|
+
console.log(`[ScaleMule] ${method} ${path}`);
|
|
757
|
+
}
|
|
758
|
+
const idempotencyKey = method === "POST" ? generateIdempotencyKey() : void 0;
|
|
759
|
+
let lastError = null;
|
|
760
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
761
|
+
if (attempt > 0 && idempotencyKey) {
|
|
762
|
+
headers["x-idempotency-key"] = idempotencyKey;
|
|
763
|
+
}
|
|
764
|
+
const controller = new AbortController();
|
|
765
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
766
|
+
if (init.signal) {
|
|
767
|
+
if (init.signal.aborted) {
|
|
768
|
+
clearTimeout(timeoutId);
|
|
769
|
+
return { data: null, error: { code: "aborted", message: "Request aborted", status: 0 } };
|
|
770
|
+
}
|
|
771
|
+
init.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const response = await fetch(url, {
|
|
775
|
+
method,
|
|
776
|
+
headers,
|
|
777
|
+
body: bodyStr,
|
|
778
|
+
signal: controller.signal
|
|
779
|
+
});
|
|
780
|
+
clearTimeout(timeoutId);
|
|
781
|
+
if (this.rateLimitQueue) {
|
|
782
|
+
this.rateLimitQueue.updateFromHeaders(response.headers);
|
|
783
|
+
}
|
|
784
|
+
let responseData;
|
|
785
|
+
const contentType = response.headers.get("Content-Type") || "";
|
|
786
|
+
if (contentType.includes("application/json")) {
|
|
787
|
+
responseData = await response.json();
|
|
788
|
+
} else {
|
|
789
|
+
const text = await response.text();
|
|
790
|
+
try {
|
|
791
|
+
responseData = JSON.parse(text);
|
|
792
|
+
} catch {
|
|
793
|
+
responseData = { message: text };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (!response.ok) {
|
|
797
|
+
const error = {
|
|
798
|
+
code: responseData?.error?.code || responseData?.code || statusToErrorCode(response.status),
|
|
799
|
+
message: responseData?.error?.message || responseData?.message || response.statusText,
|
|
800
|
+
status: response.status,
|
|
801
|
+
details: responseData?.error?.details || responseData?.details
|
|
802
|
+
};
|
|
803
|
+
if (response.status === 429) {
|
|
804
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
805
|
+
if (retryAfter) {
|
|
806
|
+
error.details = { ...error.details, retryAfter: parseInt(retryAfter, 10) };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (attempt < maxRetries && RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
810
|
+
lastError = error;
|
|
811
|
+
const delay = getBackoffDelay(attempt, this.backoffMs);
|
|
812
|
+
if (this.debug) {
|
|
813
|
+
console.log(`[ScaleMule] Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay)}ms`);
|
|
814
|
+
}
|
|
815
|
+
await sleep(delay);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
return { data: null, error };
|
|
819
|
+
}
|
|
820
|
+
const data = responseData?.data !== void 0 ? responseData.data : responseData;
|
|
821
|
+
return { data, error: null };
|
|
822
|
+
} catch (err) {
|
|
823
|
+
clearTimeout(timeoutId);
|
|
824
|
+
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
825
|
+
const error = {
|
|
826
|
+
code: isAbort ? init.signal?.aborted ? "aborted" : "timeout" : "network_error",
|
|
827
|
+
message: err instanceof Error ? err.message : "Network request failed",
|
|
828
|
+
status: 0
|
|
829
|
+
};
|
|
830
|
+
if (attempt < maxRetries && !init.signal?.aborted) {
|
|
831
|
+
lastError = error;
|
|
832
|
+
const delay = getBackoffDelay(attempt, this.backoffMs);
|
|
833
|
+
if (this.debug) {
|
|
834
|
+
console.log(`[ScaleMule] Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay)}ms (${error.code})`);
|
|
835
|
+
}
|
|
836
|
+
await sleep(delay);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
return { data: null, error };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return { data: null, error: lastError || { code: "internal_error", message: "Request failed", status: 0 } };
|
|
843
|
+
}
|
|
844
|
+
// --------------------------------------------------------------------------
|
|
845
|
+
// HTTP Verb Shortcuts
|
|
846
|
+
// --------------------------------------------------------------------------
|
|
847
|
+
async get(path, options) {
|
|
848
|
+
return this.request(path, { ...options, method: "GET" });
|
|
849
|
+
}
|
|
850
|
+
async post(path, body, options) {
|
|
851
|
+
return this.request(path, { ...options, method: "POST", body });
|
|
852
|
+
}
|
|
853
|
+
async put(path, body, options) {
|
|
854
|
+
return this.request(path, { ...options, method: "PUT", body });
|
|
855
|
+
}
|
|
856
|
+
async patch(path, body, options) {
|
|
857
|
+
return this.request(path, { ...options, method: "PATCH", body });
|
|
858
|
+
}
|
|
859
|
+
async del(path, options) {
|
|
860
|
+
return this.request(path, { ...options, method: "DELETE" });
|
|
861
|
+
}
|
|
862
|
+
// --------------------------------------------------------------------------
|
|
863
|
+
// File Upload
|
|
864
|
+
// --------------------------------------------------------------------------
|
|
865
|
+
/**
|
|
866
|
+
* Upload a file using multipart/form-data.
|
|
867
|
+
*
|
|
868
|
+
* Supports progress tracking via XMLHttpRequest (browser only).
|
|
869
|
+
* Supports cancellation via AbortController signal.
|
|
870
|
+
* Retries with exponential backoff on transient failures.
|
|
871
|
+
*/
|
|
872
|
+
async upload(path, file, additionalFields, options) {
|
|
873
|
+
const fileName = file.name || "file";
|
|
874
|
+
const sanitizedName = sanitizeFilename(fileName);
|
|
875
|
+
const sanitizedFile = sanitizedName !== fileName ? new File([file], sanitizedName, { type: file.type }) : file;
|
|
876
|
+
const buildFormData = () => {
|
|
877
|
+
const fd = new FormData();
|
|
878
|
+
fd.append("file", sanitizedFile);
|
|
879
|
+
if (additionalFields) {
|
|
880
|
+
for (const [key, value] of Object.entries(additionalFields)) {
|
|
881
|
+
fd.append(key, value);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return fd;
|
|
885
|
+
};
|
|
886
|
+
const url = `${this.baseUrl}${path}`;
|
|
887
|
+
if (this.debug) console.log(`[ScaleMule] UPLOAD ${path}`);
|
|
888
|
+
if (options?.signal?.aborted) {
|
|
889
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
890
|
+
}
|
|
891
|
+
if (options?.onProgress && typeof XMLHttpRequest !== "undefined") {
|
|
892
|
+
return this.uploadWithXHR(url, buildFormData, options.onProgress, options?.signal);
|
|
893
|
+
}
|
|
894
|
+
const maxRetries = options?.retries ?? this.maxRetries;
|
|
895
|
+
let lastError = null;
|
|
896
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
897
|
+
const headers = { "x-api-key": this.apiKey };
|
|
898
|
+
if (this.sessionToken) headers["Authorization"] = `Bearer ${this.sessionToken}`;
|
|
899
|
+
try {
|
|
900
|
+
const response = await fetch(url, {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers,
|
|
903
|
+
body: buildFormData(),
|
|
904
|
+
signal: options?.signal
|
|
905
|
+
});
|
|
906
|
+
const data = await response.json();
|
|
907
|
+
if (!response.ok) {
|
|
908
|
+
const error = {
|
|
909
|
+
code: data?.error?.code || statusToErrorCode(response.status),
|
|
910
|
+
message: data?.error?.message || data?.message || response.statusText,
|
|
911
|
+
status: response.status,
|
|
912
|
+
details: data?.error?.details
|
|
913
|
+
};
|
|
914
|
+
if (attempt < maxRetries && RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
915
|
+
lastError = error;
|
|
916
|
+
await sleep(getBackoffDelay(attempt, this.backoffMs));
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
return { data: null, error };
|
|
920
|
+
}
|
|
921
|
+
const result = data?.data !== void 0 ? data.data : data;
|
|
922
|
+
return { data: result, error: null };
|
|
923
|
+
} catch (err) {
|
|
924
|
+
if (options?.signal?.aborted) {
|
|
925
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
926
|
+
}
|
|
927
|
+
lastError = {
|
|
928
|
+
code: "upload_error",
|
|
929
|
+
message: err instanceof Error ? err.message : "Upload failed",
|
|
930
|
+
status: 0
|
|
931
|
+
};
|
|
932
|
+
if (attempt < maxRetries) {
|
|
933
|
+
await sleep(getBackoffDelay(attempt, this.backoffMs));
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return { data: null, error: lastError || { code: "upload_error", message: "Upload failed", status: 0 } };
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Single upload with XMLHttpRequest for progress tracking.
|
|
942
|
+
* Supports abort via AbortSignal.
|
|
943
|
+
*/
|
|
944
|
+
uploadWithXHR(url, buildFormData, onProgress, signal, maxRetries = this.maxRetries) {
|
|
945
|
+
return new Promise(async (resolve) => {
|
|
946
|
+
let lastError = null;
|
|
947
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
948
|
+
const result = await new Promise((res) => {
|
|
949
|
+
const xhr = new XMLHttpRequest();
|
|
950
|
+
if (signal) {
|
|
951
|
+
if (signal.aborted) {
|
|
952
|
+
res({ data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } });
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
956
|
+
}
|
|
957
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
958
|
+
if (event.lengthComputable) {
|
|
959
|
+
onProgress(Math.round(event.loaded / event.total * 100));
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
xhr.addEventListener("load", () => {
|
|
963
|
+
try {
|
|
964
|
+
const data = JSON.parse(xhr.responseText);
|
|
965
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
966
|
+
const result2 = data?.data !== void 0 ? data.data : data;
|
|
967
|
+
res({ data: result2, error: null });
|
|
968
|
+
} else {
|
|
969
|
+
res({
|
|
970
|
+
data: null,
|
|
971
|
+
error: {
|
|
972
|
+
code: data?.error?.code || statusToErrorCode(xhr.status),
|
|
973
|
+
message: data?.error?.message || data?.message || "Upload failed",
|
|
974
|
+
status: xhr.status,
|
|
975
|
+
details: data?.error?.details
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
} catch {
|
|
980
|
+
res({ data: null, error: { code: "internal_error", message: "Failed to parse response", status: 0 } });
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
xhr.addEventListener("error", () => {
|
|
984
|
+
res({ data: null, error: { code: "upload_error", message: "Upload failed", status: 0 } });
|
|
985
|
+
});
|
|
986
|
+
xhr.addEventListener("abort", () => {
|
|
987
|
+
res({ data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } });
|
|
988
|
+
});
|
|
989
|
+
xhr.open("POST", url);
|
|
990
|
+
xhr.setRequestHeader("x-api-key", this.apiKey);
|
|
991
|
+
if (this.sessionToken) {
|
|
992
|
+
xhr.setRequestHeader("Authorization", `Bearer ${this.sessionToken}`);
|
|
993
|
+
}
|
|
994
|
+
xhr.send(buildFormData());
|
|
995
|
+
});
|
|
996
|
+
if (result.error === null) {
|
|
997
|
+
resolve(result);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const isRetryable = result.error.code === "upload_error" || result.error.code === "network_error" || RETRYABLE_STATUS_CODES.has(result.error.status);
|
|
1001
|
+
if (result.error.code === "aborted") {
|
|
1002
|
+
resolve(result);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (attempt < maxRetries && isRetryable) {
|
|
1006
|
+
lastError = result.error;
|
|
1007
|
+
onProgress(0);
|
|
1008
|
+
await sleep(getBackoffDelay(attempt, this.backoffMs));
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
resolve(result);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
resolve({
|
|
1015
|
+
data: null,
|
|
1016
|
+
error: lastError || { code: "upload_error", message: "Upload failed", status: 0 }
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
// --------------------------------------------------------------------------
|
|
1021
|
+
// Offline Queue Sync
|
|
1022
|
+
// --------------------------------------------------------------------------
|
|
1023
|
+
async syncOfflineQueue() {
|
|
1024
|
+
if (!this.offlineQueue) return;
|
|
1025
|
+
const items = this.offlineQueue.getAll();
|
|
1026
|
+
if (this.debug && items.length > 0) {
|
|
1027
|
+
console.log(`[ScaleMule] Syncing ${items.length} offline requests`);
|
|
1028
|
+
}
|
|
1029
|
+
for (const item of items) {
|
|
1030
|
+
try {
|
|
1031
|
+
await this.request(item.path, {
|
|
1032
|
+
method: item.method,
|
|
1033
|
+
body: item.body ? JSON.parse(item.body) : void 0,
|
|
1034
|
+
skipRetry: true
|
|
1035
|
+
});
|
|
1036
|
+
await this.offlineQueue.remove(item.id);
|
|
1037
|
+
} catch {
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
// src/service.ts
|
|
1045
|
+
var ServiceModule = class {
|
|
1046
|
+
constructor(client) {
|
|
1047
|
+
this.client = client;
|
|
1048
|
+
}
|
|
1049
|
+
// --------------------------------------------------------------------------
|
|
1050
|
+
// Client context → headers resolution
|
|
1051
|
+
// --------------------------------------------------------------------------
|
|
1052
|
+
/**
|
|
1053
|
+
* Merge `clientContext` from RequestOptions into `headers`.
|
|
1054
|
+
* Explicit headers take precedence over context-derived ones.
|
|
1055
|
+
*/
|
|
1056
|
+
resolveOptions(options) {
|
|
1057
|
+
if (!options?.clientContext) return options;
|
|
1058
|
+
const contextHeaders = buildClientContextHeaders(options.clientContext);
|
|
1059
|
+
const { clientContext: _, ...rest } = options;
|
|
1060
|
+
return { ...rest, headers: { ...contextHeaders, ...rest.headers } };
|
|
1061
|
+
}
|
|
1062
|
+
// --------------------------------------------------------------------------
|
|
1063
|
+
// HTTP verb shortcuts (path relative to basePath)
|
|
1064
|
+
// --------------------------------------------------------------------------
|
|
1065
|
+
_get(path, options) {
|
|
1066
|
+
return this.client.get(`${this.basePath}${path}`, this.resolveOptions(options));
|
|
1067
|
+
}
|
|
1068
|
+
post(path, body, options) {
|
|
1069
|
+
return this.client.post(`${this.basePath}${path}`, body, this.resolveOptions(options));
|
|
1070
|
+
}
|
|
1071
|
+
put(path, body, options) {
|
|
1072
|
+
return this.client.put(`${this.basePath}${path}`, body, this.resolveOptions(options));
|
|
1073
|
+
}
|
|
1074
|
+
patch(path, body, options) {
|
|
1075
|
+
return this.client.patch(`${this.basePath}${path}`, body, this.resolveOptions(options));
|
|
1076
|
+
}
|
|
1077
|
+
del(path, options) {
|
|
1078
|
+
return this.client.del(`${this.basePath}${path}`, this.resolveOptions(options));
|
|
1079
|
+
}
|
|
1080
|
+
// --------------------------------------------------------------------------
|
|
1081
|
+
// Paginated list
|
|
1082
|
+
// --------------------------------------------------------------------------
|
|
1083
|
+
/**
|
|
1084
|
+
* Fetch a paginated list from the backend.
|
|
1085
|
+
*
|
|
1086
|
+
* Normalizes varying backend pagination shapes into the standard
|
|
1087
|
+
* PaginatedResponse<T> envelope. Supports backends that return:
|
|
1088
|
+
* - { data: T[], metadata: { total, ... } } (preferred)
|
|
1089
|
+
* - { items: T[], total, page, per_page } (legacy)
|
|
1090
|
+
* - T[] (bare array)
|
|
1091
|
+
*
|
|
1092
|
+
* Extra params beyond page/perPage are forwarded as query string parameters.
|
|
1093
|
+
*/
|
|
1094
|
+
async _list(path, params, options) {
|
|
1095
|
+
const qs = buildQueryString(params);
|
|
1096
|
+
const fullPath = qs ? `${this.basePath}${path}?${qs}` : `${this.basePath}${path}`;
|
|
1097
|
+
const response = await this.client.get(fullPath, this.resolveOptions(options));
|
|
1098
|
+
if (response.error) {
|
|
1099
|
+
return {
|
|
1100
|
+
data: [],
|
|
1101
|
+
metadata: { total: 0, totalPages: 0, page: asNum(params?.page) ?? 1, perPage: asNum(params?.perPage) ?? 20 },
|
|
1102
|
+
error: response.error
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
return normalizePaginatedResponse(response.data, params);
|
|
1106
|
+
}
|
|
1107
|
+
// --------------------------------------------------------------------------
|
|
1108
|
+
// File upload (delegates to client.upload)
|
|
1109
|
+
// --------------------------------------------------------------------------
|
|
1110
|
+
_upload(path, file, additionalFields, options) {
|
|
1111
|
+
return this.client.upload(`${this.basePath}${path}`, file, additionalFields, this.resolveOptions(options));
|
|
1112
|
+
}
|
|
1113
|
+
// --------------------------------------------------------------------------
|
|
1114
|
+
// Query string helper (available to subclasses)
|
|
1115
|
+
// --------------------------------------------------------------------------
|
|
1116
|
+
/**
|
|
1117
|
+
* Append query parameters to a relative path.
|
|
1118
|
+
* Use with verb methods: `this.get(this.withQuery('/items', { status: 'active' }))`
|
|
1119
|
+
* Does NOT add basePath — the verb methods handle that.
|
|
1120
|
+
*/
|
|
1121
|
+
withQuery(path, params) {
|
|
1122
|
+
const qs = buildQueryString(params);
|
|
1123
|
+
return qs ? `${path}?${qs}` : path;
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
function buildQueryString(params) {
|
|
1127
|
+
if (!params) return "";
|
|
1128
|
+
const pairs = [];
|
|
1129
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1130
|
+
if (value === void 0 || value === null) continue;
|
|
1131
|
+
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
1132
|
+
}
|
|
1133
|
+
return pairs.join("&");
|
|
1134
|
+
}
|
|
1135
|
+
function normalizePaginatedResponse(raw, params) {
|
|
1136
|
+
if (raw === null || raw === void 0) {
|
|
1137
|
+
return {
|
|
1138
|
+
data: [],
|
|
1139
|
+
metadata: { total: 0, totalPages: 0, page: 1, perPage: 20 },
|
|
1140
|
+
error: null
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
if (Array.isArray(raw)) {
|
|
1144
|
+
return {
|
|
1145
|
+
data: raw,
|
|
1146
|
+
metadata: {
|
|
1147
|
+
total: raw.length,
|
|
1148
|
+
totalPages: 1,
|
|
1149
|
+
page: 1,
|
|
1150
|
+
perPage: raw.length
|
|
1151
|
+
},
|
|
1152
|
+
error: null
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
const obj = raw;
|
|
1156
|
+
const dataArray = obj.data ?? obj.items ?? [];
|
|
1157
|
+
const metadata = {
|
|
1158
|
+
total: asNumber(obj.metadata, "total") ?? asNumber(obj, "total") ?? dataArray.length,
|
|
1159
|
+
totalPages: asNumber(obj.metadata, "totalPages") ?? asNumber(obj.metadata, "total_pages") ?? asNumber(obj, "total_pages") ?? asNumber(obj, "totalPages") ?? 0,
|
|
1160
|
+
page: asNumber(obj.metadata, "page") ?? asNumber(obj, "page") ?? asNum(params?.page) ?? 1,
|
|
1161
|
+
perPage: asNumber(obj.metadata, "perPage") ?? asNumber(obj.metadata, "per_page") ?? asNumber(obj, "per_page") ?? asNumber(obj, "perPage") ?? asNum(params?.perPage) ?? 20
|
|
1162
|
+
};
|
|
1163
|
+
if (metadata.totalPages === 0 && metadata.total > 0 && metadata.perPage > 0) {
|
|
1164
|
+
metadata.totalPages = Math.ceil(metadata.total / metadata.perPage);
|
|
1165
|
+
}
|
|
1166
|
+
const nextCursor = asString(obj.metadata, "nextCursor") ?? asString(obj.metadata, "next_cursor") ?? asString(obj, "next_cursor") ?? asString(obj, "nextCursor");
|
|
1167
|
+
if (nextCursor) {
|
|
1168
|
+
metadata.nextCursor = nextCursor;
|
|
1169
|
+
}
|
|
1170
|
+
return { data: dataArray, metadata, error: null };
|
|
1171
|
+
}
|
|
1172
|
+
function asNumber(parent, key) {
|
|
1173
|
+
if (parent === null || parent === void 0 || typeof parent !== "object") return void 0;
|
|
1174
|
+
const value = parent[key];
|
|
1175
|
+
return typeof value === "number" ? value : void 0;
|
|
1176
|
+
}
|
|
1177
|
+
function asNum(value) {
|
|
1178
|
+
return typeof value === "number" ? value : void 0;
|
|
1179
|
+
}
|
|
1180
|
+
function asString(parent, key) {
|
|
1181
|
+
if (parent === null || parent === void 0 || typeof parent !== "object") return void 0;
|
|
1182
|
+
const value = parent[key];
|
|
1183
|
+
return typeof value === "string" ? value : void 0;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/utils/phone.ts
|
|
1187
|
+
var import_phone = require("@scalemule/ui/phone");
|
|
1188
|
+
|
|
1189
|
+
// src/services/auth.ts
|
|
1190
|
+
var AuthMfaApi = class extends ServiceModule {
|
|
1191
|
+
constructor() {
|
|
1192
|
+
super(...arguments);
|
|
1193
|
+
this.basePath = "/v1/auth/mfa";
|
|
1194
|
+
}
|
|
1195
|
+
async getStatus() {
|
|
1196
|
+
return this._get("/status");
|
|
1197
|
+
}
|
|
1198
|
+
async setupTotp() {
|
|
1199
|
+
return this.post("/totp/setup");
|
|
1200
|
+
}
|
|
1201
|
+
async verifySetup(data) {
|
|
1202
|
+
return this.post("/totp/verify-setup", data);
|
|
1203
|
+
}
|
|
1204
|
+
async enableSms() {
|
|
1205
|
+
return this.post("/sms/enable");
|
|
1206
|
+
}
|
|
1207
|
+
async enableEmail() {
|
|
1208
|
+
return this.post("/email/enable");
|
|
1209
|
+
}
|
|
1210
|
+
async disable(data) {
|
|
1211
|
+
return this.post("/disable", data);
|
|
1212
|
+
}
|
|
1213
|
+
async regenerateBackupCodes() {
|
|
1214
|
+
return this.post("/backup-codes/regenerate");
|
|
1215
|
+
}
|
|
1216
|
+
async sendCode(data) {
|
|
1217
|
+
return this.post("/send-code", data);
|
|
1218
|
+
}
|
|
1219
|
+
async verify(data) {
|
|
1220
|
+
return this.post("/verify", data);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
var AuthSessionsApi = class extends ServiceModule {
|
|
1224
|
+
constructor() {
|
|
1225
|
+
super(...arguments);
|
|
1226
|
+
this.basePath = "/v1/auth/sessions";
|
|
1227
|
+
}
|
|
1228
|
+
async list() {
|
|
1229
|
+
return this._get("");
|
|
1230
|
+
}
|
|
1231
|
+
async revoke(sessionId) {
|
|
1232
|
+
return this.del(`/${sessionId}`);
|
|
1233
|
+
}
|
|
1234
|
+
async revokeAll() {
|
|
1235
|
+
return this.del("/others");
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
var AuthDevicesApi = class extends ServiceModule {
|
|
1239
|
+
constructor() {
|
|
1240
|
+
super(...arguments);
|
|
1241
|
+
this.basePath = "/v1/auth/devices";
|
|
1242
|
+
}
|
|
1243
|
+
async list() {
|
|
1244
|
+
return this._get("");
|
|
1245
|
+
}
|
|
1246
|
+
async trust(deviceId) {
|
|
1247
|
+
return this.post(`/${deviceId}/trust`);
|
|
1248
|
+
}
|
|
1249
|
+
async block(deviceId) {
|
|
1250
|
+
return this.post(`/${deviceId}/block`);
|
|
1251
|
+
}
|
|
1252
|
+
async delete(deviceId) {
|
|
1253
|
+
return this.del(`/${deviceId}`);
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
var AuthLoginHistoryApi = class extends ServiceModule {
|
|
1257
|
+
constructor() {
|
|
1258
|
+
super(...arguments);
|
|
1259
|
+
this.basePath = "/v1/auth";
|
|
1260
|
+
}
|
|
1261
|
+
async list(params) {
|
|
1262
|
+
return this._get(this.withQuery("/login-history", params));
|
|
1263
|
+
}
|
|
1264
|
+
async getSummary() {
|
|
1265
|
+
return this._get("/login-activity");
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
var AuthService = class extends ServiceModule {
|
|
1269
|
+
constructor(client) {
|
|
1270
|
+
super(client);
|
|
1271
|
+
this.basePath = "/v1/auth";
|
|
1272
|
+
this.mfa = new AuthMfaApi(client);
|
|
1273
|
+
this.sessions = new AuthSessionsApi(client);
|
|
1274
|
+
this.devices = new AuthDevicesApi(client);
|
|
1275
|
+
this.loginHistory = new AuthLoginHistoryApi(client);
|
|
1276
|
+
}
|
|
1277
|
+
sanitizePhoneField(value) {
|
|
1278
|
+
if (typeof value !== "string") return value;
|
|
1279
|
+
const normalized = (0, import_phone.normalizePhoneNumber)(value);
|
|
1280
|
+
return normalized || void 0;
|
|
1281
|
+
}
|
|
1282
|
+
// --------------------------------------------------------------------------
|
|
1283
|
+
// Core Auth
|
|
1284
|
+
// --------------------------------------------------------------------------
|
|
1285
|
+
async register(data, options) {
|
|
1286
|
+
const payload = {
|
|
1287
|
+
...data,
|
|
1288
|
+
phone: this.sanitizePhoneField(data.phone)
|
|
1289
|
+
};
|
|
1290
|
+
return this.post("/register", payload, options);
|
|
1291
|
+
}
|
|
1292
|
+
async login(data, options) {
|
|
1293
|
+
return this.post("/login", data, options);
|
|
1294
|
+
}
|
|
1295
|
+
async logout(options) {
|
|
1296
|
+
return this.post("/logout", void 0, options);
|
|
1297
|
+
}
|
|
1298
|
+
async me(options) {
|
|
1299
|
+
return this._get("/me", options);
|
|
1300
|
+
}
|
|
1301
|
+
/** Refresh the session. Alias: refreshToken() */
|
|
1302
|
+
async refreshSession(data, options) {
|
|
1303
|
+
return this.post("/refresh", data ?? {}, options);
|
|
1304
|
+
}
|
|
1305
|
+
/** @deprecated Use refreshSession() */
|
|
1306
|
+
async refreshToken(data) {
|
|
1307
|
+
return this.refreshSession(data);
|
|
1308
|
+
}
|
|
1309
|
+
// --------------------------------------------------------------------------
|
|
1310
|
+
// Passwordless Auth
|
|
1311
|
+
// --------------------------------------------------------------------------
|
|
1312
|
+
/**
|
|
1313
|
+
* Send a one-time password for passwordless sign-in.
|
|
1314
|
+
* @experimental Endpoint availability depends on backend deployment.
|
|
1315
|
+
*/
|
|
1316
|
+
async signInWithOtp(data, options) {
|
|
1317
|
+
const payload = {
|
|
1318
|
+
...data,
|
|
1319
|
+
phone: this.sanitizePhoneField(data.phone)
|
|
1320
|
+
};
|
|
1321
|
+
return this.post("/otp/send", payload, options);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Verify OTP code and create a session.
|
|
1325
|
+
* @experimental Endpoint availability depends on backend deployment.
|
|
1326
|
+
*/
|
|
1327
|
+
async verifyOtp(data, options) {
|
|
1328
|
+
const payload = {
|
|
1329
|
+
...data,
|
|
1330
|
+
phone: this.sanitizePhoneField(data.phone)
|
|
1331
|
+
};
|
|
1332
|
+
return this.post("/otp/verify", payload, options);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Send a magic link for passwordless sign-in.
|
|
1336
|
+
* @experimental Endpoint availability depends on backend deployment.
|
|
1337
|
+
*/
|
|
1338
|
+
async signInWithMagicLink(data, options) {
|
|
1339
|
+
return this.post("/magic-link/send", data, options);
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Verify a magic link token and create a session.
|
|
1343
|
+
* @experimental Endpoint availability depends on backend deployment.
|
|
1344
|
+
*/
|
|
1345
|
+
async verifyMagicLink(data, options) {
|
|
1346
|
+
return this.post("/magic-link/verify", data, options);
|
|
1347
|
+
}
|
|
1348
|
+
// --------------------------------------------------------------------------
|
|
1349
|
+
// Phone OTP (existing backend endpoints)
|
|
1350
|
+
// --------------------------------------------------------------------------
|
|
1351
|
+
async sendPhoneOtp(data, options) {
|
|
1352
|
+
const payload = {
|
|
1353
|
+
...data,
|
|
1354
|
+
phone: this.sanitizePhoneField(data.phone) ?? ""
|
|
1355
|
+
};
|
|
1356
|
+
return this.post("/phone/send-otp", payload, options);
|
|
1357
|
+
}
|
|
1358
|
+
async verifyPhoneOtp(data, options) {
|
|
1359
|
+
const payload = {
|
|
1360
|
+
...data,
|
|
1361
|
+
phone: this.sanitizePhoneField(data.phone) ?? ""
|
|
1362
|
+
};
|
|
1363
|
+
return this.post("/phone/verify-otp", payload, options);
|
|
1364
|
+
}
|
|
1365
|
+
async resendPhoneOtp(data, options) {
|
|
1366
|
+
const payload = {
|
|
1367
|
+
...data,
|
|
1368
|
+
phone: this.sanitizePhoneField(data.phone) ?? ""
|
|
1369
|
+
};
|
|
1370
|
+
return this.post("/phone/resend-otp", payload, options);
|
|
1371
|
+
}
|
|
1372
|
+
/** Login with phone OTP (sends + verifies in one flow) */
|
|
1373
|
+
async loginWithPhone(data, options) {
|
|
1374
|
+
const payload = {
|
|
1375
|
+
...data,
|
|
1376
|
+
phone: this.sanitizePhoneField(data.phone) ?? "",
|
|
1377
|
+
purpose: "login"
|
|
1378
|
+
};
|
|
1379
|
+
return this.post("/phone/verify-otp", payload, options);
|
|
1380
|
+
}
|
|
1381
|
+
// --------------------------------------------------------------------------
|
|
1382
|
+
// Password Management
|
|
1383
|
+
// --------------------------------------------------------------------------
|
|
1384
|
+
async forgotPassword(data, options) {
|
|
1385
|
+
return this.post("/forgot-password", data, options);
|
|
1386
|
+
}
|
|
1387
|
+
async resetPassword(data, options) {
|
|
1388
|
+
return this.post("/reset-password", data, options);
|
|
1389
|
+
}
|
|
1390
|
+
async changePassword(data, options) {
|
|
1391
|
+
return this.post("/password/change", data, options);
|
|
1392
|
+
}
|
|
1393
|
+
// --------------------------------------------------------------------------
|
|
1394
|
+
// Email & Phone Management
|
|
1395
|
+
// --------------------------------------------------------------------------
|
|
1396
|
+
async verifyEmail(data, options) {
|
|
1397
|
+
return this.post("/verify-email", data, options);
|
|
1398
|
+
}
|
|
1399
|
+
/** Resend email verification. Alias: resendEmailVerification() */
|
|
1400
|
+
async resendVerification(data, options) {
|
|
1401
|
+
return this.post("/resend-verification", data ?? {}, options);
|
|
1402
|
+
}
|
|
1403
|
+
/** @deprecated Use resendVerification() */
|
|
1404
|
+
async resendEmailVerification(data) {
|
|
1405
|
+
return this.resendVerification(data);
|
|
1406
|
+
}
|
|
1407
|
+
async changeEmail(data, options) {
|
|
1408
|
+
return this.post("/email/change", data, options);
|
|
1409
|
+
}
|
|
1410
|
+
async changePhone(data, options) {
|
|
1411
|
+
const payload = {
|
|
1412
|
+
...data,
|
|
1413
|
+
new_phone: this.sanitizePhoneField(data.new_phone) ?? ""
|
|
1414
|
+
};
|
|
1415
|
+
return this.post("/phone/change", payload, options);
|
|
1416
|
+
}
|
|
1417
|
+
// --------------------------------------------------------------------------
|
|
1418
|
+
// Account
|
|
1419
|
+
// --------------------------------------------------------------------------
|
|
1420
|
+
async deleteAccount(options) {
|
|
1421
|
+
return this.del("/me", options);
|
|
1422
|
+
}
|
|
1423
|
+
async exportData(options) {
|
|
1424
|
+
return this._get("/me/export", options);
|
|
1425
|
+
}
|
|
1426
|
+
// --------------------------------------------------------------------------
|
|
1427
|
+
// OAuth
|
|
1428
|
+
// --------------------------------------------------------------------------
|
|
1429
|
+
async getOAuthUrl(provider, redirectUri, options) {
|
|
1430
|
+
return this._get(this.withQuery(`/oauth/${provider}/authorize`, { redirect_uri: redirectUri }), options);
|
|
1431
|
+
}
|
|
1432
|
+
async handleOAuthCallback(data, options) {
|
|
1433
|
+
const { provider, ...rest } = data;
|
|
1434
|
+
return this._get(this.withQuery(`/oauth/${provider}/callback`, rest), options);
|
|
1435
|
+
}
|
|
1436
|
+
async listOAuthProviders(options) {
|
|
1437
|
+
return this._get("/oauth/providers", options);
|
|
1438
|
+
}
|
|
1439
|
+
async unlinkOAuthProvider(provider, options) {
|
|
1440
|
+
return this.del(`/oauth/providers/${provider}`, options);
|
|
1441
|
+
}
|
|
1442
|
+
// --------------------------------------------------------------------------
|
|
1443
|
+
// Token Management
|
|
1444
|
+
// --------------------------------------------------------------------------
|
|
1445
|
+
async refreshAccessToken(data, options) {
|
|
1446
|
+
return this.post("/token/refresh", data ?? {}, options);
|
|
1447
|
+
}
|
|
1448
|
+
async revokeRefreshToken(data, options) {
|
|
1449
|
+
return this.post("/token/revoke", data, options);
|
|
1450
|
+
}
|
|
1451
|
+
// --------------------------------------------------------------------------
|
|
1452
|
+
// Flat methods for backward compatibility (delegate to sub-APIs)
|
|
1453
|
+
// --------------------------------------------------------------------------
|
|
1454
|
+
/** @deprecated Use auth.sessions.list() */
|
|
1455
|
+
async listSessions() {
|
|
1456
|
+
return this.sessions.list();
|
|
1457
|
+
}
|
|
1458
|
+
/** @deprecated Use auth.sessions.revoke() */
|
|
1459
|
+
async revokeSession(sessionId) {
|
|
1460
|
+
return this.sessions.revoke(sessionId);
|
|
1461
|
+
}
|
|
1462
|
+
/** @deprecated Use auth.sessions.revokeAll() */
|
|
1463
|
+
async revokeOtherSessions() {
|
|
1464
|
+
return this.sessions.revokeAll();
|
|
1465
|
+
}
|
|
1466
|
+
/** @deprecated Use auth.devices.list() */
|
|
1467
|
+
async listDevices() {
|
|
1468
|
+
return this.devices.list();
|
|
1469
|
+
}
|
|
1470
|
+
/** @deprecated Use auth.devices.trust() */
|
|
1471
|
+
async trustDevice(deviceId) {
|
|
1472
|
+
return this.devices.trust(deviceId);
|
|
1473
|
+
}
|
|
1474
|
+
/** @deprecated Use auth.devices.block() */
|
|
1475
|
+
async blockDevice(deviceId) {
|
|
1476
|
+
return this.devices.block(deviceId);
|
|
1477
|
+
}
|
|
1478
|
+
/** @deprecated Use auth.devices.delete() */
|
|
1479
|
+
async deleteDevice(deviceId) {
|
|
1480
|
+
return this.devices.delete(deviceId);
|
|
1481
|
+
}
|
|
1482
|
+
/** @deprecated Use auth.loginHistory.list() */
|
|
1483
|
+
async getLoginHistory(params) {
|
|
1484
|
+
return this.loginHistory.list(params);
|
|
1485
|
+
}
|
|
1486
|
+
/** @deprecated Use auth.loginHistory.getSummary() */
|
|
1487
|
+
async getLoginActivitySummary() {
|
|
1488
|
+
return this.loginHistory.getSummary();
|
|
1489
|
+
}
|
|
1490
|
+
/** @deprecated Use auth.mfa.getStatus() */
|
|
1491
|
+
async getMfaStatus() {
|
|
1492
|
+
return this.mfa.getStatus();
|
|
1493
|
+
}
|
|
1494
|
+
/** @deprecated Use auth.mfa.setupTotp() */
|
|
1495
|
+
async setupTotp() {
|
|
1496
|
+
return this.mfa.setupTotp();
|
|
1497
|
+
}
|
|
1498
|
+
/** @deprecated Use auth.mfa.verifySetup() */
|
|
1499
|
+
async verifyTotpSetup(data) {
|
|
1500
|
+
return this.mfa.verifySetup(data);
|
|
1501
|
+
}
|
|
1502
|
+
/** @deprecated Use auth.mfa.enableSms() */
|
|
1503
|
+
async enableSmsMfa() {
|
|
1504
|
+
return this.mfa.enableSms();
|
|
1505
|
+
}
|
|
1506
|
+
/** @deprecated Use auth.mfa.enableEmail() */
|
|
1507
|
+
async enableEmailMfa() {
|
|
1508
|
+
return this.mfa.enableEmail();
|
|
1509
|
+
}
|
|
1510
|
+
/** @deprecated Use auth.mfa.disable() */
|
|
1511
|
+
async disableMfa(data) {
|
|
1512
|
+
return this.mfa.disable(data);
|
|
1513
|
+
}
|
|
1514
|
+
/** @deprecated Use auth.mfa.regenerateBackupCodes() */
|
|
1515
|
+
async regenerateBackupCodes() {
|
|
1516
|
+
return this.mfa.regenerateBackupCodes();
|
|
1517
|
+
}
|
|
1518
|
+
/** @deprecated Use auth.mfa.sendCode() */
|
|
1519
|
+
async sendMfaCode(data) {
|
|
1520
|
+
return this.mfa.sendCode(data);
|
|
1521
|
+
}
|
|
1522
|
+
/** @deprecated Use auth.mfa.verify() */
|
|
1523
|
+
async verifyMfa(data) {
|
|
1524
|
+
return this.mfa.verify(data);
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
// src/services/logger.ts
|
|
1529
|
+
var BATCH_MAX_SIZE = 100;
|
|
1530
|
+
var LoggerService = class extends ServiceModule {
|
|
1531
|
+
constructor() {
|
|
1532
|
+
super(...arguments);
|
|
1533
|
+
this.basePath = "/v1/logger";
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Write a single log entry.
|
|
1537
|
+
* Accepts both new schema (LogInput) and legacy shape ({ level, message }) for backward compatibility.
|
|
1538
|
+
*/
|
|
1539
|
+
async log(data, options) {
|
|
1540
|
+
const body = this.normalizeLogInput(data);
|
|
1541
|
+
return this.post("/logs", body, options);
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Write log entries in batch.
|
|
1545
|
+
* Auto-chunks into groups of 100 (backend hard limit) and sends sequentially.
|
|
1546
|
+
* Returns total ingested count across all chunks.
|
|
1547
|
+
*/
|
|
1548
|
+
async logBatch(logs, options) {
|
|
1549
|
+
if (logs.length === 0) {
|
|
1550
|
+
return { data: { ingested: 0 }, error: null };
|
|
1551
|
+
}
|
|
1552
|
+
let totalIngested = 0;
|
|
1553
|
+
for (let i = 0; i < logs.length; i += BATCH_MAX_SIZE) {
|
|
1554
|
+
const chunk = logs.slice(i, i + BATCH_MAX_SIZE);
|
|
1555
|
+
const result = await this.post("/logs/batch", { logs: chunk }, options);
|
|
1556
|
+
if (result.error) {
|
|
1557
|
+
return {
|
|
1558
|
+
data: { ingested: totalIngested },
|
|
1559
|
+
error: result.error
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
totalIngested += result.data?.ingested ?? chunk.length;
|
|
1563
|
+
}
|
|
1564
|
+
return { data: { ingested: totalIngested }, error: null };
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Query logs with filters. Returns paginated response.
|
|
1568
|
+
*/
|
|
1569
|
+
async queryLogs(filters, requestOptions) {
|
|
1570
|
+
return this._get(this.withQuery("/logs", filters), requestOptions);
|
|
1571
|
+
}
|
|
1572
|
+
// Convenience methods
|
|
1573
|
+
async debug(service, message, meta, options) {
|
|
1574
|
+
return this.log({ service, severity: "debug", message, metadata: meta }, options);
|
|
1575
|
+
}
|
|
1576
|
+
async info(service, message, meta, options) {
|
|
1577
|
+
return this.log({ service, severity: "info", message, metadata: meta }, options);
|
|
1578
|
+
}
|
|
1579
|
+
async warn(service, message, meta, options) {
|
|
1580
|
+
return this.log({ service, severity: "warn", message, metadata: meta }, options);
|
|
1581
|
+
}
|
|
1582
|
+
async error(service, message, meta, options) {
|
|
1583
|
+
return this.log({ service, severity: "error", message, metadata: meta }, options);
|
|
1584
|
+
}
|
|
1585
|
+
// --------------------------------------------------------------------------
|
|
1586
|
+
// Internal
|
|
1587
|
+
// --------------------------------------------------------------------------
|
|
1588
|
+
/** Normalize legacy { level, message } to { severity, service, message } */
|
|
1589
|
+
normalizeLogInput(data) {
|
|
1590
|
+
if ("severity" in data && "service" in data) {
|
|
1591
|
+
return data;
|
|
1592
|
+
}
|
|
1593
|
+
const legacy = data;
|
|
1594
|
+
return {
|
|
1595
|
+
service: "sdk",
|
|
1596
|
+
severity: legacy.level || "info",
|
|
1597
|
+
message: legacy.message,
|
|
1598
|
+
metadata: legacy.metadata
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
// src/services/upload-telemetry.ts
|
|
1604
|
+
var EVENT_SEVERITY = {
|
|
1605
|
+
"upload.failed": "error",
|
|
1606
|
+
"upload.stalled": "error",
|
|
1607
|
+
"upload.multipart.aborted": "error",
|
|
1608
|
+
"upload.retried": "warn",
|
|
1609
|
+
"upload.aborted": "warn",
|
|
1610
|
+
"upload.multipart.part_failed": "warn",
|
|
1611
|
+
"upload.multipart.url_refreshed": "warn",
|
|
1612
|
+
"upload.compression.skipped": "warn",
|
|
1613
|
+
"upload.started": "info",
|
|
1614
|
+
"upload.completed": "info",
|
|
1615
|
+
"upload.resumed": "info",
|
|
1616
|
+
"upload.multipart.started": "info",
|
|
1617
|
+
"upload.multipart.completed": "info",
|
|
1618
|
+
"upload.compression.completed": "info",
|
|
1619
|
+
"upload.progress": "debug",
|
|
1620
|
+
"upload.multipart.part_completed": "debug",
|
|
1621
|
+
"upload.compression.started": "debug"
|
|
1622
|
+
};
|
|
1623
|
+
var UploadTelemetry = class {
|
|
1624
|
+
constructor(client, config) {
|
|
1625
|
+
this.buffer = [];
|
|
1626
|
+
this.debugLogBuffer = [];
|
|
1627
|
+
this.flushTimer = null;
|
|
1628
|
+
this.flushing = false;
|
|
1629
|
+
this.client = client;
|
|
1630
|
+
this.logger = new LoggerService(client);
|
|
1631
|
+
this.config = {
|
|
1632
|
+
enabled: config?.enabled ?? true,
|
|
1633
|
+
flushIntervalMs: config?.flushIntervalMs ?? 2e3,
|
|
1634
|
+
maxBufferSize: config?.maxBufferSize ?? 50
|
|
1635
|
+
};
|
|
1636
|
+
if (this.config.enabled) {
|
|
1637
|
+
this.startFlushTimer();
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
/** Emit a telemetry event. Never throws. */
|
|
1641
|
+
emit(sessionId, event, metadata = {}) {
|
|
1642
|
+
if (!this.config.enabled) return;
|
|
1643
|
+
const payload = {
|
|
1644
|
+
upload_session_id: sessionId,
|
|
1645
|
+
event,
|
|
1646
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1647
|
+
metadata
|
|
1648
|
+
};
|
|
1649
|
+
const severity = EVENT_SEVERITY[event] || "info";
|
|
1650
|
+
if (severity !== "debug") {
|
|
1651
|
+
this.sendToLogger(payload, severity);
|
|
1652
|
+
} else {
|
|
1653
|
+
this.debugLogBuffer.push({
|
|
1654
|
+
service: "storage.upload",
|
|
1655
|
+
severity,
|
|
1656
|
+
message: `Upload ${event}: session=${sessionId}`,
|
|
1657
|
+
metadata: { upload_session_id: sessionId, event, ...metadata },
|
|
1658
|
+
trace_id: sessionId
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
this.buffer.push(payload);
|
|
1662
|
+
if (this.buffer.length >= this.config.maxBufferSize) {
|
|
1663
|
+
this.flush();
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
/** Flush buffered events immediately. Never throws. */
|
|
1667
|
+
async flush() {
|
|
1668
|
+
if (!this.config.enabled || this.buffer.length === 0 && this.debugLogBuffer.length === 0 || this.flushing) return;
|
|
1669
|
+
this.flushing = true;
|
|
1670
|
+
const batch = this.buffer.splice(0);
|
|
1671
|
+
const debugLogs = this.debugLogBuffer.splice(0);
|
|
1672
|
+
try {
|
|
1673
|
+
if (batch.length > 0) {
|
|
1674
|
+
const events = batch.map((p) => ({
|
|
1675
|
+
event: p.event,
|
|
1676
|
+
properties: {
|
|
1677
|
+
upload_session_id: p.upload_session_id,
|
|
1678
|
+
...p.metadata
|
|
1679
|
+
},
|
|
1680
|
+
timestamp: p.timestamp
|
|
1681
|
+
}));
|
|
1682
|
+
await this.client.post("/v1/analytics/v2/events/batch", { events }).catch(() => {
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
if (debugLogs.length > 0) {
|
|
1686
|
+
await this.logger.logBatch(debugLogs).catch(() => {
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
} catch {
|
|
1690
|
+
} finally {
|
|
1691
|
+
this.flushing = false;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
/** Stop the flush timer and drain remaining events. */
|
|
1695
|
+
async destroy() {
|
|
1696
|
+
if (this.flushTimer !== null) {
|
|
1697
|
+
clearInterval(this.flushTimer);
|
|
1698
|
+
this.flushTimer = null;
|
|
1699
|
+
}
|
|
1700
|
+
await this.flush();
|
|
1701
|
+
}
|
|
1702
|
+
startFlushTimer() {
|
|
1703
|
+
if (typeof setInterval !== "undefined") {
|
|
1704
|
+
this.flushTimer = setInterval(() => {
|
|
1705
|
+
this.flush();
|
|
1706
|
+
}, this.config.flushIntervalMs);
|
|
1707
|
+
if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) {
|
|
1708
|
+
this.flushTimer.unref();
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
/** Send a log entry to the logger service (fire-and-forget) */
|
|
1713
|
+
sendToLogger(payload, severity) {
|
|
1714
|
+
this.logger.log({
|
|
1715
|
+
service: "storage.upload",
|
|
1716
|
+
severity,
|
|
1717
|
+
message: `Upload ${payload.event}: session=${payload.upload_session_id}`,
|
|
1718
|
+
metadata: {
|
|
1719
|
+
upload_session_id: payload.upload_session_id,
|
|
1720
|
+
event: payload.event,
|
|
1721
|
+
...payload.metadata
|
|
1722
|
+
},
|
|
1723
|
+
trace_id: payload.upload_session_id
|
|
1724
|
+
}).catch(() => {
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
function generateUploadSessionId() {
|
|
1729
|
+
const timestamp = Date.now().toString(36);
|
|
1730
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1731
|
+
return `us_${timestamp}_${random}`;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// src/services/storage.ts
|
|
1735
|
+
var RETRY_DELAYS = [0, 1e3, 3e3];
|
|
1736
|
+
var RETRYABLE_STATUS_CODES2 = /* @__PURE__ */ new Set([500, 502, 503, 504]);
|
|
1737
|
+
var NON_RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([400, 403, 404, 413]);
|
|
1738
|
+
var DEFAULT_STALL_TIMEOUT_MS = 45e3;
|
|
1739
|
+
var SLOW_NETWORK_STALL_TIMEOUT_MS = 9e4;
|
|
1740
|
+
var MULTIPART_THRESHOLD = 8 * 1024 * 1024;
|
|
1741
|
+
var MULTIPART_THRESHOLD_SLOW = 4 * 1024 * 1024;
|
|
1742
|
+
var StorageService = class extends ServiceModule {
|
|
1743
|
+
constructor() {
|
|
1744
|
+
super(...arguments);
|
|
1745
|
+
this.basePath = "/v1/storage";
|
|
1746
|
+
this.telemetry = null;
|
|
1747
|
+
}
|
|
1748
|
+
// --------------------------------------------------------------------------
|
|
1749
|
+
// Upload (unified: direct PUT or multipart, transparent to caller)
|
|
1750
|
+
// --------------------------------------------------------------------------
|
|
1751
|
+
/**
|
|
1752
|
+
* Upload a file using the optimal strategy.
|
|
1753
|
+
*
|
|
1754
|
+
* Small files (< 8MB): 3-step presigned URL flow with retry + stall guard.
|
|
1755
|
+
* Large files (>= 8MB): Multipart with windowed presigns, resumable.
|
|
1756
|
+
*
|
|
1757
|
+
* @returns The completed file record with id, url, etc.
|
|
1758
|
+
*/
|
|
1759
|
+
async upload(file, options) {
|
|
1760
|
+
if (options?.signal?.aborted) {
|
|
1761
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
1762
|
+
}
|
|
1763
|
+
const sessionId = generateUploadSessionId();
|
|
1764
|
+
const telemetryEnabled = options?.telemetry !== false;
|
|
1765
|
+
const telemetry = telemetryEnabled ? this.getOrCreateTelemetry() : null;
|
|
1766
|
+
const startTime = Date.now();
|
|
1767
|
+
telemetry?.emit(sessionId, "upload.started", {
|
|
1768
|
+
size_bytes: file.size,
|
|
1769
|
+
content_type: file.type,
|
|
1770
|
+
strategy: this.shouldUseMultipart(file, options) ? "multipart" : "direct",
|
|
1771
|
+
network_type: getNetworkEffectiveType2()
|
|
1772
|
+
});
|
|
1773
|
+
try {
|
|
1774
|
+
let uploadFile = file;
|
|
1775
|
+
if (!options?.skipCompression && typeof window !== "undefined") {
|
|
1776
|
+
const compressed = await this.maybeCompress(file, options?.compression, sessionId, telemetry);
|
|
1777
|
+
if (compressed) uploadFile = compressed;
|
|
1778
|
+
}
|
|
1779
|
+
if (this.shouldUseMultipart(uploadFile, options)) {
|
|
1780
|
+
return await this.uploadMultipart(uploadFile, options, sessionId, telemetry);
|
|
1781
|
+
}
|
|
1782
|
+
return await this.uploadDirect(uploadFile, options, sessionId, telemetry);
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
const message = err instanceof Error ? err.message : "Upload failed";
|
|
1785
|
+
telemetry?.emit(sessionId, "upload.failed", { error: message, duration_ms: Date.now() - startTime });
|
|
1786
|
+
return { data: null, error: { code: "upload_error", message, status: 0 } };
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
// --------------------------------------------------------------------------
|
|
1790
|
+
// Direct Upload (3-step with retry + stall)
|
|
1791
|
+
// --------------------------------------------------------------------------
|
|
1792
|
+
async uploadDirect(file, options, sessionId, telemetry) {
|
|
1793
|
+
const directStart = Date.now();
|
|
1794
|
+
const requestOpts = this.withSessionHeader(sessionId, options);
|
|
1795
|
+
const filename = options?.filename || file.name || "file";
|
|
1796
|
+
const initResult = await this.post("/signed-url/upload", {
|
|
1797
|
+
filename,
|
|
1798
|
+
content_type: file.type || "application/octet-stream",
|
|
1799
|
+
size_bytes: file.size,
|
|
1800
|
+
is_public: options?.isPublic ?? true,
|
|
1801
|
+
metadata: options?.metadata
|
|
1802
|
+
}, requestOpts);
|
|
1803
|
+
if (initResult.error) {
|
|
1804
|
+
telemetry?.emit(sessionId, "upload.failed", { step: "presign", error: initResult.error.message });
|
|
1805
|
+
return { data: null, error: initResult.error };
|
|
1806
|
+
}
|
|
1807
|
+
const { file_id, upload_url, completion_token } = initResult.data;
|
|
1808
|
+
const uploadResult = await this.uploadToPresignedUrlWithRetry(
|
|
1809
|
+
upload_url,
|
|
1810
|
+
file,
|
|
1811
|
+
options?.onProgress,
|
|
1812
|
+
options?.signal,
|
|
1813
|
+
sessionId,
|
|
1814
|
+
telemetry
|
|
1815
|
+
);
|
|
1816
|
+
if (uploadResult.error) {
|
|
1817
|
+
if (uploadResult.error.code === "upload_stalled") {
|
|
1818
|
+
telemetry?.emit(sessionId, "upload.stalled", { step: "s3_put", file_id });
|
|
1819
|
+
}
|
|
1820
|
+
telemetry?.emit(sessionId, "upload.failed", {
|
|
1821
|
+
step: "s3_put",
|
|
1822
|
+
error: uploadResult.error.message,
|
|
1823
|
+
file_id,
|
|
1824
|
+
reason: uploadResult.error.code
|
|
1825
|
+
});
|
|
1826
|
+
return { data: null, error: uploadResult.error };
|
|
1827
|
+
}
|
|
1828
|
+
const completeResult = await this.post("/signed-url/complete", {
|
|
1829
|
+
file_id,
|
|
1830
|
+
completion_token
|
|
1831
|
+
}, requestOpts);
|
|
1832
|
+
if (completeResult.error) {
|
|
1833
|
+
telemetry?.emit(sessionId, "upload.failed", { step: "complete", error: completeResult.error.message, file_id, duration_ms: Date.now() - directStart });
|
|
1834
|
+
} else {
|
|
1835
|
+
telemetry?.emit(sessionId, "upload.completed", { file_id, size_bytes: file.size, duration_ms: Date.now() - directStart });
|
|
1836
|
+
}
|
|
1837
|
+
return completeResult;
|
|
1838
|
+
}
|
|
1839
|
+
// --------------------------------------------------------------------------
|
|
1840
|
+
// Multipart Upload
|
|
1841
|
+
// --------------------------------------------------------------------------
|
|
1842
|
+
async uploadMultipart(file, options, sessionId, telemetry) {
|
|
1843
|
+
const multipartStart = Date.now();
|
|
1844
|
+
const requestOpts = this.withSessionHeader(sessionId, options);
|
|
1845
|
+
const filename = options?.filename || file.name || "file";
|
|
1846
|
+
telemetry?.emit(sessionId, "upload.multipart.started", { size_bytes: file.size });
|
|
1847
|
+
let resumeStore = null;
|
|
1848
|
+
let resumeData = null;
|
|
1849
|
+
if (options?.resume !== "off" && typeof window !== "undefined") {
|
|
1850
|
+
try {
|
|
1851
|
+
const { UploadResumeStore: UploadResumeStore2 } = await Promise.resolve().then(() => (init_upload_resume(), upload_resume_exports));
|
|
1852
|
+
resumeStore = new UploadResumeStore2();
|
|
1853
|
+
await resumeStore.open();
|
|
1854
|
+
const resumeKey = await UploadResumeStore2.generateResumeKey(
|
|
1855
|
+
this.client.getApiKey?.() || "",
|
|
1856
|
+
this.client.getUserId?.() || "",
|
|
1857
|
+
filename,
|
|
1858
|
+
file.size,
|
|
1859
|
+
file.lastModified
|
|
1860
|
+
);
|
|
1861
|
+
resumeData = await resumeStore.get(resumeKey);
|
|
1862
|
+
if (resumeData) {
|
|
1863
|
+
telemetry?.emit(sessionId, "upload.resumed", {
|
|
1864
|
+
original_session_id: resumeData.upload_session_id,
|
|
1865
|
+
completed_parts: resumeData.completed_parts.length,
|
|
1866
|
+
total_parts: resumeData.total_parts
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
} catch (err) {
|
|
1870
|
+
telemetry?.emit(sessionId, "upload.retried", { step: "resume_load", error: err instanceof Error ? err.message : "Resume store unavailable" });
|
|
1871
|
+
resumeStore = null;
|
|
1872
|
+
resumeData = null;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
let startData;
|
|
1876
|
+
let completionToken;
|
|
1877
|
+
let completedParts = /* @__PURE__ */ new Map();
|
|
1878
|
+
if (resumeData) {
|
|
1879
|
+
startData = {
|
|
1880
|
+
upload_session_id: resumeData.upload_session_id,
|
|
1881
|
+
file_id: resumeData.file_id,
|
|
1882
|
+
completion_token: "",
|
|
1883
|
+
// will request part URLs separately
|
|
1884
|
+
part_size_bytes: resumeData.part_size_bytes || this.defaultChunkSize(file.size),
|
|
1885
|
+
total_parts: resumeData.total_parts,
|
|
1886
|
+
part_urls: [],
|
|
1887
|
+
expires_at: ""
|
|
1888
|
+
};
|
|
1889
|
+
completionToken = resumeData.completion_token;
|
|
1890
|
+
for (const part of resumeData.completed_parts) {
|
|
1891
|
+
completedParts.set(part.part_number, part.etag);
|
|
1892
|
+
}
|
|
1893
|
+
} else {
|
|
1894
|
+
let clientUploadKey;
|
|
1895
|
+
try {
|
|
1896
|
+
const { UploadResumeStore: UploadResumeStore2 } = await Promise.resolve().then(() => (init_upload_resume(), upload_resume_exports));
|
|
1897
|
+
clientUploadKey = await UploadResumeStore2.generateResumeKey(
|
|
1898
|
+
this.client.getApiKey?.() || "",
|
|
1899
|
+
this.client.getUserId?.() || "",
|
|
1900
|
+
filename,
|
|
1901
|
+
file.size,
|
|
1902
|
+
file.lastModified
|
|
1903
|
+
);
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
const startResult = await this.post("/signed-url/multipart/start", {
|
|
1907
|
+
filename,
|
|
1908
|
+
content_type: file.type || "application/octet-stream",
|
|
1909
|
+
size_bytes: file.size,
|
|
1910
|
+
is_public: options?.isPublic ?? true,
|
|
1911
|
+
metadata: options?.metadata,
|
|
1912
|
+
chunk_size: options?.chunkSize,
|
|
1913
|
+
...clientUploadKey ? { client_upload_key: clientUploadKey } : {}
|
|
1914
|
+
}, requestOpts);
|
|
1915
|
+
if (startResult.error) {
|
|
1916
|
+
telemetry?.emit(sessionId, "upload.multipart.aborted", { error: startResult.error.message });
|
|
1917
|
+
return { data: null, error: startResult.error };
|
|
1918
|
+
}
|
|
1919
|
+
startData = startResult.data;
|
|
1920
|
+
completionToken = startData.completion_token;
|
|
1921
|
+
}
|
|
1922
|
+
const { upload_session_id, file_id, part_size_bytes, total_parts } = startData;
|
|
1923
|
+
if (resumeStore && !resumeData) {
|
|
1924
|
+
try {
|
|
1925
|
+
const { UploadResumeStore: UploadResumeStore2 } = await Promise.resolve().then(() => (init_upload_resume(), upload_resume_exports));
|
|
1926
|
+
const resumeKey = await UploadResumeStore2.generateResumeKey(
|
|
1927
|
+
this.client.getApiKey?.() || "",
|
|
1928
|
+
this.client.getUserId?.() || "",
|
|
1929
|
+
filename,
|
|
1930
|
+
file.size,
|
|
1931
|
+
file.lastModified
|
|
1932
|
+
);
|
|
1933
|
+
await resumeStore.save(resumeKey, {
|
|
1934
|
+
upload_session_id,
|
|
1935
|
+
file_id,
|
|
1936
|
+
completion_token: completionToken,
|
|
1937
|
+
total_parts,
|
|
1938
|
+
part_size_bytes,
|
|
1939
|
+
completed_parts: [],
|
|
1940
|
+
created_at: Date.now()
|
|
1941
|
+
});
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
telemetry?.emit(sessionId, "upload.retried", { step: "resume_save", error: err instanceof Error ? err.message : "Resume save failed" });
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
const maxConcurrency = options?.maxConcurrency || this.defaultConcurrency();
|
|
1947
|
+
let availableUrls = /* @__PURE__ */ new Map();
|
|
1948
|
+
if (startData.part_urls.length > 0) {
|
|
1949
|
+
for (const pu of startData.part_urls) {
|
|
1950
|
+
availableUrls.set(pu.part_number, pu);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
const pendingParts = [];
|
|
1954
|
+
for (let i = 1; i <= total_parts; i++) {
|
|
1955
|
+
if (!completedParts.has(i)) {
|
|
1956
|
+
pendingParts.push(i);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
let uploadedCount = completedParts.size;
|
|
1960
|
+
let lastProgressMilestone = 0;
|
|
1961
|
+
const reportProgress = () => {
|
|
1962
|
+
const percent = Math.round(uploadedCount / total_parts * 100);
|
|
1963
|
+
if (options?.onProgress) {
|
|
1964
|
+
options.onProgress(percent);
|
|
1965
|
+
}
|
|
1966
|
+
const milestone = Math.floor(percent / 25) * 25;
|
|
1967
|
+
if (milestone > lastProgressMilestone && milestone < 100) {
|
|
1968
|
+
telemetry?.emit(sessionId, "upload.progress", { percent: milestone, uploaded_parts: uploadedCount, total_parts });
|
|
1969
|
+
lastProgressMilestone = milestone;
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
reportProgress();
|
|
1973
|
+
let partIndex = 0;
|
|
1974
|
+
while (partIndex < pendingParts.length) {
|
|
1975
|
+
if (options?.signal?.aborted) {
|
|
1976
|
+
await this.abortMultipart(upload_session_id, completionToken, requestOpts);
|
|
1977
|
+
telemetry?.emit(sessionId, "upload.aborted", { file_id });
|
|
1978
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
1979
|
+
}
|
|
1980
|
+
const remainingUrlCount = pendingParts.slice(partIndex).filter((p) => availableUrls.has(p)).length;
|
|
1981
|
+
if (remainingUrlCount <= 4) {
|
|
1982
|
+
const neededParts = pendingParts.slice(partIndex, partIndex + 16).filter((p) => !availableUrls.has(p));
|
|
1983
|
+
if (neededParts.length > 0) {
|
|
1984
|
+
const urlResult = await this.post(
|
|
1985
|
+
"/signed-url/multipart/part-urls",
|
|
1986
|
+
{ upload_session_id, part_numbers: neededParts, completion_token: completionToken },
|
|
1987
|
+
requestOpts
|
|
1988
|
+
);
|
|
1989
|
+
if (urlResult.data) {
|
|
1990
|
+
for (const pu of urlResult.data.part_urls) {
|
|
1991
|
+
availableUrls.set(pu.part_number, pu);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
const batch = pendingParts.slice(partIndex, partIndex + maxConcurrency);
|
|
1997
|
+
const batchPromises = batch.map(async (partNum) => {
|
|
1998
|
+
const partUrl = availableUrls.get(partNum);
|
|
1999
|
+
if (!partUrl) {
|
|
2000
|
+
const urlResult = await this.post(
|
|
2001
|
+
"/signed-url/multipart/part-urls",
|
|
2002
|
+
{ upload_session_id, part_numbers: [partNum], completion_token: completionToken },
|
|
2003
|
+
requestOpts
|
|
2004
|
+
);
|
|
2005
|
+
if (!urlResult.data?.part_urls?.[0]) {
|
|
2006
|
+
return { partNum, error: "Failed to get part URL" };
|
|
2007
|
+
}
|
|
2008
|
+
availableUrls.set(partNum, urlResult.data.part_urls[0]);
|
|
2009
|
+
}
|
|
2010
|
+
const url = availableUrls.get(partNum).url;
|
|
2011
|
+
const start = (partNum - 1) * part_size_bytes;
|
|
2012
|
+
const end = Math.min(start + part_size_bytes, file.size);
|
|
2013
|
+
const partBlob = file.slice(start, end);
|
|
2014
|
+
const result = await this.uploadPartWithRetry(url, partBlob, options?.signal, partNum, sessionId, telemetry);
|
|
2015
|
+
if (result.error) {
|
|
2016
|
+
if (result.code === "upload_stalled") {
|
|
2017
|
+
telemetry?.emit(sessionId, "upload.stalled", { step: "multipart_part", part_number: partNum });
|
|
2018
|
+
}
|
|
2019
|
+
telemetry?.emit(sessionId, "upload.multipart.part_failed", {
|
|
2020
|
+
part_number: partNum,
|
|
2021
|
+
error: result.error,
|
|
2022
|
+
code: result.code
|
|
2023
|
+
});
|
|
2024
|
+
if (result.status === 403) {
|
|
2025
|
+
telemetry?.emit(sessionId, "upload.multipart.url_refreshed", { part_number: partNum });
|
|
2026
|
+
const refreshResult = await this.post(
|
|
2027
|
+
"/signed-url/multipart/part-urls",
|
|
2028
|
+
{ upload_session_id, part_numbers: [partNum], completion_token: completionToken },
|
|
2029
|
+
requestOpts
|
|
2030
|
+
);
|
|
2031
|
+
if (refreshResult.data?.part_urls?.[0]) {
|
|
2032
|
+
availableUrls.set(partNum, refreshResult.data.part_urls[0]);
|
|
2033
|
+
const retryResult = await this.uploadPartWithRetry(
|
|
2034
|
+
refreshResult.data.part_urls[0].url,
|
|
2035
|
+
partBlob,
|
|
2036
|
+
options?.signal,
|
|
2037
|
+
partNum,
|
|
2038
|
+
sessionId,
|
|
2039
|
+
telemetry
|
|
2040
|
+
);
|
|
2041
|
+
if (retryResult.etag) {
|
|
2042
|
+
return { partNum, etag: retryResult.etag };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return { partNum, error: result.error };
|
|
2047
|
+
}
|
|
2048
|
+
return { partNum, etag: result.etag };
|
|
2049
|
+
});
|
|
2050
|
+
const results = await Promise.all(batchPromises);
|
|
2051
|
+
for (const result of results) {
|
|
2052
|
+
if (result.etag) {
|
|
2053
|
+
completedParts.set(result.partNum, result.etag);
|
|
2054
|
+
uploadedCount++;
|
|
2055
|
+
telemetry?.emit(sessionId, "upload.multipart.part_completed", { part_number: result.partNum });
|
|
2056
|
+
if (resumeStore) {
|
|
2057
|
+
try {
|
|
2058
|
+
const { UploadResumeStore: UploadResumeStore2 } = await Promise.resolve().then(() => (init_upload_resume(), upload_resume_exports));
|
|
2059
|
+
const resumeKey = await UploadResumeStore2.generateResumeKey(
|
|
2060
|
+
this.client.getApiKey?.() || "",
|
|
2061
|
+
this.client.getUserId?.() || "",
|
|
2062
|
+
filename,
|
|
2063
|
+
file.size,
|
|
2064
|
+
file.lastModified
|
|
2065
|
+
);
|
|
2066
|
+
await resumeStore.updatePart(resumeKey, result.partNum, result.etag);
|
|
2067
|
+
} catch (err) {
|
|
2068
|
+
telemetry?.emit(sessionId, "upload.retried", { step: "resume_update", part_number: result.partNum, error: err instanceof Error ? err.message : "Resume update failed" });
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
} else {
|
|
2072
|
+
const errorMsg = result.error || "Part upload returned no ETag";
|
|
2073
|
+
await this.abortMultipart(upload_session_id, completionToken, requestOpts);
|
|
2074
|
+
telemetry?.emit(sessionId, "upload.multipart.aborted", { file_id, error: errorMsg });
|
|
2075
|
+
return {
|
|
2076
|
+
data: null,
|
|
2077
|
+
error: { code: "upload_error", message: `Part ${result.partNum} failed: ${errorMsg}`, status: 0 }
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
reportProgress();
|
|
2082
|
+
partIndex += batch.length;
|
|
2083
|
+
}
|
|
2084
|
+
const parts = Array.from(completedParts.entries()).sort(([a], [b]) => a - b).map(([part_number, etag]) => ({ part_number, etag }));
|
|
2085
|
+
const completeResult = await this.post(
|
|
2086
|
+
"/signed-url/multipart/complete",
|
|
2087
|
+
{ upload_session_id, file_id, completion_token: completionToken, parts },
|
|
2088
|
+
requestOpts
|
|
2089
|
+
);
|
|
2090
|
+
if (resumeStore) {
|
|
2091
|
+
try {
|
|
2092
|
+
const { UploadResumeStore: UploadResumeStore2 } = await Promise.resolve().then(() => (init_upload_resume(), upload_resume_exports));
|
|
2093
|
+
const resumeKey = await UploadResumeStore2.generateResumeKey(
|
|
2094
|
+
this.client.getApiKey?.() || "",
|
|
2095
|
+
this.client.getUserId?.() || "",
|
|
2096
|
+
filename,
|
|
2097
|
+
file.size,
|
|
2098
|
+
file.lastModified
|
|
2099
|
+
);
|
|
2100
|
+
await resumeStore.remove(resumeKey);
|
|
2101
|
+
} catch (err) {
|
|
2102
|
+
telemetry?.emit(sessionId, "upload.retried", { step: "resume_cleanup", error: err instanceof Error ? err.message : "Resume cleanup failed" });
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (completeResult.error) {
|
|
2106
|
+
telemetry?.emit(sessionId, "upload.multipart.aborted", { file_id, error: completeResult.error.message, duration_ms: Date.now() - multipartStart });
|
|
2107
|
+
return { data: null, error: completeResult.error };
|
|
2108
|
+
}
|
|
2109
|
+
telemetry?.emit(sessionId, "upload.multipart.completed", { file_id, size_bytes: file.size, duration_ms: Date.now() - multipartStart });
|
|
2110
|
+
telemetry?.emit(sessionId, "upload.completed", { file_id, size_bytes: file.size, duration_ms: Date.now() - multipartStart });
|
|
2111
|
+
options?.onProgress?.(100);
|
|
2112
|
+
const d = completeResult.data;
|
|
2113
|
+
return {
|
|
2114
|
+
data: {
|
|
2115
|
+
id: d.file_id,
|
|
2116
|
+
filename: d.filename,
|
|
2117
|
+
content_type: d.content_type,
|
|
2118
|
+
size_bytes: d.size_bytes,
|
|
2119
|
+
url: d.url,
|
|
2120
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2121
|
+
},
|
|
2122
|
+
error: null
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
// --------------------------------------------------------------------------
|
|
2126
|
+
// Multipart Abort
|
|
2127
|
+
// --------------------------------------------------------------------------
|
|
2128
|
+
async abortMultipart(uploadSessionId, completionToken, requestOpts) {
|
|
2129
|
+
try {
|
|
2130
|
+
await this.post("/signed-url/multipart/abort", {
|
|
2131
|
+
upload_session_id: uploadSessionId,
|
|
2132
|
+
...completionToken ? { completion_token: completionToken } : {}
|
|
2133
|
+
}, requestOpts);
|
|
2134
|
+
} catch {
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
// --------------------------------------------------------------------------
|
|
2138
|
+
// Split Upload (server-side: presigned URL → client S3 upload → complete)
|
|
2139
|
+
// --------------------------------------------------------------------------
|
|
2140
|
+
/**
|
|
2141
|
+
* Get a presigned URL for direct upload to S3.
|
|
2142
|
+
* Use this when the browser uploads directly (with progress tracking)
|
|
2143
|
+
* and the server only brokers the URLs.
|
|
2144
|
+
*
|
|
2145
|
+
* Flow: server calls getUploadUrl() → returns URL to client → client PUTs to S3 → server calls completeUpload()
|
|
2146
|
+
*/
|
|
2147
|
+
async getUploadUrl(filename, contentType, options, requestOptions) {
|
|
2148
|
+
return this.post("/signed-url/upload", {
|
|
2149
|
+
filename,
|
|
2150
|
+
content_type: contentType,
|
|
2151
|
+
is_public: options?.isPublic ?? true,
|
|
2152
|
+
expires_in: options?.expiresIn ?? 3600,
|
|
2153
|
+
metadata: options?.metadata
|
|
2154
|
+
}, requestOptions);
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Complete a presigned upload after the file has been uploaded to S3.
|
|
2158
|
+
* Triggers scan and makes the file available.
|
|
2159
|
+
*/
|
|
2160
|
+
async completeUpload(fileId, completionToken, options, requestOptions) {
|
|
2161
|
+
return this.post("/signed-url/complete", {
|
|
2162
|
+
file_id: fileId,
|
|
2163
|
+
completion_token: completionToken,
|
|
2164
|
+
size_bytes: options?.sizeBytes,
|
|
2165
|
+
checksum: options?.checksum
|
|
2166
|
+
}, requestOptions);
|
|
2167
|
+
}
|
|
2168
|
+
// --------------------------------------------------------------------------
|
|
2169
|
+
// Multipart Public API (for advanced/server-side usage)
|
|
2170
|
+
// --------------------------------------------------------------------------
|
|
2171
|
+
/** Start a multipart upload session. */
|
|
2172
|
+
async startMultipartUpload(params, requestOptions) {
|
|
2173
|
+
return this.post("/signed-url/multipart/start", params, requestOptions);
|
|
2174
|
+
}
|
|
2175
|
+
/** Get presigned URLs for specific part numbers. */
|
|
2176
|
+
async getMultipartPartUrls(uploadSessionId, partNumbers, completionToken, requestOptions) {
|
|
2177
|
+
return this.post("/signed-url/multipart/part-urls", {
|
|
2178
|
+
upload_session_id: uploadSessionId,
|
|
2179
|
+
part_numbers: partNumbers,
|
|
2180
|
+
...completionToken ? { completion_token: completionToken } : {}
|
|
2181
|
+
}, requestOptions);
|
|
2182
|
+
}
|
|
2183
|
+
/** Complete a multipart upload. */
|
|
2184
|
+
async completeMultipartUpload(params, requestOptions) {
|
|
2185
|
+
return this.post("/signed-url/multipart/complete", params, requestOptions);
|
|
2186
|
+
}
|
|
2187
|
+
/** Abort a multipart upload. */
|
|
2188
|
+
async abortMultipartUpload(uploadSessionId, completionToken, requestOptions) {
|
|
2189
|
+
return this.post("/signed-url/multipart/abort", {
|
|
2190
|
+
upload_session_id: uploadSessionId,
|
|
2191
|
+
...completionToken ? { completion_token: completionToken } : {}
|
|
2192
|
+
}, requestOptions);
|
|
2193
|
+
}
|
|
2194
|
+
// --------------------------------------------------------------------------
|
|
2195
|
+
// File Operations
|
|
2196
|
+
// --------------------------------------------------------------------------
|
|
2197
|
+
/** Get file metadata (no signed URL). */
|
|
2198
|
+
async getInfo(fileId, options) {
|
|
2199
|
+
return this._get(`/files/${fileId}/info`, options);
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Get a signed view URL for inline display (img src, thumbnails).
|
|
2203
|
+
* Returns CloudFront signed URL (fast, ~1us) or S3 presigned fallback.
|
|
2204
|
+
*/
|
|
2205
|
+
async getViewUrl(fileId, options) {
|
|
2206
|
+
return this.post(`/signed-url/view/${fileId}`, {}, options);
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Get signed view URLs for multiple files (batch, up to 100).
|
|
2210
|
+
* Single network call, returns all URLs.
|
|
2211
|
+
* The shared `expires_at` is a conservative lower bound — reflects the shortest-lived
|
|
2212
|
+
* URL in the batch. Individual URLs may remain valid longer if their files are public.
|
|
2213
|
+
*/
|
|
2214
|
+
async getViewUrls(fileIds, options) {
|
|
2215
|
+
return this.post("/signed-url/view-batch", { file_ids: fileIds }, options);
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Get a signed download URL (Content-Disposition: attachment).
|
|
2219
|
+
*/
|
|
2220
|
+
async getDownloadUrl(fileId, options) {
|
|
2221
|
+
return this.post(`/signed-url/download/${fileId}`, void 0, options);
|
|
2222
|
+
}
|
|
2223
|
+
/** Delete a file (soft delete). */
|
|
2224
|
+
async delete(fileId, options) {
|
|
2225
|
+
return this.del(`/files/${fileId}`, options);
|
|
2226
|
+
}
|
|
2227
|
+
/** List the current user's files (paginated). */
|
|
2228
|
+
async list(params, options) {
|
|
2229
|
+
return this.listMethod("/my-files", params, options);
|
|
2230
|
+
}
|
|
2231
|
+
/** Check file view/access status. */
|
|
2232
|
+
async getViewStatus(fileId, options) {
|
|
2233
|
+
return this._get(`/files/${fileId}/view-status`, options);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Update a file's visibility (public/private).
|
|
2237
|
+
* Only the file owner can toggle this. Changes URL TTL — does not move the S3 object.
|
|
2238
|
+
* Public files get 7-day signed URLs; private files get 1-hour signed URLs.
|
|
2239
|
+
*/
|
|
2240
|
+
async updateVisibility(fileId, isPublic, options) {
|
|
2241
|
+
return this.patch(`/files/${fileId}/visibility`, { is_public: isPublic }, options);
|
|
2242
|
+
}
|
|
2243
|
+
// --------------------------------------------------------------------------
|
|
2244
|
+
// Legacy methods (backward compat)
|
|
2245
|
+
// --------------------------------------------------------------------------
|
|
2246
|
+
/** @deprecated Use upload() instead */
|
|
2247
|
+
async uploadFile(file, options) {
|
|
2248
|
+
return this.upload(file, {
|
|
2249
|
+
isPublic: options?.is_public,
|
|
2250
|
+
metadata: options?.metadata,
|
|
2251
|
+
onProgress: options?.onProgress,
|
|
2252
|
+
signal: options?.signal
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
/** @deprecated Use getInfo() instead */
|
|
2256
|
+
async getFile(id) {
|
|
2257
|
+
return this.getInfo(id);
|
|
2258
|
+
}
|
|
2259
|
+
/** @deprecated Use delete() instead */
|
|
2260
|
+
async deleteFile(id) {
|
|
2261
|
+
return this.delete(id);
|
|
2262
|
+
}
|
|
2263
|
+
/** @deprecated Use list() instead */
|
|
2264
|
+
async listFiles(params) {
|
|
2265
|
+
return this.list(params);
|
|
2266
|
+
}
|
|
2267
|
+
// --------------------------------------------------------------------------
|
|
2268
|
+
// Private: Upload to presigned URL with retry + stall guard
|
|
2269
|
+
// --------------------------------------------------------------------------
|
|
2270
|
+
async uploadToPresignedUrlWithRetry(url, file, onProgress, signal, sessionId, telemetry) {
|
|
2271
|
+
for (let attempt = 0; attempt < RETRY_DELAYS.length; attempt++) {
|
|
2272
|
+
if (signal?.aborted) {
|
|
2273
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
2274
|
+
}
|
|
2275
|
+
if (attempt > 0) {
|
|
2276
|
+
const delay = RETRY_DELAYS[attempt] ?? 0;
|
|
2277
|
+
if (delay > 0) await sleep2(delay);
|
|
2278
|
+
telemetry?.emit(sessionId || "", "upload.retried", { attempt });
|
|
2279
|
+
}
|
|
2280
|
+
const result = await this.uploadToPresignedUrl(url, file, onProgress, signal);
|
|
2281
|
+
if (!result.error) return result;
|
|
2282
|
+
if (result.error.code === "aborted") return result;
|
|
2283
|
+
if (result.error.status && NON_RETRYABLE_STATUS_CODES.has(result.error.status)) {
|
|
2284
|
+
return result;
|
|
2285
|
+
}
|
|
2286
|
+
const isRetryable = result.error.status === 0 || RETRYABLE_STATUS_CODES2.has(result.error.status);
|
|
2287
|
+
if (!isRetryable || attempt === RETRY_DELAYS.length - 1) {
|
|
2288
|
+
return result;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return { data: null, error: { code: "upload_error", message: "Upload failed after retries", status: 0 } };
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Upload file directly to S3 presigned URL.
|
|
2295
|
+
* Uses XHR for progress tracking in browser, fetch otherwise.
|
|
2296
|
+
* Includes stall detection.
|
|
2297
|
+
*/
|
|
2298
|
+
async uploadToPresignedUrl(url, file, onProgress, signal) {
|
|
2299
|
+
if (signal?.aborted) {
|
|
2300
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
2301
|
+
}
|
|
2302
|
+
if (typeof XMLHttpRequest !== "undefined") {
|
|
2303
|
+
return this.uploadWithXHR(url, file, onProgress, signal);
|
|
2304
|
+
}
|
|
2305
|
+
const stallTimeout = DEFAULT_STALL_TIMEOUT_MS;
|
|
2306
|
+
const controller = new AbortController();
|
|
2307
|
+
let parentSignalCleanup;
|
|
2308
|
+
const combinedSignal = signal ? AbortSignal.any?.([signal, controller.signal]) ?? (() => {
|
|
2309
|
+
const onAbort = () => controller.abort();
|
|
2310
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2311
|
+
parentSignalCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
2312
|
+
return controller.signal;
|
|
2313
|
+
})() : controller.signal;
|
|
2314
|
+
const timer = setTimeout(() => controller.abort(), stallTimeout);
|
|
2315
|
+
try {
|
|
2316
|
+
const response = await fetch(url, {
|
|
2317
|
+
method: "PUT",
|
|
2318
|
+
body: file,
|
|
2319
|
+
headers: {
|
|
2320
|
+
"Content-Type": file.type || "application/octet-stream"
|
|
2321
|
+
},
|
|
2322
|
+
signal: combinedSignal
|
|
2323
|
+
});
|
|
2324
|
+
clearTimeout(timer);
|
|
2325
|
+
parentSignalCleanup?.();
|
|
2326
|
+
if (!response.ok) {
|
|
2327
|
+
return {
|
|
2328
|
+
data: null,
|
|
2329
|
+
error: {
|
|
2330
|
+
code: "upload_error",
|
|
2331
|
+
message: `S3 upload failed: ${response.status} ${response.statusText}`,
|
|
2332
|
+
status: response.status
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
onProgress?.(100);
|
|
2337
|
+
return { data: null, error: null };
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
clearTimeout(timer);
|
|
2340
|
+
parentSignalCleanup?.();
|
|
2341
|
+
if (signal?.aborted) {
|
|
2342
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
2343
|
+
}
|
|
2344
|
+
const isStall = controller.signal.aborted && !signal?.aborted;
|
|
2345
|
+
return {
|
|
2346
|
+
data: null,
|
|
2347
|
+
error: {
|
|
2348
|
+
code: isStall ? "upload_stalled" : "upload_error",
|
|
2349
|
+
message: isStall ? `Upload stalled (no progress for ${stallTimeout / 1e3}s)` : err instanceof Error ? err.message : "S3 upload failed",
|
|
2350
|
+
status: 0
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
uploadWithXHR(url, file, onProgress, signal) {
|
|
2356
|
+
return new Promise((resolve) => {
|
|
2357
|
+
const xhr = new XMLHttpRequest();
|
|
2358
|
+
const stallTimeout = getStallTimeout();
|
|
2359
|
+
let stallTimer = null;
|
|
2360
|
+
const resetStallTimer = () => {
|
|
2361
|
+
if (stallTimer !== null) clearTimeout(stallTimer);
|
|
2362
|
+
stallTimer = setTimeout(() => {
|
|
2363
|
+
xhr.abort();
|
|
2364
|
+
resolve({
|
|
2365
|
+
data: null,
|
|
2366
|
+
error: {
|
|
2367
|
+
code: "upload_stalled",
|
|
2368
|
+
message: `Upload stalled (no progress for ${stallTimeout / 1e3}s)`,
|
|
2369
|
+
status: 0
|
|
2370
|
+
}
|
|
2371
|
+
});
|
|
2372
|
+
}, stallTimeout);
|
|
2373
|
+
};
|
|
2374
|
+
const clearStallTimer = () => {
|
|
2375
|
+
if (stallTimer !== null) {
|
|
2376
|
+
clearTimeout(stallTimer);
|
|
2377
|
+
stallTimer = null;
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
if (signal) {
|
|
2381
|
+
if (signal.aborted) {
|
|
2382
|
+
resolve({ data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } });
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
signal.addEventListener("abort", () => {
|
|
2386
|
+
clearStallTimer();
|
|
2387
|
+
xhr.abort();
|
|
2388
|
+
}, { once: true });
|
|
2389
|
+
}
|
|
2390
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
2391
|
+
resetStallTimer();
|
|
2392
|
+
if (event.lengthComputable && onProgress) {
|
|
2393
|
+
onProgress(Math.round(event.loaded / event.total * 100));
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
xhr.addEventListener("load", () => {
|
|
2397
|
+
clearStallTimer();
|
|
2398
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
2399
|
+
onProgress?.(100);
|
|
2400
|
+
resolve({ data: null, error: null });
|
|
2401
|
+
} else {
|
|
2402
|
+
resolve({
|
|
2403
|
+
data: null,
|
|
2404
|
+
error: {
|
|
2405
|
+
code: "upload_error",
|
|
2406
|
+
message: `S3 upload failed: ${xhr.status}`,
|
|
2407
|
+
status: xhr.status
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
xhr.addEventListener("error", () => {
|
|
2413
|
+
clearStallTimer();
|
|
2414
|
+
resolve({
|
|
2415
|
+
data: null,
|
|
2416
|
+
error: { code: "upload_error", message: "S3 upload failed", status: 0 }
|
|
2417
|
+
});
|
|
2418
|
+
});
|
|
2419
|
+
xhr.addEventListener("abort", () => {
|
|
2420
|
+
clearStallTimer();
|
|
2421
|
+
if (!signal?.aborted) return;
|
|
2422
|
+
resolve({
|
|
2423
|
+
data: null,
|
|
2424
|
+
error: { code: "aborted", message: "Upload aborted", status: 0 }
|
|
2425
|
+
});
|
|
2426
|
+
});
|
|
2427
|
+
xhr.open("PUT", url);
|
|
2428
|
+
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
|
2429
|
+
xhr.send(file);
|
|
2430
|
+
resetStallTimer();
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
// --------------------------------------------------------------------------
|
|
2434
|
+
// Private: Part upload with retry
|
|
2435
|
+
// --------------------------------------------------------------------------
|
|
2436
|
+
async uploadPartWithRetry(url, blob, signal, partNumber, sessionId, telemetry) {
|
|
2437
|
+
for (let attempt = 0; attempt < RETRY_DELAYS.length; attempt++) {
|
|
2438
|
+
if (signal?.aborted) return { error: "Upload aborted", code: "aborted" };
|
|
2439
|
+
if (attempt > 0) {
|
|
2440
|
+
const delay = RETRY_DELAYS[attempt] ?? 0;
|
|
2441
|
+
if (delay > 0) await sleep2(delay);
|
|
2442
|
+
telemetry?.emit(sessionId || "", "upload.retried", { attempt, part_number: partNumber });
|
|
2443
|
+
}
|
|
2444
|
+
const controller = new AbortController();
|
|
2445
|
+
let partSignalCleanup;
|
|
2446
|
+
const combinedSignal = signal ? AbortSignal.any?.([signal, controller.signal]) ?? (() => {
|
|
2447
|
+
const onAbort = () => controller.abort();
|
|
2448
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2449
|
+
partSignalCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
2450
|
+
return controller.signal;
|
|
2451
|
+
})() : controller.signal;
|
|
2452
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_STALL_TIMEOUT_MS);
|
|
2453
|
+
try {
|
|
2454
|
+
const response = await fetch(url, {
|
|
2455
|
+
method: "PUT",
|
|
2456
|
+
body: blob,
|
|
2457
|
+
signal: combinedSignal
|
|
2458
|
+
});
|
|
2459
|
+
clearTimeout(timer);
|
|
2460
|
+
partSignalCleanup?.();
|
|
2461
|
+
if (response.ok) {
|
|
2462
|
+
const etag = response.headers.get("etag");
|
|
2463
|
+
if (!etag) {
|
|
2464
|
+
if (attempt === RETRY_DELAYS.length - 1) {
|
|
2465
|
+
return { error: "Part upload succeeded but ETag missing \u2014 cannot verify integrity", code: "s3_error" };
|
|
2466
|
+
}
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2469
|
+
return { etag };
|
|
2470
|
+
}
|
|
2471
|
+
if (NON_RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
2472
|
+
return { error: `Part upload failed: ${response.status}`, status: response.status, code: "s3_error" };
|
|
2473
|
+
}
|
|
2474
|
+
if (attempt === RETRY_DELAYS.length - 1) {
|
|
2475
|
+
return { error: `Part upload failed after retries: ${response.status}`, status: response.status, code: "s3_error" };
|
|
2476
|
+
}
|
|
2477
|
+
} catch (err) {
|
|
2478
|
+
clearTimeout(timer);
|
|
2479
|
+
partSignalCleanup?.();
|
|
2480
|
+
if (signal?.aborted) return { error: "Upload aborted", code: "aborted" };
|
|
2481
|
+
const isStall = controller.signal.aborted && !signal?.aborted;
|
|
2482
|
+
if (attempt === RETRY_DELAYS.length - 1) {
|
|
2483
|
+
return {
|
|
2484
|
+
error: isStall ? `Part upload stalled (no progress for ${DEFAULT_STALL_TIMEOUT_MS / 1e3}s)` : err instanceof Error ? err.message : "Part upload failed",
|
|
2485
|
+
code: isStall ? "upload_stalled" : "network_error"
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
return { error: "Part upload failed after retries", code: "network_error" };
|
|
2491
|
+
}
|
|
2492
|
+
// --------------------------------------------------------------------------
|
|
2493
|
+
// Private: Helpers
|
|
2494
|
+
// --------------------------------------------------------------------------
|
|
2495
|
+
shouldUseMultipart(file, options) {
|
|
2496
|
+
if (options?.forceMultipart) return true;
|
|
2497
|
+
const threshold = isSlowNetwork() ? MULTIPART_THRESHOLD_SLOW : MULTIPART_THRESHOLD;
|
|
2498
|
+
return file.size >= threshold;
|
|
2499
|
+
}
|
|
2500
|
+
defaultChunkSize(fileSize) {
|
|
2501
|
+
if (fileSize > 512 * 1024 * 1024) return 16 * 1024 * 1024;
|
|
2502
|
+
const effectiveType = getNetworkEffectiveType2();
|
|
2503
|
+
if (effectiveType === "slow-2g" || effectiveType === "2g") return 5 * 1024 * 1024;
|
|
2504
|
+
if (effectiveType === "3g") return 5 * 1024 * 1024;
|
|
2505
|
+
return 8 * 1024 * 1024;
|
|
2506
|
+
}
|
|
2507
|
+
defaultConcurrency() {
|
|
2508
|
+
const effectiveType = getNetworkEffectiveType2();
|
|
2509
|
+
if (effectiveType === "slow-2g" || effectiveType === "2g") return 1;
|
|
2510
|
+
if (effectiveType === "3g") return 2;
|
|
2511
|
+
return 4;
|
|
2512
|
+
}
|
|
2513
|
+
async maybeCompress(file, config, sessionId, telemetry) {
|
|
2514
|
+
try {
|
|
2515
|
+
const { maybeCompressImage: maybeCompressImage2 } = await Promise.resolve().then(() => (init_upload_compression(), upload_compression_exports));
|
|
2516
|
+
return await maybeCompressImage2(file, config, sessionId, telemetry);
|
|
2517
|
+
} catch {
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
getOrCreateTelemetry() {
|
|
2522
|
+
if (!this.telemetry) {
|
|
2523
|
+
this.telemetry = new UploadTelemetry(this.client);
|
|
2524
|
+
}
|
|
2525
|
+
return this.telemetry;
|
|
2526
|
+
}
|
|
2527
|
+
/** Build RequestOptions with X-Upload-Session-Id header for cross-boundary correlation */
|
|
2528
|
+
withSessionHeader(sessionId, options) {
|
|
2529
|
+
const headers = { "X-Upload-Session-Id": sessionId };
|
|
2530
|
+
if (options?.clientContext) {
|
|
2531
|
+
return { clientContext: options.clientContext, headers };
|
|
2532
|
+
}
|
|
2533
|
+
return { headers };
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Use ServiceModule's list method but with a cleaner name internally
|
|
2537
|
+
* (can't call protected `list` from public method with same name).
|
|
2538
|
+
*/
|
|
2539
|
+
listMethod(path, params, options) {
|
|
2540
|
+
return super._list(path, params, options);
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
function sleep2(ms) {
|
|
2544
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2545
|
+
}
|
|
2546
|
+
function getStallTimeout() {
|
|
2547
|
+
return isSlowNetwork() ? SLOW_NETWORK_STALL_TIMEOUT_MS : DEFAULT_STALL_TIMEOUT_MS;
|
|
2548
|
+
}
|
|
2549
|
+
function isSlowNetwork() {
|
|
2550
|
+
const effectiveType = getNetworkEffectiveType2();
|
|
2551
|
+
return effectiveType === "slow-2g" || effectiveType === "2g" || effectiveType === "3g";
|
|
2552
|
+
}
|
|
2553
|
+
function getNetworkEffectiveType2() {
|
|
2554
|
+
if (typeof navigator !== "undefined" && "connection" in navigator) {
|
|
2555
|
+
const conn = navigator.connection;
|
|
2556
|
+
return conn?.effectiveType || "4g";
|
|
2557
|
+
}
|
|
2558
|
+
return "4g";
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// src/index.ts
|
|
2562
|
+
init_upload_resume();
|
|
2563
|
+
|
|
2564
|
+
// src/services/upload-strategy.ts
|
|
2565
|
+
var MULTIPART_THRESHOLD2 = 8 * 1024 * 1024;
|
|
2566
|
+
var MULTIPART_THRESHOLD_SLOW2 = 4 * 1024 * 1024;
|
|
2567
|
+
var DEFAULT_STALL_TIMEOUT_MS2 = 45e3;
|
|
2568
|
+
var SLOW_STALL_TIMEOUT_MS = 9e4;
|
|
2569
|
+
var CHUNK_SIZES = {
|
|
2570
|
+
"slow-2g": 5 * 1024 * 1024,
|
|
2571
|
+
"2g": 5 * 1024 * 1024,
|
|
2572
|
+
"3g": 5 * 1024 * 1024,
|
|
2573
|
+
"4g": 8 * 1024 * 1024,
|
|
2574
|
+
unknown: 8 * 1024 * 1024
|
|
2575
|
+
};
|
|
2576
|
+
var LARGE_FILE_CHUNK_SIZE = 16 * 1024 * 1024;
|
|
2577
|
+
var CONCURRENCY = {
|
|
2578
|
+
"slow-2g": 1,
|
|
2579
|
+
"2g": 1,
|
|
2580
|
+
"3g": 2,
|
|
2581
|
+
"4g": 4,
|
|
2582
|
+
unknown: 4
|
|
2583
|
+
};
|
|
2584
|
+
function resolveStrategy(fileSize, overrides) {
|
|
2585
|
+
const network = detectNetworkClass();
|
|
2586
|
+
const isSlowNetwork2 = network === "slow-2g" || network === "2g" || network === "3g";
|
|
2587
|
+
const threshold = isSlowNetwork2 ? MULTIPART_THRESHOLD_SLOW2 : MULTIPART_THRESHOLD2;
|
|
2588
|
+
const strategy = overrides?.forceMultipart || fileSize >= threshold ? "multipart" : "direct";
|
|
2589
|
+
const chunkSize = overrides?.chunkSize || (fileSize > 512 * 1024 * 1024 ? LARGE_FILE_CHUNK_SIZE : CHUNK_SIZES[network]);
|
|
2590
|
+
const concurrency = overrides?.concurrency || adaptConcurrency(network);
|
|
2591
|
+
const stallTimeoutMs = isSlowNetwork2 ? SLOW_STALL_TIMEOUT_MS : DEFAULT_STALL_TIMEOUT_MS2;
|
|
2592
|
+
return { strategy, chunkSize, concurrency, stallTimeoutMs };
|
|
2593
|
+
}
|
|
2594
|
+
function detectNetworkClass() {
|
|
2595
|
+
if (typeof navigator === "undefined") return "unknown";
|
|
2596
|
+
const conn = navigator.connection;
|
|
2597
|
+
if (!conn) return "unknown";
|
|
2598
|
+
const effectiveType = conn.effectiveType;
|
|
2599
|
+
if (effectiveType === "slow-2g") return "slow-2g";
|
|
2600
|
+
if (effectiveType === "2g") return "2g";
|
|
2601
|
+
if (effectiveType === "3g") return "3g";
|
|
2602
|
+
if (effectiveType === "4g") return "4g";
|
|
2603
|
+
return "unknown";
|
|
2604
|
+
}
|
|
2605
|
+
function getMeasuredBandwidthMbps() {
|
|
2606
|
+
if (typeof navigator === "undefined") return null;
|
|
2607
|
+
const conn = navigator.connection;
|
|
2608
|
+
return conn?.downlink ?? null;
|
|
2609
|
+
}
|
|
2610
|
+
function adaptConcurrency(network) {
|
|
2611
|
+
const bandwidth = getMeasuredBandwidthMbps();
|
|
2612
|
+
if (bandwidth === null) return CONCURRENCY[network];
|
|
2613
|
+
if (bandwidth < 0.5) return 1;
|
|
2614
|
+
if (bandwidth < 2) return 2;
|
|
2615
|
+
if (bandwidth < 10) return 3;
|
|
2616
|
+
return 5;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// src/services/upload-engine.ts
|
|
2620
|
+
var DEFAULT_CONFIG = {
|
|
2621
|
+
multipartEnabled: true,
|
|
2622
|
+
multipartAllowlist: [],
|
|
2623
|
+
telemetry: {}
|
|
2624
|
+
};
|
|
2625
|
+
function createUploadPlan(fileSize, contentType, options = {}, engineConfig = {}) {
|
|
2626
|
+
const config = { ...DEFAULT_CONFIG, ...engineConfig };
|
|
2627
|
+
const multipartAllowed = config.multipartEnabled && (config.multipartAllowlist.length === 0 || options.appId != null && config.multipartAllowlist.includes(options.appId));
|
|
2628
|
+
const resolved = resolveStrategy(fileSize, {
|
|
2629
|
+
forceMultipart: multipartAllowed && options.forceMultipart,
|
|
2630
|
+
chunkSize: options.chunkSize,
|
|
2631
|
+
concurrency: options.maxConcurrency
|
|
2632
|
+
});
|
|
2633
|
+
const strategy = resolved.strategy === "multipart" && !multipartAllowed ? "direct" : resolved.strategy;
|
|
2634
|
+
const isBrowser = typeof window !== "undefined";
|
|
2635
|
+
const isCompressibleType = contentType.startsWith("image/") && !contentType.includes("gif") && !contentType.includes("svg") && !contentType.includes("webp") && !contentType.includes("avif");
|
|
2636
|
+
const shouldCompress = isBrowser && !options.skipCompression && isCompressibleType && fileSize >= 100 * 1024;
|
|
2637
|
+
const shouldResume = isBrowser && strategy === "multipart" && options.resume !== "off";
|
|
2638
|
+
const totalParts = strategy === "multipart" ? Math.ceil(fileSize / resolved.chunkSize) : 1;
|
|
2639
|
+
return {
|
|
2640
|
+
strategy,
|
|
2641
|
+
chunkSize: resolved.chunkSize,
|
|
2642
|
+
concurrency: resolved.concurrency,
|
|
2643
|
+
stallTimeoutMs: resolved.stallTimeoutMs,
|
|
2644
|
+
shouldCompress,
|
|
2645
|
+
shouldResume,
|
|
2646
|
+
totalParts
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
function calculateTotalParts(fileSize, chunkSize) {
|
|
2650
|
+
return Math.ceil(fileSize / chunkSize);
|
|
2651
|
+
}
|
|
2652
|
+
function getPartRange(partNumber, chunkSize, totalSize) {
|
|
2653
|
+
const start = (partNumber - 1) * chunkSize;
|
|
2654
|
+
const end = Math.min(start + chunkSize, totalSize);
|
|
2655
|
+
return { start, end, size: end - start };
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
// src/services/realtime.ts
|
|
2659
|
+
var DEFAULT_RECONNECT_BASE_MS = 1e3;
|
|
2660
|
+
var MAX_RECONNECT_MS = 3e4;
|
|
2661
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
2662
|
+
var RealtimeService = class extends ServiceModule {
|
|
2663
|
+
constructor() {
|
|
2664
|
+
super(...arguments);
|
|
2665
|
+
this.basePath = "/v1/realtime";
|
|
2666
|
+
this.ws = null;
|
|
2667
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
2668
|
+
this.presenceCallbacks = /* @__PURE__ */ new Map();
|
|
2669
|
+
this.statusCallbacks = /* @__PURE__ */ new Set();
|
|
2670
|
+
this._status = "disconnected";
|
|
2671
|
+
this.reconnectAttempt = 0;
|
|
2672
|
+
this.reconnectTimer = null;
|
|
2673
|
+
this.heartbeatTimer = null;
|
|
2674
|
+
this.authenticated = false;
|
|
2675
|
+
}
|
|
2676
|
+
/** Current connection status */
|
|
2677
|
+
get status() {
|
|
2678
|
+
return this._status;
|
|
2679
|
+
}
|
|
2680
|
+
// --------------------------------------------------------------------------
|
|
2681
|
+
// Subscribe / Unsubscribe
|
|
2682
|
+
// --------------------------------------------------------------------------
|
|
2683
|
+
/**
|
|
2684
|
+
* Subscribe to a channel. Connects WebSocket on first call.
|
|
2685
|
+
* Returns an unsubscribe function.
|
|
2686
|
+
*/
|
|
2687
|
+
subscribe(channel, callback) {
|
|
2688
|
+
if (!this.subscriptions.has(channel)) {
|
|
2689
|
+
this.subscriptions.set(channel, /* @__PURE__ */ new Set());
|
|
2690
|
+
}
|
|
2691
|
+
this.subscriptions.get(channel).add(callback);
|
|
2692
|
+
if (this._status === "disconnected") {
|
|
2693
|
+
this.connect();
|
|
2694
|
+
} else if (this.authenticated) {
|
|
2695
|
+
this.sendWs({ type: "subscribe", channel });
|
|
2696
|
+
}
|
|
2697
|
+
return () => {
|
|
2698
|
+
const subs = this.subscriptions.get(channel);
|
|
2699
|
+
if (subs) {
|
|
2700
|
+
subs.delete(callback);
|
|
2701
|
+
if (subs.size === 0) {
|
|
2702
|
+
this.subscriptions.delete(channel);
|
|
2703
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
2704
|
+
this.sendWs({ type: "unsubscribe", channel });
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
// --------------------------------------------------------------------------
|
|
2711
|
+
// Publish
|
|
2712
|
+
// --------------------------------------------------------------------------
|
|
2713
|
+
/** Publish data to a channel via WebSocket. */
|
|
2714
|
+
publish(channel, data) {
|
|
2715
|
+
if (this._status !== "connected" || !this.authenticated) {
|
|
2716
|
+
throw new Error("Cannot publish: not connected");
|
|
2717
|
+
}
|
|
2718
|
+
this.sendWs({ type: "publish", channel, data });
|
|
2719
|
+
}
|
|
2720
|
+
// --------------------------------------------------------------------------
|
|
2721
|
+
// Presence
|
|
2722
|
+
// --------------------------------------------------------------------------
|
|
2723
|
+
/** Join a presence channel with optional user data. */
|
|
2724
|
+
joinPresence(channel, userData) {
|
|
2725
|
+
if (this._status !== "connected") {
|
|
2726
|
+
throw new Error("Cannot join presence: not connected");
|
|
2727
|
+
}
|
|
2728
|
+
this.sendWs({ type: "presence_join", channel, user_data: userData });
|
|
2729
|
+
}
|
|
2730
|
+
/** Leave a presence channel. */
|
|
2731
|
+
leavePresence(channel) {
|
|
2732
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
2733
|
+
this.sendWs({ type: "presence_leave", channel });
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
/** Listen for presence events on a channel. Returns unsubscribe function. */
|
|
2737
|
+
onPresence(channel, callback) {
|
|
2738
|
+
if (!this.presenceCallbacks.has(channel)) {
|
|
2739
|
+
this.presenceCallbacks.set(channel, /* @__PURE__ */ new Set());
|
|
2740
|
+
}
|
|
2741
|
+
this.presenceCallbacks.get(channel).add(callback);
|
|
2742
|
+
return () => {
|
|
2743
|
+
const cbs = this.presenceCallbacks.get(channel);
|
|
2744
|
+
if (cbs) {
|
|
2745
|
+
cbs.delete(callback);
|
|
2746
|
+
if (cbs.size === 0) this.presenceCallbacks.delete(channel);
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
// --------------------------------------------------------------------------
|
|
2751
|
+
// Server-side broadcast (HTTP endpoints)
|
|
2752
|
+
// --------------------------------------------------------------------------
|
|
2753
|
+
/** Broadcast to all connections for this application. */
|
|
2754
|
+
async broadcast(event, data, options) {
|
|
2755
|
+
return this.post("/broadcast", { event, data }, options);
|
|
2756
|
+
}
|
|
2757
|
+
/** Broadcast to a specific channel. */
|
|
2758
|
+
async broadcastToChannel(channel, event, data, options) {
|
|
2759
|
+
return this.post(`/broadcast/channel/${channel}`, { event, data }, options);
|
|
2760
|
+
}
|
|
2761
|
+
/** Send to a specific user's connections. */
|
|
2762
|
+
async sendToUser(userId, event, data, options) {
|
|
2763
|
+
return this.post(`/broadcast/user/${userId}`, { event, data }, options);
|
|
2764
|
+
}
|
|
2765
|
+
// --------------------------------------------------------------------------
|
|
2766
|
+
// Connection Lifecycle
|
|
2767
|
+
// --------------------------------------------------------------------------
|
|
2768
|
+
/** Listen for connection status changes. */
|
|
2769
|
+
onStatusChange(callback) {
|
|
2770
|
+
this.statusCallbacks.add(callback);
|
|
2771
|
+
return () => {
|
|
2772
|
+
this.statusCallbacks.delete(callback);
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
/** Disconnect and clean up all subscriptions. */
|
|
2776
|
+
disconnect() {
|
|
2777
|
+
this.clearTimers();
|
|
2778
|
+
this.subscriptions.clear();
|
|
2779
|
+
this.presenceCallbacks.clear();
|
|
2780
|
+
this.statusCallbacks.clear();
|
|
2781
|
+
this.authenticated = false;
|
|
2782
|
+
this.reconnectAttempt = 0;
|
|
2783
|
+
if (this.ws) {
|
|
2784
|
+
this.ws.onclose = null;
|
|
2785
|
+
this.ws.close();
|
|
2786
|
+
this.ws = null;
|
|
2787
|
+
}
|
|
2788
|
+
this.setStatus("disconnected");
|
|
2789
|
+
}
|
|
2790
|
+
// --------------------------------------------------------------------------
|
|
2791
|
+
// Private: WebSocket management
|
|
2792
|
+
// --------------------------------------------------------------------------
|
|
2793
|
+
connect() {
|
|
2794
|
+
if (this._status === "connecting" || this._status === "connected") return;
|
|
2795
|
+
const baseUrl = this.client.getBaseUrl();
|
|
2796
|
+
const wsUrl = baseUrl.replace(/^http/, "ws") + "/v1/realtime/ws";
|
|
2797
|
+
this.setStatus(this.reconnectAttempt > 0 ? "reconnecting" : "connecting");
|
|
2798
|
+
try {
|
|
2799
|
+
this.ws = new WebSocket(wsUrl);
|
|
2800
|
+
} catch {
|
|
2801
|
+
this.scheduleReconnect();
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
this.ws.onopen = () => {
|
|
2805
|
+
this.reconnectAttempt = 0;
|
|
2806
|
+
this.authenticate();
|
|
2807
|
+
this.startHeartbeat();
|
|
2808
|
+
};
|
|
2809
|
+
this.ws.onmessage = (event) => {
|
|
2810
|
+
try {
|
|
2811
|
+
const msg = JSON.parse(event.data);
|
|
2812
|
+
this.handleMessage(msg);
|
|
2813
|
+
} catch {
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
this.ws.onclose = () => {
|
|
2817
|
+
this.authenticated = false;
|
|
2818
|
+
this.clearHeartbeat();
|
|
2819
|
+
this.scheduleReconnect();
|
|
2820
|
+
};
|
|
2821
|
+
this.ws.onerror = () => {
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
authenticate() {
|
|
2825
|
+
const token = this.client.getSessionToken();
|
|
2826
|
+
this.sendWs({
|
|
2827
|
+
type: "auth",
|
|
2828
|
+
token: token || void 0
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
handleMessage(msg) {
|
|
2832
|
+
switch (msg.type) {
|
|
2833
|
+
case "auth_success":
|
|
2834
|
+
this.authenticated = true;
|
|
2835
|
+
this.setStatus("connected");
|
|
2836
|
+
for (const channel of this.subscriptions.keys()) {
|
|
2837
|
+
this.sendWs({ type: "subscribe", channel });
|
|
2838
|
+
}
|
|
2839
|
+
break;
|
|
2840
|
+
case "subscribed":
|
|
2841
|
+
break;
|
|
2842
|
+
case "message":
|
|
2843
|
+
this.dispatchMessage(msg.channel, msg.data);
|
|
2844
|
+
break;
|
|
2845
|
+
case "error":
|
|
2846
|
+
break;
|
|
2847
|
+
case "presence_state":
|
|
2848
|
+
this.dispatchPresence({
|
|
2849
|
+
type: "state",
|
|
2850
|
+
channel: msg.channel,
|
|
2851
|
+
members: msg.members
|
|
2852
|
+
});
|
|
2853
|
+
break;
|
|
2854
|
+
case "presence_join":
|
|
2855
|
+
this.dispatchPresence({
|
|
2856
|
+
type: "join",
|
|
2857
|
+
channel: msg.channel,
|
|
2858
|
+
user: msg.user
|
|
2859
|
+
});
|
|
2860
|
+
break;
|
|
2861
|
+
case "presence_leave":
|
|
2862
|
+
this.dispatchPresence({
|
|
2863
|
+
type: "leave",
|
|
2864
|
+
channel: msg.channel,
|
|
2865
|
+
user_id: msg.user_id
|
|
2866
|
+
});
|
|
2867
|
+
break;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
dispatchMessage(channel, data) {
|
|
2871
|
+
const subs = this.subscriptions.get(channel);
|
|
2872
|
+
if (subs) {
|
|
2873
|
+
for (const cb of subs) {
|
|
2874
|
+
try {
|
|
2875
|
+
cb(data, channel);
|
|
2876
|
+
} catch {
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
dispatchPresence(event) {
|
|
2882
|
+
const cbs = this.presenceCallbacks.get(event.channel);
|
|
2883
|
+
if (cbs) {
|
|
2884
|
+
for (const cb of cbs) {
|
|
2885
|
+
try {
|
|
2886
|
+
cb(event);
|
|
2887
|
+
} catch {
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
sendWs(data) {
|
|
2893
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
2894
|
+
this.ws.send(JSON.stringify(data));
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
// --------------------------------------------------------------------------
|
|
2898
|
+
// Private: Reconnection
|
|
2899
|
+
// --------------------------------------------------------------------------
|
|
2900
|
+
scheduleReconnect() {
|
|
2901
|
+
if (this.subscriptions.size === 0 && this.presenceCallbacks.size === 0) {
|
|
2902
|
+
this.setStatus("disconnected");
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
this.setStatus("reconnecting");
|
|
2906
|
+
const delay = this.getReconnectDelay();
|
|
2907
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2908
|
+
this.reconnectAttempt++;
|
|
2909
|
+
this.connect();
|
|
2910
|
+
}, delay);
|
|
2911
|
+
}
|
|
2912
|
+
getReconnectDelay() {
|
|
2913
|
+
const exponential = DEFAULT_RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt);
|
|
2914
|
+
const jitter = Math.random() * 0.3 * exponential;
|
|
2915
|
+
return Math.min(exponential + jitter, MAX_RECONNECT_MS);
|
|
2916
|
+
}
|
|
2917
|
+
// --------------------------------------------------------------------------
|
|
2918
|
+
// Private: Heartbeat
|
|
2919
|
+
// --------------------------------------------------------------------------
|
|
2920
|
+
startHeartbeat() {
|
|
2921
|
+
this.clearHeartbeat();
|
|
2922
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2923
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
2924
|
+
this.ws.send("ping");
|
|
2925
|
+
}
|
|
2926
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
2927
|
+
}
|
|
2928
|
+
clearHeartbeat() {
|
|
2929
|
+
if (this.heartbeatTimer) {
|
|
2930
|
+
clearInterval(this.heartbeatTimer);
|
|
2931
|
+
this.heartbeatTimer = null;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
clearTimers() {
|
|
2935
|
+
this.clearHeartbeat();
|
|
2936
|
+
if (this.reconnectTimer) {
|
|
2937
|
+
clearTimeout(this.reconnectTimer);
|
|
2938
|
+
this.reconnectTimer = null;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
// --------------------------------------------------------------------------
|
|
2942
|
+
// Private: Status
|
|
2943
|
+
// --------------------------------------------------------------------------
|
|
2944
|
+
setStatus(status) {
|
|
2945
|
+
if (this._status === status) return;
|
|
2946
|
+
this._status = status;
|
|
2947
|
+
for (const cb of this.statusCallbacks) {
|
|
2948
|
+
try {
|
|
2949
|
+
cb(status);
|
|
2950
|
+
} catch {
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
|
|
2956
|
+
// src/services/video.ts
|
|
2957
|
+
var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
2958
|
+
var MIN_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
2959
|
+
var VideoService = class extends ServiceModule {
|
|
2960
|
+
constructor() {
|
|
2961
|
+
super(...arguments);
|
|
2962
|
+
this.basePath = "/v1/videos";
|
|
2963
|
+
}
|
|
2964
|
+
// --------------------------------------------------------------------------
|
|
2965
|
+
// Upload (3-step chunked flow)
|
|
2966
|
+
// --------------------------------------------------------------------------
|
|
2967
|
+
/**
|
|
2968
|
+
* Upload a video file using chunked multipart upload.
|
|
2969
|
+
*
|
|
2970
|
+
* Internally:
|
|
2971
|
+
* 1. Starts a multipart upload session
|
|
2972
|
+
* 2. Uploads chunks sequentially with progress tracking
|
|
2973
|
+
* 3. Completes the upload, triggering transcoding
|
|
2974
|
+
*
|
|
2975
|
+
* @returns The video record with id, status, etc.
|
|
2976
|
+
*/
|
|
2977
|
+
async upload(file, options, requestOptions) {
|
|
2978
|
+
if (options?.signal?.aborted) {
|
|
2979
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
2980
|
+
}
|
|
2981
|
+
const chunkSize = Math.max(options?.chunkSize ?? DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE);
|
|
2982
|
+
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
2983
|
+
const filename = options?.filename || file.name || "video";
|
|
2984
|
+
const startResult = await this.post("/upload-start", {
|
|
2985
|
+
filename,
|
|
2986
|
+
content_type: file.type || "video/mp4",
|
|
2987
|
+
size_bytes: file.size,
|
|
2988
|
+
title: options?.title,
|
|
2989
|
+
description: options?.description,
|
|
2990
|
+
metadata: options?.metadata
|
|
2991
|
+
}, requestOptions);
|
|
2992
|
+
if (startResult.error) return { data: null, error: startResult.error };
|
|
2993
|
+
const { video_id, upload_id } = startResult.data;
|
|
2994
|
+
const parts = [];
|
|
2995
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
2996
|
+
if (options?.signal?.aborted) {
|
|
2997
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
2998
|
+
}
|
|
2999
|
+
const start = i * chunkSize;
|
|
3000
|
+
const end = Math.min(start + chunkSize, file.size);
|
|
3001
|
+
const chunk = file.slice(start, end);
|
|
3002
|
+
const partNumber = i + 1;
|
|
3003
|
+
const partResult = await this.uploadPart(
|
|
3004
|
+
video_id,
|
|
3005
|
+
upload_id,
|
|
3006
|
+
partNumber,
|
|
3007
|
+
chunk,
|
|
3008
|
+
options?.signal
|
|
3009
|
+
);
|
|
3010
|
+
if (partResult.error) return { data: null, error: partResult.error };
|
|
3011
|
+
parts.push({
|
|
3012
|
+
part_number: partNumber,
|
|
3013
|
+
etag: partResult.data.etag
|
|
3014
|
+
});
|
|
3015
|
+
if (options?.onProgress) {
|
|
3016
|
+
const progress = Math.round(partNumber / totalChunks * 100);
|
|
3017
|
+
options.onProgress(progress);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
const completeResult = await this.post(`/${video_id}/upload-complete`, {
|
|
3021
|
+
upload_id,
|
|
3022
|
+
parts
|
|
3023
|
+
}, requestOptions);
|
|
3024
|
+
return completeResult;
|
|
3025
|
+
}
|
|
3026
|
+
// --------------------------------------------------------------------------
|
|
3027
|
+
// Video Operations
|
|
3028
|
+
// --------------------------------------------------------------------------
|
|
3029
|
+
/** Get video metadata and status. */
|
|
3030
|
+
async get(videoId, options) {
|
|
3031
|
+
return super._get(`/${videoId}`, options);
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Get the HLS master playlist URL for streaming.
|
|
3035
|
+
* Returns the playlist URL that can be passed to a video player.
|
|
3036
|
+
*/
|
|
3037
|
+
async getStreamUrl(videoId) {
|
|
3038
|
+
const baseUrl = this.client.getBaseUrl();
|
|
3039
|
+
return {
|
|
3040
|
+
data: { url: `${baseUrl}${this.basePath}/${videoId}/playlist.m3u8` },
|
|
3041
|
+
error: null
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Track a playback event (view, play, pause, seek, complete, etc.).
|
|
3046
|
+
*/
|
|
3047
|
+
async trackPlayback(videoId, event, options) {
|
|
3048
|
+
return this.post(`/${videoId}/track`, event, options);
|
|
3049
|
+
}
|
|
3050
|
+
/** Get video analytics (views, watch time, etc.). */
|
|
3051
|
+
async getAnalytics(videoId, options) {
|
|
3052
|
+
return super._get(`/${videoId}/analytics`, options);
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Update a video's access mode (public/private).
|
|
3056
|
+
* Public videos get 7-day signed URLs; private get 1-hour signed URLs.
|
|
3057
|
+
*/
|
|
3058
|
+
async updateAccessMode(videoId, accessMode, options) {
|
|
3059
|
+
return this.patch(`/${videoId}`, { access_mode: accessMode }, options);
|
|
3060
|
+
}
|
|
3061
|
+
// --------------------------------------------------------------------------
|
|
3062
|
+
// Legacy methods (backward compat)
|
|
3063
|
+
// --------------------------------------------------------------------------
|
|
3064
|
+
/** @deprecated Use upload() instead */
|
|
3065
|
+
async uploadVideo(file, options) {
|
|
3066
|
+
return this.upload(file, {
|
|
3067
|
+
metadata: options?.metadata,
|
|
3068
|
+
onProgress: options?.onProgress,
|
|
3069
|
+
signal: options?.signal
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
/** @deprecated Use get() instead */
|
|
3073
|
+
async getVideo(id) {
|
|
3074
|
+
return this.get(id);
|
|
3075
|
+
}
|
|
3076
|
+
// --------------------------------------------------------------------------
|
|
3077
|
+
// Private: Chunk upload
|
|
3078
|
+
// --------------------------------------------------------------------------
|
|
3079
|
+
async uploadPart(videoId, uploadId, partNumber, chunk, signal) {
|
|
3080
|
+
const formData = new FormData();
|
|
3081
|
+
formData.append("file", chunk);
|
|
3082
|
+
const path = `${this.basePath}/${videoId}/upload-part?upload_id=${encodeURIComponent(uploadId)}&part_number=${partNumber}`;
|
|
3083
|
+
const headers = {
|
|
3084
|
+
"x-api-key": this.client.getApiKey()
|
|
3085
|
+
};
|
|
3086
|
+
const token = this.client.getSessionToken();
|
|
3087
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
3088
|
+
try {
|
|
3089
|
+
const response = await fetch(`${this.client.getBaseUrl()}${path}`, {
|
|
3090
|
+
method: "POST",
|
|
3091
|
+
headers,
|
|
3092
|
+
body: formData,
|
|
3093
|
+
signal
|
|
3094
|
+
});
|
|
3095
|
+
const data = await response.json();
|
|
3096
|
+
if (!response.ok) {
|
|
3097
|
+
return {
|
|
3098
|
+
data: null,
|
|
3099
|
+
error: {
|
|
3100
|
+
code: data?.error?.code || "upload_error",
|
|
3101
|
+
message: data?.error?.message || data?.message || "Part upload failed",
|
|
3102
|
+
status: response.status
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
const result = data?.data !== void 0 ? data.data : data;
|
|
3107
|
+
return { data: result, error: null };
|
|
3108
|
+
} catch (err) {
|
|
3109
|
+
if (signal?.aborted) {
|
|
3110
|
+
return { data: null, error: { code: "aborted", message: "Upload aborted", status: 0 } };
|
|
3111
|
+
}
|
|
3112
|
+
return {
|
|
3113
|
+
data: null,
|
|
3114
|
+
error: {
|
|
3115
|
+
code: "upload_error",
|
|
3116
|
+
message: err instanceof Error ? err.message : "Part upload failed",
|
|
3117
|
+
status: 0
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
|
|
3124
|
+
// src/services/data.ts
|
|
3125
|
+
var DataService = class extends ServiceModule {
|
|
3126
|
+
constructor() {
|
|
3127
|
+
super(...arguments);
|
|
3128
|
+
this.basePath = "/v1/data";
|
|
3129
|
+
}
|
|
3130
|
+
// --------------------------------------------------------------------------
|
|
3131
|
+
// Collections
|
|
3132
|
+
// --------------------------------------------------------------------------
|
|
3133
|
+
async createCollection(name, schema, options) {
|
|
3134
|
+
return this.post("/collections", { name, schema }, options);
|
|
3135
|
+
}
|
|
3136
|
+
async listCollections(options) {
|
|
3137
|
+
return this._get("/collections", options);
|
|
3138
|
+
}
|
|
3139
|
+
async deleteCollection(name, options) {
|
|
3140
|
+
return this.del(`/collections/${name}`, options);
|
|
3141
|
+
}
|
|
3142
|
+
// --------------------------------------------------------------------------
|
|
3143
|
+
// Documents — CRUD
|
|
3144
|
+
// --------------------------------------------------------------------------
|
|
3145
|
+
async create(collection, data, options) {
|
|
3146
|
+
return this.post(`/${collection}/documents`, { data }, options);
|
|
3147
|
+
}
|
|
3148
|
+
async get(collection, docId, options) {
|
|
3149
|
+
return this._get(`/${collection}/documents/${docId}`, options);
|
|
3150
|
+
}
|
|
3151
|
+
async update(collection, docId, data, options) {
|
|
3152
|
+
return this.patch(`/${collection}/documents/${docId}`, { data }, options);
|
|
3153
|
+
}
|
|
3154
|
+
async delete(collection, docId, options) {
|
|
3155
|
+
return this.del(`/${collection}/documents/${docId}`, options);
|
|
3156
|
+
}
|
|
3157
|
+
// --------------------------------------------------------------------------
|
|
3158
|
+
// Documents — Query & Aggregate
|
|
3159
|
+
// --------------------------------------------------------------------------
|
|
3160
|
+
async query(collection, options, requestOptions) {
|
|
3161
|
+
const body = {
|
|
3162
|
+
filters: options?.filters ?? [],
|
|
3163
|
+
sort: options?.sort ?? [],
|
|
3164
|
+
page: options?.page ?? 1,
|
|
3165
|
+
per_page: options?.perPage ?? 20
|
|
3166
|
+
};
|
|
3167
|
+
const response = await this.post(`/${collection}/query`, body, requestOptions);
|
|
3168
|
+
if (response.error) {
|
|
3169
|
+
return {
|
|
3170
|
+
data: [],
|
|
3171
|
+
metadata: { total: 0, totalPages: 0, page: body.page, perPage: body.per_page },
|
|
3172
|
+
error: response.error
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
const raw = response.data;
|
|
3176
|
+
const documents = raw?.documents ?? raw?.data ?? [];
|
|
3177
|
+
const total = raw?.total ?? documents.length;
|
|
3178
|
+
const totalPages = raw?.total_pages ?? (total > 0 ? Math.ceil(total / body.per_page) : 0);
|
|
3179
|
+
return {
|
|
3180
|
+
data: documents,
|
|
3181
|
+
metadata: {
|
|
3182
|
+
total,
|
|
3183
|
+
totalPages,
|
|
3184
|
+
page: raw?.page ?? body.page,
|
|
3185
|
+
perPage: raw?.per_page ?? body.per_page
|
|
3186
|
+
},
|
|
3187
|
+
error: null
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
async aggregate(collection, options, requestOptions) {
|
|
3191
|
+
return this.post(`/${collection}/aggregate`, options, requestOptions);
|
|
3192
|
+
}
|
|
3193
|
+
async myDocuments(collection, options, requestOptions) {
|
|
3194
|
+
return this._list(`/${collection}/my-documents`, options, requestOptions);
|
|
3195
|
+
}
|
|
3196
|
+
// --------------------------------------------------------------------------
|
|
3197
|
+
// Legacy methods (backward compat)
|
|
3198
|
+
// --------------------------------------------------------------------------
|
|
3199
|
+
/** @deprecated Use create() instead */
|
|
3200
|
+
async createDocument(collection, data) {
|
|
3201
|
+
return this.create(collection, data);
|
|
3202
|
+
}
|
|
3203
|
+
/** @deprecated Use get() instead */
|
|
3204
|
+
async getDocument(collection, id) {
|
|
3205
|
+
return this.get(collection, id);
|
|
3206
|
+
}
|
|
3207
|
+
/** @deprecated Use update() instead */
|
|
3208
|
+
async updateDocument(collection, id, data) {
|
|
3209
|
+
return this.update(collection, id, data);
|
|
3210
|
+
}
|
|
3211
|
+
/** @deprecated Use delete() instead */
|
|
3212
|
+
async deleteDocument(collection, id) {
|
|
3213
|
+
return this.delete(collection, id);
|
|
3214
|
+
}
|
|
3215
|
+
/** @deprecated Use query() instead */
|
|
3216
|
+
async queryDocuments(collection, options) {
|
|
3217
|
+
return this.query(collection, options);
|
|
3218
|
+
}
|
|
3219
|
+
};
|
|
3220
|
+
|
|
3221
|
+
// src/services/chat.ts
|
|
3222
|
+
var ChatService = class extends ServiceModule {
|
|
3223
|
+
constructor() {
|
|
3224
|
+
super(...arguments);
|
|
3225
|
+
this.basePath = "/v1/chat";
|
|
3226
|
+
}
|
|
3227
|
+
// --------------------------------------------------------------------------
|
|
3228
|
+
// Conversations
|
|
3229
|
+
// --------------------------------------------------------------------------
|
|
3230
|
+
async createConversation(data, options) {
|
|
3231
|
+
return this.post("/conversations", data, options);
|
|
3232
|
+
}
|
|
3233
|
+
async listConversations(params, requestOptions) {
|
|
3234
|
+
return this._list("/conversations", params, requestOptions);
|
|
3235
|
+
}
|
|
3236
|
+
async getConversation(id, options) {
|
|
3237
|
+
return this._get(`/conversations/${id}`, options);
|
|
3238
|
+
}
|
|
3239
|
+
async addParticipant(conversationId, userId, options) {
|
|
3240
|
+
return this.post(`/conversations/${conversationId}/participants`, { user_id: userId }, options);
|
|
3241
|
+
}
|
|
3242
|
+
async removeParticipant(conversationId, userId, options) {
|
|
3243
|
+
return this.del(`/conversations/${conversationId}/participants/${userId}`, options);
|
|
3244
|
+
}
|
|
3245
|
+
// --------------------------------------------------------------------------
|
|
3246
|
+
// Messages
|
|
3247
|
+
// --------------------------------------------------------------------------
|
|
3248
|
+
async sendMessage(conversationId, data, options) {
|
|
3249
|
+
return this.post(`/conversations/${conversationId}/messages`, data, options);
|
|
3250
|
+
}
|
|
3251
|
+
async getMessages(conversationId, options, requestOptions) {
|
|
3252
|
+
return this._get(this.withQuery(`/conversations/${conversationId}/messages`, options), requestOptions);
|
|
3253
|
+
}
|
|
3254
|
+
async editMessage(messageId, data, options) {
|
|
3255
|
+
return this.patch(`/messages/${messageId}`, data, options);
|
|
3256
|
+
}
|
|
3257
|
+
async deleteMessage(messageId, options) {
|
|
3258
|
+
return this.del(`/messages/${messageId}`, options);
|
|
3259
|
+
}
|
|
3260
|
+
async addReaction(messageId, data, options) {
|
|
3261
|
+
return this.post(`/messages/${messageId}/reactions`, data, options);
|
|
3262
|
+
}
|
|
3263
|
+
// --------------------------------------------------------------------------
|
|
3264
|
+
// Typing & Read Receipts
|
|
3265
|
+
// --------------------------------------------------------------------------
|
|
3266
|
+
async sendTyping(conversationId, options) {
|
|
3267
|
+
return this.post(`/conversations/${conversationId}/typing`, void 0, options);
|
|
3268
|
+
}
|
|
3269
|
+
async markRead(conversationId, options) {
|
|
3270
|
+
return this.post(`/conversations/${conversationId}/read`, void 0, options);
|
|
3271
|
+
}
|
|
3272
|
+
async getReadStatus(conversationId, options) {
|
|
3273
|
+
return this._get(`/conversations/${conversationId}/read-status`, options);
|
|
3274
|
+
}
|
|
3275
|
+
// --------------------------------------------------------------------------
|
|
3276
|
+
// Legacy methods (backward compat)
|
|
3277
|
+
// --------------------------------------------------------------------------
|
|
3278
|
+
/** @deprecated Use createConversation() instead */
|
|
3279
|
+
async createChat(data) {
|
|
3280
|
+
return this.createConversation(data);
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
|
|
3284
|
+
// src/services/social.ts
|
|
3285
|
+
var SocialService = class extends ServiceModule {
|
|
3286
|
+
constructor() {
|
|
3287
|
+
super(...arguments);
|
|
3288
|
+
this.basePath = "/v1/social";
|
|
3289
|
+
}
|
|
3290
|
+
// --------------------------------------------------------------------------
|
|
3291
|
+
// Follow / Unfollow
|
|
3292
|
+
// --------------------------------------------------------------------------
|
|
3293
|
+
async follow(userId, options) {
|
|
3294
|
+
return this.post(`/users/${userId}/follow`, void 0, options);
|
|
3295
|
+
}
|
|
3296
|
+
async unfollow(userId, options) {
|
|
3297
|
+
return this.del(`/users/${userId}/follow`, options);
|
|
3298
|
+
}
|
|
3299
|
+
async getFollowers(userId, params, requestOptions) {
|
|
3300
|
+
return this._list(`/users/${userId}/followers`, params, requestOptions);
|
|
3301
|
+
}
|
|
3302
|
+
async getFollowing(userId, params, requestOptions) {
|
|
3303
|
+
return this._list(`/users/${userId}/following`, params, requestOptions);
|
|
3304
|
+
}
|
|
3305
|
+
async getFollowStatus(userId, options) {
|
|
3306
|
+
return this._get(`/users/${userId}/follow-status`, options);
|
|
3307
|
+
}
|
|
3308
|
+
// --------------------------------------------------------------------------
|
|
3309
|
+
// Posts
|
|
3310
|
+
// --------------------------------------------------------------------------
|
|
3311
|
+
async createPost(data, options) {
|
|
3312
|
+
return this.post("/posts", data, options);
|
|
3313
|
+
}
|
|
3314
|
+
async getPost(postId, options) {
|
|
3315
|
+
return this._get(`/posts/${postId}`, options);
|
|
3316
|
+
}
|
|
3317
|
+
async deletePost(postId, options) {
|
|
3318
|
+
return this.del(`/posts/${postId}`, options);
|
|
3319
|
+
}
|
|
3320
|
+
async getUserPosts(userId, params, requestOptions) {
|
|
3321
|
+
return this._list(`/users/${userId}/posts`, params, requestOptions);
|
|
3322
|
+
}
|
|
3323
|
+
async getFeed(options, requestOptions) {
|
|
3324
|
+
return this._list("/feed", options, requestOptions);
|
|
3325
|
+
}
|
|
3326
|
+
// --------------------------------------------------------------------------
|
|
3327
|
+
// Likes
|
|
3328
|
+
// --------------------------------------------------------------------------
|
|
3329
|
+
async like(targetType, targetId, options) {
|
|
3330
|
+
return this.post(`/${targetType}/${targetId}/like`, void 0, options);
|
|
3331
|
+
}
|
|
3332
|
+
async unlike(targetType, targetId, options) {
|
|
3333
|
+
return this.del(`/${targetType}/${targetId}/like`, options);
|
|
3334
|
+
}
|
|
3335
|
+
async getLikes(targetType, targetId, params, requestOptions) {
|
|
3336
|
+
return this._list(`/${targetType}/${targetId}/likes`, params, requestOptions);
|
|
3337
|
+
}
|
|
3338
|
+
// --------------------------------------------------------------------------
|
|
3339
|
+
// Comments
|
|
3340
|
+
// --------------------------------------------------------------------------
|
|
3341
|
+
async comment(postId, data, options) {
|
|
3342
|
+
return this.post(`/posts/${postId}/comments`, data, options);
|
|
3343
|
+
}
|
|
3344
|
+
async getComments(postId, params, requestOptions) {
|
|
3345
|
+
return this._list(`/posts/${postId}/comments`, params, requestOptions);
|
|
3346
|
+
}
|
|
3347
|
+
// --------------------------------------------------------------------------
|
|
3348
|
+
// Activity Feed
|
|
3349
|
+
// --------------------------------------------------------------------------
|
|
3350
|
+
async getActivity(params, requestOptions) {
|
|
3351
|
+
return this._list("/activity", params, requestOptions);
|
|
3352
|
+
}
|
|
3353
|
+
async markActivityRead(activityId, options) {
|
|
3354
|
+
return this.patch(`/activity/${activityId}/read`, {}, options);
|
|
3355
|
+
}
|
|
3356
|
+
async markAllRead(options) {
|
|
3357
|
+
return this.patch("/activity/read-all", {}, options);
|
|
3358
|
+
}
|
|
3359
|
+
// --------------------------------------------------------------------------
|
|
3360
|
+
// Legacy methods (backward compat)
|
|
3361
|
+
// --------------------------------------------------------------------------
|
|
3362
|
+
/** @deprecated Use comment() instead */
|
|
3363
|
+
async addComment(postId, data) {
|
|
3364
|
+
return this.comment(postId, data);
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
|
|
3368
|
+
// src/services/billing.ts
|
|
3369
|
+
var BillingService = class extends ServiceModule {
|
|
3370
|
+
constructor() {
|
|
3371
|
+
super(...arguments);
|
|
3372
|
+
this.basePath = "/v1/billing";
|
|
3373
|
+
}
|
|
3374
|
+
// --------------------------------------------------------------------------
|
|
3375
|
+
// Customers
|
|
3376
|
+
// --------------------------------------------------------------------------
|
|
3377
|
+
async createCustomer(data, options) {
|
|
3378
|
+
return this.post("/customers", data, options);
|
|
3379
|
+
}
|
|
3380
|
+
async addPaymentMethod(data, options) {
|
|
3381
|
+
return this.post("/payment-methods", data, options);
|
|
3382
|
+
}
|
|
3383
|
+
// --------------------------------------------------------------------------
|
|
3384
|
+
// Subscriptions
|
|
3385
|
+
// --------------------------------------------------------------------------
|
|
3386
|
+
async subscribe(data, options) {
|
|
3387
|
+
return this.post("/subscriptions", data, options);
|
|
3388
|
+
}
|
|
3389
|
+
async listSubscriptions(params, options) {
|
|
3390
|
+
return this._list("/subscriptions", params, options);
|
|
3391
|
+
}
|
|
3392
|
+
async cancelSubscription(id, options) {
|
|
3393
|
+
return this.post(`/subscriptions/${id}/cancel`, void 0, options);
|
|
3394
|
+
}
|
|
3395
|
+
async resumeSubscription(id, options) {
|
|
3396
|
+
return this.post(`/subscriptions/${id}/resume`, void 0, options);
|
|
3397
|
+
}
|
|
3398
|
+
async upgradeSubscription(id, data, options) {
|
|
3399
|
+
return this.patch(`/subscriptions/${id}/upgrade`, data, options);
|
|
3400
|
+
}
|
|
3401
|
+
// --------------------------------------------------------------------------
|
|
3402
|
+
// Usage
|
|
3403
|
+
// --------------------------------------------------------------------------
|
|
3404
|
+
async reportUsage(data, options) {
|
|
3405
|
+
return this.post("/usage", data, options);
|
|
3406
|
+
}
|
|
3407
|
+
async getUsageSummary(options) {
|
|
3408
|
+
return this._get("/usage/summary", options);
|
|
3409
|
+
}
|
|
3410
|
+
// --------------------------------------------------------------------------
|
|
3411
|
+
// Invoices
|
|
3412
|
+
// --------------------------------------------------------------------------
|
|
3413
|
+
async listInvoices(params, options) {
|
|
3414
|
+
return this._list("/invoices", params, options);
|
|
3415
|
+
}
|
|
3416
|
+
async getInvoice(id, options) {
|
|
3417
|
+
return this._get(`/invoices/${id}`, options);
|
|
3418
|
+
}
|
|
3419
|
+
async payInvoice(id, options) {
|
|
3420
|
+
return this.post(`/invoices/${id}/pay`, void 0, options);
|
|
3421
|
+
}
|
|
3422
|
+
async getInvoicePdf(id, options) {
|
|
3423
|
+
return this._get(`/invoices/${id}/pdf`, options);
|
|
3424
|
+
}
|
|
3425
|
+
// --------------------------------------------------------------------------
|
|
3426
|
+
// Connected Accounts (Marketplace)
|
|
3427
|
+
// --------------------------------------------------------------------------
|
|
3428
|
+
async createConnectedAccount(data, options) {
|
|
3429
|
+
return this.post("/connected-accounts", data, options);
|
|
3430
|
+
}
|
|
3431
|
+
async getConnectedAccount(id, options) {
|
|
3432
|
+
return this._get(`/connected-accounts/${id}`, options);
|
|
3433
|
+
}
|
|
3434
|
+
async getMyConnectedAccount(options) {
|
|
3435
|
+
return this._get("/connected-accounts/me", options);
|
|
3436
|
+
}
|
|
3437
|
+
async createOnboardingLink(id, data, options) {
|
|
3438
|
+
return this.post(`/connected-accounts/${id}/onboarding-link`, data, options);
|
|
3439
|
+
}
|
|
3440
|
+
async getAccountBalance(id, options) {
|
|
3441
|
+
return this._get(`/connected-accounts/${id}/balance`, options);
|
|
3442
|
+
}
|
|
3443
|
+
async createAccountSession(id, options) {
|
|
3444
|
+
return this.post(`/connected-accounts/${id}/account-session`, void 0, options);
|
|
3445
|
+
}
|
|
3446
|
+
// --------------------------------------------------------------------------
|
|
3447
|
+
// Config
|
|
3448
|
+
// --------------------------------------------------------------------------
|
|
3449
|
+
async getPublishableKey(options) {
|
|
3450
|
+
return this._get("/config/publishable-key", options);
|
|
3451
|
+
}
|
|
3452
|
+
// --------------------------------------------------------------------------
|
|
3453
|
+
// Payments (Marketplace)
|
|
3454
|
+
// --------------------------------------------------------------------------
|
|
3455
|
+
async createPayment(data, options) {
|
|
3456
|
+
return this.post("/payments", data, options);
|
|
3457
|
+
}
|
|
3458
|
+
async getPayment(id, options) {
|
|
3459
|
+
return this._get(`/payments/${id}`, options);
|
|
3460
|
+
}
|
|
3461
|
+
async listPayments(params, options) {
|
|
3462
|
+
return this._list("/payments", params, options);
|
|
3463
|
+
}
|
|
3464
|
+
// --------------------------------------------------------------------------
|
|
3465
|
+
// Refunds
|
|
3466
|
+
// --------------------------------------------------------------------------
|
|
3467
|
+
async refundPayment(id, data, options) {
|
|
3468
|
+
return this.post(`/payments/${id}/refund`, data, options);
|
|
3469
|
+
}
|
|
3470
|
+
// --------------------------------------------------------------------------
|
|
3471
|
+
// Payouts
|
|
3472
|
+
// --------------------------------------------------------------------------
|
|
3473
|
+
async getPayoutHistory(accountId, params, options) {
|
|
3474
|
+
return this._list(`/connected-accounts/${accountId}/payouts`, params, options);
|
|
3475
|
+
}
|
|
3476
|
+
async getPayoutSchedule(accountId, options) {
|
|
3477
|
+
return this._get(`/connected-accounts/${accountId}/payout-schedule`, options);
|
|
3478
|
+
}
|
|
3479
|
+
async setPayoutSchedule(accountId, data, options) {
|
|
3480
|
+
return this.put(`/connected-accounts/${accountId}/payout-schedule`, data, options);
|
|
3481
|
+
}
|
|
3482
|
+
// --------------------------------------------------------------------------
|
|
3483
|
+
// Ledger
|
|
3484
|
+
// --------------------------------------------------------------------------
|
|
3485
|
+
async getTransactions(params, options) {
|
|
3486
|
+
return this._list("/transactions", params, options);
|
|
3487
|
+
}
|
|
3488
|
+
async getTransactionSummary(params, options) {
|
|
3489
|
+
return this._get(this.withQuery("/transactions/summary", params), options);
|
|
3490
|
+
}
|
|
3491
|
+
// --------------------------------------------------------------------------
|
|
3492
|
+
// Setup Sessions
|
|
3493
|
+
// --------------------------------------------------------------------------
|
|
3494
|
+
async createSetupSession(data, options) {
|
|
3495
|
+
return this.post("/setup-sessions", data, options);
|
|
3496
|
+
}
|
|
3497
|
+
// --------------------------------------------------------------------------
|
|
3498
|
+
// Connected Account Operations: Products, Prices, Subscriptions, Transfers
|
|
3499
|
+
// --------------------------------------------------------------------------
|
|
3500
|
+
async createProduct(data, options) {
|
|
3501
|
+
return this.post("/products", data, options);
|
|
3502
|
+
}
|
|
3503
|
+
async createPrice(data, options) {
|
|
3504
|
+
return this.post("/prices", data, options);
|
|
3505
|
+
}
|
|
3506
|
+
async deactivatePrice(id, options) {
|
|
3507
|
+
return this.post(`/prices/${id}/deactivate`, void 0, options);
|
|
3508
|
+
}
|
|
3509
|
+
async createConnectedSubscription(data, options) {
|
|
3510
|
+
return this.post("/connected-subscriptions", data, options);
|
|
3511
|
+
}
|
|
3512
|
+
async cancelConnectedSubscription(id, data, options) {
|
|
3513
|
+
return this.post(`/connected-subscriptions/${id}/cancel`, data, options);
|
|
3514
|
+
}
|
|
3515
|
+
async listConnectedSubscriptions(params, options) {
|
|
3516
|
+
return this._list("/connected-subscriptions", params, options);
|
|
3517
|
+
}
|
|
3518
|
+
async createConnectedSetupIntent(data, options) {
|
|
3519
|
+
return this.post("/connected-setup-intents", data, options);
|
|
3520
|
+
}
|
|
3521
|
+
async createTransfer(data, options) {
|
|
3522
|
+
return this.post("/transfers", data, options);
|
|
3523
|
+
}
|
|
3524
|
+
async syncPaymentStatus(id, options) {
|
|
3525
|
+
return this.post(`/payments/${id}/sync`, void 0, options);
|
|
3526
|
+
}
|
|
3527
|
+
// --------------------------------------------------------------------------
|
|
3528
|
+
// Legacy methods (backward compat)
|
|
3529
|
+
// --------------------------------------------------------------------------
|
|
3530
|
+
/** @deprecated Use subscribe() instead */
|
|
3531
|
+
async createSubscription(data) {
|
|
3532
|
+
return this.subscribe(data);
|
|
3533
|
+
}
|
|
3534
|
+
/** @deprecated Use listInvoices() instead */
|
|
3535
|
+
async getInvoices(params) {
|
|
3536
|
+
return this.listInvoices(params);
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
|
|
3540
|
+
// src/services/analytics.ts
|
|
3541
|
+
var AnalyticsService = class extends ServiceModule {
|
|
3542
|
+
constructor() {
|
|
3543
|
+
super(...arguments);
|
|
3544
|
+
this.basePath = "/v1/analytics";
|
|
3545
|
+
}
|
|
3546
|
+
// --------------------------------------------------------------------------
|
|
3547
|
+
// Event Tracking (v2 — JetStream buffered)
|
|
3548
|
+
// --------------------------------------------------------------------------
|
|
3549
|
+
async track(event, properties, userId, options) {
|
|
3550
|
+
return this.post("/v2/events", { event, properties, user_id: userId }, options);
|
|
3551
|
+
}
|
|
3552
|
+
async trackBatch(events, options) {
|
|
3553
|
+
return this.post("/v2/events/batch", { events }, options);
|
|
3554
|
+
}
|
|
3555
|
+
async trackPageView(data, options) {
|
|
3556
|
+
return this.post("/page-view", data, options);
|
|
3557
|
+
}
|
|
3558
|
+
// --------------------------------------------------------------------------
|
|
3559
|
+
// Identity
|
|
3560
|
+
// --------------------------------------------------------------------------
|
|
3561
|
+
async identify(userId, traits, anonymousId, options) {
|
|
3562
|
+
return this.post("/identify", { user_id: userId, traits, anonymous_id: anonymousId }, options);
|
|
3563
|
+
}
|
|
3564
|
+
async alias(userId, anonymousId, options) {
|
|
3565
|
+
return this.post("/alias", { user_id: userId, anonymous_id: anonymousId }, options);
|
|
3566
|
+
}
|
|
3567
|
+
// --------------------------------------------------------------------------
|
|
3568
|
+
// Query & Aggregations
|
|
3569
|
+
// --------------------------------------------------------------------------
|
|
3570
|
+
async queryEvents(filters) {
|
|
3571
|
+
return this._list("/events", filters);
|
|
3572
|
+
}
|
|
3573
|
+
async getAggregations(filters) {
|
|
3574
|
+
return this._get(this.withQuery("/aggregations", filters));
|
|
3575
|
+
}
|
|
3576
|
+
async getTopEvents(filters) {
|
|
3577
|
+
return this._get(this.withQuery("/top-events", filters));
|
|
3578
|
+
}
|
|
3579
|
+
async getActiveUsers() {
|
|
3580
|
+
return this._get("/users/active");
|
|
3581
|
+
}
|
|
3582
|
+
// --------------------------------------------------------------------------
|
|
3583
|
+
// Funnels
|
|
3584
|
+
// --------------------------------------------------------------------------
|
|
3585
|
+
async createFunnel(data) {
|
|
3586
|
+
return this.post("/funnels", data);
|
|
3587
|
+
}
|
|
3588
|
+
async listFunnels() {
|
|
3589
|
+
return this._get("/funnels");
|
|
3590
|
+
}
|
|
3591
|
+
async getFunnelConversions(id) {
|
|
3592
|
+
return this._get(`/funnels/${id}/conversions`);
|
|
3593
|
+
}
|
|
3594
|
+
// --------------------------------------------------------------------------
|
|
3595
|
+
// Custom Metrics
|
|
3596
|
+
// --------------------------------------------------------------------------
|
|
3597
|
+
async trackMetric(data, options) {
|
|
3598
|
+
return this.post("/metrics", data, options);
|
|
3599
|
+
}
|
|
3600
|
+
async queryMetrics(filters) {
|
|
3601
|
+
return this._get(this.withQuery("/metrics/query", filters));
|
|
3602
|
+
}
|
|
3603
|
+
// --------------------------------------------------------------------------
|
|
3604
|
+
// Legacy methods (backward compat)
|
|
3605
|
+
// --------------------------------------------------------------------------
|
|
3606
|
+
/** @deprecated Use queryEvents() instead */
|
|
3607
|
+
async query(filters) {
|
|
3608
|
+
return this._get(this.withQuery("/events", filters));
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
|
|
3612
|
+
// src/services/communication.ts
|
|
3613
|
+
var CommunicationService = class extends ServiceModule {
|
|
3614
|
+
constructor() {
|
|
3615
|
+
super(...arguments);
|
|
3616
|
+
this.basePath = "/v1/communication";
|
|
3617
|
+
}
|
|
3618
|
+
// --------------------------------------------------------------------------
|
|
3619
|
+
// Email
|
|
3620
|
+
// --------------------------------------------------------------------------
|
|
3621
|
+
async sendEmail(data, options) {
|
|
3622
|
+
return this.post("/email/send", data, options);
|
|
3623
|
+
}
|
|
3624
|
+
async sendEmailTemplate(template, data, options) {
|
|
3625
|
+
return this.post(`/email/templates/${template}/send`, data, options);
|
|
3626
|
+
}
|
|
3627
|
+
// --------------------------------------------------------------------------
|
|
3628
|
+
// SMS
|
|
3629
|
+
// --------------------------------------------------------------------------
|
|
3630
|
+
async sendSms(data, options) {
|
|
3631
|
+
return this.post("/sms/send", data, options);
|
|
3632
|
+
}
|
|
3633
|
+
async sendSmsTemplate(template, data, options) {
|
|
3634
|
+
return this.post(`/sms/templates/${template}/send`, data, options);
|
|
3635
|
+
}
|
|
3636
|
+
// --------------------------------------------------------------------------
|
|
3637
|
+
// Push Notifications
|
|
3638
|
+
// --------------------------------------------------------------------------
|
|
3639
|
+
async sendPush(data, options) {
|
|
3640
|
+
return this.post("/push/send", data, options);
|
|
3641
|
+
}
|
|
3642
|
+
async registerPushToken(data, options) {
|
|
3643
|
+
return this.post("/push/register", data, options);
|
|
3644
|
+
}
|
|
3645
|
+
async unregisterPushToken(token, options) {
|
|
3646
|
+
return this.del(`/push/tokens/${token}`, options);
|
|
3647
|
+
}
|
|
3648
|
+
// --------------------------------------------------------------------------
|
|
3649
|
+
// Message Status
|
|
3650
|
+
// --------------------------------------------------------------------------
|
|
3651
|
+
async getMessageStatus(id, options) {
|
|
3652
|
+
return this._get(`/messages/${id}`, options);
|
|
3653
|
+
}
|
|
3654
|
+
// --------------------------------------------------------------------------
|
|
3655
|
+
// Legacy methods (backward compat)
|
|
3656
|
+
// --------------------------------------------------------------------------
|
|
3657
|
+
/** @deprecated Use sendSms() instead */
|
|
3658
|
+
async sendSMS(data) {
|
|
3659
|
+
return this.sendSms(data);
|
|
3660
|
+
}
|
|
3661
|
+
/** @deprecated Use sendPush() instead */
|
|
3662
|
+
async sendPushNotification(data) {
|
|
3663
|
+
return this.sendPush(data);
|
|
3664
|
+
}
|
|
3665
|
+
};
|
|
3666
|
+
|
|
3667
|
+
// src/services/scheduler.ts
|
|
3668
|
+
var SchedulerService = class extends ServiceModule {
|
|
3669
|
+
constructor() {
|
|
3670
|
+
super(...arguments);
|
|
3671
|
+
this.basePath = "/v1/scheduler";
|
|
3672
|
+
}
|
|
3673
|
+
// --------------------------------------------------------------------------
|
|
3674
|
+
// Job CRUD
|
|
3675
|
+
// --------------------------------------------------------------------------
|
|
3676
|
+
async createJob(data, options) {
|
|
3677
|
+
return this.post("/jobs", data, options);
|
|
3678
|
+
}
|
|
3679
|
+
async listJobs(params, requestOptions) {
|
|
3680
|
+
return this._list("/jobs", params, requestOptions);
|
|
3681
|
+
}
|
|
3682
|
+
async getJob(id, options) {
|
|
3683
|
+
return this._get(`/jobs/${id}`, options);
|
|
3684
|
+
}
|
|
3685
|
+
async updateJob(id, data, options) {
|
|
3686
|
+
return this.patch(`/jobs/${id}`, data, options);
|
|
3687
|
+
}
|
|
3688
|
+
async deleteJob(id, options) {
|
|
3689
|
+
return this.del(`/jobs/${id}`, options);
|
|
3690
|
+
}
|
|
3691
|
+
// --------------------------------------------------------------------------
|
|
3692
|
+
// Job Control
|
|
3693
|
+
// --------------------------------------------------------------------------
|
|
3694
|
+
async pauseJob(id, options) {
|
|
3695
|
+
return this.post(`/jobs/${id}/pause`, void 0, options);
|
|
3696
|
+
}
|
|
3697
|
+
async resumeJob(id, options) {
|
|
3698
|
+
return this.post(`/jobs/${id}/resume`, void 0, options);
|
|
3699
|
+
}
|
|
3700
|
+
async runNow(id, options) {
|
|
3701
|
+
return this.post(`/jobs/${id}/run-now`, void 0, options);
|
|
3702
|
+
}
|
|
3703
|
+
// --------------------------------------------------------------------------
|
|
3704
|
+
// Execution History & Stats
|
|
3705
|
+
// --------------------------------------------------------------------------
|
|
3706
|
+
async getExecutions(jobId, params, requestOptions) {
|
|
3707
|
+
return this._list(`/jobs/${jobId}/executions`, params, requestOptions);
|
|
3708
|
+
}
|
|
3709
|
+
async getStats(jobId, options) {
|
|
3710
|
+
return this._get(`/jobs/${jobId}/stats`, options);
|
|
3711
|
+
}
|
|
3712
|
+
};
|
|
3713
|
+
|
|
3714
|
+
// src/services/permissions.ts
|
|
3715
|
+
function canPerform(matrix, resource, action) {
|
|
3716
|
+
if (!matrix) return false;
|
|
3717
|
+
const resourcePerms = matrix.permissions[resource];
|
|
3718
|
+
if (!resourcePerms) return false;
|
|
3719
|
+
return resourcePerms[action] === "allow";
|
|
3720
|
+
}
|
|
3721
|
+
function hasMinRoleLevel(matrix, minLevel) {
|
|
3722
|
+
if (!matrix) return false;
|
|
3723
|
+
if (matrix.roleLevel === void 0) return false;
|
|
3724
|
+
return matrix.roleLevel >= minLevel;
|
|
3725
|
+
}
|
|
3726
|
+
var PermissionsService = class extends ServiceModule {
|
|
3727
|
+
constructor() {
|
|
3728
|
+
super(...arguments);
|
|
3729
|
+
this.basePath = "/v1/permissions";
|
|
3730
|
+
}
|
|
3731
|
+
// --------------------------------------------------------------------------
|
|
3732
|
+
// Roles
|
|
3733
|
+
// --------------------------------------------------------------------------
|
|
3734
|
+
async createRole(data, options) {
|
|
3735
|
+
return this.post("/roles", data, options);
|
|
3736
|
+
}
|
|
3737
|
+
async listRoles(options) {
|
|
3738
|
+
return this._get("/roles", options);
|
|
3739
|
+
}
|
|
3740
|
+
async assignPermissions(roleId, permissions, options) {
|
|
3741
|
+
return this.post(`/roles/${roleId}/permissions`, { permissions }, options);
|
|
3742
|
+
}
|
|
3743
|
+
async assignRole(userId, roleId, options) {
|
|
3744
|
+
return this.post(`/users/${userId}/roles`, { role_id: roleId }, options);
|
|
3745
|
+
}
|
|
3746
|
+
// --------------------------------------------------------------------------
|
|
3747
|
+
// Permission Checks (unified — supports both member and user identity types)
|
|
3748
|
+
// --------------------------------------------------------------------------
|
|
3749
|
+
/** Check a single permission. Supports identity_type for unified model. */
|
|
3750
|
+
async check(identityId, permission, options) {
|
|
3751
|
+
const { identityType, resourceType, resourceId, ...reqOptions } = options || {};
|
|
3752
|
+
return this.post("/check", {
|
|
3753
|
+
identity_id: identityId,
|
|
3754
|
+
identity_type: identityType || "user",
|
|
3755
|
+
permission,
|
|
3756
|
+
resource_type: resourceType,
|
|
3757
|
+
resource_id: resourceId
|
|
3758
|
+
}, reqOptions);
|
|
3759
|
+
}
|
|
3760
|
+
/** Batch check multiple permissions for an identity. */
|
|
3761
|
+
async batchCheck(identityId, permissions, options) {
|
|
3762
|
+
const { identityType, ...reqOptions } = options || {};
|
|
3763
|
+
return this.post("/batch-check", {
|
|
3764
|
+
identity_id: identityId,
|
|
3765
|
+
identity_type: identityType || "user",
|
|
3766
|
+
permissions
|
|
3767
|
+
}, reqOptions);
|
|
3768
|
+
}
|
|
3769
|
+
/** Fetch the full permission matrix for an identity (single request, no N+1). */
|
|
3770
|
+
async getMatrix(identityId, identityType = "user", options) {
|
|
3771
|
+
const params = new URLSearchParams({ identity_id: identityId, identity_type: identityType });
|
|
3772
|
+
return this._get(`/matrix?${params.toString()}`, options);
|
|
3773
|
+
}
|
|
3774
|
+
async getUserPermissions(userId, options) {
|
|
3775
|
+
return this._get(`/users/${userId}/permissions`, options);
|
|
3776
|
+
}
|
|
3777
|
+
// --------------------------------------------------------------------------
|
|
3778
|
+
// Policies
|
|
3779
|
+
// --------------------------------------------------------------------------
|
|
3780
|
+
async createPolicy(data, options) {
|
|
3781
|
+
return this.post("/policies", data, options);
|
|
3782
|
+
}
|
|
3783
|
+
async listPolicies(options) {
|
|
3784
|
+
return this._get("/policies", options);
|
|
3785
|
+
}
|
|
3786
|
+
async evaluate(data, options) {
|
|
3787
|
+
return this.post("/evaluate", data, options);
|
|
3788
|
+
}
|
|
3789
|
+
// --------------------------------------------------------------------------
|
|
3790
|
+
// Legacy methods (backward compat)
|
|
3791
|
+
// --------------------------------------------------------------------------
|
|
3792
|
+
/** @deprecated Use assignPermissions() instead */
|
|
3793
|
+
async assignPermission(roleId, permission) {
|
|
3794
|
+
return this.assignPermissions(roleId, [permission]);
|
|
3795
|
+
}
|
|
3796
|
+
/** @deprecated Use check() instead */
|
|
3797
|
+
async checkPermission(userId, permission) {
|
|
3798
|
+
return this.check(userId, permission);
|
|
3799
|
+
}
|
|
3800
|
+
};
|
|
3801
|
+
|
|
3802
|
+
// src/services/teams.ts
|
|
3803
|
+
var TeamsService = class extends ServiceModule {
|
|
3804
|
+
constructor() {
|
|
3805
|
+
super(...arguments);
|
|
3806
|
+
this.basePath = "/v1/teams";
|
|
3807
|
+
}
|
|
3808
|
+
// --------------------------------------------------------------------------
|
|
3809
|
+
// Team CRUD
|
|
3810
|
+
// --------------------------------------------------------------------------
|
|
3811
|
+
async create(data, options) {
|
|
3812
|
+
return this.post("", data, options);
|
|
3813
|
+
}
|
|
3814
|
+
async list(params, requestOptions) {
|
|
3815
|
+
return this._list("", params, requestOptions);
|
|
3816
|
+
}
|
|
3817
|
+
async get(id, options) {
|
|
3818
|
+
return this._get(`/${id}`, options);
|
|
3819
|
+
}
|
|
3820
|
+
async update(id, data, options) {
|
|
3821
|
+
return this.patch(`/${id}`, data, options);
|
|
3822
|
+
}
|
|
3823
|
+
async delete(id, options) {
|
|
3824
|
+
return this.del(`/${id}`, options);
|
|
3825
|
+
}
|
|
3826
|
+
// --------------------------------------------------------------------------
|
|
3827
|
+
// Members
|
|
3828
|
+
// --------------------------------------------------------------------------
|
|
3829
|
+
async listMembers(teamId, params, requestOptions) {
|
|
3830
|
+
return this._list(`/${teamId}/members`, params, requestOptions);
|
|
3831
|
+
}
|
|
3832
|
+
async addMember(teamId, data, options) {
|
|
3833
|
+
return this.post(`/${teamId}/members`, data, options);
|
|
3834
|
+
}
|
|
3835
|
+
async updateMember(teamId, userId, data, options) {
|
|
3836
|
+
return this.patch(`/${teamId}/members/${userId}`, data, options);
|
|
3837
|
+
}
|
|
3838
|
+
async removeMember(teamId, userId, options) {
|
|
3839
|
+
return this.del(`/${teamId}/members/${userId}`, options);
|
|
3840
|
+
}
|
|
3841
|
+
// --------------------------------------------------------------------------
|
|
3842
|
+
// Invitations
|
|
3843
|
+
// --------------------------------------------------------------------------
|
|
3844
|
+
async invite(teamId, data, options) {
|
|
3845
|
+
return this.post(`/${teamId}/invitations`, data, options);
|
|
3846
|
+
}
|
|
3847
|
+
async listInvitations(teamId, options) {
|
|
3848
|
+
return this._get(`/${teamId}/invitations`, options);
|
|
3849
|
+
}
|
|
3850
|
+
async acceptInvitation(token, options) {
|
|
3851
|
+
return this.post(`/invitations/${token}/accept`, void 0, options);
|
|
3852
|
+
}
|
|
3853
|
+
async cancelInvitation(id, options) {
|
|
3854
|
+
return this.del(`/invitations/${id}`, options);
|
|
3855
|
+
}
|
|
3856
|
+
// --------------------------------------------------------------------------
|
|
3857
|
+
// SSO
|
|
3858
|
+
// --------------------------------------------------------------------------
|
|
3859
|
+
async configureSso(teamId, data, options) {
|
|
3860
|
+
return this.post(`/${teamId}/sso/configure`, data, options);
|
|
3861
|
+
}
|
|
3862
|
+
async getSso(teamId, options) {
|
|
3863
|
+
return this._get(`/${teamId}/sso`, options);
|
|
3864
|
+
}
|
|
3865
|
+
// --------------------------------------------------------------------------
|
|
3866
|
+
// Legacy methods (backward compat)
|
|
3867
|
+
// --------------------------------------------------------------------------
|
|
3868
|
+
/** @deprecated Use create() instead */
|
|
3869
|
+
async createTeam(data) {
|
|
3870
|
+
return this.create(data);
|
|
3871
|
+
}
|
|
3872
|
+
/** @deprecated Use invite() instead */
|
|
3873
|
+
async inviteMember(teamId, email, role) {
|
|
3874
|
+
return this.invite(teamId, { email, role });
|
|
3875
|
+
}
|
|
3876
|
+
};
|
|
3877
|
+
|
|
3878
|
+
// src/services/graph.ts
|
|
3879
|
+
var GraphService = class extends ServiceModule {
|
|
3880
|
+
constructor() {
|
|
3881
|
+
super(...arguments);
|
|
3882
|
+
this.basePath = "/v1/graph";
|
|
3883
|
+
}
|
|
3884
|
+
async createNode(data, requestOptions) {
|
|
3885
|
+
return this.post("/nodes", data, requestOptions);
|
|
3886
|
+
}
|
|
3887
|
+
async updateNode(nodeId, data, requestOptions) {
|
|
3888
|
+
return this.patch(`/nodes/${nodeId}`, data, requestOptions);
|
|
3889
|
+
}
|
|
3890
|
+
async createEdge(data, requestOptions) {
|
|
3891
|
+
return this.post("/edges", data, requestOptions);
|
|
3892
|
+
}
|
|
3893
|
+
async getEdges(nodeId, options, requestOptions) {
|
|
3894
|
+
return this._get(this.withQuery(`/nodes/${nodeId}/edges`, options), requestOptions);
|
|
3895
|
+
}
|
|
3896
|
+
async traverse(nodeId, options, requestOptions) {
|
|
3897
|
+
return this._get(this.withQuery(`/nodes/${nodeId}/traverse`, options), requestOptions);
|
|
3898
|
+
}
|
|
3899
|
+
async shortestPath(options, requestOptions) {
|
|
3900
|
+
return this.post("/shortest-path", options, requestOptions);
|
|
3901
|
+
}
|
|
3902
|
+
async neighbors(nodeId, options, requestOptions) {
|
|
3903
|
+
return this._get(this.withQuery(`/nodes/${nodeId}/neighbors`, options), requestOptions);
|
|
3904
|
+
}
|
|
3905
|
+
async pageRank(options, requestOptions) {
|
|
3906
|
+
return this.post("/algorithms/pagerank", options, requestOptions);
|
|
3907
|
+
}
|
|
3908
|
+
async centrality(options, requestOptions) {
|
|
3909
|
+
return this.post("/algorithms/centrality", options, requestOptions);
|
|
3910
|
+
}
|
|
3911
|
+
async connectedComponents(options) {
|
|
3912
|
+
return this.post("/algorithms/connected-components", void 0, options);
|
|
3913
|
+
}
|
|
3914
|
+
};
|
|
3915
|
+
|
|
3916
|
+
// src/services/functions.ts
|
|
3917
|
+
var FunctionsService = class extends ServiceModule {
|
|
3918
|
+
constructor() {
|
|
3919
|
+
super(...arguments);
|
|
3920
|
+
this.basePath = "/v1/functions";
|
|
3921
|
+
}
|
|
3922
|
+
async deploy(data, options) {
|
|
3923
|
+
return this.post("", data, options);
|
|
3924
|
+
}
|
|
3925
|
+
async list(options) {
|
|
3926
|
+
return this._get("", options);
|
|
3927
|
+
}
|
|
3928
|
+
async get(name, options) {
|
|
3929
|
+
return this._get(`/${name}`, options);
|
|
3930
|
+
}
|
|
3931
|
+
async update(name, data, options) {
|
|
3932
|
+
return this.patch(`/${name}`, data, options);
|
|
3933
|
+
}
|
|
3934
|
+
async delete(name, options) {
|
|
3935
|
+
return this.del(`/${name}`, options);
|
|
3936
|
+
}
|
|
3937
|
+
async invoke(name, payload, options) {
|
|
3938
|
+
return this.post(`/${name}/invoke`, payload, options);
|
|
3939
|
+
}
|
|
3940
|
+
async invokeAsync(name, payload, options) {
|
|
3941
|
+
return this.post(`/${name}/invoke-async`, payload, options);
|
|
3942
|
+
}
|
|
3943
|
+
async getLogs(name, options) {
|
|
3944
|
+
return this._get(`/${name}/logs`, options);
|
|
3945
|
+
}
|
|
3946
|
+
async getExecutions(name, params, requestOptions) {
|
|
3947
|
+
return this._list(`/${name}/executions`, params, requestOptions);
|
|
3948
|
+
}
|
|
3949
|
+
async getMetrics(name, options) {
|
|
3950
|
+
return this._get(`/${name}/metrics`, options);
|
|
3951
|
+
}
|
|
3952
|
+
/** @deprecated Use deploy() instead */
|
|
3953
|
+
async deployFunction(data) {
|
|
3954
|
+
return this.deploy(data);
|
|
3955
|
+
}
|
|
3956
|
+
/** @deprecated Use invoke() instead */
|
|
3957
|
+
async invokeFunction(name, payload) {
|
|
3958
|
+
return this.invoke(name, payload);
|
|
3959
|
+
}
|
|
3960
|
+
};
|
|
3961
|
+
|
|
3962
|
+
// src/services/listings.ts
|
|
3963
|
+
var ListingsService = class extends ServiceModule {
|
|
3964
|
+
constructor() {
|
|
3965
|
+
super(...arguments);
|
|
3966
|
+
this.basePath = "/v1/listings";
|
|
3967
|
+
}
|
|
3968
|
+
async create(data, options) {
|
|
3969
|
+
return this.post("", data, options);
|
|
3970
|
+
}
|
|
3971
|
+
async get(id, options) {
|
|
3972
|
+
return this._get(`/${id}`, options);
|
|
3973
|
+
}
|
|
3974
|
+
async update(id, data, options) {
|
|
3975
|
+
return this.patch(`/${id}`, data, options);
|
|
3976
|
+
}
|
|
3977
|
+
async delete(id, options) {
|
|
3978
|
+
return this.del(`/${id}`, options);
|
|
3979
|
+
}
|
|
3980
|
+
async search(query, filters, options) {
|
|
3981
|
+
return this._get(this.withQuery("/search", { query, ...filters }), options);
|
|
3982
|
+
}
|
|
3983
|
+
async nearby(nearbyOptions, options) {
|
|
3984
|
+
return this._get(this.withQuery("/nearby", nearbyOptions), options);
|
|
3985
|
+
}
|
|
3986
|
+
async getByCategory(category, params, requestOptions) {
|
|
3987
|
+
return this._list(`/categories/${category}`, params, requestOptions);
|
|
3988
|
+
}
|
|
3989
|
+
async favorite(listingId, options) {
|
|
3990
|
+
return this.post(`/${listingId}/favorite`, void 0, options);
|
|
3991
|
+
}
|
|
3992
|
+
async unfavorite(listingId, options) {
|
|
3993
|
+
return this.del(`/${listingId}/favorite`, options);
|
|
3994
|
+
}
|
|
3995
|
+
async getFavorites(params, requestOptions) {
|
|
3996
|
+
return this._list("/favorites", params, requestOptions);
|
|
3997
|
+
}
|
|
3998
|
+
async trackView(listingId, options) {
|
|
3999
|
+
return this.post(`/${listingId}/view`, void 0, options);
|
|
4000
|
+
}
|
|
4001
|
+
/** @deprecated Use create() instead */
|
|
4002
|
+
async createListing(data) {
|
|
4003
|
+
return this.create(data);
|
|
4004
|
+
}
|
|
4005
|
+
/** @deprecated Use search() instead */
|
|
4006
|
+
async searchListings(query, filters) {
|
|
4007
|
+
return this.search(query, filters);
|
|
4008
|
+
}
|
|
4009
|
+
/** @deprecated Use get() instead */
|
|
4010
|
+
async getListing(id) {
|
|
4011
|
+
return this.get(id);
|
|
4012
|
+
}
|
|
4013
|
+
};
|
|
4014
|
+
|
|
4015
|
+
// src/services/events.ts
|
|
4016
|
+
var EventsService = class extends ServiceModule {
|
|
4017
|
+
constructor() {
|
|
4018
|
+
super(...arguments);
|
|
4019
|
+
this.basePath = "/v1/events";
|
|
4020
|
+
}
|
|
4021
|
+
async create(data, options) {
|
|
4022
|
+
return this.post("", data, options);
|
|
4023
|
+
}
|
|
4024
|
+
async get(eventId, options) {
|
|
4025
|
+
return this._get(`/${eventId}`, options);
|
|
4026
|
+
}
|
|
4027
|
+
async update(eventId, data, options) {
|
|
4028
|
+
return this.patch(`/${eventId}`, data, options);
|
|
4029
|
+
}
|
|
4030
|
+
async delete(eventId, options) {
|
|
4031
|
+
return this.del(`/${eventId}`, options);
|
|
4032
|
+
}
|
|
4033
|
+
async list(filters, requestOptions) {
|
|
4034
|
+
return this._list("", filters, requestOptions);
|
|
4035
|
+
}
|
|
4036
|
+
async register(eventId, options) {
|
|
4037
|
+
return this.post(`/${eventId}/register`, void 0, options);
|
|
4038
|
+
}
|
|
4039
|
+
async unregister(eventId, options) {
|
|
4040
|
+
return this.del(`/${eventId}/register`, options);
|
|
4041
|
+
}
|
|
4042
|
+
async getAttendees(eventId, params, requestOptions) {
|
|
4043
|
+
return this._list(`/${eventId}/attendees`, params, requestOptions);
|
|
4044
|
+
}
|
|
4045
|
+
async checkIn(eventId, options) {
|
|
4046
|
+
return this.post(`/${eventId}/check-in`, void 0, options);
|
|
4047
|
+
}
|
|
4048
|
+
/** @deprecated Use create() instead */
|
|
4049
|
+
async createEvent(data) {
|
|
4050
|
+
return this.create(data);
|
|
4051
|
+
}
|
|
4052
|
+
/** @deprecated Use list() instead */
|
|
4053
|
+
async listEvents(filters) {
|
|
4054
|
+
return this.list(filters);
|
|
4055
|
+
}
|
|
4056
|
+
};
|
|
4057
|
+
|
|
4058
|
+
// src/services/leaderboard.ts
|
|
4059
|
+
var LeaderboardService = class extends ServiceModule {
|
|
4060
|
+
constructor() {
|
|
4061
|
+
super(...arguments);
|
|
4062
|
+
this.basePath = "/v1/leaderboard";
|
|
4063
|
+
}
|
|
4064
|
+
async create(data, options) {
|
|
4065
|
+
return this.post("", data, options);
|
|
4066
|
+
}
|
|
4067
|
+
async submitScore(boardId, data, options) {
|
|
4068
|
+
return this.post(`/${boardId}/scores`, data, options);
|
|
4069
|
+
}
|
|
4070
|
+
async getRankings(boardId, rankingOptions, requestOptions) {
|
|
4071
|
+
return this._get(this.withQuery(`/${boardId}/rankings`, rankingOptions), requestOptions);
|
|
4072
|
+
}
|
|
4073
|
+
async getUserRank(boardId, userId, options) {
|
|
4074
|
+
return this._get(`/${boardId}/users/${userId}/rank`, options);
|
|
4075
|
+
}
|
|
4076
|
+
async getUserHistory(boardId, userId, options) {
|
|
4077
|
+
return this._get(`/${boardId}/users/${userId}/history`, options);
|
|
4078
|
+
}
|
|
4079
|
+
async updateScore(boardId, userId, data, options) {
|
|
4080
|
+
return this.patch(`/${boardId}/users/${userId}/score`, data, options);
|
|
4081
|
+
}
|
|
4082
|
+
async deleteScore(boardId, userId, options) {
|
|
4083
|
+
return this.del(`/${boardId}/users/${userId}/score`, options);
|
|
4084
|
+
}
|
|
4085
|
+
};
|
|
4086
|
+
|
|
4087
|
+
// src/services/webhooks.ts
|
|
4088
|
+
var WebhooksService = class extends ServiceModule {
|
|
4089
|
+
constructor() {
|
|
4090
|
+
super(...arguments);
|
|
4091
|
+
this.basePath = "/v1/webhooks";
|
|
4092
|
+
}
|
|
4093
|
+
async create(data, options) {
|
|
4094
|
+
return this.post("", data, options);
|
|
4095
|
+
}
|
|
4096
|
+
async list(options) {
|
|
4097
|
+
return this._get("", options);
|
|
4098
|
+
}
|
|
4099
|
+
async get(id, options) {
|
|
4100
|
+
return this._get(`/${id}`, options);
|
|
4101
|
+
}
|
|
4102
|
+
async update(id, data, options) {
|
|
4103
|
+
return this.patch(`/${id}`, data, options);
|
|
4104
|
+
}
|
|
4105
|
+
async delete(id, options) {
|
|
4106
|
+
return this.del(`/${id}`, options);
|
|
4107
|
+
}
|
|
4108
|
+
async listEvents(options) {
|
|
4109
|
+
return this._get("/events", options);
|
|
4110
|
+
}
|
|
4111
|
+
/** @deprecated Use create() instead */
|
|
4112
|
+
async createWebhook(data) {
|
|
4113
|
+
return this.create(data);
|
|
4114
|
+
}
|
|
4115
|
+
/** @deprecated Use list() instead */
|
|
4116
|
+
async listWebhooks() {
|
|
4117
|
+
return this.list();
|
|
4118
|
+
}
|
|
4119
|
+
/** @deprecated Use delete() instead */
|
|
4120
|
+
async deleteWebhook(id) {
|
|
4121
|
+
return this.delete(id);
|
|
4122
|
+
}
|
|
4123
|
+
};
|
|
4124
|
+
|
|
4125
|
+
// src/services/search.ts
|
|
4126
|
+
var SearchService = class extends ServiceModule {
|
|
4127
|
+
constructor() {
|
|
4128
|
+
super(...arguments);
|
|
4129
|
+
this.basePath = "/v1/search";
|
|
4130
|
+
}
|
|
4131
|
+
async query(queryStr, queryOptions, requestOptions) {
|
|
4132
|
+
return this.post("", { query: queryStr, ...queryOptions }, requestOptions);
|
|
4133
|
+
}
|
|
4134
|
+
async index(indexName, document, options) {
|
|
4135
|
+
return this.post("/documents", { index: indexName, ...document }, options);
|
|
4136
|
+
}
|
|
4137
|
+
async removeDocument(indexName, docId, options) {
|
|
4138
|
+
return this.del(`/documents/${indexName}/${docId}`, options);
|
|
4139
|
+
}
|
|
4140
|
+
/** @deprecated Use query() instead */
|
|
4141
|
+
async search(queryStr, options) {
|
|
4142
|
+
return this.query(queryStr, options);
|
|
4143
|
+
}
|
|
4144
|
+
/** @deprecated Use index() instead */
|
|
4145
|
+
async indexDocument(data) {
|
|
4146
|
+
return this.post("/documents", data);
|
|
4147
|
+
}
|
|
4148
|
+
};
|
|
4149
|
+
|
|
4150
|
+
// src/services/photo.ts
|
|
4151
|
+
var PHOTO_BREAKPOINTS = [150, 320, 640, 1080];
|
|
4152
|
+
var PhotoService = class extends ServiceModule {
|
|
4153
|
+
constructor() {
|
|
4154
|
+
super(...arguments);
|
|
4155
|
+
this.basePath = "/v1/photos";
|
|
4156
|
+
}
|
|
4157
|
+
async upload(file, uploadOptions, requestOptions) {
|
|
4158
|
+
const fields = {};
|
|
4159
|
+
if (uploadOptions?.metadata) fields["metadata"] = JSON.stringify(uploadOptions.metadata);
|
|
4160
|
+
return this._upload("", file, fields, {
|
|
4161
|
+
...requestOptions,
|
|
4162
|
+
onProgress: uploadOptions?.onProgress,
|
|
4163
|
+
signal: uploadOptions?.signal
|
|
4164
|
+
});
|
|
4165
|
+
}
|
|
4166
|
+
async transform(photoId, transformations, options) {
|
|
4167
|
+
return this.post(`/${photoId}/transform`, transformations, options);
|
|
4168
|
+
}
|
|
4169
|
+
async get(id, options) {
|
|
4170
|
+
return this._get(`/${id}`, options);
|
|
4171
|
+
}
|
|
4172
|
+
async delete(id, options) {
|
|
4173
|
+
return this.del(`/${id}`, options);
|
|
4174
|
+
}
|
|
4175
|
+
/**
|
|
4176
|
+
* Build an absolute URL for the on-demand transform endpoint.
|
|
4177
|
+
*
|
|
4178
|
+
* Use in `<img src>` or `srcset` — the server negotiates the best format
|
|
4179
|
+
* (AVIF > WebP > JPEG) from the browser's Accept header automatically.
|
|
4180
|
+
* Transformed images are cached server-side on first request.
|
|
4181
|
+
*/
|
|
4182
|
+
getTransformUrl(photoId, options) {
|
|
4183
|
+
const params = new URLSearchParams();
|
|
4184
|
+
if (options?.width) params.set("width", String(options.width));
|
|
4185
|
+
if (options?.height) params.set("height", String(options.height));
|
|
4186
|
+
if (options?.fit) params.set("fit", options.fit);
|
|
4187
|
+
if (options?.format) params.set("format", options.format);
|
|
4188
|
+
if (options?.quality) params.set("quality", String(options.quality));
|
|
4189
|
+
const qs = params.toString();
|
|
4190
|
+
return `${this.client.getBaseUrl()}${this.basePath}/${photoId}/transform${qs ? `?${qs}` : ""}`;
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* Get a transform URL optimized for the given display area.
|
|
4194
|
+
*
|
|
4195
|
+
* Snaps UP to the nearest pre-generated square breakpoint (150, 320, 640, 1080)
|
|
4196
|
+
* so the response is served instantly from cache. Format is auto-negotiated
|
|
4197
|
+
* by the browser's Accept header (WebP in modern browsers, JPEG fallback).
|
|
4198
|
+
*
|
|
4199
|
+
* @param photoId Photo ID
|
|
4200
|
+
* @param displayWidth CSS pixel width of the display area
|
|
4201
|
+
* @param options.dpr Device pixel ratio (default: 1). Pass `window.devicePixelRatio` in browsers.
|
|
4202
|
+
*
|
|
4203
|
+
* @example
|
|
4204
|
+
* ```typescript
|
|
4205
|
+
* // 280px card on 2x Retina -> snaps to 640px (280×2=560, next breakpoint up)
|
|
4206
|
+
* const url = sm.photo.getOptimalUrl(photoId, 280, { dpr: 2 })
|
|
4207
|
+
*
|
|
4208
|
+
* // Profile avatar at 48px -> snaps to 150px
|
|
4209
|
+
* const url = sm.photo.getOptimalUrl(photoId, 48)
|
|
4210
|
+
* ```
|
|
4211
|
+
*/
|
|
4212
|
+
getOptimalUrl(photoId, displayWidth, options) {
|
|
4213
|
+
const requestedDpr = options?.dpr ?? 1;
|
|
4214
|
+
const dpr = Number.isFinite(requestedDpr) && requestedDpr > 0 ? requestedDpr : 1;
|
|
4215
|
+
const cssWidth = Number.isFinite(displayWidth) && displayWidth > 0 ? displayWidth : PHOTO_BREAKPOINTS[0];
|
|
4216
|
+
const physicalWidth = Math.ceil(cssWidth * dpr);
|
|
4217
|
+
const size = PHOTO_BREAKPOINTS.find((bp) => bp >= physicalWidth) ?? PHOTO_BREAKPOINTS[PHOTO_BREAKPOINTS.length - 1];
|
|
4218
|
+
return this.getTransformUrl(photoId, { width: size, height: size, fit: "cover" });
|
|
4219
|
+
}
|
|
4220
|
+
/**
|
|
4221
|
+
* Generate an HTML srcset string for responsive square photo display.
|
|
4222
|
+
*
|
|
4223
|
+
* Returns all pre-generated breakpoints as width descriptors. Pair with
|
|
4224
|
+
* the `sizes` attribute so the browser picks the optimal variant automatically.
|
|
4225
|
+
*
|
|
4226
|
+
* @example
|
|
4227
|
+
* ```tsx
|
|
4228
|
+
* const srcset = sm.photo.getSrcSet(photoId)
|
|
4229
|
+
* // -> ".../transform?width=150&height=150&fit=cover 150w, .../transform?width=320..."
|
|
4230
|
+
*
|
|
4231
|
+
* <img
|
|
4232
|
+
* src={sm.photo.getOptimalUrl(photoId, 320)}
|
|
4233
|
+
* srcSet={srcset}
|
|
4234
|
+
* sizes="(max-width: 640px) 100vw, 640px"
|
|
4235
|
+
* alt="Photo"
|
|
4236
|
+
* />
|
|
4237
|
+
* ```
|
|
4238
|
+
*/
|
|
4239
|
+
getSrcSet(photoId) {
|
|
4240
|
+
return PHOTO_BREAKPOINTS.map((size) => `${this.getTransformUrl(photoId, { width: size, height: size, fit: "cover" })} ${size}w`).join(", ");
|
|
4241
|
+
}
|
|
4242
|
+
/**
|
|
4243
|
+
* Register a photo from an already-uploaded storage file.
|
|
4244
|
+
*
|
|
4245
|
+
* Creates a photo record so the optimization pipeline can process it.
|
|
4246
|
+
* Use this when files are uploaded via the storage service (presigned URL)
|
|
4247
|
+
* instead of the photo service's upload endpoint.
|
|
4248
|
+
*
|
|
4249
|
+
* Returns the photo record with `id` that can be used with `getTransformUrl()`.
|
|
4250
|
+
*/
|
|
4251
|
+
async register(registerOptions, requestOptions) {
|
|
4252
|
+
return this.post("/register", {
|
|
4253
|
+
file_id: registerOptions.fileId,
|
|
4254
|
+
sm_user_id: registerOptions.userId
|
|
4255
|
+
}, requestOptions);
|
|
4256
|
+
}
|
|
4257
|
+
/** @deprecated Use upload() instead */
|
|
4258
|
+
async uploadPhoto(file, options) {
|
|
4259
|
+
return this.upload(file, options);
|
|
4260
|
+
}
|
|
4261
|
+
/** @deprecated Use transform() instead */
|
|
4262
|
+
async transformPhoto(photoId, transformations) {
|
|
4263
|
+
return this.transform(photoId, transformations);
|
|
4264
|
+
}
|
|
4265
|
+
/** @deprecated Use get() instead */
|
|
4266
|
+
async getPhoto(id) {
|
|
4267
|
+
return this.get(id);
|
|
4268
|
+
}
|
|
4269
|
+
};
|
|
4270
|
+
|
|
4271
|
+
// src/services/queue.ts
|
|
4272
|
+
var DeadLetterApi = class extends ServiceModule {
|
|
4273
|
+
constructor() {
|
|
4274
|
+
super(...arguments);
|
|
4275
|
+
this.basePath = "/v1/queue/dead-letter";
|
|
4276
|
+
}
|
|
4277
|
+
async list(options) {
|
|
4278
|
+
return this._get("", options);
|
|
4279
|
+
}
|
|
4280
|
+
async get(id, options) {
|
|
4281
|
+
return this._get(`/${id}`, options);
|
|
4282
|
+
}
|
|
4283
|
+
async retry(id, options) {
|
|
4284
|
+
return this.post(`/${id}/retry`, void 0, options);
|
|
4285
|
+
}
|
|
4286
|
+
async delete(id, options) {
|
|
4287
|
+
return this.del(`/${id}`, options);
|
|
4288
|
+
}
|
|
4289
|
+
};
|
|
4290
|
+
var QueueService = class extends ServiceModule {
|
|
4291
|
+
constructor(client) {
|
|
4292
|
+
super(client);
|
|
4293
|
+
this.basePath = "/v1/queue";
|
|
4294
|
+
this.deadLetter = new DeadLetterApi(client);
|
|
4295
|
+
}
|
|
4296
|
+
async enqueue(data, options) {
|
|
4297
|
+
return this.post("/jobs", data, options);
|
|
4298
|
+
}
|
|
4299
|
+
async getJob(id, options) {
|
|
4300
|
+
return this._get(`/jobs/${id}`, options);
|
|
4301
|
+
}
|
|
4302
|
+
};
|
|
4303
|
+
|
|
4304
|
+
// src/services/cache.ts
|
|
4305
|
+
var CacheService = class extends ServiceModule {
|
|
4306
|
+
constructor() {
|
|
4307
|
+
super(...arguments);
|
|
4308
|
+
this.basePath = "/v1/cache";
|
|
4309
|
+
}
|
|
4310
|
+
async get(key, options) {
|
|
4311
|
+
return this._get(`/${key}`, options);
|
|
4312
|
+
}
|
|
4313
|
+
async set(key, value, ttl, options) {
|
|
4314
|
+
return this.post("", { key, value, ttl }, options);
|
|
4315
|
+
}
|
|
4316
|
+
async delete(key, options) {
|
|
4317
|
+
return this.del(`/${key}`, options);
|
|
4318
|
+
}
|
|
4319
|
+
async flush(options) {
|
|
4320
|
+
return this.del("", options);
|
|
4321
|
+
}
|
|
4322
|
+
};
|
|
4323
|
+
|
|
4324
|
+
// src/services/compliance.ts
|
|
4325
|
+
var ComplianceService = class extends ServiceModule {
|
|
4326
|
+
constructor() {
|
|
4327
|
+
super(...arguments);
|
|
4328
|
+
this.basePath = "/v1/compliance";
|
|
4329
|
+
}
|
|
4330
|
+
/** Build query string from params object */
|
|
4331
|
+
qs(params) {
|
|
4332
|
+
if (!params) return "";
|
|
4333
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null);
|
|
4334
|
+
if (entries.length === 0) return "";
|
|
4335
|
+
return "?" + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
|
4336
|
+
}
|
|
4337
|
+
// --- Audit Logs ---
|
|
4338
|
+
async log(data, options) {
|
|
4339
|
+
return this.post("/audit-logs", data, options);
|
|
4340
|
+
}
|
|
4341
|
+
async queryAuditLogs(params, requestOptions) {
|
|
4342
|
+
return this._get(`/audit-logs${this.qs(params)}`, requestOptions);
|
|
4343
|
+
}
|
|
4344
|
+
// --- Legacy GDPR (deprecated) ---
|
|
4345
|
+
/** @deprecated Use createDataSubjectRequest({ request_type: 'access', ... }) instead */
|
|
4346
|
+
async requestDataExport(userId) {
|
|
4347
|
+
return this.post("/gdpr/access-request", { user_id: userId });
|
|
4348
|
+
}
|
|
4349
|
+
/** @deprecated Use createDataSubjectRequest({ request_type: 'deletion', ... }) instead */
|
|
4350
|
+
async requestDataDeletion(userId) {
|
|
4351
|
+
return this.post("/gdpr/deletion-request", { user_id: userId });
|
|
4352
|
+
}
|
|
4353
|
+
/** @deprecated Use log() instead */
|
|
4354
|
+
async createAuditLog(data) {
|
|
4355
|
+
return this.log(data);
|
|
4356
|
+
}
|
|
4357
|
+
// --- Consent Purposes ---
|
|
4358
|
+
async listConsentPurposes(options) {
|
|
4359
|
+
return this._get("/consent-purposes", options);
|
|
4360
|
+
}
|
|
4361
|
+
async createConsentPurpose(data, options) {
|
|
4362
|
+
return this.post("/consent-purposes", data, options);
|
|
4363
|
+
}
|
|
4364
|
+
// --- Consent v2 ---
|
|
4365
|
+
async recordConsent(data, options) {
|
|
4366
|
+
return this.post("/consent/v2", data, options);
|
|
4367
|
+
}
|
|
4368
|
+
async getUserConsents(userId, options) {
|
|
4369
|
+
return this._get(`/consent/v2/${userId}`, options);
|
|
4370
|
+
}
|
|
4371
|
+
async withdrawConsent(consentId, data, options) {
|
|
4372
|
+
return this.put(`/consent/v2/${consentId}/withdraw`, data || {}, options);
|
|
4373
|
+
}
|
|
4374
|
+
// --- Data Subject Requests ---
|
|
4375
|
+
async createDataSubjectRequest(data, options) {
|
|
4376
|
+
return this.post("/dsr", data, options);
|
|
4377
|
+
}
|
|
4378
|
+
async listDataSubjectRequests(params, requestOptions) {
|
|
4379
|
+
return this._get(`/dsr${this.qs(params)}`, requestOptions);
|
|
4380
|
+
}
|
|
4381
|
+
async getDataSubjectRequest(id, options) {
|
|
4382
|
+
return this._get(`/dsr/${id}`, options);
|
|
4383
|
+
}
|
|
4384
|
+
async updateDsrStatus(id, data, options) {
|
|
4385
|
+
return this.put(`/dsr/${id}/status`, data, options);
|
|
4386
|
+
}
|
|
4387
|
+
async createDsrAction(dsrId, data, options) {
|
|
4388
|
+
return this.post(`/dsr/${dsrId}/actions`, data, options);
|
|
4389
|
+
}
|
|
4390
|
+
async listDsrActions(dsrId, options) {
|
|
4391
|
+
return this._get(`/dsr/${dsrId}/actions`, options);
|
|
4392
|
+
}
|
|
4393
|
+
// --- Data Breaches ---
|
|
4394
|
+
async reportBreach(data, options) {
|
|
4395
|
+
return this.post("/breaches", data, options);
|
|
4396
|
+
}
|
|
4397
|
+
async listBreaches(params, requestOptions) {
|
|
4398
|
+
return this._get(`/breaches${this.qs(params)}`, requestOptions);
|
|
4399
|
+
}
|
|
4400
|
+
async getBreach(id, options) {
|
|
4401
|
+
return this._get(`/breaches/${id}`, options);
|
|
4402
|
+
}
|
|
4403
|
+
async updateBreach(id, data, options) {
|
|
4404
|
+
return this.put(`/breaches/${id}`, data, options);
|
|
4405
|
+
}
|
|
4406
|
+
// --- Retention Policies ---
|
|
4407
|
+
async listRetentionPolicies(options) {
|
|
4408
|
+
return this._get("/retention/policies", options);
|
|
4409
|
+
}
|
|
4410
|
+
async createRetentionPolicy(data, options) {
|
|
4411
|
+
return this.post("/retention/policies", data, options);
|
|
4412
|
+
}
|
|
4413
|
+
// --- Processing Activities ---
|
|
4414
|
+
async createProcessingActivity(data, options) {
|
|
4415
|
+
return this.post("/processing-activities", data, options);
|
|
4416
|
+
}
|
|
4417
|
+
async listProcessingActivities(params, requestOptions) {
|
|
4418
|
+
return this._get(`/processing-activities${this.qs(params)}`, requestOptions);
|
|
4419
|
+
}
|
|
4420
|
+
async getProcessingActivity(id, options) {
|
|
4421
|
+
return this._get(`/processing-activities/${id}`, options);
|
|
4422
|
+
}
|
|
4423
|
+
async updateProcessingActivity(id, data, options) {
|
|
4424
|
+
return this.put(`/processing-activities/${id}`, data, options);
|
|
4425
|
+
}
|
|
4426
|
+
};
|
|
4427
|
+
|
|
4428
|
+
// src/services/orchestrator.ts
|
|
4429
|
+
var OrchestratorService = class extends ServiceModule {
|
|
4430
|
+
constructor() {
|
|
4431
|
+
super(...arguments);
|
|
4432
|
+
this.basePath = "/v1/orchestrator";
|
|
4433
|
+
}
|
|
4434
|
+
async createWorkflow(data, options) {
|
|
4435
|
+
return this.post("/workflows", data, options);
|
|
4436
|
+
}
|
|
4437
|
+
async execute(workflowId, input, options) {
|
|
4438
|
+
return this.post(`/workflows/${workflowId}/execute`, input, options);
|
|
4439
|
+
}
|
|
4440
|
+
async getExecution(executionId, options) {
|
|
4441
|
+
return this._get(`/executions/${executionId}`, options);
|
|
4442
|
+
}
|
|
4443
|
+
/** @deprecated Use execute() instead */
|
|
4444
|
+
async executeWorkflow(workflowId, input) {
|
|
4445
|
+
return this.execute(workflowId, input);
|
|
4446
|
+
}
|
|
4447
|
+
};
|
|
4448
|
+
|
|
4449
|
+
// src/services/accounts.ts
|
|
4450
|
+
var AccountsService = class extends ServiceModule {
|
|
4451
|
+
constructor() {
|
|
4452
|
+
super(...arguments);
|
|
4453
|
+
this.basePath = "/v1/accounts";
|
|
4454
|
+
}
|
|
4455
|
+
async createClient(data, options) {
|
|
4456
|
+
return this.post("/clients", data, options);
|
|
4457
|
+
}
|
|
4458
|
+
async getClients(options) {
|
|
4459
|
+
return this._get("/clients", options);
|
|
4460
|
+
}
|
|
4461
|
+
async createApplication(data, options) {
|
|
4462
|
+
return this.post("/applications", data, options);
|
|
4463
|
+
}
|
|
4464
|
+
async getApplications(options) {
|
|
4465
|
+
return this._get("/applications", options);
|
|
4466
|
+
}
|
|
4467
|
+
};
|
|
4468
|
+
|
|
4469
|
+
// src/services/identity.ts
|
|
4470
|
+
var IdentityService = class extends ServiceModule {
|
|
4471
|
+
constructor() {
|
|
4472
|
+
super(...arguments);
|
|
4473
|
+
this.basePath = "/v1/identity";
|
|
4474
|
+
}
|
|
4475
|
+
async createApiKey(data, options) {
|
|
4476
|
+
return this.post("/api-keys", data, options);
|
|
4477
|
+
}
|
|
4478
|
+
async listApiKeys(options) {
|
|
4479
|
+
return this._get("/api-keys", options);
|
|
4480
|
+
}
|
|
4481
|
+
async revokeApiKey(id, options) {
|
|
4482
|
+
return this.del(`/api-keys/${id}`, options);
|
|
4483
|
+
}
|
|
4484
|
+
};
|
|
4485
|
+
|
|
4486
|
+
// src/services/catalog.ts
|
|
4487
|
+
var CatalogService = class extends ServiceModule {
|
|
4488
|
+
constructor() {
|
|
4489
|
+
super(...arguments);
|
|
4490
|
+
this.basePath = "/v1/catalog";
|
|
4491
|
+
}
|
|
4492
|
+
async listServices(options) {
|
|
4493
|
+
return this._get("/services", options);
|
|
4494
|
+
}
|
|
4495
|
+
async getServiceHealth(name, options) {
|
|
4496
|
+
return this._get(`/services/${name}/health`, options);
|
|
4497
|
+
}
|
|
4498
|
+
};
|
|
4499
|
+
|
|
4500
|
+
// src/services/flagcontent.ts
|
|
4501
|
+
var FlagContentService = class extends ServiceModule {
|
|
4502
|
+
constructor() {
|
|
4503
|
+
super(...arguments);
|
|
4504
|
+
this.basePath = "/v1/flagcontent";
|
|
4505
|
+
}
|
|
4506
|
+
async createFlag(data, options) {
|
|
4507
|
+
return this.post("/flags", data, options);
|
|
4508
|
+
}
|
|
4509
|
+
async checkFlag(params, requestOptions) {
|
|
4510
|
+
return this._get(this.withQuery("/flags/check", params), requestOptions);
|
|
4511
|
+
}
|
|
4512
|
+
async getFlag(id, options) {
|
|
4513
|
+
return this._get(`/flags/${id}`, options);
|
|
4514
|
+
}
|
|
4515
|
+
async submitAppeal(data, options) {
|
|
4516
|
+
return this.post("/appeals", data, options);
|
|
4517
|
+
}
|
|
4518
|
+
async getAppeal(id, options) {
|
|
4519
|
+
return this._get(`/appeals/${id}`, options);
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
|
|
4523
|
+
// src/index.ts
|
|
4524
|
+
var ScaleMule = class {
|
|
4525
|
+
constructor(config) {
|
|
4526
|
+
this._client = new ScaleMuleClient(config);
|
|
4527
|
+
this.auth = new AuthService(this._client);
|
|
4528
|
+
this.storage = new StorageService(this._client);
|
|
4529
|
+
this.realtime = new RealtimeService(this._client);
|
|
4530
|
+
this.video = new VideoService(this._client);
|
|
4531
|
+
this.data = new DataService(this._client);
|
|
4532
|
+
this.chat = new ChatService(this._client);
|
|
4533
|
+
this.social = new SocialService(this._client);
|
|
4534
|
+
this.billing = new BillingService(this._client);
|
|
4535
|
+
this.analytics = new AnalyticsService(this._client);
|
|
4536
|
+
this.communication = new CommunicationService(this._client);
|
|
4537
|
+
this.scheduler = new SchedulerService(this._client);
|
|
4538
|
+
this.permissions = new PermissionsService(this._client);
|
|
4539
|
+
this.teams = new TeamsService(this._client);
|
|
4540
|
+
this.accounts = new AccountsService(this._client);
|
|
4541
|
+
this.identity = new IdentityService(this._client);
|
|
4542
|
+
this.catalog = new CatalogService(this._client);
|
|
4543
|
+
this.cache = new CacheService(this._client);
|
|
4544
|
+
this.queue = new QueueService(this._client);
|
|
4545
|
+
this.search = new SearchService(this._client);
|
|
4546
|
+
this.logger = new LoggerService(this._client);
|
|
4547
|
+
this.webhooks = new WebhooksService(this._client);
|
|
4548
|
+
this.leaderboard = new LeaderboardService(this._client);
|
|
4549
|
+
this.listings = new ListingsService(this._client);
|
|
4550
|
+
this.events = new EventsService(this._client);
|
|
4551
|
+
this.graph = new GraphService(this._client);
|
|
4552
|
+
this.functions = new FunctionsService(this._client);
|
|
4553
|
+
this.photo = new PhotoService(this._client);
|
|
4554
|
+
this.flagContent = new FlagContentService(this._client);
|
|
4555
|
+
this.compliance = new ComplianceService(this._client);
|
|
4556
|
+
this.orchestrator = new OrchestratorService(this._client);
|
|
4557
|
+
}
|
|
4558
|
+
/**
|
|
4559
|
+
* Initialize the client — loads persisted session from storage.
|
|
4560
|
+
* Call this once after construction, before making authenticated requests.
|
|
4561
|
+
*/
|
|
4562
|
+
async initialize() {
|
|
4563
|
+
return this._client.initialize();
|
|
4564
|
+
}
|
|
4565
|
+
/**
|
|
4566
|
+
* Set authentication session (token + userId).
|
|
4567
|
+
* Persisted to storage for cross-session continuity.
|
|
4568
|
+
*/
|
|
4569
|
+
async setSession(token, userId) {
|
|
4570
|
+
return this._client.setSession(token, userId);
|
|
4571
|
+
}
|
|
4572
|
+
/** Clear the current session and remove from storage. */
|
|
4573
|
+
async clearSession() {
|
|
4574
|
+
return this._client.clearSession();
|
|
4575
|
+
}
|
|
4576
|
+
/** Set access token (in-memory only, not persisted). */
|
|
4577
|
+
setAccessToken(token) {
|
|
4578
|
+
this._client.setAccessToken(token);
|
|
4579
|
+
}
|
|
4580
|
+
/** Clear access token. */
|
|
4581
|
+
clearAccessToken() {
|
|
4582
|
+
this._client.clearAccessToken();
|
|
4583
|
+
}
|
|
4584
|
+
/** Current session token, or null. */
|
|
4585
|
+
getSessionToken() {
|
|
4586
|
+
return this._client.getSessionToken();
|
|
4587
|
+
}
|
|
4588
|
+
/** Current user ID, or null. */
|
|
4589
|
+
getUserId() {
|
|
4590
|
+
return this._client.getUserId();
|
|
4591
|
+
}
|
|
4592
|
+
/** Whether a session token is set. */
|
|
4593
|
+
isAuthenticated() {
|
|
4594
|
+
return this._client.isAuthenticated();
|
|
4595
|
+
}
|
|
4596
|
+
/** The base URL being used for API requests. */
|
|
4597
|
+
getBaseUrl() {
|
|
4598
|
+
return this._client.getBaseUrl();
|
|
4599
|
+
}
|
|
4600
|
+
/** Access the underlying ScaleMuleClient for advanced usage. */
|
|
4601
|
+
getClient() {
|
|
4602
|
+
return this._client;
|
|
4603
|
+
}
|
|
4604
|
+
};
|
|
4605
|
+
var index_default = ScaleMule;
|
|
4606
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4607
|
+
0 && (module.exports = {
|
|
4608
|
+
AccountsService,
|
|
4609
|
+
AnalyticsService,
|
|
4610
|
+
AuthService,
|
|
4611
|
+
BillingService,
|
|
4612
|
+
CacheService,
|
|
4613
|
+
CatalogService,
|
|
4614
|
+
ChatService,
|
|
4615
|
+
CommunicationService,
|
|
4616
|
+
ComplianceService,
|
|
4617
|
+
DataService,
|
|
4618
|
+
ErrorCodes,
|
|
4619
|
+
EventsService,
|
|
4620
|
+
FlagContentService,
|
|
4621
|
+
FunctionsService,
|
|
4622
|
+
GraphService,
|
|
4623
|
+
IdentityService,
|
|
4624
|
+
LeaderboardService,
|
|
4625
|
+
ListingsService,
|
|
4626
|
+
LoggerService,
|
|
4627
|
+
OrchestratorService,
|
|
4628
|
+
PHONE_COUNTRIES,
|
|
4629
|
+
PHOTO_BREAKPOINTS,
|
|
4630
|
+
PermissionsService,
|
|
4631
|
+
PhotoService,
|
|
4632
|
+
QueueService,
|
|
4633
|
+
RealtimeService,
|
|
4634
|
+
ScaleMule,
|
|
4635
|
+
ScaleMuleClient,
|
|
4636
|
+
SchedulerService,
|
|
4637
|
+
SearchService,
|
|
4638
|
+
ServiceModule,
|
|
4639
|
+
SocialService,
|
|
4640
|
+
StorageService,
|
|
4641
|
+
TeamsService,
|
|
4642
|
+
UploadResumeStore,
|
|
4643
|
+
UploadTelemetry,
|
|
4644
|
+
VideoService,
|
|
4645
|
+
WebhooksService,
|
|
4646
|
+
buildClientContextHeaders,
|
|
4647
|
+
calculateTotalParts,
|
|
4648
|
+
canPerform,
|
|
4649
|
+
composePhoneNumber,
|
|
4650
|
+
countryFlag,
|
|
4651
|
+
createUploadPlan,
|
|
4652
|
+
detectCountryFromE164,
|
|
4653
|
+
detectNetworkClass,
|
|
4654
|
+
extractClientContext,
|
|
4655
|
+
findPhoneCountryByCode,
|
|
4656
|
+
findPhoneCountryByDialCode,
|
|
4657
|
+
generateUploadSessionId,
|
|
4658
|
+
getMeasuredBandwidthMbps,
|
|
4659
|
+
getPartRange,
|
|
4660
|
+
hasMinRoleLevel,
|
|
4661
|
+
isValidE164Phone,
|
|
4662
|
+
normalizeAndValidatePhone,
|
|
4663
|
+
normalizePhoneNumber,
|
|
4664
|
+
resolveStrategy,
|
|
4665
|
+
validateIP
|
|
4666
|
+
});
|