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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/index.cjs +766 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +481 -0
- package/dist/index.d.ts +481 -0
- package/dist/index.mjs +747 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +50 -0
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
|