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