hydrousdb 3.0.2 → 3.5.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.mjs ADDED
@@ -0,0 +1,1581 @@
1
+ // src/utils/errors.ts
2
+ var HydrousError = class extends Error {
3
+ constructor(message, code, status, requestId, details) {
4
+ super(message);
5
+ this.name = "HydrousError";
6
+ this.code = code;
7
+ this.status = status;
8
+ this.requestId = requestId;
9
+ this.details = details;
10
+ Object.setPrototypeOf(this, new.target.prototype);
11
+ }
12
+ toString() {
13
+ return `HydrousError [${this.code}]: ${this.message}`;
14
+ }
15
+ };
16
+ var AuthError = class extends HydrousError {
17
+ constructor(message, code, status, requestId, details) {
18
+ super(message, code, status, requestId, details);
19
+ this.name = "AuthError";
20
+ }
21
+ };
22
+ var RecordError = class extends HydrousError {
23
+ constructor(message, code, status, requestId, details) {
24
+ super(message, code, status, requestId, details);
25
+ this.name = "RecordError";
26
+ }
27
+ };
28
+ var StorageError = class extends HydrousError {
29
+ constructor(message, code, status, requestId) {
30
+ super(message, code, status, requestId);
31
+ this.name = "StorageError";
32
+ }
33
+ };
34
+ var AnalyticsError = class extends HydrousError {
35
+ constructor(message, code, status, requestId) {
36
+ super(message, code, status, requestId);
37
+ this.name = "AnalyticsError";
38
+ }
39
+ };
40
+ var ValidationError = class extends HydrousError {
41
+ constructor(message, details) {
42
+ super(message, "VALIDATION_ERROR", 400, void 0, details);
43
+ this.name = "ValidationError";
44
+ }
45
+ };
46
+ var NetworkError = class extends HydrousError {
47
+ constructor(message, cause) {
48
+ super(message, "NETWORK_ERROR");
49
+ this.name = "NetworkError";
50
+ if (cause !== void 0) this.cause = cause;
51
+ }
52
+ };
53
+
54
+ // src/utils/http.ts
55
+ var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
56
+ var HttpClient = class {
57
+ constructor(baseUrl) {
58
+ this.baseUrl = baseUrl.replace(/\/$/, "");
59
+ }
60
+ // ─── Core request ──────────────────────────────────────────────────────────
61
+ async request(path, options) {
62
+ const url = `${this.baseUrl}${path}`;
63
+ const headers = { ...options.headers };
64
+ let body;
65
+ if (options.rawBody !== void 0) {
66
+ body = options.rawBody;
67
+ } else if (options.body !== void 0) {
68
+ headers["Content-Type"] = "application/json";
69
+ body = JSON.stringify(options.body);
70
+ }
71
+ let response;
72
+ try {
73
+ response = await fetch(url, {
74
+ method: options.method,
75
+ headers,
76
+ body
77
+ });
78
+ } catch (err) {
79
+ throw new NetworkError(
80
+ `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
81
+ err
82
+ );
83
+ }
84
+ const contentType = response.headers.get("content-type") ?? "";
85
+ if (!contentType.includes("application/json")) {
86
+ if (!response.ok) {
87
+ throw new HydrousError(
88
+ `Request failed with status ${response.status}`,
89
+ "REQUEST_FAILED",
90
+ response.status
91
+ );
92
+ }
93
+ return response.arrayBuffer();
94
+ }
95
+ let data;
96
+ try {
97
+ data = await response.json();
98
+ } catch {
99
+ throw new HydrousError("Failed to parse response JSON", "PARSE_ERROR", response.status);
100
+ }
101
+ if (!response.ok) {
102
+ throw new HydrousError(
103
+ data["error"] || data["message"] || `Request failed with status ${response.status}`,
104
+ data["code"] || "REQUEST_FAILED",
105
+ response.status,
106
+ data["requestId"],
107
+ data["details"]
108
+ );
109
+ }
110
+ return data;
111
+ }
112
+ // ─── Convenience wrappers ─────────────────────────────────────────────────
113
+ get(path, apiKey, extraHeaders) {
114
+ return this.request(path, {
115
+ method: "GET",
116
+ headers: apiKey ? { "X-Api-Key": apiKey, ...extraHeaders } : { ...extraHeaders }
117
+ });
118
+ }
119
+ post(path, apiKey, body) {
120
+ return this.request(path, {
121
+ method: "POST",
122
+ body,
123
+ headers: { "X-Api-Key": apiKey }
124
+ });
125
+ }
126
+ put(path, apiKey, body) {
127
+ return this.request(path, {
128
+ method: "PUT",
129
+ body,
130
+ headers: { "X-Api-Key": apiKey }
131
+ });
132
+ }
133
+ patch(path, apiKey, body) {
134
+ return this.request(path, {
135
+ method: "PATCH",
136
+ body,
137
+ headers: { "X-Api-Key": apiKey }
138
+ });
139
+ }
140
+ delete(path, apiKey, body) {
141
+ return this.request(path, {
142
+ method: "DELETE",
143
+ body,
144
+ headers: { "X-Api-Key": apiKey }
145
+ });
146
+ }
147
+ /**
148
+ * PUT directly to a signed GCS URL.
149
+ * Uses XHR in browsers for onprogress support; fetch in Node.
150
+ */
151
+ async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
152
+ const body = data instanceof Blob ? data : data instanceof Uint8Array ? data.buffer : data;
153
+ if (typeof XMLHttpRequest !== "undefined" && typeof onProgress === "function") {
154
+ await new Promise((resolve, reject) => {
155
+ const xhr = new XMLHttpRequest();
156
+ xhr.upload.onprogress = (e) => {
157
+ if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
158
+ };
159
+ xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`GCS upload failed: ${xhr.status}`));
160
+ xhr.onerror = () => reject(new Error("GCS upload network error"));
161
+ xhr.open("PUT", signedUrl);
162
+ xhr.setRequestHeader("Content-Type", mimeType);
163
+ xhr.send(body);
164
+ });
165
+ } else {
166
+ const res = await fetch(signedUrl, {
167
+ method: "PUT",
168
+ headers: { "Content-Type": mimeType },
169
+ body
170
+ });
171
+ if (!res.ok) {
172
+ throw new HydrousError(`GCS upload failed: ${res.status}`, "GCS_UPLOAD_FAILED", res.status);
173
+ }
174
+ }
175
+ }
176
+ };
177
+
178
+ // src/routes.ts
179
+ var RECORDS = {
180
+ /** GET|POST|PATCH|DELETE|HEAD /api/:bucketKey */
181
+ bucket: (bucketKey) => `/api/${bucketKey}`,
182
+ /** POST /api/:bucketKey/batch/insert */
183
+ batchInsert: (bucketKey) => `/api/${bucketKey}/batch/insert`,
184
+ /** POST /api/:bucketKey/batch/update */
185
+ batchUpdate: (bucketKey) => `/api/${bucketKey}/batch/update`,
186
+ /** POST /api/:bucketKey/batch/delete */
187
+ batchDelete: (bucketKey) => `/api/${bucketKey}/batch/delete`
188
+ };
189
+ var ANALYTICS = {
190
+ /** POST /api/analytics/:bucketKey */
191
+ query: (bucketKey) => `/api/analytics/${bucketKey}`
192
+ };
193
+ var AUTH = {
194
+ /** POST /api/auth/signup body: { email, password, fullName?, ...extra } */
195
+ signup: "/api/auth/signup",
196
+ /** POST /api/auth/signin body: { email, password } */
197
+ signin: "/api/auth/signin",
198
+ /** POST /api/auth/signout body: { sessionId, allDevices? } */
199
+ signout: "/api/auth/signout",
200
+ /** POST /api/auth/session/validate body: { sessionId } */
201
+ sessionValidate: "/api/auth/session/validate",
202
+ /** POST /api/auth/session/refresh body: { refreshToken } */
203
+ sessionRefresh: "/api/auth/session/refresh",
204
+ /** GET /api/auth/user?userId=... */
205
+ getUser: "/api/auth/user",
206
+ /** GET /api/auth/users?limit=&cursor= */
207
+ listUsers: "/api/auth/users",
208
+ /** PATCH /api/auth/user body: { sessionId, userId, updates: {...} } */
209
+ updateUser: "/api/auth/user",
210
+ /** DELETE /api/auth/user?userId=... body: { sessionId } */
211
+ deleteUser: "/api/auth/user",
212
+ /** DELETE /api/auth/user/hard?userId=... body: { sessionId } */
213
+ hardDeleteUser: "/api/auth/user/hard",
214
+ /** DELETE /api/auth/users/bulk body: { userIds, hard?, sessionId } */
215
+ bulkDeleteUsers: "/api/auth/users/bulk",
216
+ /** POST /api/auth/password/change body: { sessionId, userId, oldPassword, newPassword } */
217
+ passwordChange: "/api/auth/password/change",
218
+ /** POST /api/auth/password/reset/request body: { email } */
219
+ passwordResetRequest: "/api/auth/password/reset/request",
220
+ /** POST /api/auth/password/reset/confirm body: { resetToken, newPassword } */
221
+ passwordResetConfirm: "/api/auth/password/reset/confirm",
222
+ /** POST /api/auth/email/verify/request body: { userId } */
223
+ emailVerifyRequest: "/api/auth/email/verify/request",
224
+ /** POST /api/auth/email/verify/confirm body: { verifyToken } */
225
+ emailVerifyConfirm: "/api/auth/email/verify/confirm",
226
+ /** POST /api/auth/account/lock body: { sessionId, userId, duration? } */
227
+ accountLock: "/api/auth/account/lock",
228
+ /** POST /api/auth/account/unlock body: { sessionId, userId } */
229
+ accountUnlock: "/api/auth/account/unlock"
230
+ };
231
+ var STORAGE = {
232
+ /** GET /storage/info — no auth required */
233
+ info: "/storage/info",
234
+ /** GET /storage/public/:fullScopedPath — no auth required */
235
+ publicFile: (fullScopedPath) => `/storage/public/${fullScopedPath}`,
236
+ /** POST /storage/upload-url body: { path, mimeType, size, isPublic?, overwrite?, expiresIn? } */
237
+ uploadUrl: "/storage/upload-url",
238
+ /** POST /storage/batch-upload-urls body: { files: [...], expiresIn? } */
239
+ batchUploadUrls: "/storage/batch-upload-urls",
240
+ /** POST /storage/confirm body: { path, mimeType, isPublic? } */
241
+ confirm: "/storage/confirm",
242
+ /** POST /storage/batch-confirm body: { files: [...] } */
243
+ batchConfirm: "/storage/batch-confirm",
244
+ /** POST /storage/upload multipart/form-data: file, path, mimeType, isPublic, overwrite */
245
+ upload: "/storage/upload",
246
+ /** POST /storage/upload-raw body: { path, content, mimeType?, isPublic?, overwrite? } */
247
+ uploadRaw: "/storage/upload-raw",
248
+ /** GET /storage/list?prefix=&limit=&cursor= */
249
+ list: "/storage/list",
250
+ /** GET /storage/download/:path — requires X-Storage-Key */
251
+ download: (filePath) => `/storage/download/${filePath}`,
252
+ /** POST /storage/batch-download body: { paths: [...], concurrency? } */
253
+ batchDownload: "/storage/batch-download",
254
+ /** GET /storage/metadata/:path */
255
+ metadata: (filePath) => `/storage/metadata/${filePath}`,
256
+ /** POST /storage/signed-url body: { path, expiresIn? } */
257
+ signedUrl: "/storage/signed-url",
258
+ /** PATCH /storage/visibility body: { path, isPublic } */
259
+ visibility: "/storage/visibility",
260
+ /** POST /storage/folder body: { path } */
261
+ folder: "/storage/folder",
262
+ /** DELETE /storage/file body: { path } */
263
+ file: "/storage/file",
264
+ /** DELETE /storage/folder body: { path } */
265
+ folderDelete: "/storage/folder",
266
+ /** POST /storage/move body: { from, to } */
267
+ move: "/storage/move",
268
+ /** POST /storage/copy body: { from, to } */
269
+ copy: "/storage/copy",
270
+ /** GET /storage/stats */
271
+ stats: "/storage/stats"
272
+ };
273
+
274
+ // src/auth/client.ts
275
+ var AuthClient = class {
276
+ constructor(http, authKey) {
277
+ this.http = http;
278
+ this.authKey = authKey;
279
+ }
280
+ post(path, body) {
281
+ return this.http.post(path, this.authKey, body);
282
+ }
283
+ get(path, query, extraHeaders) {
284
+ const qs = query && Object.keys(query).length ? "?" + new URLSearchParams(query).toString() : "";
285
+ return this.http.get(`${path}${qs}`, this.authKey, extraHeaders);
286
+ }
287
+ patch(path, body) {
288
+ return this.http.patch(path, this.authKey, body);
289
+ }
290
+ // ─── Signup / Login / Logout ──────────────────────────────────────────────
291
+ /**
292
+ * Register a new user account.
293
+ *
294
+ * Server: POST /api/auth/signup
295
+ * Body: { email, password, fullName?, ...extraData }
296
+ */
297
+ async signup(options) {
298
+ const result = await this.post(AUTH.signup, options);
299
+ const user = result.data;
300
+ return this._buildAuthResult(user, result.session);
301
+ }
302
+ /**
303
+ * Sign in with email + password.
304
+ *
305
+ * Server: POST /api/auth/signin (NOT /login)
306
+ * Body: { email, password }
307
+ */
308
+ async login(options) {
309
+ const result = await this.post(AUTH.signin, options);
310
+ const user = result.data;
311
+ return this._buildAuthResult(user, result.session);
312
+ }
313
+ /**
314
+ * Sign out — revoke a session (or all sessions with `allDevices: true`).
315
+ *
316
+ * Server: POST /api/auth/signout
317
+ * Body: { sessionId, allDevices? }
318
+ */
319
+ async logout(options) {
320
+ await this.post(AUTH.signout, options);
321
+ }
322
+ // ─── Session Management ───────────────────────────────────────────────────
323
+ /**
324
+ * Validate an existing session and retrieve the current user.
325
+ *
326
+ * Server: POST /api/auth/session/validate
327
+ * Body: { sessionId }
328
+ */
329
+ async validateSession(sessionId) {
330
+ const result = await this.post(
331
+ AUTH.sessionValidate,
332
+ { sessionId }
333
+ );
334
+ return { user: result.data, session: result.session };
335
+ }
336
+ /**
337
+ * Rotate a refresh token to get a new session.
338
+ *
339
+ * Server: POST /api/auth/session/refresh
340
+ * Body: { refreshToken }
341
+ */
342
+ async refreshSession(refreshToken) {
343
+ const result = await this.post(
344
+ AUTH.sessionRefresh,
345
+ { refreshToken }
346
+ );
347
+ const s = result.session;
348
+ return {
349
+ sessionId: s.sessionId,
350
+ userId: result.data?.id ?? "",
351
+ bucketId: "",
352
+ createdAt: Date.now(),
353
+ expiresAt: s.expiresAt,
354
+ refreshToken: s.refreshToken,
355
+ refreshExpiresAt: s.expiresAt
356
+ };
357
+ }
358
+ // ─── User Profile ─────────────────────────────────────────────────────────
359
+ /**
360
+ * Fetch a user by ID.
361
+ *
362
+ * Server: GET /api/auth/user?userId=:userId
363
+ */
364
+ async getUser(userId) {
365
+ const result = await this.get(AUTH.getUser, { userId });
366
+ return result.data;
367
+ }
368
+ /**
369
+ * Update a user's profile fields.
370
+ * Regular users can only update themselves; admins can update any user.
371
+ *
372
+ * Server: PATCH /api/auth/user
373
+ * Body: { sessionId, userId, updates: { ...fields } }
374
+ */
375
+ async updateUser(options) {
376
+ const { sessionId, userId, updates } = options;
377
+ const result = await this.patch(
378
+ AUTH.updateUser,
379
+ { sessionId, userId, updates }
380
+ );
381
+ return result.data;
382
+ }
383
+ /**
384
+ * Soft-delete a user account.
385
+ * Regular users can only delete themselves; admins can delete any user.
386
+ *
387
+ * Server: DELETE /api/auth/user?userId=:userId
388
+ * Body: { sessionId }
389
+ */
390
+ async deleteUser(sessionId, userId) {
391
+ await this.http.request(
392
+ `${AUTH.deleteUser}?userId=${encodeURIComponent(userId)}`,
393
+ {
394
+ method: "DELETE",
395
+ body: { sessionId },
396
+ headers: { "X-Api-Key": this.authKey }
397
+ }
398
+ );
399
+ }
400
+ // ─── Admin Operations ─────────────────────────────────────────────────────
401
+ /**
402
+ * List all users (paginated). Admin only.
403
+ *
404
+ * Server: GET /api/auth/users?limit=&cursor=
405
+ * The sessionId is passed via the X-Session-Id header (GET body is unreliable).
406
+ */
407
+ async listUsers(options) {
408
+ const { sessionId, limit = 50, cursor } = options;
409
+ const params = { limit: String(limit) };
410
+ if (cursor) params["cursor"] = cursor;
411
+ const result = await this.http.request(
412
+ `${AUTH.listUsers}?${new URLSearchParams(params)}`,
413
+ {
414
+ method: "GET",
415
+ headers: { "X-Api-Key": this.authKey, "X-Session-Id": sessionId }
416
+ }
417
+ );
418
+ return {
419
+ users: result.data ?? [],
420
+ hasMore: result.meta?.hasMore ?? result.hasMore ?? false,
421
+ nextCursor: result.meta?.nextCursor ?? result.nextCursor ?? null
422
+ };
423
+ }
424
+ /**
425
+ * Permanently delete a user (hard delete — cannot be undone).
426
+ * Admin only. Charged at 1 HC per deletion.
427
+ *
428
+ * Server: DELETE /api/auth/user/hard?userId=:userId
429
+ * Body: { sessionId }
430
+ */
431
+ async hardDeleteUser(sessionId, userId) {
432
+ await this.http.request(
433
+ `${AUTH.hardDeleteUser}?userId=${encodeURIComponent(userId)}`,
434
+ {
435
+ method: "DELETE",
436
+ body: { sessionId },
437
+ headers: { "X-Api-Key": this.authKey }
438
+ }
439
+ );
440
+ }
441
+ /**
442
+ * Delete multiple users at once (soft or hard). Admin only.
443
+ *
444
+ * Server: DELETE /api/auth/users/bulk
445
+ * Body: { userIds, hard?, sessionId }
446
+ */
447
+ async bulkDeleteUsers(options) {
448
+ const result = await this.http.request(
449
+ AUTH.bulkDeleteUsers,
450
+ {
451
+ method: "DELETE",
452
+ body: {
453
+ userIds: options.userIds,
454
+ hard: options.hard ?? false,
455
+ sessionId: options.sessionId
456
+ },
457
+ headers: { "X-Api-Key": this.authKey }
458
+ }
459
+ );
460
+ return { succeeded: result.meta.succeeded, failed: result.meta.failed };
461
+ }
462
+ /**
463
+ * Lock a user account for a specified duration. Admin only.
464
+ *
465
+ * Server: POST /api/auth/account/lock
466
+ * Body: { sessionId, userId, duration? } — duration in ms, default 15 min
467
+ */
468
+ async lockAccount(options) {
469
+ const result = await this.post(AUTH.accountLock, options);
470
+ return result.data;
471
+ }
472
+ /**
473
+ * Unlock a locked user account. Admin only.
474
+ *
475
+ * Server: POST /api/auth/account/unlock
476
+ * Body: { sessionId, userId }
477
+ */
478
+ async unlockAccount(sessionId, userId) {
479
+ await this.post(AUTH.accountUnlock, { sessionId, userId });
480
+ }
481
+ // ─── Password Management ──────────────────────────────────────────────────
482
+ /**
483
+ * Change a user's password. Requires both a valid session AND the old password.
484
+ *
485
+ * Server: POST /api/auth/password/change
486
+ * Body: { sessionId, userId, oldPassword, newPassword }
487
+ */
488
+ async changePassword(options) {
489
+ await this.post(AUTH.passwordChange, {
490
+ sessionId: options.sessionId,
491
+ userId: options.userId,
492
+ oldPassword: options.currentPassword,
493
+ // server field is `oldPassword`
494
+ newPassword: options.newPassword
495
+ });
496
+ }
497
+ /**
498
+ * Request a password reset email.
499
+ * Always returns success regardless of whether the email exists (prevents enumeration).
500
+ *
501
+ * Server: POST /api/auth/password/reset/request
502
+ * Body: { email }
503
+ */
504
+ async requestPasswordReset(email) {
505
+ await this.post(AUTH.passwordResetRequest, { email });
506
+ }
507
+ /**
508
+ * Confirm a password reset using the token from the reset email.
509
+ *
510
+ * Server: POST /api/auth/password/reset/confirm
511
+ * Body: { resetToken, newPassword }
512
+ */
513
+ async confirmPasswordReset(resetToken, newPassword) {
514
+ await this.post(AUTH.passwordResetConfirm, { resetToken, newPassword });
515
+ }
516
+ // ─── Email Verification ───────────────────────────────────────────────────
517
+ /**
518
+ * Request a verification email be sent to a user.
519
+ *
520
+ * Server: POST /api/auth/email/verify/request
521
+ * Body: { userId }
522
+ */
523
+ async requestEmailVerification(userId) {
524
+ await this.post(AUTH.emailVerifyRequest, { userId });
525
+ }
526
+ /**
527
+ * Confirm email ownership using the token from the verification email.
528
+ *
529
+ * Server: POST /api/auth/email/verify/confirm
530
+ * Body: { verifyToken }
531
+ */
532
+ async confirmEmailVerification(verifyToken) {
533
+ await this.post(AUTH.emailVerifyConfirm, { verifyToken });
534
+ }
535
+ // ─── Private helpers ──────────────────────────────────────────────────────
536
+ _buildAuthResult(user, session) {
537
+ return {
538
+ user,
539
+ session: {
540
+ sessionId: session.sessionId,
541
+ userId: user.id,
542
+ bucketId: "",
543
+ createdAt: Date.now(),
544
+ expiresAt: session.expiresAt,
545
+ refreshToken: session.refreshToken,
546
+ refreshExpiresAt: session.expiresAt
547
+ }
548
+ };
549
+ }
550
+ };
551
+
552
+ // src/utils/query.ts
553
+ function buildQueryParams(options = {}) {
554
+ const params = new URLSearchParams();
555
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
556
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
557
+ if (options.orderBy !== void 0) params.set("sortBy", options.orderBy);
558
+ if (options.order !== void 0) params.set("sortOrder", options.order);
559
+ if (options.fields !== void 0) params.set("fields", options.fields);
560
+ if (options.startAfter !== void 0) params.set("cursor", options.startAfter);
561
+ if (options.startAt !== void 0) params.set("cursor", options.startAt);
562
+ if (options.dateRange?.start !== void 0)
563
+ params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
564
+ if (options.dateRange?.end !== void 0)
565
+ params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
566
+ if (options.filters && options.filters.length > 0) {
567
+ for (const f of options.filters) {
568
+ const key = f.op === "==" ? f.field : `${f.field}[${f.op}]`;
569
+ params.set(key, String(f.value));
570
+ }
571
+ }
572
+ const str = params.toString();
573
+ return str ? `?${str}` : "";
574
+ }
575
+ function guessMimeType(filename) {
576
+ const ext = filename.split(".").pop()?.toLowerCase();
577
+ const map = {
578
+ jpg: "image/jpeg",
579
+ jpeg: "image/jpeg",
580
+ png: "image/png",
581
+ gif: "image/gif",
582
+ webp: "image/webp",
583
+ svg: "image/svg+xml",
584
+ pdf: "application/pdf",
585
+ mp4: "video/mp4",
586
+ webm: "video/webm",
587
+ mp3: "audio/mpeg",
588
+ wav: "audio/wav",
589
+ txt: "text/plain",
590
+ html: "text/html",
591
+ css: "text/css",
592
+ js: "application/javascript",
593
+ ts: "application/typescript",
594
+ json: "application/json",
595
+ xml: "application/xml",
596
+ zip: "application/zip",
597
+ csv: "text/csv",
598
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
599
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
600
+ };
601
+ return map[ext ?? ""] ?? "application/octet-stream";
602
+ }
603
+ function assertSafeName(name, label = "name") {
604
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$/.test(name)) {
605
+ throw new Error(
606
+ `Invalid ${label} "${name}". Must start with a letter or underscore and contain only letters, numbers, underscores, dots, or hyphens.`
607
+ );
608
+ }
609
+ }
610
+
611
+ // src/records/client.ts
612
+ var RecordsClient = class {
613
+ constructor(http, bucketSecurityKey, bucketKey) {
614
+ assertSafeName(bucketKey, "bucketKey");
615
+ this.http = http;
616
+ this.bucketKey = bucketKey;
617
+ this.apiKey = bucketSecurityKey;
618
+ }
619
+ // ─── Single Record Operations ─────────────────────────────────────────────
620
+ /**
621
+ * Create a new record (auto-generated ID) or upsert by `customRecordId`.
622
+ *
623
+ * Server: POST /api/:bucketKey
624
+ * Body: { values, queryableFields?, userEmail?, customRecordId? }
625
+ * Returns 201 for new records, 200 for upserts.
626
+ *
627
+ * @example
628
+ * ```ts
629
+ * const post = await posts.create(
630
+ * { title: 'Hello', status: 'draft', authorId: 'u1' },
631
+ * { queryableFields: ['status', 'authorId'], userEmail: 'alice@example.com' },
632
+ * );
633
+ * ```
634
+ */
635
+ async create(data, options = {}) {
636
+ const { queryableFields, userEmail, customRecordId } = options;
637
+ const body = { values: data };
638
+ if (queryableFields?.length) body["queryableFields"] = queryableFields;
639
+ if (userEmail) body["userEmail"] = userEmail;
640
+ if (customRecordId) body["customRecordId"] = customRecordId;
641
+ const result = await this.http.post(
642
+ RECORDS.bucket(this.bucketKey),
643
+ this.apiKey,
644
+ body
645
+ );
646
+ return result.data;
647
+ }
648
+ /**
649
+ * Get a single record by ID.
650
+ *
651
+ * Server: GET /api/:bucketKey?recordId=:id
652
+ */
653
+ async get(id) {
654
+ const result = await this.http.get(
655
+ `${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}`,
656
+ this.apiKey
657
+ );
658
+ return result.data;
659
+ }
660
+ /**
661
+ * Partially update an existing record (PATCH semantics).
662
+ * Supports write-filter sentinels: `{ __op: 'increment', delta: 1 }` etc.
663
+ *
664
+ * Server: PATCH /api/:bucketKey
665
+ * Body: { recordId, values, userEmail?, track_record_history? }
666
+ */
667
+ async patch(id, data, options = {}) {
668
+ const { userEmail, trackHistory } = options;
669
+ const body = { recordId: id, values: data };
670
+ if (userEmail) body["userEmail"] = userEmail;
671
+ if (trackHistory) body["track_record_history"] = true;
672
+ const result = await this.http.patch(
673
+ RECORDS.bucket(this.bucketKey),
674
+ this.apiKey,
675
+ body
676
+ );
677
+ return { id: result.meta?.id ?? id, updatedAt: result.meta?.updatedAt };
678
+ }
679
+ /**
680
+ * Delete a record permanently.
681
+ *
682
+ * Server: DELETE /api/:bucketKey?recordId=:id
683
+ */
684
+ async delete(id) {
685
+ await this.http.delete(
686
+ `${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}`,
687
+ this.apiKey
688
+ );
689
+ }
690
+ /**
691
+ * Check whether a record exists (HEAD request — very lightweight).
692
+ *
693
+ * Server: HEAD /api/:bucketKey?recordId=:id
694
+ * Returns true if the record exists, false if 404.
695
+ */
696
+ async exists(id) {
697
+ try {
698
+ await this.http.request(
699
+ `${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}`,
700
+ { method: "HEAD", headers: { "X-Api-Key": this.apiKey } }
701
+ );
702
+ return true;
703
+ } catch (err) {
704
+ if (err && typeof err === "object" && "status" in err && err.status === 404) return false;
705
+ throw err;
706
+ }
707
+ }
708
+ /**
709
+ * Get a historical snapshot of a record at a specific generation.
710
+ *
711
+ * Server: GET /api/:bucketKey?recordId=:id&generation=:gen
712
+ */
713
+ async getVersion(id, generation) {
714
+ const result = await this.http.get(
715
+ `${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}&generation=${encodeURIComponent(String(generation))}`,
716
+ this.apiKey
717
+ );
718
+ return result.data;
719
+ }
720
+ /**
721
+ * Get the version history of a record.
722
+ *
723
+ * Server: GET /api/:bucketKey?recordId=:id&showHistory=true
724
+ */
725
+ async getHistory(id) {
726
+ const result = await this.http.get(
727
+ `${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}&showHistory=true`,
728
+ this.apiKey
729
+ );
730
+ return result.history ?? [];
731
+ }
732
+ // ─── Batch Operations ─────────────────────────────────────────────────────
733
+ /**
734
+ * Create up to 500 records in one request.
735
+ * Each record may include `_customRecordId` for upsert behaviour.
736
+ *
737
+ * Server: POST /api/:bucketKey/batch/insert
738
+ * Body: { records, queryableFields?, userEmail? }
739
+ *
740
+ * @example
741
+ * ```ts
742
+ * const results = await posts.batchCreate(
743
+ * [{ title: 'A' }, { title: 'B' }],
744
+ * { queryableFields: ['title'] },
745
+ * );
746
+ * ```
747
+ */
748
+ async batchCreate(items, options = {}) {
749
+ const { queryableFields, userEmail } = options;
750
+ const body = { records: items };
751
+ if (queryableFields?.length) body["queryableFields"] = queryableFields;
752
+ if (userEmail) body["userEmail"] = userEmail;
753
+ const result = await this.http.post(
754
+ RECORDS.batchInsert(this.bucketKey),
755
+ this.apiKey,
756
+ body
757
+ );
758
+ return {
759
+ results: result.data ?? [],
760
+ errors: result.errors ?? [],
761
+ successful: result.meta?.successful ?? (result.data?.length ?? 0),
762
+ failed: result.meta?.failed ?? 0
763
+ };
764
+ }
765
+ /**
766
+ * Update up to 500 records in one request.
767
+ *
768
+ * Server: POST /api/:bucketKey/batch/update
769
+ * Body: { updates: [{ recordId, values }], userEmail? }
770
+ */
771
+ async batchUpdate(updates, userEmail) {
772
+ const body = { updates };
773
+ if (userEmail) body["userEmail"] = userEmail;
774
+ const result = await this.http.post(
775
+ RECORDS.batchUpdate(this.bucketKey),
776
+ this.apiKey,
777
+ body
778
+ );
779
+ return {
780
+ successful: result.data?.successful ?? result.meta?.count ?? 0,
781
+ failed: result.data?.failed ?? []
782
+ };
783
+ }
784
+ /**
785
+ * Delete up to 500 records in one request.
786
+ *
787
+ * Server: POST /api/:bucketKey/batch/delete
788
+ * Body: { recordIds, userEmail? }
789
+ */
790
+ async batchDelete(ids, userEmail) {
791
+ const body = { recordIds: ids };
792
+ if (userEmail) body["userEmail"] = userEmail;
793
+ const result = await this.http.post(
794
+ RECORDS.batchDelete(this.bucketKey),
795
+ this.apiKey,
796
+ body
797
+ );
798
+ return {
799
+ successful: result.data?.successful ?? result.meta?.count ?? 0,
800
+ failed: result.data?.failed ?? []
801
+ };
802
+ }
803
+ // ─── Querying ─────────────────────────────────────────────────────────────
804
+ /**
805
+ * Query records with filters, sorting, and cursor-based pagination.
806
+ *
807
+ * Server: GET /api/:bucketKey?field=value&field[op]=value&limit=&sortBy=&sortOrder=&cursor=
808
+ *
809
+ * @example
810
+ * ```ts
811
+ * const { records, hasMore, nextCursor } = await posts.query({
812
+ * filters: [{ field: 'status', op: '==', value: 'published' }],
813
+ * orderBy: 'createdAt',
814
+ * order: 'desc',
815
+ * limit: 20,
816
+ * });
817
+ * ```
818
+ */
819
+ async query(options = {}) {
820
+ const qs = buildQueryParams(options);
821
+ const result = await this.http.get(
822
+ `${RECORDS.bucket(this.bucketKey)}${qs}`,
823
+ this.apiKey
824
+ );
825
+ return {
826
+ records: result.data ?? [],
827
+ total: result.meta?.total ?? result.total,
828
+ hasMore: result.meta?.hasMore ?? result.hasMore ?? false,
829
+ nextCursor: result.meta?.nextCursor ?? result.nextCursor ?? void 0
830
+ };
831
+ }
832
+ /**
833
+ * Retrieve all records matching options (no filter support — use `query()` for filters).
834
+ */
835
+ async getAll(options = {}) {
836
+ const { records } = await this.query(options);
837
+ return records;
838
+ }
839
+ };
840
+
841
+ // src/analytics/client.ts
842
+ var AnalyticsClient = class {
843
+ constructor(http, bucketSecurityKey, bucketKey) {
844
+ assertSafeName(bucketKey, "bucketKey");
845
+ this.http = http;
846
+ this.bucketSecurityKey = bucketSecurityKey;
847
+ this.bucketKey = bucketKey;
848
+ }
849
+ async run(query) {
850
+ const result = await this.http.post(
851
+ ANALYTICS.query(this.bucketKey),
852
+ this.bucketSecurityKey,
853
+ query
854
+ );
855
+ return result.data;
856
+ }
857
+ // ─── Query methods ────────────────────────────────────────────────────────
858
+ /** Count all records, optionally within a date range. */
859
+ async count(opts = {}) {
860
+ return this.run({ queryType: "count", ...opts });
861
+ }
862
+ /**
863
+ * Get value distribution for a field (e.g. records per status).
864
+ * `order` maps to `sortBy` on the wire (the server param name).
865
+ */
866
+ async distribution(opts) {
867
+ assertSafeName(opts.field, "field");
868
+ return this.run({ queryType: "distribution", ...opts });
869
+ }
870
+ /** Sum a numeric field, optionally grouped by another field. */
871
+ async sum(opts) {
872
+ assertSafeName(opts.field, "field");
873
+ if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
874
+ return this.run({ queryType: "sum", ...opts });
875
+ }
876
+ /** Count of records over time, bucketed by granularity. */
877
+ async timeSeries(opts = {}) {
878
+ return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
879
+ }
880
+ /** Aggregate a numeric field over time. */
881
+ async fieldTimeSeries(opts) {
882
+ assertSafeName(opts.field, "field");
883
+ return this.run({
884
+ queryType: "fieldTimeSeries",
885
+ aggregation: "sum",
886
+ granularity: "day",
887
+ ...opts
888
+ });
889
+ }
890
+ /** Top N values for a field by count. */
891
+ async topN(opts) {
892
+ assertSafeName(opts.field, "field");
893
+ if (opts.labelField) assertSafeName(opts.labelField, "labelField");
894
+ return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
895
+ }
896
+ /** Statistical summary (min, max, avg, sum, count, stddev) for a numeric field. */
897
+ async stats(opts) {
898
+ assertSafeName(opts.field, "field");
899
+ return this.run({ queryType: "stats", ...opts });
900
+ }
901
+ /**
902
+ * Fetch filtered records via the analytics engine (BigQuery).
903
+ * Bypasses Firestore pagination — useful for large result sets.
904
+ */
905
+ async records(opts = {}) {
906
+ if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
907
+ if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
908
+ return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
909
+ }
910
+ /**
911
+ * Compute multiple aggregations in a single request.
912
+ * `metrics` must have valid `field` and `name` (used as BigQuery column aliases).
913
+ */
914
+ async multiMetric(opts) {
915
+ opts.metrics.forEach((m) => {
916
+ assertSafeName(m.field, "metric.field");
917
+ assertSafeName(m.name, "metric.name");
918
+ });
919
+ return this.run({ queryType: "multiMetric", ...opts });
920
+ }
921
+ /** Storage usage stats for the bucket (record count, bytes, avg/min/max size). */
922
+ async storageStats(opts = {}) {
923
+ return this.run({ queryType: "storageStats", ...opts });
924
+ }
925
+ /**
926
+ * Compare a metric across multiple buckets in one query.
927
+ * The caller's key must have read access to EVERY bucket in `bucketKeys`.
928
+ * System buckets (`users`, `_sys_*`) are blocked server-side.
929
+ */
930
+ async crossBucket(opts) {
931
+ assertSafeName(opts.field, "field");
932
+ opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
933
+ return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
934
+ }
935
+ /**
936
+ * Raw query — escape hatch when the typed helpers don't cover your case.
937
+ */
938
+ async query(query) {
939
+ const data = await this.run(query);
940
+ return { queryType: query.queryType, data };
941
+ }
942
+ };
943
+
944
+ // src/storage/manager.ts
945
+ function toUploadResult(r) {
946
+ return {
947
+ path: r.path,
948
+ mimeType: r.mimeType,
949
+ size: r.size,
950
+ isPublic: r.isPublic,
951
+ publicUrl: r.publicUrl,
952
+ downloadUrl: r.downloadUrl
953
+ };
954
+ }
955
+ var StorageManager = class {
956
+ constructor(http, storageKey) {
957
+ this.http = http;
958
+ this.storageKey = storageKey;
959
+ }
960
+ get authHeaders() {
961
+ return { "X-Storage-Key": this.storageKey };
962
+ }
963
+ // ─── Upload: Server-buffered ──────────────────────────────────────────────
964
+ /**
965
+ * Upload a file in one step (server-buffered, up to 500 MB).
966
+ * For files >10 MB or when upload progress tracking is needed, use the
967
+ * signed URL flow: `getUploadUrl()` → `uploadToSignedUrl()` → `confirmUpload()`.
968
+ *
969
+ * Server: POST /storage/upload (multipart/form-data)
970
+ *
971
+ * @example
972
+ * ```ts
973
+ * const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
974
+ * console.log(result.publicUrl);
975
+ * ```
976
+ */
977
+ async upload(data, path, options = {}) {
978
+ const { isPublic = false, overwrite = false, mimeType } = options;
979
+ const mime = mimeType ?? guessMimeType(path);
980
+ const formData = new FormData();
981
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mime });
982
+ formData.append("file", blob, path.split("/").pop() ?? "file");
983
+ formData.append("path", path);
984
+ formData.append("mimeType", mime);
985
+ formData.append("isPublic", String(isPublic));
986
+ formData.append("overwrite", String(overwrite));
987
+ const result = await this.http.request(STORAGE.upload, {
988
+ method: "POST",
989
+ rawBody: formData,
990
+ headers: this.authHeaders
991
+ });
992
+ return toUploadResult(result);
993
+ }
994
+ /**
995
+ * Upload raw string or JSON data directly as a file.
996
+ *
997
+ * Server: POST /storage/upload-raw
998
+ * Body: { path, content, mimeType?, isPublic?, overwrite? }
999
+ *
1000
+ * @example
1001
+ * ```ts
1002
+ * await storage.uploadRaw({ theme: 'dark' }, 'settings/config.json');
1003
+ * ```
1004
+ */
1005
+ async uploadRaw(data, path, options = {}) {
1006
+ const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
1007
+ const content = typeof data === "string" ? data : JSON.stringify(data);
1008
+ const result = await this.http.request(STORAGE.uploadRaw, {
1009
+ method: "POST",
1010
+ body: { path, content, mimeType, isPublic, overwrite },
1011
+ headers: this.authHeaders
1012
+ });
1013
+ return toUploadResult(result);
1014
+ }
1015
+ // ─── Upload: Direct-to-GCS (recommended for large files / progress) ───────
1016
+ /**
1017
+ * Step 1 — get a signed GCS PUT URL for direct client-to-GCS upload.
1018
+ *
1019
+ * Server: POST /storage/upload-url
1020
+ * Body: { path, mimeType, size, isPublic?, overwrite?, expiresIn? }
1021
+ *
1022
+ * @example
1023
+ * ```ts
1024
+ * const { uploadUrl, path: p } = await storage.getUploadUrl({
1025
+ * path: 'videos/intro.mp4', mimeType: 'video/mp4', size: file.size,
1026
+ * });
1027
+ * await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', pct => setProgress(pct));
1028
+ * const result = await storage.confirmUpload({ path: p, mimeType: 'video/mp4' });
1029
+ * ```
1030
+ */
1031
+ async getUploadUrl(opts) {
1032
+ const { expiresInSeconds, ...rest } = opts;
1033
+ const result = await this.http.request(STORAGE.uploadUrl, {
1034
+ method: "POST",
1035
+ body: { isPublic: false, overwrite: false, expiresIn: expiresInSeconds ?? 900, ...rest },
1036
+ headers: this.authHeaders
1037
+ });
1038
+ return {
1039
+ uploadUrl: result.uploadUrl,
1040
+ path: result.path,
1041
+ mimeType: result.mimeType,
1042
+ expiresAt: result.expiresAt,
1043
+ expiresIn: result.expiresIn
1044
+ };
1045
+ }
1046
+ /**
1047
+ * Step 2 — upload data directly to the signed GCS URL.
1048
+ * Supports progress tracking in browser environments via XHR.
1049
+ */
1050
+ async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
1051
+ await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
1052
+ }
1053
+ /**
1054
+ * Step 3 — confirm a direct upload and register metadata server-side.
1055
+ *
1056
+ * Server: POST /storage/confirm
1057
+ * Body: { path, mimeType, isPublic? }
1058
+ */
1059
+ async confirmUpload(opts) {
1060
+ const result = await this.http.request(STORAGE.confirm, {
1061
+ method: "POST",
1062
+ body: { isPublic: false, ...opts },
1063
+ headers: this.authHeaders
1064
+ });
1065
+ return toUploadResult(result);
1066
+ }
1067
+ // ─── Batch Upload ─────────────────────────────────────────────────────────
1068
+ /**
1069
+ * Get signed upload URLs for up to 50 files at once.
1070
+ *
1071
+ * Server: POST /storage/batch-upload-urls
1072
+ * Body: { files: [...], expiresIn? }
1073
+ */
1074
+ async getBatchUploadUrls(files) {
1075
+ const result = await this.http.request(STORAGE.batchUploadUrls, {
1076
+ method: "POST",
1077
+ body: { files },
1078
+ headers: this.authHeaders
1079
+ });
1080
+ return {
1081
+ files: result.succeeded.map((f) => ({
1082
+ index: f.index,
1083
+ uploadUrl: f.uploadUrl,
1084
+ path: f.path,
1085
+ mimeType: f.mimeType,
1086
+ expiresAt: f.expiresAt,
1087
+ expiresIn: f.expiresIn
1088
+ }))
1089
+ };
1090
+ }
1091
+ /**
1092
+ * Confirm multiple direct uploads at once.
1093
+ *
1094
+ * Server: POST /storage/batch-confirm
1095
+ * Body: { files: [{ path, mimeType, isPublic? }] }
1096
+ */
1097
+ async batchConfirmUploads(items) {
1098
+ const result = await this.http.request(STORAGE.batchConfirm, {
1099
+ method: "POST",
1100
+ body: { files: items },
1101
+ headers: this.authHeaders
1102
+ });
1103
+ return {
1104
+ succeeded: result.succeeded.map(toUploadResult),
1105
+ failed: result.failed
1106
+ };
1107
+ }
1108
+ // ─── Download ─────────────────────────────────────────────────────────────
1109
+ /**
1110
+ * Download a private file as an ArrayBuffer.
1111
+ * For public files, use the `publicUrl` directly — no SDK needed.
1112
+ *
1113
+ * Server: GET /storage/download/:path (requires X-Storage-Key)
1114
+ */
1115
+ async download(path) {
1116
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
1117
+ return this.http.request(STORAGE.download(encoded), {
1118
+ method: "GET",
1119
+ headers: this.authHeaders
1120
+ });
1121
+ }
1122
+ /**
1123
+ * Download up to 20 files at once. Returns base64-encoded content.
1124
+ *
1125
+ * Server: POST /storage/batch-download
1126
+ * Body: { paths, concurrency? }
1127
+ */
1128
+ async batchDownload(paths, concurrency = 5) {
1129
+ const result = await this.http.request(STORAGE.batchDownload, {
1130
+ method: "POST",
1131
+ body: { paths, concurrency },
1132
+ headers: this.authHeaders
1133
+ });
1134
+ return {
1135
+ succeeded: result.succeeded.map((f) => ({
1136
+ index: f.index,
1137
+ path: f.path,
1138
+ mimeType: f.mimeType,
1139
+ size: f.size,
1140
+ content: f.content
1141
+ })),
1142
+ failed: result.failed.map((f) => ({
1143
+ index: f.index,
1144
+ path: f.path,
1145
+ error: f.error,
1146
+ code: f.code
1147
+ }))
1148
+ };
1149
+ }
1150
+ // ─── List ─────────────────────────────────────────────────────────────────
1151
+ /**
1152
+ * List files and folders at a given path prefix.
1153
+ *
1154
+ * Server: GET /storage/list?prefix=&limit=&cursor=
1155
+ *
1156
+ * @example
1157
+ * ```ts
1158
+ * const { files, folders, hasMore, nextCursor } = await storage.list({ prefix: 'avatars/', limit: 20 });
1159
+ * const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
1160
+ * ```
1161
+ */
1162
+ async list(opts = {}) {
1163
+ const params = new URLSearchParams();
1164
+ if (opts.prefix) params.set("prefix", opts.prefix);
1165
+ if (opts.limit) params.set("limit", String(opts.limit));
1166
+ if (opts.cursor) params.set("cursor", opts.cursor);
1167
+ const qs = params.toString() ? `?${params}` : "";
1168
+ const result = await this.http.request(`${STORAGE.list}${qs}`, {
1169
+ method: "GET",
1170
+ headers: this.authHeaders
1171
+ });
1172
+ if (result.items !== void 0) {
1173
+ const files = [];
1174
+ const folders = [];
1175
+ for (const item of result.items) {
1176
+ if (item.type === "folder") folders.push(item.path);
1177
+ else files.push(item);
1178
+ }
1179
+ return {
1180
+ files,
1181
+ folders,
1182
+ hasMore: result.pagination?.hasMore ?? false,
1183
+ nextCursor: result.pagination?.nextCursor ?? void 0
1184
+ };
1185
+ }
1186
+ return {
1187
+ files: result.files ?? [],
1188
+ folders: result.folders ?? [],
1189
+ hasMore: result.hasMore ?? false,
1190
+ nextCursor: result.nextCursor ?? void 0
1191
+ };
1192
+ }
1193
+ // ─── Metadata ─────────────────────────────────────────────────────────────
1194
+ /**
1195
+ * Get file metadata: size, MIME type, visibility, URLs.
1196
+ *
1197
+ * Server: GET /storage/metadata/:path
1198
+ */
1199
+ async getMetadata(path) {
1200
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
1201
+ const result = await this.http.request(STORAGE.metadata(encoded), {
1202
+ method: "GET",
1203
+ headers: this.authHeaders
1204
+ });
1205
+ return {
1206
+ path: result.path,
1207
+ size: result.size,
1208
+ mimeType: result.mimeType,
1209
+ isPublic: result.isPublic,
1210
+ publicUrl: result.publicUrl,
1211
+ downloadUrl: result.downloadUrl,
1212
+ createdAt: result.createdAt,
1213
+ updatedAt: result.updatedAt
1214
+ };
1215
+ }
1216
+ // ─── Signed URL ───────────────────────────────────────────────────────────
1217
+ /**
1218
+ * Generate a time-limited download URL for a private file.
1219
+ * Can be shared externally — no X-Storage-Key required to access.
1220
+ *
1221
+ * Note: Downloads via signed URLs bypass the server — stats are NOT tracked.
1222
+ *
1223
+ * Server: POST /storage/signed-url
1224
+ * Body: { path, expiresIn? }
1225
+ *
1226
+ * @param path File path.
1227
+ * @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
1228
+ */
1229
+ async getSignedUrl(path, expiresIn = 3600) {
1230
+ const result = await this.http.request(STORAGE.signedUrl, {
1231
+ method: "POST",
1232
+ body: { path, expiresIn },
1233
+ headers: this.authHeaders
1234
+ });
1235
+ return {
1236
+ signedUrl: result.signedUrl,
1237
+ expiresAt: result.expiresAt,
1238
+ expiresIn: result.expiresIn,
1239
+ path: result.path
1240
+ };
1241
+ }
1242
+ // ─── Visibility ───────────────────────────────────────────────────────────
1243
+ /**
1244
+ * Change a file's visibility between public and private.
1245
+ *
1246
+ * Server: PATCH /storage/visibility
1247
+ * Body: { path, isPublic }
1248
+ */
1249
+ async setVisibility(path, isPublic) {
1250
+ const result = await this.http.request(STORAGE.visibility, {
1251
+ method: "PATCH",
1252
+ body: { path, isPublic },
1253
+ headers: this.authHeaders
1254
+ });
1255
+ return {
1256
+ path: result.path,
1257
+ isPublic: result.isPublic,
1258
+ publicUrl: result.publicUrl,
1259
+ downloadUrl: result.downloadUrl
1260
+ };
1261
+ }
1262
+ // ─── Folder Operations ────────────────────────────────────────────────────
1263
+ /**
1264
+ * Create a folder (empty GCS object used as a prefix marker).
1265
+ *
1266
+ * Server: POST /storage/folder
1267
+ * Body: { path }
1268
+ */
1269
+ async createFolder(path) {
1270
+ const result = await this.http.request(STORAGE.folder, {
1271
+ method: "POST",
1272
+ body: { path },
1273
+ headers: this.authHeaders
1274
+ });
1275
+ return { path: result.path };
1276
+ }
1277
+ // ─── File Operations ──────────────────────────────────────────────────────
1278
+ /**
1279
+ * Permanently delete a file.
1280
+ *
1281
+ * Server: DELETE /storage/file
1282
+ * Body: { path }
1283
+ */
1284
+ async deleteFile(path) {
1285
+ await this.http.request(STORAGE.file, {
1286
+ method: "DELETE",
1287
+ body: { path },
1288
+ headers: this.authHeaders
1289
+ });
1290
+ }
1291
+ /**
1292
+ * Recursively delete a folder and all its contents.
1293
+ *
1294
+ * Server: DELETE /storage/folder
1295
+ * Body: { path }
1296
+ */
1297
+ async deleteFolder(path) {
1298
+ await this.http.request(STORAGE.folderDelete, {
1299
+ method: "DELETE",
1300
+ body: { path },
1301
+ headers: this.authHeaders
1302
+ });
1303
+ }
1304
+ /**
1305
+ * Move (rename) a file.
1306
+ *
1307
+ * Server: POST /storage/move
1308
+ * Body: { from, to }
1309
+ */
1310
+ async move(from, to) {
1311
+ const result = await this.http.request(STORAGE.move, {
1312
+ method: "POST",
1313
+ body: { from, to },
1314
+ headers: this.authHeaders
1315
+ });
1316
+ return { from: result.from, to: result.to };
1317
+ }
1318
+ /**
1319
+ * Copy a file to a new path.
1320
+ *
1321
+ * Server: POST /storage/copy
1322
+ * Body: { from, to }
1323
+ */
1324
+ async copy(from, to) {
1325
+ const result = await this.http.request(STORAGE.copy, {
1326
+ method: "POST",
1327
+ body: { from, to },
1328
+ headers: this.authHeaders
1329
+ });
1330
+ return { from: result.from, to: result.to };
1331
+ }
1332
+ // ─── Stats ────────────────────────────────────────────────────────────────
1333
+ /**
1334
+ * Get usage statistics for this storage key.
1335
+ *
1336
+ * Server: GET /storage/stats
1337
+ */
1338
+ async getStats() {
1339
+ const result = await this.http.request(STORAGE.stats, {
1340
+ method: "GET",
1341
+ headers: this.authHeaders
1342
+ });
1343
+ return result.stats;
1344
+ }
1345
+ /**
1346
+ * Get server info (no auth required).
1347
+ *
1348
+ * Server: GET /storage/info
1349
+ */
1350
+ async info() {
1351
+ return this.http.request(STORAGE.info, {
1352
+ method: "GET"
1353
+ });
1354
+ }
1355
+ };
1356
+
1357
+ // src/storage/scoped.ts
1358
+ var ScopedStorage = class _ScopedStorage {
1359
+ constructor(manager, prefix) {
1360
+ this.manager = manager;
1361
+ this.prefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
1362
+ }
1363
+ p(path) {
1364
+ return `${this.prefix}${path.replace(/^\/+/, "")}`;
1365
+ }
1366
+ // ─── Upload ───────────────────────────────────────────────────────────────
1367
+ async upload(data, path, options = {}) {
1368
+ return this.manager.upload(data, this.p(path), options);
1369
+ }
1370
+ async uploadRaw(data, path, options = {}) {
1371
+ return this.manager.uploadRaw(data, this.p(path), options);
1372
+ }
1373
+ async getUploadUrl(opts) {
1374
+ return this.manager.getUploadUrl({ ...opts, path: this.p(opts.path) });
1375
+ }
1376
+ async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
1377
+ return this.manager.uploadToSignedUrl(signedUrl, data, mimeType, onProgress);
1378
+ }
1379
+ async confirmUpload(opts) {
1380
+ return this.manager.confirmUpload({ ...opts, path: this.p(opts.path) });
1381
+ }
1382
+ async getBatchUploadUrls(files) {
1383
+ return this.manager.getBatchUploadUrls(
1384
+ files.map((f) => ({ ...f, path: this.p(f.path) }))
1385
+ );
1386
+ }
1387
+ async batchConfirmUploads(items) {
1388
+ return this.manager.batchConfirmUploads(
1389
+ items.map((i) => ({ ...i, path: this.p(i.path) }))
1390
+ );
1391
+ }
1392
+ // ─── Download ─────────────────────────────────────────────────────────────
1393
+ async download(path) {
1394
+ return this.manager.download(this.p(path));
1395
+ }
1396
+ async batchDownload(paths) {
1397
+ return this.manager.batchDownload(paths.map((p) => this.p(p)));
1398
+ }
1399
+ // ─── List ─────────────────────────────────────────────────────────────────
1400
+ /**
1401
+ * List files within this scope.
1402
+ * The `prefix` option is relative to the scope root.
1403
+ *
1404
+ * @example
1405
+ * ```ts
1406
+ * const userDocs = storage.scope('users/alice/');
1407
+ * // Lists users/alice/docs/
1408
+ * const { files } = await userDocs.list({ prefix: 'docs/' });
1409
+ * ```
1410
+ */
1411
+ async list(opts = {}) {
1412
+ return this.manager.list({
1413
+ ...opts,
1414
+ prefix: this.p(opts.prefix ?? "")
1415
+ });
1416
+ }
1417
+ // ─── Metadata & URLs ──────────────────────────────────────────────────────
1418
+ async getMetadata(path) {
1419
+ return this.manager.getMetadata(this.p(path));
1420
+ }
1421
+ async getSignedUrl(path, expiresIn = 3600) {
1422
+ return this.manager.getSignedUrl(this.p(path), expiresIn);
1423
+ }
1424
+ async setVisibility(path, isPublic) {
1425
+ return this.manager.setVisibility(this.p(path), isPublic);
1426
+ }
1427
+ // ─── Folder Operations ────────────────────────────────────────────────────
1428
+ async createFolder(path) {
1429
+ return this.manager.createFolder(this.p(path));
1430
+ }
1431
+ // ─── File Operations ──────────────────────────────────────────────────────
1432
+ async deleteFile(path) {
1433
+ return this.manager.deleteFile(this.p(path));
1434
+ }
1435
+ async deleteFolder(path) {
1436
+ return this.manager.deleteFolder(this.p(path));
1437
+ }
1438
+ async move(from, to) {
1439
+ return this.manager.move(this.p(from), this.p(to));
1440
+ }
1441
+ async copy(from, to) {
1442
+ return this.manager.copy(this.p(from), this.p(to));
1443
+ }
1444
+ // ─── Stats ────────────────────────────────────────────────────────────────
1445
+ async getStats() {
1446
+ return this.manager.getStats();
1447
+ }
1448
+ // ─── Nesting ──────────────────────────────────────────────────────────────
1449
+ /**
1450
+ * Create a deeper scope within this one.
1451
+ *
1452
+ * @example
1453
+ * ```ts
1454
+ * const user = storage.scope('users/alice/');
1455
+ * const userDocs = user.scope('docs/');
1456
+ * // Effective prefix: users/alice/docs/
1457
+ * ```
1458
+ */
1459
+ scope(subPrefix) {
1460
+ return new _ScopedStorage(this.manager, this.p(subPrefix));
1461
+ }
1462
+ };
1463
+
1464
+ // src/client.ts
1465
+ var HydrousClient = class {
1466
+ constructor(config) {
1467
+ this._recordsCache = /* @__PURE__ */ new Map();
1468
+ this._analyticsCache = /* @__PURE__ */ new Map();
1469
+ this._storageCache = /* @__PURE__ */ new Map();
1470
+ if (!config.authKey) {
1471
+ throw new Error("[HydrousDB] authKey is required.");
1472
+ }
1473
+ if (!config.bucketSecurityKey) {
1474
+ throw new Error("[HydrousDB] bucketSecurityKey is required.");
1475
+ }
1476
+ if (!config.storageKeys || Object.keys(config.storageKeys).length === 0) {
1477
+ throw new Error("[HydrousDB] storageKeys must define at least one key.");
1478
+ }
1479
+ this.http = new HttpClient(config.baseUrl ?? DEFAULT_BASE_URL);
1480
+ this.authKey_ = config.authKey;
1481
+ this.bucketSecurityKey_ = config.bucketSecurityKey;
1482
+ this.storageKeys_ = config.storageKeys;
1483
+ }
1484
+ // ─── Records ──────────────────────────────────────────────────────────────
1485
+ /**
1486
+ * Get a typed RecordsClient for a bucket.
1487
+ *
1488
+ * @example
1489
+ * ```ts
1490
+ * interface Post { title: string; published: boolean }
1491
+ * const posts = db.records<Post>('blog-posts');
1492
+ * const post = await posts.create({ title: 'Hello', published: false });
1493
+ * ```
1494
+ */
1495
+ records(bucketKey) {
1496
+ if (!this._recordsCache.has(bucketKey)) {
1497
+ this._recordsCache.set(
1498
+ bucketKey,
1499
+ new RecordsClient(this.http, this.bucketSecurityKey_, bucketKey)
1500
+ );
1501
+ }
1502
+ return this._recordsCache.get(bucketKey);
1503
+ }
1504
+ // ─── Auth ─────────────────────────────────────────────────────────────────
1505
+ /**
1506
+ * Get the AuthClient.
1507
+ * Auth routes are NOT bucket-scoped in the URL — the bucket is determined
1508
+ * server-side from the API key itself.
1509
+ *
1510
+ * @example
1511
+ * ```ts
1512
+ * const { user, session } = await db.auth().signup({ email: 'alice@example.com', password: 'pw' });
1513
+ * ```
1514
+ */
1515
+ auth() {
1516
+ if (!this._authClient) {
1517
+ this._authClient = new AuthClient(this.http, this.authKey_);
1518
+ }
1519
+ return this._authClient;
1520
+ }
1521
+ // ─── Analytics ────────────────────────────────────────────────────────────
1522
+ /**
1523
+ * Get an AnalyticsClient for a bucket.
1524
+ *
1525
+ * @example
1526
+ * ```ts
1527
+ * const { count } = await db.analytics('orders').count();
1528
+ * ```
1529
+ */
1530
+ analytics(bucketKey) {
1531
+ if (!this._analyticsCache.has(bucketKey)) {
1532
+ this._analyticsCache.set(
1533
+ bucketKey,
1534
+ new AnalyticsClient(this.http, this.bucketSecurityKey_, bucketKey)
1535
+ );
1536
+ }
1537
+ return this._analyticsCache.get(bucketKey);
1538
+ }
1539
+ // ─── Storage ──────────────────────────────────────────────────────────────
1540
+ /**
1541
+ * Get a StorageManager for a named storage key.
1542
+ * The name must match a key in the `storageKeys` config object.
1543
+ *
1544
+ * Attach `.scope('prefix/')` to get a path-prefixed ScopedStorage.
1545
+ *
1546
+ * @example
1547
+ * ```ts
1548
+ * const avatars = db.storage('avatars');
1549
+ * const result = await avatars.upload(file, 'alice.jpg', { isPublic: true });
1550
+ *
1551
+ * // Scoped:
1552
+ * const userFiles = db.storage('documents').scope(`users/${userId}/`);
1553
+ * await userFiles.upload(pdf, 'contract.pdf');
1554
+ * ```
1555
+ */
1556
+ storage(keyName) {
1557
+ const ssk = this.storageKeys_[keyName];
1558
+ if (!ssk) {
1559
+ const available = Object.keys(this.storageKeys_).join(", ");
1560
+ throw new Error(
1561
+ `[HydrousDB] Unknown storage key name "${keyName}". Available keys: ${available}. Add it to storageKeys in your createClient() config.`
1562
+ );
1563
+ }
1564
+ if (!this._storageCache.has(keyName)) {
1565
+ this._storageCache.set(keyName, new StorageManager(this.http, ssk));
1566
+ }
1567
+ const mgr = this._storageCache.get(keyName);
1568
+ const extended = mgr;
1569
+ if (!extended.scope) {
1570
+ extended.scope = (prefix) => new ScopedStorage(mgr, prefix);
1571
+ }
1572
+ return extended;
1573
+ }
1574
+ };
1575
+ function createClient(config) {
1576
+ return new HydrousClient(config);
1577
+ }
1578
+
1579
+ export { ANALYTICS, AUTH, AnalyticsClient, AnalyticsError, AuthClient, AuthError, DEFAULT_BASE_URL, HttpClient, HydrousClient, HydrousError, NetworkError, RECORDS, RecordError, RecordsClient, STORAGE, ScopedStorage, StorageError, StorageManager, ValidationError, createClient };
1580
+ //# sourceMappingURL=index.mjs.map
1581
+ //# sourceMappingURL=index.mjs.map