flit-baas 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,747 @@
1
+ // src/errors.ts
2
+ var FlitError = class extends Error {
3
+ code;
4
+ constructor(message, code = "FLIT_ERROR") {
5
+ super(message);
6
+ this.name = "FlitError";
7
+ this.code = code;
8
+ Object.setPrototypeOf(this, new.target.prototype);
9
+ }
10
+ };
11
+ var FlitAPIError = class extends FlitError {
12
+ status;
13
+ details;
14
+ constructor(message, status, code = "API_ERROR", details) {
15
+ super(message, code);
16
+ this.name = "FlitAPIError";
17
+ this.status = status;
18
+ this.details = details;
19
+ }
20
+ };
21
+ var FlitPaymentError = class extends FlitError {
22
+ operator;
23
+ transactionId;
24
+ constructor(message, operator, transactionId) {
25
+ super(message, "PAYMENT_FAILED");
26
+ this.name = "FlitPaymentError";
27
+ this.operator = operator;
28
+ this.transactionId = transactionId;
29
+ }
30
+ };
31
+ var FlitAuthError = class extends FlitError {
32
+ constructor(message, code = "AUTH_ERROR") {
33
+ super(message, code);
34
+ this.name = "FlitAuthError";
35
+ }
36
+ };
37
+
38
+ // src/collection.ts
39
+ var Collection = class {
40
+ client;
41
+ name;
42
+ constructor(client, name) {
43
+ this.client = client;
44
+ this.name = name.trim();
45
+ }
46
+ /**
47
+ * Helper to normalize a record returned from the PostgreSQL JSONB document store.
48
+ * Flattens { id, createdAt, updatedAt, data: { ... } } into a single intuitive object.
49
+ */
50
+ normalizeRecord(raw) {
51
+ if (!raw || typeof raw !== "object") return raw;
52
+ const { id, createdAt, updatedAt, data, ...rest } = raw;
53
+ const documentData = typeof data === "object" && data !== null ? data : {};
54
+ return {
55
+ id,
56
+ createdAt,
57
+ updatedAt,
58
+ ...documentData,
59
+ ...rest
60
+ };
61
+ }
62
+ /**
63
+ * Find documents matching optional filters, with pagination and sorting
64
+ * @param filter Key-value pairs to match against document properties
65
+ * @param options Query options (limit, offset, orderBy, order)
66
+ */
67
+ async find(filter, options = {}) {
68
+ const queryParams = new URLSearchParams();
69
+ if (options.limit !== void 0) {
70
+ queryParams.set("limit", String(options.limit));
71
+ }
72
+ if (options.offset !== void 0) {
73
+ queryParams.set("offset", String(options.offset));
74
+ }
75
+ if (options.orderBy) {
76
+ queryParams.set("orderBy", String(options.orderBy));
77
+ }
78
+ if (options.order) {
79
+ queryParams.set("order", options.order);
80
+ }
81
+ let res;
82
+ if (filter && Object.keys(filter).length > 0) {
83
+ res = await this.client.request(
84
+ `/api/baas/collections/${this.name}/query`,
85
+ {
86
+ method: "POST",
87
+ body: JSON.stringify({
88
+ appId: this.client.appId,
89
+ filter,
90
+ limit: options.limit,
91
+ offset: options.offset,
92
+ orderBy: options.orderBy,
93
+ order: options.order
94
+ })
95
+ }
96
+ );
97
+ } else {
98
+ const qs = queryParams.toString() ? `?${queryParams.toString()}` : "";
99
+ res = await this.client.request(
100
+ `/api/apps/${this.client.appId}/baas/${this.name}${qs}`,
101
+ { method: "GET" }
102
+ );
103
+ }
104
+ const records = res.records || [];
105
+ return records.map((r) => this.normalizeRecord(r));
106
+ }
107
+ /**
108
+ * Find a single document by its unique ID
109
+ * @param id Document UUID
110
+ */
111
+ async findById(id) {
112
+ if (!id) return null;
113
+ try {
114
+ const res = await this.client.request(
115
+ `/api/apps/${this.client.appId}/baas/${this.name}?id=${encodeURIComponent(id)}`,
116
+ { method: "GET" }
117
+ );
118
+ if (!res.record) return null;
119
+ return this.normalizeRecord(res.record);
120
+ } catch (err) {
121
+ if (err?.status === 404) return null;
122
+ throw err;
123
+ }
124
+ }
125
+ /**
126
+ * Insert a new document into the collection
127
+ * @param data The document payload (without id, createdAt, updatedAt)
128
+ */
129
+ async insert(data) {
130
+ const res = await this.client.request(
131
+ `/api/apps/${this.client.appId}/baas/${this.name}`,
132
+ {
133
+ method: "POST",
134
+ body: JSON.stringify({ data })
135
+ }
136
+ );
137
+ if (!res.record) {
138
+ throw new Error(`Failed to insert document into collection '${this.name}'`);
139
+ }
140
+ return this.normalizeRecord(res.record);
141
+ }
142
+ /**
143
+ * Insert multiple documents sequentially
144
+ * @param items Array of document payloads
145
+ */
146
+ async insertMany(items) {
147
+ const results = [];
148
+ for (const item of items) {
149
+ const inserted = await this.insert(item);
150
+ results.push(inserted);
151
+ }
152
+ return results;
153
+ }
154
+ /**
155
+ * Update an existing document by ID
156
+ * @param id Document UUID
157
+ * @param data Partial update payload
158
+ */
159
+ async update(id, data) {
160
+ if (!id) {
161
+ throw new Error("Document 'id' is required for update operation");
162
+ }
163
+ const res = await this.client.request(
164
+ `/api/apps/${this.client.appId}/baas/${this.name}`,
165
+ {
166
+ method: "PATCH",
167
+ body: JSON.stringify({ id, data })
168
+ }
169
+ );
170
+ if (!res.record) {
171
+ throw new Error(`Failed to update document with id '${id}' in collection '${this.name}'`);
172
+ }
173
+ return this.normalizeRecord(res.record);
174
+ }
175
+ /**
176
+ * Delete a document by ID
177
+ * @param id Document UUID
178
+ */
179
+ async delete(id) {
180
+ if (!id) {
181
+ throw new Error("Document 'id' is required for delete operation");
182
+ }
183
+ const res = await this.client.request(
184
+ `/api/apps/${this.client.appId}/baas/${this.name}?id=${encodeURIComponent(id)}`,
185
+ { method: "DELETE" }
186
+ );
187
+ return !!res.success;
188
+ }
189
+ /**
190
+ * Count documents matching an optional filter
191
+ */
192
+ async count(filter) {
193
+ const results = await this.find(filter, { limit: 1 });
194
+ return results.length;
195
+ }
196
+ };
197
+
198
+ // src/payments.ts
199
+ var FlitPayments = class {
200
+ client;
201
+ constructor(client) {
202
+ this.client = client;
203
+ }
204
+ /**
205
+ * Initiate a Mobile Money STK Push deposit (Orange Money, MTN MoMo, Wave, etc.)
206
+ * @param order Payment order specifications
207
+ */
208
+ async initiate(order) {
209
+ if (!order.phone || !order.amount || !order.operator) {
210
+ throw new FlitPaymentError(
211
+ "Missing required payment parameters: phone, amount, and operator are mandatory.",
212
+ order.operator
213
+ );
214
+ }
215
+ if (order.amount <= 0) {
216
+ throw new FlitPaymentError("Payment amount must be greater than zero.", order.operator);
217
+ }
218
+ try {
219
+ const payload = {
220
+ appId: this.client.appId,
221
+ operator: order.operator.toUpperCase(),
222
+ phone: order.phone.replace(/[\s-]/g, ""),
223
+ amount: order.amount,
224
+ currency: order.currency || "XAF",
225
+ title: order.title || `Paiement ${order.operator}`,
226
+ customerName: order.customerName,
227
+ customerEmail: order.customerEmail,
228
+ metadata: order.metadata || {}
229
+ };
230
+ const res = await this.client.request(
231
+ "/api/payments/deposit",
232
+ {
233
+ method: "POST",
234
+ body: JSON.stringify(payload)
235
+ }
236
+ );
237
+ return res;
238
+ } catch (err) {
239
+ throw new FlitPaymentError(
240
+ err.message || "Failed to initiate Mobile Money payment",
241
+ order.operator
242
+ );
243
+ }
244
+ }
245
+ /**
246
+ * Retrieve current real-time status of a payment transaction
247
+ * @param transactionId Transaction or deposit reference
248
+ */
249
+ async getStatus(transactionId) {
250
+ if (!transactionId) {
251
+ throw new FlitPaymentError("transactionId is required to query payment status");
252
+ }
253
+ return this.client.request(
254
+ `/api/payments/status?transactionId=${encodeURIComponent(transactionId)}&appId=${encodeURIComponent(
255
+ this.client.appId
256
+ )}`,
257
+ { method: "GET" }
258
+ );
259
+ }
260
+ /**
261
+ * Poll for transaction completion until a final status (SUCCESS, FAILED, EXPIRED) is reached
262
+ * @param transactionId Transaction reference
263
+ * @param options Polling configuration (timeoutMs default 60000ms, intervalMs default 3000ms)
264
+ */
265
+ async waitForStatus(transactionId, options = {}) {
266
+ const timeoutMs = options.timeoutMs || 6e4;
267
+ const intervalMs = options.intervalMs || 3e3;
268
+ const startTime = Date.now();
269
+ while (Date.now() - startTime < timeoutMs) {
270
+ const status = await this.getStatus(transactionId);
271
+ if (status.status === "SUCCESS" || status.status === "FAILED" || status.status === "EXPIRED") {
272
+ return status;
273
+ }
274
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
275
+ }
276
+ throw new FlitPaymentError(
277
+ `Payment status polling timed out after ${timeoutMs}ms for transaction ${transactionId}`
278
+ );
279
+ }
280
+ };
281
+
282
+ // src/cookies.ts
283
+ var DEFAULT_COOKIE_NAME = "flit_session";
284
+ var DEFAULT_MAX_AGE = 30 * 24 * 60 * 60;
285
+ async function deriveKey(secret) {
286
+ const enc = new TextEncoder();
287
+ const secretBytes = enc.encode(secret);
288
+ const hash = await crypto.subtle.digest("SHA-256", secretBytes);
289
+ return crypto.subtle.importKey(
290
+ "raw",
291
+ hash,
292
+ { name: "AES-GCM", length: 256 },
293
+ false,
294
+ ["encrypt", "decrypt"]
295
+ );
296
+ }
297
+ async function encryptSession(payload, secretKey) {
298
+ const key = await deriveKey(secretKey);
299
+ const iv = crypto.getRandomValues(new Uint8Array(12));
300
+ const encodedPayload = new TextEncoder().encode(JSON.stringify(payload));
301
+ const cipherBuffer = await crypto.subtle.encrypt(
302
+ { name: "AES-GCM", iv },
303
+ key,
304
+ encodedPayload
305
+ );
306
+ const combined = new Uint8Array(iv.length + cipherBuffer.byteLength);
307
+ combined.set(iv, 0);
308
+ combined.set(new Uint8Array(cipherBuffer), iv.length);
309
+ let binary = "";
310
+ for (let i = 0; i < combined.length; i++) {
311
+ binary += String.fromCharCode(combined[i]);
312
+ }
313
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
314
+ }
315
+ async function decryptSession(encryptedText, secretKey) {
316
+ try {
317
+ const key = await deriveKey(secretKey);
318
+ let base64 = encryptedText.replace(/-/g, "+").replace(/_/g, "/");
319
+ while (base64.length % 4) {
320
+ base64 += "=";
321
+ }
322
+ const binary = atob(base64);
323
+ const combined = new Uint8Array(binary.length);
324
+ for (let i = 0; i < binary.length; i++) {
325
+ combined[i] = binary.charCodeAt(i);
326
+ }
327
+ if (combined.length < 12 + 16) {
328
+ return null;
329
+ }
330
+ const iv = combined.slice(0, 12);
331
+ const ciphertext = combined.slice(12);
332
+ const decryptedBuffer = await crypto.subtle.decrypt(
333
+ { name: "AES-GCM", iv },
334
+ key,
335
+ ciphertext
336
+ );
337
+ const decoded = new TextDecoder().decode(decryptedBuffer);
338
+ const session = JSON.parse(decoded);
339
+ if (session.expiresAt && Date.now() > session.expiresAt) {
340
+ return null;
341
+ }
342
+ return session;
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
347
+ function buildSetCookieHeader(value, options = {}) {
348
+ const name = options.name || DEFAULT_COOKIE_NAME;
349
+ const path = options.path || "/";
350
+ const maxAge = options.maxAge !== void 0 ? options.maxAge : DEFAULT_MAX_AGE;
351
+ const secure = options.secure !== false;
352
+ const httpOnly = options.httpOnly !== false;
353
+ const sameSite = options.sameSite || "Lax";
354
+ const parts = [`${name}=${encodeURIComponent(value)}`, `Path=${path}`];
355
+ if (maxAge >= 0) {
356
+ parts.push(`Max-Age=${maxAge}`);
357
+ const expires = new Date(Date.now() + maxAge * 1e3).toUTCString();
358
+ parts.push(`Expires=${expires}`);
359
+ }
360
+ if (options.domain) {
361
+ parts.push(`Domain=${options.domain}`);
362
+ }
363
+ {
364
+ parts.push(`SameSite=${sameSite}`);
365
+ }
366
+ if (secure) {
367
+ parts.push("Secure");
368
+ }
369
+ if (httpOnly) {
370
+ parts.push("HttpOnly");
371
+ }
372
+ return parts.join("; ");
373
+ }
374
+ function buildClearCookieHeader(options = {}) {
375
+ return buildSetCookieHeader("", {
376
+ ...options,
377
+ maxAge: 0
378
+ });
379
+ }
380
+ function parseCookieFromHeader(cookieHeader, cookieName = DEFAULT_COOKIE_NAME) {
381
+ if (!cookieHeader) return null;
382
+ const cookies = cookieHeader.split(";");
383
+ for (const c of cookies) {
384
+ const [key, ...valParts] = c.trim().split("=");
385
+ if (key === cookieName) {
386
+ return decodeURIComponent(valParts.join("="));
387
+ }
388
+ }
389
+ return null;
390
+ }
391
+
392
+ // src/auth.ts
393
+ var FlitAuth = class {
394
+ client;
395
+ storage;
396
+ currentSession = null;
397
+ storageKey;
398
+ constructor(client, storage) {
399
+ this.client = client;
400
+ this.storage = storage;
401
+ this.storageKey = `flit_auth_session_${this.client.appId}`;
402
+ this.initSession();
403
+ }
404
+ /**
405
+ * Initialize session securely.
406
+ * If a custom storage adapter was provided (e.g. secure encrypted cookie store),
407
+ * load the session from it. Otherwise keep in-memory only (zero XSS localStorage exposure).
408
+ */
409
+ async initSession() {
410
+ if (this.storage) {
411
+ try {
412
+ const stored = await this.storage.getItem(this.storageKey);
413
+ if (stored) {
414
+ this.currentSession = JSON.parse(stored);
415
+ }
416
+ } catch {
417
+ }
418
+ }
419
+ }
420
+ async persistSession(session) {
421
+ this.currentSession = session;
422
+ if (this.storage) {
423
+ try {
424
+ if (session) {
425
+ await this.storage.setItem(this.storageKey, JSON.stringify(session));
426
+ } else {
427
+ await this.storage.removeItem(this.storageKey);
428
+ }
429
+ } catch {
430
+ }
431
+ }
432
+ }
433
+ /**
434
+ * Register a new user
435
+ */
436
+ async signUp(credentials) {
437
+ if (!credentials.email) {
438
+ throw new FlitAuthError("Email is required for registration");
439
+ }
440
+ try {
441
+ const res = await this.client.request("/api/auth/register", {
442
+ method: "POST",
443
+ body: JSON.stringify({
444
+ appId: this.client.appId,
445
+ ...credentials
446
+ })
447
+ });
448
+ if (res.session) {
449
+ await this.persistSession(res.session);
450
+ }
451
+ return res;
452
+ } catch (err) {
453
+ throw new FlitAuthError(err.message || "Registration failed");
454
+ }
455
+ }
456
+ /**
457
+ * Sign in an existing user with email and password
458
+ */
459
+ async signInWithPassword(credentials) {
460
+ if (!credentials.email || !credentials.password) {
461
+ throw new FlitAuthError("Email and password are required");
462
+ }
463
+ try {
464
+ const res = await this.client.request("/api/auth/login", {
465
+ method: "POST",
466
+ body: JSON.stringify({
467
+ appId: this.client.appId,
468
+ ...credentials
469
+ })
470
+ });
471
+ if (res.session) {
472
+ await this.persistSession(res.session);
473
+ }
474
+ return res;
475
+ } catch (err) {
476
+ throw new FlitAuthError(err.message || "Authentication failed");
477
+ }
478
+ }
479
+ /**
480
+ * Sign out the currently authenticated user
481
+ */
482
+ async signOut() {
483
+ await this.persistSession(null);
484
+ }
485
+ /**
486
+ * Get current session if available (in-memory)
487
+ */
488
+ getSession() {
489
+ return this.currentSession;
490
+ }
491
+ /**
492
+ * Get current user if authenticated
493
+ */
494
+ getUser() {
495
+ return this.currentSession?.user || null;
496
+ }
497
+ /**
498
+ * Server-Side Helper: Generate a cryptographically encrypted (AES-256-GCM) HttpOnly Set-Cookie header.
499
+ * Immunizes authentication tokens against XSS attacks.
500
+ *
501
+ * @param session The authenticated session to encrypt
502
+ * @param secretKey Encryption key (defaults to client's secret apiKey or appId)
503
+ * @param options Custom cookie attributes (Path, Domain, Max-Age, etc.)
504
+ */
505
+ async createSessionCookie(session, secretKey, options) {
506
+ const key = secretKey || this.client.apiKey || this.client.appId;
507
+ const payload = {
508
+ token: session.token,
509
+ userId: session.user.id,
510
+ appId: this.client.appId,
511
+ expiresAt: session.expiresAt,
512
+ email: session.user.email
513
+ };
514
+ const encrypted = await encryptSession(payload, key);
515
+ return buildSetCookieHeader(encrypted, {
516
+ httpOnly: true,
517
+ secure: true,
518
+ sameSite: "Lax",
519
+ path: "/",
520
+ ...options
521
+ });
522
+ }
523
+ /**
524
+ * Server-Side Helper: Decrypt and verify an incoming HttpOnly cookie header using AES-256-GCM.
525
+ *
526
+ * @param cookieHeader The incoming Cookie request header string
527
+ * @param secretKey Encryption key (must match the key used in createSessionCookie)
528
+ */
529
+ async verifySessionCookie(cookieHeader, secretKey, cookieName) {
530
+ const raw = parseCookieFromHeader(cookieHeader, cookieName);
531
+ if (!raw) return null;
532
+ const key = secretKey || this.client.apiKey || this.client.appId;
533
+ return decryptSession(raw, key);
534
+ }
535
+ /**
536
+ * Server-Side Helper: Generate a Set-Cookie header that expires and destroys the session cookie.
537
+ */
538
+ clearSessionCookie(options) {
539
+ return buildClearCookieHeader({
540
+ httpOnly: true,
541
+ secure: true,
542
+ sameSite: "Lax",
543
+ path: "/",
544
+ ...options
545
+ });
546
+ }
547
+ };
548
+
549
+ // src/storage.ts
550
+ var FlitStorage = class {
551
+ client;
552
+ constructor(client) {
553
+ this.client = client;
554
+ }
555
+ /**
556
+ * Upload a file (Blob, File, or Buffer) to Flit Cloud Storage
557
+ */
558
+ async upload(file, fileName, options = {}) {
559
+ if (!file || !fileName) {
560
+ throw new FlitError("File and fileName are required for storage upload", "INVALID_PARAMS");
561
+ }
562
+ const formData = new FormData();
563
+ formData.append("file", file, fileName);
564
+ formData.append("appId", this.client.appId);
565
+ if (options.contentType) {
566
+ formData.append("contentType", options.contentType);
567
+ }
568
+ return this.client.request("/api/baas/storage/upload", {
569
+ method: "POST",
570
+ body: formData,
571
+ headers: {
572
+ // Leave Content-Type empty so fetch sets multipart/form-data with boundary
573
+ }
574
+ });
575
+ }
576
+ /**
577
+ * Generate a public URL for a stored asset
578
+ */
579
+ getPublicUrl(key) {
580
+ const cleanKey = key.replace(/^\/+/, "");
581
+ return `${this.client.endpoint}/api/baas/storage/${encodeURIComponent(this.client.appId)}/${cleanKey}`;
582
+ }
583
+ };
584
+
585
+ // src/client.ts
586
+ var FlitClient = class {
587
+ appId;
588
+ apiKey;
589
+ endpoint;
590
+ timeout;
591
+ customHeaders;
592
+ customFetch;
593
+ debug;
594
+ credentials;
595
+ _storageAdapter;
596
+ // Sub-services
597
+ _payments;
598
+ _auth;
599
+ _storage;
600
+ constructor(options) {
601
+ if (!options || !options.appId) {
602
+ throw new FlitError("appId is required to initialize FlitClient", "MISSING_CONFIG");
603
+ }
604
+ this.appId = options.appId.trim();
605
+ this.apiKey = options.apiKey?.trim();
606
+ this.endpoint = (options.endpoint || "https://api.flit.site").replace(/\/+$/, "");
607
+ this.timeout = options.timeout || 15e3;
608
+ this.customHeaders = options.headers || {};
609
+ this.customFetch = options.fetch;
610
+ this.debug = !!options.debug;
611
+ this.credentials = options.credentials || "include";
612
+ this._storageAdapter = options.storage;
613
+ if (typeof window !== "undefined" && this.apiKey && this.apiKey.startsWith("flit_sk_")) {
614
+ throw new FlitError(
615
+ "CRITICAL SECURITY VIOLATION: Flit Secret Service Key (flit_sk_...) must NEVER be exposed in client-side browser code! Use your Public Anon Key (flit_pk_...) in frontend applications, and keep Secret Keys strictly on your server.",
616
+ "SECRET_KEY_EXPOSURE"
617
+ );
618
+ }
619
+ if (this.debug) {
620
+ console.log(`[Flit SDK] Initialized client for app: ${this.appId} on endpoint: ${this.endpoint}`);
621
+ }
622
+ }
623
+ /**
624
+ * Access a database collection
625
+ * @param name Name of the collection (e.g. 'products', 'users', 'orders')
626
+ */
627
+ collection(name) {
628
+ return new Collection(this, name);
629
+ }
630
+ /**
631
+ * Access Mobile Money payments service (Orange Money, MTN MoMo, Wave)
632
+ */
633
+ get payments() {
634
+ if (!this._payments) {
635
+ this._payments = new FlitPayments(this);
636
+ }
637
+ return this._payments;
638
+ }
639
+ /**
640
+ * Access Authentication service
641
+ */
642
+ get auth() {
643
+ if (!this._auth) {
644
+ this._auth = new FlitAuth(this, this._storageAdapter);
645
+ }
646
+ return this._auth;
647
+ }
648
+ /**
649
+ * Access Storage & File upload service
650
+ */
651
+ get storage() {
652
+ if (!this._storage) {
653
+ this._storage = new FlitStorage(this);
654
+ }
655
+ return this._storage;
656
+ }
657
+ /**
658
+ * Internal HTTP request transport with timeout and error handling
659
+ */
660
+ async request(path, init = {}) {
661
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
662
+ const url = `${this.endpoint}${cleanPath}`;
663
+ const headers = {
664
+ "Accept": "application/json",
665
+ "X-Flit-App-Id": this.appId,
666
+ ...this.customHeaders,
667
+ ...init.headers || {}
668
+ };
669
+ if (this.apiKey) {
670
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
671
+ headers["X-Flit-Api-Key"] = this.apiKey;
672
+ }
673
+ if (init.body && typeof init.body === "string" && !headers["Content-Type"]) {
674
+ headers["Content-Type"] = "application/json";
675
+ }
676
+ const controller = new AbortController();
677
+ const timer = setTimeout(() => controller.abort(), this.timeout);
678
+ const fetchFn = this.customFetch || globalThis.fetch;
679
+ if (typeof fetchFn !== "function") {
680
+ clearTimeout(timer);
681
+ throw new FlitError(
682
+ "Global fetch is not available. Provide a custom fetch implementation in FlitClientOptions.",
683
+ "FETCH_UNAVAILABLE"
684
+ );
685
+ }
686
+ try {
687
+ if (this.debug) {
688
+ console.log(`[Flit SDK] ${init.method || "GET"} ${url}`);
689
+ }
690
+ const response = await fetchFn(url, {
691
+ credentials: this.credentials,
692
+ ...init,
693
+ headers,
694
+ signal: controller.signal
695
+ });
696
+ clearTimeout(timer);
697
+ let data;
698
+ const contentType = response.headers.get("content-type") || "";
699
+ if (contentType.includes("application/json")) {
700
+ data = await response.json();
701
+ } else {
702
+ const text = await response.text();
703
+ try {
704
+ data = JSON.parse(text);
705
+ } catch {
706
+ data = { text };
707
+ }
708
+ }
709
+ if (!response.ok) {
710
+ const errorMessage = data?.error || data?.message || response.statusText || "Unknown Server Error";
711
+ throw new FlitAPIError(
712
+ `[Flit API Error ${response.status}]: ${errorMessage}`,
713
+ response.status,
714
+ data?.code || "HTTP_ERROR",
715
+ data
716
+ );
717
+ }
718
+ return data;
719
+ } catch (err) {
720
+ clearTimeout(timer);
721
+ if (err instanceof FlitAPIError) {
722
+ throw err;
723
+ }
724
+ if (err.name === "AbortError") {
725
+ throw new FlitAPIError(
726
+ `Request to ${cleanPath} timed out after ${this.timeout}ms`,
727
+ 408,
728
+ "TIMEOUT"
729
+ );
730
+ }
731
+ throw new FlitError(
732
+ err.message || "Failed to execute network request to Flit API",
733
+ "NETWORK_ERROR"
734
+ );
735
+ }
736
+ }
737
+ };
738
+
739
+ // src/index.ts
740
+ function createClient(options) {
741
+ return new FlitClient(options);
742
+ }
743
+ var index_default = createClient;
744
+
745
+ export { Collection, FlitAPIError, FlitAuth, FlitAuthError, FlitClient, FlitError, FlitPaymentError, FlitPayments, FlitStorage, buildClearCookieHeader, buildSetCookieHeader, createClient, decryptSession, index_default as default, encryptSession, parseCookieFromHeader };
746
+ //# sourceMappingURL=index.mjs.map
747
+ //# sourceMappingURL=index.mjs.map