dexie-cloud-sdk 0.1.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.js ADDED
@@ -0,0 +1,1065 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AuthManager: () => AuthManager,
34
+ BlobManager: () => BlobManager,
35
+ DataManager: () => DataManager,
36
+ DatabaseManager: () => DatabaseManager,
37
+ DatabaseSession: () => DatabaseSession,
38
+ DexieCloudAuthError: () => DexieCloudAuthError,
39
+ DexieCloudClient: () => DexieCloudClient,
40
+ DexieCloudError: () => DexieCloudError,
41
+ DexieCloudNetworkError: () => DexieCloudNetworkError,
42
+ FetchAdapter: () => FetchAdapter,
43
+ HealthManager: () => HealthManager,
44
+ NodeAdapter: () => NodeAdapter,
45
+ TSON: () => TSON,
46
+ createAdapter: () => createAdapter,
47
+ default: () => DexieCloudClient,
48
+ parse: () => parse,
49
+ stringify: () => stringify
50
+ });
51
+ module.exports = __toCommonJS(index_exports);
52
+
53
+ // src/types.ts
54
+ var DexieCloudError = class extends Error {
55
+ constructor(message, status, response) {
56
+ super(message);
57
+ this.status = status;
58
+ this.response = response;
59
+ this.name = "DexieCloudError";
60
+ }
61
+ };
62
+ var DexieCloudAuthError = class extends DexieCloudError {
63
+ constructor(message, status) {
64
+ super(message, status);
65
+ this.name = "DexieCloudAuthError";
66
+ }
67
+ };
68
+ var DexieCloudNetworkError = class extends DexieCloudError {
69
+ constructor(message) {
70
+ super(message);
71
+ this.name = "DexieCloudNetworkError";
72
+ }
73
+ };
74
+
75
+ // src/adapters.ts
76
+ var FetchAdapter = class {
77
+ constructor(config, fetchImpl = globalThis.fetch) {
78
+ this.config = config;
79
+ this.fetchImpl = fetchImpl;
80
+ if (!this.fetchImpl) {
81
+ throw new Error("Fetch is not available. Please provide a fetch implementation.");
82
+ }
83
+ }
84
+ async fetch(url, options = {}) {
85
+ const { timeout = 3e4, debug } = this.config;
86
+ const controller = new AbortController();
87
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
88
+ const fetchOptions = {
89
+ ...options,
90
+ signal: controller.signal,
91
+ headers: {
92
+ "Content-Type": "application/json",
93
+ "User-Agent": "dexie-cloud-sdk",
94
+ ...options.headers
95
+ }
96
+ };
97
+ if (debug) {
98
+ console.log(`[DexieCloud] ${options.method || "GET"} ${url}`, fetchOptions);
99
+ }
100
+ try {
101
+ const response = await this.fetchImpl(url, fetchOptions);
102
+ if (debug) {
103
+ console.log(`[DexieCloud] Response ${response.status}`, response);
104
+ }
105
+ return response;
106
+ } catch (error) {
107
+ if (error instanceof Error && error.name === "AbortError") {
108
+ throw new Error(`Request timeout after ${timeout}ms`);
109
+ }
110
+ throw error;
111
+ } finally {
112
+ clearTimeout(timeoutId);
113
+ }
114
+ }
115
+ };
116
+ var NodeAdapter = class {
117
+ constructor(config) {
118
+ this.config = config;
119
+ }
120
+ async fetch(url, options = {}) {
121
+ const { default: fetch } = await import("node-fetch");
122
+ const adapter = new FetchAdapter(this.config, fetch);
123
+ return adapter.fetch(url, options);
124
+ }
125
+ };
126
+ function createAdapter(config) {
127
+ if (config.fetch) {
128
+ return new FetchAdapter(config, config.fetch);
129
+ }
130
+ if (typeof globalThis.fetch === "function") {
131
+ return new FetchAdapter(config);
132
+ }
133
+ if (typeof process !== "undefined" && process.versions?.node) {
134
+ return new NodeAdapter(config);
135
+ }
136
+ throw new Error("No suitable HTTP adapter found. Please provide a fetch implementation in config.");
137
+ }
138
+
139
+ // src/tson.ts
140
+ var import_dexie_cloud_common = require("dexie-cloud-common");
141
+ var import_dexie_cloud_common2 = require("dexie-cloud-common");
142
+ var sdkTypeDefs = {
143
+ // URL type support
144
+ URL: {
145
+ test: (val) => val instanceof URL,
146
+ replace: (url) => ({
147
+ $t: "URL",
148
+ href: url.href
149
+ }),
150
+ revive: ({ href }) => new URL(href)
151
+ }
152
+ };
153
+ var TSON = (0, import_dexie_cloud_common.TypesonSimplified)(
154
+ import_dexie_cloud_common.builtInTypeDefs,
155
+ import_dexie_cloud_common.fileTypeDef,
156
+ sdkTypeDefs
157
+ );
158
+ function stringify(value, space) {
159
+ return TSON.stringify(value, void 0, space);
160
+ }
161
+ function parse(text) {
162
+ return TSON.parse(text);
163
+ }
164
+
165
+ // src/http-utils.ts
166
+ async function parseResponse(response) {
167
+ const text = await response.text();
168
+ if (!text || text.trim() === "") {
169
+ return void 0;
170
+ }
171
+ try {
172
+ return TSON.parse(text);
173
+ } catch (error) {
174
+ throw new Error(`Failed to parse response: ${error instanceof Error ? error.message : String(error)}`);
175
+ }
176
+ }
177
+ function stringifyBody(data) {
178
+ if (data === void 0 || data === null) {
179
+ return "";
180
+ }
181
+ try {
182
+ return TSON.stringify(data);
183
+ } catch (error) {
184
+ throw new Error(`Failed to stringify request body: ${error instanceof Error ? error.message : String(error)}`);
185
+ }
186
+ }
187
+
188
+ // src/auth.ts
189
+ var AuthManager = class {
190
+ constructor(config, http) {
191
+ this.config = config;
192
+ this.http = http;
193
+ }
194
+ get serviceUrl() {
195
+ return `${this.config.serviceUrl}/service`;
196
+ }
197
+ /**
198
+ * Step 1: Request OTP to be sent to email
199
+ * Returns otp_id needed for verification
200
+ */
201
+ async requestOTP(email, scopes = ["CREATE_DB"]) {
202
+ const response = await this.http.fetch(`${this.serviceUrl}/token`, {
203
+ method: "POST",
204
+ headers: { "Content-Type": "application/json" },
205
+ body: stringifyBody({
206
+ grant_type: "otp",
207
+ email,
208
+ scopes
209
+ })
210
+ });
211
+ if (!response.ok) {
212
+ const error = await response.text();
213
+ throw new DexieCloudAuthError(
214
+ `Failed to request OTP: ${error}`,
215
+ response.status
216
+ );
217
+ }
218
+ const data = await parseResponse(response);
219
+ if (data.type !== "otp-sent" || !data.otp_id) {
220
+ throw new DexieCloudAuthError(`Unexpected response: ${JSON.stringify(data)}`);
221
+ }
222
+ return data.otp_id;
223
+ }
224
+ /**
225
+ * Step 2: Verify OTP and get access tokens
226
+ */
227
+ async verifyOTP(email, otpId, otp, scopes = ["CREATE_DB"]) {
228
+ const response = await this.http.fetch(`${this.serviceUrl}/token`, {
229
+ method: "POST",
230
+ headers: { "Content-Type": "application/json" },
231
+ body: stringifyBody({
232
+ grant_type: "otp",
233
+ email,
234
+ scopes,
235
+ otp_id: otpId,
236
+ otp
237
+ })
238
+ });
239
+ if (!response.ok) {
240
+ const error = await response.text();
241
+ throw new DexieCloudAuthError(
242
+ `Failed to verify OTP: ${error}`,
243
+ response.status
244
+ );
245
+ }
246
+ const data = await parseResponse(response);
247
+ if (data.type !== "tokens" || !data.accessToken) {
248
+ throw new DexieCloudAuthError(`Unexpected response: ${JSON.stringify(data)}`);
249
+ }
250
+ return {
251
+ accessToken: data.accessToken,
252
+ refreshToken: data.refreshToken,
253
+ userId: data.userId
254
+ };
255
+ }
256
+ /**
257
+ * Full OTP flow: request → verify → return tokens
258
+ * The getOTP callback should return the OTP code (e.g., from email)
259
+ */
260
+ async authenticateWithOTP(email, getOTP, scopes = ["CREATE_DB"]) {
261
+ const otpId = await this.requestOTP(email, scopes);
262
+ const otp = await getOTP();
263
+ return this.verifyOTP(email, otpId, otp, scopes);
264
+ }
265
+ /**
266
+ * Request OTP for database-specific operations
267
+ */
268
+ async requestDatabaseOTP(dbUrl, email) {
269
+ const response = await this.http.fetch(`${dbUrl}/token`, {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: stringifyBody({
273
+ grant_type: "otp-email",
274
+ email
275
+ })
276
+ });
277
+ if (!response.ok) {
278
+ const error = await response.text();
279
+ throw new DexieCloudAuthError(
280
+ `Failed to request database OTP: ${error}`,
281
+ response.status
282
+ );
283
+ }
284
+ }
285
+ /**
286
+ * Verify OTP for database-specific access
287
+ */
288
+ async verifyDatabaseOTP(dbUrl, email, otp) {
289
+ const response = await this.http.fetch(`${dbUrl}/token`, {
290
+ method: "POST",
291
+ headers: { "Content-Type": "application/json" },
292
+ body: stringifyBody({
293
+ grant_type: "otp-token",
294
+ email,
295
+ otp
296
+ })
297
+ });
298
+ if (!response.ok) {
299
+ const error = await response.text();
300
+ throw new DexieCloudAuthError(
301
+ `Failed to verify database OTP: ${error}`,
302
+ response.status
303
+ );
304
+ }
305
+ return parseResponse(response);
306
+ }
307
+ /**
308
+ * Full database authentication flow
309
+ */
310
+ async authenticateDatabase(dbUrl, email, getOTP) {
311
+ await this.requestDatabaseOTP(dbUrl, email);
312
+ const otp = await getOTP();
313
+ return this.verifyDatabaseOTP(dbUrl, email, otp);
314
+ }
315
+ /**
316
+ * Authenticate using client credentials (client_id + client_secret).
317
+ *
318
+ * This is the recommended method for server-to-server access.
319
+ * The credentials are found in the `dexie-cloud.key` file generated
320
+ * by the `dexie-cloud` CLI when connecting to a database.
321
+ *
322
+ * @param dbUrl - The database URL (e.g., 'https://xxxxxxxx.dexie.cloud')
323
+ * @param clientId - The client_id from dexie-cloud.key
324
+ * @param clientSecret - The client_secret from dexie-cloud.key
325
+ * @param scopes - Requested scopes (default: ['ACCESS_DB'])
326
+ */
327
+ async authenticateWithClientCredentials(dbUrl, clientId, clientSecret, scopes = ["ACCESS_DB"]) {
328
+ const response = await this.http.fetch(`${dbUrl}/token`, {
329
+ method: "POST",
330
+ headers: { "Content-Type": "application/json" },
331
+ body: stringifyBody({
332
+ grant_type: "client_credentials",
333
+ client_id: clientId,
334
+ client_secret: clientSecret,
335
+ scopes
336
+ })
337
+ });
338
+ if (!response.ok) {
339
+ const error = await response.text();
340
+ throw new DexieCloudAuthError(
341
+ `Failed to authenticate with client credentials: ${error}`,
342
+ response.status
343
+ );
344
+ }
345
+ const data = await parseResponse(response);
346
+ if (data.type !== "tokens" || !data.accessToken) {
347
+ throw new DexieCloudAuthError(`Unexpected response: ${JSON.stringify(data)}`);
348
+ }
349
+ return {
350
+ accessToken: data.accessToken,
351
+ refreshToken: data.refreshToken,
352
+ userId: data.userId
353
+ };
354
+ }
355
+ };
356
+
357
+ // src/database.ts
358
+ var DatabaseManager = class {
359
+ constructor(config, http) {
360
+ this.config = config;
361
+ this.http = http;
362
+ }
363
+ get serviceUrl() {
364
+ return `${this.config.serviceUrl}/service`;
365
+ }
366
+ /**
367
+ * Create a new database (requires authenticated token with CREATE_DB scope)
368
+ */
369
+ async create(accessToken, options = {}) {
370
+ const response = await this.http.fetch(`${this.serviceUrl}/create-db`, {
371
+ method: "POST",
372
+ headers: {
373
+ "Authorization": `Bearer ${accessToken}`,
374
+ "Content-Type": "application/json"
375
+ },
376
+ body: stringifyBody({
377
+ timeZone: options.timeZone || "UTC",
378
+ ...options.hackathon && { hackathon: true }
379
+ })
380
+ });
381
+ if (!response.ok) {
382
+ const error = await response.text();
383
+ throw new DexieCloudError(
384
+ `Failed to create database: ${error}`,
385
+ response.status
386
+ );
387
+ }
388
+ const data = await parseResponse(response);
389
+ if (!data?.url) {
390
+ throw new DexieCloudError(`Invalid response: ${JSON.stringify(data)}`);
391
+ }
392
+ return data;
393
+ }
394
+ /**
395
+ * List databases accessible to user (placeholder - API not documented yet)
396
+ */
397
+ async list(accessToken) {
398
+ throw new Error("List databases API not yet implemented");
399
+ }
400
+ /**
401
+ * Get database information by URL (placeholder)
402
+ */
403
+ async getInfo(dbUrl, accessToken) {
404
+ throw new Error("Database info API not yet implemented");
405
+ }
406
+ /**
407
+ * Delete database (placeholder - use with extreme caution!)
408
+ */
409
+ async delete(dbUrl, accessToken) {
410
+ throw new Error("Delete database API not yet implemented");
411
+ }
412
+ };
413
+
414
+ // src/health.ts
415
+ var HealthManager = class {
416
+ constructor(config, http) {
417
+ this.config = config;
418
+ this.http = http;
419
+ }
420
+ /**
421
+ * Health check - basic server status
422
+ */
423
+ async health() {
424
+ try {
425
+ const response = await this.http.fetch(`${this.config.serviceUrl}/health`);
426
+ return response.ok;
427
+ } catch {
428
+ return false;
429
+ }
430
+ }
431
+ /**
432
+ * Ready check - server + database connection
433
+ */
434
+ async ready() {
435
+ try {
436
+ const response = await this.http.fetch(`${this.config.serviceUrl}/ready`);
437
+ return response.ok;
438
+ } catch {
439
+ return false;
440
+ }
441
+ }
442
+ /**
443
+ * Combined health status
444
+ */
445
+ async status() {
446
+ const [healthy, ready] = await Promise.all([
447
+ this.health(),
448
+ this.ready()
449
+ ]);
450
+ return { healthy, ready };
451
+ }
452
+ /**
453
+ * Wait for server to be ready
454
+ */
455
+ async waitForReady(timeout = 6e4, interval = 1e3) {
456
+ const startTime = Date.now();
457
+ while (Date.now() - startTime < timeout) {
458
+ if (await this.ready()) {
459
+ return;
460
+ }
461
+ await new Promise((resolve) => setTimeout(resolve, interval));
462
+ }
463
+ throw new DexieCloudNetworkError(`Server not ready after ${timeout}ms`);
464
+ }
465
+ /**
466
+ * Wait for server to be healthy
467
+ */
468
+ async waitForHealth(timeout = 3e4, interval = 1e3) {
469
+ const startTime = Date.now();
470
+ while (Date.now() - startTime < timeout) {
471
+ if (await this.health()) {
472
+ return;
473
+ }
474
+ await new Promise((resolve) => setTimeout(resolve, interval));
475
+ }
476
+ throw new DexieCloudNetworkError(`Server not healthy after ${timeout}ms`);
477
+ }
478
+ };
479
+
480
+ // src/data.ts
481
+ async function handleResponse(response) {
482
+ if (!response.ok) {
483
+ const text = await response.text().catch(() => "");
484
+ throw new DexieCloudError(
485
+ `HTTP ${response.status}: ${text || response.statusText}`,
486
+ response.status,
487
+ text
488
+ );
489
+ }
490
+ return parseResponse(response);
491
+ }
492
+ var DataManager = class {
493
+ constructor(dbUrl, http, blobManager, access = "my") {
494
+ this.dbUrl = dbUrl;
495
+ this.http = http;
496
+ this.blobManager = blobManager;
497
+ this.access = access;
498
+ }
499
+ tableUrl(table) {
500
+ return `${this.dbUrl}/${this.access}/${encodeURIComponent(table)}`;
501
+ }
502
+ itemUrl(table, id) {
503
+ return `${this.dbUrl}/${this.access}/${encodeURIComponent(table)}/${encodeURIComponent(id)}`;
504
+ }
505
+ /**
506
+ * List all objects in a table, optionally filtered by realm.
507
+ * BlobRefs in results are automatically resolved to inline data
508
+ * when a BlobManager is present.
509
+ */
510
+ async list(table, token, options) {
511
+ let url = this.tableUrl(table);
512
+ if (options?.realm) {
513
+ url += `?realm=${encodeURIComponent(options.realm)}`;
514
+ }
515
+ const response = await this.http.fetch(url, {
516
+ method: "GET",
517
+ headers: { Authorization: `Bearer ${token}` }
518
+ });
519
+ const result = await handleResponse(response);
520
+ const items = Array.isArray(result) ? result : result?.data ?? result ?? [];
521
+ if (this.blobManager) {
522
+ const resolved = [];
523
+ for (const item of items) {
524
+ resolved.push(await this.blobManager.processForRead(item, token));
525
+ }
526
+ return resolved;
527
+ }
528
+ return items;
529
+ }
530
+ /**
531
+ * Get a single object by id.
532
+ * BlobRefs in the result are automatically resolved to inline data
533
+ * when a BlobManager is present.
534
+ */
535
+ async get(table, id, token) {
536
+ const url = this.itemUrl(table, id);
537
+ const response = await this.http.fetch(url, {
538
+ method: "GET",
539
+ headers: { Authorization: `Bearer ${token}` }
540
+ });
541
+ const result = await handleResponse(response);
542
+ if (this.blobManager) {
543
+ return this.blobManager.processForRead(result, token);
544
+ }
545
+ return result;
546
+ }
547
+ /**
548
+ * Create an object in a table.
549
+ * Inline blobs in obj are automatically uploaded and replaced with
550
+ * BlobRefs when a BlobManager is present.
551
+ */
552
+ async create(table, obj, token) {
553
+ const url = this.tableUrl(table);
554
+ const body = this.blobManager ? await this.blobManager.processForUpload(obj, token) : obj;
555
+ const response = await this.http.fetch(url, {
556
+ method: "POST",
557
+ headers: {
558
+ Authorization: `Bearer ${token}`,
559
+ "Content-Type": "application/json"
560
+ },
561
+ body: stringifyBody(body)
562
+ });
563
+ const keys = await handleResponse(response);
564
+ const id = Array.isArray(keys) ? keys[0] : keys;
565
+ return { id: typeof id === "string" ? id : JSON.stringify(id) };
566
+ }
567
+ /**
568
+ * Replace (full update) an object by id using HTTP PUT.
569
+ *
570
+ * NOTE: This sends the complete object as a full replacement (PUT semantics).
571
+ * All fields not included in `obj` will be removed on the server.
572
+ * If you want partial updates, use a PATCH-based approach when the server
573
+ * supports it (not currently exposed here).
574
+ *
575
+ * Previously named `update()` — `update` is kept as an alias for backwards
576
+ * compatibility but may be deprecated in a future release.
577
+ */
578
+ async replace(table, id, obj, token) {
579
+ const url = this.tableUrl(table);
580
+ const merged = { ...obj, id };
581
+ const body = this.blobManager ? await this.blobManager.processForUpload(merged, token) : merged;
582
+ const response = await this.http.fetch(url, {
583
+ method: "POST",
584
+ headers: {
585
+ Authorization: `Bearer ${token}`,
586
+ "Content-Type": "application/json"
587
+ },
588
+ body: stringifyBody(body)
589
+ });
590
+ const keys = await handleResponse(response);
591
+ const returnedId = Array.isArray(keys) ? keys[0] : keys;
592
+ return { id: typeof returnedId === "string" ? returnedId : JSON.stringify(returnedId) };
593
+ }
594
+ /**
595
+ * @deprecated Use `replace()` instead. This method performs a full object
596
+ * replacement (HTTP PUT), not a partial update (PATCH). The name `update`
597
+ * is misleading and kept only for backwards compatibility.
598
+ */
599
+ update(table, id, obj, token) {
600
+ return this.replace(table, id, obj, token);
601
+ }
602
+ /**
603
+ * Delete an object by id.
604
+ */
605
+ async delete(table, id, token) {
606
+ const url = this.itemUrl(table, id);
607
+ const response = await this.http.fetch(url, {
608
+ method: "DELETE",
609
+ headers: { Authorization: `Bearer ${token}` }
610
+ });
611
+ if (!response.ok) {
612
+ const text = await response.text().catch(() => "");
613
+ throw new DexieCloudError(
614
+ `HTTP ${response.status}: ${text || response.statusText}`,
615
+ response.status,
616
+ text
617
+ );
618
+ }
619
+ }
620
+ /**
621
+ * Bulk create objects in a table in parallel.
622
+ * All requests are sent concurrently via Promise.all.
623
+ *
624
+ * NOTE: This is NOT atomic — if one create fails, others may have already
625
+ * succeeded on the server. There is no rollback for partial failures.
626
+ */
627
+ async bulkCreate(table, objects, token) {
628
+ return Promise.all(objects.map((obj) => this.create(table, obj, token)));
629
+ }
630
+ };
631
+
632
+ // src/blob.ts
633
+ var BLOB_THRESHOLD = 4096;
634
+ var MAX_CONCURRENT_DOWNLOADS = 6;
635
+ function generateBlobId() {
636
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
637
+ return crypto.randomUUID().replace(/-/g, "");
638
+ }
639
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
640
+ const bytes = new Uint8Array(16);
641
+ crypto.getRandomValues(bytes);
642
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
643
+ }
644
+ const ts = Date.now().toString(16);
645
+ const rand = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
646
+ return ts + rand;
647
+ }
648
+ async function toUint8Array(data) {
649
+ if (data instanceof Uint8Array) return data;
650
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
651
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
652
+ const buf = await data.arrayBuffer();
653
+ return new Uint8Array(buf);
654
+ }
655
+ if (ArrayBuffer.isView(data)) {
656
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
657
+ }
658
+ throw new TypeError("Unsupported data type for blob upload");
659
+ }
660
+ function isInlineBlob(val) {
661
+ return val !== null && typeof val === "object" && typeof val._bt === "string" && typeof val.v === "string";
662
+ }
663
+ function isBlobRef(val) {
664
+ return val !== null && typeof val === "object" && typeof val._bt === "string" && typeof val.ref === "string" && val.v === void 0;
665
+ }
666
+ function base64ToUint8Array(b64) {
667
+ if (typeof Buffer !== "undefined") {
668
+ return new Uint8Array(Buffer.from(b64, "base64"));
669
+ }
670
+ const binary = atob(b64);
671
+ const bytes = new Uint8Array(binary.length);
672
+ for (let i = 0; i < binary.length; i++) {
673
+ bytes[i] = binary.charCodeAt(i);
674
+ }
675
+ return bytes;
676
+ }
677
+ function uint8ArrayToBase64(data) {
678
+ if (typeof Buffer !== "undefined") {
679
+ return Buffer.from(data).toString("base64");
680
+ }
681
+ let binary = "";
682
+ for (let i = 0; i < data.length; i++) {
683
+ binary += String.fromCharCode(data[i]);
684
+ }
685
+ return btoa(binary);
686
+ }
687
+ var DEFAULT_MAX_STRING_LENGTH = 32768;
688
+ var BlobManager = class {
689
+ constructor(dbUrl, http, mode = "auto", maxStringLength = DEFAULT_MAX_STRING_LENGTH) {
690
+ this.dbUrl = dbUrl;
691
+ this.http = http;
692
+ this.mode = mode;
693
+ this.maxStringLength = maxStringLength;
694
+ }
695
+ /**
696
+ * Upload binary data to the blob store.
697
+ * Returns the blob ref (e.g. "1:abc123...").
698
+ */
699
+ async upload(data, contentType = "application/octet-stream", token) {
700
+ const blobId = generateBlobId();
701
+ const url = `${this.dbUrl}/blob/${blobId}`;
702
+ const bytes = await toUint8Array(data);
703
+ const response = await this.http.fetch(url, {
704
+ method: "PUT",
705
+ headers: {
706
+ Authorization: `Bearer ${token}`,
707
+ "Content-Type": contentType
708
+ },
709
+ body: bytes
710
+ });
711
+ if (!response.ok) {
712
+ const text2 = await response.text().catch(() => "");
713
+ throw new DexieCloudError(
714
+ `Blob upload failed HTTP ${response.status}: ${text2 || response.statusText}`,
715
+ response.status,
716
+ text2
717
+ );
718
+ }
719
+ const text = await response.text().catch(() => "");
720
+ if (text && text.trim()) {
721
+ try {
722
+ const parsed = JSON.parse(text);
723
+ if (parsed?.ref) return parsed.ref;
724
+ } catch {
725
+ }
726
+ if (text.includes(":")) return text.trim();
727
+ }
728
+ throw new DexieCloudError(
729
+ `Blob upload succeeded (HTTP ${response.status}) but server returned no parseable ref. Cannot construct a safe blob reference without the server-assigned version.`,
730
+ response.status,
731
+ text
732
+ );
733
+ }
734
+ /**
735
+ * Download a blob by ref (format: "version:blobId").
736
+ */
737
+ async download(ref, token) {
738
+ const url = `${this.dbUrl}/blob/${encodeURIComponent(ref)}`;
739
+ const response = await this.http.fetch(url, {
740
+ method: "GET",
741
+ headers: { Authorization: `Bearer ${token}` }
742
+ });
743
+ if (!response.ok) {
744
+ const text = await response.text().catch(() => "");
745
+ throw new DexieCloudError(
746
+ `Blob download failed HTTP ${response.status}: ${text || response.statusText}`,
747
+ response.status,
748
+ text
749
+ );
750
+ }
751
+ const contentType = response.headers?.get("content-type") ?? "application/octet-stream";
752
+ const arrayBuffer = await response.arrayBuffer();
753
+ return { data: new Uint8Array(arrayBuffer), contentType };
754
+ }
755
+ /**
756
+ * Process an object before uploading: find inline blobs large enough to
757
+ * offload (≥ BLOB_THRESHOLD bytes), upload them, replace with BlobRefs.
758
+ * Small binaries are left inline. Only active in 'auto' mode.
759
+ */
760
+ async processForUpload(obj, token) {
761
+ if (this.mode !== "auto") return obj;
762
+ return this._walkForUpload(obj, token);
763
+ }
764
+ /**
765
+ * Process an object after reading: find BlobRefs, download them,
766
+ * replace with inline data. Only active in 'auto' mode.
767
+ */
768
+ async processForRead(obj, token) {
769
+ if (this.mode !== "auto") return obj;
770
+ return this._walkForRead(obj, token);
771
+ }
772
+ async _walkForUpload(val, token) {
773
+ if (typeof val === "string" && val.length > this.maxStringLength && this.maxStringLength !== Infinity) {
774
+ const bytes = new TextEncoder().encode(val);
775
+ const ref = await this.upload(bytes, "text/plain;charset=utf-8", token);
776
+ return { _bt: "string", ref, size: bytes.length };
777
+ }
778
+ if (isInlineBlob(val)) {
779
+ const bytes = base64ToUint8Array(val.v);
780
+ if (bytes.length < BLOB_THRESHOLD) {
781
+ return val;
782
+ }
783
+ const contentType = val.ct ?? "application/octet-stream";
784
+ const ref = await this.upload(bytes, contentType, token);
785
+ const blobRef = {
786
+ _bt: val._bt,
787
+ ref,
788
+ size: bytes.length,
789
+ ...val._bt === "Blob" && val.ct ? { ct: val.ct } : {}
790
+ };
791
+ return blobRef;
792
+ }
793
+ if (Array.isArray(val)) {
794
+ return Promise.all(val.map((item) => this._walkForUpload(item, token)));
795
+ }
796
+ if (val !== null && typeof val === "object") {
797
+ const entries = await Promise.all(
798
+ Object.entries(val).map(async ([k, v]) => [k, await this._walkForUpload(v, token)])
799
+ );
800
+ return Object.fromEntries(entries);
801
+ }
802
+ return val;
803
+ }
804
+ async _walkForRead(val, token) {
805
+ if (isBlobRef(val)) {
806
+ if (val._bt === "string") {
807
+ const { data: data2 } = await this.download(val.ref, token);
808
+ return new TextDecoder().decode(data2);
809
+ }
810
+ const { data, contentType } = await this.download(val.ref, token);
811
+ return {
812
+ _bt: val._bt,
813
+ v: uint8ArrayToBase64(data),
814
+ ...val._bt === "Blob" ? { ct: val.ct ?? contentType } : {}
815
+ };
816
+ }
817
+ if (Array.isArray(val)) {
818
+ return this._parallelMap(val, (item) => this._walkForRead(item, token));
819
+ }
820
+ if (val !== null && typeof val === "object") {
821
+ const keys = Object.keys(val);
822
+ const resolvedValues = await this._parallelMap(
823
+ keys,
824
+ (k) => this._walkForRead(val[k], token)
825
+ );
826
+ const result = {};
827
+ for (let i = 0; i < keys.length; i++) {
828
+ result[keys[i]] = resolvedValues[i];
829
+ }
830
+ return result;
831
+ }
832
+ return val;
833
+ }
834
+ /**
835
+ * Like Promise.all but with a concurrency cap.
836
+ */
837
+ async _parallelMap(items, fn) {
838
+ const results = new Array(items.length);
839
+ let index = 0;
840
+ async function worker() {
841
+ while (index < items.length) {
842
+ const i = index++;
843
+ results[i] = await fn(items[i]);
844
+ }
845
+ }
846
+ const workers = Array.from(
847
+ { length: Math.min(MAX_CONCURRENT_DOWNLOADS, items.length) },
848
+ () => worker()
849
+ );
850
+ await Promise.all(workers);
851
+ return results;
852
+ }
853
+ };
854
+
855
+ // src/DatabaseSession.ts
856
+ function stableStringify(obj) {
857
+ const sorted = Object.keys(obj).sort().reduce(
858
+ (acc, key) => {
859
+ acc[key] = obj[key];
860
+ return acc;
861
+ },
862
+ {}
863
+ );
864
+ return JSON.stringify(sorted);
865
+ }
866
+ var tokenCache = /* @__PURE__ */ new Map();
867
+ var REFRESH_MARGIN = 300;
868
+ function cacheKey(clientId, dbUrl, claims) {
869
+ return `${clientId}::${dbUrl}::${stableStringify(claims ?? {})}`;
870
+ }
871
+ function parseJwtExp(token) {
872
+ const payload = JSON.parse(
873
+ Buffer.from(token.split(".")[1], "base64url").toString()
874
+ );
875
+ return payload.exp;
876
+ }
877
+ function createDataProxy(data, getToken) {
878
+ return new Proxy(data, {
879
+ get(target, prop) {
880
+ const original = target[prop];
881
+ if (typeof original !== "function") return original;
882
+ return async (...args) => {
883
+ const token = await getToken();
884
+ return original.apply(target, [...args, token]);
885
+ };
886
+ }
887
+ });
888
+ }
889
+ function createBlobProxy(blobs, getToken) {
890
+ return new Proxy(blobs, {
891
+ get(target, prop) {
892
+ const original = target[prop];
893
+ if (typeof original !== "function") return original;
894
+ return async (...args) => {
895
+ const token = await getToken();
896
+ return original.apply(target, [...args, token]);
897
+ };
898
+ }
899
+ });
900
+ }
901
+ var DatabaseSession = class _DatabaseSession {
902
+ constructor(dbUrl, credentials, http, blobManager, dataManager) {
903
+ this.dbUrl = dbUrl;
904
+ this.credentials = credentials;
905
+ this.http = http;
906
+ this.inflightToken = null;
907
+ const getToken = () => this.getToken();
908
+ this.data = createDataProxy(dataManager, getToken);
909
+ this.blobs = createBlobProxy(blobManager, getToken);
910
+ }
911
+ /**
912
+ * Create a new session that impersonates a specific user.
913
+ *
914
+ * The returned session shares the same HTTP adapter and managers but
915
+ * uses a separate cache entry for tokens.
916
+ */
917
+ asUser(claims) {
918
+ const bm = new BlobManager(this.dbUrl, this.http);
919
+ return new _DatabaseSession(
920
+ this.dbUrl,
921
+ { ...this.credentials, impersonate: claims },
922
+ this.http,
923
+ bm,
924
+ new DataManager(this.dbUrl, this.http, bm, "my")
925
+ );
926
+ }
927
+ // ── Token management ────────────────────────────────────────────
928
+ async getToken() {
929
+ const key = cacheKey(
930
+ this.credentials.clientId,
931
+ this.dbUrl,
932
+ this.credentials.impersonate
933
+ );
934
+ const cached = tokenCache.get(key);
935
+ const now = Math.floor(Date.now() / 1e3);
936
+ if (cached && cached.expiresAt - now > REFRESH_MARGIN) {
937
+ return cached.token;
938
+ }
939
+ if (!this.inflightToken) {
940
+ this.inflightToken = this.fetchToken(key).finally(() => {
941
+ this.inflightToken = null;
942
+ });
943
+ }
944
+ return this.inflightToken;
945
+ }
946
+ async fetchToken(key) {
947
+ const { clientId, clientSecret, impersonate } = this.credentials;
948
+ const isImpersonation = !!impersonate;
949
+ const body = {
950
+ grant_type: "client_credentials",
951
+ client_id: clientId,
952
+ client_secret: clientSecret,
953
+ scopes: isImpersonation ? ["ACCESS_DB", "IMPERSONATE", "MANAGE_DB"] : ["ACCESS_DB", "GLOBAL_READ", "GLOBAL_WRITE"]
954
+ // GLOBAL_* needed for /all/ access
955
+ };
956
+ if (isImpersonation) {
957
+ body.claims = impersonate;
958
+ }
959
+ const response = await this.http.fetch(`${this.dbUrl}/token`, {
960
+ method: "POST",
961
+ headers: { "Content-Type": "application/json" },
962
+ body: stringifyBody(body)
963
+ });
964
+ if (!response.ok) {
965
+ const error = await response.text();
966
+ throw new DexieCloudAuthError(
967
+ `Failed to authenticate with client credentials: ${error}`,
968
+ response.status
969
+ );
970
+ }
971
+ const data = await parseResponse(response);
972
+ if (data.type !== "tokens" || !data.accessToken) {
973
+ throw new DexieCloudAuthError(
974
+ `Unexpected token response: ${JSON.stringify(data)}`
975
+ );
976
+ }
977
+ const token = data.accessToken;
978
+ const expiresAt = parseJwtExp(token);
979
+ tokenCache.set(key, { token, expiresAt });
980
+ return token;
981
+ }
982
+ };
983
+
984
+ // src/client.ts
985
+ var DexieCloudClient = class {
986
+ constructor(config) {
987
+ const fullConfig = typeof config === "string" ? { serviceUrl: config } : config;
988
+ if (!fullConfig.serviceUrl) {
989
+ throw new DexieCloudError("serviceUrl is required");
990
+ }
991
+ fullConfig.serviceUrl = fullConfig.serviceUrl.replace(/\/$/, "");
992
+ if (fullConfig.dbUrl) {
993
+ fullConfig.dbUrl = fullConfig.dbUrl.replace(/\/$/, "");
994
+ }
995
+ this.http = createAdapter(fullConfig);
996
+ this.auth = new AuthManager(fullConfig, this.http);
997
+ this.databases = new DatabaseManager(fullConfig, this.http);
998
+ this.health = new HealthManager(fullConfig, this.http);
999
+ const dbUrl = fullConfig.dbUrl ?? fullConfig.serviceUrl;
1000
+ this.blobs = new BlobManager(dbUrl, this.http, fullConfig.blobHandling ?? "auto", fullConfig.maxStringLength ?? 32768);
1001
+ this.data = new DataManager(dbUrl, this.http, this.blobs);
1002
+ }
1003
+ /**
1004
+ * Convenience method: Full database creation flow with OTP
1005
+ * 1. Request OTP → 2. Get OTP from callback → 3. Verify → 4. Create DB
1006
+ */
1007
+ async createDatabase(email, getOTP, options = {}) {
1008
+ const { accessToken } = await this.auth.authenticateWithOTP(email, getOTP, ["CREATE_DB"]);
1009
+ const dbInfo = await this.databases.create(accessToken, options);
1010
+ return { ...dbInfo, accessToken };
1011
+ }
1012
+ /** Convenience: Check if service is operational */
1013
+ async isReady() {
1014
+ return this.health.ready();
1015
+ }
1016
+ /** Convenience: Get full health status */
1017
+ async getStatus() {
1018
+ return this.health.status();
1019
+ }
1020
+ /** Convenience: Wait for service to be ready */
1021
+ async waitForReady(timeout) {
1022
+ return this.health.waitForReady(timeout);
1023
+ }
1024
+ /**
1025
+ * Create a pre-authenticated DatabaseSession for a specific database.
1026
+ *
1027
+ * Tokens are acquired lazily and cached in-memory. When a token is
1028
+ * within 5 minutes of expiry a fresh one is fetched transparently.
1029
+ *
1030
+ * @example
1031
+ * ```ts
1032
+ * const db = client.db('https://xxxxxxxx.dexie.cloud', {
1033
+ * clientId: '...',
1034
+ * clientSecret: '...',
1035
+ * });
1036
+ * const items = await db.data.list('todoItems');
1037
+ * ```
1038
+ */
1039
+ db(dbUrl, credentials) {
1040
+ const normalizedUrl = dbUrl.replace(/\/$/, "");
1041
+ const blobManager = new BlobManager(normalizedUrl, this.http);
1042
+ const access = credentials.impersonate ? "my" : "all";
1043
+ const dataManager = new DataManager(normalizedUrl, this.http, blobManager, access);
1044
+ return new DatabaseSession(normalizedUrl, credentials, this.http, blobManager, dataManager);
1045
+ }
1046
+ };
1047
+ // Annotate the CommonJS export names for ESM import in node:
1048
+ 0 && (module.exports = {
1049
+ AuthManager,
1050
+ BlobManager,
1051
+ DataManager,
1052
+ DatabaseManager,
1053
+ DatabaseSession,
1054
+ DexieCloudAuthError,
1055
+ DexieCloudClient,
1056
+ DexieCloudError,
1057
+ DexieCloudNetworkError,
1058
+ FetchAdapter,
1059
+ HealthManager,
1060
+ NodeAdapter,
1061
+ TSON,
1062
+ createAdapter,
1063
+ parse,
1064
+ stringify
1065
+ });