hydrousdb 3.0.2 → 3.5.0

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