radosgw-admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1869 @@
1
+ // src/signer.ts
2
+ import { createHmac, createHash } from "crypto";
3
+ var ALGORITHM = "AWS4-HMAC-SHA256";
4
+ var SERVICE = "s3";
5
+ var UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
6
+ function sha256(data) {
7
+ return createHash("sha256").update(data).digest("hex");
8
+ }
9
+ function hmacSha256(key, data) {
10
+ return createHmac("sha256", key).update(data).digest();
11
+ }
12
+ function hmacSha256Hex(key, data) {
13
+ return createHmac("sha256", key).update(data).digest("hex");
14
+ }
15
+ function getDateStamp(date) {
16
+ return date.toISOString().replace(/[-:]/g, "").slice(0, 8);
17
+ }
18
+ function getAmzDate(date) {
19
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
20
+ }
21
+ function getSigningKey(secretKey, dateStamp, region) {
22
+ const kDate = hmacSha256(`AWS4${secretKey}`, dateStamp);
23
+ const kRegion = hmacSha256(kDate, region);
24
+ const kService = hmacSha256(kRegion, SERVICE);
25
+ return hmacSha256(kService, "aws4_request");
26
+ }
27
+ function getCanonicalQueryString(url) {
28
+ const params = Array.from(url.searchParams.entries());
29
+ params.sort((a, b) => {
30
+ if (a[0] === b[0]) return a[1].localeCompare(b[1]);
31
+ return a[0].localeCompare(b[0]);
32
+ });
33
+ return params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
34
+ }
35
+ function signRequest(request, date) {
36
+ const now = date ?? /* @__PURE__ */ new Date();
37
+ const dateStamp = getDateStamp(now);
38
+ const amzDate = getAmzDate(now);
39
+ const { method, url, accessKey, secretKey, region } = request;
40
+ const headers = {};
41
+ for (const [k, v] of Object.entries(request.headers)) {
42
+ headers[k.toLowerCase()] = v;
43
+ }
44
+ headers["host"] = url.host;
45
+ headers["x-amz-date"] = amzDate;
46
+ headers["x-amz-content-sha256"] = UNSIGNED_PAYLOAD;
47
+ const sortedHeaderKeys = Object.keys(headers).sort((a, b) => a.localeCompare(b));
48
+ const canonicalHeaders = sortedHeaderKeys.map((k) => `${k}:${headers[k].trim()}`).join("\n");
49
+ const signedHeaders = sortedHeaderKeys.join(";");
50
+ const canonicalRequest = [
51
+ method,
52
+ url.pathname,
53
+ getCanonicalQueryString(url),
54
+ canonicalHeaders + "\n",
55
+ signedHeaders,
56
+ UNSIGNED_PAYLOAD
57
+ ].join("\n");
58
+ const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
59
+ const stringToSign = [ALGORITHM, amzDate, credentialScope, sha256(canonicalRequest)].join("\n");
60
+ const signingKey = getSigningKey(secretKey, dateStamp, region);
61
+ const signature = hmacSha256Hex(signingKey, stringToSign);
62
+ const authorization = `${ALGORITHM} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
63
+ return {
64
+ "x-amz-date": amzDate,
65
+ "x-amz-content-sha256": UNSIGNED_PAYLOAD,
66
+ Authorization: authorization
67
+ };
68
+ }
69
+
70
+ // src/errors.ts
71
+ var RGWError = class extends Error {
72
+ constructor(message, statusCode, code, uid) {
73
+ super(message);
74
+ this.statusCode = statusCode;
75
+ this.code = code;
76
+ this.uid = uid;
77
+ this.name = "RGWError";
78
+ }
79
+ };
80
+ var RGWNotFoundError = class extends RGWError {
81
+ constructor(resource, id) {
82
+ super(`${resource} not found: "${id}"`, 404, "NoSuchResource");
83
+ this.name = "RGWNotFoundError";
84
+ }
85
+ };
86
+ var RGWValidationError = class extends RGWError {
87
+ constructor(message) {
88
+ super(message, void 0, "ValidationError");
89
+ this.name = "RGWValidationError";
90
+ }
91
+ };
92
+ var RGWAuthError = class extends RGWError {
93
+ constructor(message) {
94
+ super(message, 403, "AccessDenied");
95
+ this.name = "RGWAuthError";
96
+ }
97
+ };
98
+ var RGWConflictError = class extends RGWError {
99
+ constructor(resource, id) {
100
+ super(`${resource} already exists: "${id}"`, 409, "AlreadyExists");
101
+ this.name = "RGWConflictError";
102
+ }
103
+ };
104
+
105
+ // src/client.ts
106
+ var DEFAULT_ADMIN_PATH = "/admin";
107
+ var DEFAULT_TIMEOUT = 1e4;
108
+ var DEFAULT_REGION = "us-east-1";
109
+ function validateConfig(config) {
110
+ if (!config.host || typeof config.host !== "string") {
111
+ throw new RGWValidationError("host is required and must be a non-empty string");
112
+ }
113
+ if (!config.accessKey || typeof config.accessKey !== "string") {
114
+ throw new RGWValidationError("accessKey is required and must be a non-empty string");
115
+ }
116
+ if (!config.secretKey || typeof config.secretKey !== "string") {
117
+ throw new RGWValidationError("secretKey is required and must be a non-empty string");
118
+ }
119
+ if (config.port !== void 0 && (config.port < 1 || config.port > 65535 || !Number.isInteger(config.port))) {
120
+ throw new RGWValidationError("port must be an integer between 1 and 65535");
121
+ }
122
+ if (config.timeout !== void 0 && (config.timeout < 0 || !Number.isFinite(config.timeout))) {
123
+ throw new RGWValidationError("timeout must be a non-negative finite number");
124
+ }
125
+ if (config.maxRetries !== void 0 && (config.maxRetries < 0 || !Number.isInteger(config.maxRetries))) {
126
+ throw new RGWValidationError("maxRetries must be a non-negative integer");
127
+ }
128
+ if (config.retryDelay !== void 0 && (config.retryDelay < 0 || !Number.isFinite(config.retryDelay))) {
129
+ throw new RGWValidationError("retryDelay must be a non-negative finite number");
130
+ }
131
+ }
132
+ function toCamelCase(obj) {
133
+ if (Array.isArray(obj)) return obj.map(toCamelCase);
134
+ if (obj !== null && typeof obj === "object") {
135
+ return Object.fromEntries(
136
+ Object.entries(obj).map(([k, v]) => [
137
+ k.replaceAll(/[-_.]([a-z])/g, (_, c) => c.toUpperCase()),
138
+ toCamelCase(v)
139
+ ])
140
+ );
141
+ }
142
+ return obj;
143
+ }
144
+ function toKebabCase(key) {
145
+ return key.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
146
+ }
147
+ function mapHttpError(status, body, code) {
148
+ switch (status) {
149
+ case 404:
150
+ return new RGWNotFoundError("Resource", code ?? "unknown");
151
+ case 403:
152
+ return new RGWAuthError(
153
+ body || "Access denied. Check your admin credentials and user capabilities."
154
+ );
155
+ case 409:
156
+ return new RGWConflictError("Resource", code ?? "unknown");
157
+ case 400:
158
+ return new RGWValidationError(body || "Invalid request parameters");
159
+ default:
160
+ return new RGWError(body || `HTTP ${status}`, status, code);
161
+ }
162
+ }
163
+ var BaseClient = class {
164
+ host;
165
+ port;
166
+ accessKey;
167
+ secretKey;
168
+ adminPath;
169
+ timeout;
170
+ region;
171
+ debug;
172
+ maxRetries;
173
+ retryDelay;
174
+ insecure;
175
+ constructor(config) {
176
+ validateConfig(config);
177
+ let host = config.host;
178
+ while (host.endsWith("/")) {
179
+ host = host.slice(0, -1);
180
+ }
181
+ this.host = host;
182
+ this.port = config.port;
183
+ this.accessKey = config.accessKey;
184
+ this.secretKey = config.secretKey;
185
+ this.adminPath = config.adminPath ?? DEFAULT_ADMIN_PATH;
186
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
187
+ this.region = config.region ?? DEFAULT_REGION;
188
+ this.debug = config.debug ?? false;
189
+ this.maxRetries = config.maxRetries ?? 0;
190
+ this.retryDelay = config.retryDelay ?? 200;
191
+ this.insecure = config.insecure ?? false;
192
+ if (this.insecure) {
193
+ this.log("WARNING: TLS certificate verification is disabled");
194
+ }
195
+ }
196
+ /**
197
+ * Logs a debug message with optional structured data.
198
+ */
199
+ log(message, data) {
200
+ if (!this.debug) return;
201
+ const prefix = "[radosgw-admin]";
202
+ if (data) {
203
+ console.debug(prefix, message, JSON.stringify(data, null, 2));
204
+ } else {
205
+ console.debug(prefix, message);
206
+ }
207
+ }
208
+ /**
209
+ * Determines whether an error is retryable (5xx, timeouts, network errors).
210
+ */
211
+ isRetryable(error) {
212
+ if (error instanceof RGWError) {
213
+ if (error.statusCode !== void 0) {
214
+ return error.statusCode >= 500;
215
+ }
216
+ if (error.code === "NetworkError" || error.code === "Timeout") {
217
+ return true;
218
+ }
219
+ }
220
+ if (error instanceof Error) {
221
+ return error.name === "AbortError" || this.hasNetworkErrorPattern(error);
222
+ }
223
+ return false;
224
+ }
225
+ /**
226
+ * Checks an error and its cause chain for network error patterns.
227
+ */
228
+ hasNetworkErrorPattern(error) {
229
+ const patterns = [
230
+ "ECONNRESET",
231
+ "ECONNREFUSED",
232
+ "ETIMEDOUT",
233
+ "ENOTFOUND",
234
+ "EHOSTUNREACH",
235
+ "ENETUNREACH",
236
+ "ECONNABORTED",
237
+ "fetch failed"
238
+ ];
239
+ let current = error;
240
+ while (current) {
241
+ if (patterns.some((p) => current.message.includes(p))) {
242
+ return true;
243
+ }
244
+ current = current.cause instanceof Error ? current.cause : void 0;
245
+ }
246
+ return false;
247
+ }
248
+ /**
249
+ * Returns a promise that resolves after the given number of milliseconds.
250
+ */
251
+ async delay(ms) {
252
+ return new Promise((resolve) => setTimeout(resolve, ms));
253
+ }
254
+ /**
255
+ * Makes a signed HTTP request to the RGW Admin API with retry support.
256
+ *
257
+ * @param options - Request method, path, query params, and optional body
258
+ * @returns Parsed and camelCase-transformed JSON response, or void for empty responses
259
+ */
260
+ async request(options) {
261
+ let lastError;
262
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
263
+ if (attempt > 0) {
264
+ const backoff = this.retryDelay * Math.pow(2, attempt - 1);
265
+ this.log(`retry ${attempt}/${this.maxRetries} after ${backoff}ms`);
266
+ await this.delay(backoff);
267
+ }
268
+ try {
269
+ return await this.executeRequest(options);
270
+ } catch (error) {
271
+ lastError = error instanceof Error ? error : new Error(String(error));
272
+ this.log("error", { error: lastError.message });
273
+ if (attempt < this.maxRetries && this.isRetryable(error)) {
274
+ this.log("retryable error", { error: lastError.message, attempt });
275
+ continue;
276
+ }
277
+ throw error;
278
+ }
279
+ }
280
+ throw lastError ?? new RGWError("Request failed after retries", void 0, "RetryExhausted");
281
+ }
282
+ /**
283
+ * Builds the full request URL with query parameters.
284
+ */
285
+ buildUrl(path, query) {
286
+ const baseUrl = this.port ? `${this.host}:${this.port}` : this.host;
287
+ const fullPath = `${this.adminPath}${path}`;
288
+ const url = new URL(fullPath, baseUrl);
289
+ if (query) {
290
+ for (const [key, value] of Object.entries(query)) {
291
+ if (value !== void 0) {
292
+ url.searchParams.set(toKebabCase(key), String(value));
293
+ }
294
+ }
295
+ }
296
+ url.searchParams.set("format", "json");
297
+ return url;
298
+ }
299
+ /**
300
+ * Temporarily disables TLS certificate verification for insecure mode.
301
+ * Returns the previous value so it can be restored.
302
+ */
303
+ disableTlsVerification() {
304
+ const prev = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
305
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
306
+ return prev;
307
+ }
308
+ /**
309
+ * Restores the TLS verification setting to its previous value.
310
+ */
311
+ restoreTlsVerification(prev) {
312
+ if (prev === void 0) {
313
+ delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
314
+ } else {
315
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = prev;
316
+ }
317
+ }
318
+ /**
319
+ * Parses an error response body to extract the RGW error code.
320
+ */
321
+ parseErrorCode(text) {
322
+ try {
323
+ const errorBody = JSON.parse(text);
324
+ return errorBody.Code ?? void 0;
325
+ } catch {
326
+ return void 0;
327
+ }
328
+ }
329
+ /**
330
+ * Wraps a non-RGW error into the appropriate RGW error type.
331
+ */
332
+ wrapFetchError(error) {
333
+ if (error instanceof RGWError) return error;
334
+ if (error instanceof Error && error.name === "AbortError") {
335
+ return new RGWError(`Request timed out after ${this.timeout}ms`, void 0, "Timeout");
336
+ }
337
+ return new RGWError(
338
+ error instanceof Error ? error.message : "Unknown error occurred",
339
+ void 0,
340
+ "NetworkError"
341
+ );
342
+ }
343
+ /**
344
+ * Executes a single signed HTTP request to the RGW Admin API.
345
+ */
346
+ async executeRequest(options) {
347
+ const { method, path, query, body } = options;
348
+ const url = this.buildUrl(path, query);
349
+ const headers = {
350
+ "Content-Type": "application/json"
351
+ };
352
+ const signedHeaders = signRequest({
353
+ method,
354
+ url,
355
+ headers,
356
+ accessKey: this.accessKey,
357
+ secretKey: this.secretKey,
358
+ region: this.region
359
+ });
360
+ const controller = new AbortController();
361
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
362
+ const prevTls = this.insecure ? this.disableTlsVerification() : void 0;
363
+ try {
364
+ const fetchOptions = {
365
+ method,
366
+ headers: { ...headers, ...signedHeaders },
367
+ signal: controller.signal
368
+ };
369
+ if (body) {
370
+ fetchOptions.body = JSON.stringify(body);
371
+ }
372
+ const safeUrl = url.toString().replaceAll(/([?&]secret-key=)[^&]*/gi, "$1[REDACTED]");
373
+ this.log("request", { method, url: safeUrl });
374
+ const response = await fetch(url.toString(), fetchOptions);
375
+ const text = await response.text();
376
+ if (!response.ok) {
377
+ throw mapHttpError(response.status, text, this.parseErrorCode(text));
378
+ }
379
+ this.log("response", { status: response.status, body: text.slice(0, 500) });
380
+ if (!text) {
381
+ return void 0;
382
+ }
383
+ const parsed = JSON.parse(text);
384
+ return toCamelCase(parsed);
385
+ } catch (error) {
386
+ throw this.wrapFetchError(error);
387
+ } finally {
388
+ clearTimeout(timeoutId);
389
+ if (this.insecure) {
390
+ this.restoreTlsVerification(prevTls);
391
+ }
392
+ }
393
+ }
394
+ };
395
+
396
+ // src/validators.ts
397
+ function validateUid(uid) {
398
+ if (!uid || typeof uid !== "string" || uid.trim() !== uid || uid.trim().length === 0) {
399
+ throw new RGWValidationError(
400
+ "uid is required and must be a non-empty string without leading/trailing whitespace"
401
+ );
402
+ }
403
+ }
404
+ function validateUidNoColon(uid) {
405
+ validateUid(uid);
406
+ if (uid.includes(":")) {
407
+ throw new RGWValidationError(
408
+ 'uid must not contain colons \u2014 colons are reserved for subuser notation (e.g. "uid:subuser")'
409
+ );
410
+ }
411
+ }
412
+
413
+ // src/modules/users.ts
414
+ function validateEmail(email) {
415
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
416
+ throw new RGWValidationError(
417
+ `Invalid email address: "${email}". Expected format: "user@domain.tld"`
418
+ );
419
+ }
420
+ }
421
+ function validateUserCaps(caps) {
422
+ if (!caps.trim()) {
423
+ throw new RGWValidationError("userCaps must not be empty if provided");
424
+ }
425
+ const capSegment = /^[a-z][a-z-]*=(?:\*|read,\s*write|read|write)$/i;
426
+ const valid = caps.split(";").map((s) => s.trim()).every((seg) => capSegment.test(seg));
427
+ if (!valid) {
428
+ throw new RGWValidationError(
429
+ `Invalid userCaps format: "${caps}". Expected "type=perm" or "type1=perm;type2=perm". Valid perms: *, read, write, "read, write". Example: "users=*;buckets=read"`
430
+ );
431
+ }
432
+ }
433
+ var UsersModule = class {
434
+ constructor(client) {
435
+ this.client = client;
436
+ }
437
+ /**
438
+ * Create a new RGW user.
439
+ *
440
+ * @param input - User creation parameters. `uid` and `displayName` are required.
441
+ * @returns The newly created user object with keys, caps, and quotas.
442
+ * @throws {RGWValidationError} If `uid` contains colons, `displayName` is blank,
443
+ * `email` is malformed, or `userCaps` is an invalid capability string.
444
+ * @throws {RGWConflictError} If a user with the given `uid` already exists.
445
+ *
446
+ * @example
447
+ * ```typescript
448
+ * const user = await client.users.create({
449
+ * uid: 'alice',
450
+ * displayName: 'Alice Example',
451
+ * email: 'alice@example.com',
452
+ * maxBuckets: 100,
453
+ * userCaps: 'users=read;buckets=*',
454
+ * });
455
+ * console.log(user.keys[0].accessKey);
456
+ * ```
457
+ */
458
+ async create(input) {
459
+ validateUidNoColon(input.uid);
460
+ if (!input.displayName || typeof input.displayName !== "string" || input.displayName.trim().length === 0) {
461
+ throw new RGWValidationError("displayName is required and must be a non-empty string");
462
+ }
463
+ if (input.displayName !== input.displayName.trim()) {
464
+ throw new RGWValidationError("displayName must not have leading or trailing whitespace");
465
+ }
466
+ if (input.email !== void 0) {
467
+ validateEmail(input.email);
468
+ }
469
+ if (input.userCaps !== void 0) {
470
+ validateUserCaps(input.userCaps);
471
+ }
472
+ return this.client.request({
473
+ method: "PUT",
474
+ path: "/user",
475
+ query: {
476
+ uid: input.uid,
477
+ displayName: input.displayName,
478
+ email: input.email,
479
+ keyType: input.keyType,
480
+ accessKey: input.accessKey,
481
+ secretKey: input.secretKey,
482
+ userCaps: input.userCaps,
483
+ generateKey: input.generateKey,
484
+ maxBuckets: input.maxBuckets,
485
+ suspended: input.suspended,
486
+ tenant: input.tenant,
487
+ opMask: input.opMask
488
+ }
489
+ });
490
+ }
491
+ /**
492
+ * Get full user information including keys, caps, and quotas.
493
+ *
494
+ * For multi-tenant setups pass the `tenant` parameter instead of embedding
495
+ * it in the uid string. Both `get('alice', 'acme')` and `get('acme$alice')`
496
+ * resolve to the same user; the former is preferred for clarity.
497
+ *
498
+ * @param uid - The user ID (without tenant prefix).
499
+ * @param tenant - Optional tenant name. When provided, resolves to `tenant$uid`.
500
+ * @returns Full user object.
501
+ * @throws {RGWValidationError} If `uid` is empty.
502
+ * @throws {RGWNotFoundError} If the user does not exist.
503
+ *
504
+ * @example
505
+ * ```typescript
506
+ * // Standard lookup
507
+ * const user = await client.users.get('alice');
508
+ *
509
+ * // Multi-tenant lookup
510
+ * const tenantUser = await client.users.get('alice', 'acme');
511
+ * ```
512
+ */
513
+ async get(uid, tenant) {
514
+ validateUid(uid);
515
+ const effectiveUid = tenant ? `${tenant}$${uid}` : uid;
516
+ return this.client.request({
517
+ method: "GET",
518
+ path: "/user",
519
+ query: { uid: effectiveUid }
520
+ });
521
+ }
522
+ /**
523
+ * Look up a user by their S3 access key.
524
+ *
525
+ * Useful for mapping an incoming S3 request's access key to the owning user
526
+ * without knowing the uid in advance.
527
+ *
528
+ * @param accessKey - The S3 access key to look up.
529
+ * @returns The user that owns this access key.
530
+ * @throws {RGWValidationError} If `accessKey` is empty.
531
+ * @throws {RGWNotFoundError} If no user owns this access key.
532
+ *
533
+ * @example
534
+ * ```typescript
535
+ * const user = await client.users.getByAccessKey('AKIAIOSFODNN7EXAMPLE');
536
+ * console.log('Key belongs to:', user.userId);
537
+ * ```
538
+ */
539
+ async getByAccessKey(accessKey) {
540
+ if (!accessKey || typeof accessKey !== "string" || accessKey.trim().length === 0) {
541
+ throw new RGWValidationError("accessKey is required and must be a non-empty string");
542
+ }
543
+ return this.client.request({
544
+ method: "GET",
545
+ path: "/user",
546
+ query: { accessKey }
547
+ });
548
+ }
549
+ /**
550
+ * Modify an existing user's properties.
551
+ *
552
+ * Only the provided fields are updated — omitted fields retain their current values.
553
+ *
554
+ * @param input - Properties to update. `uid` is required; all other fields are optional.
555
+ * @returns The updated user object.
556
+ * @throws {RGWValidationError} If `uid` is empty, `email` is malformed,
557
+ * or `userCaps` is an invalid capability string.
558
+ * @throws {RGWNotFoundError} If the user does not exist.
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * const updated = await client.users.modify({
563
+ * uid: 'alice',
564
+ * displayName: 'Alice Updated',
565
+ * maxBuckets: 200,
566
+ * opMask: 'read, write',
567
+ * });
568
+ * ```
569
+ */
570
+ async modify(input) {
571
+ validateUid(input.uid);
572
+ if (input.email !== void 0) {
573
+ validateEmail(input.email);
574
+ }
575
+ if (input.userCaps !== void 0) {
576
+ validateUserCaps(input.userCaps);
577
+ }
578
+ return this.client.request({
579
+ method: "POST",
580
+ path: "/user",
581
+ query: {
582
+ uid: input.uid,
583
+ displayName: input.displayName,
584
+ email: input.email,
585
+ maxBuckets: input.maxBuckets,
586
+ suspended: input.suspended,
587
+ userCaps: input.userCaps,
588
+ opMask: input.opMask
589
+ }
590
+ });
591
+ }
592
+ /**
593
+ * Delete a user. Optionally purge all user data (buckets and objects).
594
+ *
595
+ * @param input - `uid` is required. Set `purgeData: true` to delete all objects.
596
+ * @throws {RGWValidationError} If `uid` is empty.
597
+ * @throws {RGWNotFoundError} If the user does not exist.
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * // Safe delete — fails if user owns buckets
602
+ * await client.users.delete({ uid: 'alice' });
603
+ *
604
+ * // ⚠️ Force delete with full data purge
605
+ * await client.users.delete({ uid: 'alice', purgeData: true });
606
+ * ```
607
+ */
608
+ async delete(input) {
609
+ validateUid(input.uid);
610
+ if (input.purgeData) {
611
+ console.warn(
612
+ `[radosgw-admin] WARNING: purgeData=true will permanently delete all objects for user "${input.uid}"`
613
+ );
614
+ }
615
+ return this.client.request({
616
+ method: "DELETE",
617
+ path: "/user",
618
+ query: {
619
+ uid: input.uid,
620
+ purgeData: input.purgeData
621
+ }
622
+ });
623
+ }
624
+ /**
625
+ * List all user IDs in the cluster (or tenant).
626
+ *
627
+ * Returns the full list in a single call. The RGW `/metadata/user` endpoint
628
+ * supports `marker` and `max-entries` for server-side pagination, but has a
629
+ * default limit of 1000 entries. This method requests up to 100,000 entries
630
+ * to avoid silent truncation on large clusters.
631
+ *
632
+ * For clusters with more than 100k users, use the planned `paginate()` method
633
+ * (see v1.6 roadmap).
634
+ *
635
+ * @returns Array of user ID strings.
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * const uids = await client.users.list();
640
+ * console.log('Total users:', uids.length);
641
+ * ```
642
+ */
643
+ async list() {
644
+ const result = await this.client.request({
645
+ method: "GET",
646
+ path: "/metadata/user",
647
+ query: { maxEntries: 1e5 }
648
+ });
649
+ if (Array.isArray(result)) {
650
+ return result;
651
+ }
652
+ return result.keys;
653
+ }
654
+ /**
655
+ * Suspend a user account. The user's data is preserved but all API access is denied.
656
+ *
657
+ * @param uid - The user ID to suspend.
658
+ * @returns The updated user object with `suspended: 1`.
659
+ * @throws {RGWValidationError} If `uid` is empty.
660
+ * @throws {RGWNotFoundError} If the user does not exist.
661
+ *
662
+ * @example
663
+ * ```typescript
664
+ * const suspended = await client.users.suspend('alice');
665
+ * console.log(suspended.suspended); // 1
666
+ * ```
667
+ */
668
+ async suspend(uid) {
669
+ validateUid(uid);
670
+ return this.client.request({
671
+ method: "POST",
672
+ path: "/user",
673
+ query: { uid, suspended: true }
674
+ });
675
+ }
676
+ /**
677
+ * Re-enable a suspended user account.
678
+ *
679
+ * @param uid - The user ID to enable.
680
+ * @returns The updated user object with `suspended: 0`.
681
+ * @throws {RGWValidationError} If `uid` is empty.
682
+ * @throws {RGWNotFoundError} If the user does not exist.
683
+ *
684
+ * @example
685
+ * ```typescript
686
+ * const enabled = await client.users.enable('alice');
687
+ * console.log(enabled.suspended); // 0
688
+ * ```
689
+ */
690
+ async enable(uid) {
691
+ validateUid(uid);
692
+ return this.client.request({
693
+ method: "POST",
694
+ path: "/user",
695
+ query: { uid, suspended: false }
696
+ });
697
+ }
698
+ /**
699
+ * Get storage usage statistics for a user.
700
+ *
701
+ * Returns the full user object with an additional `stats` field containing:
702
+ * - `size` / `sizeKb` — logical bytes/KB used
703
+ * - `sizeActual` / `sizeKbActual` — bytes/KB on disk (accounting for alignment)
704
+ * - `sizeUtilized` / `sizeKbUtilized` — bytes/KB utilized (used + overhead)
705
+ * - `numObjects` — total number of objects stored
706
+ *
707
+ * Pass `sync: true` to force RGW to recalculate stats from the backing store
708
+ * before returning (slower but accurate).
709
+ *
710
+ * @param uid - The user ID.
711
+ * @param sync - If true, forces a stats sync before returning. Default: false.
712
+ * @returns User object with embedded {@link RGWUserStatData} `stats` field.
713
+ * @throws {RGWValidationError} If `uid` is empty.
714
+ * @throws {RGWNotFoundError} If the user does not exist.
715
+ *
716
+ * @example
717
+ * ```typescript
718
+ * // Fast read (may be slightly stale)
719
+ * const result = await client.users.getStats('alice');
720
+ *
721
+ * // Force sync — accurate but slower
722
+ * const synced = await client.users.getStats('alice', true);
723
+ *
724
+ * console.log('Objects:', result.stats.numObjects);
725
+ * console.log('Size (KB):', result.stats.sizeKb);
726
+ * console.log('Actual disk (KB):', result.stats.sizeKbActual);
727
+ * ```
728
+ */
729
+ async getStats(uid, sync) {
730
+ validateUid(uid);
731
+ return this.client.request({
732
+ method: "GET",
733
+ path: "/user",
734
+ query: {
735
+ uid,
736
+ stats: true,
737
+ sync
738
+ }
739
+ });
740
+ }
741
+ };
742
+
743
+ // src/modules/keys.ts
744
+ var KeysModule = class {
745
+ constructor(client) {
746
+ this.client = client;
747
+ }
748
+ /**
749
+ * Generate a new S3 or Swift key for a user.
750
+ *
751
+ * Returns the user's **entire** key list after the operation, not just the
752
+ * newly created key. To identify the new key, compare with the key list
753
+ * before generation or look for the last entry.
754
+ *
755
+ * @param input - Key generation parameters. `uid` is required.
756
+ * @returns Array of **all** keys belonging to the user after generation.
757
+ * @throws {RGWValidationError} If `uid` is missing or invalid.
758
+ * @throws {RGWNotFoundError} If the user does not exist.
759
+ *
760
+ * @example
761
+ * ```typescript
762
+ * // Auto-generate a new S3 key
763
+ * const allKeys = await client.keys.generate({ uid: 'alice' });
764
+ * const newKey = allKeys[allKeys.length - 1]; // newest key is last
765
+ * console.log('New key:', newKey.accessKey);
766
+ *
767
+ * // Supply specific credentials (disable auto-generation)
768
+ * const allKeys = await client.keys.generate({
769
+ * uid: 'alice',
770
+ * accessKey: 'MY_ACCESS_KEY',
771
+ * secretKey: 'MY_SECRET_KEY',
772
+ * generateKey: false,
773
+ * });
774
+ * ```
775
+ */
776
+ async generate(input) {
777
+ validateUid(input.uid);
778
+ return this.client.request({
779
+ method: "PUT",
780
+ path: "/user",
781
+ query: {
782
+ key: "",
783
+ uid: input.uid,
784
+ keyType: input.keyType,
785
+ accessKey: input.accessKey,
786
+ secretKey: input.secretKey,
787
+ generateKey: input.generateKey
788
+ }
789
+ });
790
+ }
791
+ /**
792
+ * Revoke an S3 or Swift key.
793
+ *
794
+ * @param input - `accessKey` is required. `uid` is required for Swift keys.
795
+ * @returns Resolves when the key has been revoked.
796
+ * @throws {RGWValidationError} If `accessKey` is missing.
797
+ * @throws {RGWNotFoundError} If the key does not exist.
798
+ *
799
+ * @example
800
+ * ```typescript
801
+ * // Revoke an S3 key by access key ID
802
+ * await client.keys.revoke({ accessKey: 'OLDKEY123' });
803
+ *
804
+ * // Revoke a Swift key (uid required)
805
+ * await client.keys.revoke({
806
+ * accessKey: 'SWIFTKEY',
807
+ * uid: 'alice',
808
+ * keyType: 'swift',
809
+ * });
810
+ * ```
811
+ */
812
+ async revoke(input) {
813
+ if (!input.accessKey || typeof input.accessKey !== "string" || input.accessKey.trim().length === 0) {
814
+ throw new RGWValidationError("accessKey is required and must be a non-empty string");
815
+ }
816
+ if (input.keyType === "swift" && !input.uid) {
817
+ throw new RGWValidationError('uid is required when revoking a Swift key (keyType: "swift")');
818
+ }
819
+ return this.client.request({
820
+ method: "DELETE",
821
+ path: "/user",
822
+ query: {
823
+ key: "",
824
+ accessKey: input.accessKey,
825
+ uid: input.uid,
826
+ keyType: input.keyType
827
+ }
828
+ });
829
+ }
830
+ };
831
+
832
+ // src/modules/subusers.ts
833
+ function validateSubuser(subuser) {
834
+ if (!subuser || typeof subuser !== "string" || subuser.trim().length === 0) {
835
+ throw new RGWValidationError("subuser is required and must be a non-empty string");
836
+ }
837
+ if (!subuser.includes(":")) {
838
+ throw new RGWValidationError('subuser must be in "uid:name" format (e.g. "alice:swift")');
839
+ }
840
+ }
841
+ var SubusersModule = class {
842
+ constructor(client) {
843
+ this.client = client;
844
+ }
845
+ /**
846
+ * Create a new subuser for a user.
847
+ *
848
+ * Returns the user's **entire** subuser list after the operation, not just the
849
+ * newly created subuser. To identify the new entry, compare with the list
850
+ * before creation or look for the matching `id`.
851
+ *
852
+ * @param input - Subuser creation parameters. `uid` and `subuser` are required.
853
+ * `subuser` must be in `uid:name` format (e.g. `"alice:swift"`).
854
+ * @returns Array of **all** subusers belonging to the user after creation.
855
+ * @throws {RGWValidationError} If `uid` or `subuser` is missing, invalid, or not in `uid:name` format.
856
+ * @throws {RGWNotFoundError} If the parent user does not exist.
857
+ *
858
+ * @example
859
+ * ```typescript
860
+ * const subusers = await client.subusers.create({
861
+ * uid: 'alice',
862
+ * subuser: 'alice:swift',
863
+ * access: 'readwrite',
864
+ * keyType: 'swift',
865
+ * generateSecret: true,
866
+ * });
867
+ * console.log(subusers[0].id, subusers[0].permissions);
868
+ * ```
869
+ */
870
+ async create(input) {
871
+ validateUid(input.uid);
872
+ validateSubuser(input.subuser);
873
+ return this.client.request({
874
+ method: "PUT",
875
+ path: "/user",
876
+ query: {
877
+ subuser: input.subuser,
878
+ uid: input.uid,
879
+ secretKey: input.secretKey,
880
+ keyType: input.keyType,
881
+ access: input.access,
882
+ generateSecret: input.generateSecret
883
+ }
884
+ });
885
+ }
886
+ /**
887
+ * Modify an existing subuser's properties.
888
+ *
889
+ * @param input - Properties to update. `uid` and `subuser` are required.
890
+ * @returns Array of subusers belonging to the user after modification.
891
+ * @throws {RGWValidationError} If `uid` or `subuser` is missing or invalid.
892
+ * @throws {RGWNotFoundError} If the parent user or subuser does not exist.
893
+ *
894
+ * @example
895
+ * ```typescript
896
+ * const subusers = await client.subusers.modify({
897
+ * uid: 'alice',
898
+ * subuser: 'alice:swift',
899
+ * access: 'full',
900
+ * });
901
+ * ```
902
+ */
903
+ async modify(input) {
904
+ validateUid(input.uid);
905
+ validateSubuser(input.subuser);
906
+ return this.client.request({
907
+ method: "POST",
908
+ path: "/user",
909
+ query: {
910
+ subuser: input.subuser,
911
+ uid: input.uid,
912
+ secretKey: input.secretKey,
913
+ keyType: input.keyType,
914
+ access: input.access,
915
+ generateSecret: input.generateSecret
916
+ }
917
+ });
918
+ }
919
+ /**
920
+ * Remove a subuser from a user. Optionally purge the subuser's keys.
921
+ *
922
+ * @param input - `uid` and `subuser` are required. `purgeKeys` defaults to true.
923
+ * @returns Resolves when the subuser has been removed.
924
+ * @throws {RGWValidationError} If `uid` or `subuser` is missing or invalid.
925
+ * @throws {RGWNotFoundError} If the parent user or subuser does not exist.
926
+ *
927
+ * @example
928
+ * ```typescript
929
+ * // Remove subuser and purge keys
930
+ * await client.subusers.remove({
931
+ * uid: 'alice',
932
+ * subuser: 'alice:swift',
933
+ * });
934
+ *
935
+ * // Remove subuser but keep keys
936
+ * await client.subusers.remove({
937
+ * uid: 'alice',
938
+ * subuser: 'alice:swift',
939
+ * purgeKeys: false,
940
+ * });
941
+ * ```
942
+ */
943
+ async remove(input) {
944
+ validateUid(input.uid);
945
+ validateSubuser(input.subuser);
946
+ if (input.purgeKeys === true) {
947
+ console.warn(
948
+ `[radosgw-admin] WARNING: purgeKeys=true will permanently delete all keys for subuser "${input.subuser}"`
949
+ );
950
+ }
951
+ return this.client.request({
952
+ method: "DELETE",
953
+ path: "/user",
954
+ query: {
955
+ subuser: input.subuser,
956
+ uid: input.uid,
957
+ purgeKeys: input.purgeKeys
958
+ }
959
+ });
960
+ }
961
+ };
962
+
963
+ // src/modules/buckets.ts
964
+ function validateBucket(bucket) {
965
+ if (!bucket || typeof bucket !== "string" || bucket.trim().length === 0) {
966
+ throw new RGWValidationError("bucket is required and must be a non-empty string");
967
+ }
968
+ }
969
+ var BucketsModule = class {
970
+ constructor(client) {
971
+ this.client = client;
972
+ }
973
+ /**
974
+ * List all buckets in the cluster.
975
+ *
976
+ * The RGW `/bucket` endpoint has a default limit of 1000 entries.
977
+ * This method requests up to 100,000 entries to avoid silent truncation
978
+ * on large clusters.
979
+ *
980
+ * For clusters with more than 100k buckets, use the planned `paginate()`
981
+ * method (see v1.6 roadmap).
982
+ *
983
+ * @returns Array of bucket name strings.
984
+ *
985
+ * @example
986
+ * ```typescript
987
+ * const all = await client.buckets.list();
988
+ * console.log(`Cluster has ${all.length} buckets`);
989
+ * ```
990
+ */
991
+ async list() {
992
+ const result = await this.client.request({
993
+ method: "GET",
994
+ path: "/bucket",
995
+ query: { maxEntries: 1e5 }
996
+ });
997
+ if (Array.isArray(result)) {
998
+ return result;
999
+ }
1000
+ return result.buckets;
1001
+ }
1002
+ /**
1003
+ * List buckets owned by a specific user.
1004
+ *
1005
+ * @param uid - User ID to filter buckets by. Required.
1006
+ * @returns Array of bucket name strings owned by the user.
1007
+ * @throws {RGWValidationError} If `uid` is missing or invalid.
1008
+ *
1009
+ * @example
1010
+ * ```typescript
1011
+ * const userBuckets = await client.buckets.listByUser('alice');
1012
+ * console.log(`alice has ${userBuckets.length} buckets`);
1013
+ * ```
1014
+ */
1015
+ async listByUser(uid) {
1016
+ validateUid(uid);
1017
+ return this.client.request({
1018
+ method: "GET",
1019
+ path: "/bucket",
1020
+ query: { uid }
1021
+ });
1022
+ }
1023
+ /**
1024
+ * Get detailed metadata about a bucket.
1025
+ *
1026
+ * @param bucket - The bucket name to inspect.
1027
+ * @returns Full bucket object with owner, usage, quota, and placement info.
1028
+ * @throws {RGWValidationError} If `bucket` is empty.
1029
+ * @throws {RGWNotFoundError} If the bucket does not exist.
1030
+ *
1031
+ * @example
1032
+ * ```typescript
1033
+ * const info = await client.buckets.getInfo('my-bucket');
1034
+ * console.log(`Owner: ${info.owner}`);
1035
+ * console.log(`Objects: ${info.usage.rgwMain.numObjects}`);
1036
+ * console.log(`Size: ${(info.usage.rgwMain.sizeKb / 1024).toFixed(2)} MB`);
1037
+ * ```
1038
+ */
1039
+ async getInfo(bucket) {
1040
+ validateBucket(bucket);
1041
+ return this.client.request({
1042
+ method: "GET",
1043
+ path: "/bucket",
1044
+ query: { bucket }
1045
+ });
1046
+ }
1047
+ /**
1048
+ * Delete a bucket. Optionally purge all objects inside it.
1049
+ *
1050
+ * @param input - `bucket` is required. Set `purgeObjects: true` to delete all objects.
1051
+ * @returns Resolves when the bucket has been deleted.
1052
+ * @throws {RGWValidationError} If `bucket` is empty.
1053
+ * @throws {RGWNotFoundError} If the bucket does not exist.
1054
+ *
1055
+ * @example
1056
+ * ```typescript
1057
+ * // Safe delete (fails if bucket has objects)
1058
+ * await client.buckets.delete({ bucket: 'my-bucket' });
1059
+ *
1060
+ * // Force delete with object purge
1061
+ * await client.buckets.delete({ bucket: 'my-bucket', purgeObjects: true });
1062
+ * ```
1063
+ */
1064
+ async delete(input) {
1065
+ validateBucket(input.bucket);
1066
+ if (input.purgeObjects) {
1067
+ console.warn(
1068
+ `[radosgw-admin] WARNING: purgeObjects=true will permanently delete all objects in bucket "${input.bucket}"`
1069
+ );
1070
+ }
1071
+ return this.client.request({
1072
+ method: "DELETE",
1073
+ path: "/bucket",
1074
+ query: {
1075
+ bucket: input.bucket,
1076
+ purgeObjects: input.purgeObjects
1077
+ }
1078
+ });
1079
+ }
1080
+ /**
1081
+ * Transfer ownership of a bucket to a different user.
1082
+ *
1083
+ * @param input - `bucket`, `bucketId`, and `uid` are all required.
1084
+ * @returns Resolves when ownership has been transferred.
1085
+ * @throws {RGWValidationError} If any required field is missing.
1086
+ * @throws {RGWNotFoundError} If the bucket or user does not exist.
1087
+ *
1088
+ * @example
1089
+ * ```typescript
1090
+ * const info = await client.buckets.getInfo('my-bucket');
1091
+ * await client.buckets.transferOwnership({
1092
+ * bucket: 'my-bucket',
1093
+ * bucketId: info.id,
1094
+ * uid: 'bob',
1095
+ * });
1096
+ * ```
1097
+ */
1098
+ async transferOwnership(input) {
1099
+ validateBucket(input.bucket);
1100
+ validateUid(input.uid);
1101
+ if (!input.bucketId || typeof input.bucketId !== "string" || input.bucketId.trim().length === 0) {
1102
+ throw new RGWValidationError("bucketId is required and must be a non-empty string");
1103
+ }
1104
+ return this.client.request({
1105
+ method: "PUT",
1106
+ path: "/bucket",
1107
+ query: {
1108
+ bucket: input.bucket,
1109
+ bucketId: input.bucketId,
1110
+ uid: input.uid
1111
+ }
1112
+ });
1113
+ }
1114
+ /**
1115
+ * Remove ownership of a bucket from a user without deleting the bucket.
1116
+ *
1117
+ * **Warning:** This leaves the bucket in an orphaned state with no owner.
1118
+ * The bucket and its objects remain intact but cannot be managed via the
1119
+ * S3 API until ownership is reassigned with {@link transferOwnership}.
1120
+ *
1121
+ * @param input - `bucket` and `uid` are required.
1122
+ * @returns Resolves when ownership has been removed.
1123
+ * @throws {RGWValidationError} If any required field is missing.
1124
+ * @throws {RGWNotFoundError} If the bucket or user does not exist.
1125
+ *
1126
+ * @example
1127
+ * ```typescript
1128
+ * await client.buckets.removeOwnership({
1129
+ * bucket: 'my-bucket',
1130
+ * uid: 'alice',
1131
+ * });
1132
+ * ```
1133
+ */
1134
+ async removeOwnership(input) {
1135
+ validateBucket(input.bucket);
1136
+ validateUid(input.uid);
1137
+ return this.client.request({
1138
+ method: "POST",
1139
+ path: "/bucket",
1140
+ query: {
1141
+ bucket: input.bucket,
1142
+ uid: input.uid
1143
+ }
1144
+ });
1145
+ }
1146
+ /**
1147
+ * Verify and optionally repair a bucket's index.
1148
+ *
1149
+ * With `fix: false` (default), this is a safe read-only operation that reports
1150
+ * any inconsistencies. Set `fix: true` to actually repair detected issues.
1151
+ *
1152
+ * @param input - `bucket` is required. `checkObjects` and `fix` are optional.
1153
+ * @returns Index check result with invalid entries and header comparison.
1154
+ * @throws {RGWValidationError} If `bucket` is empty.
1155
+ * @throws {RGWNotFoundError} If the bucket does not exist.
1156
+ *
1157
+ * @example
1158
+ * ```typescript
1159
+ * // Dry run — check only
1160
+ * const result = await client.buckets.verifyIndex({
1161
+ * bucket: 'my-bucket',
1162
+ * checkObjects: true,
1163
+ * fix: false,
1164
+ * });
1165
+ * console.log('Invalid entries:', result.invalidMultipartEntries);
1166
+ *
1167
+ * // Fix detected issues
1168
+ * await client.buckets.verifyIndex({
1169
+ * bucket: 'my-bucket',
1170
+ * fix: true,
1171
+ * });
1172
+ * ```
1173
+ */
1174
+ async verifyIndex(input) {
1175
+ validateBucket(input.bucket);
1176
+ if (input.fix === true) {
1177
+ console.warn(
1178
+ `[radosgw-admin] WARNING: fix=true will repair the bucket index for "${input.bucket}" \u2014 this mutates index data`
1179
+ );
1180
+ }
1181
+ return this.client.request({
1182
+ method: "GET",
1183
+ path: "/bucket",
1184
+ query: {
1185
+ index: "",
1186
+ bucket: input.bucket,
1187
+ checkObjects: input.checkObjects,
1188
+ fix: input.fix
1189
+ }
1190
+ });
1191
+ }
1192
+ };
1193
+
1194
+ // src/modules/quota.ts
1195
+ function validateQuotaValue(field, value) {
1196
+ if (value !== void 0 && typeof value === "number" && value < -1) {
1197
+ throw new RGWValidationError(`${field} must be -1 (unlimited) or >= 0, got ${value}`);
1198
+ }
1199
+ }
1200
+ function parseSizeString(size) {
1201
+ if (typeof size === "number") return size;
1202
+ const units = {
1203
+ K: 1024,
1204
+ M: 1024 ** 2,
1205
+ G: 1024 ** 3,
1206
+ T: 1024 ** 4
1207
+ };
1208
+ const match = size.toUpperCase().trim().match(/^(\d+(?:\.\d+)?)\s*([KMGT]?)B?$/);
1209
+ if (!match) {
1210
+ throw new RGWValidationError(
1211
+ `Invalid size string: "${size}". Use format like "10G", "500M", "1T", or a number in bytes`
1212
+ );
1213
+ }
1214
+ const value = parseFloat(match[1]);
1215
+ const unit = match[2];
1216
+ return Math.floor(value * (units[unit] ?? 1));
1217
+ }
1218
+ var QuotaModule = class {
1219
+ constructor(client) {
1220
+ this.client = client;
1221
+ }
1222
+ /**
1223
+ * Get the user-level quota for a user.
1224
+ *
1225
+ * @param uid - The user ID.
1226
+ * @returns The user's quota configuration.
1227
+ * @throws {RGWValidationError} If `uid` is empty.
1228
+ * @throws {RGWNotFoundError} If the user does not exist.
1229
+ *
1230
+ * @example
1231
+ * ```typescript
1232
+ * const quota = await client.quota.getUserQuota('alice');
1233
+ * console.log('Enabled:', quota.enabled);
1234
+ * console.log('Max size:', quota.maxSize, 'bytes');
1235
+ * console.log('Max objects:', quota.maxObjects);
1236
+ * ```
1237
+ */
1238
+ async getUserQuota(uid) {
1239
+ validateUid(uid);
1240
+ return this.client.request({
1241
+ method: "GET",
1242
+ path: "/user",
1243
+ query: { uid, quota: "", quotaType: "user" }
1244
+ });
1245
+ }
1246
+ /**
1247
+ * Set the user-level quota for a user.
1248
+ *
1249
+ * The `maxSize` field accepts either a number (bytes) or a human-readable string
1250
+ * like `"10G"`, `"500M"`, `"1T"`.
1251
+ *
1252
+ * @param input - Quota settings. `uid` is required.
1253
+ * @throws {RGWValidationError} If `uid` is empty, `maxSize` format is invalid, or `maxSize`/`maxObjects` is a negative number other than -1.
1254
+ * @throws {RGWNotFoundError} If the user does not exist.
1255
+ *
1256
+ * @example
1257
+ * ```typescript
1258
+ * await client.quota.setUserQuota({
1259
+ * uid: 'alice',
1260
+ * maxSize: '10G',
1261
+ * maxObjects: 50000,
1262
+ * enabled: true,
1263
+ * });
1264
+ * ```
1265
+ */
1266
+ async setUserQuota(input) {
1267
+ validateUid(input.uid);
1268
+ validateQuotaValue("maxObjects", input.maxObjects);
1269
+ const maxSize = input.maxSize !== void 0 ? parseSizeString(input.maxSize) : void 0;
1270
+ validateQuotaValue("maxSize", maxSize);
1271
+ return this.client.request({
1272
+ method: "PUT",
1273
+ path: "/user",
1274
+ query: {
1275
+ uid: input.uid,
1276
+ quota: "",
1277
+ quotaType: "user",
1278
+ maxSize,
1279
+ maxObjects: input.maxObjects,
1280
+ enabled: input.enabled ?? true
1281
+ }
1282
+ });
1283
+ }
1284
+ /**
1285
+ * Enable the user-level quota for a user without changing quota values.
1286
+ *
1287
+ * @param uid - The user ID.
1288
+ * @throws {RGWValidationError} If `uid` is empty.
1289
+ * @throws {RGWNotFoundError} If the user does not exist.
1290
+ *
1291
+ * @example
1292
+ * ```typescript
1293
+ * await client.quota.enableUserQuota('alice');
1294
+ * ```
1295
+ */
1296
+ async enableUserQuota(uid) {
1297
+ validateUid(uid);
1298
+ return this.client.request({
1299
+ method: "PUT",
1300
+ path: "/user",
1301
+ query: { uid, quota: "", quotaType: "user", enabled: true }
1302
+ });
1303
+ }
1304
+ /**
1305
+ * Disable the user-level quota for a user without changing quota values.
1306
+ *
1307
+ * @param uid - The user ID.
1308
+ * @throws {RGWValidationError} If `uid` is empty.
1309
+ * @throws {RGWNotFoundError} If the user does not exist.
1310
+ *
1311
+ * @example
1312
+ * ```typescript
1313
+ * await client.quota.disableUserQuota('alice');
1314
+ * ```
1315
+ */
1316
+ async disableUserQuota(uid) {
1317
+ validateUid(uid);
1318
+ return this.client.request({
1319
+ method: "PUT",
1320
+ path: "/user",
1321
+ query: { uid, quota: "", quotaType: "user", enabled: false }
1322
+ });
1323
+ }
1324
+ /**
1325
+ * Get the bucket-level quota for a user.
1326
+ *
1327
+ * This returns the per-bucket quota applied to all buckets owned by the user,
1328
+ * not a quota for a specific bucket. RGW bucket quotas are configured per-user.
1329
+ *
1330
+ * @param uid - The user ID (bucket owner), not a bucket name.
1331
+ * @throws {RGWValidationError} If `uid` is empty.
1332
+ * @throws {RGWNotFoundError} If the user does not exist.
1333
+ *
1334
+ * @example
1335
+ * ```typescript
1336
+ * const quota = await client.quota.getBucketQuota('alice');
1337
+ * console.log('Bucket quota enabled:', quota.enabled);
1338
+ * ```
1339
+ */
1340
+ async getBucketQuota(uid) {
1341
+ validateUid(uid);
1342
+ return this.client.request({
1343
+ method: "GET",
1344
+ path: "/user",
1345
+ query: { uid, quota: "", quotaType: "bucket" }
1346
+ });
1347
+ }
1348
+ /**
1349
+ * Set the bucket-level quota for a user's buckets.
1350
+ *
1351
+ * The `maxSize` field accepts either a number (bytes) or a human-readable string
1352
+ * like `"1G"`, `"500M"`.
1353
+ *
1354
+ * @param input - Quota settings. `uid` is required.
1355
+ * @throws {RGWValidationError} If `uid` is empty, `maxSize` format is invalid, or `maxSize`/`maxObjects` is a negative number other than -1.
1356
+ * @throws {RGWNotFoundError} If the user does not exist.
1357
+ *
1358
+ * @example
1359
+ * ```typescript
1360
+ * await client.quota.setBucketQuota({
1361
+ * uid: 'alice',
1362
+ * maxSize: '1G',
1363
+ * maxObjects: 10000,
1364
+ * enabled: true,
1365
+ * });
1366
+ * ```
1367
+ */
1368
+ async setBucketQuota(input) {
1369
+ validateUid(input.uid);
1370
+ validateQuotaValue("maxObjects", input.maxObjects);
1371
+ const maxSize = input.maxSize !== void 0 ? parseSizeString(input.maxSize) : void 0;
1372
+ validateQuotaValue("maxSize", maxSize);
1373
+ return this.client.request({
1374
+ method: "PUT",
1375
+ path: "/user",
1376
+ query: {
1377
+ uid: input.uid,
1378
+ quota: "",
1379
+ quotaType: "bucket",
1380
+ maxSize,
1381
+ maxObjects: input.maxObjects,
1382
+ enabled: input.enabled ?? true
1383
+ }
1384
+ });
1385
+ }
1386
+ /**
1387
+ * Enable the bucket-level quota for a user without changing quota values.
1388
+ *
1389
+ * @param uid - The user ID.
1390
+ * @throws {RGWValidationError} If `uid` is empty.
1391
+ * @throws {RGWNotFoundError} If the user does not exist.
1392
+ *
1393
+ * @example
1394
+ * ```typescript
1395
+ * await client.quota.enableBucketQuota('alice');
1396
+ * ```
1397
+ */
1398
+ async enableBucketQuota(uid) {
1399
+ validateUid(uid);
1400
+ return this.client.request({
1401
+ method: "PUT",
1402
+ path: "/user",
1403
+ query: { uid, quota: "", quotaType: "bucket", enabled: true }
1404
+ });
1405
+ }
1406
+ /**
1407
+ * Disable the bucket-level quota for a user without changing quota values.
1408
+ *
1409
+ * @param uid - The user ID.
1410
+ * @throws {RGWValidationError} If `uid` is empty.
1411
+ * @throws {RGWNotFoundError} If the user does not exist.
1412
+ *
1413
+ * @example
1414
+ * ```typescript
1415
+ * await client.quota.disableBucketQuota('alice');
1416
+ * ```
1417
+ */
1418
+ async disableBucketQuota(uid) {
1419
+ validateUid(uid);
1420
+ return this.client.request({
1421
+ method: "PUT",
1422
+ path: "/user",
1423
+ query: { uid, quota: "", quotaType: "bucket", enabled: false }
1424
+ });
1425
+ }
1426
+ };
1427
+
1428
+ // src/modules/ratelimit.ts
1429
+ function validateBucket2(bucket) {
1430
+ if (!bucket || typeof bucket !== "string" || bucket.trim().length === 0) {
1431
+ throw new RGWValidationError("bucket is required and must be a non-empty string");
1432
+ }
1433
+ }
1434
+ function validateRateLimitValue(field, value) {
1435
+ if (value !== void 0 && typeof value === "number" && value < 0) {
1436
+ throw new RGWValidationError(`${field} must be >= 0 (0 = unlimited), got ${value}`);
1437
+ }
1438
+ }
1439
+ function validateRateLimitFields(input) {
1440
+ validateRateLimitValue("maxReadOps", input.maxReadOps);
1441
+ validateRateLimitValue("maxWriteOps", input.maxWriteOps);
1442
+ validateRateLimitValue("maxReadBytes", input.maxReadBytes);
1443
+ validateRateLimitValue("maxWriteBytes", input.maxWriteBytes);
1444
+ }
1445
+ var RateLimitModule = class {
1446
+ constructor(client) {
1447
+ this.client = client;
1448
+ }
1449
+ /**
1450
+ * Get the rate limit configuration for a user.
1451
+ *
1452
+ * @param uid - The user ID.
1453
+ * @returns The user's rate limit configuration.
1454
+ * @throws {RGWValidationError} If `uid` is empty.
1455
+ * @throws {RGWNotFoundError} If the user does not exist.
1456
+ *
1457
+ * @example
1458
+ * ```typescript
1459
+ * const limit = await client.rateLimit.getUserLimit('alice');
1460
+ * console.log('Read ops/min:', limit.maxReadOps);
1461
+ * console.log('Write ops/min:', limit.maxWriteOps);
1462
+ * ```
1463
+ */
1464
+ async getUserLimit(uid) {
1465
+ validateUid(uid);
1466
+ return this.client.request({
1467
+ method: "GET",
1468
+ path: "/ratelimit",
1469
+ query: { ratelimit: "", uid, scope: "user" }
1470
+ });
1471
+ }
1472
+ /**
1473
+ * Set the rate limit for a user.
1474
+ *
1475
+ * Values are per RGW instance. Divide by the number of RGW daemons for cluster-wide limits.
1476
+ *
1477
+ * @param input - Rate limit settings. `uid` is required. `enabled` defaults to `true`.
1478
+ * @throws {RGWValidationError} If `uid` is empty or any rate limit value is negative.
1479
+ * @throws {RGWNotFoundError} If the user does not exist.
1480
+ *
1481
+ * @example
1482
+ * ```typescript
1483
+ * await client.rateLimit.setUserLimit({
1484
+ * uid: 'alice',
1485
+ * maxReadOps: 100,
1486
+ * maxWriteOps: 50,
1487
+ * maxWriteBytes: 52428800, // 50MB/min
1488
+ * enabled: true,
1489
+ * });
1490
+ * ```
1491
+ */
1492
+ async setUserLimit(input) {
1493
+ validateUid(input.uid);
1494
+ validateRateLimitFields(input);
1495
+ return this.client.request({
1496
+ method: "POST",
1497
+ path: "/ratelimit",
1498
+ query: {
1499
+ ratelimit: "",
1500
+ uid: input.uid,
1501
+ scope: "user",
1502
+ maxReadOps: input.maxReadOps,
1503
+ maxWriteOps: input.maxWriteOps,
1504
+ maxReadBytes: input.maxReadBytes,
1505
+ maxWriteBytes: input.maxWriteBytes,
1506
+ enabled: input.enabled ?? true
1507
+ }
1508
+ });
1509
+ }
1510
+ /**
1511
+ * Disable the rate limit for a user without changing the configured values.
1512
+ *
1513
+ * @param uid - The user ID.
1514
+ * @throws {RGWValidationError} If `uid` is empty.
1515
+ * @throws {RGWNotFoundError} If the user does not exist.
1516
+ *
1517
+ * @example
1518
+ * ```typescript
1519
+ * await client.rateLimit.disableUserLimit('alice');
1520
+ * ```
1521
+ */
1522
+ async disableUserLimit(uid) {
1523
+ validateUid(uid);
1524
+ return this.client.request({
1525
+ method: "POST",
1526
+ path: "/ratelimit",
1527
+ query: { ratelimit: "", uid, scope: "user", enabled: false }
1528
+ });
1529
+ }
1530
+ /**
1531
+ * Get the rate limit configuration for a bucket.
1532
+ *
1533
+ * @param bucket - The bucket name.
1534
+ * @returns The bucket's rate limit configuration.
1535
+ * @throws {RGWValidationError} If `bucket` is empty.
1536
+ * @throws {RGWNotFoundError} If the bucket does not exist.
1537
+ *
1538
+ * @example
1539
+ * ```typescript
1540
+ * const limit = await client.rateLimit.getBucketLimit('my-bucket');
1541
+ * console.log('Read ops/min:', limit.maxReadOps);
1542
+ * ```
1543
+ */
1544
+ async getBucketLimit(bucket) {
1545
+ validateBucket2(bucket);
1546
+ return this.client.request({
1547
+ method: "GET",
1548
+ path: "/ratelimit",
1549
+ query: { ratelimit: "", bucket, scope: "bucket" }
1550
+ });
1551
+ }
1552
+ /**
1553
+ * Set the rate limit for a bucket.
1554
+ *
1555
+ * Values are per RGW instance. Divide by the number of RGW daemons for cluster-wide limits.
1556
+ *
1557
+ * @param input - Rate limit settings. `bucket` is required. `enabled` defaults to `true`.
1558
+ * @throws {RGWValidationError} If `bucket` is empty or any rate limit value is negative.
1559
+ * @throws {RGWNotFoundError} If the bucket does not exist.
1560
+ *
1561
+ * @example
1562
+ * ```typescript
1563
+ * await client.rateLimit.setBucketLimit({
1564
+ * bucket: 'my-bucket',
1565
+ * maxReadOps: 200,
1566
+ * maxWriteOps: 100,
1567
+ * enabled: true,
1568
+ * });
1569
+ * ```
1570
+ */
1571
+ async setBucketLimit(input) {
1572
+ validateBucket2(input.bucket);
1573
+ validateRateLimitFields(input);
1574
+ return this.client.request({
1575
+ method: "POST",
1576
+ path: "/ratelimit",
1577
+ query: {
1578
+ ratelimit: "",
1579
+ bucket: input.bucket,
1580
+ scope: "bucket",
1581
+ maxReadOps: input.maxReadOps,
1582
+ maxWriteOps: input.maxWriteOps,
1583
+ maxReadBytes: input.maxReadBytes,
1584
+ maxWriteBytes: input.maxWriteBytes,
1585
+ enabled: input.enabled ?? true
1586
+ }
1587
+ });
1588
+ }
1589
+ /**
1590
+ * Disable the rate limit for a bucket without changing the configured values.
1591
+ *
1592
+ * @param bucket - The bucket name.
1593
+ * @throws {RGWValidationError} If `bucket` is empty.
1594
+ * @throws {RGWNotFoundError} If the bucket does not exist.
1595
+ *
1596
+ * @example
1597
+ * ```typescript
1598
+ * await client.rateLimit.disableBucketLimit('my-bucket');
1599
+ * ```
1600
+ */
1601
+ async disableBucketLimit(bucket) {
1602
+ validateBucket2(bucket);
1603
+ return this.client.request({
1604
+ method: "POST",
1605
+ path: "/ratelimit",
1606
+ query: { ratelimit: "", bucket, scope: "bucket", enabled: false }
1607
+ });
1608
+ }
1609
+ /**
1610
+ * Get the global rate limit configuration for all scopes (user, bucket, anonymous).
1611
+ *
1612
+ * @returns Global rate limits for user, bucket, and anonymous scopes.
1613
+ *
1614
+ * @example
1615
+ * ```typescript
1616
+ * const global = await client.rateLimit.getGlobal();
1617
+ * console.log('Anonymous read limit:', global.anonymous.maxReadOps);
1618
+ * ```
1619
+ */
1620
+ async getGlobal() {
1621
+ return this.client.request({
1622
+ method: "GET",
1623
+ path: "/ratelimit",
1624
+ query: { ratelimit: "", global: "" }
1625
+ });
1626
+ }
1627
+ /**
1628
+ * Set a global rate limit for a specific scope.
1629
+ *
1630
+ * Use `scope: 'anonymous'` to protect public-read buckets from abuse.
1631
+ *
1632
+ * @param input - Rate limit settings. `scope` is required. `enabled` defaults to `true`.
1633
+ * @throws {RGWValidationError} If `scope` is invalid or any rate limit value is negative.
1634
+ *
1635
+ * @example
1636
+ * ```typescript
1637
+ * // Limit anonymous access globally
1638
+ * await client.rateLimit.setGlobal({
1639
+ * scope: 'anonymous',
1640
+ * maxReadOps: 50,
1641
+ * maxWriteOps: 0,
1642
+ * enabled: true,
1643
+ * });
1644
+ * ```
1645
+ */
1646
+ async setGlobal(input) {
1647
+ const validScopes = ["user", "bucket", "anonymous"];
1648
+ if (!validScopes.includes(input.scope)) {
1649
+ throw new RGWValidationError(
1650
+ `scope must be one of: ${validScopes.join(", ")}. Got: "${input.scope}"`
1651
+ );
1652
+ }
1653
+ validateRateLimitFields(input);
1654
+ return this.client.request({
1655
+ method: "POST",
1656
+ path: "/ratelimit",
1657
+ query: {
1658
+ ratelimit: "",
1659
+ global: "",
1660
+ scope: input.scope,
1661
+ maxReadOps: input.maxReadOps,
1662
+ maxWriteOps: input.maxWriteOps,
1663
+ maxReadBytes: input.maxReadBytes,
1664
+ maxWriteBytes: input.maxWriteBytes,
1665
+ enabled: input.enabled ?? true
1666
+ }
1667
+ });
1668
+ }
1669
+ };
1670
+
1671
+ // src/modules/usage.ts
1672
+ function normalizeDate(value) {
1673
+ if (value instanceof Date) {
1674
+ if (isNaN(value.getTime())) {
1675
+ throw new RGWValidationError("Invalid Date object provided for date range filter");
1676
+ }
1677
+ return value.toISOString().slice(0, 10);
1678
+ }
1679
+ if (!/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2})?$/.test(value.trim())) {
1680
+ throw new RGWValidationError(
1681
+ `Invalid date string: "${value}". Use "YYYY-MM-DD" or "YYYY-MM-DD HH:MM:SS"`
1682
+ );
1683
+ }
1684
+ return value.trim();
1685
+ }
1686
+ function warnIfInvertedRange(start, end) {
1687
+ if (start !== void 0 && end !== void 0 && start > end) {
1688
+ console.warn(
1689
+ `[radosgw-admin] WARNING: start "${start}" is after end "${end}" \u2014 RGW will return empty results`
1690
+ );
1691
+ }
1692
+ }
1693
+ var UsageModule = class {
1694
+ constructor(client) {
1695
+ this.client = client;
1696
+ }
1697
+ /**
1698
+ * Retrieve a usage report for a user or the entire cluster.
1699
+ *
1700
+ * Omit `uid` to get cluster-wide usage across all users.
1701
+ * Omit `start`/`end` to get all available usage data.
1702
+ *
1703
+ * > **Note:** Usage logging must be enabled in `ceph.conf`:
1704
+ * > `rgw enable usage log = true`
1705
+ *
1706
+ * @param input - Optional filters: `uid`, `start`, `end`, `showEntries`, `showSummary`.
1707
+ * @returns A usage report with `entries` (per-bucket detail) and `summary` (per-user totals).
1708
+ * @throws {RGWValidationError} If `uid` is provided but empty/whitespace.
1709
+ * @throws {RGWValidationError} If a date string is not in a recognised format.
1710
+ *
1711
+ * @example
1712
+ * ```typescript
1713
+ * // Cluster-wide usage, all time
1714
+ * const all = await client.usage.get();
1715
+ *
1716
+ * // Single user, date range
1717
+ * const report = await client.usage.get({
1718
+ * uid: 'alice',
1719
+ * start: '2025-01-01',
1720
+ * end: '2025-01-31',
1721
+ * });
1722
+ *
1723
+ * for (const s of report.summary) {
1724
+ * console.log(s.user, 'sent', s.total.bytesSent, 'bytes');
1725
+ * }
1726
+ * ```
1727
+ */
1728
+ async get(input = {}) {
1729
+ if (input.uid !== void 0) {
1730
+ validateUid(input.uid);
1731
+ }
1732
+ const start = input.start !== void 0 ? normalizeDate(input.start) : void 0;
1733
+ const end = input.end !== void 0 ? normalizeDate(input.end) : void 0;
1734
+ warnIfInvertedRange(start, end);
1735
+ return this.client.request({
1736
+ method: "GET",
1737
+ path: "/usage",
1738
+ query: {
1739
+ uid: input.uid,
1740
+ start,
1741
+ end,
1742
+ showEntries: input.showEntries,
1743
+ showSummary: input.showSummary
1744
+ }
1745
+ });
1746
+ }
1747
+ /**
1748
+ * Delete (trim) usage log entries matching the given filters.
1749
+ *
1750
+ * **Destructive operation** — deleted log entries cannot be recovered.
1751
+ *
1752
+ * When no `uid` is provided the trim applies cluster-wide. In that case
1753
+ * `removeAll: true` is required to prevent accidental full-cluster log wipes.
1754
+ *
1755
+ * @param input - Trim filters. Omit entirely to trim nothing (use `removeAll: true` explicitly).
1756
+ * @throws {RGWValidationError} If `uid` is provided but empty/whitespace.
1757
+ * @throws {RGWValidationError} If `uid` is omitted but `removeAll` is not `true`.
1758
+ * @throws {RGWValidationError} If a date string is not in a recognised format.
1759
+ *
1760
+ * @example
1761
+ * ```typescript
1762
+ * // Trim a specific user's logs up to end of 2024
1763
+ * await client.usage.trim({ uid: 'alice', end: '2024-12-31' });
1764
+ *
1765
+ * // ⚠️ Trim all logs before 2024 across the entire cluster
1766
+ * await client.usage.trim({ end: '2023-12-31', removeAll: true });
1767
+ * ```
1768
+ */
1769
+ async trim(input = {}) {
1770
+ if (input.uid !== void 0) {
1771
+ validateUid(input.uid);
1772
+ }
1773
+ if (!input.uid && input.removeAll !== true) {
1774
+ throw new RGWValidationError(
1775
+ "trim() without a uid removes logs for ALL users. Set removeAll: true to confirm."
1776
+ );
1777
+ }
1778
+ if (input.removeAll) {
1779
+ console.warn(
1780
+ "[radosgw-admin] WARNING: usage.trim() with removeAll=true will permanently delete usage logs" + (input.uid ? ` for user "${input.uid}"` : " for ALL users")
1781
+ );
1782
+ }
1783
+ const start = input.start !== void 0 ? normalizeDate(input.start) : void 0;
1784
+ const end = input.end !== void 0 ? normalizeDate(input.end) : void 0;
1785
+ warnIfInvertedRange(start, end);
1786
+ return this.client.request({
1787
+ method: "DELETE",
1788
+ path: "/usage",
1789
+ query: {
1790
+ uid: input.uid,
1791
+ start,
1792
+ end,
1793
+ removeAll: input.removeAll
1794
+ }
1795
+ });
1796
+ }
1797
+ };
1798
+
1799
+ // src/modules/info.ts
1800
+ var InfoModule = class {
1801
+ constructor(client) {
1802
+ this.client = client;
1803
+ }
1804
+ /**
1805
+ * Get basic cluster information including the Ceph cluster FSID.
1806
+ *
1807
+ * Useful for confirming connectivity and identifying which cluster the
1808
+ * client is connected to when managing multiple environments.
1809
+ *
1810
+ * @returns Cluster info containing the cluster ID (FSID).
1811
+ * @throws {RGWAuthError} If the credentials are invalid or lack permission.
1812
+ *
1813
+ * @example
1814
+ * ```typescript
1815
+ * const info = await client.info.get();
1816
+ * console.log('Cluster FSID:', info.info.storageBackends[0].clusterId);
1817
+ * ```
1818
+ */
1819
+ async get() {
1820
+ return this.client.request({
1821
+ method: "GET",
1822
+ path: "/info",
1823
+ query: {}
1824
+ });
1825
+ }
1826
+ };
1827
+
1828
+ // src/index.ts
1829
+ var RadosGWAdminClient = class {
1830
+ /** @internal */
1831
+ _client;
1832
+ /** User management operations. */
1833
+ users;
1834
+ /** S3/Swift key management operations. */
1835
+ keys;
1836
+ /** Subuser management operations. */
1837
+ subusers;
1838
+ /** Bucket management operations. */
1839
+ buckets;
1840
+ /** Quota management operations (user-level and bucket-level). */
1841
+ quota;
1842
+ /** Rate limit management operations (user, bucket, and global). */
1843
+ rateLimit;
1844
+ /** Usage & analytics operations — query and trim RGW usage logs. */
1845
+ usage;
1846
+ /** Cluster info operations. */
1847
+ info;
1848
+ constructor(config) {
1849
+ this._client = new BaseClient(config);
1850
+ this.users = new UsersModule(this._client);
1851
+ this.keys = new KeysModule(this._client);
1852
+ this.subusers = new SubusersModule(this._client);
1853
+ this.buckets = new BucketsModule(this._client);
1854
+ this.quota = new QuotaModule(this._client);
1855
+ this.rateLimit = new RateLimitModule(this._client);
1856
+ this.usage = new UsageModule(this._client);
1857
+ this.info = new InfoModule(this._client);
1858
+ }
1859
+ };
1860
+ export {
1861
+ BaseClient,
1862
+ RGWAuthError,
1863
+ RGWConflictError,
1864
+ RGWError,
1865
+ RGWNotFoundError,
1866
+ RGWValidationError,
1867
+ RadosGWAdminClient,
1868
+ signRequest
1869
+ };