hydrousdb 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1305 +1,1632 @@
1
- 'use strict';
2
-
3
1
  // src/utils/errors.ts
4
- var HydrousSDKError = class extends Error {
5
- constructor(message, code = "SDK_ERROR", status) {
2
+ var HydrousError = class extends Error {
3
+ constructor(message, code, status, requestId, details) {
6
4
  super(message);
7
- this.name = "HydrousSDKError";
5
+ this.name = "HydrousError";
8
6
  this.code = code;
9
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}`;
10
14
  }
11
15
  };
12
- function toHydrousError(err) {
13
- if (err instanceof HydrousSDKError) {
14
- return { message: err.message, code: err.code, status: err.status };
16
+ var AuthError = class extends HydrousError {
17
+ constructor(message, code, status, requestId, details) {
18
+ super(message, code, status, requestId, details);
19
+ this.name = "AuthError";
15
20
  }
16
- if (err instanceof Error) {
17
- return { message: err.message, code: "UNKNOWN_ERROR" };
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";
18
26
  }
19
- return { message: String(err), code: "UNKNOWN_ERROR" };
20
- }
21
- function isHydrousError(err) {
22
- return err instanceof HydrousSDKError;
23
- }
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
+ this.cause = cause;
51
+ }
52
+ };
24
53
 
25
54
  // src/utils/http.ts
26
- async function parseResponse(res) {
27
- let body;
28
- try {
29
- body = await res.json();
30
- } catch (e) {
31
- if (!res.ok) {
32
- throw new HydrousSDKError(`HTTP ${res.status}`, "HTTP_ERROR", res.status);
33
- }
34
- return void 0;
35
- }
36
- if (!res.ok) {
37
- const err = body;
38
- throw new HydrousSDKError(
39
- err.error || err.message || `HTTP ${res.status}`,
40
- err.code || "HTTP_ERROR",
41
- res.status
42
- );
55
+ var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
56
+ var HttpClient = class {
57
+ constructor(baseUrl, securityKey) {
58
+ this.baseUrl = baseUrl.replace(/\/$/, "");
59
+ this.securityKey = securityKey;
43
60
  }
44
- return body;
45
- }
46
- function buildUrl(base, path, params) {
47
- const url = new URL(path, base.endsWith("/") ? base : base + "/");
48
- if (params) {
49
- for (const [k, v] of Object.entries(params)) {
50
- if (v !== void 0 && v !== null) {
51
- url.searchParams.set(k, String(v));
52
- }
61
+ async request(path, opts = {}) {
62
+ const {
63
+ method = "GET",
64
+ body,
65
+ headers = {},
66
+ rawBody,
67
+ contentType = "application/json"
68
+ } = opts;
69
+ const url = `${this.baseUrl}${path}`;
70
+ const reqHeaders = {
71
+ "X-Api-Key": this.securityKey,
72
+ ...headers
73
+ };
74
+ let reqBody = null;
75
+ if (rawBody !== void 0) {
76
+ reqBody = rawBody;
77
+ if (contentType) reqHeaders["Content-Type"] = contentType;
78
+ } else if (body !== void 0) {
79
+ reqBody = JSON.stringify(body);
80
+ reqHeaders["Content-Type"] = "application/json";
53
81
  }
54
- }
55
- return url.toString();
56
- }
57
- function mergeHeaders(defaults, overrides) {
58
- return { ...defaults, ...overrides };
59
- }
60
- async function readSSEStream(response, onEvent) {
61
- if (!response.body) return;
62
- const reader = response.body.getReader();
63
- const decoder = new TextDecoder();
64
- let buf = "";
65
- const flush = (chunk) => {
66
- var _a;
67
- buf += chunk;
68
- const blocks = buf.split("\n\n");
69
- buf = (_a = blocks.pop()) != null ? _a : "";
70
- for (const block of blocks) {
71
- if (!block.trim()) continue;
72
- let eventType = "message";
73
- let dataLine = null;
74
- for (const line of block.split("\n")) {
75
- if (line.startsWith("event:")) eventType = line.slice(6).trim();
76
- if (line.startsWith("data:")) dataLine = line.slice(5).trim();
77
- }
78
- if (dataLine === null) continue;
79
- try {
80
- onEvent(eventType, JSON.parse(dataLine));
81
- } catch (e) {
82
+ let response;
83
+ try {
84
+ response = await fetch(url, {
85
+ method,
86
+ headers: reqHeaders,
87
+ body: reqBody
88
+ });
89
+ } catch (err) {
90
+ throw new NetworkError(
91
+ `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
92
+ err
93
+ );
94
+ }
95
+ const ct = response.headers.get("content-type") ?? "";
96
+ if (!ct.includes("application/json")) {
97
+ if (!response.ok) {
98
+ throw new HydrousError(
99
+ `Request failed with status ${response.status}`,
100
+ `HTTP_${response.status}`,
101
+ response.status
102
+ );
82
103
  }
104
+ const buffer = await response.arrayBuffer();
105
+ return buffer;
83
106
  }
84
- };
85
- while (true) {
86
- const { done, value } = await reader.read();
87
- if (done) break;
88
- flush(decoder.decode(value, { stream: true }));
89
- }
90
- if (buf.trim()) flush("");
91
- }
92
- function xhrUpload(url, body, headers, onXhrProgress) {
93
- return new Promise((resolve, reject) => {
94
- const xhr = new XMLHttpRequest();
95
- xhr.open("POST", url);
96
- for (const [key, val] of Object.entries(headers)) {
97
- xhr.setRequestHeader(key, val);
107
+ let responseData;
108
+ try {
109
+ responseData = await response.json();
110
+ } catch {
111
+ throw new HydrousError(
112
+ "Failed to parse server response as JSON",
113
+ "PARSE_ERROR",
114
+ response.status
115
+ );
98
116
  }
99
- xhr.responseType = "text";
100
- if (onXhrProgress) {
101
- xhr.upload.onprogress = (e) => {
102
- if (e.lengthComputable) onXhrProgress(e.loaded, e.total);
103
- };
117
+ if (!response.ok) {
118
+ const errData = responseData;
119
+ throw new HydrousError(
120
+ errData["error"] ?? `Request failed with status ${response.status}`,
121
+ errData["code"] ?? `HTTP_${response.status}`,
122
+ response.status,
123
+ errData["requestId"],
124
+ errData["details"]
125
+ );
104
126
  }
105
- xhr.onload = () => {
106
- var _a;
107
- if (xhr.status >= 200 && xhr.status < 300) {
108
- resolve(xhr.responseText);
109
- } else {
110
- try {
111
- const d = JSON.parse(xhr.responseText);
112
- reject(new HydrousSDKError((_a = d.error) != null ? _a : `HTTP ${xhr.status}`, "HTTP_ERROR", xhr.status));
113
- } catch (e) {
114
- reject(new HydrousSDKError(`HTTP ${xhr.status}`, "HTTP_ERROR", xhr.status));
115
- }
116
- }
117
- };
118
- xhr.onerror = () => reject(new HydrousSDKError("Network error", "NETWORK_ERROR"));
119
- xhr.onabort = () => reject(new HydrousSDKError("Upload aborted", "UPLOAD_ABORTED"));
120
- xhr.ontimeout = () => reject(new HydrousSDKError("Upload timed out", "UPLOAD_TIMEOUT"));
121
- xhr.send(body);
122
- });
123
- }
124
- function parseSSEText(text, onEvent) {
125
- const blocks = text.split("\n\n");
126
- for (const block of blocks) {
127
- if (!block.trim()) continue;
128
- let eventType = "message";
129
- let dataLine = null;
130
- for (const line of block.split("\n")) {
131
- if (line.startsWith("event:")) eventType = line.slice(6).trim();
132
- if (line.startsWith("data:")) dataLine = line.slice(5).trim();
127
+ return responseData;
128
+ }
129
+ get(path, headers) {
130
+ return this.request(path, { method: "GET", headers });
131
+ }
132
+ post(path, body, headers) {
133
+ return this.request(path, { method: "POST", body, headers });
134
+ }
135
+ put(path, body, headers) {
136
+ return this.request(path, { method: "PUT", body, headers });
137
+ }
138
+ patch(path, body, headers) {
139
+ return this.request(path, { method: "PATCH", body, headers });
140
+ }
141
+ delete(path, body, headers) {
142
+ return this.request(path, { method: "DELETE", body, headers });
143
+ }
144
+ async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
145
+ if (typeof XMLHttpRequest !== "undefined" && onProgress) {
146
+ return new Promise((resolve, reject) => {
147
+ const xhr = new XMLHttpRequest();
148
+ xhr.upload.onprogress = (e) => {
149
+ if (e.lengthComputable) {
150
+ onProgress(Math.round(e.loaded / e.total * 100));
151
+ }
152
+ };
153
+ xhr.onload = () => {
154
+ if (xhr.status >= 200 && xhr.status < 300) {
155
+ resolve();
156
+ } else {
157
+ reject(new Error(`Upload failed: ${xhr.status}`));
158
+ }
159
+ };
160
+ xhr.onerror = () => reject(new Error("Upload network error"));
161
+ xhr.open("PUT", signedUrl);
162
+ xhr.setRequestHeader("Content-Type", mimeType);
163
+ const payload = data instanceof Blob ? data : new Blob([data], { type: mimeType });
164
+ xhr.send(payload);
165
+ });
133
166
  }
134
- if (dataLine === null) continue;
135
- try {
136
- onEvent(eventType, JSON.parse(dataLine));
137
- } catch (e) {
167
+ const fetchBody = data instanceof Blob ? data : new Blob([data], { type: mimeType });
168
+ const response = await fetch(signedUrl, {
169
+ method: "PUT",
170
+ headers: { "Content-Type": mimeType },
171
+ body: fetchBody
172
+ });
173
+ if (!response.ok) {
174
+ throw new NetworkError(`Signed URL upload failed with status ${response.status}`);
138
175
  }
139
176
  }
140
- }
177
+ };
141
178
 
142
179
  // src/auth/client.ts
143
180
  var AuthClient = class {
144
- constructor(config) {
145
- this.session = null;
146
- this.baseUrl = config.url;
147
- this.headers = {
148
- "Content-Type": "application/json",
149
- "Authorization": `Bearer ${config.apiKey}`
150
- };
181
+ constructor(http, bucketKey) {
182
+ this.http = http;
183
+ this.bucketKey = bucketKey;
184
+ this.basePath = `/auth/${bucketKey}`;
151
185
  }
152
- // ─── SIGN UP ───────────────────────────────────────────────────────────────
186
+ // ─── Registration & Login ─────────────────────────────────────────────────
153
187
  /**
154
- * Create a new user account and return a session.
188
+ * Register a new user in this bucket.
155
189
  *
156
190
  * @example
157
- * const { data, error } = await hydrous.auth.signUp({
158
- * email: 'user@example.com',
159
- * password: 'supersecret',
191
+ * ```ts
192
+ * const { user, session } = await auth.signup({
193
+ * email: 'alice@example.com',
194
+ * password: 'hunter2',
195
+ * fullName: 'Alice Wonderland',
196
+ * // Any extra fields are stored on the user record:
197
+ * plan: 'pro',
160
198
  * });
199
+ * ```
161
200
  */
162
- async signUp(options) {
163
- try {
164
- const url = buildUrl(this.baseUrl, "auth/signup");
165
- const res = await fetch(url, {
166
- method: "POST",
167
- headers: this.headers,
168
- body: JSON.stringify(options)
169
- });
170
- const json = await parseResponse(res);
171
- this.session = json.data;
172
- return { data: json.data, error: null };
173
- } catch (err) {
174
- return { data: null, error: toHydrousError(err) };
175
- }
201
+ async signup(options) {
202
+ const result = await this.http.post(`${this.basePath}/signup`, options);
203
+ return { user: result.user, session: result.session };
176
204
  }
177
- // ─── SIGN IN ───────────────────────────────────────────────────────────────
178
205
  /**
179
- * Sign in with email and password.
206
+ * Authenticate an existing user and create a session.
207
+ * Sessions are valid for 24 hours; use `refreshSession` to extend.
180
208
  *
181
209
  * @example
182
- * const { data, error } = await hydrous.auth.signIn({
183
- * email: 'user@example.com',
184
- * password: 'supersecret',
210
+ * ```ts
211
+ * const { user, session } = await auth.login({
212
+ * email: 'alice@example.com',
213
+ * password: 'hunter2',
185
214
  * });
186
- * if (data) console.log('Signed in as', data.user.email);
215
+ * // Store session.sessionId safely — you'll need it for other calls.
216
+ * ```
187
217
  */
188
- async signIn(options) {
189
- try {
190
- const url = buildUrl(this.baseUrl, "auth/signin");
191
- const res = await fetch(url, {
192
- method: "POST",
193
- headers: this.headers,
194
- body: JSON.stringify(options)
195
- });
196
- const json = await parseResponse(res);
197
- this.session = json.data;
198
- return { data: json.data, error: null };
199
- } catch (err) {
200
- return { data: null, error: toHydrousError(err) };
201
- }
218
+ async login(options) {
219
+ const result = await this.http.post(`${this.basePath}/login`, options);
220
+ return { user: result.user, session: result.session };
221
+ }
222
+ /**
223
+ * Invalidate a session (sign out).
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * await auth.logout({ sessionId: session.sessionId });
228
+ * ```
229
+ */
230
+ async logout({ sessionId }) {
231
+ await this.http.post(`${this.basePath}/logout`, { sessionId });
202
232
  }
203
- // ─── SIGN OUT ──────────────────────────────────────────────────────────────
204
233
  /**
205
- * Sign out the current user and invalidate their session.
234
+ * Extend a session using its refresh token.
235
+ * Returns a new session object.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * const newSession = await auth.refreshSession({ refreshToken: session.refreshToken });
240
+ * ```
206
241
  */
207
- async signOut() {
208
- try {
209
- const url = buildUrl(this.baseUrl, "auth/signout");
210
- const res = await fetch(url, {
211
- method: "POST",
212
- headers: mergeHeaders(this.headers, this._sessionHeader())
213
- });
214
- await parseResponse(res);
215
- this.session = null;
216
- return { data: void 0, error: null };
217
- } catch (err) {
218
- return { data: null, error: toHydrousError(err) };
219
- }
242
+ async refreshSession({ refreshToken }) {
243
+ const result = await this.http.post(
244
+ `${this.basePath}/session/refresh`,
245
+ { refreshToken }
246
+ );
247
+ return result.session;
220
248
  }
221
- // ─── GET USER ──────────────────────────────────────────────────────────────
222
- /** Return the currently authenticated user, or null if not signed in. */
223
- async getUser() {
224
- try {
225
- const url = buildUrl(this.baseUrl, "auth/user");
226
- const res = await fetch(url, {
227
- headers: mergeHeaders(this.headers, this._sessionHeader())
228
- });
229
- const json = await parseResponse(res);
230
- return { data: json.data, error: null };
231
- } catch (err) {
232
- return { data: null, error: toHydrousError(err) };
233
- }
249
+ // ─── User Profile ─────────────────────────────────────────────────────────
250
+ /**
251
+ * Fetch a user by their ID.
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * const user = await auth.getUser({ userId: 'usr_abc123' });
256
+ * ```
257
+ */
258
+ async getUser({ userId }) {
259
+ const result = await this.http.get(`${this.basePath}/user/${userId}`);
260
+ return result.user;
234
261
  }
235
- // ─── REFRESH TOKEN ────────────────────────────────────────────────────────
236
262
  /**
237
- * Refresh the access token using the stored refresh token.
238
- * Called automatically by the SDK when a 401 is received.
263
+ * Update fields on a user record. Requires a valid session.
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * const updated = await auth.updateUser({
268
+ * sessionId: session.sessionId,
269
+ * userId: user.id,
270
+ * data: { fullName: 'Alice Smith', plan: 'enterprise' },
271
+ * });
272
+ * ```
239
273
  */
240
- async refreshSession() {
241
- var _a;
242
- if (!((_a = this.session) == null ? void 0 : _a.refreshToken)) {
243
- return { data: null, error: { message: "No session", code: "NO_SESSION" } };
244
- }
245
- try {
246
- const url = buildUrl(this.baseUrl, "auth/refresh");
247
- const res = await fetch(url, {
248
- method: "POST",
249
- headers: this.headers,
250
- body: JSON.stringify({ refreshToken: this.session.refreshToken })
251
- });
252
- const json = await parseResponse(res);
253
- this.session = json.data;
254
- return { data: json.data, error: null };
255
- } catch (err) {
256
- return { data: null, error: toHydrousError(err) };
257
- }
274
+ async updateUser(options) {
275
+ const { sessionId, userId, data } = options;
276
+ const result = await this.http.patch(
277
+ `${this.basePath}/user`,
278
+ { sessionId, userId, ...data }
279
+ );
280
+ return result.user;
281
+ }
282
+ /**
283
+ * Soft-delete a user. The record is marked deleted but not removed from storage.
284
+ * Requires a valid session (the user can delete themselves, or an admin can delete any user).
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * await auth.deleteUser({ sessionId: session.sessionId, userId: user.id });
289
+ * ```
290
+ */
291
+ async deleteUser({ sessionId, userId }) {
292
+ await this.http.delete(`${this.basePath}/user`, { sessionId, userId });
293
+ }
294
+ // ─── Admin Operations ─────────────────────────────────────────────────────
295
+ /**
296
+ * List all users in the bucket. **Admin session required.**
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * const { users, total } = await auth.listUsers({
301
+ * sessionId: adminSession.sessionId,
302
+ * limit: 50,
303
+ * offset: 0,
304
+ * });
305
+ * ```
306
+ */
307
+ async listUsers(options) {
308
+ const { sessionId, limit = 50, offset = 0 } = options;
309
+ const result = await this.http.post(
310
+ `${this.basePath}/users/list`,
311
+ { sessionId, limit, offset }
312
+ );
313
+ return { users: result.users, total: result.total, limit: result.limit, offset: result.offset };
314
+ }
315
+ /**
316
+ * Permanently hard-delete a user and all their data. **Admin session required.**
317
+ * This action is irreversible.
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * await auth.hardDeleteUser({ sessionId: adminSession.sessionId, userId: user.id });
322
+ * ```
323
+ */
324
+ async hardDeleteUser({ sessionId, userId }) {
325
+ await this.http.delete(`${this.basePath}/user/hard`, { sessionId, userId });
326
+ }
327
+ /**
328
+ * Bulk delete multiple users. **Admin session required.**
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * const result = await auth.bulkDeleteUsers({
333
+ * sessionId: adminSession.sessionId,
334
+ * userIds: ['usr_a', 'usr_b'],
335
+ * });
336
+ * ```
337
+ */
338
+ async bulkDeleteUsers({
339
+ sessionId,
340
+ userIds
341
+ }) {
342
+ const result = await this.http.post(
343
+ `${this.basePath}/users/bulk-delete`,
344
+ { sessionId, userIds }
345
+ );
346
+ return { deleted: result.deleted, failed: result.failed };
347
+ }
348
+ /**
349
+ * Lock a user account, preventing login. **Admin session required.**
350
+ *
351
+ * @param options.duration Lock duration in milliseconds. Defaults to 15 minutes.
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * await auth.lockAccount({
356
+ * sessionId: adminSession.sessionId,
357
+ * userId: user.id,
358
+ * duration: 60 * 60 * 1000, // 1 hour
359
+ * });
360
+ * ```
361
+ */
362
+ async lockAccount({
363
+ sessionId,
364
+ userId,
365
+ duration
366
+ }) {
367
+ const result = await this.http.post(`${this.basePath}/account/lock`, { sessionId, userId, duration });
368
+ return result.data;
369
+ }
370
+ /**
371
+ * Unlock a previously locked user account. **Admin session required.**
372
+ *
373
+ * @example
374
+ * ```ts
375
+ * await auth.unlockAccount({ sessionId: adminSession.sessionId, userId: user.id });
376
+ * ```
377
+ */
378
+ async unlockAccount({
379
+ sessionId,
380
+ userId
381
+ }) {
382
+ await this.http.post(`${this.basePath}/account/unlock`, { sessionId, userId });
258
383
  }
259
- /** Return the current in-memory session (may be null). */
260
- getSession() {
261
- return this.session;
384
+ // ─── Password Management ──────────────────────────────────────────────────
385
+ /**
386
+ * Change a user's password. The user must supply their current password.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * await auth.changePassword({
391
+ * sessionId: session.sessionId,
392
+ * userId: user.id,
393
+ * currentPassword: 'hunter2',
394
+ * newPassword: 'correcthorsebatterystaple',
395
+ * });
396
+ * ```
397
+ */
398
+ async changePassword(options) {
399
+ await this.http.post(`${this.basePath}/password/change`, options);
262
400
  }
263
- _sessionHeader() {
264
- var _a;
265
- return ((_a = this.session) == null ? void 0 : _a.accessToken) ? { "X-Session-Token": this.session.accessToken } : {};
401
+ /**
402
+ * Request a password reset email for a user.
403
+ * Always returns success to prevent user enumeration.
404
+ *
405
+ * @example
406
+ * ```ts
407
+ * await auth.requestPasswordReset({ email: 'alice@example.com' });
408
+ * ```
409
+ */
410
+ async requestPasswordReset({ email }) {
411
+ await this.http.post(`${this.basePath}/password/reset/request`, { email });
412
+ }
413
+ /**
414
+ * Complete a password reset using the token from the reset email.
415
+ *
416
+ * @example
417
+ * ```ts
418
+ * await auth.confirmPasswordReset({
419
+ * resetToken: 'tok_from_email',
420
+ * newPassword: 'correcthorsebatterystaple',
421
+ * });
422
+ * ```
423
+ */
424
+ async confirmPasswordReset({
425
+ resetToken,
426
+ newPassword
427
+ }) {
428
+ await this.http.post(`${this.basePath}/password/reset/confirm`, {
429
+ resetToken,
430
+ newPassword
431
+ });
432
+ }
433
+ // ─── Email Verification ───────────────────────────────────────────────────
434
+ /**
435
+ * Send (or resend) an email verification message to a user.
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * await auth.requestEmailVerification({ userId: user.id });
440
+ * ```
441
+ */
442
+ async requestEmailVerification({ userId }) {
443
+ await this.http.post(`${this.basePath}/email/verify/request`, { userId });
444
+ }
445
+ /**
446
+ * Confirm an email address using the token from the verification email.
447
+ *
448
+ * @example
449
+ * ```ts
450
+ * await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
451
+ * ```
452
+ */
453
+ async confirmEmailVerification({ verifyToken }) {
454
+ await this.http.post(`${this.basePath}/email/verify/confirm`, { verifyToken });
266
455
  }
267
456
  };
268
457
 
269
458
  // src/utils/query.ts
270
- function serialiseQuery(opts = {}) {
271
- var _a;
272
- const params = {};
273
- if (opts.limit !== void 0) params["limit"] = String(opts.limit);
274
- if (opts.offset !== void 0) params["offset"] = String(opts.offset);
275
- if (opts.select && opts.select.length > 0) {
276
- params["select"] = opts.select.join(",");
277
- }
278
- if (opts.orderBy) {
279
- params["orderBy"] = opts.orderBy.field;
280
- params["direction"] = (_a = opts.orderBy.direction) != null ? _a : "asc";
281
- }
282
- const filters = opts.where ? Array.isArray(opts.where) ? opts.where : [opts.where] : [];
283
- if (filters.length > 0) {
284
- params["where"] = JSON.stringify(filters);
285
- }
286
- return params;
287
- }
288
- function eq(field, value) {
289
- return { field, operator: "eq", value };
290
- }
291
- function neq(field, value) {
292
- return { field, operator: "neq", value };
293
- }
294
- function gt(field, value) {
295
- return { field, operator: "gt", value };
459
+ function buildQueryParams(options = {}) {
460
+ const params = new URLSearchParams();
461
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
462
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
463
+ if (options.orderBy !== void 0) params.set("orderBy", options.orderBy);
464
+ if (options.order !== void 0) params.set("order", options.order);
465
+ if (options.fields !== void 0) params.set("fields", options.fields);
466
+ if (options.startAfter !== void 0) params.set("startAfter", options.startAfter);
467
+ if (options.startAt !== void 0) params.set("startAt", options.startAt);
468
+ if (options.endAt !== void 0) params.set("endAt", options.endAt);
469
+ if (options.dateRange?.start !== void 0)
470
+ params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
471
+ if (options.dateRange?.end !== void 0)
472
+ params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
473
+ if (options.filters && options.filters.length > 0) {
474
+ params.set("filters", JSON.stringify(options.filters));
475
+ }
476
+ const str = params.toString();
477
+ return str ? `?${str}` : "";
296
478
  }
297
- function lt(field, value) {
298
- return { field, operator: "lt", value };
479
+ function guessMimeType(filename) {
480
+ const ext = filename.split(".").pop()?.toLowerCase();
481
+ const map = {
482
+ jpg: "image/jpeg",
483
+ jpeg: "image/jpeg",
484
+ png: "image/png",
485
+ gif: "image/gif",
486
+ webp: "image/webp",
487
+ svg: "image/svg+xml",
488
+ pdf: "application/pdf",
489
+ mp4: "video/mp4",
490
+ webm: "video/webm",
491
+ mp3: "audio/mpeg",
492
+ wav: "audio/wav",
493
+ txt: "text/plain",
494
+ html: "text/html",
495
+ css: "text/css",
496
+ js: "application/javascript",
497
+ json: "application/json",
498
+ xml: "application/xml",
499
+ zip: "application/zip",
500
+ csv: "text/csv"
501
+ };
502
+ return map[ext ?? ""] ?? "application/octet-stream";
299
503
  }
300
- function inArray(field, value) {
301
- return { field, operator: "in", value };
504
+ function assertSafeName(name, label = "name") {
505
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$/.test(name)) {
506
+ throw new Error(
507
+ `Invalid ${label} "${name}". Must start with a letter or underscore and contain only letters, numbers, underscores, dots, or hyphens.`
508
+ );
509
+ }
302
510
  }
303
511
 
304
512
  // src/records/client.ts
305
513
  var RecordsClient = class {
306
- constructor(config) {
307
- this.baseUrl = config.url;
308
- this.headers = {
309
- "Content-Type": "application/json",
310
- "Authorization": `Bearer ${config.apiKey}`
311
- };
514
+ constructor(http, bucketKey) {
515
+ assertSafeName(bucketKey, "bucketKey");
516
+ this.http = http;
517
+ this.bucketKey = bucketKey;
518
+ this.basePath = `/records/${bucketKey}`;
312
519
  }
313
- // ─── SELECT ────────────────────────────────────────────────────────────────
520
+ // ─── Single Record Operations ─────────────────────────────────────────────
314
521
  /**
315
- * Query records from a collection.
316
- *
317
- * @param collection - Collection name (e.g. "users")
318
- * @param options - Filters, ordering, pagination
522
+ * Create a new record.
319
523
  *
320
524
  * @example
321
- * const { data, error } = await hydrous.records.select('users', {
322
- * where: { field: 'role', operator: 'eq', value: 'admin' },
323
- * orderBy: { field: 'createdAt', direction: 'desc' },
324
- * limit: 20,
525
+ * ```ts
526
+ * const user = await records.create({
527
+ * name: 'Alice',
528
+ * email: 'alice@example.com',
529
+ * score: 100,
325
530
  * });
531
+ * console.log(user.id); // "rec_xxxxxxxx"
532
+ * ```
326
533
  */
327
- async select(collection, options = {}) {
328
- try {
329
- const params = serialiseQuery(options);
330
- const url = buildUrl(this.baseUrl, `records/${collection}`, params);
331
- const res = await fetch(url, { headers: this.headers });
332
- const json = await parseResponse(res);
333
- return { data: json.data, count: json.count, error: null };
334
- } catch (err) {
335
- return { data: [], count: 0, error: toHydrousError(err) };
336
- }
534
+ async create(data) {
535
+ const result = await this.http.post(this.basePath, data);
536
+ return result.record ?? result.data;
337
537
  }
338
- // ─── GET ONE ───────────────────────────────────────────────────────────────
339
538
  /**
340
- * Fetch a single record by its ID.
539
+ * Fetch a single record by ID.
341
540
  *
342
541
  * @example
343
- * const { data, error } = await hydrous.records.get('users', 'user_abc123');
542
+ * ```ts
543
+ * const post = await records.get('rec_abc123');
544
+ * ```
344
545
  */
345
- async get(collection, id) {
346
- try {
347
- const url = buildUrl(this.baseUrl, `records/${collection}/${id}`);
348
- const res = await fetch(url, { headers: this.headers });
349
- const json = await parseResponse(res);
350
- return { data: json.data, error: null };
351
- } catch (err) {
352
- return { data: null, error: toHydrousError(err) };
353
- }
546
+ async get(id) {
547
+ const result = await this.http.get(`${this.basePath}/${id}`);
548
+ return result.record ?? result.data;
354
549
  }
355
- // ─── INSERT ────────────────────────────────────────────────────────────────
356
550
  /**
357
- * Insert one or more records into a collection.
358
- *
359
- * @param collection - Collection name
360
- * @param payload - A single record object or an array of record objects
551
+ * Overwrite a record entirely (full replace).
361
552
  *
362
553
  * @example
363
- * // Single insert
364
- * const { data, error } = await hydrous.records.insert('users', {
365
- * name: 'Alice', email: 'alice@example.com'
554
+ * ```ts
555
+ * const updated = await records.set('rec_abc123', {
556
+ * name: 'Alice Updated',
557
+ * email: 'alice2@example.com',
366
558
  * });
559
+ * ```
560
+ */
561
+ async set(id, data) {
562
+ const result = await this.http.put(`${this.basePath}/${id}`, data);
563
+ return result.record ?? result.data;
564
+ }
565
+ /**
566
+ * Partially update a record (merge by default).
367
567
  *
368
- * // Bulk insert
369
- * const { data, error } = await hydrous.records.insert('users', [
370
- * { name: 'Alice' }, { name: 'Bob' }
568
+ * @example
569
+ * ```ts
570
+ * // Merge: only the provided fields are updated
571
+ * const updated = await records.patch('rec_abc123', { score: 200 });
572
+ *
573
+ * // Replace: equivalent to set()
574
+ * const replaced = await records.patch('rec_abc123', { score: 200 }, { merge: false });
575
+ * ```
576
+ */
577
+ async patch(id, data, options = {}) {
578
+ const { merge = true } = options;
579
+ const result = await this.http.patch(
580
+ `${this.basePath}/${id}`,
581
+ { ...data, _merge: merge }
582
+ );
583
+ return result.record ?? result.data;
584
+ }
585
+ /**
586
+ * Delete a record permanently.
587
+ *
588
+ * @example
589
+ * ```ts
590
+ * await records.delete('rec_abc123');
591
+ * ```
592
+ */
593
+ async delete(id) {
594
+ await this.http.delete(`${this.basePath}/${id}`);
595
+ }
596
+ // ─── Batch Operations ─────────────────────────────────────────────────────
597
+ /**
598
+ * Create multiple records in one request.
599
+ *
600
+ * @example
601
+ * ```ts
602
+ * const created = await records.batchCreate([
603
+ * { name: 'Alice', score: 100 },
604
+ * { name: 'Bob', score: 200 },
371
605
  * ]);
606
+ * ```
372
607
  */
373
- async insert(collection, payload) {
374
- try {
375
- const url = buildUrl(this.baseUrl, `records/${collection}`);
376
- const res = await fetch(url, {
377
- method: "POST",
378
- headers: this.headers,
379
- body: JSON.stringify(payload)
380
- });
381
- const json = await parseResponse(res);
382
- return { data: json.data, count: json.count, error: null };
383
- } catch (err) {
384
- return { data: [], count: 0, error: toHydrousError(err) };
385
- }
608
+ async batchCreate(items) {
609
+ const result = await this.http.post(
610
+ `${this.basePath}/batch`,
611
+ { records: items }
612
+ );
613
+ return result.records;
614
+ }
615
+ /**
616
+ * Delete multiple records by ID in one request.
617
+ *
618
+ * @example
619
+ * ```ts
620
+ * await records.batchDelete(['rec_a', 'rec_b', 'rec_c']);
621
+ * ```
622
+ */
623
+ async batchDelete(ids) {
624
+ const result = await this.http.post(`${this.basePath}/batch/delete`, { ids });
625
+ return { deleted: result.deleted, failed: result.failed };
386
626
  }
387
- // ─── UPDATE ────────────────────────────────────────────────────────────────
627
+ // ─── Querying ─────────────────────────────────────────────────────────────
388
628
  /**
389
- * Update a record by ID.
629
+ * Query records with optional filters, sorting, and pagination.
390
630
  *
391
631
  * @example
392
- * const { data, error } = await hydrous.records.update('users', 'user_abc123', {
393
- * name: 'Alice Smith'
632
+ * ```ts
633
+ * // Simple query
634
+ * const { records } = await posts.query({ limit: 10 });
635
+ *
636
+ * // Filtered query with cursor pagination
637
+ * const page1 = await posts.query({
638
+ * filters: [
639
+ * { field: 'status', op: '==', value: 'published' },
640
+ * { field: 'views', op: '>', value: 1000 },
641
+ * ],
642
+ * orderBy: 'createdAt',
643
+ * order: 'desc',
644
+ * limit: 20,
645
+ * });
646
+ *
647
+ * const page2 = await posts.query({
648
+ * filters: [{ field: 'status', op: '==', value: 'published' }],
649
+ * limit: 20,
650
+ * startAfter: page1.nextCursor,
394
651
  * });
652
+ * ```
395
653
  */
396
- async update(collection, id, payload) {
397
- try {
398
- const url = buildUrl(this.baseUrl, `records/${collection}/${id}`);
399
- const res = await fetch(url, {
400
- method: "PATCH",
401
- headers: this.headers,
402
- body: JSON.stringify(payload)
403
- });
404
- const json = await parseResponse(res);
405
- return { data: json.data, error: null };
406
- } catch (err) {
407
- return { data: null, error: toHydrousError(err) };
408
- }
654
+ async query(options = {}) {
655
+ const qs = buildQueryParams(options);
656
+ const result = await this.http.get(`${this.basePath}${qs}`);
657
+ return {
658
+ records: result.records,
659
+ total: result.total,
660
+ hasMore: result.hasMore,
661
+ nextCursor: result.nextCursor
662
+ };
409
663
  }
410
- // ─── DELETE ────────────────────────────────────────────────────────────────
411
664
  /**
412
- * Delete a record by ID.
665
+ * Convenience alias: get all records up to `limit` (default 100).
413
666
  *
414
667
  * @example
415
- * const { error } = await hydrous.records.delete('users', 'user_abc123');
668
+ * ```ts
669
+ * const allPosts = await posts.getAll({ limit: 500 });
670
+ * ```
416
671
  */
417
- async delete(collection, id) {
418
- try {
419
- const url = buildUrl(this.baseUrl, `records/${collection}/${id}`);
420
- const res = await fetch(url, { method: "DELETE", headers: this.headers });
421
- await parseResponse(res);
422
- return { data: void 0, error: null };
423
- } catch (err) {
424
- return { data: null, error: toHydrousError(err) };
425
- }
672
+ async getAll(options = {}) {
673
+ const { records } = await this.query(options);
674
+ return records;
675
+ }
676
+ /**
677
+ * Count records matching optional filters.
678
+ *
679
+ * @example
680
+ * ```ts
681
+ * const total = await posts.count([{ field: 'status', op: '==', value: 'published' }]);
682
+ * ```
683
+ */
684
+ async count(filters = []) {
685
+ const result = await this.http.post(
686
+ `${this.basePath}/count`,
687
+ { filters }
688
+ );
689
+ return result.count;
690
+ }
691
+ // ─── Version History ──────────────────────────────────────────────────────
692
+ /**
693
+ * Retrieve the full version history of a record.
694
+ * Each write creates a new version stored in GCS.
695
+ *
696
+ * @example
697
+ * ```ts
698
+ * const history = await records.getHistory('rec_abc123');
699
+ * console.log(history[0].data); // latest version
700
+ * ```
701
+ */
702
+ async getHistory(id) {
703
+ const result = await this.http.get(`${this.basePath}/${id}/history`);
704
+ return result.history;
705
+ }
706
+ /**
707
+ * Restore a record to a specific historical version.
708
+ *
709
+ * @example
710
+ * ```ts
711
+ * const history = await records.getHistory('rec_abc123');
712
+ * const restored = await records.restoreVersion('rec_abc123', history[2].version);
713
+ * ```
714
+ */
715
+ async restoreVersion(id, version) {
716
+ const result = await this.http.post(
717
+ `${this.basePath}/${id}/restore`,
718
+ { version }
719
+ );
720
+ return result.record ?? result.data;
426
721
  }
427
722
  };
428
723
 
429
724
  // src/analytics/client.ts
430
725
  var AnalyticsClient = class {
431
- constructor(config) {
432
- this.baseUrl = config.url;
433
- this.headers = {
434
- "Content-Type": "application/json",
435
- "Authorization": `Bearer ${config.apiKey}`
436
- };
726
+ constructor(http, bucketKey) {
727
+ assertSafeName(bucketKey, "bucketKey");
728
+ this.http = http;
729
+ this.bucketKey = bucketKey;
730
+ this.basePath = `/analytics/${bucketKey}`;
437
731
  }
438
- // ─── TRACK ────────────────────────────────────────────────────────────────
732
+ /** Internal dispatcher — all queries POST to the same endpoint. */
733
+ async run(query) {
734
+ const result = await this.http.post(this.basePath, query);
735
+ return result.data;
736
+ }
737
+ // ─── Count ────────────────────────────────────────────────────────────────
439
738
  /**
440
- * Track an analytics event.
739
+ * Count the total number of records in the bucket, with optional date filter.
441
740
  *
442
741
  * @example
443
- * await hydrous.analytics.track({
444
- * event: 'page_view',
445
- * properties: { page: '/home', referrer: 'google.com' },
446
- * userId: 'user_abc123',
742
+ * ```ts
743
+ * const { count } = await analytics.count();
744
+ * // { count: 4821 }
745
+ *
746
+ * // Count only this month's records
747
+ * const { count } = await analytics.count({
748
+ * dateRange: { start: new Date('2025-01-01').getTime(), end: Date.now() },
447
749
  * });
750
+ * ```
448
751
  */
449
- async track(options) {
450
- var _a;
451
- try {
452
- const url = buildUrl(this.baseUrl, "analytics/track");
453
- const res = await fetch(url, {
454
- method: "POST",
455
- headers: this.headers,
456
- body: JSON.stringify({
457
- ...options,
458
- timestamp: (_a = options.timestamp) != null ? _a : Date.now()
459
- })
460
- });
461
- await parseResponse(res);
462
- return { data: void 0, error: null };
463
- } catch (err) {
464
- return { data: null, error: toHydrousError(err) };
465
- }
752
+ async count(opts = {}) {
753
+ return this.run({ queryType: "count", ...opts });
466
754
  }
467
- // ─── QUERY ────────────────────────────────────────────────────────────────
755
+ // ─── Distribution ─────────────────────────────────────────────────────────
468
756
  /**
469
- * Query recorded analytics events.
757
+ * Count how many records have each unique value for a given field.
758
+ * Great for pie charts and bar charts.
470
759
  *
471
760
  * @example
472
- * const { data } = await hydrous.analytics.query({
473
- * event: 'page_view',
474
- * from: '2024-01-01',
475
- * to: '2024-01-31',
476
- * limit: 100,
761
+ * ```ts
762
+ * const rows = await analytics.distribution({
763
+ * field: 'status',
764
+ * limit: 10,
765
+ * order: 'desc',
477
766
  * });
767
+ * // → [{ value: 'completed', count: 312 }, { value: 'pending', count: 88 }, ...]
768
+ * ```
478
769
  */
479
- async query(options = {}) {
480
- try {
481
- const params = {};
482
- if (options.event) params["event"] = options.event;
483
- if (options.from) params["from"] = options.from;
484
- if (options.to) params["to"] = options.to;
485
- if (options.limit) params["limit"] = String(options.limit);
486
- if (options.groupBy) params["groupBy"] = options.groupBy;
487
- const url = buildUrl(this.baseUrl, "analytics/events", params);
488
- const res = await fetch(url, { headers: this.headers });
489
- const json = await parseResponse(res);
490
- return { data: json.data, count: json.count, error: null };
491
- } catch (err) {
492
- return { data: [], count: 0, error: toHydrousError(err) };
493
- }
770
+ async distribution(opts) {
771
+ assertSafeName(opts.field, "field");
772
+ return this.run({ queryType: "distribution", ...opts });
494
773
  }
495
- // ─── BATCH TRACK ─────────────────────────────────────────────────────────
774
+ // ─── Sum ──────────────────────────────────────────────────────────────────
496
775
  /**
497
- * Track multiple events in a single request (more efficient than
498
- * calling `track` in a loop).
776
+ * Sum a numeric field, optionally grouped by another field.
499
777
  *
500
778
  * @example
501
- * await hydrous.analytics.trackBatch([
502
- * { event: 'signup', userId: 'u1' },
503
- * { event: 'onboarded', userId: 'u1' },
504
- * ]);
779
+ * ```ts
780
+ * // Total revenue
781
+ * const rows = await analytics.sum({ field: 'amount' });
782
+ * // → [{ sum: 198432.50 }]
783
+ *
784
+ * // Revenue by country
785
+ * const rows = await analytics.sum({ field: 'amount', groupBy: 'country', limit: 10 });
786
+ * // → [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, ...]
787
+ * ```
505
788
  */
506
- async trackBatch(events) {
507
- try {
508
- const url = buildUrl(this.baseUrl, "analytics/track/batch");
509
- const stamped = events.map((e) => {
510
- var _a;
511
- return {
512
- ...e,
513
- timestamp: (_a = e.timestamp) != null ? _a : Date.now()
514
- };
515
- });
516
- const res = await fetch(url, {
517
- method: "POST",
518
- headers: this.headers,
519
- body: JSON.stringify({ events: stamped })
520
- });
521
- await parseResponse(res);
522
- return { data: void 0, error: null };
523
- } catch (err) {
524
- return { data: null, error: toHydrousError(err) };
525
- }
789
+ async sum(opts) {
790
+ assertSafeName(opts.field, "field");
791
+ if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
792
+ return this.run({ queryType: "sum", ...opts });
526
793
  }
527
- };
528
-
529
- // src/storage/client.ts
530
- var isBrowser = typeof window !== "undefined" && typeof XMLHttpRequest !== "undefined";
531
- function bucketFromKey(key) {
532
- return encodeURIComponent(key);
533
- }
534
- function storageUrl(base, bucketKey, path) {
535
- const bucket = bucketFromKey(bucketKey);
536
- return `${base.replace(/\/$/, "")}/storage/${bucket}/${path.replace(/^\//, "")}`;
537
- }
538
- function storageHeaders(bucketKey) {
539
- return { "X-Storage-Key": bucketKey };
540
- }
541
- function drainSSEProgress(rawText, onProgress) {
542
- const results = [];
543
- const errors = [];
544
- parseSSEText(rawText, (eventType, data) => {
545
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
546
- const d = data;
547
- if (eventType === "progress" && onProgress) {
548
- onProgress({
549
- index: (_a = d["index"]) != null ? _a : 0,
550
- total: (_b = d["total"]) != null ? _b : 1,
551
- path: (_c = d["path"]) != null ? _c : "",
552
- stage: (_d = d["stage"]) != null ? _d : "uploading",
553
- bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
554
- totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
555
- percent: (_g = d["percent"]) != null ? _g : 0,
556
- bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
557
- eta: (_i = d["eta"]) != null ? _i : null,
558
- result: d["result"],
559
- error: d["error"],
560
- code: d["code"]
561
- });
562
- }
563
- if (eventType === "done") {
564
- if (d["path"]) {
565
- results.push(d);
566
- } else if (Array.isArray(d["errors"])) {
567
- const succeeded = (_j = d["succeeded"]) != null ? _j : [];
568
- const errs = d["errors"];
569
- results.push(...succeeded);
570
- errors.push(...errs);
571
- }
572
- }
573
- if (eventType === "error") {
574
- errors.push({
575
- path: "",
576
- error: (_k = d["error"]) != null ? _k : "Unknown error",
577
- code: (_l = d["code"]) != null ? _l : "UNKNOWN"
578
- });
579
- }
580
- });
581
- return { results, errors };
582
- }
583
- var StorageClient = class {
584
- constructor(config) {
585
- this.baseUrl = config.url;
586
- }
587
- // ══════════════════════════════════════════════════════════════════════════
588
- // UPLOAD
589
- // ══════════════════════════════════════════════════════════════════════════
590
- /**
591
- * Upload a single file to a bucket.
592
- *
593
- * The bucket key **always comes first**.
594
- * Supply an `onProgress` callback to receive live upload progress including
595
- * bytes transferred, speed (bytes/sec), ETA, and lifecycle stage.
596
- *
597
- * ### Stages fired via `onProgress`
598
- * | Stage | Meaning |
599
- * |-------------|------------------------------------------|
600
- * | `pending` | Queued, not yet started |
601
- * | `compressing` | Server is compressing the file |
602
- * | `uploading` | Bytes flowing to cloud storage |
603
- * | `done` | Confirmed written to cloud storage |
604
- * | `error` | Something went wrong |
605
- *
606
- * @param bucketKey Your storage bucket key (`ssk_…`)
607
- * @param file A `File`, `Blob`, or `Buffer` (Node)
608
- * @param options Path, overwrite flag, progress callback
609
- *
610
- * @example
611
- * const { data, error } = await hydrous.storage.upload(
612
- * 'ssk_my_bucket_key',
613
- * file,
614
- * {
615
- * path: 'avatars/alice.jpg',
616
- * overwrite: true,
617
- * onProgress: (p) => {
618
- * console.log(`${p.stage} — ${p.percent}% ${p.bytesPerSecond} B/s ETA ${p.eta}s`);
619
- * },
620
- * }
621
- * );
794
+ // ─── Time Series ──────────────────────────────────────────────────────────
795
+ /**
796
+ * Count records created over time, grouped by a time granularity.
797
+ * Perfect for line charts and activity graphs.
798
+ *
799
+ * @example
800
+ * ```ts
801
+ * const rows = await analytics.timeSeries({
802
+ * granularity: 'day',
803
+ * dateRange: { start: Date.now() - 7 * 86400000, end: Date.now() },
804
+ * });
805
+ * // → [{ date: '2025-06-01', count: 42 }, { date: '2025-06-02', count: 67 }, ...]
806
+ * ```
622
807
  */
623
- async upload(bucketKey, file, options = {}) {
624
- var _a, _b;
625
- const { path, overwrite = false, onProgress } = options;
626
- try {
627
- const url = storageUrl(this.baseUrl, bucketKey, "upload");
628
- const form = new FormData();
629
- if (file instanceof Uint8Array) {
630
- form.append("file", new Blob([file.buffer]), path != null ? path : "file");
631
- } else if (file instanceof ArrayBuffer) {
632
- form.append("file", new Blob([file]), path != null ? path : "file");
633
- } else {
634
- form.append("file", file, path != null ? path : file instanceof File ? file.name : "file");
635
- }
636
- if (path) form.append("path", path);
637
- if (overwrite) form.append("overwrite", "true");
638
- const headers = storageHeaders(bucketKey);
639
- if (isBrowser) {
640
- const totalBytes = file instanceof Blob ? file.size : file instanceof Uint8Array ? file.byteLength : file.byteLength;
641
- const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
642
- if (onProgress) {
643
- onProgress({
644
- index: 0,
645
- total: 1,
646
- path: path != null ? path : "",
647
- stage: "uploading",
648
- bytesUploaded: loaded,
649
- totalBytes: total || totalBytes,
650
- percent: Math.min(99, Math.round(loaded / (total || totalBytes) * 100)),
651
- bytesPerSecond: null,
652
- eta: null
653
- });
654
- }
655
- });
656
- const { results, errors } = drainSSEProgress(rawBody, onProgress);
657
- if (errors.length > 0 && results.length === 0) {
658
- return { data: null, error: { message: errors[0].error, code: errors[0].code } };
659
- }
660
- const result = (_a = results[0]) != null ? _a : null;
661
- if (result && onProgress) {
662
- onProgress({
663
- index: 0,
664
- total: 1,
665
- path: result.path,
666
- stage: "done",
667
- bytesUploaded: totalBytes,
668
- totalBytes,
669
- percent: 100,
670
- bytesPerSecond: null,
671
- eta: 0,
672
- result
673
- });
674
- }
675
- return { data: result, error: null };
676
- }
677
- const res = await fetch(url, { method: "POST", headers, body: form });
678
- if (!res.ok) {
679
- const err = await res.json().catch(() => ({}));
680
- throw new HydrousSDKError((_b = err.error) != null ? _b : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
681
- }
682
- let finalResult = null;
683
- await readSSEStream(res, (eventType, data) => {
684
- var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
685
- const d = data;
686
- if (eventType === "progress" && onProgress) {
687
- onProgress({
688
- index: (_a2 = d["index"]) != null ? _a2 : 0,
689
- total: (_b2 = d["total"]) != null ? _b2 : 1,
690
- path: (_d = (_c = d["path"]) != null ? _c : path) != null ? _d : "",
691
- stage: (_e = d["stage"]) != null ? _e : "uploading",
692
- bytesUploaded: (_f = d["bytesUploaded"]) != null ? _f : 0,
693
- totalBytes: (_g = d["totalBytes"]) != null ? _g : 0,
694
- percent: (_h = d["percent"]) != null ? _h : 0,
695
- bytesPerSecond: (_i = d["bytesPerSecond"]) != null ? _i : null,
696
- eta: (_j = d["eta"]) != null ? _j : null,
697
- result: d["result"],
698
- error: d["error"]
699
- });
700
- }
701
- if (eventType === "done") finalResult = data;
702
- if (eventType === "error") {
703
- throw new HydrousSDKError(
704
- (_k = d["error"]) != null ? _k : "Upload failed",
705
- (_l = d["code"]) != null ? _l : "UPLOAD_ERROR"
706
- );
707
- }
708
- });
709
- return { data: finalResult, error: null };
710
- } catch (err) {
711
- return { data: null, error: toHydrousError(err) };
712
- }
808
+ async timeSeries(opts = {}) {
809
+ return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
713
810
  }
714
- // ══════════════════════════════════════════════════════════════════════════
715
- // UPLOAD RAW (text / JSON / binary from string)
716
- // ══════════════════════════════════════════════════════════════════════════
811
+ // ─── Field Time Series ────────────────────────────────────────────────────
717
812
  /**
718
- * Upload raw text or JSON content directly no `File` object needed.
719
- * Great for saving generated content, config files, or JSON records.
813
+ * Aggregate a numeric field over time (e.g. daily revenue, hourly signups).
720
814
  *
721
- * @param bucketKey Your storage bucket key (`ssk_…`)
722
- * @param path Destination path (e.g. `"configs/settings.json"`)
723
- * @param content String content to store
724
- * @param options `mimeType`, `overwrite`, `onProgress`
815
+ * @example
816
+ * ```ts
817
+ * const rows = await analytics.fieldTimeSeries({
818
+ * field: 'amount',
819
+ * aggregation: 'sum',
820
+ * granularity: 'week',
821
+ * });
822
+ * // → [{ date: '2025-W22', value: 14230.50 }, ...]
823
+ * ```
824
+ */
825
+ async fieldTimeSeries(opts) {
826
+ assertSafeName(opts.field, "field");
827
+ return this.run({
828
+ queryType: "fieldTimeSeries",
829
+ aggregation: "sum",
830
+ granularity: "day",
831
+ ...opts
832
+ });
833
+ }
834
+ // ─── Top N ────────────────────────────────────────────────────────────────
835
+ /**
836
+ * Get the top N values by frequency for a field.
837
+ * Optionally pair with a `labelField` for human-readable labels.
725
838
  *
726
839
  * @example
727
- * await hydrous.storage.uploadText(
728
- * 'ssk_my_bucket_key',
729
- * 'reports/summary.txt',
730
- * 'Hello from Hydrous!',
731
- * { mimeType: 'text/plain' }
732
- * );
840
+ * ```ts
841
+ * // Top 5 most purchased products
842
+ * const rows = await analytics.topN({
843
+ * field: 'productId',
844
+ * labelField: 'productName',
845
+ * n: 5,
846
+ * });
847
+ * // → [{ value: 'prod_123', label: 'Widget Pro', count: 892 }, ...]
848
+ * ```
733
849
  */
734
- async uploadText(bucketKey, path, content, options = {}) {
735
- var _a;
736
- const { mimeType = "text/plain", overwrite = false, onProgress } = options;
737
- try {
738
- const url = storageUrl(this.baseUrl, bucketKey, "upload-raw");
739
- const headers = { ...storageHeaders(bucketKey), "Content-Type": "application/json" };
740
- const res = await fetch(url, {
741
- method: "POST",
742
- headers,
743
- body: JSON.stringify({ path, content, mimeType, overwrite })
744
- });
745
- if (!res.ok) {
746
- const e = await res.json().catch(() => ({}));
747
- throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
748
- }
749
- let finalResult = null;
750
- await readSSEStream(res, (eventType, data) => {
751
- var _a2, _b, _c, _d, _e, _f;
752
- const d = data;
753
- if (eventType === "progress" && onProgress) {
754
- onProgress({
755
- index: 0,
756
- total: 1,
757
- path,
758
- stage: (_a2 = d["stage"]) != null ? _a2 : "uploading",
759
- bytesUploaded: (_b = d["bytesUploaded"]) != null ? _b : 0,
760
- totalBytes: (_c = d["totalBytes"]) != null ? _c : 0,
761
- percent: (_d = d["percent"]) != null ? _d : 0,
762
- bytesPerSecond: (_e = d["bytesPerSecond"]) != null ? _e : null,
763
- eta: (_f = d["eta"]) != null ? _f : null
764
- });
765
- }
766
- if (eventType === "done") finalResult = data;
767
- });
768
- return { data: finalResult, error: null };
769
- } catch (err) {
770
- return { data: null, error: toHydrousError(err) };
771
- }
850
+ async topN(opts) {
851
+ assertSafeName(opts.field, "field");
852
+ if (opts.labelField) assertSafeName(opts.labelField, "labelField");
853
+ return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
772
854
  }
773
- // ══════════════════════════════════════════════════════════════════════════
774
- // BATCH UPLOAD
775
- // ══════════════════════════════════════════════════════════════════════════
855
+ // ─── Field Stats ──────────────────────────────────────────────────────────
776
856
  /**
777
- * Upload multiple files in one request.
857
+ * Get statistical summary (min, max, avg, sum, count, stddev) for a numeric field.
778
858
  *
779
- * `onProgress` fires for **every file individually** — the `index` field
780
- * tells you which file the event belongs to (0-based, same order as `files`).
781
- * All files receive a `pending` event upfront before any uploads start,
782
- * so you can render all progress bars immediately.
859
+ * @example
860
+ * ```ts
861
+ * const stats = await analytics.stats({ field: 'orderValue' });
862
+ * // { min: 4.99, max: 9999.99, avg: 87.23, sum: 420948.27, count: 4823, stddev: 143.2 }
863
+ * ```
864
+ */
865
+ async stats(opts) {
866
+ assertSafeName(opts.field, "field");
867
+ return this.run({ queryType: "stats", ...opts });
868
+ }
869
+ // ─── Filtered Records ─────────────────────────────────────────────────────
870
+ /**
871
+ * Query raw records with filters, field selection, and pagination.
872
+ * This is the analytics version of `records.query()` but powered by BigQuery.
783
873
  *
784
- * @param bucketKey Your storage bucket key (`ssk_…`)
785
- * @param files Array of `File` objects (browser) or `{ name, data }` objects (Node)
786
- * @param options Prefix, per-file paths, overwrite, concurrency, onProgress
874
+ * @example
875
+ * ```ts
876
+ * const { records } = await analytics.records({
877
+ * filters: [{ field: 'status', op: '==', value: 'refunded' }],
878
+ * selectFields: ['orderId', 'amount', 'createdAt'],
879
+ * limit: 50,
880
+ * orderBy: 'amount',
881
+ * order: 'desc',
882
+ * });
883
+ * ```
884
+ */
885
+ async records(opts = {}) {
886
+ if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
887
+ if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
888
+ return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
889
+ }
890
+ // ─── Multi Metric ─────────────────────────────────────────────────────────
891
+ /**
892
+ * Calculate multiple aggregations in a single query.
893
+ * Ideal for dashboards that need several numbers at once.
787
894
  *
788
895
  * @example
789
- * await hydrous.storage.batchUpload(
790
- * 'ssk_my_bucket_key',
791
- * fileArray,
792
- * {
793
- * prefix: 'uploads/2024/',
794
- * onProgress: (p) => {
795
- * console.log(`File ${p.index}: ${p.stage} ${p.percent}%`);
796
- * },
797
- * }
798
- * );
896
+ * ```ts
897
+ * const result = await analytics.multiMetric({
898
+ * metrics: [
899
+ * { field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
900
+ * { field: 'amount', name: 'avgOrderValue', aggregation: 'avg' },
901
+ * { field: 'userId', name: 'uniqueCustomers', aggregation: 'count' },
902
+ * ],
903
+ * });
904
+ * // → { totalRevenue: 198432.50, avgOrderValue: 87.23, uniqueCustomers: 2275 }
905
+ * ```
799
906
  */
800
- async batchUpload(bucketKey, files, options = {}) {
801
- var _a;
802
- const { prefix = "", paths, overwrite = false, onProgress } = options;
803
- try {
804
- const url = storageUrl(this.baseUrl, bucketKey, "batch-upload");
805
- const form = new FormData();
806
- const resolvedPaths = files.map(
807
- (f, i) => {
808
- var _a2;
809
- return (_a2 = paths == null ? void 0 : paths[i]) != null ? _a2 : `${prefix}${f.name}`;
810
- }
811
- );
812
- files.forEach((f) => form.append("files", f, f.name));
813
- form.append("paths", JSON.stringify(resolvedPaths));
814
- if (overwrite) form.append("overwrite", "true");
815
- const headers = storageHeaders(bucketKey);
816
- if (isBrowser) {
817
- const totalBytes = files.reduce((s, f) => s + f.size, 0);
818
- const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
819
- if (onProgress) {
820
- let cursor = 0;
821
- for (let i = 0; i < files.length; i++) {
822
- const share = files[i].size / (totalBytes || 1);
823
- const myStart = cursor;
824
- const myEnd = cursor + share;
825
- const fileLoaded = Math.max(0, Math.min(
826
- files[i].size,
827
- (loaded / (total || totalBytes) - myStart) / share * files[i].size
828
- ));
829
- onProgress({
830
- index: i,
831
- total: files.length,
832
- path: resolvedPaths[i],
833
- stage: "uploading",
834
- bytesUploaded: Math.round(fileLoaded),
835
- totalBytes: files[i].size,
836
- percent: Math.min(99, Math.round(fileLoaded / files[i].size * 100)),
837
- bytesPerSecond: null,
838
- eta: null
839
- });
840
- cursor = myEnd;
841
- }
842
- }
843
- });
844
- const { results, errors } = drainSSEProgress(rawBody, onProgress);
845
- return {
846
- data: {
847
- succeeded: results,
848
- failed: errors
849
- },
850
- error: null
851
- };
852
- }
853
- const res = await fetch(url, { method: "POST", headers, body: form });
854
- if (!res.ok) {
855
- const e = await res.json().catch(() => ({}));
856
- throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
857
- }
858
- const succeeded = [];
859
- const failed = [];
860
- await readSSEStream(res, (eventType, data) => {
861
- var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j;
862
- const d = data;
863
- if (eventType === "progress" && onProgress) {
864
- onProgress({
865
- index: (_a2 = d["index"]) != null ? _a2 : 0,
866
- total: (_b = d["total"]) != null ? _b : files.length,
867
- path: (_c = d["path"]) != null ? _c : "",
868
- stage: (_d = d["stage"]) != null ? _d : "uploading",
869
- bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
870
- totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
871
- percent: (_g = d["percent"]) != null ? _g : 0,
872
- bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
873
- eta: (_i = d["eta"]) != null ? _i : null,
874
- result: d["result"],
875
- error: d["error"],
876
- code: d["code"]
877
- });
878
- }
879
- if (eventType === "done" && d["succeeded"]) {
880
- succeeded.push(...d["succeeded"]);
881
- failed.push(...(_j = d["errors"]) != null ? _j : []);
882
- }
883
- });
884
- return { data: { succeeded, failed }, error: null };
885
- } catch (err) {
886
- return { data: null, error: toHydrousError(err) };
887
- }
907
+ async multiMetric(opts) {
908
+ opts.metrics.forEach((m) => {
909
+ assertSafeName(m.field, "metric.field");
910
+ assertSafeName(m.name, "metric.name");
911
+ });
912
+ return this.run({ queryType: "multiMetric", ...opts });
888
913
  }
889
- // ══════════════════════════════════════════════════════════════════════════
890
- // DOWNLOAD
891
- // ══════════════════════════════════════════════════════════════════════════
914
+ // ─── Storage Stats ────────────────────────────────────────────────────────
892
915
  /**
893
- * Download a single file and return its content as an `ArrayBuffer`.
916
+ * Get storage statistics for this bucket: record counts, byte sizes.
894
917
  *
895
- * @param bucketKey Your storage bucket key (`ssk_…`)
896
- * @param filePath Path of the file within your bucket
918
+ * @example
919
+ * ```ts
920
+ * const stats = await analytics.storageStats();
921
+ * // → { totalRecords: 4821, totalBytes: 48293820, avgBytes: 10015, ... }
922
+ * ```
923
+ */
924
+ async storageStats(opts = {}) {
925
+ return this.run({ queryType: "storageStats", ...opts });
926
+ }
927
+ // ─── Cross-Bucket Comparison ──────────────────────────────────────────────
928
+ /**
929
+ * Compare the same field aggregation across multiple buckets in one query.
930
+ * Your security key must have read access to ALL listed buckets.
897
931
  *
898
932
  * @example
899
- * const { data, error } = await hydrous.storage.download(
900
- * 'ssk_my_bucket_key',
901
- * 'avatars/alice.jpg'
902
- * );
903
- * if (data) {
904
- * const blob = new Blob([data]);
905
- * const url = URL.createObjectURL(blob);
906
- * }
933
+ * ```ts
934
+ * const rows = await analytics.crossBucket({
935
+ * bucketKeys: ['orders-us', 'orders-eu', 'orders-apac'],
936
+ * field: 'amount',
937
+ * aggregation: 'sum',
938
+ * });
939
+ * // → [
940
+ * // { bucket: 'orders-us', value: 120000 },
941
+ * // { bucket: 'orders-eu', value: 45000 },
942
+ * // { bucket: 'orders-apac', value: 33000 },
943
+ * // ]
944
+ * ```
907
945
  */
908
- async download(bucketKey, filePath) {
909
- var _a;
910
- try {
911
- const url = storageUrl(this.baseUrl, bucketKey, `download/${filePath}`);
912
- const res = await fetch(url, { headers: storageHeaders(bucketKey) });
913
- if (!res.ok) {
914
- const e = await res.json().catch(() => ({}));
915
- throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
916
- }
917
- const buffer = await res.arrayBuffer();
918
- return { data: buffer, error: null };
919
- } catch (err) {
920
- return { data: null, error: toHydrousError(err) };
921
- }
946
+ async crossBucket(opts) {
947
+ assertSafeName(opts.field, "field");
948
+ opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
949
+ return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
922
950
  }
923
- // ══════════════════════════════════════════════════════════════════════════
924
- // BATCH DOWNLOAD
925
- // ══════════════════════════════════════════════════════════════════════════
951
+ // ─── Raw Query ────────────────────────────────────────────────────────────
926
952
  /**
927
- * Download multiple files in one request.
953
+ * Send a raw analytics query object. Use this when you need full control
954
+ * over the query shape or want to use a queryType not covered by the helpers.
928
955
  *
929
- * When `autoSave: true` (browser only) each file is automatically saved
930
- * to the user's Downloads folder as it arrives.
956
+ * @example
957
+ * ```ts
958
+ * const result = await analytics.query({
959
+ * queryType: 'topN',
960
+ * field: 'category',
961
+ * n: 3,
962
+ * order: 'asc',
963
+ * });
964
+ * ```
965
+ */
966
+ async query(query) {
967
+ const data = await this.run(query);
968
+ return { queryType: query.queryType, data };
969
+ }
970
+ };
971
+
972
+ // src/storage/manager.ts
973
+ var StorageManager = class {
974
+ constructor(http, storageKey) {
975
+ this.basePath = "/storage";
976
+ this.http = http;
977
+ this.storageKey = storageKey;
978
+ }
979
+ /** Headers for all storage requests — uses X-Storage-Key, not X-Api-Key. */
980
+ get authHeaders() {
981
+ return { "X-Storage-Key": this.storageKey };
982
+ }
983
+ // ─── Upload: Simple (server-buffered) ────────────────────────────────────
984
+ /**
985
+ * Upload a file to storage in one step (server-buffered, up to 500 MB).
986
+ * For files >10 MB or when you need upload progress, use `getUploadUrl()` instead.
931
987
  *
932
- * @param bucketKey Your storage bucket key (`ssk_…`)
933
- * @param filePaths Array of file paths within your bucket
934
- * @param options Concurrency, onProgress, autoSave
988
+ * @param data File data as a Blob, Buffer, Uint8Array, or ArrayBuffer.
989
+ * @param path Destination path in your storage (e.g. `"avatars/alice.jpg"`).
990
+ * @param options Upload options: isPublic, overwrite, mimeType.
935
991
  *
936
992
  * @example
937
- * const { data } = await hydrous.storage.batchDownload(
938
- * 'ssk_my_bucket_key',
939
- * ['reports/jan.pdf', 'reports/feb.pdf'],
940
- * {
941
- * onProgress: (p) => console.log(p.path, p.status),
942
- * autoSave: true, // triggers browser file-save dialog per file
943
- * }
993
+ * ```ts
994
+ * // Upload a public avatar
995
+ * const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
996
+ * console.log(result.publicUrl); // → https://...
997
+ *
998
+ * // Upload a private document
999
+ * const result = await storage.upload(pdfBuffer, 'docs/contract.pdf');
1000
+ * console.log(result.downloadUrl); // → /storage/download/docs/contract.pdf
1001
+ * ```
1002
+ */
1003
+ async upload(data, path, options = {}) {
1004
+ const { isPublic = false, overwrite = false, mimeType } = options;
1005
+ const mime = mimeType ?? guessMimeType(path);
1006
+ const formData = new FormData();
1007
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mime });
1008
+ formData.append("file", blob, path.split("/").pop() ?? "file");
1009
+ formData.append("path", path);
1010
+ formData.append("mimeType", mime);
1011
+ formData.append("isPublic", String(isPublic));
1012
+ formData.append("overwrite", String(overwrite));
1013
+ const result = await this.http.request(`${this.basePath}/upload`, {
1014
+ method: "POST",
1015
+ rawBody: formData,
1016
+ headers: this.authHeaders
1017
+ });
1018
+ return {
1019
+ path: result.path,
1020
+ mimeType: result.mimeType,
1021
+ size: result.size,
1022
+ isPublic: result.isPublic,
1023
+ publicUrl: result.publicUrl,
1024
+ downloadUrl: result.downloadUrl
1025
+ };
1026
+ }
1027
+ /**
1028
+ * Upload raw JSON or plain text data as a file.
1029
+ *
1030
+ * @example
1031
+ * ```ts
1032
+ * const result = await storage.uploadRaw(
1033
+ * { config: { theme: 'dark' } },
1034
+ * 'settings/user-config.json',
1035
+ * { isPublic: false },
944
1036
  * );
1037
+ * ```
945
1038
  */
946
- async batchDownload(bucketKey, filePaths, options = {}) {
947
- var _a;
948
- const { concurrency = 5, onProgress, autoSave = false } = options;
949
- try {
950
- const url = storageUrl(this.baseUrl, bucketKey, "batch-download");
951
- const res = await fetch(url, {
952
- method: "POST",
953
- headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
954
- body: JSON.stringify({ paths: filePaths, concurrency })
955
- });
956
- if (!res.ok) {
957
- const e = await res.json().catch(() => ({}));
958
- throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
959
- }
960
- const downloadedFiles = [];
961
- await readSSEStream(res, (eventType, data) => {
962
- var _a2, _b, _c, _d, _e, _f, _g, _h;
963
- const d = data;
964
- if (eventType === "file") {
965
- const base64 = d["content"];
966
- const mimeType = (_a2 = d["mimeType"]) != null ? _a2 : "application/octet-stream";
967
- const path = (_b = d["path"]) != null ? _b : "";
968
- const size = (_c = d["size"]) != null ? _c : 0;
969
- const index = (_d = d["index"]) != null ? _d : 0;
970
- const binary = atob(base64);
971
- const bytes = new Uint8Array(binary.length);
972
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
973
- const content = bytes.buffer;
974
- downloadedFiles.push({ path, content, mimeType, size });
975
- if (onProgress) {
976
- onProgress({
977
- index,
978
- total: filePaths.length,
979
- path,
980
- status: "success",
981
- size,
982
- mimeType
983
- });
984
- }
985
- if (autoSave && isBrowser) {
986
- const blob = new Blob([content], { type: mimeType });
987
- const blobUrl = URL.createObjectURL(blob);
988
- const a = document.createElement("a");
989
- a.href = blobUrl;
990
- a.download = (_e = path.split("/").pop()) != null ? _e : "download";
991
- a.click();
992
- setTimeout(() => URL.revokeObjectURL(blobUrl), 5e3);
993
- }
994
- }
995
- if (eventType === "error" && onProgress) {
996
- const index = (_f = d["index"]) != null ? _f : 0;
997
- onProgress({
998
- index,
999
- total: filePaths.length,
1000
- path: (_g = filePaths[index]) != null ? _g : "",
1001
- status: "error",
1002
- error: (_h = d["error"]) != null ? _h : "Download failed"
1003
- });
1004
- }
1005
- });
1006
- return { data: downloadedFiles, error: null };
1007
- } catch (err) {
1008
- return { data: null, error: toHydrousError(err) };
1009
- }
1039
+ async uploadRaw(data, path, options = {}) {
1040
+ const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
1041
+ const body = typeof data === "string" ? data : JSON.stringify(data);
1042
+ const result = await this.http.request(`${this.basePath}/upload-raw`, {
1043
+ method: "POST",
1044
+ body: { path, data: body, mimeType, isPublic, overwrite },
1045
+ headers: this.authHeaders
1046
+ });
1047
+ return {
1048
+ path: result.path,
1049
+ mimeType: result.mimeType,
1050
+ size: result.size,
1051
+ isPublic: result.isPublic,
1052
+ publicUrl: result.publicUrl,
1053
+ downloadUrl: result.downloadUrl
1054
+ };
1010
1055
  }
1011
- // ══════════════════════════════════════════════════════════════════════════
1012
- // LIST
1013
- // ══════════════════════════════════════════════════════════════════════════
1056
+ // ─── Upload: Direct-to-GCS (recommended for large files) ─────────────────
1014
1057
  /**
1015
- * List files and folders inside a bucket (or a folder within it).
1058
+ * Step 1 of the recommended upload flow.
1059
+ * Get a signed URL to upload directly to GCS from the client (supports progress).
1060
+ *
1061
+ * @example
1062
+ * ```ts
1063
+ * const { uploadUrl, path: confirmedPath } = await storage.getUploadUrl({
1064
+ * path: 'videos/intro.mp4',
1065
+ * mimeType: 'video/mp4',
1066
+ * size: file.size,
1067
+ * isPublic: true,
1068
+ * });
1016
1069
  *
1017
- * Results are paginated use `pagination.nextCursor` to fetch the next page.
1070
+ * // Upload using XHR for progress tracking
1071
+ * await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (pct) => {
1072
+ * console.log(`${pct}% uploaded`);
1073
+ * });
1074
+ *
1075
+ * // Step 3: confirm
1076
+ * const result = await storage.confirmUpload({ path: confirmedPath, mimeType: 'video/mp4', isPublic: true });
1077
+ * ```
1078
+ */
1079
+ async getUploadUrl(opts) {
1080
+ const result = await this.http.request(`${this.basePath}/upload-url`, {
1081
+ method: "POST",
1082
+ body: { isPublic: false, overwrite: false, expiresInSeconds: 900, ...opts },
1083
+ headers: this.authHeaders
1084
+ });
1085
+ return result;
1086
+ }
1087
+ /**
1088
+ * Upload data directly to a signed GCS URL (no auth headers needed).
1089
+ * Optionally tracks progress via a callback.
1018
1090
  *
1019
- * @param bucketKey Your storage bucket key (`ssk_…`)
1020
- * @param options `prefix`, `limit`, `cursor`
1091
+ * @param signedUrl The URL returned by `getUploadUrl()`.
1092
+ * @param data File data.
1093
+ * @param mimeType Must match what was used in `getUploadUrl()`.
1094
+ * @param onProgress Optional callback called with 0–100 progress percentage.
1095
+ */
1096
+ async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
1097
+ await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
1098
+ }
1099
+ /**
1100
+ * Step 3 of the recommended upload flow.
1101
+ * Confirm a direct upload and register metadata on the server.
1021
1102
  *
1022
1103
  * @example
1023
- * const { data } = await hydrous.storage.list('ssk_my_bucket_key', {
1024
- * prefix: 'avatars/',
1025
- * limit: 50,
1104
+ * ```ts
1105
+ * const result = await storage.confirmUpload({
1106
+ * path: 'videos/intro.mp4',
1107
+ * mimeType: 'video/mp4',
1108
+ * isPublic: true,
1026
1109
  * });
1027
- * for (const item of data.items) {
1028
- * console.log(item.type, item.path);
1110
+ * console.log(result.publicUrl);
1111
+ * ```
1112
+ */
1113
+ async confirmUpload(opts) {
1114
+ const result = await this.http.request(`${this.basePath}/confirm`, {
1115
+ method: "POST",
1116
+ body: { isPublic: false, ...opts },
1117
+ headers: this.authHeaders
1118
+ });
1119
+ return {
1120
+ path: result.path,
1121
+ mimeType: result.mimeType,
1122
+ size: result.size,
1123
+ isPublic: result.isPublic,
1124
+ publicUrl: result.publicUrl,
1125
+ downloadUrl: result.downloadUrl
1126
+ };
1127
+ }
1128
+ // ─── Batch Upload ─────────────────────────────────────────────────────────
1129
+ /**
1130
+ * Get signed upload URLs for multiple files at once.
1131
+ *
1132
+ * @example
1133
+ * ```ts
1134
+ * const { files } = await storage.getBatchUploadUrls([
1135
+ * { path: 'images/photo1.jpg', mimeType: 'image/jpeg', size: 204800 },
1136
+ * { path: 'images/photo2.jpg', mimeType: 'image/jpeg', size: 153600 },
1137
+ * ]);
1138
+ *
1139
+ * // Upload each file and confirm
1140
+ * for (const f of files) {
1141
+ * await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
1142
+ * await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
1029
1143
  * }
1144
+ * ```
1030
1145
  */
1031
- async list(bucketKey, options = {}) {
1032
- const { prefix = "", limit = 50, cursor } = options;
1033
- try {
1034
- const params = {
1035
- prefix: prefix || void 0,
1036
- limit,
1037
- cursor: cursor || void 0
1038
- };
1039
- const url = buildUrl(
1040
- this.baseUrl,
1041
- `storage/${bucketFromKey(bucketKey)}/list`,
1042
- params
1043
- );
1044
- const res = await fetch(url, { headers: storageHeaders(bucketKey) });
1045
- const json = await parseResponse(res);
1046
- return { data: json, error: null };
1047
- } catch (err) {
1048
- return { data: null, error: toHydrousError(err) };
1049
- }
1146
+ async getBatchUploadUrls(files) {
1147
+ const result = await this.http.request(
1148
+ `${this.basePath}/batch-upload-urls`,
1149
+ { method: "POST", body: { files }, headers: this.authHeaders }
1150
+ );
1151
+ return { files: result.files };
1050
1152
  }
1051
- // ══════════════════════════════════════════════════════════════════════════
1052
- // METADATA
1053
- // ══════════════════════════════════════════════════════════════════════════
1054
1153
  /**
1055
- * Get metadata for a specific file (size, MIME type, compression info, etc.)
1154
+ * Confirm multiple direct uploads at once.
1155
+ */
1156
+ async batchConfirmUploads(items) {
1157
+ const result = await this.http.request(
1158
+ `${this.basePath}/batch-confirm`,
1159
+ { method: "POST", body: { files: items }, headers: this.authHeaders }
1160
+ );
1161
+ return result.files.map((r) => ({
1162
+ path: r.path,
1163
+ mimeType: r.mimeType,
1164
+ size: r.size,
1165
+ isPublic: r.isPublic,
1166
+ publicUrl: r.publicUrl,
1167
+ downloadUrl: r.downloadUrl
1168
+ }));
1169
+ }
1170
+ // ─── Download ─────────────────────────────────────────────────────────────
1171
+ /**
1172
+ * Download a private file as an ArrayBuffer.
1173
+ * For public files, use the `publicUrl` directly — no SDK needed.
1056
1174
  *
1057
- * @param bucketKey Your storage bucket key (`ssk_…`)
1058
- * @param filePath Path of the file within your bucket
1175
+ * @example
1176
+ * ```ts
1177
+ * const buffer = await storage.download('docs/contract.pdf');
1178
+ * const blob = new Blob([buffer], { type: 'application/pdf' });
1179
+ * // Open in browser:
1180
+ * window.open(URL.createObjectURL(blob));
1181
+ * ```
1182
+ */
1183
+ async download(path) {
1184
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
1185
+ return this.http.request(`${this.basePath}/download/${encoded}`, {
1186
+ method: "GET",
1187
+ headers: this.authHeaders
1188
+ });
1189
+ }
1190
+ /**
1191
+ * Download multiple files at once, returned as a JSON map of `{ path: base64 }`.
1059
1192
  *
1060
1193
  * @example
1061
- * const { data } = await hydrous.storage.metadata(
1062
- * 'ssk_my_bucket_key',
1063
- * 'avatars/alice.jpg'
1064
- * );
1065
- * console.log(data.size, data.mimeType);
1194
+ * ```ts
1195
+ * const files = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
1196
+ * ```
1066
1197
  */
1067
- async metadata(bucketKey, filePath) {
1068
- try {
1069
- const url = storageUrl(this.baseUrl, bucketKey, `metadata/${filePath}`);
1070
- const res = await fetch(url, { headers: storageHeaders(bucketKey) });
1071
- const json = await parseResponse(res);
1072
- return { data: json.data, error: null };
1073
- } catch (err) {
1074
- return { data: null, error: toHydrousError(err) };
1075
- }
1198
+ async batchDownload(paths) {
1199
+ const result = await this.http.request(
1200
+ `${this.basePath}/batch-download`,
1201
+ { method: "POST", body: { paths }, headers: this.authHeaders }
1202
+ );
1203
+ return result.files;
1204
+ }
1205
+ // ─── List ─────────────────────────────────────────────────────────────────
1206
+ /**
1207
+ * List files and folders at a given path prefix.
1208
+ *
1209
+ * @example
1210
+ * ```ts
1211
+ * // List everything in the root
1212
+ * const { files, folders } = await storage.list();
1213
+ *
1214
+ * // List a specific folder
1215
+ * const { files, folders, hasMore, nextCursor } = await storage.list({
1216
+ * prefix: 'avatars/',
1217
+ * limit: 20,
1218
+ * });
1219
+ *
1220
+ * // Next page
1221
+ * const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
1222
+ * ```
1223
+ */
1224
+ async list(opts = {}) {
1225
+ const params = new URLSearchParams();
1226
+ if (opts.prefix) params.set("prefix", opts.prefix);
1227
+ if (opts.limit) params.set("limit", String(opts.limit));
1228
+ if (opts.cursor) params.set("cursor", opts.cursor);
1229
+ if (opts.recursive) params.set("recursive", "true");
1230
+ const qs = params.toString() ? `?${params}` : "";
1231
+ const result = await this.http.request(`${this.basePath}/list${qs}`, {
1232
+ method: "GET",
1233
+ headers: this.authHeaders
1234
+ });
1235
+ return {
1236
+ files: result.files,
1237
+ folders: result.folders,
1238
+ hasMore: result.hasMore,
1239
+ nextCursor: result.nextCursor
1240
+ };
1241
+ }
1242
+ // ─── Metadata ─────────────────────────────────────────────────────────────
1243
+ /**
1244
+ * Get metadata for a file (size, MIME type, visibility, URLs).
1245
+ *
1246
+ * @example
1247
+ * ```ts
1248
+ * const meta = await storage.getMetadata('avatars/alice.jpg');
1249
+ * console.log(meta.size, meta.isPublic, meta.publicUrl);
1250
+ * ```
1251
+ */
1252
+ async getMetadata(path) {
1253
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
1254
+ const result = await this.http.request(
1255
+ `${this.basePath}/metadata/${encoded}`,
1256
+ { method: "GET", headers: this.authHeaders }
1257
+ );
1258
+ return {
1259
+ path: result.path,
1260
+ size: result.size,
1261
+ mimeType: result.mimeType,
1262
+ isPublic: result.isPublic,
1263
+ publicUrl: result.publicUrl,
1264
+ downloadUrl: result.downloadUrl,
1265
+ createdAt: result.createdAt,
1266
+ updatedAt: result.updatedAt
1267
+ };
1268
+ }
1269
+ // ─── Signed URL (time-limited share) ─────────────────────────────────────
1270
+ /**
1271
+ * Generate a time-limited download URL for a private file.
1272
+ * The URL can be shared externally without requiring an `X-Storage-Key`.
1273
+ *
1274
+ * > **Note:** Downloads via signed URLs bypass the server, so download stats
1275
+ * > are NOT tracked. Use `downloadUrl` for tracked downloads.
1276
+ *
1277
+ * @param path Path to the file.
1278
+ * @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
1279
+ *
1280
+ * @example
1281
+ * ```ts
1282
+ * const { signedUrl, expiresAt } = await storage.getSignedUrl('docs/invoice.pdf', 1800);
1283
+ * // Share signedUrl with the recipient — it expires in 30 minutes.
1284
+ * ```
1285
+ */
1286
+ async getSignedUrl(path, expiresIn = 3600) {
1287
+ const result = await this.http.request(`${this.basePath}/signed-url`, {
1288
+ method: "POST",
1289
+ body: { path, expiresIn },
1290
+ headers: this.authHeaders
1291
+ });
1292
+ return {
1293
+ signedUrl: result.signedUrl,
1294
+ expiresAt: result.expiresAt,
1295
+ expiresIn: result.expiresIn,
1296
+ path: result.path
1297
+ };
1298
+ }
1299
+ // ─── Visibility ───────────────────────────────────────────────────────────
1300
+ /**
1301
+ * Change a file's visibility between public and private after upload.
1302
+ *
1303
+ * @example
1304
+ * ```ts
1305
+ * // Make a file public
1306
+ * const result = await storage.setVisibility('avatars/alice.jpg', true);
1307
+ * console.log(result.publicUrl); // CDN URL
1308
+ *
1309
+ * // Make a file private
1310
+ * const result = await storage.setVisibility('avatars/alice.jpg', false);
1311
+ * console.log(result.downloadUrl); // Auth-required URL
1312
+ * ```
1313
+ */
1314
+ async setVisibility(path, isPublic) {
1315
+ const result = await this.http.request(`${this.basePath}/visibility`, {
1316
+ method: "PATCH",
1317
+ body: { path, isPublic },
1318
+ headers: this.authHeaders
1319
+ });
1320
+ return {
1321
+ path: result.path,
1322
+ isPublic: result.isPublic,
1323
+ publicUrl: result.publicUrl,
1324
+ downloadUrl: result.downloadUrl
1325
+ };
1326
+ }
1327
+ // ─── Folder Operations ────────────────────────────────────────────────────
1328
+ /**
1329
+ * Create a folder (a GCS prefix placeholder).
1330
+ *
1331
+ * @example
1332
+ * ```ts
1333
+ * await storage.createFolder('uploads/2025/');
1334
+ * ```
1335
+ */
1336
+ async createFolder(path) {
1337
+ const result = await this.http.request(
1338
+ `${this.basePath}/folder`,
1339
+ { method: "POST", body: { path }, headers: this.authHeaders }
1340
+ );
1341
+ return { path: result.path };
1076
1342
  }
1077
- // ══════════════════════════════════════════════════════════════════════════
1078
- // DELETE FILE
1079
- // ══════════════════════════════════════════════════════════════════════════
1343
+ // ─── File Operations ──────────────────────────────────────────────────────
1080
1344
  /**
1081
1345
  * Delete a single file.
1082
1346
  *
1083
- * @param bucketKey Your storage bucket key (`ssk_…`)
1084
- * @param filePath Path of the file to delete
1347
+ * @example
1348
+ * ```ts
1349
+ * await storage.deleteFile('avatars/old-avatar.jpg');
1350
+ * ```
1351
+ */
1352
+ async deleteFile(path) {
1353
+ await this.http.request(`${this.basePath}/file`, {
1354
+ method: "DELETE",
1355
+ body: { path },
1356
+ headers: this.authHeaders
1357
+ });
1358
+ }
1359
+ /**
1360
+ * Delete a folder and all its contents recursively.
1085
1361
  *
1086
1362
  * @example
1087
- * await hydrous.storage.deleteFile('ssk_my_bucket_key', 'avatars/old.jpg');
1363
+ * ```ts
1364
+ * await storage.deleteFolder('temp/');
1365
+ * ```
1088
1366
  */
1089
- async deleteFile(bucketKey, filePath) {
1090
- try {
1091
- const url = storageUrl(this.baseUrl, bucketKey, "file");
1092
- const res = await fetch(url, {
1093
- method: "DELETE",
1094
- headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1095
- body: JSON.stringify({ path: filePath })
1096
- });
1097
- await parseResponse(res);
1098
- return { data: void 0, error: null };
1099
- } catch (err) {
1100
- return { data: null, error: toHydrousError(err) };
1101
- }
1367
+ async deleteFolder(path) {
1368
+ await this.http.request(`${this.basePath}/folder`, {
1369
+ method: "DELETE",
1370
+ body: { path },
1371
+ headers: this.authHeaders
1372
+ });
1102
1373
  }
1103
- // ══════════════════════════════════════════════════════════════════════════
1104
- // DELETE FOLDER
1105
- // ══════════════════════════════════════════════════════════════════════════
1106
1374
  /**
1107
- * Recursively delete a folder and all of its contents.
1375
+ * Move or rename a file.
1108
1376
  *
1109
- * @param bucketKey Your storage bucket key (`ssk_…`)
1110
- * @param folderPath Folder path to delete (e.g. `"old-uploads/"`)
1377
+ * @example
1378
+ * ```ts
1379
+ * // Rename
1380
+ * await storage.move('docs/draft.pdf', 'docs/final.pdf');
1381
+ * // Move to a different folder
1382
+ * await storage.move('inbox/report.xlsx', 'archive/2025/report.xlsx');
1383
+ * ```
1384
+ */
1385
+ async move(from, to) {
1386
+ const result = await this.http.request(
1387
+ `${this.basePath}/move`,
1388
+ { method: "POST", body: { from, to }, headers: this.authHeaders }
1389
+ );
1390
+ return { from: result.from, to: result.to };
1391
+ }
1392
+ /**
1393
+ * Copy a file to a new path.
1111
1394
  *
1112
1395
  * @example
1113
- * await hydrous.storage.deleteFolder('ssk_my_bucket_key', 'temp/');
1396
+ * ```ts
1397
+ * await storage.copy('templates/base.html', 'sites/my-site/index.html');
1398
+ * ```
1114
1399
  */
1115
- async deleteFolder(bucketKey, folderPath) {
1116
- try {
1117
- const url = storageUrl(this.baseUrl, bucketKey, "folder");
1118
- const res = await fetch(url, {
1119
- method: "DELETE",
1120
- headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1121
- body: JSON.stringify({ path: folderPath })
1122
- });
1123
- await parseResponse(res);
1124
- return { data: void 0, error: null };
1125
- } catch (err) {
1126
- return { data: null, error: toHydrousError(err) };
1127
- }
1400
+ async copy(from, to) {
1401
+ const result = await this.http.request(
1402
+ `${this.basePath}/copy`,
1403
+ { method: "POST", body: { from, to }, headers: this.authHeaders }
1404
+ );
1405
+ return { from: result.from, to: result.to };
1128
1406
  }
1129
- // ══════════════════════════════════════════════════════════════════════════
1130
- // CREATE FOLDER
1131
- // ══════════════════════════════════════════════════════════════════════════
1407
+ // ─── Stats ────────────────────────────────────────────────────────────────
1132
1408
  /**
1133
- * Create an empty folder.
1409
+ * Get storage statistics for your key: total files, bytes, operation counts.
1134
1410
  *
1135
- * @param bucketKey Your storage bucket key (`ssk_…`)
1136
- * @param folderPath Path for the new folder (e.g. `"avatars/2024/"`)
1411
+ * @example
1412
+ * ```ts
1413
+ * const stats = await storage.getStats();
1414
+ * console.log(`${stats.totalFiles} files, ${(stats.totalBytes / 1e6).toFixed(1)} MB`);
1415
+ * ```
1416
+ */
1417
+ async getStats() {
1418
+ const result = await this.http.request(`${this.basePath}/stats`, {
1419
+ method: "GET",
1420
+ headers: this.authHeaders
1421
+ });
1422
+ return result.stats;
1423
+ }
1424
+ // ─── Info (no auth) ───────────────────────────────────────────────────────
1425
+ /**
1426
+ * Ping the storage service. No authentication required.
1137
1427
  *
1138
1428
  * @example
1139
- * await hydrous.storage.createFolder('ssk_my_bucket_key', 'avatars/2024/');
1429
+ * ```ts
1430
+ * const info = await storage.info();
1431
+ * // → { ok: true, storageRoot: 'hydrous-storage' }
1432
+ * ```
1140
1433
  */
1141
- async createFolder(bucketKey, folderPath) {
1142
- try {
1143
- const url = storageUrl(this.baseUrl, bucketKey, "folder");
1144
- const res = await fetch(url, {
1145
- method: "POST",
1146
- headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1147
- body: JSON.stringify({ path: folderPath })
1148
- });
1149
- await parseResponse(res);
1150
- return { data: void 0, error: null };
1151
- } catch (err) {
1152
- return { data: null, error: toHydrousError(err) };
1434
+ async info() {
1435
+ return this.http.get(`${this.basePath}/info`);
1436
+ }
1437
+ };
1438
+
1439
+ // src/storage/scoped.ts
1440
+ var ScopedStorage = class _ScopedStorage {
1441
+ constructor(manager, prefix) {
1442
+ this.manager = manager;
1443
+ this.prefix = prefix.replace(/\/+$/, "") + "/";
1444
+ }
1445
+ scopedPath(userPath) {
1446
+ return `${this.prefix}${userPath.replace(/^\/+/, "")}`;
1447
+ }
1448
+ /** Upload a file within the scoped folder. */
1449
+ upload(data, path, options) {
1450
+ return this.manager.upload(data, this.scopedPath(path), options);
1451
+ }
1452
+ /** Upload raw JSON or text within the scoped folder. */
1453
+ uploadRaw(data, path, options) {
1454
+ return this.manager.uploadRaw(data, this.scopedPath(path), options);
1455
+ }
1456
+ /** Get a signed upload URL for a file within the scoped folder. */
1457
+ getUploadUrl(opts) {
1458
+ return this.manager.getUploadUrl({ ...opts, path: this.scopedPath(opts.path) });
1459
+ }
1460
+ /** Upload data directly to a signed GCS URL with optional progress tracking. */
1461
+ uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
1462
+ return this.manager.uploadToSignedUrl(signedUrl, data, mimeType, onProgress);
1463
+ }
1464
+ /** Confirm a direct upload within the scoped folder. */
1465
+ confirmUpload(opts) {
1466
+ return this.manager.confirmUpload({ ...opts, path: this.scopedPath(opts.path) });
1467
+ }
1468
+ /** Download a file within the scoped folder. */
1469
+ download(path) {
1470
+ return this.manager.download(this.scopedPath(path));
1471
+ }
1472
+ /** List files within the scoped folder. */
1473
+ list(opts = {}) {
1474
+ return this.manager.list({ ...opts, prefix: this.scopedPath(opts.prefix ?? "") });
1475
+ }
1476
+ /** Get metadata for a file within the scoped folder. */
1477
+ getMetadata(path) {
1478
+ return this.manager.getMetadata(this.scopedPath(path));
1479
+ }
1480
+ /** Get a time-limited signed URL for a file within the scoped folder. */
1481
+ getSignedUrl(path, expiresIn) {
1482
+ return this.manager.getSignedUrl(this.scopedPath(path), expiresIn);
1483
+ }
1484
+ /** Change visibility of a file within the scoped folder. */
1485
+ setVisibility(path, isPublic) {
1486
+ return this.manager.setVisibility(this.scopedPath(path), isPublic);
1487
+ }
1488
+ /** Delete a file within the scoped folder. */
1489
+ deleteFile(path) {
1490
+ return this.manager.deleteFile(this.scopedPath(path));
1491
+ }
1492
+ /** Delete a sub-folder within the scoped folder. */
1493
+ deleteFolder(path) {
1494
+ return this.manager.deleteFolder(this.scopedPath(path));
1495
+ }
1496
+ /** Move a file within the scoped folder. */
1497
+ move(from, to) {
1498
+ return this.manager.move(this.scopedPath(from), this.scopedPath(to));
1499
+ }
1500
+ /** Copy a file within the scoped folder. */
1501
+ copy(from, to) {
1502
+ return this.manager.copy(this.scopedPath(from), this.scopedPath(to));
1503
+ }
1504
+ /** Create a sub-folder within the scoped folder. */
1505
+ createFolder(path) {
1506
+ return this.manager.createFolder(this.scopedPath(path));
1507
+ }
1508
+ /**
1509
+ * Create a further-scoped instance nested within this scope.
1510
+ *
1511
+ * @example
1512
+ * ```ts
1513
+ * const uploads = db.storage.scope('user-uploads');
1514
+ * const images = uploads.scope('images'); // → "user-uploads/images/"
1515
+ * ```
1516
+ */
1517
+ scope(subPrefix) {
1518
+ return new _ScopedStorage(this.manager, this.scopedPath(subPrefix));
1519
+ }
1520
+ };
1521
+
1522
+ // src/client.ts
1523
+ var HydrousClient = class {
1524
+ constructor(config) {
1525
+ this._storage = null;
1526
+ this._recordsCache = /* @__PURE__ */ new Map();
1527
+ this._authCache = /* @__PURE__ */ new Map();
1528
+ this._analyticsCache = /* @__PURE__ */ new Map();
1529
+ if (!config.securityKey) {
1530
+ throw new Error(
1531
+ "[HydrousDB] securityKey is required. Get yours from https://hydrousdb.com/dashboard."
1532
+ );
1153
1533
  }
1534
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1535
+ this.http = new HttpClient(baseUrl, config.securityKey);
1536
+ this._storageKey = config.securityKey;
1154
1537
  }
1155
- // ══════════════════════════════════════════════════════════════════════════
1156
- // MOVE
1157
- // ══════════════════════════════════════════════════════════════════════════
1538
+ // ─── Records ─────────────────────────────────────────────────────────────
1158
1539
  /**
1159
- * Move (rename) a file to a new path.
1540
+ * Get a typed records client for the given bucket.
1541
+ *
1542
+ * The generic type parameter `T` describes the shape of records in this
1543
+ * bucket. Leave it unset for a generic `Record<string, unknown>` shape.
1160
1544
  *
1161
- * @param bucketKey Your storage bucket key (`ssk_…`)
1162
- * @param fromPath Current path of the file
1163
- * @param toPath New path for the file
1545
+ * @param bucketKey The name of your bucket (must match what you created in the dashboard).
1164
1546
  *
1165
1547
  * @example
1166
- * await hydrous.storage.move(
1167
- * 'ssk_my_bucket_key',
1168
- * 'drafts/report.pdf',
1169
- * 'published/report.pdf'
1170
- * );
1548
+ * ```ts
1549
+ * interface Post { title: string; body: string; published: boolean }
1550
+ * const posts = db.records<Post>('blog-posts');
1551
+ *
1552
+ * const post = await posts.create({ title: 'Hello', body: '...', published: false });
1553
+ * // post.id, post.createdAt, post.updatedAt are added automatically
1554
+ * ```
1171
1555
  */
1172
- async move(bucketKey, fromPath, toPath) {
1173
- try {
1174
- const url = storageUrl(this.baseUrl, bucketKey, "move");
1175
- const res = await fetch(url, {
1176
- method: "POST",
1177
- headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1178
- body: JSON.stringify({ from: fromPath, to: toPath })
1179
- });
1180
- await parseResponse(res);
1181
- return { data: void 0, error: null };
1182
- } catch (err) {
1183
- return { data: null, error: toHydrousError(err) };
1556
+ records(bucketKey) {
1557
+ if (!this._recordsCache.has(bucketKey)) {
1558
+ this._recordsCache.set(bucketKey, new RecordsClient(this.http, bucketKey));
1184
1559
  }
1560
+ return this._recordsCache.get(bucketKey);
1185
1561
  }
1186
- // ══════════════════════════════════════════════════════════════════════════
1187
- // COPY
1188
- // ══════════════════════════════════════════════════════════════════════════
1562
+ // ─── Auth ─────────────────────────────────────────────────────────────────
1189
1563
  /**
1190
- * Copy a file to a new path (original is kept).
1564
+ * Get an auth client for the given user bucket.
1191
1565
  *
1192
- * @param bucketKey Your storage bucket key (`ssk_…`)
1193
- * @param fromPath Source path
1194
- * @param toPath Destination path
1566
+ * @param bucketKey The name of your user bucket (e.g. `"app-users"`).
1195
1567
  *
1196
1568
  * @example
1197
- * await hydrous.storage.copy(
1198
- * 'ssk_my_bucket_key',
1199
- * 'templates/invoice.pdf',
1200
- * 'invoices/invoice-001.pdf'
1201
- * );
1569
+ * ```ts
1570
+ * const auth = db.auth('app-users');
1571
+ * const { user, session } = await auth.login({ email: '...', password: '...' });
1572
+ * ```
1202
1573
  */
1203
- async copy(bucketKey, fromPath, toPath) {
1204
- try {
1205
- const url = storageUrl(this.baseUrl, bucketKey, "copy");
1206
- const res = await fetch(url, {
1207
- method: "POST",
1208
- headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1209
- body: JSON.stringify({ from: fromPath, to: toPath })
1210
- });
1211
- await parseResponse(res);
1212
- return { data: void 0, error: null };
1213
- } catch (err) {
1214
- return { data: null, error: toHydrousError(err) };
1574
+ auth(bucketKey) {
1575
+ if (!this._authCache.has(bucketKey)) {
1576
+ this._authCache.set(bucketKey, new AuthClient(this.http, bucketKey));
1215
1577
  }
1578
+ return this._authCache.get(bucketKey);
1216
1579
  }
1217
- // ══════════════════════════════════════════════════════════════════════════
1218
- // SIGNED URL
1219
- // ══════════════════════════════════════════════════════════════════════════
1580
+ // ─── Analytics ────────────────────────────────────────────────────────────
1220
1581
  /**
1221
- * Generate a time-limited public URL for a private file.
1582
+ * Get an analytics client for the given bucket.
1222
1583
  *
1223
- * @param bucketKey Your storage bucket key (`ssk_…`)
1224
- * @param filePath Path of the file
1225
- * @param options `expiresIn` seconds (default: 3600)
1584
+ * @param bucketKey The name of the bucket to analyse.
1226
1585
  *
1227
1586
  * @example
1228
- * const { data } = await hydrous.storage.signedUrl(
1229
- * 'ssk_my_bucket_key',
1230
- * 'private/contract.pdf',
1231
- * { expiresIn: 300 } // 5 minutes
1232
- * );
1233
- * console.log(data.signedUrl); // share this URL
1587
+ * ```ts
1588
+ * const analytics = db.analytics('orders');
1589
+ * const { count } = await analytics.count();
1590
+ * ```
1234
1591
  */
1235
- async signedUrl(bucketKey, filePath, options = {}) {
1236
- const { expiresIn = 3600 } = options;
1237
- try {
1238
- const url = storageUrl(this.baseUrl, bucketKey, "signed-url");
1239
- const res = await fetch(url, {
1240
- method: "POST",
1241
- headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1242
- body: JSON.stringify({ path: filePath, expiresInSeconds: expiresIn })
1243
- });
1244
- const json = await parseResponse(res);
1245
- return { data: json, error: null };
1246
- } catch (err) {
1247
- return { data: null, error: toHydrousError(err) };
1592
+ analytics(bucketKey) {
1593
+ if (!this._analyticsCache.has(bucketKey)) {
1594
+ this._analyticsCache.set(bucketKey, new AnalyticsClient(this.http, bucketKey));
1248
1595
  }
1596
+ return this._analyticsCache.get(bucketKey);
1249
1597
  }
1250
- // ══════════════════════════════════════════════════════════════════════════
1251
- // STATS
1252
- // ══════════════════════════════════════════════════════════════════════════
1598
+ // ─── Storage ──────────────────────────────────────────────────────────────
1253
1599
  /**
1254
- * Get usage and billing statistics for this bucket key.
1600
+ * The storage manager for uploading, downloading, listing, and managing files.
1255
1601
  *
1256
- * @param bucketKey Your storage bucket key (`ssk_…`)
1602
+ * Scoped to your project you can never access another project's files.
1257
1603
  *
1258
1604
  * @example
1259
- * const { data } = await hydrous.storage.stats('ssk_my_bucket_key');
1260
- * console.log(`${data.totalFiles} files, ${data.totalSizeBytes} bytes stored`);
1605
+ * ```ts
1606
+ * // Upload a file
1607
+ * const result = await db.storage.upload(file, 'images/photo.jpg', { isPublic: true });
1608
+ *
1609
+ * // Scope to a folder
1610
+ * const avatars = db.storage.scope('user-avatars');
1611
+ * await avatars.upload(blob, `${userId}.jpg`, { isPublic: true });
1612
+ * ```
1261
1613
  */
1262
- async stats(bucketKey) {
1263
- try {
1264
- const url = buildUrl(this.baseUrl, `storage/${bucketFromKey(bucketKey)}/stats`);
1265
- const res = await fetch(url, { headers: storageHeaders(bucketKey) });
1266
- const json = await parseResponse(res);
1267
- return { data: json.data, error: null };
1268
- } catch (err) {
1269
- return { data: null, error: toHydrousError(err) };
1614
+ get storage() {
1615
+ if (!this._storage) {
1616
+ this._storage = new StorageManager(this.http, this._storageKey);
1270
1617
  }
1618
+ const mgr = this._storage;
1619
+ const extended = mgr;
1620
+ if (!extended.scope) {
1621
+ extended.scope = (prefix) => new ScopedStorage(mgr, prefix);
1622
+ }
1623
+ return extended;
1271
1624
  }
1272
1625
  };
1273
-
1274
- // src/client.ts
1275
- var HydrousClient = class {
1276
- constructor(config) {
1277
- if (!config.url) throw new Error("[Hydrous] config.url is required");
1278
- if (!config.apiKey) throw new Error("[Hydrous] config.apiKey is required");
1279
- this.auth = new AuthClient(config);
1280
- this.records = new RecordsClient(config);
1281
- this.analytics = new AnalyticsClient(config);
1282
- this.storage = new StorageClient(config);
1283
- }
1284
- };
1285
-
1286
- // src/index.ts
1287
1626
  function createClient(config) {
1288
1627
  return new HydrousClient(config);
1289
1628
  }
1290
1629
 
1291
- exports.AnalyticsClient = AnalyticsClient;
1292
- exports.AuthClient = AuthClient;
1293
- exports.HydrousClient = HydrousClient;
1294
- exports.HydrousSDKError = HydrousSDKError;
1295
- exports.RecordsClient = RecordsClient;
1296
- exports.StorageClient = StorageClient;
1297
- exports.createClient = createClient;
1298
- exports.eq = eq;
1299
- exports.gt = gt;
1300
- exports.inArray = inArray;
1301
- exports.isHydrousError = isHydrousError;
1302
- exports.lt = lt;
1303
- exports.neq = neq;
1630
+ export { AnalyticsClient, AnalyticsError, AuthClient, AuthError, HydrousClient, HydrousError, NetworkError, RecordError, RecordsClient, ScopedStorage, StorageError, StorageManager, ValidationError, createClient };
1304
1631
  //# sourceMappingURL=index.js.map
1305
1632
  //# sourceMappingURL=index.js.map