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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/index.d.mts +236 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.js +1065 -0
- package/dist/index.mjs +1017 -0
- package/package.json +79 -0
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
|
+
});
|