hydrousdb 2.0.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1194 @@
1
+ 'use strict';
2
+
3
+ // src/utils/errors.ts
4
+ var HydrousError = class extends Error {
5
+ constructor(message, code, status, requestId, details) {
6
+ super(message);
7
+ this.name = "HydrousError";
8
+ this.code = code;
9
+ this.status = status;
10
+ this.requestId = requestId;
11
+ this.details = details;
12
+ Object.setPrototypeOf(this, new.target.prototype);
13
+ }
14
+ toString() {
15
+ return `HydrousError [${this.code}]: ${this.message}`;
16
+ }
17
+ };
18
+ var AuthError = class extends HydrousError {
19
+ constructor(message, code, status, requestId, details) {
20
+ super(message, code, status, requestId, details);
21
+ this.name = "AuthError";
22
+ }
23
+ };
24
+ var RecordError = class extends HydrousError {
25
+ constructor(message, code, status, requestId, details) {
26
+ super(message, code, status, requestId, details);
27
+ this.name = "RecordError";
28
+ }
29
+ };
30
+ var StorageError = class extends HydrousError {
31
+ constructor(message, code, status, requestId) {
32
+ super(message, code, status, requestId);
33
+ this.name = "StorageError";
34
+ }
35
+ };
36
+ var AnalyticsError = class extends HydrousError {
37
+ constructor(message, code, status, requestId) {
38
+ super(message, code, status, requestId);
39
+ this.name = "AnalyticsError";
40
+ }
41
+ };
42
+ var ValidationError = class extends HydrousError {
43
+ constructor(message, details) {
44
+ super(message, "VALIDATION_ERROR", 400, void 0, details);
45
+ this.name = "ValidationError";
46
+ }
47
+ };
48
+ var NetworkError = class extends HydrousError {
49
+ constructor(message, cause) {
50
+ super(message, "NETWORK_ERROR");
51
+ this.name = "NetworkError";
52
+ if (cause) this.cause = cause;
53
+ }
54
+ };
55
+
56
+ // src/utils/http.ts
57
+ var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
58
+ var HttpClient = class {
59
+ constructor(baseUrl) {
60
+ this.baseUrl = baseUrl.replace(/\/$/, "");
61
+ }
62
+ async request(path, apiKeyOrOpts, opts = {}) {
63
+ let apiKey;
64
+ let resolvedOpts;
65
+ if (typeof apiKeyOrOpts === "string") {
66
+ apiKey = apiKeyOrOpts;
67
+ resolvedOpts = opts;
68
+ } else {
69
+ apiKey = void 0;
70
+ resolvedOpts = apiKeyOrOpts ?? opts;
71
+ }
72
+ const {
73
+ method = "GET",
74
+ body,
75
+ headers = {},
76
+ rawBody,
77
+ contentType = "application/json"
78
+ } = resolvedOpts;
79
+ const url = `${this.baseUrl}${path}`;
80
+ const reqHeaders = {
81
+ ...apiKey ? { "X-Api-Key": apiKey } : {},
82
+ ...headers
83
+ };
84
+ let reqBody = null;
85
+ if (rawBody !== void 0) {
86
+ reqBody = rawBody;
87
+ if (contentType) reqHeaders["Content-Type"] = contentType;
88
+ } else if (body !== void 0) {
89
+ reqBody = JSON.stringify(body);
90
+ reqHeaders["Content-Type"] = "application/json";
91
+ }
92
+ let response;
93
+ try {
94
+ response = await fetch(url, {
95
+ method,
96
+ headers: reqHeaders,
97
+ body: reqBody
98
+ });
99
+ } catch (err) {
100
+ throw new NetworkError(
101
+ `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
102
+ err
103
+ );
104
+ }
105
+ const ct = response.headers.get("content-type") ?? "";
106
+ if (!ct.includes("application/json")) {
107
+ if (!response.ok) {
108
+ throw new HydrousError(
109
+ `Request failed with status ${response.status}`,
110
+ `HTTP_${response.status}`,
111
+ response.status
112
+ );
113
+ }
114
+ const buffer = await response.arrayBuffer();
115
+ return buffer;
116
+ }
117
+ let responseData;
118
+ try {
119
+ responseData = await response.json();
120
+ } catch {
121
+ throw new HydrousError(
122
+ "Failed to parse server response as JSON",
123
+ "PARSE_ERROR",
124
+ response.status
125
+ );
126
+ }
127
+ if (!response.ok) {
128
+ const errData = responseData;
129
+ throw new HydrousError(
130
+ errData["error"] ?? `Request failed with status ${response.status}`,
131
+ errData["code"] ?? `HTTP_${response.status}`,
132
+ response.status,
133
+ errData["requestId"],
134
+ errData["details"]
135
+ );
136
+ }
137
+ return responseData;
138
+ }
139
+ get(path, apiKey, headers) {
140
+ return this.request(path, apiKey, { method: "GET", headers });
141
+ }
142
+ post(path, apiKey, body, headers) {
143
+ return this.request(path, apiKey, { method: "POST", body, headers });
144
+ }
145
+ put(path, apiKey, body, headers) {
146
+ return this.request(path, apiKey, { method: "PUT", body, headers });
147
+ }
148
+ patch(path, apiKey, body, headers) {
149
+ return this.request(path, apiKey, { method: "PATCH", body, headers });
150
+ }
151
+ delete(path, apiKey, body, headers) {
152
+ return this.request(path, apiKey, { method: "DELETE", body, headers });
153
+ }
154
+ async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
155
+ const XHR = typeof globalThis["XMLHttpRequest"] !== "undefined" ? globalThis["XMLHttpRequest"] : void 0;
156
+ if (XHR && onProgress) {
157
+ return new Promise((resolve, reject) => {
158
+ const xhr = new XHR();
159
+ xhr.upload.onprogress = (e) => {
160
+ if (e.lengthComputable) {
161
+ onProgress(Math.round(e.loaded / e.total * 100));
162
+ }
163
+ };
164
+ xhr.onload = () => {
165
+ if (xhr.status >= 200 && xhr.status < 300) {
166
+ resolve();
167
+ } else {
168
+ reject(new Error(`Upload failed: ${xhr.status}`));
169
+ }
170
+ };
171
+ xhr.onerror = () => reject(new Error("Upload network error"));
172
+ xhr.open("PUT", signedUrl);
173
+ xhr.setRequestHeader("Content-Type", mimeType);
174
+ const payload = data instanceof Blob ? data : new Blob([data], { type: mimeType });
175
+ xhr.send(payload);
176
+ });
177
+ }
178
+ const fetchBody = data instanceof Blob ? data : new Blob([data], { type: mimeType });
179
+ const response = await fetch(signedUrl, {
180
+ method: "PUT",
181
+ headers: { "Content-Type": mimeType },
182
+ body: fetchBody
183
+ });
184
+ if (!response.ok) {
185
+ throw new NetworkError(`Signed URL upload failed with status ${response.status}`);
186
+ }
187
+ }
188
+ };
189
+
190
+ // src/auth/client.ts
191
+ var AuthClient = class {
192
+ constructor(http, authKey, bucketKey) {
193
+ this.http = http;
194
+ this.authKey = authKey;
195
+ this.basePath = `/auth/${bucketKey}`;
196
+ }
197
+ post(path, body) {
198
+ return this.http.post(path, this.authKey, body);
199
+ }
200
+ get(path) {
201
+ return this.http.get(path, this.authKey);
202
+ }
203
+ patch(path, body) {
204
+ return this.http.patch(path, this.authKey, body);
205
+ }
206
+ delete(path, body) {
207
+ return this.http.delete(path, this.authKey, body);
208
+ }
209
+ // ─── Registration & Login ─────────────────────────────────────────────────
210
+ async signup(options) {
211
+ const result = await this.post(`${this.basePath}/signup`, options);
212
+ return { user: result.user, session: result.session };
213
+ }
214
+ async login(options) {
215
+ const result = await this.post(`${this.basePath}/login`, options);
216
+ return { user: result.user, session: result.session };
217
+ }
218
+ async logout({ sessionId }) {
219
+ await this.post(`${this.basePath}/logout`, { sessionId });
220
+ }
221
+ async refreshSession({ refreshToken }) {
222
+ const result = await this.post(`${this.basePath}/session/refresh`, { refreshToken });
223
+ return result.session;
224
+ }
225
+ // ─── User Profile ─────────────────────────────────────────────────────────
226
+ async getUser({ userId }) {
227
+ const result = await this.get(`${this.basePath}/user/${userId}`);
228
+ return result.user;
229
+ }
230
+ async updateUser(options) {
231
+ const { sessionId, userId, data } = options;
232
+ const result = await this.patch(`${this.basePath}/user`, { sessionId, userId, ...data });
233
+ return result.user;
234
+ }
235
+ async deleteUser({ sessionId, userId }) {
236
+ await this.delete(`${this.basePath}/user`, { sessionId, userId });
237
+ }
238
+ // ─── Admin Operations ─────────────────────────────────────────────────────
239
+ async listUsers(options) {
240
+ const { sessionId, limit = 50, offset = 0 } = options;
241
+ const result = await this.post(
242
+ `${this.basePath}/users/list`,
243
+ { sessionId, limit, offset }
244
+ );
245
+ return { users: result.users, total: result.total, limit: result.limit, offset: result.offset };
246
+ }
247
+ async hardDeleteUser({ sessionId, userId }) {
248
+ await this.delete(`${this.basePath}/user/hard`, { sessionId, userId });
249
+ }
250
+ async bulkDeleteUsers({ sessionId, userIds }) {
251
+ const result = await this.post(
252
+ `${this.basePath}/users/bulk-delete`,
253
+ { sessionId, userIds }
254
+ );
255
+ return { deleted: result.deleted, failed: result.failed };
256
+ }
257
+ async lockAccount({ sessionId, userId, duration }) {
258
+ const result = await this.post(
259
+ `${this.basePath}/account/lock`,
260
+ { sessionId, userId, duration }
261
+ );
262
+ return result.data;
263
+ }
264
+ async unlockAccount({ sessionId, userId }) {
265
+ await this.post(`${this.basePath}/account/unlock`, { sessionId, userId });
266
+ }
267
+ // ─── Password Management ──────────────────────────────────────────────────
268
+ async changePassword(options) {
269
+ await this.post(`${this.basePath}/password/change`, options);
270
+ }
271
+ async requestPasswordReset({ email }) {
272
+ await this.post(`${this.basePath}/password/reset/request`, { email });
273
+ }
274
+ async confirmPasswordReset({ resetToken, newPassword }) {
275
+ await this.post(`${this.basePath}/password/reset/confirm`, { resetToken, newPassword });
276
+ }
277
+ // ─── Email Verification ───────────────────────────────────────────────────
278
+ async requestEmailVerification({ userId }) {
279
+ await this.post(`${this.basePath}/email/verify/request`, { userId });
280
+ }
281
+ async confirmEmailVerification({ verifyToken }) {
282
+ await this.post(`${this.basePath}/email/verify/confirm`, { verifyToken });
283
+ }
284
+ };
285
+
286
+ // src/utils/query.ts
287
+ function buildQueryParams(options = {}) {
288
+ const params = new URLSearchParams();
289
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
290
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
291
+ if (options.orderBy !== void 0) params.set("orderBy", options.orderBy);
292
+ if (options.order !== void 0) params.set("order", options.order);
293
+ if (options.fields !== void 0) params.set("fields", options.fields);
294
+ if (options.startAfter !== void 0) params.set("startAfter", options.startAfter);
295
+ if (options.startAt !== void 0) params.set("startAt", options.startAt);
296
+ if (options.endAt !== void 0) params.set("endAt", options.endAt);
297
+ if (options.dateRange?.start !== void 0)
298
+ params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
299
+ if (options.dateRange?.end !== void 0)
300
+ params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
301
+ if (options.filters && options.filters.length > 0) {
302
+ params.set("filters", JSON.stringify(options.filters));
303
+ }
304
+ const str = params.toString();
305
+ return str ? `?${str}` : "";
306
+ }
307
+ function guessMimeType(filename) {
308
+ const ext = filename.split(".").pop()?.toLowerCase();
309
+ const map = {
310
+ jpg: "image/jpeg",
311
+ jpeg: "image/jpeg",
312
+ png: "image/png",
313
+ gif: "image/gif",
314
+ webp: "image/webp",
315
+ svg: "image/svg+xml",
316
+ pdf: "application/pdf",
317
+ mp4: "video/mp4",
318
+ webm: "video/webm",
319
+ mp3: "audio/mpeg",
320
+ wav: "audio/wav",
321
+ txt: "text/plain",
322
+ html: "text/html",
323
+ css: "text/css",
324
+ js: "application/javascript",
325
+ json: "application/json",
326
+ xml: "application/xml",
327
+ zip: "application/zip",
328
+ csv: "text/csv"
329
+ };
330
+ return map[ext ?? ""] ?? "application/octet-stream";
331
+ }
332
+ function assertSafeName(name, label = "name") {
333
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$/.test(name)) {
334
+ throw new Error(
335
+ `Invalid ${label} "${name}". Must start with a letter or underscore and contain only letters, numbers, underscores, dots, or hyphens.`
336
+ );
337
+ }
338
+ }
339
+
340
+ // src/records/client.ts
341
+ var RecordsClient = class {
342
+ constructor(http, bucketSecurityKey, bucketKey) {
343
+ assertSafeName(bucketKey, "bucketKey");
344
+ this.http = http;
345
+ this.bucketKey = bucketKey;
346
+ this.bucketKey_ = bucketSecurityKey;
347
+ this.basePath = `/records/${bucketKey}`;
348
+ }
349
+ get key() {
350
+ return this.bucketKey_;
351
+ }
352
+ // ─── Single Record Operations ─────────────────────────────────────────────
353
+ async create(data) {
354
+ const result = await this.http.post(this.basePath, this.key, data);
355
+ return result.record ?? result.data;
356
+ }
357
+ async get(id) {
358
+ const result = await this.http.get(`${this.basePath}/${id}`, this.key);
359
+ return result.record ?? result.data;
360
+ }
361
+ async set(id, data) {
362
+ const result = await this.http.put(`${this.basePath}/${id}`, this.key, data);
363
+ return result.record ?? result.data;
364
+ }
365
+ async patch(id, data, options = {}) {
366
+ const { merge = true } = options;
367
+ const result = await this.http.patch(
368
+ `${this.basePath}/${id}`,
369
+ this.key,
370
+ { ...data, _merge: merge }
371
+ );
372
+ return result.record ?? result.data;
373
+ }
374
+ async delete(id) {
375
+ await this.http.delete(`${this.basePath}/${id}`, this.key);
376
+ }
377
+ // ─── Batch Operations ─────────────────────────────────────────────────────
378
+ async batchCreate(items) {
379
+ const result = await this.http.post(
380
+ `${this.basePath}/batch`,
381
+ this.key,
382
+ { records: items }
383
+ );
384
+ return result.records;
385
+ }
386
+ async batchDelete(ids) {
387
+ const result = await this.http.post(
388
+ `${this.basePath}/batch/delete`,
389
+ this.key,
390
+ { ids }
391
+ );
392
+ return { deleted: result.deleted, failed: result.failed };
393
+ }
394
+ // ─── Querying ─────────────────────────────────────────────────────────────
395
+ async query(options = {}) {
396
+ const qs = buildQueryParams(options);
397
+ const result = await this.http.get(`${this.basePath}${qs}`, this.key);
398
+ return {
399
+ records: result.records,
400
+ total: result.total,
401
+ hasMore: result.hasMore,
402
+ nextCursor: result.nextCursor
403
+ };
404
+ }
405
+ async getAll(options = {}) {
406
+ const { records } = await this.query(options);
407
+ return records;
408
+ }
409
+ async count(filters = []) {
410
+ const result = await this.http.post(
411
+ `${this.basePath}/count`,
412
+ this.key,
413
+ { filters }
414
+ );
415
+ return result.count;
416
+ }
417
+ // ─── Version History ──────────────────────────────────────────────────────
418
+ async getHistory(id) {
419
+ const result = await this.http.get(`${this.basePath}/${id}/history`, this.key);
420
+ return result.history;
421
+ }
422
+ async restoreVersion(id, version) {
423
+ const result = await this.http.post(
424
+ `${this.basePath}/${id}/restore`,
425
+ this.key,
426
+ { version }
427
+ );
428
+ return result.record ?? result.data;
429
+ }
430
+ };
431
+
432
+ // src/analytics/client.ts
433
+ var AnalyticsClient = class {
434
+ constructor(http, bucketSecurityKey, bucketKey) {
435
+ assertSafeName(bucketKey, "bucketKey");
436
+ this.http = http;
437
+ this.bucketSecurityKey = bucketSecurityKey;
438
+ this.basePath = `/analytics/${bucketKey}`;
439
+ }
440
+ async run(query) {
441
+ const result = await this.http.post(
442
+ this.basePath,
443
+ this.bucketSecurityKey,
444
+ query
445
+ );
446
+ return result.data;
447
+ }
448
+ async count(opts = {}) {
449
+ return this.run({ queryType: "count", ...opts });
450
+ }
451
+ async distribution(opts) {
452
+ assertSafeName(opts.field, "field");
453
+ return this.run({ queryType: "distribution", ...opts });
454
+ }
455
+ async sum(opts) {
456
+ assertSafeName(opts.field, "field");
457
+ if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
458
+ return this.run({ queryType: "sum", ...opts });
459
+ }
460
+ async timeSeries(opts = {}) {
461
+ return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
462
+ }
463
+ async fieldTimeSeries(opts) {
464
+ assertSafeName(opts.field, "field");
465
+ return this.run({ queryType: "fieldTimeSeries", aggregation: "sum", granularity: "day", ...opts });
466
+ }
467
+ async topN(opts) {
468
+ assertSafeName(opts.field, "field");
469
+ if (opts.labelField) assertSafeName(opts.labelField, "labelField");
470
+ return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
471
+ }
472
+ async stats(opts) {
473
+ assertSafeName(opts.field, "field");
474
+ return this.run({ queryType: "stats", ...opts });
475
+ }
476
+ async records(opts = {}) {
477
+ if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
478
+ if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
479
+ return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
480
+ }
481
+ async multiMetric(opts) {
482
+ opts.metrics.forEach((m) => {
483
+ assertSafeName(m.field, "metric.field");
484
+ assertSafeName(m.name, "metric.name");
485
+ });
486
+ return this.run({ queryType: "multiMetric", ...opts });
487
+ }
488
+ async storageStats(opts = {}) {
489
+ return this.run({ queryType: "storageStats", ...opts });
490
+ }
491
+ async crossBucket(opts) {
492
+ assertSafeName(opts.field, "field");
493
+ opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
494
+ return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
495
+ }
496
+ async query(query) {
497
+ const data = await this.run(query);
498
+ return { queryType: query.queryType, data };
499
+ }
500
+ };
501
+
502
+ // src/storage/manager.ts
503
+ var StorageManager = class {
504
+ constructor(http, storageKey) {
505
+ this.basePath = "/storage";
506
+ this.http = http;
507
+ this.storageKey = storageKey;
508
+ }
509
+ /** Headers for all storage requests — uses X-Storage-Key, not X-Api-Key. */
510
+ get authHeaders() {
511
+ return { "X-Storage-Key": this.storageKey };
512
+ }
513
+ // ─── Upload: Simple (server-buffered) ────────────────────────────────────
514
+ /**
515
+ * Upload a file to storage in one step (server-buffered, up to 500 MB).
516
+ * For files >10 MB or when you need upload progress, use `getUploadUrl()` instead.
517
+ *
518
+ * @param data File data as a Blob, Buffer, Uint8Array, or ArrayBuffer.
519
+ * @param path Destination path in your storage (e.g. `"avatars/alice.jpg"`).
520
+ * @param options Upload options: isPublic, overwrite, mimeType.
521
+ *
522
+ * @example
523
+ * ```ts
524
+ * // Upload a public avatar
525
+ * const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
526
+ * console.log(result.publicUrl); // → https://...
527
+ *
528
+ * // Upload a private document
529
+ * const result = await storage.upload(pdfBuffer, 'docs/contract.pdf');
530
+ * console.log(result.downloadUrl); // → /storage/download/docs/contract.pdf
531
+ * ```
532
+ */
533
+ async upload(data, path, options = {}) {
534
+ const { isPublic = false, overwrite = false, mimeType } = options;
535
+ const mime = mimeType ?? guessMimeType(path);
536
+ const formData = new FormData();
537
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mime });
538
+ formData.append("file", blob, path.split("/").pop() ?? "file");
539
+ formData.append("path", path);
540
+ formData.append("mimeType", mime);
541
+ formData.append("isPublic", String(isPublic));
542
+ formData.append("overwrite", String(overwrite));
543
+ const result = await this.http.request(`${this.basePath}/upload`, {
544
+ method: "POST",
545
+ rawBody: formData,
546
+ headers: this.authHeaders
547
+ });
548
+ return {
549
+ path: result.path,
550
+ mimeType: result.mimeType,
551
+ size: result.size,
552
+ isPublic: result.isPublic,
553
+ publicUrl: result.publicUrl,
554
+ downloadUrl: result.downloadUrl
555
+ };
556
+ }
557
+ /**
558
+ * Upload raw JSON or plain text data as a file.
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * const result = await storage.uploadRaw(
563
+ * { config: { theme: 'dark' } },
564
+ * 'settings/user-config.json',
565
+ * { isPublic: false },
566
+ * );
567
+ * ```
568
+ */
569
+ async uploadRaw(data, path, options = {}) {
570
+ const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
571
+ const body = typeof data === "string" ? data : JSON.stringify(data);
572
+ const result = await this.http.request(`${this.basePath}/upload-raw`, {
573
+ method: "POST",
574
+ body: { path, data: body, mimeType, isPublic, overwrite },
575
+ headers: this.authHeaders
576
+ });
577
+ return {
578
+ path: result.path,
579
+ mimeType: result.mimeType,
580
+ size: result.size,
581
+ isPublic: result.isPublic,
582
+ publicUrl: result.publicUrl,
583
+ downloadUrl: result.downloadUrl
584
+ };
585
+ }
586
+ // ─── Upload: Direct-to-GCS (recommended for large files) ─────────────────
587
+ /**
588
+ * Step 1 of the recommended upload flow.
589
+ * Get a signed URL to upload directly to GCS from the client (supports progress).
590
+ *
591
+ * @example
592
+ * ```ts
593
+ * const { uploadUrl, path: confirmedPath } = await storage.getUploadUrl({
594
+ * path: 'videos/intro.mp4',
595
+ * mimeType: 'video/mp4',
596
+ * size: file.size,
597
+ * isPublic: true,
598
+ * });
599
+ *
600
+ * // Upload using XHR for progress tracking
601
+ * await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (pct) => {
602
+ * console.log(`${pct}% uploaded`);
603
+ * });
604
+ *
605
+ * // Step 3: confirm
606
+ * const result = await storage.confirmUpload({ path: confirmedPath, mimeType: 'video/mp4', isPublic: true });
607
+ * ```
608
+ */
609
+ async getUploadUrl(opts) {
610
+ const result = await this.http.request(`${this.basePath}/upload-url`, {
611
+ method: "POST",
612
+ body: { isPublic: false, overwrite: false, expiresInSeconds: 900, ...opts },
613
+ headers: this.authHeaders
614
+ });
615
+ return result;
616
+ }
617
+ /**
618
+ * Upload data directly to a signed GCS URL (no auth headers needed).
619
+ * Optionally tracks progress via a callback.
620
+ *
621
+ * @param signedUrl The URL returned by `getUploadUrl()`.
622
+ * @param data File data.
623
+ * @param mimeType Must match what was used in `getUploadUrl()`.
624
+ * @param onProgress Optional callback called with 0–100 progress percentage.
625
+ */
626
+ async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
627
+ await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
628
+ }
629
+ /**
630
+ * Step 3 of the recommended upload flow.
631
+ * Confirm a direct upload and register metadata on the server.
632
+ *
633
+ * @example
634
+ * ```ts
635
+ * const result = await storage.confirmUpload({
636
+ * path: 'videos/intro.mp4',
637
+ * mimeType: 'video/mp4',
638
+ * isPublic: true,
639
+ * });
640
+ * console.log(result.publicUrl);
641
+ * ```
642
+ */
643
+ async confirmUpload(opts) {
644
+ const result = await this.http.request(`${this.basePath}/confirm`, {
645
+ method: "POST",
646
+ body: { isPublic: false, ...opts },
647
+ headers: this.authHeaders
648
+ });
649
+ return {
650
+ path: result.path,
651
+ mimeType: result.mimeType,
652
+ size: result.size,
653
+ isPublic: result.isPublic,
654
+ publicUrl: result.publicUrl,
655
+ downloadUrl: result.downloadUrl
656
+ };
657
+ }
658
+ // ─── Batch Upload ─────────────────────────────────────────────────────────
659
+ /**
660
+ * Get signed upload URLs for multiple files at once.
661
+ *
662
+ * @example
663
+ * ```ts
664
+ * const { files } = await storage.getBatchUploadUrls([
665
+ * { path: 'images/photo1.jpg', mimeType: 'image/jpeg', size: 204800 },
666
+ * { path: 'images/photo2.jpg', mimeType: 'image/jpeg', size: 153600 },
667
+ * ]);
668
+ *
669
+ * // Upload each file and confirm
670
+ * for (const f of files) {
671
+ * await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
672
+ * await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
673
+ * }
674
+ * ```
675
+ */
676
+ async getBatchUploadUrls(files) {
677
+ const result = await this.http.request(
678
+ `${this.basePath}/batch-upload-urls`,
679
+ { method: "POST", body: { files }, headers: this.authHeaders }
680
+ );
681
+ return { files: result.files };
682
+ }
683
+ /**
684
+ * Confirm multiple direct uploads at once.
685
+ */
686
+ async batchConfirmUploads(items) {
687
+ const result = await this.http.request(
688
+ `${this.basePath}/batch-confirm`,
689
+ { method: "POST", body: { files: items }, headers: this.authHeaders }
690
+ );
691
+ return result.files.map((r) => ({
692
+ path: r.path,
693
+ mimeType: r.mimeType,
694
+ size: r.size,
695
+ isPublic: r.isPublic,
696
+ publicUrl: r.publicUrl,
697
+ downloadUrl: r.downloadUrl
698
+ }));
699
+ }
700
+ // ─── Download ─────────────────────────────────────────────────────────────
701
+ /**
702
+ * Download a private file as an ArrayBuffer.
703
+ * For public files, use the `publicUrl` directly — no SDK needed.
704
+ *
705
+ * @example
706
+ * ```ts
707
+ * const buffer = await storage.download('docs/contract.pdf');
708
+ * const blob = new Blob([buffer], { type: 'application/pdf' });
709
+ * // Open in browser:
710
+ * window.open(URL.createObjectURL(blob));
711
+ * ```
712
+ */
713
+ async download(path) {
714
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
715
+ return this.http.request(`${this.basePath}/download/${encoded}`, {
716
+ method: "GET",
717
+ headers: this.authHeaders
718
+ });
719
+ }
720
+ /**
721
+ * Download multiple files at once, returned as a JSON map of `{ path: base64 }`.
722
+ *
723
+ * @example
724
+ * ```ts
725
+ * const files = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
726
+ * ```
727
+ */
728
+ async batchDownload(paths) {
729
+ const result = await this.http.request(
730
+ `${this.basePath}/batch-download`,
731
+ { method: "POST", body: { paths }, headers: this.authHeaders }
732
+ );
733
+ return result.files;
734
+ }
735
+ // ─── List ─────────────────────────────────────────────────────────────────
736
+ /**
737
+ * List files and folders at a given path prefix.
738
+ *
739
+ * @example
740
+ * ```ts
741
+ * // List everything in the root
742
+ * const { files, folders } = await storage.list();
743
+ *
744
+ * // List a specific folder
745
+ * const { files, folders, hasMore, nextCursor } = await storage.list({
746
+ * prefix: 'avatars/',
747
+ * limit: 20,
748
+ * });
749
+ *
750
+ * // Next page
751
+ * const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
752
+ * ```
753
+ */
754
+ async list(opts = {}) {
755
+ const params = new URLSearchParams();
756
+ if (opts.prefix) params.set("prefix", opts.prefix);
757
+ if (opts.limit) params.set("limit", String(opts.limit));
758
+ if (opts.cursor) params.set("cursor", opts.cursor);
759
+ if (opts.recursive) params.set("recursive", "true");
760
+ const qs = params.toString() ? `?${params}` : "";
761
+ const result = await this.http.request(`${this.basePath}/list${qs}`, {
762
+ method: "GET",
763
+ headers: this.authHeaders
764
+ });
765
+ return {
766
+ files: result.files,
767
+ folders: result.folders,
768
+ hasMore: result.hasMore,
769
+ nextCursor: result.nextCursor
770
+ };
771
+ }
772
+ // ─── Metadata ─────────────────────────────────────────────────────────────
773
+ /**
774
+ * Get metadata for a file (size, MIME type, visibility, URLs).
775
+ *
776
+ * @example
777
+ * ```ts
778
+ * const meta = await storage.getMetadata('avatars/alice.jpg');
779
+ * console.log(meta.size, meta.isPublic, meta.publicUrl);
780
+ * ```
781
+ */
782
+ async getMetadata(path) {
783
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
784
+ const result = await this.http.request(
785
+ `${this.basePath}/metadata/${encoded}`,
786
+ { method: "GET", headers: this.authHeaders }
787
+ );
788
+ return {
789
+ path: result.path,
790
+ size: result.size,
791
+ mimeType: result.mimeType,
792
+ isPublic: result.isPublic,
793
+ publicUrl: result.publicUrl,
794
+ downloadUrl: result.downloadUrl,
795
+ createdAt: result.createdAt,
796
+ updatedAt: result.updatedAt
797
+ };
798
+ }
799
+ // ─── Signed URL (time-limited share) ─────────────────────────────────────
800
+ /**
801
+ * Generate a time-limited download URL for a private file.
802
+ * The URL can be shared externally without requiring an `X-Storage-Key`.
803
+ *
804
+ * > **Note:** Downloads via signed URLs bypass the server, so download stats
805
+ * > are NOT tracked. Use `downloadUrl` for tracked downloads.
806
+ *
807
+ * @param path Path to the file.
808
+ * @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
809
+ *
810
+ * @example
811
+ * ```ts
812
+ * const { signedUrl, expiresAt } = await storage.getSignedUrl('docs/invoice.pdf', 1800);
813
+ * // Share signedUrl with the recipient — it expires in 30 minutes.
814
+ * ```
815
+ */
816
+ async getSignedUrl(path, expiresIn = 3600) {
817
+ const result = await this.http.request(`${this.basePath}/signed-url`, {
818
+ method: "POST",
819
+ body: { path, expiresIn },
820
+ headers: this.authHeaders
821
+ });
822
+ return {
823
+ signedUrl: result.signedUrl,
824
+ expiresAt: result.expiresAt,
825
+ expiresIn: result.expiresIn,
826
+ path: result.path
827
+ };
828
+ }
829
+ // ─── Visibility ───────────────────────────────────────────────────────────
830
+ /**
831
+ * Change a file's visibility between public and private after upload.
832
+ *
833
+ * @example
834
+ * ```ts
835
+ * // Make a file public
836
+ * const result = await storage.setVisibility('avatars/alice.jpg', true);
837
+ * console.log(result.publicUrl); // CDN URL
838
+ *
839
+ * // Make a file private
840
+ * const result = await storage.setVisibility('avatars/alice.jpg', false);
841
+ * console.log(result.downloadUrl); // Auth-required URL
842
+ * ```
843
+ */
844
+ async setVisibility(path, isPublic) {
845
+ const result = await this.http.request(`${this.basePath}/visibility`, {
846
+ method: "PATCH",
847
+ body: { path, isPublic },
848
+ headers: this.authHeaders
849
+ });
850
+ return {
851
+ path: result.path,
852
+ isPublic: result.isPublic,
853
+ publicUrl: result.publicUrl,
854
+ downloadUrl: result.downloadUrl
855
+ };
856
+ }
857
+ // ─── Folder Operations ────────────────────────────────────────────────────
858
+ /**
859
+ * Create a folder (a GCS prefix placeholder).
860
+ *
861
+ * @example
862
+ * ```ts
863
+ * await storage.createFolder('uploads/2025/');
864
+ * ```
865
+ */
866
+ async createFolder(path) {
867
+ const result = await this.http.request(
868
+ `${this.basePath}/folder`,
869
+ { method: "POST", body: { path }, headers: this.authHeaders }
870
+ );
871
+ return { path: result.path };
872
+ }
873
+ // ─── File Operations ──────────────────────────────────────────────────────
874
+ /**
875
+ * Delete a single file.
876
+ *
877
+ * @example
878
+ * ```ts
879
+ * await storage.deleteFile('avatars/old-avatar.jpg');
880
+ * ```
881
+ */
882
+ async deleteFile(path) {
883
+ await this.http.request(`${this.basePath}/file`, {
884
+ method: "DELETE",
885
+ body: { path },
886
+ headers: this.authHeaders
887
+ });
888
+ }
889
+ /**
890
+ * Delete a folder and all its contents recursively.
891
+ *
892
+ * @example
893
+ * ```ts
894
+ * await storage.deleteFolder('temp/');
895
+ * ```
896
+ */
897
+ async deleteFolder(path) {
898
+ await this.http.request(`${this.basePath}/folder`, {
899
+ method: "DELETE",
900
+ body: { path },
901
+ headers: this.authHeaders
902
+ });
903
+ }
904
+ /**
905
+ * Move or rename a file.
906
+ *
907
+ * @example
908
+ * ```ts
909
+ * // Rename
910
+ * await storage.move('docs/draft.pdf', 'docs/final.pdf');
911
+ * // Move to a different folder
912
+ * await storage.move('inbox/report.xlsx', 'archive/2025/report.xlsx');
913
+ * ```
914
+ */
915
+ async move(from, to) {
916
+ const result = await this.http.request(
917
+ `${this.basePath}/move`,
918
+ { method: "POST", body: { from, to }, headers: this.authHeaders }
919
+ );
920
+ return { from: result.from, to: result.to };
921
+ }
922
+ /**
923
+ * Copy a file to a new path.
924
+ *
925
+ * @example
926
+ * ```ts
927
+ * await storage.copy('templates/base.html', 'sites/my-site/index.html');
928
+ * ```
929
+ */
930
+ async copy(from, to) {
931
+ const result = await this.http.request(
932
+ `${this.basePath}/copy`,
933
+ { method: "POST", body: { from, to }, headers: this.authHeaders }
934
+ );
935
+ return { from: result.from, to: result.to };
936
+ }
937
+ // ─── Stats ────────────────────────────────────────────────────────────────
938
+ /**
939
+ * Get storage statistics for your key: total files, bytes, operation counts.
940
+ *
941
+ * @example
942
+ * ```ts
943
+ * const stats = await storage.getStats();
944
+ * console.log(`${stats.totalFiles} files, ${(stats.totalBytes / 1e6).toFixed(1)} MB`);
945
+ * ```
946
+ */
947
+ async getStats() {
948
+ const result = await this.http.request(`${this.basePath}/stats`, {
949
+ method: "GET",
950
+ headers: this.authHeaders
951
+ });
952
+ return result.stats;
953
+ }
954
+ // ─── Info (no auth) ───────────────────────────────────────────────────────
955
+ /**
956
+ * Ping the storage service. No authentication required.
957
+ *
958
+ * @example
959
+ * ```ts
960
+ * const info = await storage.info();
961
+ * // → { ok: true, storageRoot: 'hydrous-storage' }
962
+ * ```
963
+ */
964
+ async info() {
965
+ return this.http.get(`${this.basePath}/info`);
966
+ }
967
+ };
968
+
969
+ // src/storage/scoped.ts
970
+ var ScopedStorage = class _ScopedStorage {
971
+ constructor(manager, prefix) {
972
+ this.manager = manager;
973
+ this.prefix = prefix.replace(/\/+$/, "") + "/";
974
+ }
975
+ scopedPath(userPath) {
976
+ return `${this.prefix}${userPath.replace(/^\/+/, "")}`;
977
+ }
978
+ /** Upload a file within the scoped folder. */
979
+ upload(data, path, options) {
980
+ return this.manager.upload(data, this.scopedPath(path), options);
981
+ }
982
+ /** Upload raw JSON or text within the scoped folder. */
983
+ uploadRaw(data, path, options) {
984
+ return this.manager.uploadRaw(data, this.scopedPath(path), options);
985
+ }
986
+ /** Get a signed upload URL for a file within the scoped folder. */
987
+ getUploadUrl(opts) {
988
+ return this.manager.getUploadUrl({ ...opts, path: this.scopedPath(opts.path) });
989
+ }
990
+ /** Confirm a direct upload within the scoped folder. */
991
+ confirmUpload(opts) {
992
+ return this.manager.confirmUpload({ ...opts, path: this.scopedPath(opts.path) });
993
+ }
994
+ /** Download a file within the scoped folder. */
995
+ download(path) {
996
+ return this.manager.download(this.scopedPath(path));
997
+ }
998
+ /**
999
+ * List files within the scoped folder.
1000
+ * `prefix` in options is relative to the scope.
1001
+ */
1002
+ list(opts = {}) {
1003
+ const scopedOpts = {
1004
+ ...opts,
1005
+ prefix: this.scopedPath(opts.prefix ?? "")
1006
+ };
1007
+ return this.manager.list(scopedOpts);
1008
+ }
1009
+ /** Get metadata for a file within the scoped folder. */
1010
+ getMetadata(path) {
1011
+ return this.manager.getMetadata(this.scopedPath(path));
1012
+ }
1013
+ /** Get a time-limited signed URL for a file within the scoped folder. */
1014
+ getSignedUrl(path, expiresIn) {
1015
+ return this.manager.getSignedUrl(this.scopedPath(path), expiresIn);
1016
+ }
1017
+ /** Change visibility of a file within the scoped folder. */
1018
+ setVisibility(path, isPublic) {
1019
+ return this.manager.setVisibility(this.scopedPath(path), isPublic);
1020
+ }
1021
+ /** Delete a file within the scoped folder. */
1022
+ deleteFile(path) {
1023
+ return this.manager.deleteFile(this.scopedPath(path));
1024
+ }
1025
+ /** Delete a sub-folder within the scoped folder. */
1026
+ deleteFolder(path) {
1027
+ return this.manager.deleteFolder(this.scopedPath(path));
1028
+ }
1029
+ /** Move a file within the scoped folder. */
1030
+ move(from, to) {
1031
+ return this.manager.move(this.scopedPath(from), this.scopedPath(to));
1032
+ }
1033
+ /** Copy a file within the scoped folder. */
1034
+ copy(from, to) {
1035
+ return this.manager.copy(this.scopedPath(from), this.scopedPath(to));
1036
+ }
1037
+ /** Create a sub-folder within the scoped folder. */
1038
+ createFolder(path) {
1039
+ return this.manager.createFolder(this.scopedPath(path));
1040
+ }
1041
+ /**
1042
+ * Create a further-scoped instance nested within this scope.
1043
+ *
1044
+ * @example
1045
+ * ```ts
1046
+ * const uploads = db.storage.scope('user-uploads');
1047
+ * const images = uploads.scope('images'); // → "user-uploads/images/"
1048
+ * ```
1049
+ */
1050
+ scope(subPrefix) {
1051
+ return new _ScopedStorage(this.manager, this.scopedPath(subPrefix));
1052
+ }
1053
+ };
1054
+
1055
+ // src/client.ts
1056
+ var HydrousClient = class {
1057
+ constructor(config) {
1058
+ this._recordsCache = /* @__PURE__ */ new Map();
1059
+ this._authCache = /* @__PURE__ */ new Map();
1060
+ this._analyticsCache = /* @__PURE__ */ new Map();
1061
+ this._storageCache = /* @__PURE__ */ new Map();
1062
+ if (!config.authKey) {
1063
+ throw new Error("[HydrousDB] authKey is required. Get yours from https://hydrousdb.com/dashboard.");
1064
+ }
1065
+ if (!config.bucketSecurityKey) {
1066
+ throw new Error("[HydrousDB] bucketSecurityKey is required. Get yours from https://hydrousdb.com/dashboard.");
1067
+ }
1068
+ if (!config.storageKeys || Object.keys(config.storageKeys).length === 0) {
1069
+ throw new Error("[HydrousDB] storageKeys is required. Define at least one storage key from https://hydrousdb.com/dashboard.");
1070
+ }
1071
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1072
+ this.http = new HttpClient(baseUrl);
1073
+ this.authKey_ = config.authKey;
1074
+ this.bucketSecurityKey_ = config.bucketSecurityKey;
1075
+ this.storageKeys_ = config.storageKeys;
1076
+ }
1077
+ // ─── Records ─────────────────────────────────────────────────────────────
1078
+ /**
1079
+ * Get a typed records client for the named bucket.
1080
+ * Uses your `bucketSecurityKey` automatically.
1081
+ *
1082
+ * @example
1083
+ * ```ts
1084
+ * interface Post { title: string; published: boolean }
1085
+ * const posts = db.records<Post>('blog-posts');
1086
+ * const post = await posts.create({ title: 'Hello', published: false });
1087
+ * ```
1088
+ */
1089
+ records(bucketKey) {
1090
+ if (!this._recordsCache.has(bucketKey)) {
1091
+ this._recordsCache.set(
1092
+ bucketKey,
1093
+ new RecordsClient(this.http, this.bucketSecurityKey_, bucketKey)
1094
+ );
1095
+ }
1096
+ return this._recordsCache.get(bucketKey);
1097
+ }
1098
+ // ─── Auth ─────────────────────────────────────────────────────────────────
1099
+ /**
1100
+ * Get an auth client for the named user bucket.
1101
+ * Uses your `authKey` automatically.
1102
+ *
1103
+ * @example
1104
+ * ```ts
1105
+ * const auth = db.auth('app-users');
1106
+ * const { user, session } = await auth.login({ email: '…', password: '…' });
1107
+ * ```
1108
+ */
1109
+ auth(bucketKey) {
1110
+ if (!this._authCache.has(bucketKey)) {
1111
+ this._authCache.set(bucketKey, new AuthClient(this.http, this.authKey_, bucketKey));
1112
+ }
1113
+ return this._authCache.get(bucketKey);
1114
+ }
1115
+ // ─── Analytics ────────────────────────────────────────────────────────────
1116
+ /**
1117
+ * Get an analytics client for the named bucket.
1118
+ * Uses your `bucketSecurityKey` automatically.
1119
+ *
1120
+ * @example
1121
+ * ```ts
1122
+ * const analytics = db.analytics('orders');
1123
+ * const { count } = await analytics.count();
1124
+ * ```
1125
+ */
1126
+ analytics(bucketKey) {
1127
+ if (!this._analyticsCache.has(bucketKey)) {
1128
+ this._analyticsCache.set(
1129
+ bucketKey,
1130
+ new AnalyticsClient(this.http, this.bucketSecurityKey_, bucketKey)
1131
+ );
1132
+ }
1133
+ return this._analyticsCache.get(bucketKey);
1134
+ }
1135
+ // ─── Storage ──────────────────────────────────────────────────────────────
1136
+ /**
1137
+ * Get a storage manager for the named storage key.
1138
+ * The name must match a key you defined in `storageKeys` when calling `createClient`.
1139
+ * Uses the corresponding `ssk_…` key automatically via `X-Storage-Key` header.
1140
+ *
1141
+ * @param keyName The name of the storage key (e.g. `"avatars"`, `"documents"`, `"main"`).
1142
+ *
1143
+ * @example
1144
+ * ```ts
1145
+ * const avatars = db.storage('avatars');
1146
+ * const documents = db.storage('documents');
1147
+ *
1148
+ * // Upload to avatars bucket
1149
+ * await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
1150
+ *
1151
+ * // Scope to a sub-folder
1152
+ * const userDocs = db.storage('documents').scope(`users/${userId}`);
1153
+ * await userDocs.upload(pdfBuffer, 'contract.pdf');
1154
+ * ```
1155
+ */
1156
+ storage(keyName) {
1157
+ const ssk = this.storageKeys_[keyName];
1158
+ if (!ssk) {
1159
+ const available = Object.keys(this.storageKeys_).join(", ");
1160
+ throw new Error(
1161
+ `[HydrousDB] Unknown storage key name "${keyName}". Available keys: ${available}. Add it to storageKeys in your createClient() config.`
1162
+ );
1163
+ }
1164
+ if (!this._storageCache.has(keyName)) {
1165
+ this._storageCache.set(keyName, new StorageManager(this.http, ssk));
1166
+ }
1167
+ const mgr = this._storageCache.get(keyName);
1168
+ const extended = mgr;
1169
+ if (!extended.scope) {
1170
+ extended.scope = (prefix) => new ScopedStorage(mgr, prefix);
1171
+ }
1172
+ return extended;
1173
+ }
1174
+ };
1175
+ function createClient(config) {
1176
+ return new HydrousClient(config);
1177
+ }
1178
+
1179
+ exports.AnalyticsClient = AnalyticsClient;
1180
+ exports.AnalyticsError = AnalyticsError;
1181
+ exports.AuthClient = AuthClient;
1182
+ exports.AuthError = AuthError;
1183
+ exports.HydrousClient = HydrousClient;
1184
+ exports.HydrousError = HydrousError;
1185
+ exports.NetworkError = NetworkError;
1186
+ exports.RecordError = RecordError;
1187
+ exports.RecordsClient = RecordsClient;
1188
+ exports.ScopedStorage = ScopedStorage;
1189
+ exports.StorageError = StorageError;
1190
+ exports.StorageManager = StorageManager;
1191
+ exports.ValidationError = ValidationError;
1192
+ exports.createClient = createClient;
1193
+ //# sourceMappingURL=index.cjs.map
1194
+ //# sourceMappingURL=index.cjs.map